sketchmark 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/ANIMATABLE_MATRIX.md +177 -0
  2. package/KERNEL_SPEC.md +412 -0
  3. package/PACKS.md +81 -0
  4. package/PRESETS.md +182 -0
  5. package/README.md +274 -188
  6. package/bin/editor-ui.cjs +2285 -0
  7. package/bin/preview-ui.cjs +74 -0
  8. package/bin/sketchmark.cjs +648 -2008
  9. package/dist/src/animatable.d.ts +21 -0
  10. package/dist/src/animatable.js +439 -0
  11. package/dist/src/builders/index.d.ts +1 -11
  12. package/dist/src/builders/index.js +1 -19
  13. package/dist/src/diagnostics.js +1 -64
  14. package/dist/src/edit.d.ts +27 -0
  15. package/dist/src/edit.js +162 -0
  16. package/dist/src/index.d.ts +4 -13
  17. package/dist/src/index.js +4 -13
  18. package/dist/src/keyframes.d.ts +48 -0
  19. package/dist/src/keyframes.js +182 -0
  20. package/dist/src/motion.d.ts +4 -0
  21. package/dist/src/motion.js +262 -0
  22. package/dist/src/normalize.js +120 -151
  23. package/dist/src/presets/characters.d.ts +15 -0
  24. package/dist/src/presets/characters.js +113 -0
  25. package/dist/src/presets/compose.d.ts +5 -0
  26. package/dist/src/presets/compose.js +80 -0
  27. package/dist/src/presets/effects.d.ts +40 -0
  28. package/dist/src/presets/effects.js +79 -0
  29. package/dist/src/presets/helpers.d.ts +33 -0
  30. package/dist/src/presets/helpers.js +165 -0
  31. package/dist/src/presets/index.d.ts +9 -0
  32. package/dist/src/presets/index.js +48 -0
  33. package/dist/src/presets/motions.d.ts +33 -0
  34. package/dist/src/presets/motions.js +75 -0
  35. package/dist/src/presets/scenes.d.ts +35 -0
  36. package/dist/src/presets/scenes.js +134 -0
  37. package/dist/src/presets/shapes.d.ts +71 -0
  38. package/dist/src/presets/shapes.js +96 -0
  39. package/dist/src/presets/transitions.d.ts +29 -0
  40. package/dist/src/presets/transitions.js +113 -0
  41. package/dist/src/presets/types.d.ts +34 -0
  42. package/dist/src/presets/types.js +2 -0
  43. package/dist/src/render/html.js +1 -4
  44. package/dist/src/render/svg.d.ts +2 -2
  45. package/dist/src/render/svg.js +86 -82
  46. package/dist/src/render/three-html.js +67 -113
  47. package/dist/src/scenes.js +1 -0
  48. package/dist/src/schema.js +218 -280
  49. package/dist/src/shapes/builtins.js +11 -47
  50. package/dist/src/shapes/common.js +12 -11
  51. package/dist/src/shapes/registry.d.ts +0 -1
  52. package/dist/src/shapes/registry.js +0 -4
  53. package/dist/src/shapes/types.d.ts +1 -3
  54. package/dist/src/types.d.ts +57 -288
  55. package/dist/src/utils.d.ts +2 -11
  56. package/dist/src/utils.js +13 -70
  57. package/dist/src/validate.js +321 -275
  58. package/dist/tests/run.js +576 -510
  59. package/examples/1730642890464.jpg +0 -0
  60. package/examples/app-screen.svg +1 -0
  61. package/examples/app-screen.visual.json +503 -0
  62. package/examples/dashboard-table.svg +1 -0
  63. package/examples/dashboard-table.visual.json +708 -0
  64. package/examples/dev-docs.svg +1 -0
  65. package/examples/dev-docs.visual.json +248 -0
  66. package/examples/explainer.mp4 +0 -0
  67. package/examples/explainer.visual.json +1713 -0
  68. package/examples/group-origin-effects-lab-check.svg +1 -0
  69. package/examples/group-origin-effects-lab.visual.json +1880 -0
  70. package/examples/image-clip-radius.visual.json +271 -0
  71. package/examples/make-app-screen.cjs +368 -0
  72. package/examples/make-dashboard-table.cjs +277 -0
  73. package/examples/make-dev-docs.cjs +233 -0
  74. package/examples/make-explainer.cjs +438 -0
  75. package/examples/make-group-origin-effects-lab.cjs +370 -0
  76. package/examples/make-image-clip-radius.cjs +169 -0
  77. package/examples/make-modal-dialog.cjs +355 -0
  78. package/examples/make-origin-effects-lab.cjs +311 -0
  79. package/examples/make-preset-character-motion.cjs +32 -0
  80. package/examples/make-presets-demo.cjs +30 -0
  81. package/examples/make-pricing.cjs +286 -0
  82. package/examples/make-product-demo.cjs +468 -0
  83. package/examples/make-product-hero.cjs +223 -0
  84. package/examples/make-release-notes.cjs +333 -0
  85. package/examples/make-settings-panel.cjs +435 -0
  86. package/examples/make-split-preview.cjs +248 -0
  87. package/examples/make-storyboard.cjs +215 -0
  88. package/examples/make-transcript.cjs +234 -0
  89. package/examples/make-typography-test.cjs +397 -0
  90. package/examples/make-ui-demo-explainer.cjs +1094 -0
  91. package/examples/make-ui-flow.cjs +762 -0
  92. package/examples/make-walkthrough.cjs +815 -0
  93. package/examples/modal-dialog.svg +1 -0
  94. package/examples/modal-dialog.visual.json +239 -0
  95. package/examples/origin-effects-lab-check.svg +1 -0
  96. package/examples/origin-effects-lab.visual.json +1412 -0
  97. package/examples/preset-character-motion.visual.json +949 -0
  98. package/examples/presets-demo.visual.json +787 -0
  99. package/examples/pricing.svg +1 -0
  100. package/examples/pricing.visual.json +652 -0
  101. package/examples/product-demo.mp4 +0 -0
  102. package/examples/product-demo.visual.json +866 -0
  103. package/examples/product-hero.svg +1 -0
  104. package/examples/product-hero.visual.json +242 -0
  105. package/examples/release-notes.svg +1 -0
  106. package/examples/release-notes.visual.json +467 -0
  107. package/examples/settings-panel.svg +1 -0
  108. package/examples/settings-panel.visual.json +501 -0
  109. package/examples/split-preview.svg +1 -0
  110. package/examples/split-preview.visual.json +124 -0
  111. package/examples/storyboard.svg +1 -0
  112. package/examples/storyboard.visual.json +312 -0
  113. package/examples/transcript.svg +1 -0
  114. package/examples/transcript.visual.json +407 -0
  115. package/examples/typography-indent-check.svg +1 -0
  116. package/examples/typography-lineheight-0.svg +1 -0
  117. package/examples/typography-lineheight-2.svg +1 -0
  118. package/examples/typography-test-check.svg +1 -0
  119. package/examples/typography-test.svg +1 -0
  120. package/examples/typography-test.visual.json +757 -0
  121. package/examples/ui-demo-explainer-billing.svg +1 -0
  122. package/examples/ui-demo-explainer-check.svg +1 -0
  123. package/examples/ui-demo-explainer-save.svg +1 -0
  124. package/examples/ui-demo-explainer-toggle.svg +1 -0
  125. package/examples/ui-demo-explainer.mp4 +0 -0
  126. package/examples/ui-demo-explainer.visual.json +2597 -0
  127. package/examples/ui-flow.mp4 +0 -0
  128. package/examples/ui-flow.visual.json +1211 -0
  129. package/examples/walkthrough.mp4 +0 -0
  130. package/examples/walkthrough.visual.json +1372 -0
  131. package/package.json +52 -52
  132. package/schema/visual.schema.json +1086 -930
