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,355 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const width = 800;
5
+ const height = 600;
6
+ const bg = "rgba(15, 23, 42, 0.6)";
7
+ const font = "Inter, system-ui, sans-serif";
8
+
9
+ const colors = {
10
+ overlay: "#0f172a",
11
+ modalBg: "#ffffff",
12
+ modalBorder: "#e2e8f0",
13
+ title: "#0f172a",
14
+ description: "#475569",
15
+ label: "#1e293b",
16
+ helper: "#64748b",
17
+ inputBg: "#ffffff",
18
+ inputBorder: "#cbd5e1",
19
+ placeholder: "#94a3b8",
20
+ warningBg: "#fef2f2",
21
+ warningBorder: "#fecaca",
22
+ warningText: "#991b1b",
23
+ footerBg: "#f8fafc",
24
+ footerBorder: "#e2e8f0",
25
+ btnPrimaryBg: "#dc2626",
26
+ btnPrimaryText: "#ffffff",
27
+ btnSecondaryBg: "#ffffff",
28
+ btnSecondaryText: "#475569",
29
+ btnSecondaryBorder: "#cbd5e1"
30
+ };
31
+
32
+ const elements = [];
33
+
34
+ // Overlay background
35
+ elements.push({
36
+ id: "overlay",
37
+ type: "path",
38
+ d: `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`,
39
+ fill: colors.overlay,
40
+ opacity: 0.5,
41
+ stroke: "none"
42
+ });
43
+
44
+ // Modal dimensions
45
+ const modalW = 520;
46
+ const modalH = 480;
47
+ const modalX = (width - modalW) / 2;
48
+ const modalY = (height - modalH) / 2;
49
+ const modalR = 12;
50
+ const modalPad = 28;
51
+
52
+ // Modal background
53
+ elements.push({
54
+ id: "modal-bg",
55
+ type: "path",
56
+ d: roundedRect(modalX, modalY, modalW, modalH, modalR),
57
+ fill: colors.modalBg,
58
+ stroke: colors.modalBorder,
59
+ strokeWidth: 1,
60
+ effects: { shadow: { dx: 0, dy: 8, blur: 32, color: "#000000", opacity: 0.15 } }
61
+ });
62
+
63
+ let y = modalY + modalPad;
64
+ const contentX = modalX + modalPad;
65
+ const contentW = modalW - modalPad * 2;
66
+
67
+ // Title (left aligned)
68
+ elements.push({
69
+ id: "modal-title",
70
+ type: "text",
71
+ x: contentX,
72
+ y: y,
73
+ text: "Delete Project",
74
+ align: "left",
75
+ valign: "top",
76
+ fontSize: 20,
77
+ fontFamily: font,
78
+ weight: 700,
79
+ fill: colors.title
80
+ });
81
+ y += 32;
82
+
83
+ // Description (left aligned)
84
+ elements.push({
85
+ id: "modal-desc",
86
+ type: "text",
87
+ x: contentX,
88
+ y: y,
89
+ text: "This action will permanently remove the project and all associated\ndata. This cannot be undone.",
90
+ align: "left",
91
+ valign: "top",
92
+ fontSize: 14,
93
+ fontFamily: font,
94
+ weight: 400,
95
+ lineHeight: 1.55,
96
+ fill: colors.description,
97
+ maxWidth: contentW
98
+ });
99
+ y += 56;
100
+
101
+ // Warning note
102
+ const warnH = 56;
103
+ const warnR = 6;
104
+ const warnPad = 14;
105
+
106
+ elements.push({
107
+ id: "warning-bg",
108
+ type: "path",
109
+ d: roundedRect(contentX, y, contentW, warnH, warnR),
110
+ fill: colors.warningBg,
111
+ stroke: colors.warningBorder,
112
+ strokeWidth: 1
113
+ });
114
+
115
+ elements.push({
116
+ id: "warning-text",
117
+ type: "text",
118
+ x: contentX + warnPad,
119
+ y: y + warnPad,
120
+ text: "Warning: 3 team members will lose access immediately. Any\nactive integrations connected to this project will stop working.",
121
+ align: "left",
122
+ valign: "top",
123
+ fontSize: 12,
124
+ fontFamily: font,
125
+ weight: 500,
126
+ lineHeight: 1.5,
127
+ fill: colors.warningText,
128
+ maxWidth: contentW - warnPad * 2
129
+ });
130
+ y += warnH + 24;
131
+
132
+ // Field 1: Project name
133
+ elements.push({
134
+ id: "field1-label",
135
+ type: "text",
136
+ x: contentX,
137
+ y: y,
138
+ text: "Project Name",
139
+ align: "left",
140
+ valign: "top",
141
+ fontSize: 13,
142
+ fontFamily: font,
143
+ weight: 600,
144
+ fill: colors.label
145
+ });
146
+ y += 22;
147
+
148
+ elements.push({
149
+ id: "field1-helper",
150
+ type: "text",
151
+ x: contentX,
152
+ y: y,
153
+ text: "Enter the project name exactly as shown to confirm deletion.\nThis helps prevent accidental removal of important data.",
154
+ align: "left",
155
+ valign: "top",
156
+ fontSize: 12,
157
+ fontFamily: font,
158
+ weight: 400,
159
+ lineHeight: 1.5,
160
+ fill: colors.helper,
161
+ maxWidth: contentW
162
+ });
163
+ y += 42;
164
+
165
+ // Input field 1
166
+ const inputH = 38;
167
+ const inputR = 6;
168
+
169
+ elements.push({
170
+ id: "field1-input-bg",
171
+ type: "path",
172
+ d: roundedRect(contentX, y, contentW, inputH, inputR),
173
+ fill: colors.inputBg,
174
+ stroke: colors.inputBorder,
175
+ strokeWidth: 1
176
+ });
177
+
178
+ elements.push({
179
+ id: "field1-placeholder",
180
+ type: "text",
181
+ x: contentX + 12,
182
+ y: y + inputH / 2,
183
+ text: "acme-dashboard-v2",
184
+ align: "left",
185
+ valign: "middle",
186
+ fontSize: 13,
187
+ fontFamily: font,
188
+ weight: 400,
189
+ fill: colors.placeholder
190
+ });
191
+ y += inputH + 20;
192
+
193
+ // Field 2: Confirmation phrase
194
+ elements.push({
195
+ id: "field2-label",
196
+ type: "text",
197
+ x: contentX,
198
+ y: y,
199
+ text: "Confirmation Phrase",
200
+ align: "left",
201
+ valign: "top",
202
+ fontSize: 13,
203
+ fontFamily: font,
204
+ weight: 600,
205
+ fill: colors.label
206
+ });
207
+ y += 22;
208
+
209
+ elements.push({
210
+ id: "field2-helper",
211
+ type: "text",
212
+ x: contentX,
213
+ y: y,
214
+ text: "Type \"delete my project\" to enable the delete button.",
215
+ align: "left",
216
+ valign: "top",
217
+ fontSize: 12,
218
+ fontFamily: font,
219
+ weight: 400,
220
+ fill: colors.helper,
221
+ maxWidth: contentW
222
+ });
223
+ y += 26;
224
+
225
+ // Input field 2
226
+ elements.push({
227
+ id: "field2-input-bg",
228
+ type: "path",
229
+ d: roundedRect(contentX, y, contentW, inputH, inputR),
230
+ fill: colors.inputBg,
231
+ stroke: colors.inputBorder,
232
+ strokeWidth: 1
233
+ });
234
+
235
+ elements.push({
236
+ id: "field2-placeholder",
237
+ type: "text",
238
+ x: contentX + 12,
239
+ y: y + inputH / 2,
240
+ text: "delete my project",
241
+ align: "left",
242
+ valign: "middle",
243
+ fontSize: 13,
244
+ fontFamily: font,
245
+ weight: 400,
246
+ fill: colors.placeholder
247
+ });
248
+
249
+ // Footer
250
+ const footerH = 68;
251
+ const footerY = modalY + modalH - footerH;
252
+ const footerR = modalR;
253
+
254
+ // Footer background (bottom rounded corners only)
255
+ elements.push({
256
+ id: "footer-bg",
257
+ type: "path",
258
+ d: `M ${modalX} ${footerY} L ${modalX + modalW} ${footerY} L ${modalX + modalW} ${modalY + modalH - footerR} Q ${modalX + modalW} ${modalY + modalH} ${modalX + modalW - footerR} ${modalY + modalH} L ${modalX + footerR} ${modalY + modalH} Q ${modalX} ${modalY + modalH} ${modalX} ${modalY + modalH - footerR} Z`,
259
+ fill: colors.footerBg,
260
+ stroke: "none"
261
+ });
262
+
263
+ // Footer top border
264
+ elements.push({
265
+ id: "footer-border",
266
+ type: "path",
267
+ d: `M ${modalX} ${footerY} L ${modalX + modalW} ${footerY}`,
268
+ stroke: colors.footerBorder,
269
+ strokeWidth: 1,
270
+ fill: "none"
271
+ });
272
+
273
+ // Footer buttons (right-aligned group)
274
+ const btnH = 38;
275
+ const btnR = 6;
276
+ const btnGap = 12;
277
+ const cancelBtnW = 80;
278
+ const deleteBtnW = 120;
279
+ const btnGroupW = cancelBtnW + btnGap + deleteBtnW;
280
+ const btnGroupX = modalX + modalW - modalPad - btnGroupW;
281
+ const btnY = footerY + (footerH - btnH) / 2;
282
+
283
+ // Cancel button (secondary)
284
+ elements.push({
285
+ id: "btn-cancel-bg",
286
+ type: "path",
287
+ d: roundedRect(btnGroupX, btnY, cancelBtnW, btnH, btnR),
288
+ fill: colors.btnSecondaryBg,
289
+ stroke: colors.btnSecondaryBorder,
290
+ strokeWidth: 1
291
+ });
292
+
293
+ elements.push({
294
+ id: "btn-cancel-label",
295
+ type: "text",
296
+ x: btnGroupX + cancelBtnW / 2,
297
+ y: btnY + btnH / 2,
298
+ text: "Cancel",
299
+ align: "center",
300
+ valign: "middle",
301
+ fontSize: 13,
302
+ fontFamily: font,
303
+ weight: 500,
304
+ fill: colors.btnSecondaryText
305
+ });
306
+
307
+ // Delete button (primary/destructive)
308
+ const deleteBtnX = btnGroupX + cancelBtnW + btnGap;
309
+
310
+ elements.push({
311
+ id: "btn-delete-bg",
312
+ type: "path",
313
+ d: roundedRect(deleteBtnX, btnY, deleteBtnW, btnH, btnR),
314
+ fill: colors.btnPrimaryBg,
315
+ stroke: "none"
316
+ });
317
+
318
+ elements.push({
319
+ id: "btn-delete-label",
320
+ type: "text",
321
+ x: deleteBtnX + deleteBtnW / 2,
322
+ y: btnY + btnH / 2,
323
+ text: "Delete Project",
324
+ align: "center",
325
+ valign: "middle",
326
+ fontSize: 13,
327
+ fontFamily: font,
328
+ weight: 600,
329
+ fill: colors.btnPrimaryText
330
+ });
331
+
332
+ function roundedRect(x, y, w, h, r) {
333
+ return [
334
+ `M ${x + r} ${y}`,
335
+ `L ${x + w - r} ${y}`,
336
+ `Q ${x + w} ${y} ${x + w} ${y + r}`,
337
+ `L ${x + w} ${y + h - r}`,
338
+ `Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
339
+ `L ${x + r} ${y + h}`,
340
+ `Q ${x} ${y + h} ${x} ${y + h - r}`,
341
+ `L ${x} ${y + r}`,
342
+ `Q ${x} ${y} ${x + r} ${y}`,
343
+ "Z"
344
+ ].join(" ");
345
+ }
346
+
347
+ const doc = {
348
+ version: 1,
349
+ canvas: { width, height, background: "#64748b" },
350
+ elements
351
+ };
352
+
353
+ const outPath = path.join(__dirname, "modal-dialog.visual.json");
354
+ fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
355
+ console.log("Written:", outPath);
@@ -0,0 +1,311 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const width = 1280;
5
+ const height = 760;
6
+ const duration = 6;
7
+ const fps = 30;
8
+ const bg = "#07111f";
9
+ const font = "Roboto, Arial, sans-serif";
10
+
11
+ const colors = {
12
+ panel: "#0d1a2e",
13
+ panelStroke: "#193252",
14
+ title: "#f8fafc",
15
+ body: "#94a3b8",
16
+ marker: "#fde047",
17
+ markerStroke: "#0f172a",
18
+ grid: "#173054",
19
+ rowLabel: "#7dd3fc",
20
+ fills: ["#818cf8", "#38bdf8", "#22c55e", "#f59e0b"]
21
+ };
22
+
23
+ const curves = {
24
+ ease: { type: "cubicBezier", x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 },
25
+ easeOut: { type: "cubicBezier", x1: 0, y1: 0, x2: 0.2, y2: 1 }
26
+ };
27
+
28
+ const rectW = 160;
29
+ const rectH = 100;
30
+ const cellW = 270;
31
+ const cellH = 240;
32
+ const gridX = 70;
33
+ const gridGap = 25;
34
+ const scaleRowY = 150;
35
+ const rotateRowY = 435;
36
+ const shapeOffsetX = 58;
37
+ const shapeOffsetY = 98;
38
+ const rectShape = roundedRect(0, 0, rectW, rectH, 18);
39
+
40
+ const originVariants = [
41
+ { key: "top_left", title: "Top-left origin", note: "[0, 0]", local: [0, 0] },
42
+ { key: "center", title: "Center origin", note: "[80, 50]", local: [rectW / 2, rectH / 2] },
43
+ { key: "bottom_right", title: "Bottom-right origin", note: "[160, 100]", local: [rectW, rectH] },
44
+ { key: "outside_right", title: "Outside-right origin", note: "[200, 50]", local: [rectW + 40, rectH / 2] }
45
+ ];
46
+
47
+ const elements = [];
48
+
49
+ elements.push({
50
+ id: "lab_title",
51
+ type: "text",
52
+ x: 70,
53
+ y: 52,
54
+ text: "Origin Transform Lab",
55
+ fontSize: 38,
56
+ fontFamily: font,
57
+ weight: 700,
58
+ fill: colors.title
59
+ });
60
+
61
+ elements.push({
62
+ id: "lab_body",
63
+ type: "text",
64
+ x: 70,
65
+ y: 96,
66
+ text: "Same rounded rectangle, animated as a group. Only the origin changes.",
67
+ fontSize: 16,
68
+ fontFamily: font,
69
+ weight: 400,
70
+ fill: colors.body
71
+ });
72
+
73
+ elements.push({
74
+ id: "scale_row_label",
75
+ type: "text",
76
+ x: 70,
77
+ y: scaleRowY - 38,
78
+ text: "Scale tests",
79
+ fontSize: 18,
80
+ fontFamily: font,
81
+ weight: 700,
82
+ fill: colors.rowLabel
83
+ });
84
+
85
+ elements.push({
86
+ id: "rotate_row_label",
87
+ type: "text",
88
+ x: 70,
89
+ y: rotateRowY - 38,
90
+ text: "Rotation tests",
91
+ fontSize: 18,
92
+ fontFamily: font,
93
+ weight: 700,
94
+ fill: colors.rowLabel
95
+ });
96
+
97
+ originVariants.forEach((variant, index) => {
98
+ const cellX = gridX + index * (cellW + gridGap);
99
+ const fill = colors.fills[index % colors.fills.length];
100
+ addPanel(cellX, scaleRowY, `scale_${variant.key}`, variant.title, variant.note);
101
+ addScaleDemo(cellX, scaleRowY, variant, fill);
102
+ addPanel(cellX, rotateRowY, `rotate_${variant.key}`, variant.title, variant.note);
103
+ addRotateDemo(cellX, rotateRowY, variant, fill);
104
+ });
105
+
106
+ const doc = {
107
+ version: 1,
108
+ canvas: { width, height, background: bg, duration, fps },
109
+ elements
110
+ };
111
+
112
+ const outPath = path.join(__dirname, "origin-effects-lab.visual.json");
113
+ fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
114
+ console.log("Written:", outPath);
115
+
116
+ function addPanel(x, y, id, title, note) {
117
+ elements.push({
118
+ id: `${id}_panel`,
119
+ type: "path",
120
+ d: roundedRect(x, y, cellW, cellH, 22),
121
+ fill: colors.panel,
122
+ stroke: colors.panelStroke,
123
+ strokeWidth: 1.5
124
+ });
125
+
126
+ elements.push({
127
+ id: `${id}_title`,
128
+ type: "text",
129
+ x: x + 20,
130
+ y: y + 18,
131
+ text: title,
132
+ fontSize: 16,
133
+ fontFamily: font,
134
+ weight: 700,
135
+ fill: colors.title
136
+ });
137
+
138
+ elements.push({
139
+ id: `${id}_note`,
140
+ type: "text",
141
+ x: x + 20,
142
+ y: y + 42,
143
+ text: note,
144
+ fontSize: 12,
145
+ fontFamily: font,
146
+ weight: 500,
147
+ fill: colors.body
148
+ });
149
+
150
+ elements.push({
151
+ id: `${id}_guide_h`,
152
+ type: "path",
153
+ d: `M ${x + 20} ${y + shapeOffsetY + rectH / 2} L ${x + cellW - 20} ${y + shapeOffsetY + rectH / 2}`,
154
+ fill: "none",
155
+ stroke: colors.grid,
156
+ strokeWidth: 1,
157
+ dashArray: [6, 6]
158
+ });
159
+
160
+ elements.push({
161
+ id: `${id}_guide_v`,
162
+ type: "path",
163
+ d: `M ${x + shapeOffsetX + rectW / 2} ${y + 68} L ${x + shapeOffsetX + rectW / 2} ${y + cellH - 20}`,
164
+ fill: "none",
165
+ stroke: colors.grid,
166
+ strokeWidth: 1,
167
+ dashArray: [6, 6]
168
+ });
169
+ }
170
+
171
+ function addScaleDemo(cellX, cellY, variant, fill) {
172
+ const groupX = cellX + shapeOffsetX;
173
+ const groupY = cellY + shapeOffsetY;
174
+ const originAbs = absoluteOrigin(groupX, groupY, variant.local);
175
+
176
+ elements.push(originMarker(`scale_${variant.key}_origin`, originAbs[0], originAbs[1]));
177
+
178
+ elements.push({
179
+ id: `scale_${variant.key}_group`,
180
+ type: "group",
181
+ x: groupX,
182
+ y: groupY,
183
+ width: rectW,
184
+ height: rectH,
185
+ origin: originAbs,
186
+ children: [
187
+ {
188
+ id: `scale_${variant.key}_rect`,
189
+ type: "path",
190
+ d: rectShape,
191
+ fill,
192
+ stroke: "#dbeafe",
193
+ strokeWidth: 2
194
+ }
195
+ ],
196
+ timeline: {
197
+ tracks: {
198
+ scale: {
199
+ keyframes: [
200
+ { time: 0, value: 1, out: curves.easeOut },
201
+ { time: 1.3, value: 1.45, out: curves.ease },
202
+ { time: 2.6, value: 0.72, out: curves.ease },
203
+ { time: 4, value: 1.18, out: curves.ease },
204
+ { time: 5.4, value: 1 }
205
+ ]
206
+ }
207
+ }
208
+ }
209
+ });
210
+ }
211
+
212
+ function addRotateDemo(cellX, cellY, variant, fill) {
213
+ const groupX = cellX + shapeOffsetX;
214
+ const groupY = cellY + shapeOffsetY;
215
+ const originAbs = absoluteOrigin(groupX, groupY, variant.local);
216
+
217
+ elements.push(originMarker(`rotate_${variant.key}_origin`, originAbs[0], originAbs[1]));
218
+
219
+ elements.push({
220
+ id: `rotate_${variant.key}_group`,
221
+ type: "group",
222
+ x: groupX,
223
+ y: groupY,
224
+ width: rectW,
225
+ height: rectH,
226
+ origin: originAbs,
227
+ children: [
228
+ {
229
+ id: `rotate_${variant.key}_rect`,
230
+ type: "path",
231
+ d: rectShape,
232
+ fill,
233
+ stroke: "#dbeafe",
234
+ strokeWidth: 2
235
+ }
236
+ ],
237
+ timeline: {
238
+ tracks: {
239
+ rotation: {
240
+ keyframes: [
241
+ { time: 0, value: 0, out: curves.easeOut },
242
+ { time: 1.5, value: 42, out: curves.ease },
243
+ { time: 3, value: -28, out: curves.ease },
244
+ { time: 4.5, value: 18, out: curves.ease },
245
+ { time: 5.8, value: 0 }
246
+ ]
247
+ }
248
+ }
249
+ }
250
+ });
251
+ }
252
+
253
+ function originMarker(id, x, y) {
254
+ return {
255
+ id,
256
+ type: "group",
257
+ x: x - 16,
258
+ y: y - 16,
259
+ children: [
260
+ {
261
+ id: `${id}_cross_h`,
262
+ type: "path",
263
+ d: "M 0 16 L 32 16",
264
+ fill: "none",
265
+ stroke: colors.marker,
266
+ strokeWidth: 2,
267
+ strokeCap: "round"
268
+ },
269
+ {
270
+ id: `${id}_cross_v`,
271
+ type: "path",
272
+ d: "M 16 0 L 16 32",
273
+ fill: "none",
274
+ stroke: colors.marker,
275
+ strokeWidth: 2,
276
+ strokeCap: "round"
277
+ },
278
+ {
279
+ id: `${id}_dot`,
280
+ type: "path",
281
+ d: circlePath(16, 16, 6),
282
+ fill: colors.marker,
283
+ stroke: colors.markerStroke,
284
+ strokeWidth: 1.5
285
+ }
286
+ ]
287
+ };
288
+ }
289
+
290
+ function absoluteOrigin(groupX, groupY, localOrigin) {
291
+ return [groupX + localOrigin[0], groupY + localOrigin[1]];
292
+ }
293
+
294
+ function circlePath(cx, cy, r) {
295
+ return `M ${cx - r} ${cy} a ${r} ${r} 0 1 0 ${r * 2} 0 a ${r} ${r} 0 1 0 ${-r * 2} 0`;
296
+ }
297
+
298
+ function roundedRect(x, y, w, h, r) {
299
+ return [
300
+ `M ${x + r} ${y}`,
301
+ `L ${x + w - r} ${y}`,
302
+ `Q ${x + w} ${y} ${x + w} ${y + r}`,
303
+ `L ${x + w} ${y + h - r}`,
304
+ `Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
305
+ `L ${x + r} ${y + h}`,
306
+ `Q ${x} ${y + h} ${x} ${y + h - r}`,
307
+ `L ${x} ${y + r}`,
308
+ `Q ${x} ${y} ${x + r} ${y}`,
309
+ "Z"
310
+ ].join(" ");
311
+ }
@@ -0,0 +1,32 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const { applyPresetFragments, characters, effects, motions, scenes, shapes } = require("../dist/src/presets");
5
+
6
+ const outputPath = path.join(__dirname, "preset-character-motion.visual.json");
7
+
8
+ const doc = {
9
+ version: 1,
10
+ canvas: { width: 960, height: 540, background: "#f8fafc", duration: 4, fps: 30 },
11
+ elements: []
12
+ };
13
+
14
+ const visual = applyPresetFragments(doc, [
15
+ scenes.gridBackground({ id: "stage.grid", x: 0, y: 0, width: 960, height: 540, step: 48 }),
16
+ shapes.line({ id: "stage.ground", from: [80, 430], to: [880, 430], stroke: "#cbd5e1", strokeWidth: 8 }),
17
+ characters.stickPerson({ id: "hero", x: 150, y: 235, height: 190 }),
18
+ characters.simpleDog({ id: "dog", x: 470, y: 335, width: 190, height: 100 }),
19
+ characters.simpleSpider({ id: "spider", x: 730, y: 330, width: 130, height: 130 }),
20
+ shapes.speechBubble({ id: "caption", x: 280, y: 84, width: 400, height: 90, text: "Characters are just kernel groups", fill: "#ffffff", stroke: "#94a3b8" }),
21
+ effects.dropShadow({ id: "hero", dy: 8, blur: 18, opacity: 0.16 }),
22
+ effects.dropShadow({ id: "dog", dy: 7, blur: 16, opacity: 0.14 }),
23
+ effects.glow({ id: "spider", color: "#60a5fa", blur: 12, opacity: 0.22 }),
24
+ motions.slideIn({ id: "hero", from: [80, 235], to: [150, 235], start: 0, duration: 0.7 }),
25
+ motions.bob({ id: "dog", to: [470, 335], start: 0.4, duration: 1.4, distance: 12 }),
26
+ motions.shake({ id: "spider", start: 1.1, duration: 0.8, amount: 10 }),
27
+ motions.stagger({ ids: ["hero.head", "hero.body", "hero.leftArm", "hero.rightArm"], start: 0.1, duration: 0.35, each: 0.08 }),
28
+ motions.fadeIn({ id: "caption", start: 0.45, duration: 0.45 })
29
+ ]);
30
+
31
+ fs.writeFileSync(outputPath, JSON.stringify(visual, null, 2));
32
+ console.log(`Wrote ${outputPath}`);