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,1094 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const width = 1280;
5
+ const height = 720;
6
+ const duration = 24;
7
+ const fps = 30;
8
+ const bg = "#07111f";
9
+ const font = "Roboto, Arial, sans-serif";
10
+
11
+ const colors = {
12
+ bg,
13
+ shell: "#f8fafc",
14
+ shellBorder: "#dbe4f0",
15
+ topBar: "#ffffff",
16
+ sidebar: "#0f172a",
17
+ sidebarMuted: "#94a3b8",
18
+ sidebarActive: "#eff6ff",
19
+ sidebarActiveText: "#2563eb",
20
+ pageTitle: "#0f172a",
21
+ body: "#475569",
22
+ muted: "#64748b",
23
+ card: "#ffffff",
24
+ cardBorder: "#dbe4f0",
25
+ accent: "#2563eb",
26
+ accentSoft: "#dbeafe",
27
+ success: "#10b981",
28
+ successSoft: "#d1fae5",
29
+ warning: "#f59e0b",
30
+ warningSoft: "#fef3c7",
31
+ toggleOff: "#dbe3ee",
32
+ toggleKnob: "#ffffff",
33
+ divider: "#e5edf6",
34
+ chipBg: "#102444",
35
+ chipStroke: "#264b87",
36
+ chipText: "#93c5fd",
37
+ captionBg: "#09192e",
38
+ captionBorder: "#173054",
39
+ captionText: "#e2e8f0",
40
+ captionMuted: "#94a3b8",
41
+ cursor: "#0f172a",
42
+ cursorRing: "#38bdf8",
43
+ focus: "#22d3ee",
44
+ ghost: "#cbd5e1"
45
+ };
46
+
47
+ const curves = {
48
+ ease: { type: "cubicBezier", x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 },
49
+ easeOut: { type: "cubicBezier", x1: 0, y1: 0, x2: 0.2, y2: 1 },
50
+ snap: { type: "cubicBezier", x1: 0.2, y1: 1, x2: 0.2, y2: 1 }
51
+ };
52
+
53
+ const screen = { x: 0, y: 0, w: 1120, h: 580 };
54
+ const contentX = 236;
55
+
56
+ const notificationsCard = { x: 618, y: 96, w: 430, h: 198 };
57
+ const billingCard = { x: 236, y: 316, w: 400, h: 188 };
58
+ const reviewCard = { x: 658, y: 332, w: 390, h: 172 };
59
+
60
+ const weeklyToggleCenter = { x: notificationsCard.x + 352, y: notificationsCard.y + 105 };
61
+ const annualPillCenter = { x: billingCard.x + 160, y: billingCard.y + 103 };
62
+ const saveButtonCenter = { x: reviewCard.x + 94, y: reviewCard.y + 128 };
63
+
64
+ const overviewPose = {
65
+ x: (width - screen.w * 0.92) / 2,
66
+ y: 92,
67
+ scale: 0.92
68
+ };
69
+ const notifPose = cameraPose(notificationsCard.x + notificationsCard.w / 2, notificationsCard.y + notificationsCard.h / 2, 1.23);
70
+ const billingPose = cameraPose(billingCard.x + billingCard.w / 2, billingCard.y + billingCard.h / 2, 1.26);
71
+ const reviewPose = cameraPose(reviewCard.x + reviewCard.w / 2, reviewCard.y + reviewCard.h / 2, 1.34);
72
+
73
+ const elements = [];
74
+
75
+ elements.push({
76
+ id: "intro-title",
77
+ type: "text",
78
+ x: width / 2,
79
+ y: 92,
80
+ text: "Explaining a UI demo with cursor + zoom",
81
+ align: "center",
82
+ valign: "middle",
83
+ fontSize: 42,
84
+ fontFamily: font,
85
+ weight: 700,
86
+ fill: "#f8fafc",
87
+ opacity: 0,
88
+ timeline: {
89
+ tracks: {
90
+ opacity: {
91
+ keyframes: [
92
+ { time: 0, value: 0, out: curves.ease },
93
+ { time: 0.8, value: 1 },
94
+ { time: 2.8, value: 1, out: curves.ease },
95
+ { time: 3.4, value: 0 }
96
+ ]
97
+ },
98
+ y: {
99
+ keyframes: [
100
+ { time: 0, value: 108, out: curves.easeOut },
101
+ { time: 0.8, value: 92 }
102
+ ]
103
+ }
104
+ }
105
+ }
106
+ });
107
+
108
+ elements.push({
109
+ id: "intro-subtitle",
110
+ type: "text",
111
+ x: width / 2,
112
+ y: 138,
113
+ text: "One clean screen. One action at a time.",
114
+ align: "center",
115
+ valign: "middle",
116
+ fontSize: 18,
117
+ fontFamily: font,
118
+ weight: 400,
119
+ fill: "#93a9c7",
120
+ opacity: 0,
121
+ timeline: {
122
+ tracks: {
123
+ opacity: {
124
+ keyframes: [
125
+ { time: 0.2, value: 0, out: curves.ease },
126
+ { time: 1, value: 1 },
127
+ { time: 2.8, value: 1, out: curves.ease },
128
+ { time: 3.4, value: 0 }
129
+ ]
130
+ }
131
+ }
132
+ }
133
+ });
134
+
135
+ elements.push(captionCard(
136
+ "caption_overview",
137
+ 2.4,
138
+ 7.4,
139
+ "Overview",
140
+ "Start wide, show the whole product, and establish where the user is."
141
+ ));
142
+ elements.push(captionCard(
143
+ "caption_notifications",
144
+ 7.2,
145
+ 13,
146
+ "Interaction",
147
+ "Zoom into the control, move the cursor with intent, and make a single change."
148
+ ));
149
+ elements.push(captionCard(
150
+ "caption_billing",
151
+ 12.8,
152
+ 18.2,
153
+ "Plan change",
154
+ "Use camera zoom to spotlight one setting instead of crowding the screen."
155
+ ));
156
+ elements.push(captionCard(
157
+ "caption_save",
158
+ 18,
159
+ 23.4,
160
+ "Confirmation",
161
+ "End on the save action and a visible success state so the flow feels complete."
162
+ ));
163
+
164
+ elements.push(buildAppCamera());
165
+
166
+ const doc = {
167
+ version: 1,
168
+ canvas: { width, height, background: bg, duration, fps },
169
+ elements
170
+ };
171
+
172
+ const outPath = path.join(__dirname, "ui-demo-explainer.visual.json");
173
+ fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
174
+ console.log("Written:", outPath);
175
+
176
+ function buildAppCamera() {
177
+ return {
178
+ id: "app-camera",
179
+ type: "group",
180
+ x: overviewPose.x,
181
+ y: overviewPose.y,
182
+ scale: overviewPose.scale,
183
+ origin: [0, 0],
184
+ opacity: 0,
185
+ children: [
186
+ buildAppShell(),
187
+ buildSidebar(),
188
+ buildTopBar(),
189
+ buildPageHeader(),
190
+ buildProfileCard(),
191
+ buildNotificationsCard(),
192
+ buildBillingCard(),
193
+ buildReviewCard(),
194
+ buildToast(),
195
+ buildFocusOutline("focus_notifications", notificationsCard, 7.2, 12.9),
196
+ buildFocusOutline("focus_billing", billingCard, 12.8, 18.1),
197
+ buildFocusOutline("focus_review", reviewCard, 18, 22.4),
198
+ buildClickRing("click_weekly", weeklyToggleCenter, 8.9),
199
+ buildClickRing("click_annual", annualPillCenter, 14.1),
200
+ buildClickRing("click_save", saveButtonCenter, 19.6),
201
+ buildCursor()
202
+ ],
203
+ timeline: {
204
+ tracks: {
205
+ opacity: {
206
+ keyframes: [
207
+ { time: 1.8, value: 0, out: curves.ease },
208
+ { time: 2.6, value: 1 }
209
+ ]
210
+ },
211
+ x: {
212
+ keyframes: [
213
+ { time: 2.2, value: overviewPose.x, out: curves.ease },
214
+ { time: 3, value: overviewPose.x },
215
+ { time: 7.1, value: overviewPose.x, out: curves.easeOut },
216
+ { time: 8, value: notifPose.x },
217
+ { time: 12.6, value: notifPose.x, out: curves.easeOut },
218
+ { time: 13.4, value: billingPose.x },
219
+ { time: 17.8, value: billingPose.x, out: curves.easeOut },
220
+ { time: 18.6, value: reviewPose.x },
221
+ { time: 21.4, value: reviewPose.x, out: curves.easeOut },
222
+ { time: 22.2, value: overviewPose.x }
223
+ ]
224
+ },
225
+ y: {
226
+ keyframes: [
227
+ { time: 2.2, value: overviewPose.y, out: curves.ease },
228
+ { time: 3, value: overviewPose.y },
229
+ { time: 7.1, value: overviewPose.y, out: curves.easeOut },
230
+ { time: 8, value: notifPose.y },
231
+ { time: 12.6, value: notifPose.y, out: curves.easeOut },
232
+ { time: 13.4, value: billingPose.y },
233
+ { time: 17.8, value: billingPose.y, out: curves.easeOut },
234
+ { time: 18.6, value: reviewPose.y },
235
+ { time: 21.4, value: reviewPose.y, out: curves.easeOut },
236
+ { time: 22.2, value: overviewPose.y }
237
+ ]
238
+ },
239
+ scale: {
240
+ keyframes: [
241
+ { time: 2.2, value: 0.88, out: curves.ease },
242
+ { time: 3, value: overviewPose.scale },
243
+ { time: 7.1, value: overviewPose.scale, out: curves.easeOut },
244
+ { time: 8, value: notifPose.scale },
245
+ { time: 12.6, value: notifPose.scale, out: curves.easeOut },
246
+ { time: 13.4, value: billingPose.scale },
247
+ { time: 17.8, value: billingPose.scale, out: curves.easeOut },
248
+ { time: 18.6, value: reviewPose.scale },
249
+ { time: 21.4, value: reviewPose.scale, out: curves.easeOut },
250
+ { time: 22.2, value: overviewPose.scale }
251
+ ]
252
+ }
253
+ }
254
+ }
255
+ };
256
+ }
257
+
258
+ function buildAppShell() {
259
+ return {
260
+ id: "shell",
261
+ type: "group",
262
+ x: screen.x,
263
+ y: screen.y,
264
+ children: [
265
+ {
266
+ id: "shell-bg",
267
+ type: "path",
268
+ d: roundedRect(0, 0, screen.w, screen.h, 24),
269
+ fill: colors.shell,
270
+ stroke: colors.shellBorder,
271
+ strokeWidth: 1.5
272
+ },
273
+ {
274
+ id: "shell-top",
275
+ type: "path",
276
+ d: roundedRect(0, 0, screen.w, 58, 24),
277
+ fill: colors.topBar,
278
+ stroke: colors.shellBorder,
279
+ strokeWidth: 1
280
+ },
281
+ {
282
+ id: "shell-sidebar",
283
+ type: "path",
284
+ d: roundedRect(0, 0, 208, screen.h, 24),
285
+ fill: colors.sidebar,
286
+ stroke: "none"
287
+ },
288
+ {
289
+ id: "shell-content-bg",
290
+ type: "path",
291
+ d: roundedRect(208, 58, screen.w - 208, screen.h - 58, 0),
292
+ fill: "#eff4fb",
293
+ stroke: "none"
294
+ }
295
+ ]
296
+ };
297
+ }
298
+
299
+ function buildSidebar() {
300
+ return {
301
+ id: "sidebar",
302
+ type: "group",
303
+ x: 0,
304
+ y: 0,
305
+ children: [
306
+ text("brand-mark", 32, 24, "sketchmark", {
307
+ fontSize: 19,
308
+ weight: 700,
309
+ fill: "#f8fafc"
310
+ }),
311
+ text("brand-sub", 32, 46, "workspace demo", {
312
+ fontSize: 11,
313
+ weight: 500,
314
+ fill: "#9fb3cf"
315
+ }),
316
+ navItem("nav-home", 20, 108, "Overview", false),
317
+ navItem("nav-projects", 20, 154, "Projects", false),
318
+ navItem("nav-settings", 20, 200, "Team Settings", true),
319
+ navItem("nav-billing", 20, 246, "Billing", false),
320
+ navItem("nav-audit", 20, 292, "Audit Log", false),
321
+ {
322
+ id: "sidebar-footer-chip",
323
+ type: "group",
324
+ x: 28,
325
+ y: screen.h - 92,
326
+ children: [
327
+ {
328
+ id: "sidebar-chip-bg",
329
+ type: "path",
330
+ d: roundedRect(0, 0, 152, 40, 12),
331
+ fill: "#112544",
332
+ stroke: "#20406f",
333
+ strokeWidth: 1
334
+ },
335
+ text("sidebar-chip-text", 16, 12, "4 teammates online", {
336
+ fontSize: 12,
337
+ weight: 500,
338
+ fill: "#bfdbfe"
339
+ })
340
+ ]
341
+ }
342
+ ]
343
+ };
344
+ }
345
+
346
+ function buildTopBar() {
347
+ return {
348
+ id: "topbar",
349
+ type: "group",
350
+ x: 232,
351
+ y: 12,
352
+ children: [
353
+ {
354
+ id: "search-bg",
355
+ type: "path",
356
+ d: roundedRect(0, 0, 300, 34, 10),
357
+ fill: "#f8fafc",
358
+ stroke: "#d7e2f0",
359
+ strokeWidth: 1
360
+ },
361
+ text("search-text", 16, 10, "Search settings, billing, teammates", {
362
+ fontSize: 12,
363
+ weight: 400,
364
+ fill: "#94a3b8"
365
+ }),
366
+ pillGroup("top-chip", 724, 2, 128, 30, "Preview mode", colors.chipBg, colors.chipStroke, colors.chipText),
367
+ avatarGroup("top-avatar", 974, 0, 34, "#c7d2fe", "JD")
368
+ ]
369
+ };
370
+ }
371
+
372
+ function buildPageHeader() {
373
+ return {
374
+ id: "page-header",
375
+ type: "group",
376
+ x: contentX,
377
+ y: 92,
378
+ children: [
379
+ text("page-eyebrow", 0, 0, "Team workspace", {
380
+ fontSize: 12,
381
+ weight: 600,
382
+ fill: "#2563eb"
383
+ }),
384
+ text("page-title", 0, 22, "Settings that stay easy to explain", {
385
+ fontSize: 30,
386
+ weight: 700,
387
+ fill: colors.pageTitle
388
+ }),
389
+ text("page-body", 0, 64, "A demo flow works best when the camera and cursor both guide attention.", {
390
+ fontSize: 15,
391
+ weight: 400,
392
+ fill: colors.body
393
+ })
394
+ ]
395
+ };
396
+ }
397
+
398
+ function buildProfileCard() {
399
+ return cardGroup("profile-card", 236, 96, 358, 198, [
400
+ text("profile-title", 24, 20, "Workspace profile", {
401
+ fontSize: 17,
402
+ weight: 600,
403
+ fill: colors.pageTitle
404
+ }),
405
+ text("profile-body", 24, 44, "Show identity details before you zoom into smaller controls.", {
406
+ fontSize: 13,
407
+ weight: 400,
408
+ fill: colors.body
409
+ }),
410
+ avatarGroup("profile-avatar", 24, 88, 48, "#bfdbfe", "NS"),
411
+ text("profile-name", 86, 90, "Northstar Studio", {
412
+ fontSize: 18,
413
+ weight: 600,
414
+ fill: colors.pageTitle
415
+ }),
416
+ text("profile-meta", 86, 116, "8 members • EU region", {
417
+ fontSize: 12,
418
+ weight: 500,
419
+ fill: colors.muted
420
+ }),
421
+ fieldLine("profile-field-1", 24, 152, 150, "Workspace name"),
422
+ fieldLine("profile-field-2", 186, 152, 148, "Support email")
423
+ ]);
424
+ }
425
+
426
+ function buildNotificationsCard() {
427
+ return cardGroup("notifications-card", notificationsCard.x, notificationsCard.y, notificationsCard.w, notificationsCard.h, [
428
+ text("notif-title", 24, 20, "Notifications", {
429
+ fontSize: 17,
430
+ weight: 600,
431
+ fill: colors.pageTitle
432
+ }),
433
+ text("notif-body", 24, 44, "Zooming here turns a tiny toggle into a clear, narrated moment.", {
434
+ fontSize: 13,
435
+ weight: 400,
436
+ fill: colors.body
437
+ }),
438
+ toggleRow("notif-weekly", 24, 82, "Weekly summary", "Send one digest every Monday morning.", false, true),
439
+ divider("notif-divider-1", 24, 128, notificationsCard.w - 48),
440
+ toggleRow("notif-incidents", 24, 140, "Critical incidents", "Always enabled for admins and owners.", true, false)
441
+ ]);
442
+ }
443
+
444
+ function buildBillingCard() {
445
+ return cardGroup("billing-card", billingCard.x, billingCard.y, billingCard.w, billingCard.h, [
446
+ text("billing-title", 24, 20, "Billing", {
447
+ fontSize: 17,
448
+ weight: 600,
449
+ fill: colors.pageTitle
450
+ }),
451
+ text("billing-body", 24, 44, "Use a small motion change to make plan selection feel deliberate.", {
452
+ fontSize: 13,
453
+ weight: 400,
454
+ fill: colors.body
455
+ }),
456
+ {
457
+ id: "billing-segment-bg",
458
+ type: "path",
459
+ d: roundedRect(24, 82, 186, 42, 14),
460
+ fill: "#edf3fb",
461
+ stroke: "#d8e4f2",
462
+ strokeWidth: 1
463
+ },
464
+ {
465
+ id: "billing-segment-highlight",
466
+ type: "path",
467
+ d: roundedRect(30, 88, 82, 30, 12),
468
+ x: 0,
469
+ y: 0,
470
+ fill: colors.accentSoft,
471
+ stroke: "#93c5fd",
472
+ strokeWidth: 1,
473
+ timeline: {
474
+ tracks: {
475
+ x: {
476
+ keyframes: [
477
+ { time: 0, value: 0 },
478
+ { time: 14.05, value: 0, out: curves.snap },
479
+ { time: 14.25, value: 89 }
480
+ ]
481
+ }
482
+ }
483
+ }
484
+ },
485
+ text("billing-monthly-label", 71, 103, "Monthly", {
486
+ align: "center",
487
+ valign: "middle",
488
+ fontSize: 13,
489
+ weight: 600,
490
+ fill: colors.accent,
491
+ timeline: {
492
+ tracks: {
493
+ fill: {
494
+ keyframes: [
495
+ { time: 0, value: colors.accent },
496
+ { time: 14.2, value: colors.muted }
497
+ ]
498
+ }
499
+ }
500
+ }
501
+ }),
502
+ text("billing-annual-label", 160, 103, "Annual", {
503
+ align: "center",
504
+ valign: "middle",
505
+ fontSize: 13,
506
+ weight: 600,
507
+ fill: colors.muted,
508
+ timeline: {
509
+ tracks: {
510
+ fill: {
511
+ keyframes: [
512
+ { time: 0, value: colors.muted },
513
+ { time: 14.2, value: colors.accent }
514
+ ]
515
+ }
516
+ }
517
+ }
518
+ }),
519
+ text("billing-price", 24, 140, "$49 / seat / month", {
520
+ fontSize: 28,
521
+ weight: 700,
522
+ fill: colors.pageTitle,
523
+ timeline: {
524
+ tracks: {
525
+ text: {
526
+ keyframes: [
527
+ { time: 0, value: "$49 / seat / month" },
528
+ { time: 14.2, value: "$39 / seat / month" }
529
+ ]
530
+ }
531
+ }
532
+ }
533
+ }),
534
+ text("billing-price-note", 24, 170, "Annual billing unlocks a 20% savings and a cleaner renewal cycle.", {
535
+ fontSize: 12,
536
+ weight: 500,
537
+ fill: colors.success
538
+ })
539
+ ]);
540
+ }
541
+
542
+ function buildReviewCard() {
543
+ return cardGroup("review-card", reviewCard.x, reviewCard.y, reviewCard.w, reviewCard.h, [
544
+ text("review-title", 24, 20, "Review changes", {
545
+ fontSize: 17,
546
+ weight: 600,
547
+ fill: colors.pageTitle
548
+ }),
549
+ text("review-body", 24, 44, "End the sequence on a single confident action: save and show success.", {
550
+ fontSize: 13,
551
+ weight: 400,
552
+ fill: colors.body
553
+ }),
554
+ pillGroup("review-pill-1", 24, 78, 110, 30, "Weekly summary", "#eff6ff", "#bfdbfe", colors.accent),
555
+ pillGroup("review-pill-2", 144, 78, 88, 30, "Annual", "#ecfdf5", "#a7f3d0", colors.success),
556
+ {
557
+ id: "save-button",
558
+ type: "group",
559
+ x: 24,
560
+ y: 106,
561
+ origin: [70, 22],
562
+ children: [
563
+ {
564
+ id: "save-button-bg",
565
+ type: "path",
566
+ d: roundedRect(0, 0, 140, 44, 14),
567
+ fill: colors.accent,
568
+ stroke: "none"
569
+ },
570
+ text("save-button-label", 70, 22, "Save changes", {
571
+ align: "center",
572
+ valign: "middle",
573
+ fontSize: 14,
574
+ weight: 600,
575
+ fill: "#ffffff",
576
+ timeline: {
577
+ tracks: {
578
+ text: {
579
+ keyframes: [
580
+ { time: 0, value: "Save changes" },
581
+ { time: 19.9, value: "Saved" }
582
+ ]
583
+ }
584
+ }
585
+ }
586
+ })
587
+ ],
588
+ timeline: {
589
+ tracks: {
590
+ scale: {
591
+ keyframes: [
592
+ { time: 19.5, value: 1, out: curves.snap },
593
+ { time: 19.7, value: 0.95 },
594
+ { time: 19.95, value: 1 }
595
+ ]
596
+ }
597
+ }
598
+ }
599
+ },
600
+ {
601
+ id: "ghost-button",
602
+ type: "group",
603
+ x: 176,
604
+ y: 106,
605
+ children: [
606
+ {
607
+ id: "ghost-button-bg",
608
+ type: "path",
609
+ d: roundedRect(0, 0, 110, 44, 14),
610
+ fill: "#ffffff",
611
+ stroke: "#d4e0ee",
612
+ strokeWidth: 1
613
+ },
614
+ text("ghost-button-label", 55, 22, "Cancel", {
615
+ align: "center",
616
+ valign: "middle",
617
+ fontSize: 14,
618
+ weight: 500,
619
+ fill: colors.muted
620
+ })
621
+ ]
622
+ }
623
+ ]);
624
+ }
625
+
626
+ function buildToast() {
627
+ return {
628
+ id: "toast",
629
+ type: "group",
630
+ x: 760,
631
+ y: 20,
632
+ opacity: 0,
633
+ children: [
634
+ {
635
+ id: "toast-bg",
636
+ type: "path",
637
+ d: roundedRect(0, 0, 292, 62, 16),
638
+ fill: colors.card,
639
+ stroke: "#bfe9d8",
640
+ strokeWidth: 1.5
641
+ },
642
+ {
643
+ id: "toast-dot",
644
+ type: "path",
645
+ d: circlePath(24, 31, 9),
646
+ fill: colors.successSoft,
647
+ stroke: "none"
648
+ },
649
+ text("toast-title", 42, 16, "Changes saved", {
650
+ fontSize: 14,
651
+ weight: 700,
652
+ fill: colors.pageTitle
653
+ }),
654
+ text("toast-body", 42, 34, "Notifications and billing have been updated.", {
655
+ fontSize: 12,
656
+ weight: 400,
657
+ fill: colors.body
658
+ })
659
+ ],
660
+ timeline: {
661
+ tracks: {
662
+ opacity: {
663
+ keyframes: [
664
+ { time: 19.9, value: 0, out: curves.easeOut },
665
+ { time: 20.3, value: 1 },
666
+ { time: 23.1, value: 1, out: curves.easeOut },
667
+ { time: 23.7, value: 0 }
668
+ ]
669
+ },
670
+ y: {
671
+ keyframes: [
672
+ { time: 19.9, value: 8, out: curves.easeOut },
673
+ { time: 20.3, value: 20 }
674
+ ]
675
+ }
676
+ }
677
+ }
678
+ };
679
+ }
680
+
681
+ function buildFocusOutline(id, box, start, end) {
682
+ return {
683
+ id,
684
+ type: "path",
685
+ d: roundedRect(box.x - 8, box.y - 8, box.w + 16, box.h + 16, 24),
686
+ fill: "none",
687
+ stroke: colors.focus,
688
+ strokeWidth: 2,
689
+ dashArray: [8, 6],
690
+ opacity: 0,
691
+ timeline: {
692
+ tracks: {
693
+ opacity: {
694
+ keyframes: [
695
+ { time: start - 0.2, value: 0, out: curves.ease },
696
+ { time: start, value: 0.85 },
697
+ { time: end, value: 0.85, out: curves.ease },
698
+ { time: end + 0.3, value: 0 }
699
+ ]
700
+ }
701
+ }
702
+ }
703
+ };
704
+ }
705
+
706
+ function buildClickRing(id, center, start) {
707
+ return {
708
+ id,
709
+ type: "path",
710
+ d: circlePath(center.x, center.y, 16),
711
+ fill: "none",
712
+ stroke: colors.cursorRing,
713
+ strokeWidth: 2,
714
+ opacity: 0,
715
+ origin: [center.x, center.y],
716
+ timeline: {
717
+ tracks: {
718
+ opacity: {
719
+ keyframes: [
720
+ { time: start, value: 0 },
721
+ { time: start + 0.1, value: 0.7 },
722
+ { time: start + 0.55, value: 0 }
723
+ ]
724
+ },
725
+ scale: {
726
+ keyframes: [
727
+ { time: start, value: 0.4, out: curves.easeOut },
728
+ { time: start + 0.55, value: 1.5 }
729
+ ]
730
+ }
731
+ }
732
+ }
733
+ };
734
+ }
735
+
736
+ function buildCursor() {
737
+ return {
738
+ id: "cursor",
739
+ type: "group",
740
+ x: -80,
741
+ y: -80,
742
+ children: [
743
+ {
744
+ id: "cursor-arrow",
745
+ type: "path",
746
+ d: "M 0 0 L 0 22 L 6 18 L 10 28 L 15 26 L 11 16 L 20 16 Z",
747
+ fill: colors.cursor,
748
+ stroke: "#ffffff",
749
+ strokeWidth: 1.6
750
+ }
751
+ ],
752
+ timeline: {
753
+ tracks: {
754
+ x: {
755
+ keyframes: [
756
+ { time: 0, value: -80 },
757
+ { time: 6.7, value: -80, out: curves.easeOut },
758
+ { time: 7.8, value: weeklyToggleCenter.x },
759
+ { time: 9.3, value: weeklyToggleCenter.x },
760
+ { time: 12.7, value: weeklyToggleCenter.x, out: curves.easeOut },
761
+ { time: 13.9, value: annualPillCenter.x },
762
+ { time: 14.5, value: annualPillCenter.x },
763
+ { time: 17.8, value: annualPillCenter.x, out: curves.easeOut },
764
+ { time: 19.1, value: saveButtonCenter.x },
765
+ { time: 20.1, value: saveButtonCenter.x },
766
+ { time: 22.2, value: saveButtonCenter.x, out: curves.easeOut },
767
+ { time: 23.1, value: screen.w + 180 }
768
+ ]
769
+ },
770
+ y: {
771
+ keyframes: [
772
+ { time: 0, value: -80 },
773
+ { time: 6.7, value: -80, out: curves.easeOut },
774
+ { time: 7.8, value: weeklyToggleCenter.y },
775
+ { time: 9.3, value: weeklyToggleCenter.y },
776
+ { time: 12.7, value: weeklyToggleCenter.y, out: curves.easeOut },
777
+ { time: 13.9, value: annualPillCenter.y },
778
+ { time: 14.5, value: annualPillCenter.y },
779
+ { time: 17.8, value: annualPillCenter.y, out: curves.easeOut },
780
+ { time: 19.1, value: saveButtonCenter.y },
781
+ { time: 20.1, value: saveButtonCenter.y },
782
+ { time: 22.2, value: saveButtonCenter.y, out: curves.easeOut },
783
+ { time: 23.1, value: screen.h + 140 }
784
+ ]
785
+ }
786
+ }
787
+ }
788
+ };
789
+ }
790
+
791
+ function cardGroup(id, x, y, w, h, children) {
792
+ return {
793
+ id,
794
+ type: "group",
795
+ x,
796
+ y,
797
+ children: [
798
+ {
799
+ id: `${id}-bg`,
800
+ type: "path",
801
+ d: roundedRect(0, 0, w, h, 20),
802
+ fill: colors.card,
803
+ stroke: colors.cardBorder,
804
+ strokeWidth: 1
805
+ },
806
+ ...children
807
+ ]
808
+ };
809
+ }
810
+
811
+ function pillGroup(id, x, y, w, h, label, fill, stroke, textFill) {
812
+ return {
813
+ id,
814
+ type: "group",
815
+ x,
816
+ y,
817
+ children: [
818
+ {
819
+ id: `${id}-bg`,
820
+ type: "path",
821
+ d: roundedRect(0, 0, w, h, h / 2),
822
+ fill,
823
+ stroke,
824
+ strokeWidth: 1
825
+ },
826
+ text(`${id}-text`, w / 2, h / 2, label, {
827
+ align: "center",
828
+ valign: "middle",
829
+ fontSize: 12,
830
+ weight: 600,
831
+ fill: textFill
832
+ })
833
+ ]
834
+ };
835
+ }
836
+
837
+ function avatarGroup(id, x, y, size, fill, label) {
838
+ return {
839
+ id,
840
+ type: "group",
841
+ x,
842
+ y,
843
+ children: [
844
+ {
845
+ id: `${id}-circle`,
846
+ type: "path",
847
+ d: circlePath(size / 2, size / 2, size / 2),
848
+ fill,
849
+ stroke: "none"
850
+ },
851
+ text(`${id}-label`, size / 2, size / 2, label, {
852
+ align: "center",
853
+ valign: "middle",
854
+ fontSize: Math.max(11, Math.round(size * 0.32)),
855
+ weight: 700,
856
+ fill: "#1e3a8a"
857
+ })
858
+ ]
859
+ };
860
+ }
861
+
862
+ function navItem(id, x, y, label, active) {
863
+ return {
864
+ id,
865
+ type: "group",
866
+ x,
867
+ y,
868
+ children: [
869
+ {
870
+ id: `${id}-bg`,
871
+ type: "path",
872
+ d: roundedRect(0, 0, 164, 36, 12),
873
+ fill: active ? colors.sidebarActive : "none",
874
+ stroke: active ? "#bfdbfe" : "none",
875
+ strokeWidth: 1
876
+ },
877
+ text(`${id}-text`, 16, 10, label, {
878
+ fontSize: 13,
879
+ weight: active ? 700 : 500,
880
+ fill: active ? colors.sidebarActiveText : "#dbe5f2"
881
+ })
882
+ ]
883
+ };
884
+ }
885
+
886
+ function fieldLine(id, x, y, w, label) {
887
+ return {
888
+ id,
889
+ type: "group",
890
+ x,
891
+ y,
892
+ children: [
893
+ text(`${id}-label`, 0, 0, label, {
894
+ fontSize: 11,
895
+ weight: 600,
896
+ fill: colors.muted
897
+ }),
898
+ {
899
+ id: `${id}-line`,
900
+ type: "path",
901
+ d: `M 0 24 L ${w} 24`,
902
+ stroke: colors.divider,
903
+ strokeWidth: 2,
904
+ fill: "none"
905
+ }
906
+ ]
907
+ };
908
+ }
909
+
910
+ function toggleRow(id, x, y, label, description, on, animatesOn) {
911
+ const knobStart = on ? 334 : 308;
912
+ const bgStart = on ? colors.accent : colors.toggleOff;
913
+ return {
914
+ id,
915
+ type: "group",
916
+ x,
917
+ y,
918
+ children: [
919
+ text(`${id}-label`, 0, 0, label, {
920
+ fontSize: 15,
921
+ weight: 600,
922
+ fill: colors.pageTitle
923
+ }),
924
+ text(`${id}-body`, 0, 22, description, {
925
+ fontSize: 12,
926
+ weight: 400,
927
+ fill: colors.body
928
+ }),
929
+ {
930
+ id: `${id}-toggle-bg`,
931
+ type: "path",
932
+ d: roundedRect(300, 8, 56, 30, 15),
933
+ fill: bgStart,
934
+ stroke: "none",
935
+ timeline: animatesOn ? {
936
+ tracks: {
937
+ fill: {
938
+ keyframes: [
939
+ { time: 0, value: colors.toggleOff },
940
+ { time: 8.95, value: colors.toggleOff, out: curves.snap },
941
+ { time: 9.15, value: colors.accent }
942
+ ]
943
+ }
944
+ }
945
+ } : undefined
946
+ },
947
+ {
948
+ id: `${id}-toggle-knob`,
949
+ type: "path",
950
+ d: circlePath(knobStart, 23, 11),
951
+ fill: colors.toggleKnob,
952
+ stroke: "none",
953
+ timeline: animatesOn ? {
954
+ tracks: {
955
+ x: {
956
+ keyframes: [
957
+ { time: 0, value: 0 },
958
+ { time: 8.95, value: 0, out: curves.snap },
959
+ { time: 9.15, value: 26 }
960
+ ]
961
+ }
962
+ }
963
+ } : undefined
964
+ }
965
+ ]
966
+ };
967
+ }
968
+
969
+ function divider(id, x, y, w) {
970
+ return {
971
+ id,
972
+ type: "path",
973
+ d: `M ${x} ${y} L ${x + w} ${y}`,
974
+ stroke: colors.divider,
975
+ strokeWidth: 1,
976
+ fill: "none"
977
+ };
978
+ }
979
+
980
+ function captionCard(id, start, end, label, body) {
981
+ const boxW = 620;
982
+ const boxH = 92;
983
+ const chipW = 96;
984
+ const x = (width - boxW) / 2;
985
+ const y = 584;
986
+ return {
987
+ id,
988
+ type: "group",
989
+ x,
990
+ y,
991
+ opacity: 0,
992
+ children: [
993
+ {
994
+ id: `${id}-bg`,
995
+ type: "path",
996
+ d: roundedRect(0, 0, boxW, boxH, 22),
997
+ fill: colors.captionBg,
998
+ stroke: colors.captionBorder,
999
+ strokeWidth: 1.5
1000
+ },
1001
+ {
1002
+ id: `${id}-chip`,
1003
+ type: "group",
1004
+ x: 20,
1005
+ y: 18,
1006
+ children: [
1007
+ {
1008
+ id: `${id}-chip-bg`,
1009
+ type: "path",
1010
+ d: roundedRect(0, 0, chipW, 28, 14),
1011
+ fill: "#0b2648",
1012
+ stroke: "#1d4c7f",
1013
+ strokeWidth: 1
1014
+ },
1015
+ text(`${id}-chip-text`, chipW / 2, 14, label, {
1016
+ align: "center",
1017
+ valign: "middle",
1018
+ fontSize: 12,
1019
+ weight: 700,
1020
+ fill: "#7dd3fc"
1021
+ })
1022
+ ]
1023
+ },
1024
+ text(`${id}-body`, 20, 56, body, {
1025
+ fontSize: 15,
1026
+ weight: 400,
1027
+ fill: colors.captionText,
1028
+ maxWidth: boxW - 40
1029
+ })
1030
+ ],
1031
+ timeline: {
1032
+ tracks: {
1033
+ opacity: {
1034
+ keyframes: [
1035
+ { time: start - 0.3, value: 0, out: curves.easeOut },
1036
+ { time: start, value: 1 },
1037
+ { time: end, value: 1, out: curves.easeOut },
1038
+ { time: end + 0.35, value: 0 }
1039
+ ]
1040
+ },
1041
+ y: {
1042
+ keyframes: [
1043
+ { time: start - 0.3, value: y + 20, out: curves.easeOut },
1044
+ { time: start, value: y }
1045
+ ]
1046
+ }
1047
+ }
1048
+ }
1049
+ };
1050
+ }
1051
+
1052
+ function text(id, x, y, value, extra) {
1053
+ return {
1054
+ id,
1055
+ type: "text",
1056
+ x,
1057
+ y,
1058
+ text: value,
1059
+ align: "left",
1060
+ valign: "top",
1061
+ fontSize: 14,
1062
+ fontFamily: font,
1063
+ weight: 400,
1064
+ fill: colors.pageTitle,
1065
+ ...extra
1066
+ };
1067
+ }
1068
+
1069
+ function cameraPose(targetX, targetY, scale) {
1070
+ return {
1071
+ x: width / 2 - targetX * scale,
1072
+ y: height / 2 - targetY * scale,
1073
+ scale
1074
+ };
1075
+ }
1076
+
1077
+ function circlePath(cx, cy, r) {
1078
+ return `M ${cx - r} ${cy} a ${r} ${r} 0 1 0 ${r * 2} 0 a ${r} ${r} 0 1 0 ${-r * 2} 0`;
1079
+ }
1080
+
1081
+ function roundedRect(x, y, w, h, r) {
1082
+ return [
1083
+ `M ${x + r} ${y}`,
1084
+ `L ${x + w - r} ${y}`,
1085
+ `Q ${x + w} ${y} ${x + w} ${y + r}`,
1086
+ `L ${x + w} ${y + h - r}`,
1087
+ `Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
1088
+ `L ${x + r} ${y + h}`,
1089
+ `Q ${x} ${y + h} ${x} ${y + h - r}`,
1090
+ `L ${x} ${y + r}`,
1091
+ `Q ${x} ${y} ${x + r} ${y}`,
1092
+ "Z"
1093
+ ].join(" ");
1094
+ }