@@ -0,0 +1,21 @@
1
+ import type { MotionValue, VisualElement } from "./types";
2
+ export type AnimatableValueKind = "number" | "string" | "numberOrString" | "paint" | "point2" | "numberArray" | "stringArray" | "object";
3
+ export type AnimatableInterpolation = "number" | "color" | "point2" | "numberArray" | "discrete";
4
+ export interface AnimatablePropertySpec {
5
+ property: string;
6
+ valueKind: AnimatableValueKind;
7
+ interpolation: AnimatableInterpolation;
8
+ elementTypes: readonly string[];
9
+ defaultValue?: MotionValue | ((element: VisualElement, property: string) => MotionValue | undefined);
10
+ ensure?: (element: VisualElement, property: string) => void;
11
+ }
12
+ export declare function animatablePropertySpec(element: VisualElement, property: string): AnimatablePropertySpec | undefined;
13
+ export declare function knownAnimatableProperty(property: string): boolean;
14
+ export declare function animatablePropertiesForElement(element: VisualElement): AnimatablePropertySpec[];
15
+ export declare function validateMotionValueForProperty(spec: AnimatablePropertySpec, value: unknown): boolean;
16
+ export declare function isTimelineValue(value: unknown): value is MotionValue;
17
+ export declare function getPropertyValue(element: VisualElement, property: string): MotionValue | undefined;
18
+ export declare function baseValueForProperty(element: VisualElement, property: string): MotionValue | undefined;
19
+ export declare function applyPropertyValue(element: VisualElement, property: string, value: MotionValue): void;
20
+ export declare function parsePropertyPath(property: string): Array<string | number>;
21
+ export declare function conflictWarningsForTracks(trackNames: string[]): string[];
@@ -0,0 +1,439 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.animatablePropertySpec = animatablePropertySpec;
4
+ exports.knownAnimatableProperty = knownAnimatableProperty;
5
+ exports.animatablePropertiesForElement = animatablePropertiesForElement;
6
+ exports.validateMotionValueForProperty = validateMotionValueForProperty;
7
+ exports.isTimelineValue = isTimelineValue;
8
+ exports.getPropertyValue = getPropertyValue;
9
+ exports.baseValueForProperty = baseValueForProperty;
10
+ exports.applyPropertyValue = applyPropertyValue;
11
+ exports.parsePropertyPath = parsePropertyPath;
12
+ exports.conflictWarningsForTracks = conflictWarningsForTracks;
13
+ const utils_1 = require("./utils");
14
+ const POSITION_TYPES = ["path", "point", "text", "image", "group"];
15
+ const VISIBLE_TYPES = ["path", "text", "image", "group"];
16
+ const COMMON_TYPES = ["path", "point", "text", "image", "group"];
17
+ const PAINT_FILL_TYPES = ["path", "text"];
18
+ const PATH_TYPES = ["path"];
19
+ const TEXT_TYPES = ["text"];
20
+ const IMAGE_TYPES = ["image"];
21
+ const BASE_SPECS = [
22
+ { property: "position", valueKind: "point2", interpolation: "point2", elementTypes: POSITION_TYPES, defaultValue: (element) => canUsePosition(element) ? [Number(element.x ?? 0), Number(element.y ?? 0)] : undefined },
23
+ { property: "x", valueKind: "number", interpolation: "number", elementTypes: POSITION_TYPES, defaultValue: 0 },
24
+ { property: "y", valueKind: "number", interpolation: "number", elementTypes: POSITION_TYPES, defaultValue: 0 },
25
+ { property: "rotation", valueKind: "number", interpolation: "number", elementTypes: VISIBLE_TYPES, defaultValue: 0 },
26
+ { property: "scale", valueKind: "number", interpolation: "number", elementTypes: VISIBLE_TYPES, defaultValue: 1 },
27
+ { property: "scaleX", valueKind: "number", interpolation: "number", elementTypes: VISIBLE_TYPES, defaultValue: 1 },
28
+ { property: "scaleY", valueKind: "number", interpolation: "number", elementTypes: VISIBLE_TYPES, defaultValue: 1 },
29
+ { property: "origin", valueKind: "point2", interpolation: "point2", elementTypes: VISIBLE_TYPES },
30
+ { property: "opacity", valueKind: "number", interpolation: "number", elementTypes: COMMON_TYPES, defaultValue: 1 },
31
+ { property: "d", valueKind: "string", interpolation: "discrete", elementTypes: PATH_TYPES },
32
+ { property: "fill", valueKind: "paint", interpolation: "color", elementTypes: PAINT_FILL_TYPES },
33
+ { property: "stroke", valueKind: "paint", interpolation: "color", elementTypes: PATH_TYPES },
34
+ { property: "strokeWidth", valueKind: "number", interpolation: "number", elementTypes: PATH_TYPES, defaultValue: 1 },
35
+ { property: "strokeCap", valueKind: "string", interpolation: "discrete", elementTypes: PATH_TYPES },
36
+ { property: "strokeJoin", valueKind: "string", interpolation: "discrete", elementTypes: PATH_TYPES },
37
+ { property: "miterLimit", valueKind: "number", interpolation: "number", elementTypes: PATH_TYPES },
38
+ { property: "dashArray", valueKind: "numberArray", interpolation: "numberArray", elementTypes: PATH_TYPES },
39
+ { property: "dashOffset", valueKind: "number", interpolation: "number", elementTypes: PATH_TYPES, defaultValue: 0 },
40
+ { property: "drawStart", valueKind: "number", interpolation: "number", elementTypes: PATH_TYPES, defaultValue: 0 },
41
+ { property: "drawEnd", valueKind: "number", interpolation: "number", elementTypes: PATH_TYPES, defaultValue: 1 },
42
+ { property: "text", valueKind: "string", interpolation: "discrete", elementTypes: TEXT_TYPES },
43
+ { property: "lines", valueKind: "stringArray", interpolation: "discrete", elementTypes: TEXT_TYPES },
44
+ { property: "align", valueKind: "string", interpolation: "discrete", elementTypes: TEXT_TYPES },
45
+ { property: "valign", valueKind: "string", interpolation: "discrete", elementTypes: TEXT_TYPES },
46
+ { property: "fontFamily", valueKind: "string", interpolation: "discrete", elementTypes: TEXT_TYPES },
47
+ { property: "fontStyle", valueKind: "string", interpolation: "discrete", elementTypes: TEXT_TYPES },
48
+ { property: "fontSize", valueKind: "number", interpolation: "number", elementTypes: TEXT_TYPES, defaultValue: 16 },
49
+ { property: "lineHeight", valueKind: "number", interpolation: "number", elementTypes: TEXT_TYPES, defaultValue: 1.2 },
50
+ { property: "letterSpacing", valueKind: "number", interpolation: "number", elementTypes: TEXT_TYPES, defaultValue: 0 },
51
+ { property: "maxWidth", valueKind: "number", interpolation: "number", elementTypes: TEXT_TYPES },
52
+ { property: "weight", valueKind: "numberOrString", interpolation: "discrete", elementTypes: TEXT_TYPES, defaultValue: 400 },
53
+ { property: "width", valueKind: "number", interpolation: "number", elementTypes: ["image", "group"], defaultValue: 0 },
54
+ { property: "height", valueKind: "number", interpolation: "number", elementTypes: ["image", "group"], defaultValue: 0 },
55
+ { property: "src", valueKind: "string", interpolation: "discrete", elementTypes: IMAGE_TYPES },
56
+ { property: "fit", valueKind: "string", interpolation: "discrete", elementTypes: IMAGE_TYPES },
57
+ { property: "blendMode", valueKind: "string", interpolation: "discrete", elementTypes: VISIBLE_TYPES },
58
+ { property: "clip.d", valueKind: "string", interpolation: "discrete", elementTypes: VISIBLE_TYPES, ensure: ensureClip },
59
+ { property: "mask.d", valueKind: "string", interpolation: "discrete", elementTypes: VISIBLE_TYPES, ensure: ensureMask },
60
+ { property: "mask.opacity", valueKind: "number", interpolation: "number", elementTypes: VISIBLE_TYPES, defaultValue: 1, ensure: ensureMask }
61
+ ];
62
+ const BASE_BY_PROPERTY = new Map(BASE_SPECS.map((spec) => [spec.property, spec]));
63
+ function animatablePropertySpec(element, property) {
64
+ const base = BASE_BY_PROPERTY.get(property);
65
+ if (base)
66
+ return supportsType(base, element.type) ? base : undefined;
67
+ const dynamic = dynamicPropertySpec(property, element);
68
+ return dynamic && supportsType(dynamic, element.type) ? dynamic : undefined;
69
+ }
70
+ function knownAnimatableProperty(property) {
71
+ return BASE_BY_PROPERTY.has(property) || dynamicPropertySpec(property) !== undefined;
72
+ }
73
+ function animatablePropertiesForElement(element) {
74
+ const out = BASE_SPECS.filter((spec) => supportsType(spec, element.type));
75
+ for (const property of dynamicPropertiesForElement(element)) {
76
+ const spec = animatablePropertySpec(element, property);
77
+ if (spec)
78
+ out.push(spec);
79
+ }
80
+ return out;
81
+ }
82
+ function validateMotionValueForProperty(spec, value) {
83
+ switch (spec.valueKind) {
84
+ case "number":
85
+ return (0, utils_1.isFiniteNumber)(value);
86
+ case "string":
87
+ return typeof value === "string";
88
+ case "numberOrString":
89
+ return (0, utils_1.isFiniteNumber)(value) || typeof value === "string";
90
+ case "paint":
91
+ return isPaintValue(value);
92
+ case "point2":
93
+ return (0, utils_1.isPoint2)(value);
94
+ case "numberArray":
95
+ return isNumberArray(value);
96
+ case "stringArray":
97
+ return isStringArray(value);
98
+ case "object":
99
+ return isJsonObject(value);
100
+ }
101
+ }
102
+ function isTimelineValue(value) {
103
+ return (0, utils_1.isFiniteNumber)(value) || typeof value === "string" || (0, utils_1.isPoint2)(value) || isNumberArray(value) || isStringArray(value) || isJsonObject(value);
104
+ }
105
+ function getPropertyValue(element, property) {
106
+ if (property === "position" && canUsePosition(element))
107
+ return [Number(element.x ?? 0), Number(element.y ?? 0)];
108
+ const stop = parseGradientStopProperty(property);
109
+ if (stop)
110
+ return getGradientStopValue(element, stop);
111
+ const value = getPathValue(element, parsePropertyPath(property));
112
+ return isTimelineValue(value) ? (0, utils_1.clone)(value) : undefined;
113
+ }
114
+ function baseValueForProperty(element, property) {
115
+ const value = getPropertyValue(element, property);
116
+ if (value !== undefined)
117
+ return value;
118
+ const spec = animatablePropertySpec(element, property);
119
+ if (!spec || spec.defaultValue === undefined)
120
+ return undefined;
121
+ return typeof spec.defaultValue === "function" ? spec.defaultValue(element, property) : (0, utils_1.clone)(spec.defaultValue);
122
+ }
123
+ function applyPropertyValue(element, property, value) {
124
+ if (property === "position") {
125
+ if (!(0, utils_1.isPoint2)(value))
126
+ return;
127
+ if (canUsePosition(element)) {
128
+ const record = element;
129
+ record.x = value[0];
130
+ record.y = value[1];
131
+ }
132
+ return;
133
+ }
134
+ const spec = animatablePropertySpec(element, property);
135
+ if (spec?.ensure)
136
+ spec.ensure(element, property);
137
+ const stop = parseGradientStopProperty(property);
138
+ if (stop) {
139
+ setGradientStopValue(element, stop, value);
140
+ return;
141
+ }
142
+ const path = parsePropertyPath(property);
143
+ if (path.length > 1) {
144
+ setPathValue(element, path, (0, utils_1.clone)(value), !!spec?.ensure);
145
+ return;
146
+ }
147
+ element[property] = (0, utils_1.clone)(value);
148
+ }
149
+ function parsePropertyPath(property) {
150
+ return property.split(".").filter(Boolean).map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
151
+ }
152
+ function conflictWarningsForTracks(trackNames) {
153
+ const tracks = new Set(trackNames);
154
+ const warnings = [];
155
+ if (tracks.has("position") && (tracks.has("x") || tracks.has("y")))
156
+ warnings.push("position overlaps with x/y tracks; prefer one representation.");
157
+ if (tracks.has("scale") && (tracks.has("scaleX") || tracks.has("scaleY")))
158
+ warnings.push("scale overlaps with scaleX/scaleY tracks; prefer one representation.");
159
+ for (const root of ["fill", "stroke"]) {
160
+ if (tracks.has(root) && trackNames.some((name) => name.startsWith(`${root}.`)))
161
+ warnings.push(`${root} overlaps with nested ${root}.* tracks; prefer whole-paint switching or nested paint animation, not both.`);
162
+ }
163
+ return warnings;
164
+ }
165
+ function dynamicPropertySpec(property, element) {
166
+ const gradient = parsePaintProperty(property, element);
167
+ if (gradient)
168
+ return gradient;
169
+ if (/^effects\.(blur|brightness|contrast|saturate|hueRotate)$/.test(property)) {
170
+ return { property, valueKind: "number", interpolation: "number", elementTypes: VISIBLE_TYPES, defaultValue: effectDefault(property), ensure: ensureEffects };
171
+ }
172
+ if (/^effects\.shadow\.(dx|dy|blur|opacity)$/.test(property)) {
173
+ return { property, valueKind: "number", interpolation: "number", elementTypes: VISIBLE_TYPES, defaultValue: shadowDefault(property), ensure: ensureShadow };
174
+ }
175
+ if (property === "effects.shadow.color") {
176
+ return { property, valueKind: "string", interpolation: "color", elementTypes: VISIBLE_TYPES, defaultValue: "#000000", ensure: ensureShadow };
177
+ }
178
+ if (/^source\.(x|y|width|height)$/.test(property)) {
179
+ return { property, valueKind: "number", interpolation: "number", elementTypes: IMAGE_TYPES, defaultValue: sourceDefault(property), ensure: ensureImageSource };
180
+ }
181
+ return undefined;
182
+ }
183
+ function parsePaintProperty(property, element) {
184
+ const pattern = /^(fill|stroke)\.(x|y|width|height|opacity)$/.exec(property);
185
+ if (pattern) {
186
+ if (element && !isPaintType(element, pattern[1], "pattern"))
187
+ return undefined;
188
+ return { property, valueKind: "number", interpolation: "number", elementTypes: paintElementTypes(pattern[1]) };
189
+ }
190
+ const rootMatch = /^(fill|stroke)\.(from|to)$/.exec(property);
191
+ if (rootMatch) {
192
+ if (element && !isPaintType(element, rootMatch[1], "linearGradient"))
193
+ return undefined;
194
+ return { property, valueKind: "point2", interpolation: "point2", elementTypes: paintElementTypes(rootMatch[1]) };
195
+ }
196
+ const radialPoint = /^(fill|stroke)\.(center|focus)$/.exec(property);
197
+ if (radialPoint) {
198
+ if (element && !isPaintType(element, radialPoint[1], "radialGradient"))
199
+ return undefined;
200
+ return { property, valueKind: "point2", interpolation: "point2", elementTypes: paintElementTypes(radialPoint[1]) };
201
+ }
202
+ const radialRadius = /^(fill|stroke)\.radius$/.exec(property);
203
+ if (radialRadius) {
204
+ if (element && !isPaintType(element, radialRadius[1], "radialGradient"))
205
+ return undefined;
206
+ return { property, valueKind: "number", interpolation: "number", elementTypes: paintElementTypes(radialRadius[1]), defaultValue: 40 };
207
+ }
208
+ const stop = parseGradientStopProperty(property);
209
+ if (!stop)
210
+ return undefined;
211
+ if (element && !hasGradientStop(element, stop))
212
+ return undefined;
213
+ return {
214
+ property,
215
+ valueKind: stop.channel === "offset" ? "number" : "string",
216
+ interpolation: stop.channel === "offset" ? "number" : "color",
217
+ elementTypes: paintElementTypes(stop.root)
218
+ };
219
+ }
220
+ function dynamicPropertiesForElement(element) {
221
+ const out = [
222
+ "effects.blur",
223
+ "effects.brightness",
224
+ "effects.contrast",
225
+ "effects.saturate",
226
+ "effects.hueRotate",
227
+ "effects.shadow.dx",
228
+ "effects.shadow.dy",
229
+ "effects.shadow.blur",
230
+ "effects.shadow.color",
231
+ "effects.shadow.opacity"
232
+ ];
233
+ if (element.type === "image")
234
+ out.push("source.x", "source.y", "source.width", "source.height");
235
+ for (const root of ["fill", "stroke"]) {
236
+ if (!supportsType({ elementTypes: paintElementTypes(root) }, element.type))
237
+ continue;
238
+ const paint = element[root];
239
+ if (isPatternPaint(paint))
240
+ out.push(`${root}.x`, `${root}.y`, `${root}.width`, `${root}.height`, `${root}.opacity`);
241
+ if (isStructuredPaint(paint)) {
242
+ if (paint.type === "linearGradient")
243
+ out.push(`${root}.from`, `${root}.to`);
244
+ if (paint.type === "radialGradient")
245
+ out.push(`${root}.center`, `${root}.focus`, `${root}.radius`);
246
+ for (let index = 0; index < paint.stops.length; index += 1) {
247
+ out.push(`${root}.stops.${index}.offset`, `${root}.stops.${index}.color`);
248
+ }
249
+ }
250
+ }
251
+ return out;
252
+ }
253
+ function supportsType(spec, type) {
254
+ return spec.elementTypes.includes(type);
255
+ }
256
+ function canUsePosition(element) {
257
+ return element.type === "path" || element.type === "point" || element.type === "text" || element.type === "image" || element.type === "group";
258
+ }
259
+ function paintElementTypes(root) {
260
+ return root === "fill" ? PAINT_FILL_TYPES : PATH_TYPES;
261
+ }
262
+ function getPathValue(target, path) {
263
+ let cursor = target;
264
+ for (const segment of path) {
265
+ if (cursor === undefined || cursor === null)
266
+ return undefined;
267
+ cursor = readSegment(cursor, segment);
268
+ }
269
+ return cursor;
270
+ }
271
+ function setPathValue(target, path, value, create) {
272
+ let cursor = target;
273
+ for (let index = 0; index < path.length - 1; index += 1) {
274
+ const segment = path[index];
275
+ const next = path[index + 1];
276
+ const current = readSegment(cursor, segment);
277
+ if (current === undefined || current === null) {
278
+ if (!create)
279
+ return;
280
+ writeSegment(cursor, segment, typeof next === "number" ? [] : {});
281
+ }
282
+ cursor = readSegment(cursor, segment);
283
+ }
284
+ writeSegment(cursor, path[path.length - 1], value);
285
+ }
286
+ function readSegment(target, segment) {
287
+ if (Array.isArray(target) && typeof segment === "number")
288
+ return target[segment];
289
+ if (isRecord(target))
290
+ return target[String(segment)];
291
+ return undefined;
292
+ }
293
+ function writeSegment(target, segment, value) {
294
+ if (Array.isArray(target) && typeof segment === "number") {
295
+ target[segment] = value;
296
+ return;
297
+ }
298
+ if (isRecord(target))
299
+ target[String(segment)] = value;
300
+ }
301
+ function parseGradientStopProperty(property) {
302
+ const match = /^(fill|stroke)\.stops\.(\d+)\.(offset|color)$/.exec(property);
303
+ if (!match)
304
+ return undefined;
305
+ return { root: match[1], index: Number(match[2]), channel: match[3] };
306
+ }
307
+ function getGradientStopValue(element, stopPath) {
308
+ const paint = element[stopPath.root];
309
+ if (!isStructuredPaint(paint))
310
+ return undefined;
311
+ const stop = paint.stops[stopPath.index];
312
+ if (!stop)
313
+ return undefined;
314
+ const value = Array.isArray(stop) ? (stopPath.channel === "offset" ? stop[0] : stop[1]) : stop[stopPath.channel];
315
+ return isTimelineValue(value) ? (0, utils_1.clone)(value) : undefined;
316
+ }
317
+ function setGradientStopValue(element, stopPath, value) {
318
+ const paint = element[stopPath.root];
319
+ if (!isStructuredPaint(paint))
320
+ return;
321
+ const stop = paint.stops[stopPath.index];
322
+ if (!stop)
323
+ return;
324
+ if (Array.isArray(stop)) {
325
+ if (stopPath.channel === "offset" && (0, utils_1.isFiniteNumber)(value))
326
+ stop[0] = value;
327
+ if (stopPath.channel === "color" && typeof value === "string")
328
+ stop[1] = value;
329
+ }
330
+ else {
331
+ stop[stopPath.channel] = (0, utils_1.clone)(value);
332
+ }
333
+ }
334
+ function ensureEffects(element) {
335
+ const record = element;
336
+ if (!isRecord(record.effects))
337
+ record.effects = {};
338
+ }
339
+ function ensureShadow(element) {
340
+ ensureEffects(element);
341
+ const effects = element.effects;
342
+ if (!isRecord(effects.shadow))
343
+ effects.shadow = { dx: 0, dy: 0, blur: 0, color: "#000000", opacity: 1 };
344
+ }
345
+ function ensureImageSource(element) {
346
+ if (element.type !== "image")
347
+ return;
348
+ if (!element.source) {
349
+ element.source = {
350
+ x: 0,
351
+ y: 0,
352
+ width: element.width,
353
+ height: element.height,
354
+ imageWidth: element.width,
355
+ imageHeight: element.height
356
+ };
357
+ }
358
+ }
359
+ function ensureClip(element) {
360
+ if (!element.clip)
361
+ element.clip = { type: "path", d: fullPlanePath() };
362
+ }
363
+ function ensureMask(element) {
364
+ if (!element.mask)
365
+ element.mask = { type: "path", d: fullPlanePath(), opacity: 1 };
366
+ }
367
+ function effectDefault(property) {
368
+ if (property === "effects.brightness" || property === "effects.contrast" || property === "effects.saturate")
369
+ return 1;
370
+ return 0;
371
+ }
372
+ function shadowDefault(property) {
373
+ return property === "effects.shadow.opacity" ? 1 : 0;
374
+ }
375
+ function sourceDefault(property) {
376
+ return (element) => {
377
+ if (element.type !== "image")
378
+ return undefined;
379
+ if (property === "source.width")
380
+ return element.width;
381
+ if (property === "source.height")
382
+ return element.height;
383
+ return 0;
384
+ };
385
+ }
386
+ function fullPlanePath() {
387
+ return "M -100000 -100000 H 100000 V 100000 H -100000 Z";
388
+ }
389
+ function isStructuredPaint(value) {
390
+ return isRecord(value) && typeof value.type === "string" && Array.isArray(value.stops);
391
+ }
392
+ function isPatternPaint(value) {
393
+ return isRecord(value) && value.type === "pattern";
394
+ }
395
+ function isPaintType(element, root, type) {
396
+ const paint = element[root];
397
+ return isRecord(paint) && paint.type === type;
398
+ }
399
+ function hasGradientStop(element, stopPath) {
400
+ const paint = element[stopPath.root];
401
+ return isStructuredPaint(paint) && !!paint.stops[stopPath.index];
402
+ }
403
+ function isPaintValue(value) {
404
+ if (typeof value === "string")
405
+ return true;
406
+ if (!isRecord(value) || typeof value.type !== "string")
407
+ return false;
408
+ if (value.type === "linearGradient")
409
+ return (0, utils_1.isPoint2)(value.from) && (0, utils_1.isPoint2)(value.to) && isGradientStops(value.stops);
410
+ if (value.type === "radialGradient")
411
+ return (0, utils_1.isPoint2)(value.center) && (0, utils_1.isFiniteNumber)(value.radius) && (value.focus === undefined || (0, utils_1.isPoint2)(value.focus)) && isGradientStops(value.stops);
412
+ if (value.type === "pattern")
413
+ return typeof value.src === "string" && (0, utils_1.isFiniteNumber)(value.width) && (0, utils_1.isFiniteNumber)(value.height);
414
+ return false;
415
+ }
416
+ function isGradientStops(value) {
417
+ return Array.isArray(value) && value.length >= 2 && value.every((stop) => {
418
+ if (Array.isArray(stop))
419
+ return (0, utils_1.isFiniteNumber)(stop[0]) && typeof stop[1] === "string";
420
+ return isRecord(stop) && (0, utils_1.isFiniteNumber)(stop.offset) && typeof stop.color === "string";
421
+ });
422
+ }
423
+ function isNumberArray(value) {
424
+ return Array.isArray(value) && value.every((item) => (0, utils_1.isFiniteNumber)(item));
425
+ }
426
+ function isStringArray(value) {
427
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
428
+ }
429
+ function isJsonObject(value) {
430
+ if (!isRecord(value))
431
+ return false;
432
+ return Object.values(value).every(isJsonValue);
433
+ }
434
+ function isJsonValue(value) {
435
+ return value === null || typeof value === "string" || typeof value === "boolean" || (0, utils_1.isFiniteNumber)(value) || (Array.isArray(value) && value.every(isJsonValue)) || isJsonObject(value);
436
+ }
437
+ function isRecord(value) {
438
+ return !!value && typeof value === "object" && !Array.isArray(value);
439
+ }
@@ -1,4 +1,4 @@
1
- import type { AnimationValue, ArcElement, CircleElement, CurveElement, EllipseElement, Endpoint, GroupElement, LineElement, PointElement, PolygonElement, PolylineElement, RectElement, TextElement, VisualDocument, VisualElement } from "../types";
1
+ import type { ArcElement, CircleElement, CurveElement, EllipseElement, Endpoint, GroupElement, LineElement, PointElement, PolygonElement, PolylineElement, RectElement, TextElement, VisualDocument, VisualElement } from "../types";
2
2
  export type ElementInput = VisualElement | VisualElement[];
