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,815 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const width = 1280;
5
+ const height = 720;
6
+ const duration = 24;
7
+ const fps = 30;
8
+ const bg = "#f8fafc";
9
+ const font = "Inter, system-ui, sans-serif";
10
+
11
+ const colors = {
12
+ hero: "#0f172a",
13
+ heroSub: "#64748b",
14
+ panelBg: "#ffffff",
15
+ panelBorder: "#e2e8f0",
16
+ panelTitle: "#1e293b",
17
+ panelBody: "#475569",
18
+ panelMuted: "#94a3b8",
19
+ inputBg: "#f8fafc",
20
+ inputBorder: "#cbd5e1",
21
+ inputText: "#64748b",
22
+ btnPrimaryBg: "#2563eb",
23
+ btnPrimaryText: "#ffffff",
24
+ btnSecondaryBg: "#ffffff",
25
+ btnSecondaryText: "#475569",
26
+ btnSecondaryBorder: "#e2e8f0",
27
+ accent: "#2563eb",
28
+ success: "#10b981",
29
+ successBg: "#d1fae5",
30
+ cursor: "#0f172a",
31
+ cursorRing: "#3b82f6"
32
+ };
33
+
34
+ const curves = {
35
+ ease: { type: "cubicBezier", x1: 0.4, y1: 0, x2: 0.2, y2: 1 },
36
+ easeOut: { type: "cubicBezier", x1: 0, y1: 0, x2: 0.2, y2: 1 },
37
+ snap: { type: "cubicBezier", x1: 0.2, y1: 1, x2: 0.2, y2: 1 }
38
+ };
39
+
40
+ const elements = [];
41
+
42
+ // Cursor elements stored separately, added at the end so they render on top
43
+ // Button positions (calculated from layout):
44
+ // Create button: dashX + dashW - 140 + 56 = 996, dashY + 24 + 20 = 164
45
+ // Input field: ~640, ~335 (center of input)
46
+ // Save button: modalX + modalW - 150 + 61 = 791, modalY + modalH - 68 + 20 = 482
47
+ const createBtnX = 996;
48
+ const createBtnY = 164;
49
+ const inputFieldX = 640;
50
+ const inputFieldY = 335;
51
+ const saveBtnX = 791;
52
+ const saveBtnY = 482;
53
+
54
+ const cursorElement = {
55
+ id: "cursor",
56
+ type: "group",
57
+ x: -50,
58
+ y: -50,
59
+ children: [
60
+ {
61
+ id: "cursor-arrow",
62
+ type: "path",
63
+ d: "M 0 0 L 0 20 L 5 16 L 8 24 L 12 22 L 9 14 L 15 14 Z",
64
+ fill: colors.cursor,
65
+ stroke: "#ffffff",
66
+ strokeWidth: 1.5
67
+ }
68
+ ],
69
+ timeline: {
70
+ tracks: {
71
+ x: {
72
+ keyframes: [
73
+ { time: 0, value: -50 },
74
+ // Move to Create button
75
+ { time: 5.5, value: -50, out: curves.ease },
76
+ { time: 6.5, value: createBtnX },
77
+ { time: 8, value: createBtnX },
78
+ // Move to input field
79
+ { time: 10, value: createBtnX, out: curves.ease },
80
+ { time: 11, value: inputFieldX },
81
+ // Move to Save button
82
+ { time: 16, value: inputFieldX, out: curves.ease },
83
+ { time: 17, value: saveBtnX },
84
+ // Exit
85
+ { time: 20, value: saveBtnX, out: curves.ease },
86
+ { time: 21, value: 1400 }
87
+ ]
88
+ },
89
+ y: {
90
+ keyframes: [
91
+ { time: 0, value: -50 },
92
+ { time: 5.5, value: -50, out: curves.ease },
93
+ { time: 6.5, value: createBtnY },
94
+ { time: 8, value: createBtnY },
95
+ { time: 10, value: createBtnY, out: curves.ease },
96
+ { time: 11, value: inputFieldY },
97
+ { time: 16, value: inputFieldY, out: curves.ease },
98
+ { time: 17, value: saveBtnY },
99
+ { time: 20, value: saveBtnY, out: curves.ease },
100
+ { time: 21, value: 800 }
101
+ ]
102
+ }
103
+ }
104
+ }
105
+ };
106
+
107
+ const clickRingElement = {
108
+ id: "click-ring",
109
+ type: "path",
110
+ d: "M 0 0 m -16 0 a 16 16 0 1 1 32 0 a 16 16 0 1 1 -32 0",
111
+ x: createBtnX,
112
+ y: createBtnY,
113
+ fill: "none",
114
+ stroke: colors.cursorRing,
115
+ strokeWidth: 2,
116
+ opacity: 0,
117
+ origin: [0, 0],
118
+ timeline: {
119
+ tracks: {
120
+ opacity: {
121
+ keyframes: [
122
+ // Click 1: Create button
123
+ { time: 7, value: 0 },
124
+ { time: 7.1, value: 0.6 },
125
+ { time: 7.5, value: 0 },
126
+ // Click 2: Save button
127
+ { time: 17.5, value: 0 },
128
+ { time: 17.6, value: 0.6 },
129
+ { time: 18, value: 0 }
130
+ ]
131
+ },
132
+ scale: {
133
+ keyframes: [
134
+ { time: 7, value: 0.5, out: curves.easeOut },
135
+ { time: 7.5, value: 1.5 },
136
+ { time: 17.5, value: 0.5, out: curves.easeOut },
137
+ { time: 18, value: 1.5 }
138
+ ]
139
+ },
140
+ x: {
141
+ keyframes: [
142
+ { time: 7, value: createBtnX },
143
+ { time: 17.5, value: saveBtnX }
144
+ ]
145
+ },
146
+ y: {
147
+ keyframes: [
148
+ { time: 7, value: createBtnY },
149
+ { time: 17.5, value: saveBtnY }
150
+ ]
151
+ }
152
+ }
153
+ }
154
+ };
155
+
156
+ // === SCENE 1: Hero (0-5s) ===
157
+ elements.push({
158
+ id: "hero-title",
159
+ type: "text",
160
+ x: width / 2,
161
+ y: 280,
162
+ text: "Build beautiful interfaces",
163
+ align: "center",
164
+ valign: "middle",
165
+ fontSize: 56,
166
+ fontFamily: font,
167
+ weight: 800,
168
+ fill: colors.hero,
169
+ opacity: 0,
170
+ timeline: {
171
+ tracks: {
172
+ opacity: {
173
+ keyframes: [
174
+ { time: 0.2, value: 0, out: curves.ease },
175
+ { time: 0.8, value: 1 },
176
+ { time: 4, value: 1, out: curves.ease },
177
+ { time: 4.6, value: 0 }
178
+ ]
179
+ },
180
+ y: {
181
+ keyframes: [
182
+ { time: 0.2, value: 300, out: curves.ease },
183
+ { time: 0.8, value: 280 }
184
+ ]
185
+ },
186
+ scale: {
187
+ keyframes: [
188
+ { time: 0.2, value: 0.95, out: curves.ease },
189
+ { time: 0.8, value: 1 }
190
+ ]
191
+ }
192
+ }
193
+ }
194
+ });
195
+
196
+ elements.push({
197
+ id: "hero-sub",
198
+ type: "text",
199
+ x: width / 2,
200
+ y: 360,
201
+ text: "Design, prototype, and ship — all in one place.",
202
+ align: "center",
203
+ valign: "middle",
204
+ fontSize: 22,
205
+ fontFamily: font,
206
+ weight: 400,
207
+ fill: colors.heroSub,
208
+ opacity: 0,
209
+ timeline: {
210
+ tracks: {
211
+ opacity: {
212
+ keyframes: [
213
+ { time: 0.6, value: 0, out: curves.ease },
214
+ { time: 1.2, value: 1 },
215
+ { time: 4, value: 1, out: curves.ease },
216
+ { time: 4.6, value: 0 }
217
+ ]
218
+ }
219
+ }
220
+ }
221
+ });
222
+
223
+ // === SCENE 2: Dashboard panel (5-10s) ===
224
+ const dashX = 200;
225
+ const dashY = 120;
226
+ const dashW = 880;
227
+ const dashH = 480;
228
+
229
+ elements.push({
230
+ id: "dash-panel",
231
+ type: "group",
232
+ x: dashX,
233
+ y: dashY,
234
+ opacity: 0,
235
+ children: [
236
+ {
237
+ id: "dash-bg",
238
+ type: "path",
239
+ d: roundedRect(0, 0, dashW, dashH, 12),
240
+ fill: colors.panelBg,
241
+ stroke: colors.panelBorder,
242
+ strokeWidth: 1
243
+ },
244
+ // Panel header
245
+ {
246
+ id: "dash-title",
247
+ type: "text",
248
+ x: 28,
249
+ y: 28,
250
+ text: "Your Projects",
251
+ align: "left",
252
+ valign: "top",
253
+ fontSize: 20,
254
+ fontFamily: font,
255
+ weight: 600,
256
+ fill: colors.panelTitle
257
+ },
258
+ {
259
+ id: "dash-subtitle",
260
+ type: "text",
261
+ x: 28,
262
+ y: 56,
263
+ text: "3 projects · Last updated 2 hours ago",
264
+ align: "left",
265
+ valign: "top",
266
+ fontSize: 13,
267
+ fontFamily: font,
268
+ weight: 400,
269
+ fill: colors.panelMuted
270
+ },
271
+ // Divider
272
+ {
273
+ id: "dash-divider",
274
+ type: "path",
275
+ d: `M 0 90 L ${dashW} 90`,
276
+ stroke: colors.panelBorder,
277
+ strokeWidth: 1,
278
+ fill: "none"
279
+ },
280
+ // Project rows
281
+ ...projectRow(0, "Marketing Website", "12 screens · Published", 110),
282
+ ...projectRow(1, "Mobile App v2", "8 screens · Draft", 170),
283
+ ...projectRow(2, "Dashboard Redesign", "24 screens · In review", 230)
284
+ ],
285
+ timeline: {
286
+ tracks: {
287
+ opacity: {
288
+ keyframes: [
289
+ { time: 5, value: 0, out: curves.ease },
290
+ { time: 5.6, value: 1 },
291
+ { time: 9, value: 1, out: curves.ease },
292
+ { time: 9.6, value: 0 }
293
+ ]
294
+ },
295
+ scale: {
296
+ keyframes: [
297
+ { time: 5, value: 0.96, out: curves.ease },
298
+ { time: 5.6, value: 1 }
299
+ ]
300
+ }
301
+ }
302
+ }
303
+ });
304
+
305
+ // Create button (separate for zoom effect on click)
306
+ elements.push({
307
+ id: "create-btn",
308
+ type: "group",
309
+ x: dashX + dashW - 140,
310
+ y: dashY + 24,
311
+ opacity: 0,
312
+ children: [
313
+ {
314
+ id: "create-btn-bg",
315
+ type: "path",
316
+ d: roundedRect(0, 0, 112, 40, 8),
317
+ fill: colors.btnPrimaryBg,
318
+ stroke: "none"
319
+ },
320
+ {
321
+ id: "create-btn-text",
322
+ type: "text",
323
+ x: 56,
324
+ y: 20,
325
+ text: "Create New",
326
+ align: "center",
327
+ valign: "middle",
328
+ fontSize: 14,
329
+ fontFamily: font,
330
+ weight: 600,
331
+ fill: colors.btnPrimaryText
332
+ }
333
+ ],
334
+ timeline: {
335
+ tracks: {
336
+ opacity: {
337
+ keyframes: [
338
+ { time: 5.3, value: 0, out: curves.ease },
339
+ { time: 5.8, value: 1 },
340
+ { time: 9, value: 1, out: curves.ease },
341
+ { time: 9.6, value: 0 }
342
+ ]
343
+ },
344
+ scale: {
345
+ keyframes: [
346
+ { time: 7, value: 1, out: curves.snap },
347
+ { time: 7.15, value: 0.95 },
348
+ { time: 7.3, value: 1 }
349
+ ]
350
+ }
351
+ }
352
+ },
353
+ origin: [56, 20]
354
+ });
355
+
356
+ // === SCENE 3: Create form modal (10-17s) ===
357
+ const modalW = 480;
358
+ const modalH = 340;
359
+ const modalX = (width - modalW) / 2;
360
+ const modalY = (height - modalH) / 2;
361
+
362
+ // Modal backdrop
363
+ elements.push({
364
+ id: "modal-backdrop",
365
+ type: "path",
366
+ d: `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`,
367
+ fill: colors.hero,
368
+ opacity: 0,
369
+ timeline: {
370
+ tracks: {
371
+ opacity: {
372
+ keyframes: [
373
+ { time: 10, value: 0, out: curves.ease },
374
+ { time: 10.4, value: 0.4 },
375
+ { time: 19, value: 0.4, out: curves.ease },
376
+ { time: 19.5, value: 0 }
377
+ ]
378
+ }
379
+ }
380
+ }
381
+ });
382
+
383
+ elements.push({
384
+ id: "modal",
385
+ type: "group",
386
+ x: modalX,
387
+ y: modalY,
388
+ opacity: 0,
389
+ children: [
390
+ {
391
+ id: "modal-bg",
392
+ type: "path",
393
+ d: roundedRect(0, 0, modalW, modalH, 12),
394
+ fill: colors.panelBg,
395
+ stroke: "none"
396
+ },
397
+ {
398
+ id: "modal-title",
399
+ type: "text",
400
+ x: 28,
401
+ y: 28,
402
+ text: "Create new project",
403
+ align: "left",
404
+ valign: "top",
405
+ fontSize: 18,
406
+ fontFamily: font,
407
+ weight: 600,
408
+ fill: colors.panelTitle
409
+ },
410
+ {
411
+ id: "modal-desc",
412
+ type: "text",
413
+ x: 28,
414
+ y: 56,
415
+ text: "Give your project a name and choose a template to get started.",
416
+ align: "left",
417
+ valign: "top",
418
+ fontSize: 13,
419
+ fontFamily: font,
420
+ weight: 400,
421
+ fill: colors.panelBody,
422
+ maxWidth: modalW - 56
423
+ },
424
+ // Project name field
425
+ {
426
+ id: "field-label",
427
+ type: "text",
428
+ x: 28,
429
+ y: 100,
430
+ text: "Project name",
431
+ align: "left",
432
+ valign: "top",
433
+ fontSize: 13,
434
+ fontFamily: font,
435
+ weight: 500,
436
+ fill: colors.panelTitle
437
+ },
438
+ {
439
+ id: "field-input-bg",
440
+ type: "path",
441
+ d: roundedRect(28, 124, modalW - 56, 42, 6),
442
+ fill: colors.inputBg,
443
+ stroke: colors.inputBorder,
444
+ strokeWidth: 1
445
+ },
446
+ {
447
+ id: "field-placeholder",
448
+ type: "text",
449
+ x: 40,
450
+ y: 145,
451
+ text: "Enter project name...",
452
+ align: "left",
453
+ valign: "middle",
454
+ fontSize: 14,
455
+ fontFamily: font,
456
+ weight: 400,
457
+ fill: colors.inputText
458
+ },
459
+ // Template selector
460
+ {
461
+ id: "template-label",
462
+ type: "text",
463
+ x: 28,
464
+ y: 186,
465
+ text: "Template",
466
+ align: "left",
467
+ valign: "top",
468
+ fontSize: 13,
469
+ fontFamily: font,
470
+ weight: 500,
471
+ fill: colors.panelTitle
472
+ },
473
+ ...templateOption(0, "Blank", 28, 210, true),
474
+ ...templateOption(1, "Dashboard", 138, 210, false),
475
+ ...templateOption(2, "Landing", 268, 210, false)
476
+ ],
477
+ timeline: {
478
+ tracks: {
479
+ opacity: {
480
+ keyframes: [
481
+ { time: 10, value: 0, out: curves.ease },
482
+ { time: 10.5, value: 1 },
483
+ { time: 19, value: 1, out: curves.ease },
484
+ { time: 19.5, value: 0 }
485
+ ]
486
+ },
487
+ scale: {
488
+ keyframes: [
489
+ { time: 10, value: 0.9, out: curves.ease },
490
+ { time: 10.5, value: 1 },
491
+ { time: 19, value: 1, out: curves.ease },
492
+ { time: 19.5, value: 0.95 }
493
+ ]
494
+ },
495
+ y: {
496
+ keyframes: [
497
+ { time: 10, value: modalY + 30, out: curves.ease },
498
+ { time: 10.5, value: modalY }
499
+ ]
500
+ }
501
+ }
502
+ },
503
+ origin: [modalW / 2, modalH / 2]
504
+ });
505
+
506
+ // Typing animation - text appears
507
+ elements.push({
508
+ id: "typed-text",
509
+ type: "text",
510
+ x: modalX + 40,
511
+ y: modalY + 145,
512
+ text: "Product Launch 2024",
513
+ align: "left",
514
+ valign: "middle",
515
+ fontSize: 14,
516
+ fontFamily: font,
517
+ weight: 400,
518
+ fill: colors.panelTitle,
519
+ opacity: 0,
520
+ timeline: {
521
+ tracks: {
522
+ opacity: {
523
+ keyframes: [
524
+ { time: 12, value: 0 },
525
+ { time: 12.5, value: 1 },
526
+ { time: 19, value: 1, out: curves.ease },
527
+ { time: 19.5, value: 0 }
528
+ ]
529
+ }
530
+ }
531
+ }
532
+ });
533
+
534
+ // Modal buttons
535
+ elements.push({
536
+ id: "cancel-btn",
537
+ type: "group",
538
+ x: modalX + 28,
539
+ y: modalY + modalH - 68,
540
+ opacity: 0,
541
+ children: [
542
+ {
543
+ id: "cancel-btn-bg",
544
+ type: "path",
545
+ d: roundedRect(0, 0, 90, 40, 8),
546
+ fill: colors.btnSecondaryBg,
547
+ stroke: colors.btnSecondaryBorder,
548
+ strokeWidth: 1
549
+ },
550
+ {
551
+ id: "cancel-btn-text",
552
+ type: "text",
553
+ x: 45,
554
+ y: 20,
555
+ text: "Cancel",
556
+ align: "center",
557
+ valign: "middle",
558
+ fontSize: 14,
559
+ fontFamily: font,
560
+ weight: 500,
561
+ fill: colors.btnSecondaryText
562
+ }
563
+ ],
564
+ timeline: {
565
+ tracks: {
566
+ opacity: {
567
+ keyframes: [
568
+ { time: 10.6, value: 0, out: curves.ease },
569
+ { time: 11, value: 1 },
570
+ { time: 19, value: 1, out: curves.ease },
571
+ { time: 19.5, value: 0 }
572
+ ]
573
+ }
574
+ }
575
+ }
576
+ });
577
+
578
+ elements.push({
579
+ id: "save-btn",
580
+ type: "group",
581
+ x: modalX + modalW - 150,
582
+ y: modalY + modalH - 68,
583
+ opacity: 0,
584
+ children: [
585
+ {
586
+ id: "save-btn-bg",
587
+ type: "path",
588
+ d: roundedRect(0, 0, 122, 40, 8),
589
+ fill: colors.btnPrimaryBg,
590
+ stroke: "none"
591
+ },
592
+ {
593
+ id: "save-btn-text",
594
+ type: "text",
595
+ x: 61,
596
+ y: 20,
597
+ text: "Create Project",
598
+ align: "center",
599
+ valign: "middle",
600
+ fontSize: 14,
601
+ fontFamily: font,
602
+ weight: 600,
603
+ fill: colors.btnPrimaryText
604
+ }
605
+ ],
606
+ origin: [61, 20],
607
+ timeline: {
608
+ tracks: {
609
+ opacity: {
610
+ keyframes: [
611
+ { time: 10.6, value: 0, out: curves.ease },
612
+ { time: 11, value: 1 },
613
+ { time: 19, value: 1, out: curves.ease },
614
+ { time: 19.5, value: 0 }
615
+ ]
616
+ },
617
+ scale: {
618
+ keyframes: [
619
+ { time: 17.5, value: 1, out: curves.snap },
620
+ { time: 17.65, value: 0.95 },
621
+ { time: 17.8, value: 1 }
622
+ ]
623
+ }
624
+ }
625
+ }
626
+ });
627
+
628
+ // === SCENE 4: Success state (19-24s) ===
629
+ elements.push({
630
+ id: "success-panel",
631
+ type: "group",
632
+ x: (width - 400) / 2,
633
+ y: 240,
634
+ opacity: 0,
635
+ children: [
636
+ {
637
+ id: "success-bg",
638
+ type: "path",
639
+ d: roundedRect(0, 0, 400, 200, 12),
640
+ fill: colors.panelBg,
641
+ stroke: colors.panelBorder,
642
+ strokeWidth: 1
643
+ },
644
+ // Checkmark circle
645
+ {
646
+ id: "success-circle",
647
+ type: "path",
648
+ d: "M 200 50 m -30 0 a 30 30 0 1 1 60 0 a 30 30 0 1 1 -60 0",
649
+ fill: colors.successBg,
650
+ stroke: "none"
651
+ },
652
+ {
653
+ id: "success-check",
654
+ type: "path",
655
+ d: "M 186 50 L 196 60 L 214 42",
656
+ fill: "none",
657
+ stroke: colors.success,
658
+ strokeWidth: 3,
659
+ strokeCap: "round",
660
+ strokeJoin: "round"
661
+ },
662
+ {
663
+ id: "success-title",
664
+ type: "text",
665
+ x: 200,
666
+ y: 105,
667
+ text: "Project created!",
668
+ align: "center",
669
+ valign: "top",
670
+ fontSize: 20,
671
+ fontFamily: font,
672
+ weight: 600,
673
+ fill: colors.panelTitle
674
+ },
675
+ {
676
+ id: "success-desc",
677
+ type: "text",
678
+ x: 200,
679
+ y: 135,
680
+ text: "Your new project is ready. Start designing\nyour first screen now.",
681
+ align: "center",
682
+ valign: "top",
683
+ fontSize: 14,
684
+ fontFamily: font,
685
+ weight: 400,
686
+ lineHeight: 1.5,
687
+ fill: colors.panelBody,
688
+ maxWidth: 340
689
+ }
690
+ ],
691
+ timeline: {
692
+ tracks: {
693
+ opacity: {
694
+ keyframes: [
695
+ { time: 19.5, value: 0, out: curves.ease },
696
+ { time: 20.2, value: 1 },
697
+ { time: 23, value: 1, out: curves.ease },
698
+ { time: 23.6, value: 0 }
699
+ ]
700
+ },
701
+ scale: {
702
+ keyframes: [
703
+ { time: 19.5, value: 0.9, out: curves.ease },
704
+ { time: 20.2, value: 1 }
705
+ ]
706
+ },
707
+ y: {
708
+ keyframes: [
709
+ { time: 19.5, value: 260, out: curves.ease },
710
+ { time: 20.2, value: 240 }
711
+ ]
712
+ }
713
+ }
714
+ },
715
+ origin: [200, 100]
716
+ });
717
+
718
+ // --- Helpers ---
719
+
720
+ function projectRow(i, name, meta, yPos) {
721
+ return [
722
+ {
723
+ id: `proj-${i}-name`,
724
+ type: "text",
725
+ x: 28,
726
+ y: yPos,
727
+ text: name,
728
+ align: "left",
729
+ valign: "top",
730
+ fontSize: 15,
731
+ fontFamily: font,
732
+ weight: 500,
733
+ fill: colors.panelTitle
734
+ },
735
+ {
736
+ id: `proj-${i}-meta`,
737
+ type: "text",
738
+ x: 28,
739
+ y: yPos + 22,
740
+ text: meta,
741
+ align: "left",
742
+ valign: "top",
743
+ fontSize: 12,
744
+ fontFamily: font,
745
+ weight: 400,
746
+ fill: colors.panelMuted
747
+ }
748
+ ];
749
+ }
750
+
751
+ function templateOption(i, label, x, y, selected) {
752
+ const w = 100;
753
+ const h = 60;
754
+ return [
755
+ {
756
+ id: `tpl-${i}-bg`,
757
+ type: "path",
758
+ d: roundedRect(x, y, w, h, 6),
759
+ fill: selected ? colors.accent : colors.inputBg,
760
+ stroke: selected ? colors.accent : colors.inputBorder,
761
+ strokeWidth: selected ? 2 : 1,
762
+ opacity: selected ? 0.1 : 1
763
+ },
764
+ {
765
+ id: `tpl-${i}-border`,
766
+ type: "path",
767
+ d: roundedRect(x, y, w, h, 6),
768
+ fill: "none",
769
+ stroke: selected ? colors.accent : colors.inputBorder,
770
+ strokeWidth: selected ? 2 : 1
771
+ },
772
+ {
773
+ id: `tpl-${i}-label`,
774
+ type: "text",
775
+ x: x + w / 2,
776
+ y: y + h / 2,
777
+ text: label,
778
+ align: "center",
779
+ valign: "middle",
780
+ fontSize: 13,
781
+ fontFamily: font,
782
+ weight: selected ? 600 : 400,
783
+ fill: selected ? colors.accent : colors.panelBody
784
+ }
785
+ ];
786
+ }
787
+
788
+ function roundedRect(x, y, w, h, r) {
789
+ return [
790
+ `M ${x + r} ${y}`,
791
+ `L ${x + w - r} ${y}`,
792
+ `Q ${x + w} ${y} ${x + w} ${y + r}`,
793
+ `L ${x + w} ${y + h - r}`,
794
+ `Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
795
+ `L ${x + r} ${y + h}`,
796
+ `Q ${x} ${y + h} ${x} ${y + h - r}`,
797
+ `L ${x} ${y + r}`,
798
+ `Q ${x} ${y} ${x + r} ${y}`,
799
+ "Z"
800
+ ].join(" ");
801
+ }
802
+
803
+ // Add cursor elements last so they render on top of everything
804
+ elements.push(clickRingElement);
805
+ elements.push(cursorElement);
806
+
807
+ const doc = {
808
+ version: 1,
809
+ canvas: { width, height, background: bg, duration, fps },
810
+ elements
811
+ };
812
+
813
+ const outPath = path.join(__dirname, "walkthrough.visual.json");
814
+ fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
815
+ console.log("Written:", outPath);