sketchmark 2.0.0 → 2.1.1
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/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/package.json +46 -52
- package/schema/visual.schema.json +1086 -930
package/dist/src/render/svg.js
CHANGED
|
@@ -2,102 +2,122 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderToSvg = renderToSvg;
|
|
4
4
|
exports.renderResolvedSvg = renderResolvedSvg;
|
|
5
|
-
const
|
|
5
|
+
const normalize_1 = require("../normalize");
|
|
6
6
|
const utils_1 = require("../utils");
|
|
7
|
+
const DEFAULT_FONT_FAMILY = "Roboto, Arial, sans-serif";
|
|
8
|
+
const LEGACY_DEFAULT_FONT_STACKS = new Set([
|
|
9
|
+
"inter,system-ui,sans-serif",
|
|
10
|
+
"system-ui,sans-serif",
|
|
11
|
+
"arial,helvetica,sans-serif"
|
|
12
|
+
]);
|
|
7
13
|
function renderToSvg(document, options = {}) {
|
|
8
|
-
|
|
9
|
-
return renderResolvedSvg(frame, options);
|
|
14
|
+
return renderResolvedSvg((0, normalize_1.resolveVisualFrame)(document, options.time ?? 0), options);
|
|
10
15
|
}
|
|
11
16
|
function renderResolvedSvg(document, options = {}) {
|
|
12
|
-
const kernel = isKernelVisualDocument(document) ? document : (0, kernel_1.lowerResolvedVisualDocument)(document);
|
|
13
17
|
const width = document.canvas.width;
|
|
14
18
|
const height = document.canvas.height;
|
|
15
19
|
const background = document.canvas.background ?? "#ffffff";
|
|
16
|
-
const context = { defs: [], nextId: 0
|
|
17
|
-
const elements =
|
|
20
|
+
const context = { defs: [], nextId: 0 };
|
|
21
|
+
const elements = document.elements.map((element) => renderElement(element, context)).join("");
|
|
18
22
|
const backdrop = options.transparent ? "" : `<rect x="0" y="0" width="${width}" height="${height}" fill="${escapeAttr(background)}"/>`;
|
|
19
23
|
const defs = context.defs.length ? `<defs>${context.defs.join("")}</defs>` : "";
|
|
20
24
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img">${defs}${backdrop}${elements}</svg>`;
|
|
21
25
|
}
|
|
22
26
|
function renderElement(element, context) {
|
|
23
|
-
const attrs =
|
|
27
|
+
const attrs = commonAttrParts(element, context);
|
|
24
28
|
const fill = paintValue(element.fill, context, element.type === "text" ? "#111827" : "none");
|
|
25
29
|
const stroke = strokeAttrs(element, context, "none");
|
|
26
30
|
switch (element.type) {
|
|
27
31
|
case "point":
|
|
28
32
|
return "";
|
|
29
|
-
case "path":
|
|
30
|
-
|
|
31
|
-
return `<path${attrs} d="${escapeAttr(element.d)}" fill="${fill}"${stroke}${marker}${drawAttrs(element)}/>`;
|
|
32
|
-
}
|
|
33
|
+
case "path":
|
|
34
|
+
return `<path${joinCommonAttrs(attrs)} d="${escapeAttr(element.d)}" fill="${fill}"${stroke}${drawAttrs(element)}/>`;
|
|
33
35
|
case "text":
|
|
34
|
-
return renderText(element, attrs, fill);
|
|
36
|
+
return renderText(element, joinCommonAttrs(attrs), fill);
|
|
35
37
|
case "image":
|
|
36
38
|
return renderImage(element, attrs, context);
|
|
37
39
|
case "group": {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
return `<g${attrs}${transform}>${children}</g>`;
|
|
40
|
+
const children = element.children.map((child) => renderElement(child, context)).join("");
|
|
41
|
+
return `<g${joinCommonAttrs(attrs)}${groupTransform(element)}>${children}</g>`;
|
|
41
42
|
}
|
|
42
43
|
default:
|
|
43
44
|
return "";
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
function isKernelVisualDocument(document) {
|
|
47
|
-
return (document.elements ?? []).every((element) => element.type === "group" || element.type === "path" || element.type === "text" || element.type === "image" || element.type === "point" || element.type === "group3d" || element.type === "mesh3d" || element.type === "line3d" || element.type === "text3d" || element.type === "point3d" || element.type === "light");
|
|
48
|
-
}
|
|
49
47
|
function renderImage(element, attrs, context) {
|
|
48
|
+
const imageClip = imageSourceClipAttr(element, context);
|
|
49
|
+
const wrapped = !!attrs.clip;
|
|
50
|
+
const imageAttrs = joinCommonAttrs(attrs, wrapped ? ["clip", "transform"] : []);
|
|
50
51
|
if (element.source) {
|
|
51
|
-
const id = nextId(context, "image-crop");
|
|
52
52
|
const scaleX = element.width / element.source.width;
|
|
53
53
|
const scaleY = element.height / element.source.height;
|
|
54
54
|
const imageX = element.x - element.source.x * scaleX;
|
|
55
55
|
const imageY = element.y - element.source.y * scaleY;
|
|
56
56
|
const imageWidth = element.source.imageWidth * scaleX;
|
|
57
57
|
const imageHeight = element.source.imageHeight * scaleY;
|
|
58
|
-
|
|
59
|
-
return `<
|
|
58
|
+
const image = `<image${imageAttrs} href="${escapeAttr(element.src)}" x="${imageX}" y="${imageY}" width="${imageWidth}" height="${imageHeight}" preserveAspectRatio="none"${imageClip}/>`;
|
|
59
|
+
return wrapped ? `<g${attrs.clip}${attrs.transform}>${image}</g>` : image;
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
const image = `<image${imageAttrs} href="${escapeAttr(element.src)}" x="${element.x}" y="${element.y}" width="${element.width}" height="${element.height}" preserveAspectRatio="${imageFit(element.fit)}"${imageClip}/>`;
|
|
62
|
+
return wrapped ? `<g${attrs.clip}${attrs.transform}>${image}</g>` : image;
|
|
62
63
|
}
|
|
63
64
|
function renderText(element, attrs, fill) {
|
|
64
|
-
const anchor = element.align === "
|
|
65
|
+
const anchor = element.align === "center" ? "middle" : element.align === "right" ? "end" : "start";
|
|
65
66
|
const fontSize = Number(element.fontSize ?? 16);
|
|
66
67
|
const lineHeight = fontSize * Number(element.lineHeight ?? 1.2);
|
|
67
68
|
const lines = (0, utils_1.textLines)(element);
|
|
68
69
|
const weight = escapeAttr(String(element.weight ?? 400));
|
|
69
|
-
const fontFamily = escapeAttr(
|
|
70
|
+
const fontFamily = escapeAttr(resolveFontFamily(element.fontFamily));
|
|
70
71
|
const fontStyle = element.fontStyle ? ` font-style="${escapeAttr(String(element.fontStyle))}"` : "";
|
|
71
72
|
const letterSpacing = (0, utils_1.isFiniteNumber)(element.letterSpacing) ? ` letter-spacing="${element.letterSpacing}"` : "";
|
|
72
73
|
const firstY = textFirstLineY(element, lines.length, fontSize, lineHeight);
|
|
73
|
-
const content = lines
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
const content = lines.map((line, index) => `<tspan x="${element.x}" y="${firstY + index * lineHeight}">${escapeTextLine(line)}</tspan>`).join("");
|
|
75
|
+
return `<text${attrs} xml:space="preserve" text-anchor="${anchor}" dominant-baseline="middle" font-family="${fontFamily}" font-size="${fontSize}" font-weight="${weight}"${fontStyle}${letterSpacing} fill="${fill}">${content}</text>`;
|
|
76
|
+
}
|
|
77
|
+
function resolveFontFamily(value) {
|
|
78
|
+
const text = String(value ?? "").trim();
|
|
79
|
+
if (!text)
|
|
80
|
+
return DEFAULT_FONT_FAMILY;
|
|
81
|
+
const normalized = normalizeFontStack(text);
|
|
82
|
+
if (LEGACY_DEFAULT_FONT_STACKS.has(normalized))
|
|
83
|
+
return DEFAULT_FONT_FAMILY;
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
function normalizeFontStack(value) {
|
|
87
|
+
return value
|
|
88
|
+
.split(",")
|
|
89
|
+
.map((item) => item.trim().replace(/^['"]|['"]$/g, "").toLowerCase())
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join(",");
|
|
77
92
|
}
|
|
78
93
|
function textFirstLineY(element, lineCount, fontSize, lineHeight) {
|
|
79
|
-
if (element.valign === "
|
|
80
|
-
return element.y
|
|
94
|
+
if (element.valign === "middle")
|
|
95
|
+
return element.y - ((lineCount - 1) * lineHeight) / 2;
|
|
81
96
|
if (element.valign === "bottom")
|
|
82
97
|
return element.y - fontSize / 2 - (lineCount - 1) * lineHeight;
|
|
83
|
-
return element.y
|
|
98
|
+
return element.y + fontSize / 2;
|
|
99
|
+
}
|
|
100
|
+
function commonAttrParts(element, context) {
|
|
101
|
+
return {
|
|
102
|
+
id: element.id ? ` id="${escapeAttr(element.id)}"` : "",
|
|
103
|
+
opacity: element.opacity === undefined ? "" : ` opacity="${Number(element.opacity)}"`,
|
|
104
|
+
filter: effectFilter(element.effects, context),
|
|
105
|
+
clip: clipPath(element.clip, context),
|
|
106
|
+
mask: maskPath(element.mask, context),
|
|
107
|
+
blend: element.blendMode && element.blendMode !== "normal" ? ` style="mix-blend-mode:${escapeAttr(element.blendMode)}"` : "",
|
|
108
|
+
transform: element.type === "group" ? "" : elementTransform(element)
|
|
109
|
+
};
|
|
84
110
|
}
|
|
85
|
-
function
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
const filter = effectFilter(element.effects, context);
|
|
89
|
-
const clip = clipPath(element.clip, context);
|
|
90
|
-
const mask = maskPath(element.mask, context);
|
|
91
|
-
const blend = element.blendMode && element.blendMode !== "normal" ? ` style="mix-blend-mode:${escapeAttr(element.blendMode)}"` : "";
|
|
92
|
-
const transform = element.type === "group" ? "" : elementTransform(element);
|
|
93
|
-
return `${id}${opacity}${filter}${clip}${mask}${blend}${transform}`;
|
|
111
|
+
function joinCommonAttrs(parts, omit = []) {
|
|
112
|
+
const skipped = new Set(omit);
|
|
113
|
+
return `${skipped.has("id") ? "" : parts.id}${skipped.has("opacity") ? "" : parts.opacity}${skipped.has("filter") ? "" : parts.filter}${skipped.has("clip") ? "" : parts.clip}${skipped.has("mask") ? "" : parts.mask}${skipped.has("blend") ? "" : parts.blend}${skipped.has("transform") ? "" : parts.transform}`;
|
|
94
114
|
}
|
|
95
115
|
function strokeAttrs(element, context, fallback) {
|
|
96
116
|
const hasStroke = element.stroke !== undefined || element.strokeWidth !== undefined || fallback !== "none";
|
|
97
117
|
if (!hasStroke)
|
|
98
118
|
return "";
|
|
99
119
|
const stroke = paintValue(element.stroke, context, fallback);
|
|
100
|
-
const width = Number(element.strokeWidth ?? (fallback
|
|
120
|
+
const width = Number(element.strokeWidth ?? (element.stroke !== undefined || fallback !== "none" ? 1 : 0));
|
|
101
121
|
const cap = element.strokeCap ? ` stroke-linecap="${escapeAttr(element.strokeCap)}"` : "";
|
|
102
122
|
const join = element.strokeJoin ? ` stroke-linejoin="${escapeAttr(element.strokeJoin)}"` : "";
|
|
103
123
|
const miter = (0, utils_1.isFiniteNumber)(element.miterLimit) ? ` stroke-miterlimit="${element.miterLimit}"` : "";
|
|
@@ -137,23 +157,11 @@ function paintValue(paint, context, fallback) {
|
|
|
137
157
|
return `url(#${id})`;
|
|
138
158
|
}
|
|
139
159
|
function gradientStops(stops) {
|
|
140
|
-
return stops
|
|
141
|
-
.map((stop) => {
|
|
160
|
+
return stops.map((stop) => {
|
|
142
161
|
const offset = Array.isArray(stop) ? stop[0] : stop.offset;
|
|
143
162
|
const color = Array.isArray(stop) ? stop[1] : stop.color;
|
|
144
163
|
return `<stop offset="${Number(offset) * 100}%" stop-color="${escapeAttr(String(color))}"/>`;
|
|
145
|
-
})
|
|
146
|
-
.join("");
|
|
147
|
-
}
|
|
148
|
-
function markerForStroke(stroke, context) {
|
|
149
|
-
const color = typeof stroke === "string" ? stroke : "#111827";
|
|
150
|
-
const cached = context.markerIds.get(color);
|
|
151
|
-
if (cached)
|
|
152
|
-
return cached;
|
|
153
|
-
const id = nextId(context, "arrow");
|
|
154
|
-
context.markerIds.set(color, id);
|
|
155
|
-
context.defs.push(`<marker id="${id}" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 5 L 0 10 z" fill="${escapeAttr(color)}"/></marker>`);
|
|
156
|
-
return id;
|
|
164
|
+
}).join("");
|
|
157
165
|
}
|
|
158
166
|
function effectFilter(effects, context) {
|
|
159
167
|
if (!effects)
|
|
@@ -185,15 +193,14 @@ function clipPath(clip, context) {
|
|
|
185
193
|
if (!clip)
|
|
186
194
|
return "";
|
|
187
195
|
const id = nextId(context, "clip");
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
196
|
+
context.defs.push(`<clipPath id="${id}" clipPathUnits="userSpaceOnUse"><path d="${escapeAttr(clip.d)}"/></clipPath>`);
|
|
197
|
+
return ` clip-path="url(#${id})"`;
|
|
198
|
+
}
|
|
199
|
+
function imageSourceClipAttr(element, context) {
|
|
200
|
+
if (!element.source)
|
|
201
|
+
return "";
|
|
202
|
+
const id = nextId(context, "image-clip");
|
|
203
|
+
context.defs.push(`<clipPath id="${id}" clipPathUnits="userSpaceOnUse"><rect x="${element.x}" y="${element.y}" width="${element.width}" height="${element.height}"/></clipPath>`);
|
|
197
204
|
return ` clip-path="url(#${id})"`;
|
|
198
205
|
}
|
|
199
206
|
function maskPath(mask, context) {
|
|
@@ -201,25 +208,21 @@ function maskPath(mask, context) {
|
|
|
201
208
|
return "";
|
|
202
209
|
const id = nextId(context, "mask");
|
|
203
210
|
const opacity = mask.opacity === undefined ? "" : ` opacity="${Number(mask.opacity)}"`;
|
|
204
|
-
|
|
205
|
-
context.defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" x="-100000" y="-100000" width="200000" height="200000"><rect x="${mask.x}" y="${mask.y}" width="${mask.width}" height="${mask.height}" rx="${Number(mask.radius ?? 0)}" fill="#ffffff"${opacity}/></mask>`);
|
|
206
|
-
}
|
|
207
|
-
else if (mask.type === "circle") {
|
|
208
|
-
context.defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" x="-100000" y="-100000" width="200000" height="200000"><circle cx="${mask.cx}" cy="${mask.cy}" r="${mask.radius}" fill="#ffffff"${opacity}/></mask>`);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
context.defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" x="-100000" y="-100000" width="200000" height="200000"><path d="${escapeAttr(mask.d)}" fill="#ffffff"${opacity}/></mask>`);
|
|
212
|
-
}
|
|
211
|
+
context.defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" x="-100000" y="-100000" width="200000" height="200000"><path d="${escapeAttr(mask.d)}" fill="#ffffff"${opacity}/></mask>`);
|
|
213
212
|
return ` mask="url(#${id})"`;
|
|
214
213
|
}
|
|
215
214
|
function elementTransform(element) {
|
|
215
|
+
const x = element.type === "path" ? Number(element.x ?? 0) : 0;
|
|
216
|
+
const y = element.type === "path" ? Number(element.y ?? 0) : 0;
|
|
216
217
|
const rotation = Number(element.rotation ?? 0);
|
|
217
218
|
const scaleX = Number(element.scaleX ?? element.scale ?? 1);
|
|
218
219
|
const scaleY = Number(element.scaleY ?? element.scale ?? 1);
|
|
219
|
-
if (rotation === 0 && scaleX === 1 && scaleY === 1)
|
|
220
|
+
if (x === 0 && y === 0 && rotation === 0 && scaleX === 1 && scaleY === 1)
|
|
220
221
|
return "";
|
|
221
222
|
const origin = originPoint(element);
|
|
222
223
|
const transforms = [];
|
|
224
|
+
if (x !== 0 || y !== 0)
|
|
225
|
+
transforms.push(`translate(${x} ${y})`);
|
|
223
226
|
if (rotation !== 0)
|
|
224
227
|
transforms.push(`rotate(${rotation} ${origin[0]} ${origin[1]})`);
|
|
225
228
|
if (scaleX !== 1 || scaleY !== 1)
|
|
@@ -227,12 +230,11 @@ function elementTransform(element) {
|
|
|
227
230
|
return transforms.length ? ` transform="${transforms.join(" ")}"` : "";
|
|
228
231
|
}
|
|
229
232
|
function groupTransform(element) {
|
|
230
|
-
const
|
|
233
|
+
const transforms = [`translate(${element.x} ${element.y})`];
|
|
231
234
|
const rotation = Number(element.rotation ?? 0);
|
|
232
235
|
const scaleX = Number(element.scaleX ?? element.scale ?? 1);
|
|
233
236
|
const scaleY = Number(element.scaleY ?? element.scale ?? 1);
|
|
234
237
|
const origin = groupOrigin(element);
|
|
235
|
-
const transforms = [base];
|
|
236
238
|
if (rotation !== 0)
|
|
237
239
|
transforms.push(`rotate(${rotation} ${origin[0]} ${origin[1]})`);
|
|
238
240
|
if (scaleX !== 1 || scaleY !== 1)
|
|
@@ -242,17 +244,16 @@ function groupTransform(element) {
|
|
|
242
244
|
function originPoint(element) {
|
|
243
245
|
if ((0, utils_1.isPoint2)(element.origin))
|
|
244
246
|
return element.origin;
|
|
247
|
+
if (element.type === "text" || element.type === "point") {
|
|
248
|
+
return [Number(element.x ?? 0), Number(element.y ?? 0)];
|
|
249
|
+
}
|
|
245
250
|
const box = (0, utils_1.elementBox)(element);
|
|
246
|
-
|
|
247
|
-
return [0, 0];
|
|
248
|
-
return (0, utils_1.anchorPoint)(box, typeof element.origin === "string" ? element.origin : "center");
|
|
251
|
+
return box ? [box.x + box.width / 2, box.y + box.height / 2] : [0, 0];
|
|
249
252
|
}
|
|
250
253
|
function groupOrigin(element) {
|
|
251
254
|
if ((0, utils_1.isPoint2)(element.origin))
|
|
252
255
|
return [element.origin[0] - element.x, element.origin[1] - element.y];
|
|
253
|
-
|
|
254
|
-
const height = Number(element.height ?? 0);
|
|
255
|
-
return (0, utils_1.anchorPoint)({ x: 0, y: 0, width, height }, typeof element.origin === "string" ? element.origin : "center");
|
|
256
|
+
return [Number(element.width ?? 0) / 2, Number(element.height ?? 0) / 2];
|
|
256
257
|
}
|
|
257
258
|
function imageFit(fit) {
|
|
258
259
|
if (fit === "contain")
|
|
@@ -272,6 +273,9 @@ function escapeAttr(value) {
|
|
|
272
273
|
function escapeText(value) {
|
|
273
274
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
274
275
|
}
|
|
276
|
+
function escapeTextLine(value) {
|
|
277
|
+
return escapeText(value.replace(/\t/g, " ")).replace(/ /g, " ");
|
|
278
|
+
}
|
|
275
279
|
function clamp(value, min, max) {
|
|
276
280
|
return Math.max(min, Math.min(max, value));
|
|
277
281
|
}
|
|
@@ -7,9 +7,8 @@ function renderToThreeHtml(document, options = {}) {
|
|
|
7
7
|
const width = document.canvas.width;
|
|
8
8
|
const height = document.canvas.height;
|
|
9
9
|
const background = options.transparent ? "transparent" : (document.canvas.background ?? "#ffffff");
|
|
10
|
-
const kernel = (0, kernel_1.lowerVisualDocument)(document);
|
|
11
|
-
const elements = JSON.stringify(kernel.elements ?? []);
|
|
12
10
|
const initialTime = Number(options.time ?? 0);
|
|
11
|
+
const frames = JSON.stringify(sampleKernelFrames(document, initialTime));
|
|
13
12
|
const threeRuntime = options.threeRuntime ?? DEFAULT_THREE_RUNTIME;
|
|
14
13
|
return `<!doctype html>
|
|
15
14
|
<html>
|
|
@@ -54,7 +53,7 @@ import * as THREE from ${JSON.stringify(threeRuntime)};
|
|
|
54
53
|
const width = ${JSON.stringify(width)};
|
|
55
54
|
const height = ${JSON.stringify(height)};
|
|
56
55
|
const background = ${JSON.stringify(background)};
|
|
57
|
-
const
|
|
56
|
+
const frames = ${frames};
|
|
58
57
|
const canvas = document.getElementById("stage");
|
|
59
58
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, preserveDrawingBuffer: true });
|
|
60
59
|
renderer.setSize(width, height, false);
|
|
@@ -67,24 +66,9 @@ const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000);
|
|
|
67
66
|
camera.position.set(6, 4, 8);
|
|
68
67
|
camera.lookAt(0, 0, 0);
|
|
69
68
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const object = createObject(element);
|
|
74
|
-
if (!object) continue;
|
|
75
|
-
bindObject(object, element);
|
|
76
|
-
scene.add(object);
|
|
77
|
-
objects.push(object);
|
|
78
|
-
if (element.type === "light") hasLight = true;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!hasLight) {
|
|
82
|
-
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
|
|
83
|
-
scene.add(ambient);
|
|
84
|
-
const light = new THREE.DirectionalLight(0xffffff, 1);
|
|
85
|
-
light.position.set(5, 8, 5);
|
|
86
|
-
scene.add(light);
|
|
87
|
-
}
|
|
69
|
+
const content = new THREE.Group();
|
|
70
|
+
scene.add(content);
|
|
71
|
+
let currentFrameIndex = -1;
|
|
88
72
|
|
|
89
73
|
function createObject(element) {
|
|
90
74
|
if (element.type === "mesh3d") {
|
|
@@ -165,107 +149,62 @@ function materialFor(element) {
|
|
|
165
149
|
});
|
|
166
150
|
}
|
|
167
151
|
|
|
168
|
-
function bindObject(object, element) {
|
|
169
|
-
object.userData.sketchmark = {
|
|
170
|
-
element,
|
|
171
|
-
basePosition: object.position.clone(),
|
|
172
|
-
baseRotation: object.rotation.clone(),
|
|
173
|
-
baseScale: object.scale.clone(),
|
|
174
|
-
baseOpacity: materialOpacity(object),
|
|
175
|
-
baseIntensity: typeof object.intensity === "number" ? object.intensity : undefined
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
152
|
function showTime(rawTime = 0) {
|
|
180
153
|
const time = Number.isFinite(Number(rawTime)) ? Number(rawTime) : 0;
|
|
181
|
-
|
|
154
|
+
const frameIndex = nearestFrameIndex(time);
|
|
155
|
+
if (frameIndex !== currentFrameIndex) {
|
|
156
|
+
currentFrameIndex = frameIndex;
|
|
157
|
+
setElements(frames[frameIndex] ? frames[frameIndex].elements : []);
|
|
158
|
+
}
|
|
182
159
|
renderer.render(scene, camera);
|
|
183
160
|
return true;
|
|
184
161
|
}
|
|
185
162
|
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const sx = baseScale.x * valueAt(animation.scaleX, time, number(element.scaleX, allScale));
|
|
206
|
-
const sy = baseScale.y * valueAt(animation.scaleY, time, number(element.scaleY, allScale));
|
|
207
|
-
const sz = baseScale.z * valueAt(animation.scaleZ, time, number(element.scaleZ, allScale));
|
|
208
|
-
object.scale.set(sx, sy, sz);
|
|
209
|
-
|
|
210
|
-
const opacity = valueAt(animation.opacity, time, number(element.opacity, data.baseOpacity ?? 1));
|
|
211
|
-
setMaterialOpacity(object, opacity);
|
|
212
|
-
|
|
213
|
-
if (typeof object.intensity === "number") {
|
|
214
|
-
object.intensity = valueAt(animation.intensity, time, number(element.intensity, data.baseIntensity ?? 1));
|
|
163
|
+
function setElements(elements) {
|
|
164
|
+
while (content.children.length) {
|
|
165
|
+
const child = content.children[0];
|
|
166
|
+
content.remove(child);
|
|
167
|
+
disposeObject(child);
|
|
168
|
+
}
|
|
169
|
+
let hasLight = false;
|
|
170
|
+
for (const element of elements || []) {
|
|
171
|
+
const object = createObject(element);
|
|
172
|
+
if (!object) continue;
|
|
173
|
+
content.add(object);
|
|
174
|
+
if (element.type === "light") hasLight = true;
|
|
175
|
+
}
|
|
176
|
+
if (!hasLight) {
|
|
177
|
+
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
|
|
178
|
+
content.add(ambient);
|
|
179
|
+
const light = new THREE.DirectionalLight(0xffffff, 1);
|
|
180
|
+
light.position.set(5, 8, 5);
|
|
181
|
+
content.add(light);
|
|
215
182
|
}
|
|
216
183
|
}
|
|
217
184
|
|
|
218
|
-
function
|
|
219
|
-
if (!
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
for (let index = 0; index < frames.length - 1; index += 1) {
|
|
228
|
-
const from = frames[index];
|
|
229
|
-
const to = frames[index + 1];
|
|
230
|
-
if (time > to[0]) continue;
|
|
231
|
-
const span = Math.max(0.000001, to[0] - from[0]);
|
|
232
|
-
const t = easeValue((time - from[0]) / span, animation.ease);
|
|
233
|
-
return from[1] + (to[1] - from[1]) * t;
|
|
185
|
+
function nearestFrameIndex(time) {
|
|
186
|
+
if (!frames.length) return 0;
|
|
187
|
+
let best = 0;
|
|
188
|
+
let bestDistance = Infinity;
|
|
189
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
190
|
+
const distance = Math.abs(number(frames[index].time, 0) - time);
|
|
191
|
+
if (distance < bestDistance) {
|
|
192
|
+
best = index;
|
|
193
|
+
bestDistance = distance;
|
|
234
194
|
}
|
|
235
|
-
return frames[frames.length - 1][1];
|
|
236
195
|
}
|
|
237
|
-
|
|
238
|
-
const from = number(animation.from, fallback);
|
|
239
|
-
const to = number(animation.to, fallback);
|
|
240
|
-
const delay = number(animation.delay, 0);
|
|
241
|
-
const duration = Math.max(0.000001, number(animation.duration, 1));
|
|
242
|
-
const t = easeValue((time - delay) / duration, animation.ease);
|
|
243
|
-
if (time <= delay) return from;
|
|
244
|
-
if (time >= delay + duration) return to;
|
|
245
|
-
return from + (to - from) * t;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function easeValue(value, kind) {
|
|
249
|
-
const t = clamp(value, 0, 1);
|
|
250
|
-
if (kind === "ease-in") return t * t;
|
|
251
|
-
if (kind === "ease-out") return 1 - (1 - t) * (1 - t);
|
|
252
|
-
if (kind === "ease-in-out") return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
253
|
-
return t;
|
|
196
|
+
return best;
|
|
254
197
|
}
|
|
255
198
|
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
material.opacity = opacity;
|
|
266
|
-
material.transparent = opacity < 1;
|
|
267
|
-
material.needsUpdate = true;
|
|
268
|
-
}
|
|
199
|
+
function disposeObject(object) {
|
|
200
|
+
object.traverse((child) => {
|
|
201
|
+
if (child.geometry) child.geometry.dispose();
|
|
202
|
+
const materials = Array.isArray(child.material) ? child.material : child.material ? [child.material] : [];
|
|
203
|
+
for (const material of materials) {
|
|
204
|
+
if (material.map) material.map.dispose();
|
|
205
|
+
material.dispose();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
269
208
|
}
|
|
270
209
|
|
|
271
210
|
function applyPosition(object, position) {
|
|
@@ -286,10 +225,6 @@ function degrees(value) {
|
|
|
286
225
|
return number(value, 0) * Math.PI / 180;
|
|
287
226
|
}
|
|
288
227
|
|
|
289
|
-
function clamp(value, min, max) {
|
|
290
|
-
return Math.max(min, Math.min(max, Number(value) || 0));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
228
|
window.__SKETCHMARK_SHOW_TIME__ = showTime;
|
|
294
229
|
window.__SKETCHMARK_READY__ = true;
|
|
295
230
|
window.addEventListener("message", (event) => {
|
|
@@ -301,3 +236,22 @@ showTime(${JSON.stringify(initialTime)});
|
|
|
301
236
|
</body>
|
|
302
237
|
</html>`;
|
|
303
238
|
}
|
|
239
|
+
function sampleKernelFrames(document, initialTime) {
|
|
240
|
+
const duration = finiteNumber(document.canvas.duration, 0);
|
|
241
|
+
const fps = Math.max(1, Math.min(60, finiteNumber(document.canvas.fps, 30)));
|
|
242
|
+
const times = new Set([0, Math.max(0, initialTime)]);
|
|
243
|
+
if (duration > 0) {
|
|
244
|
+
const frameCount = Math.ceil(duration * fps);
|
|
245
|
+
for (let index = 0; index <= frameCount; index += 1) {
|
|
246
|
+
times.add(Number((index / fps).toFixed(6)));
|
|
247
|
+
}
|
|
248
|
+
times.add(duration);
|
|
249
|
+
}
|
|
250
|
+
return [...times]
|
|
251
|
+
.sort((left, right) => left - right)
|
|
252
|
+
.map((time) => ({ time, elements: (0, kernel_1.resolveKernelFrame)(document, time).elements ?? [] }));
|
|
253
|
+
}
|
|
254
|
+
function finiteNumber(value, fallback) {
|
|
255
|
+
const numeric = Number(value);
|
|
256
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
257
|
+
}
|
package/dist/src/scenes.js
CHANGED
|
@@ -16,6 +16,7 @@ function documentForScene(document, sceneId) {
|
|
|
16
16
|
...(0, utils_1.clone)(document),
|
|
17
17
|
canvas: { ...document.canvas, ...(scene.canvas ?? {}) },
|
|
18
18
|
elements: (0, utils_1.clone)(scene.elements),
|
|
19
|
+
motion: (0, utils_1.clone)(scene.motion),
|
|
19
20
|
scenes: undefined,
|
|
20
21
|
sequences: undefined
|
|
21
22
|
};
|