3
3
  export declare function scene(input: Omit<VisualDocument, "version"> & {
4
4
  version?: 1;
@@ -15,8 +15,6 @@ export declare function arrow(input: Omit<LineElement, "type">): LineElement;
15
15
  export declare function arc(input: Omit<ArcElement, "type">): ArcElement;
16
16
  export declare function curve(input: Omit<CurveElement, "type">): CurveElement;
17
17
  export declare function group(input: Omit<GroupElement, "type">): GroupElement;
18
- export declare function animate(from: number | string, to: number | string, options?: Omit<AnimationValue, "from" | "to">): AnimationValue;
19
- export declare function keyframes(values: Array<[number, number | string]>): AnimationValue;
20
18
  export interface NodeOptions {
21
19
  id: string;
22
20
  label: string;
@@ -43,14 +41,6 @@ export interface FlowOptions {
43
41
  labelY?: number;
44
42
  }
45
43
  export declare function flow(options: FlowOptions): VisualElement[];
46
- export interface PacketOptions {
47
- id: string;
48
- on: string;
49
- radius?: number;
50
- fill?: string;
51
- progress: number | AnimationValue;
52
- }
53
- export declare function packet(options: PacketOptions): CircleElement;
54
44
  export interface CalloutOptions {
55
45
  id: string;
56
46
  text: string;
@@ -13,11 +13,8 @@ exports.arrow = arrow;
13
13
  exports.arc = arc;
14
14
  exports.curve = curve;
15
15
  exports.group = group;
16
- exports.animate = animate;
17
- exports.keyframes = keyframes;
18
16
  exports.node = node;
19
17
  exports.flow = flow;
20
- exports.packet = packet;
21
18
  exports.callout = callout;
22
19
  exports.row = row;
23
20
  exports.column = column;
@@ -61,12 +58,6 @@ function curve(input) {
61
58
  function group(input) {
62
59
  return { type: "group", ...input };
63
60
  }
64
- function animate(from, to, options = {}) {
65
- return { from, to, ...options };
66
- }
67
- function keyframes(values) {
68
- return { keyframes: values };
69
- }
70
61
  function node(options) {
71
62
  return [
72
63
  rect({
@@ -119,15 +110,6 @@ function flow(options) {
119
110
  }
120
111
  return out;
121
112
  }
122
- function packet(options) {
123
- return circle({
124
- id: options.id,
125
- radius: options.radius ?? 7,
126
- fill: options.fill ?? "#ef4444",
127
- follow: options.on,
128
- progress: options.progress
129
- });
130
- }
131
113
  function callout(options) {
132
114
  const boxId = `${options.id}_box`;
133
115
  return [
@@ -153,7 +135,7 @@ function callout(options) {
153
135
  }),
154
136
  arrow({
155
137
  id: `${options.id}_arrow`,
156
- from: `${boxId}.bottom`,
138
+ from: { target: boxId, anchor: (0, utils_1.anchorSpec)("bottom") },
157
139
  to: options.target,
158
140
  stroke: options.stroke ?? "#ef4444",
159
141
  strokeWidth: 2
@@ -15,7 +15,7 @@ function lintVisualDocument(document) {
15
15
  path,
16
16
  code: "element_outside_canvas",
17
17
  message: `Element '${element.id ?? element.type}' extends outside the canvas.`,
18
- suggestion: "Move it inside the canvas or intentionally clip it with a group/mask later."
18
+ suggestion: "Move it inside the canvas or intentionally clip it."
19
19
  });
20
20
  }
21
21
  if (element.type === "text" && Number(element.fontSize ?? 16) < 10) {
@@ -26,27 +26,6 @@ function lintVisualDocument(document) {
26
26
  suggestion: "Use fontSize 12 or larger for explainer visuals."
27
27
  });
28
28
  }
29
- if (element.type === "text") {
30
- const containingBox = boxes.find((item) => item.element.type === "rect" && item.element.id !== element.id && pointInside(element.x, element.y, item.box));
31
- if (containingBox && !boxInside(box, containingBox.box)) {
32
- warnings.push({
33
- path,
34
- code: "text_likely_outside_box",
35
- message: `Text '${element.id ?? ""}' is anchored inside '${containingBox.element.id ?? "rect"}' but its estimated bounds extend outside.`,
36
- suggestion: "Move the text explicitly, reduce fontSize, or set maxWidth/wrap intentionally."
37
- });
38
- }
39
- const contrastAgainst = containingBox?.element.fill ?? document.canvas.background ?? "#ffffff";
40
- const ratio = contrastRatio(String(element.fill ?? "#111827"), String(contrastAgainst));
41
- if (ratio !== undefined && ratio < 3) {
42
- warnings.push({
43
- path,
44
- code: "low_text_contrast",
45
- message: `Text '${element.id ?? ""}' may not have enough contrast.`,
46
- suggestion: "Choose a darker or lighter explicit fill color; diagnostics do not recolor text."
47
- });
48
- }
49
- }
50
29
  }
51
30
  for (let left = 0; left < boxes.length; left += 1) {
52
31
  for (let right = left + 1; right < boxes.length; right += 1) {
@@ -69,45 +48,3 @@ function lintVisualDocument(document) {
69
48
  function overlaps(a, b) {
70
49
  return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
71
50
  }
72
- function pointInside(x, y, box) {
73
- return x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
74
- }
75
- function boxInside(inner, outer) {
76
- return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
77
- }
78
- function contrastRatio(foreground, background) {
79
- const fg = parseHexColor(foreground);
80
- const bg = parseHexColor(background);
81
- if (!fg || !bg)
82
- return undefined;
83
- const a = relativeLuminance(fg);
84
- const b = relativeLuminance(bg);
85
- const light = Math.max(a, b);
86
- const dark = Math.min(a, b);
87
- return (light + 0.05) / (dark + 0.05);
88
- }
89
- function parseHexColor(value) {
90
- const text = value.trim();
91
- if (/^#[0-9a-fA-F]{3}$/.test(text)) {
92
- return [
93
- parseInt(text[1] + text[1], 16),
94
- parseInt(text[2] + text[2], 16),
95
- parseInt(text[3] + text[3], 16)
96
- ];
97
- }
98
- if (/^#[0-9a-fA-F]{6}$/.test(text)) {
99
- return [
100
- parseInt(text.slice(1, 3), 16),
101
- parseInt(text.slice(3, 5), 16),
102
- parseInt(text.slice(5, 7), 16)
103
- ];
104
- }
105
- return undefined;
106
- }
107
- function relativeLuminance([r, g, b]) {
108
- const values = [r, g, b].map((channel) => {
109
- const value = channel / 255;
110
- return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
111
- });
112
- return 0.2126 * values[0] + 0.7152 * values[1] + 0.0722 * values[2];
113
- }
@@ -0,0 +1,27 @@
1
+ import type { ClipShape, ImageElement, MotionValue, TimelineCurve, TimelineKeyframe, VisualDocument, VisualElement } from "./types";
2
+ export interface ElementReference {
3
+ id: string;
4
+ type: string;
5
+ depth: number;
6
+ path: number[];
7
+ }
8
+ export interface SetKeyframeOptions {
9
+ in?: TimelineCurve;
10
+ out?: TimelineCurve;
11
+ interpolation?: TimelineCurve;
12
+ curve?: TimelineCurve;
13
+ ease?: string;
14
+ }
15
+ export declare function listElementReferences(document: VisualDocument): ElementReference[];
16
+ export declare function findElementById(document: VisualDocument, id: string): VisualElement | undefined;
17
+ export declare function setElementProperty(document: VisualDocument, id: string, property: string, value: MotionValue): VisualDocument;
18
+ export declare function setTimelineKeyframe(document: VisualDocument, id: string, property: string, time: number, value: MotionValue, options?: SetKeyframeOptions): VisualDocument;
19
+ export declare function removeTimelineKeyframe(document: VisualDocument, id: string, property: string, time: number): VisualDocument;
20
+ export declare function listTimelineTracks(document: VisualDocument, id: string): Array<{
21
+ property: string;
22
+ keyframes: TimelineKeyframe[];
23
+ curve?: TimelineCurve;
24
+ ease?: string;
25
+ }>;
26
+ export declare function roundedRectClipPath(x: number, y: number, width: number, height: number, radius?: number): string;
27
+ export declare function imageRoundedClip(element: Pick<ImageElement, "x" | "y" | "width" | "height">, radius?: number): ClipShape;