From 46dbff9b3b33b624a7a61837911401234d6d1070 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 18:39:43 +0200 Subject: [PATCH 01/31] cascade transforms by composing canvas -ctx actions (ctx.translate, ctx.rotate) instead of updating DOMMatrix. Note: this does not take into account skewing. further implementation will use `ctx.setTransform` and manually calculating the values instead. --- src/components/Object2D/Group.tsx | 20 +++++++++--- src/utils/createPath2D.ts | 52 ++++++++++++++++++------------- src/utils/createShape2D.ts | 11 ++++++- src/utils/createUpdatedContext.ts | 5 +-- src/utils/renderPath.ts | 8 ++--- 5 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/components/Object2D/Group.tsx b/src/components/Object2D/Group.tsx index fd0c8be..95c8a2b 100644 --- a/src/components/Object2D/Group.tsx +++ b/src/components/Object2D/Group.tsx @@ -1,11 +1,11 @@ import { createToken } from '@solid-primitives/jsx-tokenizer' import { Accessor, mergeProps, splitProps } from 'solid-js' import { JSX } from 'solid-js/jsx-runtime' +import { useInternalContext } from 'src/context/InternalContext' import { RegisterControllerEvents } from 'src/controllers/controllers' import { CanvasToken, parser } from 'src/parser' import { Color, Object2DProps, ResolvedShape2DProps, Vector } from 'src/types' import { createParenthood } from 'src/utils/createParenthood' -import { createUpdatedContext } from 'src/utils/createUpdatedContext' import { SingleOrArray } from 'src/utils/typehelpers' import { T } from 'vitest/dist/types-c800444e' @@ -37,12 +37,24 @@ const Group = createToken(parser, (props: Object2DProps) => { // until then we will only allow controllers for types extending `Shape2DProps` // const controlled = createControlledProps(mergedProps) - const context = createUpdatedContext(() => mergedProps) - const parenthood = createParenthood(props, context) + const context = useInternalContext() + const parenthood = createParenthood(props, context!) return { type: 'Object2D', id: 'Group', - render: ctx => parenthood.render(ctx), + render: ctx => { + ctx.translate( + props.transform?.position?.x ?? 0, + props.transform?.position?.y ?? 0, + ) + ctx.rotate(props.transform?.rotation ?? 0) + parenthood.render(ctx) + ctx.translate( + (props.transform?.position?.x ?? 0) * -1, + (props.transform?.position?.y ?? 0) * -1, + ) + ctx.rotate((props.transform?.rotation ?? 0) * -1) + }, debug: () => {}, hitTest: event => { parenthood.hitTest(event) diff --git a/src/utils/createPath2D.ts b/src/utils/createPath2D.ts index 8e62b7a..49f4527 100644 --- a/src/utils/createPath2D.ts +++ b/src/utils/createPath2D.ts @@ -15,6 +15,7 @@ import { deepMergeGetters } from './mergeGetters' import { DeepRequired, RequireOptionals, SingleOrArray } from './typehelpers' import { Hover } from 'src/controllers/Hover' import { isPointInShape2D } from './isPointInShape2D' +import { useInternalContext } from 'src/context/InternalContext' const createPath2D = (arg: { id: string @@ -36,15 +37,15 @@ const createPath2D = (arg: { }) const controlled = createControlledProps(props, [ - /* Hover({ + /* Hover({ style: props.style?.['&:hover'], transform: props.transform?.['&:hover'], }), */ ]) - const context = createUpdatedContext(() => controlled.props) + const context = useInternalContext() - const parenthood = createParenthood(arg.props, context) + const parenthood = createParenthood(arg.props, context!) const path = createMemo(() => arg.path(controlled.props)) @@ -57,23 +58,39 @@ const createPath2D = (arg: { id: arg.id, path, render: ctx => { - renderPath(context, controlled.props, path()) + ctx.translate( + controlled.props.transform?.position?.x ?? 0, + controlled.props.transform?.position?.y ?? 0, + ) + ctx.rotate(controlled.props.transform?.rotation ?? 0) + + renderPath(context!, controlled.props, path()) parenthood.render(ctx) + + ctx.translate( + (controlled.props.transform?.position?.x ?? 0) * -1, + (controlled.props.transform?.position?.y ?? 0) * -1, + ) + ctx.rotate((controlled.props.transform?.rotation ?? 0) * -1) controlled.emit.onRender(ctx) }, debug: ctx => { // renderPath(context, defaultBoundsProps, bounds().path) }, hitTest: event => { + event.ctx.translate( + controlled.props.transform?.position?.x ?? 0, + controlled.props.transform?.position?.y ?? 0, + ) + event.ctx.rotate(controlled.props.transform?.rotation ?? 0) + parenthood.hitTest(event) if (!event.propagation) return false controlled.emit.onHitTest(event) if (!event.propagation) return false - if (controlled.props.style.pointerEvents === false) return false - context.ctx.save() - context.ctx.setTransform(context.matrix) - context.ctx.lineWidth = controlled.props.style.lineWidth + event.ctx.save() + event.ctx.lineWidth = controlled.props.style.lineWidth ? controlled.props.style.lineWidth < 20 ? 20 : controlled.props.style.lineWidth @@ -81,8 +98,6 @@ const createPath2D = (arg: { const hit = isPointInShape2D(event, props, path()) - context.ctx.resetTransform() - if (hit) { event.target.push(token) let controlledListeners = controlled.props[ @@ -95,15 +110,6 @@ const createPath2D = (arg: { else controlledListeners(event) } - /* let propListeners = props[ - event.type - ] as SingleOrArray - - if (propListeners) { - if (Array.isArray(propListeners)) propListeners.forEach(l => l(event)) - else propListeners(event) - } */ - if (controlled.props.cursor) event.cursor = controlled.props.style.cursor @@ -128,8 +134,12 @@ const createPath2D = (arg: { controlled.emit.onMouseLeave(event) } } - context.ctx.restore() - + event.ctx.restore() + event.ctx.translate( + (controlled.props.transform?.position?.x ?? 0) * -1, + (controlled.props.transform?.position?.y ?? 0) * -1, + ) + event.ctx.rotate((controlled.props.transform?.rotation ?? 0) * -1) return hit }, } diff --git a/src/utils/createShape2D.ts b/src/utils/createShape2D.ts index 0afe9b0..3c46ccf 100644 --- a/src/utils/createShape2D.ts +++ b/src/utils/createShape2D.ts @@ -96,12 +96,21 @@ const createShape2D = < debug: event => path().data.debug(event), render: ctx => { if (!arg.dimensions) return - + ctx.translate( + arg.props.transform?.position?.x ?? 0, + arg.props.transform?.position?.y ?? 0, + ) + ctx.rotate(arg.props.transform?.rotation ?? 0) path().data.render(ctx) // TODO: fix any arg.render(controlled.props as any, context, context.matrix) parenthood.render(ctx) controlled.emit.onRender(ctx) + ctx.translate( + (arg.props.transform?.position?.x ?? 0) * -1, + (arg.props.transform?.position?.x ?? 0) * -1, + ) + ctx.rotate(arg.props.transform?.rotation ?? 0) }, } return token diff --git a/src/utils/createUpdatedContext.ts b/src/utils/createUpdatedContext.ts index 62b0377..927283a 100644 --- a/src/utils/createUpdatedContext.ts +++ b/src/utils/createUpdatedContext.ts @@ -8,12 +8,13 @@ const createUpdatedContext = ( props: Accessor<{ transform?: Partial }>, ) => { const internalContext = useInternalContext() - const matrix = createMatrix(props, internalContext) + /* const matrix = createMatrix(props, internalContext) return deepMergeGetters(internalContext, { get matrix() { return matrix() }, - }) + }) */ + return internalContext! } export { createUpdatedContext } diff --git a/src/utils/renderPath.ts b/src/utils/renderPath.ts index ba7c2b3..2a6f53a 100644 --- a/src/utils/renderPath.ts +++ b/src/utils/renderPath.ts @@ -19,11 +19,10 @@ export default ( context.ctx.shadowColor = resolveColor(props.style.shadow.color ?? 'black') ?? 'black' } - // if (props.style.composite) - context.ctx.globalCompositeOperation = props.style.composite ?? 'source-over' + if (props.style.composite) + context.ctx.globalCompositeOperation = props.style.composite if (props.style.opacity) context.ctx.globalAlpha = props.style.opacity - context.ctx.setTransform(context.matrix) if (props.style.fill) { context.ctx.fillStyle = resolveExtendedColor(props.style.fill) ?? 'transparent' @@ -36,13 +35,12 @@ export default ( context.ctx.lineJoin = props.style.lineJoin ?? 'bevel' if (context.ctx.lineCap) context.ctx.lineCap = props.style.lineCap ?? 'round' - // if (props.style.lineDash) context.ctx.setLineDash(props.style.lineDash) + if (props.style.lineDash) context.ctx.setLineDash(props.style.lineDash) context.ctx.strokeStyle = resolveExtendedColor(props.style.stroke) ?? 'black' context.ctx.stroke(path) } - context.ctx.resetTransform() context.ctx.restore() } From b3b5932b24d9b10223fc3dcf87042960e03ff7c2 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 19:32:26 +0200 Subject: [PATCH 02/31] default `style.pointerEvents` === false `pointerEvents` are now opt-in to prevent unnecessary hitTests we still have to traverse the parent (and thus transform the ctx) in case a sibling has a `pointerEvents: true` fix group.hitTest to properly transform ctx before parentHood. --- src/components/Object2D/Group.tsx | 11 ++++ src/defaultProps.ts | 4 +- src/utils/createPath2D.ts | 94 ++++++++++++++++--------------- src/utils/createShape2D.ts | 4 +- 4 files changed, 65 insertions(+), 48 deletions(-) diff --git a/src/components/Object2D/Group.tsx b/src/components/Object2D/Group.tsx index 95c8a2b..58aea45 100644 --- a/src/components/Object2D/Group.tsx +++ b/src/components/Object2D/Group.tsx @@ -57,7 +57,18 @@ const Group = createToken(parser, (props: Object2DProps) => { }, debug: () => {}, hitTest: event => { + event.ctx.translate( + props.transform?.position?.x ?? 0, + props.transform?.position?.y ?? 0, + ) + event.ctx.rotate(props.transform?.rotation ?? 0) parenthood.hitTest(event) + event.ctx.translate( + (props.transform?.position?.x ?? 0) * -1, + (props.transform?.position?.y ?? 0) * -1, + ) + event.ctx.rotate((props.transform?.rotation ?? 0) * -1) + if (!event.propagation) return false return true }, diff --git a/src/defaultProps.ts b/src/defaultProps.ts index c244bce..75a8b33 100644 --- a/src/defaultProps.ts +++ b/src/defaultProps.ts @@ -9,7 +9,7 @@ const defaultShape2DProps: ResolvedShape2DProps = { lineJoin: 'round', miterLimit: 10, lineWidth: 2, - pointerEvents: true, + pointerEvents: false, opacity: 1, cursor: 'default', }, @@ -35,7 +35,7 @@ const defaultBoundsProps: ResolvedShape2DProps = { opacity: 1, composite: 'destination-over', cursor: undefined, - pointerEvents: true, + pointerEvents: false, }, transform: { position: { x: 0, y: 0 }, diff --git a/src/utils/createPath2D.ts b/src/utils/createPath2D.ts index 49f4527..63d9505 100644 --- a/src/utils/createPath2D.ts +++ b/src/utils/createPath2D.ts @@ -37,10 +37,10 @@ const createPath2D = (arg: { }) const controlled = createControlledProps(props, [ - /* Hover({ + Hover({ style: props.style?.['&:hover'], transform: props.transform?.['&:hover'], - }), */ + }), ]) const context = useInternalContext() @@ -78,6 +78,8 @@ const createPath2D = (arg: { // renderPath(context, defaultBoundsProps, bounds().path) }, hitTest: event => { + // NOTE: we could prevent having to transform ctx + // if props.children.length === 0 && !style.pointerEvents; event.ctx.translate( controlled.props.transform?.position?.x ?? 0, controlled.props.transform?.position?.y ?? 0, @@ -85,56 +87,58 @@ const createPath2D = (arg: { event.ctx.rotate(controlled.props.transform?.rotation ?? 0) parenthood.hitTest(event) - if (!event.propagation) return false - controlled.emit.onHitTest(event) - if (!event.propagation) return false - - event.ctx.save() - event.ctx.lineWidth = controlled.props.style.lineWidth - ? controlled.props.style.lineWidth < 20 - ? 20 - : controlled.props.style.lineWidth - : 20 - - const hit = isPointInShape2D(event, props, path()) - - if (hit) { - event.target.push(token) - let controlledListeners = controlled.props[ - event.type - ] as SingleOrArray - - if (controlledListeners) { - if (Array.isArray(controlledListeners)) - controlledListeners.forEach(l => l(event)) - else controlledListeners(event) - } - if (controlled.props.cursor) - event.cursor = controlled.props.style.cursor + let hit = false + if (controlled.props.style.pointerEvents) { + if (!event.propagation) return false + controlled.emit.onHitTest(event) + if (!event.propagation) return false + + event.ctx.lineWidth = controlled.props.style.lineWidth + ? controlled.props.style.lineWidth < 20 + ? 20 + : controlled.props.style.lineWidth + : 20 + + hit = isPointInShape2D(event, props, path()) + + if (hit) { + event.target.push(token) + let controlledListeners = controlled.props[ + event.type + ] as SingleOrArray + + if (controlledListeners) { + if (Array.isArray(controlledListeners)) + controlledListeners.forEach(l => l(event)) + else controlledListeners(event) + } - if (event.type === 'onMouseMove') { - if (event.target.length === 1) { - if (!hover()) { - setHover(true) - controlled.emit.onMouseEnter(event) - } - } else { - if (hover()) { - setHover(false) - controlled.emit.onMouseLeave(event) + if (controlled.props.cursor) + event.cursor = controlled.props.style.cursor + + if (event.type === 'onMouseMove') { + if (event.target.length === 1) { + if (!hover()) { + setHover(true) + controlled.emit.onMouseEnter(event) + } + } else { + if (hover()) { + setHover(false) + controlled.emit.onMouseLeave(event) + } } } - } - controlled.emit[event.type](event) - } else { - if (hover() && event.type === 'onMouseMove') { - setHover(false) - controlled.emit.onMouseLeave(event) + controlled.emit[event.type](event) + } else { + if (hover() && event.type === 'onMouseMove') { + setHover(false) + controlled.emit.onMouseLeave(event) + } } } - event.ctx.restore() event.ctx.translate( (controlled.props.transform?.position?.x ?? 0) * -1, (controlled.props.transform?.position?.y ?? 0) * -1, diff --git a/src/utils/createShape2D.ts b/src/utils/createShape2D.ts index 3c46ccf..68c9054 100644 --- a/src/utils/createShape2D.ts +++ b/src/utils/createShape2D.ts @@ -84,12 +84,14 @@ const createShape2D = < hitTest: event => { parenthood.hitTest(event) if (!event.propagation) return false - let hit = path().data.hitTest(event) + if (!arg.props.style?.pointerEvents) return false + let hit = path().data.hitTest(event) if (hit) { controlled.emit[event.type](event) } controlled.emit.onHitTest(event) + if (!event.propagation) return false return hit }, From 108683479afb914781ee046912e8746a86fe0433 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 19:33:00 +0200 Subject: [PATCH 03/31] cache `mergeProps` of `Drag` and `Hover` --- src/controllers/Drag.ts | 6 ++++++ src/controllers/Hover.ts | 14 ++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/controllers/Drag.ts b/src/controllers/Drag.ts index fe7a0ab..c65cdde 100644 --- a/src/controllers/Drag.ts +++ b/src/controllers/Drag.ts @@ -61,6 +61,9 @@ const Drag = createController((props, events, options) => { events.onMouseDown(dragEventHandler) let position = { x: 0, y: 0 } + + createEffect(() => {}) + return { transform: { get position() { @@ -72,6 +75,9 @@ const Drag = createController((props, events, options) => { } }, }, + style: { + pointerEvents: true, + }, } }) diff --git a/src/controllers/Hover.ts b/src/controllers/Hover.ts index 0ba739b..7ab6091 100644 --- a/src/controllers/Hover.ts +++ b/src/controllers/Hover.ts @@ -32,19 +32,17 @@ const Hover = createController((props, events, options) => { }) }) - const styles = createMemo(() => mergeProps(props().style, options.style)) - const transforms = createMemo(() => - options.transform - ? mergeProps(props().transform, options.transform) - : props().transform, - ) + const mergedStyle = mergeProps(props().style, options.style) + const mergedTransform = mergeProps(props().transform, options.transform) return { get style() { - return options.style && isHovered() ? styles() : props().style + return options.style && isHovered() ? mergedStyle : props().style }, get transform() { - return options.transform && isHovered() ? transforms() : props().transform + return options.transform && isHovered() + ? mergedTransform + : props().transform }, } }) From bd1851f7668fd9a7bccf3a14bcc1096147284fc6 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 20:29:33 +0200 Subject: [PATCH 04/31] introduce `context.flags` with `shouldHitTest`` Controllers can now set internal flags. first usecase is `Drag()`: when dragging we set `context.flags.shouldHitTest` to false which short-circuits `shouldHitTest` in `createMouseEventHandler` This eliminates jank that came apparent in bigger scenes: `examples/Smileys` is able to drag now smoothly on ios/firefox (m2) with 600 instances. --- src/components/Canvas.tsx | 80 +++++++++++++++++----------- src/context/InternalContext.ts | 6 ++- src/controllers/Drag.ts | 6 ++- src/types.ts | 2 + src/utils/createMouseEventHandler.ts | 27 +++++----- 5 files changed, 71 insertions(+), 50 deletions(-) diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 72c8d1b..b8c6568 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -19,6 +19,7 @@ import { UserContext } from 'src/context/UserContext' import { CanvasToken, parser } from 'src/parser' import { + CanvasFlags, CanvasMouseEvent, CanvasMouseEventTypes, Color, @@ -32,6 +33,7 @@ import { createMouseEventHandler } from 'src/utils/createMouseEventHandler' import forEachReversed from 'src/utils/forEachReversed' import { resolveColor } from 'src/utils/resolveColor' import withContext from 'src/utils/withContext' +import { should } from 'vitest' /** * All `solid-canvas`-components have to be inside a `Canvas` @@ -107,6 +109,47 @@ export const Canvas: Component<{ const matrix = createMatrix(() => props) + const flags: Record = { + shouldHitTest: true, + } + + const setFlag = (key: CanvasFlags, value: boolean) => { + flags[key] = value + } + + const context = { + ctx, + setFlag: setFlag, + get flags() { + return flags + }, + get debug() { + return !!props.debug + }, + get matrix() { + return matrix() + }, + addEventListener: ( + type: CanvasMouseEvent['type'], + callback: (event: CanvasMouseEvent) => void, + ) => { + setEventListeners(type, listeners => [...listeners, callback]) + }, + removeEventListener: ( + type: CanvasMouseEvent['type'], + callback: (event: CanvasMouseEvent) => void, + ) => { + setEventListeners(type, listeners => { + const index = listeners.indexOf(callback) + const result = [ + ...listeners.slice(0, index), + ...listeners.slice(index + 1), + ] + return result + }) + }, + } + const tokens = resolveTokens( parser, withContext( @@ -114,34 +157,7 @@ export const Canvas: Component<{ [ { context: InternalContext, - value: { - ctx, - get debug() { - return !!props.debug - }, - get matrix() { - return matrix() - }, - addEventListener: ( - type: CanvasMouseEvent['type'], - callback: (event: CanvasMouseEvent) => void, - ) => { - setEventListeners(type, listeners => [...listeners, callback]) - }, - removeEventListener: ( - type: CanvasMouseEvent['type'], - callback: (event: CanvasMouseEvent) => void, - ) => { - setEventListeners(type, listeners => { - const index = listeners.indexOf(callback) - const result = [ - ...listeners.slice(0, index), - ...listeners.slice(index + 1), - ] - return result - }) - }, - }, + value: context, }, { context: UserContext, @@ -232,7 +248,7 @@ export const Canvas: Component<{ } } - const scheduled = createScheduled(fn => throttle(fn, 1000 / 120)) + const scheduled = createScheduled(fn => throttle(fn)) createEffect(() => { if (!!props.clock || props.clock === 0) return @@ -265,7 +281,7 @@ export const Canvas: Component<{ const mouseMoveHandler = createMouseEventHandler( 'onMouseMove', tokens, - ctx, + context, eventListeners, event => { props.onMouseMove?.(event) @@ -275,7 +291,7 @@ export const Canvas: Component<{ const mouseDownHandler = createMouseEventHandler( 'onMouseDown', tokens, - ctx, + context, eventListeners, event => { if (props.draggable) { @@ -294,7 +310,7 @@ export const Canvas: Component<{ const mouseUpHandler = createMouseEventHandler( 'onMouseUp', tokens, - ctx, + context, eventListeners, event => { props.onMouseUp?.(event) diff --git a/src/context/InternalContext.ts b/src/context/InternalContext.ts index 68a770a..58cd2b1 100644 --- a/src/context/InternalContext.ts +++ b/src/context/InternalContext.ts @@ -1,8 +1,10 @@ -import { createContext, useContext } from 'solid-js' +import { Accessor, Setter, createContext, useContext } from 'solid-js' import { CanvasToken } from 'src/parser' -import { CanvasMouseEvent } from '../types' +import { CanvasFlags, CanvasMouseEvent } from '../types' export type InternalContextType = { + setFlag: (key: CanvasFlags, value: boolean) => void + flags: Record ctx: CanvasRenderingContext2D matrix: DOMMatrix debug: boolean diff --git a/src/controllers/Drag.ts b/src/controllers/Drag.ts index c65cdde..e5278f6 100644 --- a/src/controllers/Drag.ts +++ b/src/controllers/Drag.ts @@ -1,6 +1,6 @@ -import { Accessor, createEffect, createSignal, onCleanup } from 'solid-js' +import { createEffect, createSignal, onCleanup } from 'solid-js' import { useInternalContext } from 'src/context/InternalContext' -import { CanvasMouseEvent, Vector, Shape2DProps } from 'src/types' +import { CanvasMouseEvent, Vector } from 'src/types' import { createController } from './createController' type DragOptions = { @@ -30,6 +30,7 @@ const Drag = createController((props, events, options) => { options.onDragMove?.(dragPosition(), event) } const handleMouseUp = (event: CanvasMouseEvent) => { + internalContext?.setFlag('shouldHitTest', true) setSelected(false) } @@ -54,6 +55,7 @@ const Drag = createController((props, events, options) => { ? true : options.active ) { + internalContext?.setFlag('shouldHitTest', false) setSelected(true) event.propagation = false } diff --git a/src/types.ts b/src/types.ts index db6a9b6..9c8c447 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,8 @@ import { CanvasToken } from './parser' import { RequiredPartially, SingleOrArray } from './utils/typehelpers' import { InternalContextType } from './context/InternalContext' +export type CanvasFlags = 'shouldHitTest' + export type Object2DProps = { transform?: Transforms style?: { diff --git a/src/utils/createMouseEventHandler.ts b/src/utils/createMouseEventHandler.ts index c2617f1..8af9918 100644 --- a/src/utils/createMouseEventHandler.ts +++ b/src/utils/createMouseEventHandler.ts @@ -3,22 +3,21 @@ import { Accessor } from 'solid-js' import { InternalContextType } from 'src/context/InternalContext' import { CanvasToken } from 'src/parser' import { - Vector, + CanvasFlags, CanvasMouseEvent, - CanvasMouseEventListener, CanvasMouseEventTypes, + Vector, } from 'src/types' -import forEachReversed from './forEachReversed' const createMouseEventHandler = ( type: 'onMouseDown' | 'onMouseMove' | 'onMouseUp', tokens: Accessor[]>, - ctx: CanvasRenderingContext2D, + context: InternalContextType, eventListeners: Record< CanvasMouseEventTypes, ((event: CanvasMouseEvent) => void)[] >, - final?: (event: CanvasMouseEvent) => void, + final: (event: CanvasMouseEvent) => void, ) => { let position: Vector let delta: Vector @@ -37,7 +36,7 @@ const createMouseEventHandler = ( // NOTE: `event` gets mutated by `token.hitTest` event = { - ctx, + ctx: context.ctx, position, delta, propagation: true, @@ -45,14 +44,14 @@ const createMouseEventHandler = ( type, cursor: 'move', } - - tokens().forEach(({ data }) => { - // forEachReversed(tokens(), ({ data }) => { - if (!event.propagation) return - if ('hitTest' in data) { - data.hitTest(event) - } - }) + if (context.flags.shouldHitTest) { + tokens().forEach(({ data }) => { + if (!event.propagation) return + if ('hitTest' in data) { + data.hitTest(event) + } + }) + } if (event.propagation && final) final(event) From 7c57584623c3d534f428c34c85433ca17a338522 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 20:30:16 +0200 Subject: [PATCH 05/31] memo return-type of `createControlledProps` --- src/utils/createControlledProps.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/utils/createControlledProps.ts b/src/utils/createControlledProps.ts index 41b22d4..a34d673 100644 --- a/src/utils/createControlledProps.ts +++ b/src/utils/createControlledProps.ts @@ -1,13 +1,8 @@ import { createLazyMemo } from '@solid-primitives/memo' -import { Accessor, mapArray } from 'solid-js' +import { Accessor, createMemo, mapArray } from 'solid-js' import { ControllerEvents } from 'src/controllers/controllers' import { ResolvedShape2DProps, Shape2DProps } from 'src/types' import { DeepRequired } from './typehelpers' -import withContext from './withContext' -import { - InternalContext, - InternalContextType, -} from 'src/context/InternalContext' const createControlledProps = < T extends Record, @@ -72,10 +67,16 @@ const createControlledProps = < ), ) + const output = createMemo( + () => + (controllers()[controllers().length - 1]?.() ?? + props) as ResolvedShape2DProps & DeepRequired, + ) + return { get props() { - return (controllers()[controllers().length - 1]?.() ?? - props) as ResolvedShape2DProps & DeepRequired + // return props + return output() }, emit, } From aa905d4b792aa35c0d8da7a9565f652ee0d7a379 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 20:31:46 +0200 Subject: [PATCH 06/31] update `Smiley` to include an options-modal --- dev/pages/Smileys.tsx | 54 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/dev/pages/Smileys.tsx b/dev/pages/Smileys.tsx index 6b4232c..70d3239 100644 --- a/dev/pages/Smileys.tsx +++ b/dev/pages/Smileys.tsx @@ -55,7 +55,15 @@ const Smiley = (props: { counter: number }) => { { const clock = createClock() clock.start() + const [amount, setAmount] = createSignal(1) + const [shouldUseClock, setShouldUseClock] = createSignal(false) + return ( <> +
+
+ + setAmount(+e.currentTarget.value)} + step={10} + /> +
+
+ + setShouldUseClock(e.currentTarget.checked)} + /> +
+
- + {() => } From e311ff47b8606084088cf46653094d8497f427b0 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 21:26:22 +0200 Subject: [PATCH 07/31] all hitTests now set `propagation` to `false` and we perform a check before each `hitTest`. This basically short-circuits the `hitTest` once it encounters a hit eliminating jank while for example hovering. event-bubbling still is possible, but then it should be explicitly set to true inside an event-handler set by the user --- src/components/Object2D/Group.tsx | 9 ++++++--- src/types.ts | 16 ++++++++-------- src/utils/createPath2D.ts | 17 +++++------------ src/utils/createShape2D.ts | 5 ++--- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/components/Object2D/Group.tsx b/src/components/Object2D/Group.tsx index 58aea45..1197b22 100644 --- a/src/components/Object2D/Group.tsx +++ b/src/components/Object2D/Group.tsx @@ -57,20 +57,23 @@ const Group = createToken(parser, (props: Object2DProps) => { }, debug: () => {}, hitTest: event => { + if (!event.propagation) return event.ctx.translate( props.transform?.position?.x ?? 0, props.transform?.position?.y ?? 0, ) event.ctx.rotate(props.transform?.rotation ?? 0) - parenthood.hitTest(event) + const hit = parenthood.hitTest(event) + if (hit) { + props[event.type]?.(event) + } event.ctx.translate( (props.transform?.position?.x ?? 0) * -1, (props.transform?.position?.y ?? 0) * -1, ) event.ctx.rotate((props.transform?.rotation ?? 0) * -1) - if (!event.propagation) return false - return true + return hit }, paths: () => [], tokens: [], diff --git a/src/types.ts b/src/types.ts index 9c8c447..b8bd184 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ import { InternalContextType } from './context/InternalContext' export type CanvasFlags = 'shouldHitTest' -export type Object2DProps = { +export type Object2DProps = CanvasMouseEvents & { transform?: Transforms style?: { composite?: Composite @@ -19,7 +19,7 @@ export type Object2DProps = { // controllers?: ((props: Object2DProps, events: ControllerEvents) => any)[] } -export type Shape2DProps = Shape2DEvents & { +export type Shape2DProps = CanvasMouseEvents & { transform?: Transforms & { '&:hover'?: Transforms } style?: Shape2DStyle & { '&:hover'?: Shape2DStyle } @@ -59,27 +59,27 @@ export type ResolvedShape2DProps = Shape2DProps & { transform: Required } -type Shape2DEvents = { +type CanvasMouseEvents = { /** * Set onMouseDown-eventhandler. */ - onMouseDown?: SingleOrArray<(event: CanvasMouseEvent) => void> + onMouseDown?: (event: CanvasMouseEvent) => void /** * Set onMouseUp-eventhandler. */ - onMouseUp?: SingleOrArray<(event: CanvasMouseEvent) => void> + onMouseUp?: (event: CanvasMouseEvent) => void /** * Set onMouseMove-eventhandler. */ - onMouseMove?: SingleOrArray<(event: CanvasMouseEvent) => void> + onMouseMove?: (event: CanvasMouseEvent) => void /** * Set onMouseEnter-eventhandler. */ - onMouseEnter?: SingleOrArray<(event: CanvasMouseEvent) => void> + onMouseEnter?: (event: CanvasMouseEvent) => void /** * Set onMouseLeave-eventhandler. */ - onMouseLeave?: SingleOrArray<(event: CanvasMouseEvent) => void> + onMouseLeave?: (event: CanvasMouseEvent) => void } export interface Transforms { diff --git a/src/utils/createPath2D.ts b/src/utils/createPath2D.ts index 63d9505..8a83094 100644 --- a/src/utils/createPath2D.ts +++ b/src/utils/createPath2D.ts @@ -78,6 +78,8 @@ const createPath2D = (arg: { // renderPath(context, defaultBoundsProps, bounds().path) }, hitTest: event => { + if (!event.propagation) return false + // NOTE: we could prevent having to transform ctx // if props.children.length === 0 && !style.pointerEvents; event.ctx.translate( @@ -90,10 +92,7 @@ const createPath2D = (arg: { let hit = false if (controlled.props.style.pointerEvents) { - if (!event.propagation) return false controlled.emit.onHitTest(event) - if (!event.propagation) return false - event.ctx.lineWidth = controlled.props.style.lineWidth ? controlled.props.style.lineWidth < 20 ? 20 @@ -103,16 +102,10 @@ const createPath2D = (arg: { hit = isPointInShape2D(event, props, path()) if (hit) { + event.propagation = false event.target.push(token) - let controlledListeners = controlled.props[ - event.type - ] as SingleOrArray - - if (controlledListeners) { - if (Array.isArray(controlledListeners)) - controlledListeners.forEach(l => l(event)) - else controlledListeners(event) - } + + controlled.props[event.type]?.(event) if (controlled.props.cursor) event.cursor = controlled.props.style.cursor diff --git a/src/utils/createShape2D.ts b/src/utils/createShape2D.ts index 68c9054..f97301c 100644 --- a/src/utils/createShape2D.ts +++ b/src/utils/createShape2D.ts @@ -82,17 +82,16 @@ const createShape2D = < type: 'Object2D', id: arg.id, hitTest: event => { + if (!event.propagation) return false parenthood.hitTest(event) if (!event.propagation) return false if (!arg.props.style?.pointerEvents) return false - let hit = path().data.hitTest(event) if (hit) { controlled.emit[event.type](event) + arg.props[event.type]?.(event) } controlled.emit.onHitTest(event) - - if (!event.propagation) return false return hit }, debug: event => path().data.debug(event), From a8d04b77029bdae8ffec259ab68f2b6e233b0a1e Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 22:35:11 +0200 Subject: [PATCH 08/31] `transformedCallback`: set/reset ctx.transform uses the more optimal `ctx.translate` and `ctx.rotate` if no skew is present. If skew is present, it will `ctx.getTransform` twice mutate matrix1, mutate it, `ctx.setTransform(matrix1)` and then revert it back to `ctx.setTransform(matrix2)`. This is pretty wasteful, so there is room for improvement there. --- src/components/Object2D/Group.tsx | 23 ++---- src/utils/createPath2D.ts | 119 +++++++++++++----------------- src/utils/createShape2D.ts | 41 +++++----- src/utils/transformedCallback.ts | 44 +++++++++++ 4 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 src/utils/transformedCallback.ts diff --git a/src/components/Object2D/Group.tsx b/src/components/Object2D/Group.tsx index 1197b22..ebcf16a 100644 --- a/src/components/Object2D/Group.tsx +++ b/src/components/Object2D/Group.tsx @@ -6,6 +6,7 @@ import { RegisterControllerEvents } from 'src/controllers/controllers' import { CanvasToken, parser } from 'src/parser' import { Color, Object2DProps, ResolvedShape2DProps, Vector } from 'src/types' import { createParenthood } from 'src/utils/createParenthood' +import { transformedCallback } from 'src/utils/transformedCallback' import { SingleOrArray } from 'src/utils/typehelpers' import { T } from 'vitest/dist/types-c800444e' @@ -58,22 +59,14 @@ const Group = createToken(parser, (props: Object2DProps) => { debug: () => {}, hitTest: event => { if (!event.propagation) return - event.ctx.translate( - props.transform?.position?.x ?? 0, - props.transform?.position?.y ?? 0, - ) - event.ctx.rotate(props.transform?.rotation ?? 0) - const hit = parenthood.hitTest(event) - if (hit) { - props[event.type]?.(event) - } - event.ctx.translate( - (props.transform?.position?.x ?? 0) * -1, - (props.transform?.position?.y ?? 0) * -1, - ) - event.ctx.rotate((props.transform?.rotation ?? 0) * -1) - return hit + return transformedCallback(event.ctx, props, () => { + const hit = parenthood.hitTest(event) + if (hit) { + props[event.type]?.(event) + } + return hit + }) }, paths: () => [], tokens: [], diff --git a/src/utils/createPath2D.ts b/src/utils/createPath2D.ts index 8a83094..b1b7f30 100644 --- a/src/utils/createPath2D.ts +++ b/src/utils/createPath2D.ts @@ -16,6 +16,7 @@ import { DeepRequired, RequireOptionals, SingleOrArray } from './typehelpers' import { Hover } from 'src/controllers/Hover' import { isPointInShape2D } from './isPointInShape2D' import { useInternalContext } from 'src/context/InternalContext' +import { transformedCallback } from './transformedCallback' const createPath2D = (arg: { id: string @@ -58,21 +59,11 @@ const createPath2D = (arg: { id: arg.id, path, render: ctx => { - ctx.translate( - controlled.props.transform?.position?.x ?? 0, - controlled.props.transform?.position?.y ?? 0, - ) - ctx.rotate(controlled.props.transform?.rotation ?? 0) - - renderPath(context!, controlled.props, path()) - parenthood.render(ctx) - - ctx.translate( - (controlled.props.transform?.position?.x ?? 0) * -1, - (controlled.props.transform?.position?.y ?? 0) * -1, - ) - ctx.rotate((controlled.props.transform?.rotation ?? 0) * -1) - controlled.emit.onRender(ctx) + transformedCallback(ctx, controlled.props, () => { + renderPath(context!, controlled.props, path()) + parenthood.render(ctx) + controlled.emit.onRender(ctx) + }) }, debug: ctx => { // renderPath(context, defaultBoundsProps, bounds().path) @@ -80,64 +71,56 @@ const createPath2D = (arg: { hitTest: event => { if (!event.propagation) return false - // NOTE: we could prevent having to transform ctx - // if props.children.length === 0 && !style.pointerEvents; - event.ctx.translate( - controlled.props.transform?.position?.x ?? 0, - controlled.props.transform?.position?.y ?? 0, - ) - event.ctx.rotate(controlled.props.transform?.rotation ?? 0) - - parenthood.hitTest(event) - - let hit = false - if (controlled.props.style.pointerEvents) { - controlled.emit.onHitTest(event) - event.ctx.lineWidth = controlled.props.style.lineWidth - ? controlled.props.style.lineWidth < 20 - ? 20 - : controlled.props.style.lineWidth - : 20 - - hit = isPointInShape2D(event, props, path()) - - if (hit) { - event.propagation = false - event.target.push(token) - - controlled.props[event.type]?.(event) - - if (controlled.props.cursor) - event.cursor = controlled.props.style.cursor - - if (event.type === 'onMouseMove') { - if (event.target.length === 1) { - if (!hover()) { - setHover(true) - controlled.emit.onMouseEnter(event) - } - } else { - if (hover()) { - setHover(false) - controlled.emit.onMouseLeave(event) + return transformedCallback(event.ctx, controlled.props, () => { + parenthood.hitTest(event) + + let hit = false + if (controlled.props.style.pointerEvents) { + controlled.emit.onHitTest(event) + event.ctx.lineWidth = controlled.props.style.lineWidth + ? controlled.props.style.lineWidth < 20 + ? 20 + : controlled.props.style.lineWidth + : 20 + + hit = isPointInShape2D(event, props, path()) + + if (hit) { + event.propagation = false + event.target.push(token) + + controlled.props[event.type]?.(event) + + if (controlled.props.cursor) + event.cursor = controlled.props.style.cursor + + if (event.type === 'onMouseMove') { + if (event.target.length === 1) { + if (!hover()) { + setHover(true) + controlled.emit.onMouseEnter(event) + } + } else { + if (hover()) { + setHover(false) + controlled.emit.onMouseLeave(event) + } } } - } - controlled.emit[event.type](event) - } else { - if (hover() && event.type === 'onMouseMove') { - setHover(false) - controlled.emit.onMouseLeave(event) + controlled.emit[event.type](event) + } else { + if (hover() && event.type === 'onMouseMove') { + setHover(false) + controlled.emit.onMouseLeave(event) + } } } - } - event.ctx.translate( - (controlled.props.transform?.position?.x ?? 0) * -1, - (controlled.props.transform?.position?.y ?? 0) * -1, - ) - event.ctx.rotate((controlled.props.transform?.rotation ?? 0) * -1) - return hit + return hit + }) + + // NOTE: we could prevent having to transform ctx + // if props.children.length === 0 && !style.pointerEvents; }, } return token diff --git a/src/utils/createShape2D.ts b/src/utils/createShape2D.ts index f97301c..c2f0b28 100644 --- a/src/utils/createShape2D.ts +++ b/src/utils/createShape2D.ts @@ -14,6 +14,7 @@ import { createUpdatedContext } from './createUpdatedContext' import { deepMergeGetters, mergeGetters } from './mergeGetters' import { mergeShape2DProps } from './mergeShape2DProps' import withContext from './withContext' +import { transformedCallback } from './transformedCallback' const createShape2D = < T, @@ -86,32 +87,28 @@ const createShape2D = < parenthood.hitTest(event) if (!event.propagation) return false if (!arg.props.style?.pointerEvents) return false - let hit = path().data.hitTest(event) - if (hit) { - controlled.emit[event.type](event) - arg.props[event.type]?.(event) - } - controlled.emit.onHitTest(event) - return hit + + return transformedCallback(event.ctx, arg.props, () => { + let hit = path().data.hitTest(event) + if (hit) { + controlled.emit[event.type](event) + arg.props[event.type]?.(event) + } + controlled.emit.onHitTest(event) + return hit + }) }, debug: event => path().data.debug(event), render: ctx => { if (!arg.dimensions) return - ctx.translate( - arg.props.transform?.position?.x ?? 0, - arg.props.transform?.position?.y ?? 0, - ) - ctx.rotate(arg.props.transform?.rotation ?? 0) - path().data.render(ctx) - // TODO: fix any - arg.render(controlled.props as any, context, context.matrix) - parenthood.render(ctx) - controlled.emit.onRender(ctx) - ctx.translate( - (arg.props.transform?.position?.x ?? 0) * -1, - (arg.props.transform?.position?.x ?? 0) * -1, - ) - ctx.rotate(arg.props.transform?.rotation ?? 0) + + transformedCallback(ctx, arg.props, () => { + path().data.render(ctx) + // TODO: fix any + arg.render(controlled.props as any, context, context.matrix) + parenthood.render(ctx) + controlled.emit.onRender(ctx) + }) }, } return token diff --git a/src/utils/transformedCallback.ts b/src/utils/transformedCallback.ts new file mode 100644 index 0000000..e464267 --- /dev/null +++ b/src/utils/transformedCallback.ts @@ -0,0 +1,44 @@ +import { Transforms } from 'src/types' + +let matrix: DOMMatrix, matrix2: DOMMatrix, result: any + +const transformedCallback = ( + ctx: CanvasRenderingContext2D, + props: { transform?: Transforms }, + callback: () => T, +): T => { + if (!props.transform) { + return callback() + } else if (props.transform.skew) { + matrix = ctx.getTransform() + matrix2 = ctx.getTransform() + matrix.translateSelf( + props.transform.position?.x ?? 0, + props.transform.position?.y ?? 0, + ) + matrix.rotateSelf(props.transform.rotation ?? 0) + matrix.skewXSelf(props.transform.skew?.x ?? 0) + matrix.skewYSelf(props.transform.skew?.y ?? 0) + ctx.setTransform(matrix) + result = callback() + ctx.setTransform(matrix2) + return result + } else { + ctx.translate( + props.transform.position?.x ?? 0, + props.transform.position?.y ?? 0, + ) + ctx.rotate(props.transform.rotation ?? 0) + + result = callback() + + ctx.translate( + (props.transform.position?.x ?? 0) * -1, + (props.transform.position?.y ?? 0) * -1, + ) + ctx.rotate((props.transform.rotation ?? 0) * -1) + return result + } +} + +export { transformedCallback } From b97b8c5880d545fc005a511624c27e2dcd12fdc1 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 23:17:54 +0200 Subject: [PATCH 09/31] `context.registerInteractiveToken` Tokens with `style.pointerEvents: true` now register themselves. Currently it's only being used to set the flag `hasInteractiveTokens` which is being checked in `createMouseEventHandler`, later iterations could use this information to only perform hitTests on interactive tokens. --- src/components/Canvas.tsx | 34 ++++++++++++++++++++++++++-- src/context/InternalContext.ts | 4 +++- src/types.ts | 3 +-- src/utils/createMouseEventHandler.ts | 2 +- src/utils/createPath2D.ts | 16 ++++++++++++- src/utils/createShape2D.ts | 6 +++++ 6 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index b8c6568..4c1b3c7 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -14,7 +14,10 @@ import { untrack, } from 'solid-js' import { createStore } from 'solid-js/store' -import { InternalContext } from 'src/context/InternalContext' +import { + InternalContext, + InternalContextType, +} from 'src/context/InternalContext' import { UserContext } from 'src/context/UserContext' import { CanvasToken, parser } from 'src/parser' @@ -111,13 +114,36 @@ export const Canvas: Component<{ const flags: Record = { shouldHitTest: true, + hasInteractiveTokens: false, + } + + const [interactiveTokens, setInteractiveTokens] = createSignal( + [], + ) + + createEffect(() => { + if (interactiveTokens().length > 0) { + setFlag('hasInteractiveTokens', true) + } else { + setFlag('hasInteractiveTokens', false) + } + }) + + const registerInteractiveToken = (token: CanvasToken, add = true) => { + if (add) { + setInteractiveTokens(tokens => [...tokens, token]) + } else { + if (interactiveTokens().includes(token)) { + setInteractiveTokens(tokens => tokens.filter(t => t !== token)) + } + } } const setFlag = (key: CanvasFlags, value: boolean) => { flags[key] = value } - const context = { + const context: InternalContextType = { ctx, setFlag: setFlag, get flags() { @@ -129,6 +155,10 @@ export const Canvas: Component<{ get matrix() { return matrix() }, + registerInteractiveToken, + get interactiveTokens() { + return interactiveTokens() + }, addEventListener: ( type: CanvasMouseEvent['type'], callback: (event: CanvasMouseEvent) => void, diff --git a/src/context/InternalContext.ts b/src/context/InternalContext.ts index 58cd2b1..17e5f13 100644 --- a/src/context/InternalContext.ts +++ b/src/context/InternalContext.ts @@ -1,8 +1,10 @@ -import { Accessor, Setter, createContext, useContext } from 'solid-js' +import { createContext, useContext } from 'solid-js' import { CanvasToken } from 'src/parser' import { CanvasFlags, CanvasMouseEvent } from '../types' export type InternalContextType = { + registerInteractiveToken: (token: CanvasToken, add?: boolean) => void + interactiveTokens: CanvasToken[] setFlag: (key: CanvasFlags, value: boolean) => void flags: Record ctx: CanvasRenderingContext2D diff --git a/src/types.ts b/src/types.ts index b8bd184..8d4328a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,9 +2,8 @@ import { Accessor, JSX } from 'solid-js' import { RegisterControllerEvents } from './controllers/controllers' import { CanvasToken } from './parser' import { RequiredPartially, SingleOrArray } from './utils/typehelpers' -import { InternalContextType } from './context/InternalContext' -export type CanvasFlags = 'shouldHitTest' +export type CanvasFlags = 'shouldHitTest' | 'hasInteractiveTokens' export type Object2DProps = CanvasMouseEvents & { transform?: Transforms diff --git a/src/utils/createMouseEventHandler.ts b/src/utils/createMouseEventHandler.ts index 8af9918..e14f25f 100644 --- a/src/utils/createMouseEventHandler.ts +++ b/src/utils/createMouseEventHandler.ts @@ -44,7 +44,7 @@ const createMouseEventHandler = ( type, cursor: 'move', } - if (context.flags.shouldHitTest) { + if (context.flags.shouldHitTest && context.flags.hasInteractiveTokens) { tokens().forEach(({ data }) => { if (!event.propagation) return if ('hitTest' in data) { diff --git a/src/utils/createPath2D.ts b/src/utils/createPath2D.ts index b1b7f30..66f5946 100644 --- a/src/utils/createPath2D.ts +++ b/src/utils/createPath2D.ts @@ -1,4 +1,10 @@ -import { createMemo, createSignal, createUniqueId, mergeProps } from 'solid-js' +import { + createEffect, + createMemo, + createSignal, + createUniqueId, + mergeProps, +} from 'solid-js' import { defaultShape2DProps } from 'src/defaultProps' import { Shape2DToken } from 'src/parser' import { @@ -123,6 +129,14 @@ const createPath2D = (arg: { // if props.children.length === 0 && !style.pointerEvents; }, } + + createEffect(() => + context?.registerInteractiveToken( + token, + controlled.props.style.pointerEvents, + ), + ) + return token } diff --git a/src/utils/createShape2D.ts b/src/utils/createShape2D.ts index c2f0b28..c87e6a3 100644 --- a/src/utils/createShape2D.ts +++ b/src/utils/createShape2D.ts @@ -111,6 +111,12 @@ const createShape2D = < }) }, } + createEffect(() => + context?.registerInteractiveToken( + token, + controlled.props.style.pointerEvents, + ), + ) return token } export { createShape2D } From 6a2447558bb16e48e052ba9614d12f950d00c644 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 23:49:19 +0200 Subject: [PATCH 10/31] hitTest only interactive tokens interactive tokens keep track of their transform-matrix and `setTransform/resetTransform` the ctx. --- src/components/Canvas.tsx | 6 +- src/utils/createMouseEventHandler.ts | 4 +- src/utils/createPath2D.ts | 95 +++++++++++++++------------- 3 files changed, 55 insertions(+), 50 deletions(-) diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 4c1b3c7..5de9489 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -310,7 +310,7 @@ export const Canvas: Component<{ const mouseMoveHandler = createMouseEventHandler( 'onMouseMove', - tokens, + interactiveTokens, context, eventListeners, event => { @@ -320,7 +320,7 @@ export const Canvas: Component<{ const mouseDownHandler = createMouseEventHandler( 'onMouseDown', - tokens, + interactiveTokens, context, eventListeners, event => { @@ -339,7 +339,7 @@ export const Canvas: Component<{ const mouseUpHandler = createMouseEventHandler( 'onMouseUp', - tokens, + interactiveTokens, context, eventListeners, event => { diff --git a/src/utils/createMouseEventHandler.ts b/src/utils/createMouseEventHandler.ts index e14f25f..6cfdfb0 100644 --- a/src/utils/createMouseEventHandler.ts +++ b/src/utils/createMouseEventHandler.ts @@ -11,7 +11,7 @@ import { const createMouseEventHandler = ( type: 'onMouseDown' | 'onMouseMove' | 'onMouseUp', - tokens: Accessor[]>, + tokens: Accessor, context: InternalContextType, eventListeners: Record< CanvasMouseEventTypes, @@ -45,7 +45,7 @@ const createMouseEventHandler = ( cursor: 'move', } if (context.flags.shouldHitTest && context.flags.hasInteractiveTokens) { - tokens().forEach(({ data }) => { + tokens().forEach(data => { if (!event.propagation) return if ('hitTest' in data) { data.hitTest(event) diff --git a/src/utils/createPath2D.ts b/src/utils/createPath2D.ts index 66f5946..9296c4a 100644 --- a/src/utils/createPath2D.ts +++ b/src/utils/createPath2D.ts @@ -60,6 +60,8 @@ const createPath2D = (arg: { const [hover, setHover] = createSignal(false) + let matrix: DOMMatrix + const token: Shape2DToken = { type: 'Shape2D', id: arg.id, @@ -69,6 +71,12 @@ const createPath2D = (arg: { renderPath(context!, controlled.props, path()) parenthood.render(ctx) controlled.emit.onRender(ctx) + if ( + controlled.props.style.pointerEvents && + context?.flags.shouldHitTest + ) { + matrix = ctx.getTransform() + } }) }, debug: ctx => { @@ -76,57 +84,54 @@ const createPath2D = (arg: { }, hitTest: event => { if (!event.propagation) return false - - return transformedCallback(event.ctx, controlled.props, () => { - parenthood.hitTest(event) - - let hit = false - if (controlled.props.style.pointerEvents) { - controlled.emit.onHitTest(event) - event.ctx.lineWidth = controlled.props.style.lineWidth - ? controlled.props.style.lineWidth < 20 - ? 20 - : controlled.props.style.lineWidth - : 20 - - hit = isPointInShape2D(event, props, path()) - - if (hit) { - event.propagation = false - event.target.push(token) - - controlled.props[event.type]?.(event) - - if (controlled.props.cursor) - event.cursor = controlled.props.style.cursor - - if (event.type === 'onMouseMove') { - if (event.target.length === 1) { - if (!hover()) { - setHover(true) - controlled.emit.onMouseEnter(event) - } - } else { - if (hover()) { - setHover(false) - controlled.emit.onMouseLeave(event) - } + parenthood.hitTest(event) + + event.ctx.setTransform(matrix) + let hit = false + if (controlled.props.style.pointerEvents) { + controlled.emit.onHitTest(event) + event.ctx.lineWidth = controlled.props.style.lineWidth + ? controlled.props.style.lineWidth < 20 + ? 20 + : controlled.props.style.lineWidth + : 20 + + hit = isPointInShape2D(event, props, path()) + + if (hit) { + event.propagation = false + event.target.push(token) + + controlled.props[event.type]?.(event) + + if (controlled.props.cursor) + event.cursor = controlled.props.style.cursor + + if (event.type === 'onMouseMove') { + if (event.target.length === 1) { + if (!hover()) { + setHover(true) + controlled.emit.onMouseEnter(event) + } + } else { + if (hover()) { + setHover(false) + controlled.emit.onMouseLeave(event) } } + } - controlled.emit[event.type](event) - } else { - if (hover() && event.type === 'onMouseMove') { - setHover(false) - controlled.emit.onMouseLeave(event) - } + controlled.emit[event.type](event) + } else { + if (hover() && event.type === 'onMouseMove') { + setHover(false) + controlled.emit.onMouseLeave(event) } } - return hit - }) + } + event.ctx.resetTransform() - // NOTE: we could prevent having to transform ctx - // if props.children.length === 0 && !style.pointerEvents; + return hit }, } From c87983f3e9e08fa336b0ff9fadb5d722ece1440c Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 8 Apr 2023 23:55:49 +0200 Subject: [PATCH 11/31] only hitTest interactive `Group` and `Shape2D` --- src/components/Object2D/Group.tsx | 12 +++++------- src/utils/createShape2D.ts | 23 ++++++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/components/Object2D/Group.tsx b/src/components/Object2D/Group.tsx index ebcf16a..4d95d3c 100644 --- a/src/components/Object2D/Group.tsx +++ b/src/components/Object2D/Group.tsx @@ -60,13 +60,11 @@ const Group = createToken(parser, (props: Object2DProps) => { hitTest: event => { if (!event.propagation) return - return transformedCallback(event.ctx, props, () => { - const hit = parenthood.hitTest(event) - if (hit) { - props[event.type]?.(event) - } - return hit - }) + const hit = parenthood.hitTest(event) + if (hit) { + props[event.type]?.(event) + } + return hit }, paths: () => [], tokens: [], diff --git a/src/utils/createShape2D.ts b/src/utils/createShape2D.ts index c87e6a3..d8973ce 100644 --- a/src/utils/createShape2D.ts +++ b/src/utils/createShape2D.ts @@ -79,6 +79,8 @@ const createShape2D = < context, ) as Accessor> + let matrix: DOMMatrix + const token: Object2DToken = { type: 'Object2D', id: arg.id, @@ -88,15 +90,15 @@ const createShape2D = < if (!event.propagation) return false if (!arg.props.style?.pointerEvents) return false - return transformedCallback(event.ctx, arg.props, () => { - let hit = path().data.hitTest(event) - if (hit) { - controlled.emit[event.type](event) - arg.props[event.type]?.(event) - } - controlled.emit.onHitTest(event) - return hit - }) + event.ctx.setTransform(matrix) + let hit = path().data.hitTest(event) + if (hit) { + controlled.emit[event.type](event) + arg.props[event.type]?.(event) + } + controlled.emit.onHitTest(event) + event.ctx.resetTransform() + return hit }, debug: event => path().data.debug(event), render: ctx => { @@ -108,6 +110,9 @@ const createShape2D = < arg.render(controlled.props as any, context, context.matrix) parenthood.render(ctx) controlled.emit.onRender(ctx) + if (arg.props.style?.pointerEvents && context.flags.shouldHitTest) { + matrix = ctx.getTransform() + } }) }, } From e1e32ff60f69264ad32de0442ea9d36abbf9eec1 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Tue, 11 Apr 2023 01:13:44 +0200 Subject: [PATCH 12/31] cache `props.style` in `renderPath` A big amount of time (10%) was spent in getting styles in `Hover`. Caching the result up front in a mutated `style` variable brings the cost of accessing props.style down to 2%. --- src/utils/renderPath.ts | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/utils/renderPath.ts b/src/utils/renderPath.ts index 2a6f53a..37eced3 100644 --- a/src/utils/renderPath.ts +++ b/src/utils/renderPath.ts @@ -2,43 +2,40 @@ import { InternalContextType } from 'src/context/InternalContext' import { ResolvedShape2DProps } from 'src/types' import { resolveColor, resolveExtendedColor } from './resolveColor' +let style export default ( context: InternalContextType, props: ResolvedShape2DProps, path: Path2D, ) => { + style = props.style context.ctx.save() // NOTE: it would be more performant if we would compose commands from the styles with mapArray // and forEach execute them, instead of doing checks on each renderPath. - if (props.style.shadow) { - context.ctx.shadowBlur = props.style.shadow.blur ?? 0 - context.ctx.shadowOffsetX = props.style.shadow.offset?.x ?? 0 - context.ctx.shadowOffsetY = props.style.shadow.offset?.y ?? 0 + if (style.shadow) { + context.ctx.shadowBlur = style.shadow.blur ?? 0 + context.ctx.shadowOffsetX = style.shadow.offset?.x ?? 0 + context.ctx.shadowOffsetY = style.shadow.offset?.y ?? 0 context.ctx.shadowColor = - resolveColor(props.style.shadow.color ?? 'black') ?? 'black' + resolveColor(style.shadow.color ?? 'black') ?? 'black' } - if (props.style.composite) - context.ctx.globalCompositeOperation = props.style.composite - if (props.style.opacity) context.ctx.globalAlpha = props.style.opacity + if (style.composite) context.ctx.globalCompositeOperation = style.composite + if (style.opacity) context.ctx.globalAlpha = style.opacity - if (props.style.fill) { - context.ctx.fillStyle = - resolveExtendedColor(props.style.fill) ?? 'transparent' + if (style.fill) { + context.ctx.fillStyle = resolveExtendedColor(style.fill) ?? 'transparent' context.ctx.fill(path) } - if (props.style.stroke && props.style.stroke !== 'transparent') { - if (props.style.lineWidth) context.ctx.lineWidth = props.style.lineWidth - if (props.style.miterLimit) context.ctx.miterLimit = props.style.miterLimit - if (props.style.lineJoin) - context.ctx.lineJoin = props.style.lineJoin ?? 'bevel' - if (context.ctx.lineCap) - context.ctx.lineCap = props.style.lineCap ?? 'round' - if (props.style.lineDash) context.ctx.setLineDash(props.style.lineDash) + if (style.stroke && style.stroke !== 'transparent') { + if (style.lineWidth) context.ctx.lineWidth = style.lineWidth + if (style.miterLimit) context.ctx.miterLimit = style.miterLimit + if (style.lineJoin) context.ctx.lineJoin = style.lineJoin ?? 'bevel' + if (context.ctx.lineCap) context.ctx.lineCap = style.lineCap ?? 'round' + if (style.lineDash) context.ctx.setLineDash(style.lineDash) - context.ctx.strokeStyle = - resolveExtendedColor(props.style.stroke) ?? 'black' + context.ctx.strokeStyle = resolveExtendedColor(style.stroke) ?? 'black' context.ctx.stroke(path) } From ccf103bcb0e63eb7810accb01ceb1a11535ad30c Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 15:48:08 +0200 Subject: [PATCH 13/31] throttle mouse event --- src/utils/createMouseEventHandler.ts | 9 ++++++--- src/utils/throttle.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/utils/throttle.ts diff --git a/src/utils/createMouseEventHandler.ts b/src/utils/createMouseEventHandler.ts index 6cfdfb0..c46752c 100644 --- a/src/utils/createMouseEventHandler.ts +++ b/src/utils/createMouseEventHandler.ts @@ -8,6 +8,7 @@ import { CanvasMouseEventTypes, Vector, } from 'src/types' +import { throttle } from './throttle' const createMouseEventHandler = ( type: 'onMouseDown' | 'onMouseMove' | 'onMouseUp', @@ -24,7 +25,7 @@ const createMouseEventHandler = ( let event: CanvasMouseEvent let lastCursorPosition: Vector - return (e: MouseEvent) => { + const func = throttle((e: MouseEvent) => { position = { x: e.clientX, y: e.clientY } delta = lastCursorPosition ? { @@ -53,14 +54,16 @@ const createMouseEventHandler = ( }) } - if (event.propagation && final) final(event) + final?.(event) // setCursorStyle(event.cursor) eventListeners[type].forEach(listener => listener(event)) return event - } + }) + + return func } export { createMouseEventHandler } diff --git a/src/utils/throttle.ts b/src/utils/throttle.ts new file mode 100644 index 0000000..3d61564 --- /dev/null +++ b/src/utils/throttle.ts @@ -0,0 +1,14 @@ +let amount = 60 +export const throttle = (callback: (...args: any[]) => void) => { + let lastTime: number = performance.now() + return (...args: any[]) => { + if (performance.now() - lastTime < 1000 / amount) { + lastTime = performance.now() + queueMicrotask(() => callback(...args)) + return + } else { + lastTime = performance.now() + return callback(...args) + } + } +} From 837da5d02dee156e8f354bf12328927f1d56daad Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 15:50:58 +0200 Subject: [PATCH 14/31] pass `token` to controllers --- src/controllers/Drag.ts | 6 ++- src/controllers/Hover.ts | 63 +++++++++++++---------------- src/controllers/createController.ts | 13 ++++-- src/utils/createControlledProps.ts | 3 ++ 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/controllers/Drag.ts b/src/controllers/Drag.ts index e5278f6..777ff54 100644 --- a/src/controllers/Drag.ts +++ b/src/controllers/Drag.ts @@ -9,7 +9,7 @@ type DragOptions = { onDragMove?: (position: Vector, event: CanvasMouseEvent) => void } -const Drag = createController((props, events, options) => { +const Drag = createController((props, events, token, options) => { const [dragPosition, setDragPosition] = createSignal({ x: 0, y: 0 }) const [selected, setSelected] = createSignal(false) const internalContext = useInternalContext() @@ -63,6 +63,10 @@ const Drag = createController((props, events, options) => { events.onMouseDown(dragEventHandler) let position = { x: 0, y: 0 } + /* const position = createMemo(() => ({ + x: (props().transform.position?.x || 0) + dragPosition().x, + y: (props().transform.position?.y || 0) + dragPosition().y + })) */ createEffect(() => {}) diff --git a/src/controllers/Hover.ts b/src/controllers/Hover.ts index 7ab6091..c5205a6 100644 --- a/src/controllers/Hover.ts +++ b/src/controllers/Hover.ts @@ -1,15 +1,6 @@ -import { - createEffect, - createMemo, - createSignal, - indexArray, - mergeProps, - onCleanup, - untrack, -} from 'solid-js' -import { RGB, Shape2DStyle, Transforms } from 'src/types' +import { createEffect, createSignal, mergeProps } from 'solid-js' +import { Shape2DStyle, Transforms } from 'src/types' import { createController } from './createController' -import { deepMergeGetters } from 'src/utils/mergeGetters' type HoverOptions = { active?: boolean @@ -17,34 +8,34 @@ type HoverOptions = { transform: Transforms | undefined } -const Hover = createController((props, events, options) => { - const [isHovered, setIsHovered] = createSignal(false) +const Hover = createController( + (props, events, token, options) => { + const [isHovered, setIsHovered] = createSignal(false) - createEffect(() => { - if (!options.style) return - events.onMouseEnter(() => { - setIsHovered(true) - // tweens().forEach(t => t[1]('forward')) + createEffect(() => { + if (!options.style) return + events.onMouseEnter(() => { + setIsHovered(true) + }) + events.onMouseLeave(() => { + setIsHovered(false) + }) }) - events.onMouseLeave(() => { - setIsHovered(false) - // tweens().forEach(t => t[1]('backward')) - }) - }) - const mergedStyle = mergeProps(props().style, options.style) - const mergedTransform = mergeProps(props().transform, options.transform) + const mergedStyle = mergeProps(props().style, options.style) + const mergedTransform = mergeProps(props().transform, options.transform) - return { - get style() { - return options.style && isHovered() ? mergedStyle : props().style - }, - get transform() { - return options.transform && isHovered() - ? mergedTransform - : props().transform - }, - } -}) + return { + get style() { + return options.style && isHovered() ? mergedStyle : props().style + }, + get transform() { + return options.transform && isHovered() + ? mergedTransform + : props().transform + }, + } + }, +) export { Hover } diff --git a/src/controllers/createController.ts b/src/controllers/createController.ts index 79881cc..a6576de 100644 --- a/src/controllers/createController.ts +++ b/src/controllers/createController.ts @@ -2,18 +2,21 @@ import { Accessor, createMemo } from 'solid-js' import { ResolvedShape2DProps, Shape2DProps } from 'src/types' import { deepMergeGetters } from 'src/utils/mergeGetters' import { RegisterControllerEvents } from './controllers' +import { CanvasToken } from 'src/parser' const createController = < ControllerOptions extends Record, AdditionalProperties = {}, + Token = CanvasToken, >( callback: ( props: Accessor< ResolvedShape2DProps & AdditionalProperties >, events: RegisterControllerEvents, + token: Accessor, options: ControllerOptions, - ) => Partial, + ) => Partial | undefined, ) => { function Controller( options?: ControllerOptions, @@ -23,6 +26,7 @@ const createController = < ResolvedShape2DProps & AdditionalProperties >, events: RegisterControllerEvents, + token: Accessor, options: ControllerOptions, ): Accessor & AdditionalProperties> function Controller( @@ -30,6 +34,7 @@ const createController = < | Accessor> | ControllerOptions, events?: RegisterControllerEvents, + token?: Accessor, options?: ControllerOptions, ) { if (!events) { @@ -38,7 +43,8 @@ const createController = < ResolvedShape2DProps & AdditionalProperties >, events: RegisterControllerEvents, - ) => Controller(props, events, propsOrOptions as ControllerOptions) + token: Accessor, + ) => Controller(props, events, token, propsOrOptions as ControllerOptions) } // NOTE: `result` needs to be defined outside `mergeProps` for unknown reasons @@ -48,6 +54,7 @@ const createController = < ResolvedShape2DProps & AdditionalProperties >, events, + token!, options!, ) @@ -67,7 +74,7 @@ const createController = < )(), result, ), - ) as Accessor & AdditionalProperties> + ) as Accessor> } return Controller } diff --git a/src/utils/createControlledProps.ts b/src/utils/createControlledProps.ts index a34d673..d7fb98a 100644 --- a/src/utils/createControlledProps.ts +++ b/src/utils/createControlledProps.ts @@ -3,6 +3,7 @@ import { Accessor, createMemo, mapArray } from 'solid-js' import { ControllerEvents } from 'src/controllers/controllers' import { ResolvedShape2DProps, Shape2DProps } from 'src/types' import { DeepRequired } from './typehelpers' +import { CanvasToken } from 'src/parser' const createControlledProps = < T extends Record, @@ -10,6 +11,7 @@ const createControlledProps = < >( props: U, defaultControllers: Accessor>[] = [], + token: Accessor, ) => { const events: { [K in keyof ControllerEvents]: ControllerEvents[K][] @@ -62,6 +64,7 @@ const createControlledProps = < onRender: callback => events.onRender.push(callback), onHitTest: callback => events.onHitTest.push(callback), }, + token, ) }, ), From 59a6878084fb3b59804779de52467dc3e1f67c22 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:01:28 +0200 Subject: [PATCH 15/31] createTransformedCallback and createBounds --- src/utils/createBounds.ts | 19 +++---- src/utils/createPath2D.ts | 42 +++++++------- src/utils/createShape2D.ts | 9 ++- src/utils/transformedCallback.ts | 98 +++++++++++++++++++------------- 4 files changed, 95 insertions(+), 73 deletions(-) diff --git a/src/utils/createBounds.ts b/src/utils/createBounds.ts index 7d5e217..4d43709 100644 --- a/src/utils/createBounds.ts +++ b/src/utils/createBounds.ts @@ -1,9 +1,8 @@ import { Accessor, createMemo } from 'solid-js' -import { InternalContextType } from 'src/context/InternalContext' -const createBounds = ( +export const createBounds = ( points: Accessor<{ x: number; y: number }[]>, - context: InternalContextType, + matrix: Accessor, ) => { let dimensions: { width: number; height: number } let position: { x: number; y: number } @@ -19,6 +18,7 @@ const createBounds = ( max: -Infinity, }, } + return createMemo(() => { bounds = { x: { @@ -32,11 +32,11 @@ const createBounds = ( } points().forEach(({ x, y }) => { - point = new DOMPoint(x, y).matrixTransform(context.matrix) - if (point.x < bounds.x.min) bounds.x.min = x - if (point.x > bounds.x.max) bounds.x.max = x - if (point.y < bounds.y.min) bounds.y.min = y - if (point.y > bounds.y.max) bounds.y.max = y + point = new DOMPoint(x, y).matrixTransform(matrix()) + if (point.x < bounds.x.min) bounds.x.min = point.x + if (point.x > bounds.x.max) bounds.x.max = point.x + if (point.y < bounds.y.min) bounds.y.min = point.y + if (point.y > bounds.y.max) bounds.y.max = point.y }) dimensions = { @@ -50,7 +50,6 @@ const createBounds = ( path = new Path2D() path.rect(position.x, position.y, dimensions.width, dimensions.height) - return { path, position, @@ -58,5 +57,3 @@ const createBounds = ( } }) } - -export { createBounds } diff --git a/src/utils/createPath2D.ts b/src/utils/createPath2D.ts index 9296c4a..69f3aeb 100644 --- a/src/utils/createPath2D.ts +++ b/src/utils/createPath2D.ts @@ -5,7 +5,7 @@ import { createUniqueId, mergeProps, } from 'solid-js' -import { defaultShape2DProps } from 'src/defaultProps' +import { defaultBoundsProps, defaultShape2DProps } from 'src/defaultProps' import { Shape2DToken } from 'src/parser' import { CanvasMouseEventListener, @@ -22,7 +22,8 @@ import { DeepRequired, RequireOptionals, SingleOrArray } from './typehelpers' import { Hover } from 'src/controllers/Hover' import { isPointInShape2D } from './isPointInShape2D' import { useInternalContext } from 'src/context/InternalContext' -import { transformedCallback } from './transformedCallback' +import { createTransformedCallback } from './transformedCallback' +import { createBounds } from './createBounds' const createPath2D = (arg: { id: string @@ -43,12 +44,16 @@ const createPath2D = (arg: { }, }) - const controlled = createControlledProps(props, [ - Hover({ - style: props.style?.['&:hover'], - transform: props.transform?.['&:hover'], - }), - ]) + const controlled = createControlledProps( + props, + [ + Hover({ + style: props.style?.['&:hover'], + transform: props.transform?.['&:hover'], + }), + ], + () => token, + ) const context = useInternalContext() @@ -56,37 +61,36 @@ const createPath2D = (arg: { const path = createMemo(() => arg.path(controlled.props)) - // const bounds = createBounds(() => arg.bounds(props), context) - const [hover, setHover] = createSignal(false) - let matrix: DOMMatrix + const [matrix, setMatrix] = createSignal(new DOMMatrix()) + const bounds = createBounds(() => arg.bounds(controlled.props), matrix) + + const transformedCallback = createTransformedCallback() const token: Shape2DToken = { type: 'Shape2D', id: arg.id, path, + bounds, render: ctx => { transformedCallback(ctx, controlled.props, () => { + setMatrix(ctx.getTransform()) renderPath(context!, controlled.props, path()) parenthood.render(ctx) controlled.emit.onRender(ctx) - if ( - controlled.props.style.pointerEvents && - context?.flags.shouldHitTest - ) { - matrix = ctx.getTransform() - } + return 0 }) }, debug: ctx => { - // renderPath(context, defaultBoundsProps, bounds().path) + parenthood.debug(ctx) + renderPath(context!, defaultBoundsProps, bounds().path) }, hitTest: event => { if (!event.propagation) return false parenthood.hitTest(event) - event.ctx.setTransform(matrix) + event.ctx.setTransform(matrix()) let hit = false if (controlled.props.style.pointerEvents) { controlled.emit.onHitTest(event) diff --git a/src/utils/createShape2D.ts b/src/utils/createShape2D.ts index d8973ce..41aa966 100644 --- a/src/utils/createShape2D.ts +++ b/src/utils/createShape2D.ts @@ -14,7 +14,7 @@ import { createUpdatedContext } from './createUpdatedContext' import { deepMergeGetters, mergeGetters } from './mergeGetters' import { mergeShape2DProps } from './mergeShape2DProps' import withContext from './withContext' -import { transformedCallback } from './transformedCallback' +import { createTransformedCallback } from './transformedCallback' const createShape2D = < T, @@ -81,6 +81,8 @@ const createShape2D = < let matrix: DOMMatrix + const transformedCallback = createTransformedCallback() + const token: Object2DToken = { type: 'Object2D', id: arg.id, @@ -100,7 +102,10 @@ const createShape2D = < event.ctx.resetTransform() return hit }, - debug: event => path().data.debug(event), + debug: event => { + console.log('this happens?') + path().data.debug(event) + }, render: ctx => { if (!arg.dimensions) return diff --git a/src/utils/transformedCallback.ts b/src/utils/transformedCallback.ts index e464267..d4151cf 100644 --- a/src/utils/transformedCallback.ts +++ b/src/utils/transformedCallback.ts @@ -1,44 +1,60 @@ -import { Transforms } from 'src/types' - -let matrix: DOMMatrix, matrix2: DOMMatrix, result: any - -const transformedCallback = ( - ctx: CanvasRenderingContext2D, - props: { transform?: Transforms }, - callback: () => T, -): T => { - if (!props.transform) { - return callback() - } else if (props.transform.skew) { - matrix = ctx.getTransform() - matrix2 = ctx.getTransform() - matrix.translateSelf( - props.transform.position?.x ?? 0, - props.transform.position?.y ?? 0, - ) - matrix.rotateSelf(props.transform.rotation ?? 0) - matrix.skewXSelf(props.transform.skew?.x ?? 0) - matrix.skewYSelf(props.transform.skew?.y ?? 0) - ctx.setTransform(matrix) - result = callback() - ctx.setTransform(matrix2) - return result - } else { - ctx.translate( - props.transform.position?.x ?? 0, - props.transform.position?.y ?? 0, - ) - ctx.rotate(props.transform.rotation ?? 0) - - result = callback() - - ctx.translate( - (props.transform.position?.x ?? 0) * -1, - (props.transform.position?.y ?? 0) * -1, - ) - ctx.rotate((props.transform.rotation ?? 0) * -1) - return result +import { Transforms, Vector } from 'src/types' + +const transferMatrix = (matrix1: DOMMatrix, matrix2: DOMMatrix) => { + matrix1.a = matrix2.a + matrix1.b = matrix2.b + matrix1.c = matrix2.c + matrix1.d = matrix2.d + matrix1.e = matrix2.e + matrix1.f = matrix2.f +} + +const createTransformedCallback = () => { + let matrix = new DOMMatrix() + let matrix2 = new DOMMatrix() + let result: unknown | undefined + let position: Vector | undefined + let rotation: number | undefined + let transform: Partial | undefined + let left: number + let top: number + + return ( + ctx: CanvasRenderingContext2D, + props: { transform?: Transforms }, + callback: () => T, + ) => { + transform = props.transform + position = props.transform?.position + left = position?.x ?? 0 + top = position?.y ?? 0 + rotation = transform?.rotation ?? 0 + + if (!transform) { + return callback() + } else if (transform.skew) { + transferMatrix(matrix2, matrix) + + matrix.translateSelf(left, top) + matrix.rotateSelf(rotation ?? 0) + matrix.skewXSelf(transform.skew?.x ?? 0) + matrix.skewYSelf(transform.skew?.y ?? 0) + + ctx.setTransform(matrix) + result = callback() + ctx.setTransform(matrix2) + + transferMatrix(matrix, matrix2) + + return result + } else { + ctx.translate(left, top) + result = callback() + ctx.translate(left * -1, top * -1) + ctx.rotate(rotation * -1) + return result + } } } -export { transformedCallback } +export { createTransformedCallback } From 201b2b96bc7dad3ea7eecead003a8998397879ac Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:02:08 +0200 Subject: [PATCH 16/31] Canvas: render and debug in two stages --- src/components/Canvas.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 5de9489..6961ef9 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -247,10 +247,12 @@ export const Canvas: Component<{ ctx.restore() forEachReversed(tokens(), token => { - if (props.debug && 'debug' in token.data) token.data.debug(ctx) if ('render' in token.data) token.data.render(ctx) }) - + ctx.resetTransform() + forEachReversed(tokens(), token => { + if (props.debug && 'debug' in token.data) token.data.debug(ctx) + }) if (props.fill) { ctx.save() ctx.globalCompositeOperation = 'destination-over' From 74c1409e809afea3f3fec0776972015e5d8f332f Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:02:45 +0200 Subject: [PATCH 17/31] parenthood: add `.debug` --- src/components/Object2D/Group.tsx | 4 +++- src/utils/createParenthood.ts | 14 ++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/Object2D/Group.tsx b/src/components/Object2D/Group.tsx index 4d95d3c..e6869c3 100644 --- a/src/components/Object2D/Group.tsx +++ b/src/components/Object2D/Group.tsx @@ -56,7 +56,9 @@ const Group = createToken(parser, (props: Object2DProps) => { ) ctx.rotate((props.transform?.rotation ?? 0) * -1) }, - debug: () => {}, + debug: ctx => { + parenthood.debug(ctx) + }, hitTest: event => { if (!event.propagation) return diff --git a/src/utils/createParenthood.ts b/src/utils/createParenthood.ts index 445c315..db6daea 100644 --- a/src/utils/createParenthood.ts +++ b/src/utils/createParenthood.ts @@ -67,6 +67,9 @@ function createParenthood( forEachReversed(tokens(), ({ data }) => { if ('render' in data) data.render?.(ctx) }) + } + + const debug = (ctx: CanvasRenderingContext2D) => { forEachReversed(tokens(), ({ data }) => { if ('debug' in data && context.debug) data.debug(ctx) }) @@ -85,19 +88,10 @@ function createParenthood( } } }) - /* forEachReversed(tokens(), token => { - if (!event.propagation) return - if ('hitTest' in token.data) { - hitTestHit = token.data.hitTest(event) - if (hitTestHit) { - hitTestResult.push(token) - } - } - }) */ return hitTestHit } - return { render, hitTest } + return { render, hitTest, debug } } export { createParenthood } From 023e21a15b9f16f4229fdffa664b3be360cf0f1a Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:03:42 +0200 Subject: [PATCH 18/31] init and externalize variables `mergeGetters` --- src/utils/mergeGetters.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/utils/mergeGetters.ts b/src/utils/mergeGetters.ts index d363cca..694c6d0 100644 --- a/src/utils/mergeGetters.ts +++ b/src/utils/mergeGetters.ts @@ -9,6 +9,8 @@ function mergeGetters(a: A, b: B) { return result as A & Omit } +// a bit unorthodox maybe, but since deepMergeGetters can run a lot, I believe it's worth it. +let prop, props1, props2, descriptor1, descriptor2 function deepMergeGetters< T extends { [key: string]: any }, U extends { [key: string]: any }, @@ -21,13 +23,12 @@ function deepMergeGetters< } const result = {} as T & U + props1 = Object.getOwnPropertyNames(obj1) + props2 = Object.getOwnPropertyNames(obj2) - const props1 = Object.getOwnPropertyNames(obj1) - const props2 = Object.getOwnPropertyNames(obj2) - let prop: keyof T & keyof U for (prop of [...props1, ...props2]) { - const descriptor1 = Object.getOwnPropertyDescriptor(obj1, prop) - const descriptor2 = Object.getOwnPropertyDescriptor(obj2, prop) + descriptor1 = Object.getOwnPropertyDescriptor(obj1, prop) + descriptor2 = Object.getOwnPropertyDescriptor(obj2, prop) if (descriptor2 && descriptor2.get) { Object.defineProperty(result, prop, descriptor2) } else if (typeof obj2[prop] === 'object') { From 07adf4686e10366fcb684d94013f83c8d060f14f Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:06:10 +0200 Subject: [PATCH 19/31] add bounds to `CanvasToken`-type --- src/parser.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index a6ef2db..b98b496 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,10 +1,15 @@ import { createTokenizer, TokenElement } from '@solid-primitives/jsx-tokenizer' import { Accessor } from 'solid-js' -import { CanvasMouseEvent } from './types' +import { CanvasMouseEvent, Dimensions, Vector } from './types' export type Shape2DToken = { type: 'Shape2D' id: string + bounds: Accessor<{ + path: Path2D + dimensions: Dimensions + position: Vector + }> render: (ctx: CanvasRenderingContext2D) => void debug: (ctx: CanvasRenderingContext2D) => void hitTest: (event: CanvasMouseEvent) => boolean From 7953c34097cccad80c11af7f8dca9a294cc636fd Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:06:54 +0200 Subject: [PATCH 20/31] add `.token` `Shape2DProps.controllers`-type --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index 8d4328a..80fd7c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,6 +39,7 @@ export type Shape2DProps = CanvasMouseEvents & { controllers?: (( props: Accessor>, events: RegisterControllerEvents, + token: Accessor, ) => T | Shape2DProps)[] } From 442df32d62b68ce93c8ff8a0f82461f2d472f340 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:07:07 +0200 Subject: [PATCH 21/31] update defaultBoundsProps --- src/defaultProps.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/defaultProps.ts b/src/defaultProps.ts index 75a8b33..b044529 100644 --- a/src/defaultProps.ts +++ b/src/defaultProps.ts @@ -26,14 +26,15 @@ const defaultShape2DProps: ResolvedShape2DProps = { const defaultBoundsProps: ResolvedShape2DProps = { style: { stroke: 'grey', - fill: 'transparent', + // fill: 'black', + fill: false, lineDash: [], lineCap: 'butt', lineJoin: 'round', miterLimit: 10, - lineWidth: 0.5, + lineWidth: 2, opacity: 1, - composite: 'destination-over', + composite: 'source-over', cursor: undefined, pointerEvents: false, }, From 94f02ee6614be660fe5d983e93a5ab976da0f058 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:07:24 +0200 Subject: [PATCH 22/31] update `Rectangles`-example --- dev/pages/rectangles.tsx | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/dev/pages/rectangles.tsx b/dev/pages/rectangles.tsx index 73fc62f..c2e8735 100644 --- a/dev/pages/rectangles.tsx +++ b/dev/pages/rectangles.tsx @@ -38,29 +38,29 @@ const App: Component = () => { draggable > ({ - position: { - x: Math.random() * (window.innerWidth + 200) - 100, - y: Math.random() * (window.innerHeight + 200) - 100, + each={new Array(600).fill('').map(v => ({ + transform: { + position: { + x: Math.random() * (window.innerWidth + 200) - 100, + y: Math.random() * (window.innerHeight + 200) - 100, + }, + skew: { + y: Math.random() * 90, + }, }, - fill: { - r: Math.random() * 215, - g: Math.random() * 215, - b: Math.random() * 215, + style: { + fill: { + r: Math.random() * 215, + g: Math.random() * 215, + b: Math.random() * 215, + }, + stroke: false, + dimensions: { width: 100, height: 100 }, + composite: 'hard-light' as const, }, - skewY: Math.random() * 90, }))} > - {data => ( - - )} + {data => } From a421f802f467ade006af773d51cbc5bb83a8e930 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:07:37 +0200 Subject: [PATCH 23/31] update `Smileys`-example --- dev/pages/Smileys.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/pages/Smileys.tsx b/dev/pages/Smileys.tsx index 70d3239..ae4b5af 100644 --- a/dev/pages/Smileys.tsx +++ b/dev/pages/Smileys.tsx @@ -95,9 +95,9 @@ const App: Component = () => { })` const clock = createClock() - clock.start() + clock.start(1000 / 30) - const [amount, setAmount] = createSignal(1) + const [amount, setAmount] = createSignal(300) const [shouldUseClock, setShouldUseClock] = createSignal(false) return ( From d638cab1f09e95fd42c18560269e47ee173ca6a8 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:08:06 +0200 Subject: [PATCH 24/31] update `main.yml` --- .github/workflows/main.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..fe96d8a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,19 @@ +name: CI + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: deploy pages + uses: JamesIves/github-pages-deploy-action@v4.4.1 + with: + branch: gh-pages + folder: dev/dist From dbc7b2008c9dc4af5b1b0d35f2936d42a29d3028 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:12:17 +0200 Subject: [PATCH 25/31] add `token` to clickStyle-controller (fix) --- src/controllers/ClickStyle.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/controllers/ClickStyle.ts b/src/controllers/ClickStyle.ts index 3d08578..a84ca8f 100644 --- a/src/controllers/ClickStyle.ts +++ b/src/controllers/ClickStyle.ts @@ -8,20 +8,22 @@ type HoverOptions = { stroke?: ExtendedColor fill?: ExtendedColor } -const ClickStyle = createController((props, events, options) => { - const [selected, setSelected] = createSignal(false) +const ClickStyle = createController( + (props, events, token, options) => { + const [selected, setSelected] = createSignal(false) - events.onMouseDown(() => setSelected(true)) - events.onMouseUp(() => setSelected(false)) + events.onMouseDown(() => setSelected(true)) + events.onMouseUp(() => setSelected(false)) - return mergeGetters(props(), { - get stroke() { - return selected() ? options.stroke : props().style.stroke - }, - get fill() { - return selected() ? options.fill : props().style.fill - }, - }) -}) + return mergeGetters(props(), { + get stroke() { + return selected() ? options.stroke : props().style.stroke + }, + get fill() { + return selected() ? options.fill : props().style.fill + }, + }) + }, +) export { ClickStyle } From e11b4b72f15e9bb1aab574f572f8343e0e2932d6 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:19:42 +0200 Subject: [PATCH 26/31] `mergeGetters`: fix types --- src/utils/mergeGetters.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/utils/mergeGetters.ts b/src/utils/mergeGetters.ts index 694c6d0..d3959d6 100644 --- a/src/utils/mergeGetters.ts +++ b/src/utils/mergeGetters.ts @@ -26,21 +26,22 @@ function deepMergeGetters< props1 = Object.getOwnPropertyNames(obj1) props2 = Object.getOwnPropertyNames(obj2) - for (prop of [...props1, ...props2]) { + for (prop of [...props1, ...props2] as (keyof T & U)[]) { descriptor1 = Object.getOwnPropertyDescriptor(obj1, prop) descriptor2 = Object.getOwnPropertyDescriptor(obj2, prop) if (descriptor2 && descriptor2.get) { Object.defineProperty(result, prop, descriptor2) - } else if (typeof obj2[prop] === 'object') { - result[prop] = deepMergeGetters(obj1[prop], obj2[prop]) - } else if (obj2[prop]) { - result[prop] = obj2[prop] + } else if (typeof obj2[prop as string] === 'object') { + result[prop] = deepMergeGetters(obj1[prop], obj2[prop as string]) + } else if (obj2[prop as string]) { + result[prop] = obj2[prop as string] } else if (descriptor1 && descriptor1.get) { Object.defineProperty(result, prop, descriptor1) } else if (typeof obj1[prop] === 'object') { - result[prop] = deepMergeGetters(obj1[prop], obj2[prop]) + result[prop] = deepMergeGetters(obj1[prop], obj2[prop as string]) } else { - result[prop] = obj2[prop] !== undefined ? obj2[prop] : obj1[prop] + result[prop] = + obj2[prop as string] !== undefined ? obj2[prop as string] : obj1[prop] } } From 4c3156405297cc51a228b03e492660e554d12d82 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:20:08 +0200 Subject: [PATCH 27/31] `createShape2D`: update createControlledProps --- src/utils/createShape2D.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/createShape2D.ts b/src/utils/createShape2D.ts index 41aa966..bce37b1 100644 --- a/src/utils/createShape2D.ts +++ b/src/utils/createShape2D.ts @@ -36,10 +36,10 @@ const createShape2D = < defaultValues: U }) => { const controlled = createControlledProps( - // TODO: fix any deepMergeGetters(arg.defaultValues, arg.props), + [], + () => token, ) - // const matrix = createMatrix(() => arg.props) const context = createUpdatedContext(() => controlled.props) const parenthood = createParenthood(arg.props, context) @@ -103,7 +103,6 @@ const createShape2D = < return hit }, debug: event => { - console.log('this happens?') path().data.debug(event) }, render: ctx => { From b92923851df234240bc39943939cf26335e6ef13 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:20:31 +0200 Subject: [PATCH 28/31] `createPath2D` make style-prop optional --- src/utils/createPath2D.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/createPath2D.ts b/src/utils/createPath2D.ts index 69f3aeb..b29e503 100644 --- a/src/utils/createPath2D.ts +++ b/src/utils/createPath2D.ts @@ -25,7 +25,7 @@ import { useInternalContext } from 'src/context/InternalContext' import { createTransformedCallback } from './transformedCallback' import { createBounds } from './createBounds' -const createPath2D = (arg: { +const createPath2D = (arg: { id: string props: Shape2DProps & T defaultStyle: RequireOptionals From 3c82d906295b3587ee51f18b9af0b79c0535b686 Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:20:43 +0200 Subject: [PATCH 29/31] `Line` make style-prop optional --- src/components/Object2D/Shape2D/Path2D/Line.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Object2D/Shape2D/Path2D/Line.tsx b/src/components/Object2D/Shape2D/Path2D/Line.tsx index fd7b4a1..bc769cc 100644 --- a/src/components/Object2D/Shape2D/Path2D/Line.tsx +++ b/src/components/Object2D/Shape2D/Path2D/Line.tsx @@ -6,7 +6,7 @@ import { createPath2D } from '../../../../utils/createPath2D' type LineProps = { points: Vector[] - style: { + style?: { close?: boolean } } @@ -18,7 +18,9 @@ type LineProps = { const Line = createToken( parser, (props: Shape2DProps & LineProps) => { - let path2D: Path2D, point: Vector | undefined, index: number + let path2D: Path2D + let point: Vector | undefined + let index: number return createPath2D({ id: 'Line', props, From 37dbdde2de1b2e6cbc3bb4f652107cfce8ca551a Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:20:59 +0200 Subject: [PATCH 30/31] `Group` remove unnecessary imports --- src/components/Object2D/Group.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Object2D/Group.tsx b/src/components/Object2D/Group.tsx index e6869c3..82947ab 100644 --- a/src/components/Object2D/Group.tsx +++ b/src/components/Object2D/Group.tsx @@ -6,9 +6,7 @@ import { RegisterControllerEvents } from 'src/controllers/controllers' import { CanvasToken, parser } from 'src/parser' import { Color, Object2DProps, ResolvedShape2DProps, Vector } from 'src/types' import { createParenthood } from 'src/utils/createParenthood' -import { transformedCallback } from 'src/utils/transformedCallback' import { SingleOrArray } from 'src/utils/typehelpers' -import { T } from 'vitest/dist/types-c800444e' export type GroupProps = { children: SingleOrArray From 37428c70a52c9c8fb73c0596ca53ecf796776efe Mon Sep 17 00:00:00 2001 From: bigmistqke Date: Sat, 22 Apr 2023 16:21:16 +0200 Subject: [PATCH 31/31] add `vite:build` to scripts --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 4ef23f0..3bd9413 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ }, "scripts": { "dev": "vite serve dev", + "dev:build": "vite build dev", "build": "tsup", "test": "concurrently pnpm:test:*", "test:client": "vitest",