From 18d5fe6b31a2ebb9fc6245f533f624f5fb3cc3bd Mon Sep 17 00:00:00 2001 From: yonava Date: Sat, 9 Aug 2025 19:50:16 -0400 Subject: [PATCH 1/6] consolidated stroke application --- .../src/playground/shape/ShapePlayground.vue | 70 ++++++------------- .../src/shapes/animation/timeline/compile.ts | 8 +-- client/src/shapes/helpers.ts | 12 ++++ client/src/shapes/shapes/ellipse/draw.ts | 12 +--- client/src/shapes/shapes/rect/draw.ts | 13 +--- client/src/shapes/shapes/triangle/draw.ts | 12 +--- 6 files changed, 43 insertions(+), 84 deletions(-) diff --git a/client/src/playground/shape/ShapePlayground.vue b/client/src/playground/shape/ShapePlayground.vue index 947ce2cd..acc64cb6 100644 --- a/client/src/playground/shape/ShapePlayground.vue +++ b/client/src/playground/shape/ShapePlayground.vue @@ -3,67 +3,41 @@ import { useMagicCanvas } from '@canvas/index'; import colors from '@colors'; import { useAnimatedShapes } from '@shape/animation'; - import type { Shape } from '@shape/types'; import { cross } from '@shapes/cross'; import Button from '@ui/core/button/Button.vue'; - import { ref } from 'vue'; - const { defineTimeline, shapes } = useAnimatedShapes(); const { play, stop, pause, resume } = defineTimeline({ - forShapes: ['line'], - durationMs: 4000, + forShapes: ['circle'], + durationMs: 3000, customInterpolations: { - fillGradient: { - value: (p) => [ - { - color: 'red', - offset: 0, - }, - { - color: 'red', - offset: p < 0.5 ? p * 2 : 2 - p * 2, - }, - { - color: 'black', - offset: p < 0.5 ? p * 2 : 2 - p * 2, - }, - ], + stroke: { + value: (p) => ({ + lineWidth: 10 + (p < 0.5 ? p * 10 : 10 - p * 10), + color: 'red', + dash: [10, 10 + (p < 0.5 ? p * 10 : 10 - p * 10)], + }), }, }, - keyframes: [ - { - progress: 0.5, - properties: { - end: { x: 50, y: 300 }, - start: { x: 250, y: 0 }, - lineWidth: 50, - textArea: (ta) => ({ - textBlock: { - fontSize: ta.textBlock.fontSize + 12, - }, - }), - }, - }, - ], + keyframes: [], }); - const paintedShapes = ref([]); - - paintedShapes.value.push( - shapes.line({ - id: 'test', - start: { x: 0, y: 0 }, - end: { x: 200, y: 200 }, - textArea: { textBlock: { content: 'real' } }, - fillColor: 'purple', - }), - ); + const cir = shapes.circle({ + id: 'test', + at: { x: 0, y: 0 }, + radius: 50, + stroke: { + color: 'red', + lineWidth: 10, + }, + textArea: { textBlock: { content: 'real' } }, + }); const magic = useMagicCanvas(); - magic.draw.content.value = (ctx) => - paintedShapes.value.forEach((i) => i.draw(ctx)); + magic.draw.content.value = (ctx) => { + cir.draw(ctx); + }; magic.draw.backgroundPattern.value = (ctx, at) => { cross({ diff --git a/client/src/shapes/animation/timeline/compile.ts b/client/src/shapes/animation/timeline/compile.ts index 9f51c2d0..98014b55 100644 --- a/client/src/shapes/animation/timeline/compile.ts +++ b/client/src/shapes/animation/timeline/compile.ts @@ -200,10 +200,10 @@ export const compileTimeline = (timeline: Timeline): CompiledTimeline => { }; }); - // @ts-expect-error could make TS happy, but would make this verbose unfortunately return interpolation.fn( keyframes, getDefaultEasing(propName), + // @ts-expect-error could make TS happy, but would make this verbose unfortunately rawPropVal, )(progress); }; @@ -216,12 +216,10 @@ export const compileTimeline = (timeline: Timeline): CompiledTimeline => { ImperativeTrack, ][]; for (const [propName, interpolationOptions] of allCustomInterpolations) { - if (!interpolationOptions) - throw 'custom path received with no options. this should never happen!'; const { easing: easingRaw, value } = interpolationOptions; const easing = easingRaw ?? getDefaultEasing(propName); - tl.properties[propName] = (_, progress) => - value(easingOptionToFunction(easing)(progress)); + const easingFn = easingOptionToFunction(easing); + tl.properties[propName] = (_, progress) => value(easingFn(progress)); } } diff --git a/client/src/shapes/helpers.ts b/client/src/shapes/helpers.ts index 2dd798c7..500f7aee 100644 --- a/client/src/shapes/helpers.ts +++ b/client/src/shapes/helpers.ts @@ -7,6 +7,7 @@ import type { BoundingBox, Coordinate, GradientStop, + Stroke, } from './types/utility'; /** @@ -355,3 +356,14 @@ export const toBorderRadiusArray = ( ? [borderRadius, borderRadius, borderRadius, borderRadius] : borderRadius; }; + +export const drawStrokeOntoShape = (ctx: CanvasRenderingContext2D, stroke: Stroke) => { + const { color, lineWidth: width, dash = [] } = stroke; + ctx.strokeStyle = color; + ctx.lineWidth = width; + // setLineDash does not support passing readonly arrays in ts! + // safe assertion since setLineDash does not perform mutations + ctx.setLineDash(dash as number[]); + ctx.stroke(); + ctx.setLineDash([]); +} \ No newline at end of file diff --git a/client/src/shapes/shapes/ellipse/draw.ts b/client/src/shapes/shapes/ellipse/draw.ts index a3651ef1..f91f44a1 100644 --- a/client/src/shapes/shapes/ellipse/draw.ts +++ b/client/src/shapes/shapes/ellipse/draw.ts @@ -1,3 +1,4 @@ +import { drawStrokeOntoShape } from '@shape/helpers'; import type { EllipseSchemaWithDefaults } from './defaults'; export const drawEllipseWithCtx = @@ -9,16 +10,7 @@ export const drawEllipseWithCtx = ctx.fillStyle = color; ctx.fill(); - if (stroke) { - const { color, lineWidth: width, dash = [] } = stroke; - ctx.strokeStyle = color; - ctx.lineWidth = width; - // setLineDash does not support passing readonly arrays in ts! - // safe assertion since setLineDash does not perform mutations - ctx.setLineDash(dash as number[]); - ctx.stroke(); - ctx.setLineDash([]); - } + if (stroke) drawStrokeOntoShape(ctx, stroke) ctx.closePath(); }; diff --git a/client/src/shapes/shapes/rect/draw.ts b/client/src/shapes/shapes/rect/draw.ts index b06015ff..7dbb9e15 100644 --- a/client/src/shapes/shapes/rect/draw.ts +++ b/client/src/shapes/shapes/rect/draw.ts @@ -1,4 +1,4 @@ -import { normalizeBoundingBox, toBorderRadiusArray } from '@shape/helpers'; +import { drawStrokeOntoShape, normalizeBoundingBox, toBorderRadiusArray } from '@shape/helpers'; import type { RectSchemaWithDefaults } from './defaults'; @@ -113,16 +113,7 @@ export const drawRectWithCtx = ctx.fill(); } - if (stroke) { - const { color, lineWidth, dash = [] } = stroke; - ctx.strokeStyle = color; - ctx.lineWidth = lineWidth; - // setLineDash does not support passing readonly arrays in ts! - // safe assertion since setLineDash does not perform mutations - ctx.setLineDash(dash as number[]); - ctx.stroke(); - ctx.setLineDash([]); - } + if (stroke) drawStrokeOntoShape(ctx, stroke) ctx.restore(); }; diff --git a/client/src/shapes/shapes/triangle/draw.ts b/client/src/shapes/shapes/triangle/draw.ts index 0bcdfc4b..1027c619 100644 --- a/client/src/shapes/shapes/triangle/draw.ts +++ b/client/src/shapes/shapes/triangle/draw.ts @@ -1,3 +1,4 @@ +import { drawStrokeOntoShape } from '@shape/helpers'; import type { TriangleSchemaWithDefaults } from './defaults'; export const drawTriangleWithCtx = @@ -31,14 +32,5 @@ export const drawTriangleWithCtx = ctx.fill(); ctx.closePath(); - if (stroke) { - ctx.lineWidth = stroke.lineWidth; - ctx.strokeStyle = stroke.color; - // setLineDash does not support passing readonly arrays in ts! - // safe assertion since setLineDash does not perform mutations - ctx.setLineDash((stroke.dash ?? []) as number[]); - ctx.stroke(); - - ctx.setLineDash([]); - } + if (stroke) drawStrokeOntoShape(ctx, stroke) }; From 616df2a2342bf6e1f5257b1ae8900244154d56c8 Mon Sep 17 00:00:00 2001 From: yonava Date: Sat, 9 Aug 2025 21:09:27 -0400 Subject: [PATCH 2/6] much better define timeline ts --- .../src/playground/shape/ShapePlayground.vue | 32 +++++++-- client/src/shapes/animation/index.ts | 60 ++++++++-------- .../src/shapes/animation/timeline/compile.ts | 4 +- .../src/shapes/animation/timeline/define.ts | 69 ++++++------------- client/src/shapes/defaults/shapes.ts | 8 ++- client/src/shapes/helpers.ts | 20 ++++-- client/src/shapes/types/utility.ts | 8 ++- 7 files changed, 110 insertions(+), 91 deletions(-) diff --git a/client/src/playground/shape/ShapePlayground.vue b/client/src/playground/shape/ShapePlayground.vue index acc64cb6..5f3cd43a 100644 --- a/client/src/playground/shape/ShapePlayground.vue +++ b/client/src/playground/shape/ShapePlayground.vue @@ -10,17 +10,21 @@ const { play, stop, pause, resume } = defineTimeline({ forShapes: ['circle'], - durationMs: 3000, + durationMs: 6000, customInterpolations: { stroke: { value: (p) => ({ lineWidth: 10 + (p < 0.5 ? p * 10 : 10 - p * 10), color: 'red', - dash: [10, 10 + (p < 0.5 ? p * 10 : 10 - p * 10)], + dash: { + pattern: [40, 22.832], + // circum = 2 pi r = 2 pi 50 = 100 pi + offset: p * (100 * Math.PI), + }, }), }, }, - keyframes: [], + keyframes: [{ progress: 0.5, properties: { radius: 100 } }], }); const cir = shapes.circle({ @@ -28,15 +32,35 @@ at: { x: 0, y: 0 }, radius: 50, stroke: { + lineWidth: 10, color: 'red', + dash: { + pattern: [40, 22.832], + offset: 0, + }, + }, + textArea: { textBlock: { content: '1' } }, + }); + + const cir2 = shapes.circle({ + id: 'test2', + at: { x: 200, y: 0 }, + radius: 50, + stroke: { lineWidth: 10, + color: 'red', + dash: { + pattern: [40, 22.832], + offset: 90, + }, }, - textArea: { textBlock: { content: 'real' } }, + textArea: { textBlock: { content: '2' } }, }); const magic = useMagicCanvas(); magic.draw.content.value = (ctx) => { cir.draw(ctx); + cir2.draw(ctx); }; magic.draw.backgroundPattern.value = (ctx, at) => { diff --git a/client/src/shapes/animation/index.ts b/client/src/shapes/animation/index.ts index 357d5dde..ed0d36c0 100644 --- a/client/src/shapes/animation/index.ts +++ b/client/src/shapes/animation/index.ts @@ -1,4 +1,4 @@ -import { shapeDefaults } from '@shape/defaults/shapes'; +import { getSchemaWithDefaults } from '@shape/defaults/shapes'; import type { EverySchemaPropName, SchemaId, @@ -127,44 +127,44 @@ export const useAnimatedShapes = () => { factory: ShapeFactory, shapeName: ShapeName, ): ShapeFactory> => - (schema) => - new Proxy(factory(schema), { - get: (target, rawProp) => { - const prop = rawProp as keyof Shape; - if (!shapeProps.has(prop)) return target[prop]; + (schema) => + new Proxy(factory(schema), { + get: (target, rawProp) => { + const prop = rawProp as keyof Shape; + if (!shapeProps.has(prop)) return target[prop]; - const animations = activeAnimations.get(schema.id); + const animations = activeAnimations.get(schema.id); - const defaultResolver: - | ((schema: LooseSchema) => LooseSchema) - | undefined = (shapeDefaults as any)?.[shapeName]; - if (!defaultResolver) - throw new Error(`cant find defaults for ${shapeName}`); - const schemaWithDefaults = defaultResolver(schema); + const defaultResolver: + | ((schema: LooseSchema) => LooseSchema) + | undefined = (getSchemaWithDefaults as any)?.[shapeName]; + if (!defaultResolver) + throw new Error(`cant find defaults for ${shapeName}`); + const schemaWithDefaults = defaultResolver(schema); - autoAnimate.captureSchemaState(schemaWithDefaults, shapeName); + autoAnimate.captureSchemaState(schemaWithDefaults, shapeName); - const targetMapSchema = autoAnimate.snapshotMap.get(schema.id); - if (targetMapSchema) - return factory(targetMapSchema as WithId)[prop]; + const targetMapSchema = autoAnimate.snapshotMap.get(schema.id); + if (targetMapSchema) + return factory(targetMapSchema as WithId)[prop]; - if (!animations || animations.length === 0) return target[prop]; - if (!animations[0]?.schema) animations[0].schema = schemaWithDefaults; + if (!animations || animations.length === 0) return target[prop]; + if (!animations[0]?.schema) animations[0].schema = schemaWithDefaults; - if (prop === 'startTextAreaEdit') - return console.warn( - 'shapes with active animations cannot spawn text inputs', - ); + if (prop === 'startTextAreaEdit') + return console.warn( + 'shapes with active animations cannot spawn text inputs', + ); - const hasShapeName = schemaIdToShapeName.get(schema.id); - if (!hasShapeName) schemaIdToShapeName.set(schema.id, shapeName); + const hasShapeName = schemaIdToShapeName.get(schema.id); + if (!hasShapeName) schemaIdToShapeName.set(schema.id, shapeName); - const animatedSchema = getAnimatedSchema(schema.id); - if (!animatedSchema) return target[prop]; + const animatedSchema = getAnimatedSchema(schema.id); + if (!animatedSchema) return target[prop]; - return factory(animatedSchema as WithId)[prop]; - }, - }); + return factory(animatedSchema as WithId)[prop]; + }, + }); return { shapes: { diff --git a/client/src/shapes/animation/timeline/compile.ts b/client/src/shapes/animation/timeline/compile.ts index 98014b55..860d8a25 100644 --- a/client/src/shapes/animation/timeline/compile.ts +++ b/client/src/shapes/animation/timeline/compile.ts @@ -213,13 +213,13 @@ export const compileTimeline = (timeline: Timeline): CompiledTimeline => { if (customInterpolations) { const allCustomInterpolations = Object.entries(customInterpolations) as [ EverySchemaPropName, - ImperativeTrack, + ImperativeTrack, ][]; for (const [propName, interpolationOptions] of allCustomInterpolations) { const { easing: easingRaw, value } = interpolationOptions; const easing = easingRaw ?? getDefaultEasing(propName); const easingFn = easingOptionToFunction(easing); - tl.properties[propName] = (_, progress) => value(easingFn(progress)); + tl.properties[propName] = (schemaWithDefaults, progress) => value(easingFn(progress), schemaWithDefaults); } } diff --git a/client/src/shapes/animation/timeline/define.ts b/client/src/shapes/animation/timeline/define.ts index 72d18760..a2f6a3d9 100644 --- a/client/src/shapes/animation/timeline/define.ts +++ b/client/src/shapes/animation/timeline/define.ts @@ -1,14 +1,14 @@ -import type { TextArea } from '@shape/text/types'; // @typescript-eslint/no-unused-vars reports unused even if referenced in jsdoc // eslint-disable-next-line import type { EverySchemaProp, ShapeNameToSchema, WithId } from '@shape/types'; import { generateId } from '@utils/id'; -import type { DeepPartial, DeepRequired } from 'ts-essentials'; +import type { DeepRequired } from 'ts-essentials'; import type { DeepReadonly } from 'vue'; import type { EasingOption } from '../easing'; import { type CompiledTimeline, compileTimeline } from './compile'; +import type { SchemaWithDefaults } from '@shape/defaults/shapes'; type ShapeTarget = { /** @@ -64,62 +64,37 @@ type TimelineControls = { dispose: () => void; }; -type SharedSchemaProps = - keyof ShapeNameToSchema[T] & - { - [K in keyof ShapeNameToSchema[T]]: T extends any - ? K extends keyof ShapeNameToSchema[T] - ? unknown - : never - : never; - }[keyof ShapeNameToSchema[T]]; - -type SchemaProps = Pick< - ShapeNameToSchema[T], - SharedSchemaProps ->; - -type InterceptedSchemaProps = { - textArea: DeepPartial