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.
- package/ANIMATABLE_MATRIX.md +177 -0
- package/KERNEL_SPEC.md +412 -0
- package/PACKS.md +81 -0
- package/PRESETS.md +182 -0
- package/README.md +274 -188
- package/bin/editor-ui.cjs +2285 -0
- package/bin/preview-ui.cjs +74 -0
- package/bin/sketchmark.cjs +648 -2008
- package/dist/src/animatable.d.ts +21 -0
- package/dist/src/animatable.js +439 -0
- package/dist/src/builders/index.d.ts +1 -11
- package/dist/src/builders/index.js +1 -19
- package/dist/src/diagnostics.js +1 -64
- package/dist/src/edit.d.ts +27 -0
- package/dist/src/edit.js +162 -0
- package/dist/src/index.d.ts +4 -13
- package/dist/src/index.js +4 -13
- package/dist/src/keyframes.d.ts +48 -0
- package/dist/src/keyframes.js +182 -0
- package/dist/src/motion.d.ts +4 -0
- package/dist/src/motion.js +262 -0
- package/dist/src/normalize.js +120 -151
- package/dist/src/presets/characters.d.ts +15 -0
- package/dist/src/presets/characters.js +113 -0
- package/dist/src/presets/compose.d.ts +5 -0
- package/dist/src/presets/compose.js +80 -0
- package/dist/src/presets/effects.d.ts +40 -0
- package/dist/src/presets/effects.js +79 -0
- package/dist/src/presets/helpers.d.ts +33 -0
- package/dist/src/presets/helpers.js +165 -0
- package/dist/src/presets/index.d.ts +9 -0
- package/dist/src/presets/index.js +48 -0
- package/dist/src/presets/motions.d.ts +33 -0
- package/dist/src/presets/motions.js +75 -0
- package/dist/src/presets/scenes.d.ts +35 -0
- package/dist/src/presets/scenes.js +134 -0
- package/dist/src/presets/shapes.d.ts +71 -0
- package/dist/src/presets/shapes.js +96 -0
- package/dist/src/presets/transitions.d.ts +29 -0
- package/dist/src/presets/transitions.js +113 -0
- package/dist/src/presets/types.d.ts +34 -0
- package/dist/src/presets/types.js +2 -0
- package/dist/src/render/html.js +1 -4
- package/dist/src/render/svg.d.ts +2 -2
- package/dist/src/render/svg.js +86 -82
- package/dist/src/render/three-html.js +67 -113
- package/dist/src/scenes.js +1 -0
- package/dist/src/schema.js +218 -280
- package/dist/src/shapes/builtins.js +11 -47
- package/dist/src/shapes/common.js +12 -11
- package/dist/src/shapes/registry.d.ts +0 -1
- package/dist/src/shapes/registry.js +0 -4
- package/dist/src/shapes/types.d.ts +1 -3
- package/dist/src/types.d.ts +57 -288
- package/dist/src/utils.d.ts +2 -11
- package/dist/src/utils.js +13 -70
- package/dist/src/validate.js +321 -275
- package/dist/tests/run.js +576 -510
- package/examples/1730642890464.jpg +0 -0
- package/examples/app-screen.svg +1 -0
- package/examples/app-screen.visual.json +503 -0
- package/examples/dashboard-table.svg +1 -0
- package/examples/dashboard-table.visual.json +708 -0
- package/examples/dev-docs.svg +1 -0
- package/examples/dev-docs.visual.json +248 -0
- package/examples/explainer.mp4 +0 -0
- package/examples/explainer.visual.json +1713 -0
- package/examples/group-origin-effects-lab-check.svg +1 -0
- package/examples/group-origin-effects-lab.visual.json +1880 -0
- package/examples/image-clip-radius.visual.json +271 -0
- package/examples/make-app-screen.cjs +368 -0
- package/examples/make-dashboard-table.cjs +277 -0
- package/examples/make-dev-docs.cjs +233 -0
- package/examples/make-explainer.cjs +438 -0
- package/examples/make-group-origin-effects-lab.cjs +370 -0
- package/examples/make-image-clip-radius.cjs +169 -0
- package/examples/make-modal-dialog.cjs +355 -0
- package/examples/make-origin-effects-lab.cjs +311 -0
- package/examples/make-preset-character-motion.cjs +32 -0
- package/examples/make-presets-demo.cjs +30 -0
- package/examples/make-pricing.cjs +286 -0
- package/examples/make-product-demo.cjs +468 -0
- package/examples/make-product-hero.cjs +223 -0
- package/examples/make-release-notes.cjs +333 -0
- package/examples/make-settings-panel.cjs +435 -0
- package/examples/make-split-preview.cjs +248 -0
- package/examples/make-storyboard.cjs +215 -0
- package/examples/make-transcript.cjs +234 -0
- package/examples/make-typography-test.cjs +397 -0
- package/examples/make-ui-demo-explainer.cjs +1094 -0
- package/examples/make-ui-flow.cjs +762 -0
- package/examples/make-walkthrough.cjs +815 -0
- package/examples/modal-dialog.svg +1 -0
- package/examples/modal-dialog.visual.json +239 -0
- package/examples/origin-effects-lab-check.svg +1 -0
- package/examples/origin-effects-lab.visual.json +1412 -0
- package/examples/preset-character-motion.visual.json +949 -0
- package/examples/presets-demo.visual.json +787 -0
- package/examples/pricing.svg +1 -0
- package/examples/pricing.visual.json +652 -0
- package/examples/product-demo.mp4 +0 -0
- package/examples/product-demo.visual.json +866 -0
- package/examples/product-hero.svg +1 -0
- package/examples/product-hero.visual.json +242 -0
- package/examples/release-notes.svg +1 -0
- package/examples/release-notes.visual.json +467 -0
- package/examples/settings-panel.svg +1 -0
- package/examples/settings-panel.visual.json +501 -0
- package/examples/split-preview.svg +1 -0
- package/examples/split-preview.visual.json +124 -0
- package/examples/storyboard.svg +1 -0
- package/examples/storyboard.visual.json +312 -0
- package/examples/transcript.svg +1 -0
- package/examples/transcript.visual.json +407 -0
- package/examples/typography-indent-check.svg +1 -0
- package/examples/typography-lineheight-0.svg +1 -0
- package/examples/typography-lineheight-2.svg +1 -0
- package/examples/typography-test-check.svg +1 -0
- package/examples/typography-test.svg +1 -0
- package/examples/typography-test.visual.json +757 -0
- package/examples/ui-demo-explainer-billing.svg +1 -0
- package/examples/ui-demo-explainer-check.svg +1 -0
- package/examples/ui-demo-explainer-save.svg +1 -0
- package/examples/ui-demo-explainer-toggle.svg +1 -0
- package/examples/ui-demo-explainer.mp4 +0 -0
- package/examples/ui-demo-explainer.visual.json +2597 -0
- package/examples/ui-flow.mp4 +0 -0
- package/examples/ui-flow.visual.json +1211 -0
- package/examples/walkthrough.mp4 +0 -0
- package/examples/walkthrough.visual.json +1372 -0
- package/package.json +52 -52
- package/schema/visual.schema.json +1086 -930
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const width = 1100;
|
|
5
|
+
const height = 720;
|
|
6
|
+
const bg = "#f1f5f9";
|
|
7
|
+
const font = "Inter, system-ui, sans-serif";
|
|
8
|
+
|
|
9
|
+
const colors = {
|
|
10
|
+
pageTitle: "#0f172a",
|
|
11
|
+
pageSubtitle: "#64748b",
|
|
12
|
+
panelBg: "#ffffff",
|
|
13
|
+
panelBorder: "#e2e8f0",
|
|
14
|
+
panelNumber: "#cbd5e1",
|
|
15
|
+
panelTitle: "#1e293b",
|
|
16
|
+
panelCaption: "#475569",
|
|
17
|
+
tagBg: "#0f172a",
|
|
18
|
+
tagText: "#ffffff"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const padX = 48;
|
|
22
|
+
const padY = 40;
|
|
23
|
+
let y = padY;
|
|
24
|
+
|
|
25
|
+
const elements = [];
|
|
26
|
+
|
|
27
|
+
// Page title
|
|
28
|
+
elements.push({
|
|
29
|
+
id: "page-title",
|
|
30
|
+
type: "text",
|
|
31
|
+
x: padX,
|
|
32
|
+
y: y,
|
|
33
|
+
text: "Product Launch Storyboard",
|
|
34
|
+
align: "left",
|
|
35
|
+
valign: "top",
|
|
36
|
+
fontSize: 28,
|
|
37
|
+
fontFamily: font,
|
|
38
|
+
weight: 700,
|
|
39
|
+
fill: colors.pageTitle
|
|
40
|
+
});
|
|
41
|
+
y += 38;
|
|
42
|
+
|
|
43
|
+
elements.push({
|
|
44
|
+
id: "page-subtitle",
|
|
45
|
+
type: "text",
|
|
46
|
+
x: padX,
|
|
47
|
+
y: y,
|
|
48
|
+
text: "Four key moments in the announcement video sequence.",
|
|
49
|
+
align: "left",
|
|
50
|
+
valign: "top",
|
|
51
|
+
fontSize: 14,
|
|
52
|
+
fontFamily: font,
|
|
53
|
+
weight: 400,
|
|
54
|
+
fill: colors.pageSubtitle
|
|
55
|
+
});
|
|
56
|
+
y += 44;
|
|
57
|
+
|
|
58
|
+
// Panel grid (2x2)
|
|
59
|
+
const panelGap = 24;
|
|
60
|
+
const gridW = width - padX * 2;
|
|
61
|
+
const panelW = (gridW - panelGap) / 2;
|
|
62
|
+
const panelH = 260;
|
|
63
|
+
const panelR = 10;
|
|
64
|
+
const panelPad = 20;
|
|
65
|
+
|
|
66
|
+
const panels = [
|
|
67
|
+
{
|
|
68
|
+
id: "panel-1",
|
|
69
|
+
number: "01",
|
|
70
|
+
tag: "Intro",
|
|
71
|
+
title: "Brand Logo Reveal",
|
|
72
|
+
caption: "Open on a dark screen. The logo fades in from\nthe center with a subtle glow effect. Hold for\ntwo seconds to establish brand presence."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "panel-2",
|
|
76
|
+
number: "02",
|
|
77
|
+
tag: "Action",
|
|
78
|
+
title: "Feature Demonstration",
|
|
79
|
+
caption: "Quick cuts showing the product in use. Focus\non three key interactions: search, create, and\nshare. Each clip lasts 1.5 seconds."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "panel-3",
|
|
83
|
+
number: "03",
|
|
84
|
+
tag: "Pause",
|
|
85
|
+
title: "Customer Testimonial",
|
|
86
|
+
caption: "A single customer speaks directly to camera.\nKeep framing tight on their face. Let the quote\nbreathe with natural pauses for emphasis."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "panel-4",
|
|
90
|
+
number: "04",
|
|
91
|
+
tag: "End",
|
|
92
|
+
title: "Call to Action",
|
|
93
|
+
caption: "Product name and tagline animate in from\nthe left. URL and QR code appear below.\nEnd with logo lockup in the corner."
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
panels.forEach((panel, i) => {
|
|
98
|
+
const col = i % 2;
|
|
99
|
+
const row = Math.floor(i / 2);
|
|
100
|
+
const px = padX + col * (panelW + panelGap);
|
|
101
|
+
const py = y + row * (panelH + panelGap);
|
|
102
|
+
|
|
103
|
+
// Panel background
|
|
104
|
+
elements.push({
|
|
105
|
+
id: `${panel.id}-bg`,
|
|
106
|
+
type: "path",
|
|
107
|
+
d: roundedRect(px, py, panelW, panelH, panelR),
|
|
108
|
+
fill: colors.panelBg,
|
|
109
|
+
stroke: colors.panelBorder,
|
|
110
|
+
strokeWidth: 1
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Panel number (top-left, large muted)
|
|
114
|
+
elements.push({
|
|
115
|
+
id: `${panel.id}-number`,
|
|
116
|
+
type: "text",
|
|
117
|
+
x: px + panelPad,
|
|
118
|
+
y: py + panelPad,
|
|
119
|
+
text: panel.number,
|
|
120
|
+
align: "left",
|
|
121
|
+
valign: "top",
|
|
122
|
+
fontSize: 32,
|
|
123
|
+
fontFamily: font,
|
|
124
|
+
weight: 700,
|
|
125
|
+
fill: colors.panelNumber
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Tag pill (top-right, centered text)
|
|
129
|
+
const tagW = panel.tag.length * 9 + 24;
|
|
130
|
+
const tagH = 26;
|
|
131
|
+
const tagX = px + panelW - panelPad - tagW;
|
|
132
|
+
const tagY = py + panelPad;
|
|
133
|
+
const tagR = 13;
|
|
134
|
+
|
|
135
|
+
elements.push({
|
|
136
|
+
id: `${panel.id}-tag-bg`,
|
|
137
|
+
type: "path",
|
|
138
|
+
d: roundedRect(tagX, tagY, tagW, tagH, tagR),
|
|
139
|
+
fill: colors.tagBg,
|
|
140
|
+
stroke: "none"
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
elements.push({
|
|
144
|
+
id: `${panel.id}-tag-text`,
|
|
145
|
+
type: "text",
|
|
146
|
+
x: tagX + tagW / 2,
|
|
147
|
+
y: tagY + tagH / 2,
|
|
148
|
+
text: panel.tag,
|
|
149
|
+
align: "center",
|
|
150
|
+
valign: "middle",
|
|
151
|
+
fontSize: 11,
|
|
152
|
+
fontFamily: font,
|
|
153
|
+
weight: 600,
|
|
154
|
+
fill: colors.tagText
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Panel title (left-aligned)
|
|
158
|
+
const titleY = py + panelPad + 52;
|
|
159
|
+
elements.push({
|
|
160
|
+
id: `${panel.id}-title`,
|
|
161
|
+
type: "text",
|
|
162
|
+
x: px + panelPad,
|
|
163
|
+
y: titleY,
|
|
164
|
+
text: panel.title,
|
|
165
|
+
align: "left",
|
|
166
|
+
valign: "top",
|
|
167
|
+
fontSize: 18,
|
|
168
|
+
fontFamily: font,
|
|
169
|
+
weight: 600,
|
|
170
|
+
fill: colors.panelTitle
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Panel caption (left-aligned, multiline)
|
|
174
|
+
const captionY = titleY + 32;
|
|
175
|
+
elements.push({
|
|
176
|
+
id: `${panel.id}-caption`,
|
|
177
|
+
type: "text",
|
|
178
|
+
x: px + panelPad,
|
|
179
|
+
y: captionY,
|
|
180
|
+
text: panel.caption,
|
|
181
|
+
align: "left",
|
|
182
|
+
valign: "top",
|
|
183
|
+
fontSize: 13,
|
|
184
|
+
fontFamily: font,
|
|
185
|
+
weight: 400,
|
|
186
|
+
lineHeight: 1.6,
|
|
187
|
+
fill: colors.panelCaption,
|
|
188
|
+
maxWidth: panelW - panelPad * 2
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
function roundedRect(x, y, w, h, r) {
|
|
193
|
+
return [
|
|
194
|
+
`M ${x + r} ${y}`,
|
|
195
|
+
`L ${x + w - r} ${y}`,
|
|
196
|
+
`Q ${x + w} ${y} ${x + w} ${y + r}`,
|
|
197
|
+
`L ${x + w} ${y + h - r}`,
|
|
198
|
+
`Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
|
|
199
|
+
`L ${x + r} ${y + h}`,
|
|
200
|
+
`Q ${x} ${y + h} ${x} ${y + h - r}`,
|
|
201
|
+
`L ${x} ${y + r}`,
|
|
202
|
+
`Q ${x} ${y} ${x + r} ${y}`,
|
|
203
|
+
"Z"
|
|
204
|
+
].join(" ");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const doc = {
|
|
208
|
+
version: 1,
|
|
209
|
+
canvas: { width, height, background: bg },
|
|
210
|
+
elements
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const outPath = path.join(__dirname, "storyboard.visual.json");
|
|
214
|
+
fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
|
|
215
|
+
console.log("Written:", outPath);
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const width = 900;
|
|
5
|
+
const height = 1140;
|
|
6
|
+
const bg = "#ffffff";
|
|
7
|
+
const font = "Inter, system-ui, sans-serif";
|
|
8
|
+
|
|
9
|
+
const colors = {
|
|
10
|
+
title: "#0f172a",
|
|
11
|
+
subtitle: "#64748b",
|
|
12
|
+
speaker: "#1e293b",
|
|
13
|
+
timestamp: "#94a3b8",
|
|
14
|
+
messageBg: "#f8fafc",
|
|
15
|
+
messageBorder: "#e2e8f0",
|
|
16
|
+
messageText: "#334155",
|
|
17
|
+
divider: "#e2e8f0",
|
|
18
|
+
btnBg: "#1e293b",
|
|
19
|
+
btnText: "#ffffff"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const padX = 56;
|
|
23
|
+
const contentW = width - padX * 2;
|
|
24
|
+
let y = 44;
|
|
25
|
+
|
|
26
|
+
const elements = [];
|
|
27
|
+
|
|
28
|
+
// Title
|
|
29
|
+
elements.push({
|
|
30
|
+
id: "title",
|
|
31
|
+
type: "text",
|
|
32
|
+
x: padX,
|
|
33
|
+
y: y,
|
|
34
|
+
text: "Meeting Transcript",
|
|
35
|
+
align: "left",
|
|
36
|
+
valign: "top",
|
|
37
|
+
fontSize: 30,
|
|
38
|
+
fontFamily: font,
|
|
39
|
+
weight: 700,
|
|
40
|
+
fill: colors.title
|
|
41
|
+
});
|
|
42
|
+
y += 42;
|
|
43
|
+
|
|
44
|
+
// Subtitle
|
|
45
|
+
elements.push({
|
|
46
|
+
id: "subtitle",
|
|
47
|
+
type: "text",
|
|
48
|
+
x: padX,
|
|
49
|
+
y: y,
|
|
50
|
+
text: "Product Sync — May 27, 2026 · 10:00 AM · 4 participants",
|
|
51
|
+
align: "left",
|
|
52
|
+
valign: "top",
|
|
53
|
+
fontSize: 13,
|
|
54
|
+
fontFamily: font,
|
|
55
|
+
weight: 400,
|
|
56
|
+
fill: colors.subtitle
|
|
57
|
+
});
|
|
58
|
+
y += 32;
|
|
59
|
+
|
|
60
|
+
// Divider
|
|
61
|
+
elements.push({
|
|
62
|
+
id: "divider-top",
|
|
63
|
+
type: "path",
|
|
64
|
+
d: `M ${padX} ${y} L ${width - padX} ${y}`,
|
|
65
|
+
stroke: colors.divider,
|
|
66
|
+
strokeWidth: 1,
|
|
67
|
+
fill: "none"
|
|
68
|
+
});
|
|
69
|
+
y += 32;
|
|
70
|
+
|
|
71
|
+
// Transcript messages
|
|
72
|
+
const messages = [
|
|
73
|
+
{
|
|
74
|
+
speaker: "Sarah Chen",
|
|
75
|
+
time: "10:01",
|
|
76
|
+
text: "Alright, let's get started. The main topic today is the timeline\nfor the v2.4 release. We need to finalize which features ship\nand which get pushed to the next cycle."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
speaker: "Marcus Taylor",
|
|
80
|
+
time: "10:02",
|
|
81
|
+
text: "From the backend side, the API gateway migration is on track.\nWe finished the schema federation layer last week and rate\nlimiting is in review right now."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
speaker: "Priya Nair",
|
|
85
|
+
time: "10:03",
|
|
86
|
+
text: "Design system updates are about 80% done. The new tokens\nare merged but we still need to audit accessibility on the\nrefactored form components before release."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
speaker: "Sarah Chen",
|
|
90
|
+
time: "10:04",
|
|
91
|
+
text: "Good. Let's set a hard cutoff for Friday on the a11y audit.\nIf it's not passing by then we defer the form components\nand ship everything else on schedule."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
speaker: "James Olsen",
|
|
95
|
+
time: "10:05",
|
|
96
|
+
text: "I can help with the audit. I've been running automated scans\non the onboarding flow already so I know the tooling well.\nShould have results by Thursday."
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
speaker: "Priya Nair",
|
|
100
|
+
time: "10:06",
|
|
101
|
+
text: "That would be great. I'll share the component list and the\ntest matrix with you after this call so we don't duplicate work."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
speaker: "Sarah Chen",
|
|
105
|
+
time: "10:07",
|
|
106
|
+
text: "Perfect. Let's reconvene Thursday afternoon to review the\naudit results and make the final call. I'll send a calendar\ninvite. Anything else before we wrap?"
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const speakerX = padX;
|
|
111
|
+
const timeX = padX + 136;
|
|
112
|
+
const bubbleX = padX;
|
|
113
|
+
const bubbleW = contentW;
|
|
114
|
+
const bubblePad = 14;
|
|
115
|
+
const bubbleR = 8;
|
|
116
|
+
|
|
117
|
+
messages.forEach((msg, i) => {
|
|
118
|
+
// Speaker name
|
|
119
|
+
elements.push({
|
|
120
|
+
id: `msg-${i}-speaker`,
|
|
121
|
+
type: "text",
|
|
122
|
+
x: speakerX,
|
|
123
|
+
y: y,
|
|
124
|
+
text: msg.speaker,
|
|
125
|
+
align: "left",
|
|
126
|
+
valign: "top",
|
|
127
|
+
fontSize: 13,
|
|
128
|
+
fontFamily: font,
|
|
129
|
+
weight: 600,
|
|
130
|
+
fill: colors.speaker
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Timestamp (right-aligned to a fixed column)
|
|
134
|
+
elements.push({
|
|
135
|
+
id: `msg-${i}-time`,
|
|
136
|
+
type: "text",
|
|
137
|
+
x: width - padX,
|
|
138
|
+
y: y,
|
|
139
|
+
text: msg.time,
|
|
140
|
+
align: "right",
|
|
141
|
+
valign: "top",
|
|
142
|
+
fontSize: 12,
|
|
143
|
+
fontFamily: font,
|
|
144
|
+
weight: 400,
|
|
145
|
+
fill: colors.timestamp
|
|
146
|
+
});
|
|
147
|
+
y += 24;
|
|
148
|
+
|
|
149
|
+
// Message bubble
|
|
150
|
+
const lineCount = msg.text.split("\n").length;
|
|
151
|
+
const bubbleH = lineCount * 21 + bubblePad * 2;
|
|
152
|
+
|
|
153
|
+
elements.push({
|
|
154
|
+
id: `msg-${i}-bg`,
|
|
155
|
+
type: "path",
|
|
156
|
+
d: roundedRect(bubbleX, y, bubbleW, bubbleH, bubbleR),
|
|
157
|
+
fill: colors.messageBg,
|
|
158
|
+
stroke: colors.messageBorder,
|
|
159
|
+
strokeWidth: 1
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
elements.push({
|
|
163
|
+
id: `msg-${i}-text`,
|
|
164
|
+
type: "text",
|
|
165
|
+
x: bubbleX + bubblePad,
|
|
166
|
+
y: y + bubblePad,
|
|
167
|
+
text: msg.text,
|
|
168
|
+
align: "left",
|
|
169
|
+
valign: "top",
|
|
170
|
+
fontSize: 13,
|
|
171
|
+
fontFamily: font,
|
|
172
|
+
weight: 400,
|
|
173
|
+
lineHeight: 1.6,
|
|
174
|
+
fill: colors.messageText,
|
|
175
|
+
maxWidth: bubbleW - bubblePad * 2
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
y += bubbleH + 20;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
y += 12;
|
|
182
|
+
|
|
183
|
+
// Centered Export button
|
|
184
|
+
const btnW = 140;
|
|
185
|
+
const btnH = 42;
|
|
186
|
+
const btnX = (width - btnW) / 2;
|
|
187
|
+
const btnR = 8;
|
|
188
|
+
|
|
189
|
+
elements.push({
|
|
190
|
+
id: "btn-export-bg",
|
|
191
|
+
type: "path",
|
|
192
|
+
d: roundedRect(btnX, y, btnW, btnH, btnR),
|
|
193
|
+
fill: colors.btnBg,
|
|
194
|
+
stroke: "none"
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
elements.push({
|
|
198
|
+
id: "btn-export-label",
|
|
199
|
+
type: "text",
|
|
200
|
+
x: width / 2,
|
|
201
|
+
y: y + btnH / 2,
|
|
202
|
+
text: "Export Transcript",
|
|
203
|
+
align: "center",
|
|
204
|
+
valign: "middle",
|
|
205
|
+
fontSize: 14,
|
|
206
|
+
fontFamily: font,
|
|
207
|
+
weight: 600,
|
|
208
|
+
fill: colors.btnText
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
function roundedRect(x, y, w, h, r) {
|
|
212
|
+
return [
|
|
213
|
+
`M ${x + r} ${y}`,
|
|
214
|
+
`L ${x + w - r} ${y}`,
|
|
215
|
+
`Q ${x + w} ${y} ${x + w} ${y + r}`,
|
|
216
|
+
`L ${x + w} ${y + h - r}`,
|
|
217
|
+
`Q ${x + w} ${y + h} ${x + w - r} ${y + h}`,
|
|
218
|
+
`L ${x + r} ${y + h}`,
|
|
219
|
+
`Q ${x} ${y + h} ${x} ${y + h - r}`,
|
|
220
|
+
`L ${x} ${y + r}`,
|
|
221
|
+
`Q ${x} ${y} ${x + r} ${y}`,
|
|
222
|
+
"Z"
|
|
223
|
+
].join(" ");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const doc = {
|
|
227
|
+
version: 1,
|
|
228
|
+
canvas: { width, height, background: bg },
|
|
229
|
+
elements
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const outPath = path.join(__dirname, "transcript.visual.json");
|
|
233
|
+
fs.writeFileSync(outPath, JSON.stringify(doc, null, 2));
|
|
234
|
+
console.log("Written:", outPath);
|