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,468 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const width = 1280;
5
+ const height = 720;
6
+ const duration = 12;
7
+ const fps = 30;
8
+ const bg = "#0f172a";
9
+ const font = "Inter, system-ui, sans-serif";
10
+ const monoFont = "JetBrains Mono, Fira Code, monospace";
11
+
12
+ const colors = {
13
+ headline: "#ffffff",
14
+ subtext: "#94a3b8",
15
+ accent: "#3b82f6",
16
+ accentGlow: "#60a5fa",
17
+ panelBg: "#1e293b",
18
+ panelBorder: "#334155",
19
+ codeBg: "#0f172a",
20
+ codeText: "#e2e8f0",
21
+ pillBg: "#3b82f6",
22
+ pillText: "#ffffff",
23
+ btnBg: "#ffffff",
24
+ btnText: "#0f172a",
25
+ labelText: "#64748b"
26
+ };
27
+
28
+ const curves = {
29
+ easeOut: { type: "cubicBezier", x1: 0.16, y1: 1, x2: 0.3, y2: 1 },
30
+ easeInOut: { type: "cubicBezier", x1: 0.42, y1: 0, x2: 0.58, y2: 1 }
31
+ };
32
+
33
+ const elements = [];
34
+
35
+ // === SCENE 1: Logo + Tagline (0s - 3s) ===
36
+
37
+ // Logo text
38
+ elements.push({
39
+ id: "logo",
40
+ type: "text",
41
+ x: width / 2,
42
+ y: height / 2 - 40,
43
+ text: "Sketchmark",
44
+ align: "center",
45
+ valign: "middle",
46
+ fontSize: 72,
47
+ fontFamily: font,
48
+ weight: 800,
49
+ fill: colors.headline,
50
+ timeline: {
51
+ tracks: {
52
+ opacity: {
53
+ keyframes: [
54
+ { time: 0, value: 0, out: curves.easeOut },
55
+ { time: 0.6, value: 1 },
56
+ { time: 2.4, value: 1, out: curves.easeOut },
57
+ { time: 3, value: 0 }
58
+ ]
59
+ },
60
+ y: {
61
+ keyframes: [
62
+ { time: 0, value: height / 2 - 20, out: curves.easeOut },
63
+ { time: 0.6, value: height / 2 - 40 }
64
+ ]
65
+ }
66
+ }
67
+ }
68
+ });
69
+
70
+ // Tagline
71
+ elements.push({
72
+ id: "tagline",
73
+ type: "text",
74
+ x: width / 2,
75
+ y: height / 2 + 30,
76
+ text: "Visual documents as JSON",
77
+ align: "center",
78
+ valign: "middle",
79
+ fontSize: 24,
80
+ fontFamily: font,
81
+ weight: 400,
82
+ fill: colors.subtext,
83
+ timeline: {
84
+ tracks: {
85
+ opacity: {
86
+ keyframes: [
87
+ { time: 0.3, value: 0, out: curves.easeOut },
88
+ { time: 0.9, value: 1 },
89
+ { time: 2.4, value: 1, out: curves.easeOut },
90
+ { time: 3, value: 0 }
91
+ ]
92
+ }
93
+ }
94
+ }
95
+ });
96
+
97
+ // === SCENE 2: Feature Pills (3s - 6s) ===
98
+
99
+ const features = ["Render", "Animate", "Export"];
100
+ const pillW = 140;
101
+ const pillH = 48;
102
+ const pillGap = 24;
103
+ const pillsStartX = (width - (pillW * 3 + pillGap * 2)) / 2;
104
+
105
+ features.forEach((label, i) => {
106
+ const px = pillsStartX + i * (pillW + pillGap);
107
+ const py = height / 2 - pillH / 2;
108
+ const delay = i * 0.15;
109
+
110
+ // Use a group to keep pill bg and text together
111
+ elements.push({
112
+ id: `pill-${i}`,
113
+ type: "group",
114
+ x: px,
115
+ y: py,
116
+ children: [
117
+ {
118
+ id: `pill-${i}-bg`,
119
+ type: "path",
120
+ d: roundedRect(0, 0, pillW, pillH, pillH / 2),
121
+ fill: colors.pillBg,
122
+ stroke: "none"
123
+ },
124
+ {
125
+ id: `pill-${i}-text`,
126
+ type: "text",
127
+ x: pillW / 2,
128
+ y: pillH / 2,
129
+ text: label,
130
+ align: "center",
131
+ valign: "middle",
132
+ fontSize: 18,
133
+ fontFamily: font,
134
+ weight: 600,
135
+ fill: colors.pillText
136
+ }
137
+ ],
138
+ opacity: 0,
139
+ timeline: {
140
+ tracks: {
141
+ opacity: {
142
+ keyframes: [
143
+ { time: 3 + delay, value: 0, out: curves.easeOut },
144
+ { time: 3.4 + delay, value: 1 },
145
+ { time: 5.4, value: 1, out: curves.easeOut },
146
+ { time: 6, value: 0 }
147
+ ]
148
+ },
149
+ y: {
150
+ keyframes: [
151
+ { time: 3 + delay, value: py + 20, out: curves.easeOut },
152
+ { time: 3.4 + delay, value: py }
153
+ ]
154
+ }
155
+ }
156
+ }
157
+ });
158
+ });
159
+
160
+ // Scene 2 headline
161
+ elements.push({
162
+ id: "scene2-headline",
163
+ type: "text",
164
+ x: width / 2,
165
+ y: height / 2 - 80,
166
+ text: "Three simple commands",
167
+ align: "center",
168
+ valign: "middle",
169
+ fontSize: 20,
170
+ fontFamily: font,
171
+ weight: 500,
172
+ fill: colors.subtext,
173
+ opacity: 0,
174
+ timeline: {
175
+ tracks: {
176
+ opacity: {
177
+ keyframes: [
178
+ { time: 3, value: 0, out: curves.easeOut },
179
+ { time: 3.4, value: 1 },
180
+ { time: 5.4, value: 1, out: curves.easeOut },
181
+ { time: 6, value: 0 }
182
+ ]
183
+ }
184
+ }
185
+ }
186
+ });
187
+
188
+ // === SCENE 3: UI Panel Demo (6s - 10s) ===
189
+
190
+ const panelW = 600;
191
+ const panelH = 320;
192
+ const panelX = (width - panelW) / 2;
193
+ const panelY = (height - panelH) / 2 + 20;
194
+
195
+ // Panel background
196
+ elements.push({
197
+ id: "panel-bg",
198
+ type: "path",
199
+ d: roundedRect(panelX, panelY, panelW, panelH, 12),
200
+ fill: colors.panelBg,
201
+ stroke: colors.panelBorder,
202
+ strokeWidth: 1,
203
+ opacity: 0,
204
+ timeline: {
205
+ tracks: {
206
+ opacity: {
207
+ keyframes: [
208
+ { time: 6, value: 0, out: curves.easeOut },
209
+ { time: 6.5, value: 1 },
210
+ { time: 9.5, value: 1, out: curves.easeOut },
211
+ { time: 10, value: 0 }
212
+ ]
213
+ },
214
+ scale: {
215
+ keyframes: [
216
+ { time: 6, value: 0.95, out: curves.easeOut },
217
+ { time: 6.5, value: 1 }
218
+ ]
219
+ }
220
+ }
221
+ }
222
+ });
223
+
224
+ // Panel title bar
225
+ elements.push({
226
+ id: "panel-title",
227
+ type: "text",
228
+ x: panelX + 20,
229
+ y: panelY + 20,
230
+ text: "scene.visual.json",
231
+ align: "left",
232
+ valign: "top",
233
+ fontSize: 12,
234
+ fontFamily: monoFont,
235
+ weight: 500,
236
+ fill: colors.labelText,
237
+ opacity: 0,
238
+ timeline: {
239
+ tracks: {
240
+ opacity: {
241
+ keyframes: [
242
+ { time: 6.3, value: 0, out: curves.easeOut },
243
+ { time: 6.7, value: 1 },
244
+ { time: 9.5, value: 1, out: curves.easeOut },
245
+ { time: 10, value: 0 }
246
+ ]
247
+ }
248
+ }
249
+ }
250
+ });
251
+
252
+ // Code preview
253
+ const codeContent = `{
254
+ "version": 1,
255
+ "canvas": { "width": 1280, "height": 720 },
256
+ "elements": [
257
+ {
258
+ "id": "title",
259
+ "type": "text",
260
+ "text": "Hello World",
261
+ "x": 640, "y": 360,
262
+ "fontSize": 48
263
+ }
264
+ ]
265
+ }`;
266
+
267
+ elements.push({
268
+ id: "panel-code",
269
+ type: "text",
270
+ x: panelX + 20,
271
+ y: panelY + 48,
272
+ text: codeContent,
273
+ align: "left",
274
+ valign: "top",
275
+ fontSize: 13,
276
+ fontFamily: monoFont,
277
+ weight: 400,
278
+ lineHeight: 1.45,
279
+ fill: colors.codeText,
280
+ opacity: 0,
281
+ maxWidth: panelW - 40,
282
+ timeline: {
283
+ tracks: {
284
+ opacity: {
285
+ keyframes: [
286
+ { time: 6.5, value: 0, out: curves.easeOut },
287
+ { time: 7, value: 1 },
288
+ { time: 9.5, value: 1, out: curves.easeOut },
289
+ { time: 10, value: 0 }
290
+ ]
291
+ }
292
+ }
293
+ }
294
+ });
295
+
296
+ // Scene 3 headline
297
+ elements.push({
298
+ id: "scene3-headline",
299
+ type: "text",
300
+ x: width / 2,
301
+ y: panelY - 50,
302
+ text: "Define visuals in JSON",
303
+ align: "center",
304
+ valign: "middle",
305
+ fontSize: 28,
306
+ fontFamily: font,
307
+ weight: 700,
308
+ fill: colors.headline,
309
+ opacity: 0,
310
+ timeline: {
311
+ tracks: {
312
+ opacity: {
313
+ keyframes: [
314
+ { time: 6, value: 0, out: curves.easeOut },
315
+ { time: 6.5, value: 1 },
316
+ { time: 9.5, value: 1, out: curves.easeOut },
317
+ { time: 10, value: 0 }
318
+ ]
319
+ }
320
+ }
321
+ }
322
+ });
323
+
324
+ // === SCENE 4: CTA (10s - 12s) ===
325
+
326
+ elements.push({
327
+ id: "cta-headline",
328
+ type: "text",
329
+ x: width / 2,
330
+ y: height / 2 - 60,
331
+ text: "Start building",
332
+ align: "center",
333
+ valign: "middle",
334
+ fontSize: 52,
335
+ fontFamily: font,
336
+ weight: 800,
337
+ fill: colors.headline,
338
+ opacity: 0,
339
+ timeline: {
340
+ tracks: {
341
+ opacity: {
342
+ keyframes: [
343
+ { time: 10, value: 0, out: curves.easeOut },
344
+ { time: 10.5, value: 1 }
345
+ ]
346
+ },
347
+ y: {
348
+ keyframes: [
349
+ { time: 10, value: height / 2 - 40, out: curves.easeOut },
350
+ { time: 10.5, value: height / 2 - 60 }
351
+ ]
352
+ }
353
+ }
354
+ }
355
+ });
356
+
357
+ // CTA button
358
+ const btnW = 200;
359
+ const btnH = 52;
360
+ const btnX = (width - btnW) / 2;
361
+ const btnY = height / 2 + 10;
362
+
363
+ elements.push({
364
+ id: "cta-btn-bg",
365
+ type: "path",
366
+ d: roundedRect(btnX, btnY, btnW, btnH, 8),
367
+ fill: colors.btnBg,
368
+ stroke: "none",
369
+ opacity: 0,
370
+ timeline: {
371
+ tracks: {
372
+ opacity: {
373
+ keyframes: [
374
+ { time: 10.3, value: 0, out: curves.easeOut },
375
+ { time: 10.7, value: 1 }
376
+ ]
377
+ },
378
+ y: {
379
+ keyframes: [
380
+ { time: 10.3, value: btnY + 20, out: curves.easeOut },
381
+ { time: 10.7, value: btnY }
382
+ ]
383
+ }
384
+ }
385
+ }
386
+ });
387
+
388
+ elements.push({
389
+ id: "cta-btn-text",
390
+ type: "text",
391
+ x: width / 2,
392
+ y: btnY + btnH / 2,
393
+ text: "Get Started",
394
+ align: "center",
395
+ valign: "middle",
396
+ fontSize: 16,
397
+ fontFamily: font,
398
+ weight: 600,
399
+ fill: colors.btnText,
400
+ opacity: 0,
401
+ timeline: {
402
+ tracks: {
403
+ opacity: {
404
+ keyframes: [
405
+ { time: 10.3, value: 0, out: curves.easeOut },
406
+ { time: 10.7, value: 1 }
407
+ ]
408
+ },
409
+ y: {
410
+ keyframes: [
411
+ { time: 10.3, value: btnY + btnH / 2 + 20, out: curves.easeOut },
412
+ { time: 10.7, value: btnY + btnH / 2 }
413
+ ]
414
+ }
415
+ }
416
+ }
417
+ });
418
+
419
+ // Footer label
420
+ elements.push({
421
+ id: "footer-label",
422
+ type: "text",
423
+ x: width / 2,
424
+ y: height - 40,
425
+ text: "sketchmark.dev",
426
+ align: "center",
427
+ valign: "middle",
428
+ fontSize: 14,
429
+ fontFamily: font,
430
+ weight: 500,
431
+ fill: colors.subtext,
432
+ opacity: 0,
433
+ timeline: {
434
+ tracks: {
435
+ opacity: {
436
+ keyframes: [
437
+ { time: 10.5, value: 0, out: curves.easeOut },
438
+ { time: 11, value: 0.7 }
439
+ ]
440
+ }
441
+ }
442
+ }
443
+ });
444
+
445
+ function roundedRect(x, y, w, h, r) {
446
+ return [
447
+ `M ${x + r} ${y}`,
448
+ `L ${x + w - r} ${y}`,
449
+ `Q ${x + w} ${y} ${x + w} ${y + r}`,
450
+ `L ${x + w} ${y + h - r}`,
451
+ `Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
452
+ `L ${x + r} ${y + h}`,
453
+ `Q ${x} ${y + h} ${x} ${y + h - r}`,
454
+ `L ${x} ${y + r}`,
455
+ `Q ${x} ${y} ${x + r} ${y}`,
456
+ "Z"
457
+ ].join(" ");
458
+ }
459
+
460
+ const doc = {
461
+ version: 1,
462
+ canvas: { width, height, background: bg, duration, fps },
463
+ elements
464
+ };
465
+
466
+ const outPath = path.join(__dirname, "product-demo.visual.json");
467
+ fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
468
+ console.log("Written:", outPath);
@@ -0,0 +1,223 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const width = 1120;
5
+ const height = 880;
6
+ const bg = "#ffffff";
7
+ const font = "Inter, system-ui, sans-serif";
8
+
9
+ const colors = {
10
+ headline: "#0f172a",
11
+ subheadline: "#475569",
12
+ chipBg: "#f1f5f9",
13
+ chipBorder: "#e2e8f0",
14
+ chipText: "#334155",
15
+ sectionTitle: "#1e293b",
16
+ body: "#475569",
17
+ bullet: "#2563eb",
18
+ divider: "#e2e8f0"
19
+ };
20
+
21
+ const padX = 80;
22
+ const contentW = width - padX * 2;
23
+ let y = 72;
24
+
25
+ const elements = [];
26
+
27
+ // === HERO SECTION (centered) ===
28
+
29
+ // Large centered headline
30
+ elements.push({
31
+ id: "hero-headline",
32
+ type: "text",
33
+ x: width / 2,
34
+ y: y,
35
+ text: "Ship faster with less friction",
36
+ align: "center",
37
+ valign: "top",
38
+ fontSize: 48,
39
+ fontFamily: font,
40
+ weight: 800,
41
+ fill: colors.headline
42
+ });
43
+ y += 68;
44
+
45
+ // Centered subheadline
46
+ elements.push({
47
+ id: "hero-subheadline",
48
+ type: "text",
49
+ x: width / 2,
50
+ y: y,
51
+ text: "A modern platform for teams who want to build, deploy, and iterate\nwithout wrestling infrastructure or waiting on bottlenecks.",
52
+ align: "center",
53
+ valign: "top",
54
+ fontSize: 18,
55
+ fontFamily: font,
56
+ weight: 400,
57
+ lineHeight: 1.55,
58
+ fill: colors.subheadline,
59
+ maxWidth: contentW
60
+ });
61
+ y += 72;
62
+
63
+ // Feature chips (centered row, centered text in each)
64
+ const chips = [
65
+ "Real-time Collaboration",
66
+ "One-click Deploy",
67
+ "Built-in Analytics"
68
+ ];
69
+
70
+ const chipH = 36;
71
+ const chipR = 18;
72
+ const chipPadX = 20;
73
+ const chipGap = 14;
74
+
75
+ // Calculate total row width to center the group
76
+ let totalChipW = 0;
77
+ const chipWidths = chips.map(label => {
78
+ const w = label.length * 8.5 + chipPadX * 2;
79
+ totalChipW += w;
80
+ return w;
81
+ });
82
+ totalChipW += chipGap * (chips.length - 1);
83
+
84
+ let chipX = (width - totalChipW) / 2;
85
+
86
+ chips.forEach((label, i) => {
87
+ const cw = chipWidths[i];
88
+
89
+ elements.push({
90
+ id: `chip-bg-${i}`,
91
+ type: "path",
92
+ d: roundedRect(chipX, y, cw, chipH, chipR),
93
+ fill: colors.chipBg,
94
+ stroke: colors.chipBorder,
95
+ strokeWidth: 1
96
+ });
97
+
98
+ elements.push({
99
+ id: `chip-text-${i}`,
100
+ type: "text",
101
+ x: chipX + cw / 2,
102
+ y: y + chipH / 2,
103
+ text: label,
104
+ align: "center",
105
+ valign: "middle",
106
+ fontSize: 14,
107
+ fontFamily: font,
108
+ weight: 500,
109
+ fill: colors.chipText
110
+ });
111
+
112
+ chipX += cw + chipGap;
113
+ });
114
+ y += chipH + 56;
115
+
116
+ // Divider between hero and body
117
+ elements.push({
118
+ id: "hero-divider",
119
+ type: "path",
120
+ d: `M ${padX} ${y} L ${width - padX} ${y}`,
121
+ stroke: colors.divider,
122
+ strokeWidth: 1,
123
+ fill: "none"
124
+ });
125
+ y += 48;
126
+
127
+ // === BODY SECTION (left-aligned) ===
128
+
129
+ // Section title
130
+ elements.push({
131
+ id: "section-title",
132
+ type: "text",
133
+ x: padX,
134
+ y: y,
135
+ text: "Why teams choose us",
136
+ align: "left",
137
+ valign: "top",
138
+ fontSize: 26,
139
+ fontFamily: font,
140
+ weight: 700,
141
+ fill: colors.sectionTitle
142
+ });
143
+ y += 44;
144
+
145
+ // Body paragraph
146
+ elements.push({
147
+ id: "section-para",
148
+ type: "text",
149
+ x: padX,
150
+ y: y,
151
+ text: "Most platforms promise speed but deliver complexity. We took a different approach:\nstrip away everything that doesn't directly help you ship. No configuration mazes,\nno permission matrices, no waiting for DevOps. Just write code, push, and see it live.",
152
+ align: "left",
153
+ valign: "top",
154
+ fontSize: 15,
155
+ fontFamily: font,
156
+ weight: 400,
157
+ lineHeight: 1.7,
158
+ fill: colors.body,
159
+ maxWidth: contentW
160
+ });
161
+ y += 100;
162
+
163
+ // Bullet list
164
+ const bullets = [
165
+ "Zero-config deployments with automatic scaling and rollback",
166
+ "Collaborative editing with live cursors and instant sync",
167
+ "Unified dashboard for logs, metrics, and error tracking",
168
+ "Native integrations with GitHub, GitLab, and Bitbucket",
169
+ "SOC 2 Type II certified with end-to-end encryption"
170
+ ];
171
+
172
+ bullets.forEach((text, i) => {
173
+ const by = y + i * 36;
174
+
175
+ // Bullet dot
176
+ elements.push({
177
+ id: `bullet-dot-${i}`,
178
+ type: "path",
179
+ d: `M ${padX + 5} ${by + 9} A 4 4 0 1 1 ${padX + 5} ${by + 9.01} Z`,
180
+ fill: colors.bullet,
181
+ stroke: "none"
182
+ });
183
+
184
+ elements.push({
185
+ id: `bullet-text-${i}`,
186
+ type: "text",
187
+ x: padX + 20,
188
+ y: by,
189
+ text: text,
190
+ align: "left",
191
+ valign: "top",
192
+ fontSize: 15,
193
+ fontFamily: font,
194
+ weight: 400,
195
+ fill: colors.body,
196
+ maxWidth: contentW - 20
197
+ });
198
+ });
199
+
200
+ function roundedRect(x, y, w, h, r) {
201
+ return [
202
+ `M ${x + r} ${y}`,
203
+ `L ${x + w - r} ${y}`,
204
+ `Q ${x + w} ${y} ${x + w} ${y + r}`,
205
+ `L ${x + w} ${y + h - r}`,
206
+ `Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
207
+ `L ${x + r} ${y + h}`,
208
+ `Q ${x} ${y + h} ${x} ${y + h - r}`,
209
+ `L ${x} ${y + r}`,
210
+ `Q ${x} ${y} ${x + r} ${y}`,
211
+ "Z"
212
+ ].join(" ");
213
+ }
214
+
215
+ const doc = {
216
+ version: 1,
217
+ canvas: { width, height, background: bg },
218
+ elements
219
+ };
220
+
221
+ const outPath = path.join(__dirname, "product-hero.visual.json");
222
+ fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
223
+ console.log("Written:", outPath);