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,762 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const width = 1280;
5
+ const height = 720;
6
+ const duration = 18;
7
+ const fps = 30;
8
+ const bg = "#f8fafc";
9
+ const font = "Inter, system-ui, sans-serif";
10
+
11
+ const colors = {
12
+ text: "#0f172a",
13
+ textMuted: "#64748b",
14
+ panelBg: "#ffffff",
15
+ panelBorder: "#e2e8f0",
16
+ inputBg: "#f8fafc",
17
+ inputBorder: "#cbd5e1",
18
+ btnPrimary: "#2563eb",
19
+ btnPrimaryText: "#ffffff",
20
+ btnSecondary: "#ffffff",
21
+ btnSecondaryText: "#475569",
22
+ btnSecondaryBorder: "#e2e8f0",
23
+ accent: "#2563eb",
24
+ success: "#10b981",
25
+ successBg: "#ecfdf5",
26
+ cursor: "#0f172a",
27
+ cursorRing: "#3b82f6",
28
+ backdrop: "#0f172a"
29
+ };
30
+
31
+ const curves = {
32
+ ease: { type: "cubicBezier", x1: 0.4, y1: 0, x2: 0.2, y2: 1 },
33
+ easeOut: { type: "cubicBezier", x1: 0, y1: 0, x2: 0.2, y2: 1 },
34
+ gentle: { type: "cubicBezier", x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 }
35
+ };
36
+
37
+ const elements = [];
38
+
39
+ // Layout constants
40
+ const cardW = 520;
41
+ const cardH = 400;
42
+ const cardX = (width - cardW) / 2;
43
+ const cardY = (height - cardH) / 2;
44
+
45
+ // Button positions (for cursor targeting)
46
+ const uploadBtnX = cardX + cardW / 2;
47
+ const uploadBtnY = cardY + 260;
48
+ const fileBtnX = cardX + cardW / 2;
49
+ const fileBtnY = cardY + 200;
50
+ const confirmBtnX = cardX + cardW - 90;
51
+ const confirmBtnY = cardY + cardH - 52;
52
+
53
+ // === Card background (persistent) ===
54
+ elements.push({
55
+ id: "main-card-bg",
56
+ type: "path",
57
+ d: roundedRect(cardX, cardY, cardW, cardH, 16),
58
+ fill: colors.panelBg,
59
+ stroke: colors.panelBorder,
60
+ strokeWidth: 1,
61
+ opacity: 0,
62
+ timeline: {
63
+ tracks: {
64
+ opacity: {
65
+ keyframes: [
66
+ { time: 0, value: 0, out: curves.ease },
67
+ { time: 0.6, value: 1 },
68
+ { time: 17, value: 1, out: curves.ease },
69
+ { time: 17.5, value: 0 }
70
+ ]
71
+ }
72
+ }
73
+ }
74
+ });
75
+
76
+ // Card title (persistent)
77
+ elements.push({
78
+ id: "card-title",
79
+ type: "text",
80
+ x: cardX + 32,
81
+ y: cardY + 32,
82
+ text: "Upload Document",
83
+ align: "left",
84
+ valign: "top",
85
+ fontSize: 20,
86
+ fontFamily: font,
87
+ weight: 600,
88
+ fill: colors.text,
89
+ opacity: 0,
90
+ timeline: {
91
+ tracks: {
92
+ opacity: {
93
+ keyframes: [
94
+ { time: 0, value: 0, out: curves.ease },
95
+ { time: 0.6, value: 1 },
96
+ { time: 17, value: 1, out: curves.ease },
97
+ { time: 17.5, value: 0 }
98
+ ]
99
+ }
100
+ }
101
+ }
102
+ });
103
+
104
+ // Card description (persistent until success)
105
+ elements.push({
106
+ id: "card-desc",
107
+ type: "text",
108
+ x: cardX + 32,
109
+ y: cardY + 62,
110
+ text: "Add a file to get started with your project.",
111
+ align: "left",
112
+ valign: "top",
113
+ fontSize: 14,
114
+ fontFamily: font,
115
+ weight: 400,
116
+ fill: colors.textMuted,
117
+ opacity: 0,
118
+ timeline: {
119
+ tracks: {
120
+ opacity: {
121
+ keyframes: [
122
+ { time: 0, value: 0, out: curves.ease },
123
+ { time: 0.6, value: 1 },
124
+ { time: 13.5, value: 1, out: curves.ease },
125
+ { time: 14, value: 0 }
126
+ ]
127
+ }
128
+ }
129
+ }
130
+ });
131
+
132
+ // === SCENE 1: Empty state (0-6s) - upload zone ===
133
+ elements.push({
134
+ id: "upload-zone",
135
+ type: "group",
136
+ x: cardX + 32,
137
+ y: cardY + 100,
138
+ children: [
139
+ {
140
+ id: "upload-zone-bg",
141
+ type: "path",
142
+ d: roundedRect(0, 0, cardW - 64, 140, 12),
143
+ fill: colors.inputBg,
144
+ stroke: colors.inputBorder,
145
+ strokeWidth: 2,
146
+ dashArray: [8, 4]
147
+ },
148
+ {
149
+ id: "upload-icon",
150
+ type: "path",
151
+ d: "M 0 12 L 0 4 A 4 4 0 0 1 4 0 L 20 0 A 4 4 0 0 1 24 4 L 24 12 M 12 20 L 12 6 M 6 12 L 12 6 L 18 12",
152
+ x: (cardW - 64) / 2 - 12,
153
+ y: 40,
154
+ fill: "none",
155
+ stroke: colors.textMuted,
156
+ strokeWidth: 2,
157
+ strokeCap: "round",
158
+ strokeJoin: "round"
159
+ },
160
+ {
161
+ id: "upload-text",
162
+ type: "text",
163
+ x: (cardW - 64) / 2,
164
+ y: 90,
165
+ text: "Drop files here or click to browse",
166
+ align: "center",
167
+ valign: "top",
168
+ fontSize: 13,
169
+ fontFamily: font,
170
+ weight: 400,
171
+ fill: colors.textMuted
172
+ }
173
+ ],
174
+ opacity: 0,
175
+ timeline: {
176
+ tracks: {
177
+ opacity: {
178
+ keyframes: [
179
+ { time: 0.3, value: 0, out: curves.ease },
180
+ { time: 0.8, value: 1 },
181
+ // Fade out when file is selected
182
+ { time: 5.8, value: 1, out: curves.ease },
183
+ { time: 6.3, value: 0 }
184
+ ]
185
+ }
186
+ }
187
+ }
188
+ });
189
+
190
+ // Upload button (in empty state)
191
+ elements.push({
192
+ id: "upload-btn",
193
+ type: "group",
194
+ x: cardX + cardW / 2 - 70,
195
+ y: cardY + 280,
196
+ children: [
197
+ {
198
+ id: "upload-btn-bg",
199
+ type: "path",
200
+ d: roundedRect(0, 0, 140, 40, 8),
201
+ fill: colors.btnPrimary,
202
+ stroke: "none"
203
+ },
204
+ {
205
+ id: "upload-btn-text",
206
+ type: "text",
207
+ x: 70,
208
+ y: 20,
209
+ text: "Choose File",
210
+ align: "center",
211
+ valign: "middle",
212
+ fontSize: 14,
213
+ fontFamily: font,
214
+ weight: 600,
215
+ fill: colors.btnPrimaryText
216
+ }
217
+ ],
218
+ opacity: 0,
219
+ origin: [70, 20],
220
+ timeline: {
221
+ tracks: {
222
+ opacity: {
223
+ keyframes: [
224
+ { time: 0.5, value: 0, out: curves.ease },
225
+ { time: 1, value: 1 },
226
+ // Fade out with upload zone
227
+ { time: 5.8, value: 1, out: curves.ease },
228
+ { time: 6.3, value: 0 }
229
+ ]
230
+ },
231
+ scale: {
232
+ keyframes: [
233
+ { time: 4.8, value: 1, out: curves.ease },
234
+ { time: 4.95, value: 0.95 },
235
+ { time: 5.1, value: 1 }
236
+ ]
237
+ }
238
+ }
239
+ }
240
+ });
241
+
242
+ // === SCENE 2: File selected state (6-14s) ===
243
+ elements.push({
244
+ id: "file-item",
245
+ type: "group",
246
+ x: cardX + 32,
247
+ y: cardY + 100,
248
+ children: [
249
+ {
250
+ id: "file-bg",
251
+ type: "path",
252
+ d: roundedRect(0, 0, cardW - 64, 64, 10),
253
+ fill: colors.panelBg,
254
+ stroke: colors.accent,
255
+ strokeWidth: 2
256
+ },
257
+ // File icon
258
+ {
259
+ id: "file-icon-bg",
260
+ type: "path",
261
+ d: roundedRect(16, 14, 36, 36, 6),
262
+ fill: colors.successBg,
263
+ stroke: "none"
264
+ },
265
+ {
266
+ id: "file-icon",
267
+ type: "path",
268
+ d: "M 28 24 L 28 40 M 28 32 L 22 32 L 28 26 L 34 32 L 28 32",
269
+ fill: "none",
270
+ stroke: colors.success,
271
+ strokeWidth: 2,
272
+ strokeCap: "round",
273
+ strokeJoin: "round"
274
+ },
275
+ {
276
+ id: "file-name",
277
+ type: "text",
278
+ x: 68,
279
+ y: 22,
280
+ text: "quarterly-report.pdf",
281
+ align: "left",
282
+ valign: "top",
283
+ fontSize: 14,
284
+ fontFamily: font,
285
+ weight: 500,
286
+ fill: colors.text
287
+ },
288
+ {
289
+ id: "file-size",
290
+ type: "text",
291
+ x: 68,
292
+ y: 42,
293
+ text: "2.4 MB · PDF Document",
294
+ align: "left",
295
+ valign: "top",
296
+ fontSize: 12,
297
+ fontFamily: font,
298
+ weight: 400,
299
+ fill: colors.textMuted
300
+ },
301
+ // Checkmark
302
+ {
303
+ id: "file-check",
304
+ type: "path",
305
+ d: "M 0 0 m -10 0 a 10 10 0 1 1 20 0 a 10 10 0 1 1 -20 0",
306
+ x: cardW - 64 - 32,
307
+ y: 32,
308
+ fill: colors.success,
309
+ stroke: "none"
310
+ },
311
+ {
312
+ id: "file-check-mark",
313
+ type: "path",
314
+ d: "M -4 0 L -1 3 L 4 -3",
315
+ x: cardW - 64 - 32,
316
+ y: 32,
317
+ fill: "none",
318
+ stroke: "#ffffff",
319
+ strokeWidth: 2,
320
+ strokeCap: "round",
321
+ strokeJoin: "round"
322
+ }
323
+ ],
324
+ opacity: 0,
325
+ timeline: {
326
+ tracks: {
327
+ opacity: {
328
+ keyframes: [
329
+ // Fade in after upload zone fades out
330
+ { time: 6.3, value: 0, out: curves.ease },
331
+ { time: 6.8, value: 1 },
332
+ // Stay visible until success state
333
+ { time: 13.5, value: 1, out: curves.ease },
334
+ { time: 14, value: 0 }
335
+ ]
336
+ },
337
+ y: {
338
+ keyframes: [
339
+ { time: 6.3, value: cardY + 110, out: curves.ease },
340
+ { time: 6.8, value: cardY + 100 }
341
+ ]
342
+ }
343
+ }
344
+ }
345
+ });
346
+
347
+ // Status text - ready
348
+ elements.push({
349
+ id: "status-text",
350
+ type: "text",
351
+ x: cardX + cardW / 2,
352
+ y: cardY + 190,
353
+ text: "File ready to upload",
354
+ align: "center",
355
+ valign: "top",
356
+ fontSize: 13,
357
+ fontFamily: font,
358
+ weight: 500,
359
+ fill: colors.success,
360
+ opacity: 0,
361
+ timeline: {
362
+ tracks: {
363
+ opacity: {
364
+ keyframes: [
365
+ { time: 7, value: 0, out: curves.ease },
366
+ { time: 7.4, value: 1 },
367
+ // Fade out when uploading starts
368
+ { time: 10.5, value: 1, out: curves.ease },
369
+ { time: 11, value: 0 }
370
+ ]
371
+ }
372
+ }
373
+ }
374
+ });
375
+
376
+ // Confirm button
377
+ const confirmBtnW = 140;
378
+ const confirmBtnH = 42;
379
+ elements.push({
380
+ id: "confirm-btn",
381
+ type: "group",
382
+ x: cardX + cardW - 32 - confirmBtnW,
383
+ y: cardY + cardH - 32 - confirmBtnH,
384
+ children: [
385
+ {
386
+ id: "confirm-btn-bg",
387
+ type: "path",
388
+ d: roundedRect(0, 0, confirmBtnW, confirmBtnH, 8),
389
+ fill: colors.btnPrimary,
390
+ stroke: "none"
391
+ },
392
+ {
393
+ id: "confirm-btn-text",
394
+ type: "text",
395
+ x: confirmBtnW / 2,
396
+ y: confirmBtnH / 2,
397
+ text: "Upload Now",
398
+ align: "center",
399
+ valign: "middle",
400
+ fontSize: 14,
401
+ fontFamily: font,
402
+ weight: 600,
403
+ fill: colors.btnPrimaryText
404
+ }
405
+ ],
406
+ opacity: 0,
407
+ origin: [confirmBtnW / 2, confirmBtnH / 2],
408
+ timeline: {
409
+ tracks: {
410
+ opacity: {
411
+ keyframes: [
412
+ { time: 7.2, value: 0, out: curves.ease },
413
+ { time: 7.6, value: 1 },
414
+ // Fade out when upload starts
415
+ { time: 10.5, value: 1, out: curves.ease },
416
+ { time: 11, value: 0 }
417
+ ]
418
+ },
419
+ scale: {
420
+ keyframes: [
421
+ { time: 9.8, value: 1, out: curves.ease },
422
+ { time: 9.95, value: 0.95 },
423
+ { time: 10.1, value: 1 }
424
+ ]
425
+ }
426
+ }
427
+ }
428
+ });
429
+
430
+ // Cancel button
431
+ elements.push({
432
+ id: "cancel-btn",
433
+ type: "group",
434
+ x: cardX + 32,
435
+ y: cardY + cardH - 32 - confirmBtnH,
436
+ children: [
437
+ {
438
+ id: "cancel-btn-bg",
439
+ type: "path",
440
+ d: roundedRect(0, 0, 100, confirmBtnH, 8),
441
+ fill: colors.btnSecondary,
442
+ stroke: colors.btnSecondaryBorder,
443
+ strokeWidth: 1
444
+ },
445
+ {
446
+ id: "cancel-btn-text",
447
+ type: "text",
448
+ x: 50,
449
+ y: confirmBtnH / 2,
450
+ text: "Cancel",
451
+ align: "center",
452
+ valign: "middle",
453
+ fontSize: 14,
454
+ fontFamily: font,
455
+ weight: 500,
456
+ fill: colors.btnSecondaryText
457
+ }
458
+ ],
459
+ opacity: 0,
460
+ timeline: {
461
+ tracks: {
462
+ opacity: {
463
+ keyframes: [
464
+ { time: 7.2, value: 0, out: curves.ease },
465
+ { time: 7.6, value: 1 },
466
+ // Fade out when upload starts
467
+ { time: 10.5, value: 1, out: curves.ease },
468
+ { time: 11, value: 0 }
469
+ ]
470
+ }
471
+ }
472
+ }
473
+ });
474
+
475
+ // === SCENE 3: Uploading state (11-14s) ===
476
+ elements.push({
477
+ id: "uploading-text",
478
+ type: "text",
479
+ x: cardX + cardW / 2,
480
+ y: cardY + 190,
481
+ text: "Uploading...",
482
+ align: "center",
483
+ valign: "top",
484
+ fontSize: 13,
485
+ fontFamily: font,
486
+ weight: 500,
487
+ fill: colors.accent,
488
+ opacity: 0,
489
+ timeline: {
490
+ tracks: {
491
+ opacity: {
492
+ keyframes: [
493
+ { time: 11, value: 0, out: curves.ease },
494
+ { time: 11.4, value: 1 },
495
+ { time: 13.5, value: 1, out: curves.ease },
496
+ { time: 14, value: 0 }
497
+ ]
498
+ }
499
+ }
500
+ }
501
+ });
502
+
503
+ // Progress bar track
504
+ elements.push({
505
+ id: "progress-track",
506
+ type: "path",
507
+ d: roundedRect(cardX + 80, cardY + 220, cardW - 160, 8, 4),
508
+ fill: colors.inputBg,
509
+ stroke: colors.inputBorder,
510
+ strokeWidth: 1,
511
+ opacity: 0,
512
+ timeline: {
513
+ tracks: {
514
+ opacity: {
515
+ keyframes: [
516
+ { time: 11, value: 0, out: curves.ease },
517
+ { time: 11.4, value: 1 },
518
+ { time: 13.5, value: 1, out: curves.ease },
519
+ { time: 14, value: 0 }
520
+ ]
521
+ }
522
+ }
523
+ }
524
+ });
525
+
526
+ // Progress bar fill (using drawEnd for animation)
527
+ elements.push({
528
+ id: "progress-fill",
529
+ type: "path",
530
+ d: `M ${cardX + 82} ${cardY + 222} L ${cardX + cardW - 82} ${cardY + 222} L ${cardX + cardW - 82} ${cardY + 226} L ${cardX + 82} ${cardY + 226} Z`,
531
+ fill: colors.accent,
532
+ stroke: "none",
533
+ opacity: 0,
534
+ drawEnd: 0,
535
+ timeline: {
536
+ tracks: {
537
+ opacity: {
538
+ keyframes: [
539
+ { time: 11.2, value: 0, out: curves.ease },
540
+ { time: 11.5, value: 1 },
541
+ { time: 13.5, value: 1, out: curves.ease },
542
+ { time: 14, value: 0 }
543
+ ]
544
+ },
545
+ drawEnd: {
546
+ keyframes: [
547
+ { time: 11.5, value: 0, out: curves.gentle },
548
+ { time: 13.5, value: 1 }
549
+ ]
550
+ }
551
+ }
552
+ }
553
+ });
554
+
555
+ // === SCENE 4: Success state (14-18s) ===
556
+ elements.push({
557
+ id: "success-overlay",
558
+ type: "group",
559
+ x: cardX + cardW / 2,
560
+ y: cardY + cardH / 2 - 20,
561
+ children: [
562
+ // Large checkmark circle
563
+ {
564
+ id: "success-circle",
565
+ type: "path",
566
+ d: "M 0 0 m -40 0 a 40 40 0 1 1 80 0 a 40 40 0 1 1 -80 0",
567
+ fill: colors.successBg,
568
+ stroke: colors.success,
569
+ strokeWidth: 2
570
+ },
571
+ {
572
+ id: "success-check",
573
+ type: "path",
574
+ d: "M -14 0 L -4 10 L 14 -10",
575
+ fill: "none",
576
+ stroke: colors.success,
577
+ strokeWidth: 3,
578
+ strokeCap: "round",
579
+ strokeJoin: "round"
580
+ },
581
+ {
582
+ id: "success-title",
583
+ type: "text",
584
+ x: 0,
585
+ y: 70,
586
+ text: "Upload Complete",
587
+ align: "center",
588
+ valign: "top",
589
+ fontSize: 18,
590
+ fontFamily: font,
591
+ weight: 600,
592
+ fill: colors.text
593
+ },
594
+ {
595
+ id: "success-desc",
596
+ type: "text",
597
+ x: 0,
598
+ y: 98,
599
+ text: "Your document has been uploaded successfully.",
600
+ align: "center",
601
+ valign: "top",
602
+ fontSize: 13,
603
+ fontFamily: font,
604
+ weight: 400,
605
+ fill: colors.textMuted
606
+ }
607
+ ],
608
+ opacity: 0,
609
+ origin: [0, 0],
610
+ timeline: {
611
+ tracks: {
612
+ opacity: {
613
+ keyframes: [
614
+ { time: 14, value: 0, out: curves.ease },
615
+ { time: 14.6, value: 1 },
616
+ { time: 17, value: 1, out: curves.ease },
617
+ { time: 17.5, value: 0 }
618
+ ]
619
+ },
620
+ scale: {
621
+ keyframes: [
622
+ { time: 14, value: 0.8, out: curves.ease },
623
+ { time: 14.6, value: 1 }
624
+ ]
625
+ }
626
+ }
627
+ }
628
+ });
629
+
630
+ // === CURSOR (added last to render on top) ===
631
+
632
+ // Calculated positions:
633
+ // Upload button: x = cardX + cardW/2 = 640, y = cardY + 280 + 20 = 460
634
+ // Confirm button: x = cardX + cardW - 32 - confirmBtnW/2 = 798, y = cardY + cardH - 32 - confirmBtnH/2 = 507
635
+ const uploadBtnCenter = { x: cardX + cardW / 2, y: cardY + 280 + 20 };
636
+ const confirmBtnCenter = { x: cardX + cardW - 32 - confirmBtnW / 2, y: cardY + cardH - 32 - confirmBtnH / 2 };
637
+
638
+ // Click ring
639
+ elements.push({
640
+ id: "click-ring",
641
+ type: "path",
642
+ d: "M 0 0 m -18 0 a 18 18 0 1 1 36 0 a 18 18 0 1 1 -36 0",
643
+ x: uploadBtnCenter.x,
644
+ y: uploadBtnCenter.y,
645
+ fill: "none",
646
+ stroke: colors.cursorRing,
647
+ strokeWidth: 2,
648
+ opacity: 0,
649
+ origin: [0, 0],
650
+ timeline: {
651
+ tracks: {
652
+ opacity: {
653
+ keyframes: [
654
+ // Click 1: Upload button
655
+ { time: 4.8, value: 0 },
656
+ { time: 4.9, value: 0.5 },
657
+ { time: 5.3, value: 0 },
658
+ // Click 2: Confirm button
659
+ { time: 9.8, value: 0 },
660
+ { time: 9.9, value: 0.5 },
661
+ { time: 10.3, value: 0 }
662
+ ]
663
+ },
664
+ scale: {
665
+ keyframes: [
666
+ { time: 4.8, value: 0.5, out: curves.easeOut },
667
+ { time: 5.3, value: 1.8 },
668
+ { time: 9.8, value: 0.5, out: curves.easeOut },
669
+ { time: 10.3, value: 1.8 }
670
+ ]
671
+ },
672
+ x: {
673
+ keyframes: [
674
+ { time: 4.8, value: uploadBtnCenter.x },
675
+ { time: 9.8, value: confirmBtnCenter.x }
676
+ ]
677
+ },
678
+ y: {
679
+ keyframes: [
680
+ { time: 4.8, value: uploadBtnCenter.y },
681
+ { time: 9.8, value: confirmBtnCenter.y }
682
+ ]
683
+ }
684
+ }
685
+ }
686
+ });
687
+
688
+ // Cursor
689
+ elements.push({
690
+ id: "cursor",
691
+ type: "group",
692
+ x: -50,
693
+ y: height / 2,
694
+ children: [
695
+ {
696
+ id: "cursor-arrow",
697
+ type: "path",
698
+ d: "M 0 0 L 0 18 L 4.5 14.5 L 7.5 22 L 11 20.5 L 8 13 L 13.5 13 Z",
699
+ fill: colors.cursor,
700
+ stroke: "#ffffff",
701
+ strokeWidth: 1.5
702
+ }
703
+ ],
704
+ timeline: {
705
+ tracks: {
706
+ x: {
707
+ keyframes: [
708
+ // Enter from left
709
+ { time: 2, value: -50, out: curves.ease },
710
+ { time: 3, value: uploadBtnCenter.x },
711
+ // Stay on upload button, click at 4.8
712
+ { time: 5.5, value: uploadBtnCenter.x },
713
+ // Move to confirm button
714
+ { time: 7.5, value: uploadBtnCenter.x, out: curves.ease },
715
+ { time: 8.5, value: confirmBtnCenter.x },
716
+ // Stay on confirm button, click at 9.8
717
+ { time: 10.5, value: confirmBtnCenter.x },
718
+ // Exit
719
+ { time: 13, value: confirmBtnCenter.x, out: curves.ease },
720
+ { time: 14, value: width + 50 }
721
+ ]
722
+ },
723
+ y: {
724
+ keyframes: [
725
+ { time: 2, value: height / 2, out: curves.ease },
726
+ { time: 3, value: uploadBtnCenter.y },
727
+ { time: 5.5, value: uploadBtnCenter.y },
728
+ { time: 7.5, value: uploadBtnCenter.y, out: curves.ease },
729
+ { time: 8.5, value: confirmBtnCenter.y },
730
+ { time: 10.5, value: confirmBtnCenter.y },
731
+ { time: 13, value: confirmBtnCenter.y, out: curves.ease },
732
+ { time: 14, value: height + 50 }
733
+ ]
734
+ }
735
+ }
736
+ }
737
+ });
738
+
739
+ function roundedRect(x, y, w, h, r) {
740
+ return [
741
+ `M ${x + r} ${y}`,
742
+ `L ${x + w - r} ${y}`,
743
+ `Q ${x + w} ${y} ${x + w} ${y + r}`,
744
+ `L ${x + w} ${y + h - r}`,
745
+ `Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
746
+ `L ${x + r} ${y + h}`,
747
+ `Q ${x} ${y + h} ${x} ${y + h - r}`,
748
+ `L ${x} ${y + r}`,
749
+ `Q ${x} ${y} ${x + r} ${y}`,
750
+ "Z"
751
+ ].join(" ");
752
+ }
753
+
754
+ const doc = {
755
+ version: 1,
756
+ canvas: { width, height, background: bg, duration, fps },
757
+ elements
758
+ };
759
+
760
+ const outPath = path.join(__dirname, "ui-flow.visual.json");
761
+ fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
762
+ console.log("Written:", outPath);