sketchmark 2.0.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/README.md +188 -0
- package/bin/sketchmark.cjs +2008 -0
- package/dist/src/builders/index.d.ts +74 -0
- package/dist/src/builders/index.js +230 -0
- package/dist/src/compounds.d.ts +13 -0
- package/dist/src/compounds.js +118 -0
- package/dist/src/deck.d.ts +4 -0
- package/dist/src/deck.js +91 -0
- package/dist/src/diagnostics.d.ts +5 -0
- package/dist/src/diagnostics.js +113 -0
- package/dist/src/export/index.d.ts +8 -0
- package/dist/src/export/index.js +15 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +35 -0
- package/dist/src/kernel.d.ts +8 -0
- package/dist/src/kernel.js +68 -0
- package/dist/src/normalize.d.ts +6 -0
- package/dist/src/normalize.js +191 -0
- package/dist/src/patch.d.ts +5 -0
- package/dist/src/patch.js +72 -0
- package/dist/src/path-sampling.d.ts +3 -0
- package/dist/src/path-sampling.js +275 -0
- package/dist/src/player/index.d.ts +68 -0
- package/dist/src/player/index.js +600 -0
- package/dist/src/project.d.ts +11 -0
- package/dist/src/project.js +107 -0
- package/dist/src/render/html.d.ts +2 -0
- package/dist/src/render/html.js +13 -0
- package/dist/src/render/raw-three.d.ts +7 -0
- package/dist/src/render/raw-three.js +17 -0
- package/dist/src/render/svg.d.ts +3 -0
- package/dist/src/render/svg.js +277 -0
- package/dist/src/render/three-html.d.ts +2 -0
- package/dist/src/render/three-html.js +303 -0
- package/dist/src/render/three-preview-svg.d.ts +3 -0
- package/dist/src/render/three-preview-svg.js +102 -0
- package/dist/src/scenes.d.ts +4 -0
- package/dist/src/scenes.js +25 -0
- package/dist/src/schema.d.ts +2 -0
- package/dist/src/schema.js +403 -0
- package/dist/src/sequences.d.ts +43 -0
- package/dist/src/sequences.js +109 -0
- package/dist/src/shapes/builtins.d.ts +2 -0
- package/dist/src/shapes/builtins.js +429 -0
- package/dist/src/shapes/common.d.ts +9 -0
- package/dist/src/shapes/common.js +75 -0
- package/dist/src/shapes/geometry.d.ts +22 -0
- package/dist/src/shapes/geometry.js +166 -0
- package/dist/src/shapes/index.d.ts +2 -0
- package/dist/src/shapes/index.js +18 -0
- package/dist/src/shapes/registry.d.ts +9 -0
- package/dist/src/shapes/registry.js +35 -0
- package/dist/src/shapes/types.d.ts +34 -0
- package/dist/src/shapes/types.js +2 -0
- package/dist/src/types.d.ts +439 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils.d.ts +25 -0
- package/dist/src/utils.js +157 -0
- package/dist/src/validate.d.ts +2 -0
- package/dist/src/validate.js +434 -0
- package/dist/tests/run.d.ts +1 -0
- package/dist/tests/run.js +651 -0
- package/package.json +52 -0
- package/schema/visual.schema.json +930 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadVisualProject = loadVisualProject;
|
|
4
|
+
exports.buildSymbolIndex = buildSymbolIndex;
|
|
5
|
+
exports.validateVisualProject = validateVisualProject;
|
|
6
|
+
const validate_1 = require("./validate");
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
function loadVisualProject(entryPath) {
|
|
11
|
+
const entry = path.resolve(entryPath);
|
|
12
|
+
const root = path.dirname(entry);
|
|
13
|
+
const files = {};
|
|
14
|
+
const document = loadRecursive(entry, root, files, new Set());
|
|
15
|
+
const project = { root, entry, document, files, symbols: [] };
|
|
16
|
+
project.symbols = buildSymbolIndex(project);
|
|
17
|
+
const result = validateVisualProject(project);
|
|
18
|
+
if (!result.ok) {
|
|
19
|
+
const first = result.issues[0];
|
|
20
|
+
throw new Error(first ? `${first.path}: ${first.message}` : "Invalid visual project.");
|
|
21
|
+
}
|
|
22
|
+
return project;
|
|
23
|
+
}
|
|
24
|
+
function buildSymbolIndex(project) {
|
|
25
|
+
const symbols = [];
|
|
26
|
+
const addElements = (elements, file, scene, prefix) => {
|
|
27
|
+
for (const [index, element] of (0, utils_1.flattenElements)(elements ?? []).entries()) {
|
|
28
|
+
if (!element.id)
|
|
29
|
+
continue;
|
|
30
|
+
symbols.push({
|
|
31
|
+
id: element.id,
|
|
32
|
+
type: element.type,
|
|
33
|
+
...(file ? { file } : {}),
|
|
34
|
+
...(scene ? { scene } : {}),
|
|
35
|
+
path: `${prefix}/${index}`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
for (const [file, document] of Object.entries(project.files ?? {})) {
|
|
40
|
+
addElements(document.elements, file, undefined, `/files/${file}/elements`);
|
|
41
|
+
for (const [sceneId, scene] of Object.entries(document.scenes ?? {})) {
|
|
42
|
+
addElements(scene.elements, file, sceneId, `/files/${file}/scenes/${sceneId}/elements`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!project.files || !Object.keys(project.files).length) {
|
|
46
|
+
addElements(project.document.elements, undefined, undefined, "/elements");
|
|
47
|
+
for (const [sceneId, scene] of Object.entries(project.document.scenes ?? {})) {
|
|
48
|
+
addElements(scene.elements, undefined, sceneId, `/scenes/${sceneId}/elements`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return symbols;
|
|
52
|
+
}
|
|
53
|
+
function validateVisualProject(project) {
|
|
54
|
+
const merged = (0, validate_1.validateVisualDocument)(project.document);
|
|
55
|
+
const issues = [...merged.issues];
|
|
56
|
+
const warnings = [...merged.warnings];
|
|
57
|
+
for (const [file, document] of Object.entries(project.files ?? {})) {
|
|
58
|
+
const result = (0, validate_1.validateVisualDocument)({ ...document, sequences: undefined });
|
|
59
|
+
issues.push(...result.issues.map((item) => ({ ...item, path: `/files/${file}${item.path}` })));
|
|
60
|
+
warnings.push(...result.warnings.map((item) => ({ ...item, path: `/files/${file}${item.path}` })));
|
|
61
|
+
}
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const symbol of project.symbols ?? []) {
|
|
64
|
+
const scope = `${symbol.file ?? "<merged>"}:${symbol.scene ?? "<root>"}:${symbol.id}`;
|
|
65
|
+
if (seen.has(scope)) {
|
|
66
|
+
issues.push({
|
|
67
|
+
path: symbol.path,
|
|
68
|
+
code: "duplicate_symbol",
|
|
69
|
+
message: `Duplicate id '${symbol.id}' in the same file/scene scope.`,
|
|
70
|
+
suggestion: "Use stable unique ids so AI patches can target one primitive."
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
seen.add(scope);
|
|
74
|
+
}
|
|
75
|
+
return { ok: issues.length === 0, issues, warnings };
|
|
76
|
+
}
|
|
77
|
+
function loadRecursive(filePath, root, files, seen) {
|
|
78
|
+
const absolute = path.resolve(filePath);
|
|
79
|
+
const relative = normalizePath(path.relative(root, absolute));
|
|
80
|
+
if (seen.has(absolute))
|
|
81
|
+
throw new Error(`Circular import detected at '${relative}'.`);
|
|
82
|
+
seen.add(absolute);
|
|
83
|
+
const source = JSON.parse(fs.readFileSync(absolute, "utf8"));
|
|
84
|
+
files[relative] = source;
|
|
85
|
+
const merged = {
|
|
86
|
+
...source,
|
|
87
|
+
elements: [...(source.elements ?? [])],
|
|
88
|
+
scenes: { ...(source.scenes ?? {}) },
|
|
89
|
+
sequences: { ...(source.sequences ?? {}) },
|
|
90
|
+
assets: { ...(source.assets ?? {}) }
|
|
91
|
+
};
|
|
92
|
+
for (const [key, importPath] of Object.entries(source.imports ?? {})) {
|
|
93
|
+
const child = loadRecursive(path.resolve(path.dirname(absolute), importPath), root, files, seen);
|
|
94
|
+
if (child.elements?.length) {
|
|
95
|
+
merged.scenes = merged.scenes ?? {};
|
|
96
|
+
merged.scenes[key] = { id: key, canvas: child.canvas, elements: child.elements };
|
|
97
|
+
}
|
|
98
|
+
merged.scenes = { ...(merged.scenes ?? {}), ...(child.scenes ?? {}) };
|
|
99
|
+
merged.sequences = { ...(merged.sequences ?? {}), ...(child.sequences ?? {}) };
|
|
100
|
+
merged.assets = { ...(merged.assets ?? {}), ...(child.assets ?? {}) };
|
|
101
|
+
}
|
|
102
|
+
seen.delete(absolute);
|
|
103
|
+
return merged;
|
|
104
|
+
}
|
|
105
|
+
function normalizePath(value) {
|
|
106
|
+
return value.replace(/\\/g, "/");
|
|
107
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderToHtml = renderToHtml;
|
|
4
|
+
const three_html_1 = require("./three-html");
|
|
5
|
+
const svg_1 = require("./svg");
|
|
6
|
+
function renderToHtml(document, options = {}) {
|
|
7
|
+
if (document.canvas.renderer === "three")
|
|
8
|
+
return (0, three_html_1.renderToThreeHtml)(document, options);
|
|
9
|
+
const svg = (0, svg_1.renderToSvg)(document, options);
|
|
10
|
+
const duration = Number(document.canvas.duration ?? 0);
|
|
11
|
+
const background = options.transparent ? "transparent" : "#111827";
|
|
12
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sketchmark Visual</title><style>html,body{margin:0;width:100%;height:100%;background:${background};display:grid;place-items:center}.sketchmark-frame{max-width:100vw;max-height:100vh}</style></head><body><div class="sketchmark-frame" data-duration="${duration}">${svg}</div></body></html>`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderRawThreeModuleHtml = renderRawThreeModuleHtml;
|
|
4
|
+
function renderRawThreeModuleHtml(options) {
|
|
5
|
+
const background = options.background ?? "#ffffff";
|
|
6
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sketchmark Raw Three Module</title><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:${escapeCss(background)}}canvas{display:block}</style></head><body><canvas id="stage"></canvas><script type="module">
|
|
7
|
+
import { createSketchmarkThreeScene } from ${JSON.stringify(options.moduleUrl)};
|
|
8
|
+
const canvas = document.getElementById("stage");
|
|
9
|
+
const runtime = await createSketchmarkThreeScene({ canvas, width: ${Number(options.width)}, height: ${Number(options.height)}, background: ${JSON.stringify(background)} });
|
|
10
|
+
if (runtime && typeof runtime.render === "function") runtime.render(0);
|
|
11
|
+
window.__SKETCHMARK_SHOW_TIME__ = (time) => runtime && typeof runtime.render === "function" ? runtime.render(time || 0) : undefined;
|
|
12
|
+
window.__SKETCHMARK_READY__ = true;
|
|
13
|
+
</script></body></html>`;
|
|
14
|
+
}
|
|
15
|
+
function escapeCss(value) {
|
|
16
|
+
return String(value).replace(/[<>"'()\\]/g, "");
|
|
17
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { KernelVisualDocument, RenderOptions, ResolvedVisualDocument, VisualDocument } from "../types";
|
|
2
|
+
export declare function renderToSvg(document: VisualDocument, options?: RenderOptions): string;
|
|
3
|
+
export declare function renderResolvedSvg(document: ResolvedVisualDocument | KernelVisualDocument, options?: RenderOptions): string;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderToSvg = renderToSvg;
|
|
4
|
+
exports.renderResolvedSvg = renderResolvedSvg;
|
|
5
|
+
const kernel_1 = require("../kernel");
|
|
6
|
+
const utils_1 = require("../utils");
|
|
7
|
+
function renderToSvg(document, options = {}) {
|
|
8
|
+
const frame = (0, kernel_1.resolveKernelFrame)(document, options.time ?? 0);
|
|
9
|
+
return renderResolvedSvg(frame, options);
|
|
10
|
+
}
|
|
11
|
+
function renderResolvedSvg(document, options = {}) {
|
|
12
|
+
const kernel = isKernelVisualDocument(document) ? document : (0, kernel_1.lowerResolvedVisualDocument)(document);
|
|
13
|
+
const width = document.canvas.width;
|
|
14
|
+
const height = document.canvas.height;
|
|
15
|
+
const background = document.canvas.background ?? "#ffffff";
|
|
16
|
+
const context = { defs: [], nextId: 0, markerIds: new Map() };
|
|
17
|
+
const elements = (kernel.elements ?? []).map((element) => renderElement(element, context)).join("");
|
|
18
|
+
const backdrop = options.transparent ? "" : `<rect x="0" y="0" width="${width}" height="${height}" fill="${escapeAttr(background)}"/>`;
|
|
19
|
+
const defs = context.defs.length ? `<defs>${context.defs.join("")}</defs>` : "";
|
|
20
|
+
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
|
+
}
|
|
22
|
+
function renderElement(element, context) {
|
|
23
|
+
const attrs = commonAttrs(element, context);
|
|
24
|
+
const fill = paintValue(element.fill, context, element.type === "text" ? "#111827" : "none");
|
|
25
|
+
const stroke = strokeAttrs(element, context, "none");
|
|
26
|
+
switch (element.type) {
|
|
27
|
+
case "point":
|
|
28
|
+
return "";
|
|
29
|
+
case "path": {
|
|
30
|
+
const marker = element.metadata?.markerEnd === "arrow" ? ` marker-end="url(#${markerForStroke(element.stroke, context)})"` : "";
|
|
31
|
+
return `<path${attrs} d="${escapeAttr(element.d)}" fill="${fill}"${stroke}${marker}${drawAttrs(element)}/>`;
|
|
32
|
+
}
|
|
33
|
+
case "text":
|
|
34
|
+
return renderText(element, attrs, fill);
|
|
35
|
+
case "image":
|
|
36
|
+
return renderImage(element, attrs, context);
|
|
37
|
+
case "group": {
|
|
38
|
+
const transform = groupTransform(element);
|
|
39
|
+
const children = (element.children ?? []).map((child) => renderElement(child, context)).join("");
|
|
40
|
+
return `<g${attrs}${transform}>${children}</g>`;
|
|
41
|
+
}
|
|
42
|
+
default:
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
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
|
+
function renderImage(element, attrs, context) {
|
|
50
|
+
if (element.source) {
|
|
51
|
+
const id = nextId(context, "image-crop");
|
|
52
|
+
const scaleX = element.width / element.source.width;
|
|
53
|
+
const scaleY = element.height / element.source.height;
|
|
54
|
+
const imageX = element.x - element.source.x * scaleX;
|
|
55
|
+
const imageY = element.y - element.source.y * scaleY;
|
|
56
|
+
const imageWidth = element.source.imageWidth * scaleX;
|
|
57
|
+
const imageHeight = element.source.imageHeight * scaleY;
|
|
58
|
+
context.defs.push(`<clipPath id="${id}" clipPathUnits="userSpaceOnUse"><rect x="${element.x}" y="${element.y}" width="${element.width}" height="${element.height}"/></clipPath>`);
|
|
59
|
+
return `<image${attrs} href="${escapeAttr(element.src)}" x="${imageX}" y="${imageY}" width="${imageWidth}" height="${imageHeight}" preserveAspectRatio="none" clip-path="url(#${id})"/>`;
|
|
60
|
+
}
|
|
61
|
+
return `<image${attrs} href="${escapeAttr(element.src)}" x="${element.x}" y="${element.y}" width="${element.width}" height="${element.height}" preserveAspectRatio="${imageFit(element.fit)}"/>`;
|
|
62
|
+
}
|
|
63
|
+
function renderText(element, attrs, fill) {
|
|
64
|
+
const anchor = element.align === "right" ? "end" : element.align === "left" ? "start" : "middle";
|
|
65
|
+
const fontSize = Number(element.fontSize ?? 16);
|
|
66
|
+
const lineHeight = fontSize * Number(element.lineHeight ?? 1.2);
|
|
67
|
+
const lines = (0, utils_1.textLines)(element);
|
|
68
|
+
const weight = escapeAttr(String(element.weight ?? 400));
|
|
69
|
+
const fontFamily = escapeAttr(String(element.fontFamily ?? "Inter, Arial, sans-serif"));
|
|
70
|
+
const fontStyle = element.fontStyle ? ` font-style="${escapeAttr(String(element.fontStyle))}"` : "";
|
|
71
|
+
const letterSpacing = (0, utils_1.isFiniteNumber)(element.letterSpacing) ? ` letter-spacing="${element.letterSpacing}"` : "";
|
|
72
|
+
const firstY = textFirstLineY(element, lines.length, fontSize, lineHeight);
|
|
73
|
+
const content = lines
|
|
74
|
+
.map((line, index) => `<tspan x="${element.x}" y="${firstY + index * lineHeight}">${escapeText(line)}</tspan>`)
|
|
75
|
+
.join("");
|
|
76
|
+
return `<text${attrs} text-anchor="${anchor}" dominant-baseline="middle" font-family="${fontFamily}" font-size="${fontSize}" font-weight="${weight}"${fontStyle}${letterSpacing} fill="${fill}">${content}</text>`;
|
|
77
|
+
}
|
|
78
|
+
function textFirstLineY(element, lineCount, fontSize, lineHeight) {
|
|
79
|
+
if (element.valign === "top")
|
|
80
|
+
return element.y + fontSize / 2;
|
|
81
|
+
if (element.valign === "bottom")
|
|
82
|
+
return element.y - fontSize / 2 - (lineCount - 1) * lineHeight;
|
|
83
|
+
return element.y - ((lineCount - 1) * lineHeight) / 2;
|
|
84
|
+
}
|
|
85
|
+
function commonAttrs(element, context) {
|
|
86
|
+
const id = element.id ? ` id="${escapeAttr(element.id)}"` : "";
|
|
87
|
+
const opacity = element.opacity === undefined ? "" : ` opacity="${Number(element.opacity)}"`;
|
|
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}`;
|
|
94
|
+
}
|
|
95
|
+
function strokeAttrs(element, context, fallback) {
|
|
96
|
+
const hasStroke = element.stroke !== undefined || element.strokeWidth !== undefined || fallback !== "none";
|
|
97
|
+
if (!hasStroke)
|
|
98
|
+
return "";
|
|
99
|
+
const stroke = paintValue(element.stroke, context, fallback);
|
|
100
|
+
const width = Number(element.strokeWidth ?? (fallback === "none" ? 0 : 1));
|
|
101
|
+
const cap = element.strokeCap ? ` stroke-linecap="${escapeAttr(element.strokeCap)}"` : "";
|
|
102
|
+
const join = element.strokeJoin ? ` stroke-linejoin="${escapeAttr(element.strokeJoin)}"` : "";
|
|
103
|
+
const miter = (0, utils_1.isFiniteNumber)(element.miterLimit) ? ` stroke-miterlimit="${element.miterLimit}"` : "";
|
|
104
|
+
const dash = element.drawStart !== undefined || element.drawEnd !== undefined ? "" : dashAttrs(element);
|
|
105
|
+
return ` stroke="${stroke}" stroke-width="${width}"${cap}${join}${miter}${dash}`;
|
|
106
|
+
}
|
|
107
|
+
function dashAttrs(element) {
|
|
108
|
+
const dash = Array.isArray(element.dashArray) ? ` stroke-dasharray="${element.dashArray.join(" ")}"` : "";
|
|
109
|
+
const offset = (0, utils_1.isFiniteNumber)(element.dashOffset) ? ` stroke-dashoffset="${element.dashOffset}"` : "";
|
|
110
|
+
return `${dash}${offset}`;
|
|
111
|
+
}
|
|
112
|
+
function drawAttrs(element) {
|
|
113
|
+
if (element.drawStart === undefined && element.drawEnd === undefined)
|
|
114
|
+
return "";
|
|
115
|
+
const start = clamp(Number(element.drawStart ?? 0), 0, 1);
|
|
116
|
+
const end = clamp(Number(element.drawEnd ?? 1), 0, 1);
|
|
117
|
+
const visible = Math.max(0, end - start);
|
|
118
|
+
return ` pathLength="1" stroke-dasharray="${visible} 1" stroke-dashoffset="${-start}"`;
|
|
119
|
+
}
|
|
120
|
+
function paintValue(paint, context, fallback) {
|
|
121
|
+
if (paint === undefined)
|
|
122
|
+
return escapeAttr(fallback);
|
|
123
|
+
if (typeof paint === "string")
|
|
124
|
+
return escapeAttr(paint);
|
|
125
|
+
const id = nextId(context, paint.type === "linearGradient" ? "linear-gradient" : paint.type === "radialGradient" ? "radial-gradient" : "pattern");
|
|
126
|
+
if (paint.type === "linearGradient") {
|
|
127
|
+
context.defs.push(`<linearGradient id="${id}" gradientUnits="userSpaceOnUse" x1="${paint.from[0]}" y1="${paint.from[1]}" x2="${paint.to[0]}" y2="${paint.to[1]}">${gradientStops(paint.stops)}</linearGradient>`);
|
|
128
|
+
}
|
|
129
|
+
else if (paint.type === "radialGradient") {
|
|
130
|
+
const focus = paint.focus ?? paint.center;
|
|
131
|
+
context.defs.push(`<radialGradient id="${id}" gradientUnits="userSpaceOnUse" cx="${paint.center[0]}" cy="${paint.center[1]}" r="${paint.radius}" fx="${focus[0]}" fy="${focus[1]}">${gradientStops(paint.stops)}</radialGradient>`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const opacity = paint.opacity === undefined ? "" : ` opacity="${Number(paint.opacity)}"`;
|
|
135
|
+
context.defs.push(`<pattern id="${id}" patternUnits="userSpaceOnUse" x="${Number(paint.x ?? 0)}" y="${Number(paint.y ?? 0)}" width="${paint.width}" height="${paint.height}"><image href="${escapeAttr(paint.src)}" x="0" y="0" width="${paint.width}" height="${paint.height}" preserveAspectRatio="${imageFit(paint.fit)}"${opacity}/></pattern>`);
|
|
136
|
+
}
|
|
137
|
+
return `url(#${id})`;
|
|
138
|
+
}
|
|
139
|
+
function gradientStops(stops) {
|
|
140
|
+
return stops
|
|
141
|
+
.map((stop) => {
|
|
142
|
+
const offset = Array.isArray(stop) ? stop[0] : stop.offset;
|
|
143
|
+
const color = Array.isArray(stop) ? stop[1] : stop.color;
|
|
144
|
+
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;
|
|
157
|
+
}
|
|
158
|
+
function effectFilter(effects, context) {
|
|
159
|
+
if (!effects)
|
|
160
|
+
return "";
|
|
161
|
+
const parts = [];
|
|
162
|
+
if ((0, utils_1.isFiniteNumber)(effects.blur) && effects.blur > 0)
|
|
163
|
+
parts.push(`<feGaussianBlur stdDeviation="${effects.blur}" result="blur"/>`);
|
|
164
|
+
if ((0, utils_1.isFiniteNumber)(effects.brightness) || (0, utils_1.isFiniteNumber)(effects.contrast)) {
|
|
165
|
+
const brightness = effects.brightness ?? 1;
|
|
166
|
+
const contrast = effects.contrast ?? 1;
|
|
167
|
+
const intercept = 0.5 - contrast * 0.5;
|
|
168
|
+
parts.push(`<feComponentTransfer><feFuncR type="linear" slope="${brightness * contrast}" intercept="${intercept}"/><feFuncG type="linear" slope="${brightness * contrast}" intercept="${intercept}"/><feFuncB type="linear" slope="${brightness * contrast}" intercept="${intercept}"/></feComponentTransfer>`);
|
|
169
|
+
}
|
|
170
|
+
if ((0, utils_1.isFiniteNumber)(effects.saturate))
|
|
171
|
+
parts.push(`<feColorMatrix type="saturate" values="${effects.saturate}"/>`);
|
|
172
|
+
if ((0, utils_1.isFiniteNumber)(effects.hueRotate))
|
|
173
|
+
parts.push(`<feColorMatrix type="hueRotate" values="${effects.hueRotate}"/>`);
|
|
174
|
+
if (effects.shadow) {
|
|
175
|
+
const opacity = effects.shadow.opacity ?? 1;
|
|
176
|
+
parts.push(`<feDropShadow dx="${effects.shadow.dx}" dy="${effects.shadow.dy}" stdDeviation="${effects.shadow.blur}" flood-color="${escapeAttr(effects.shadow.color)}" flood-opacity="${opacity}"/>`);
|
|
177
|
+
}
|
|
178
|
+
if (!parts.length)
|
|
179
|
+
return "";
|
|
180
|
+
const id = nextId(context, "filter");
|
|
181
|
+
context.defs.push(`<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%">${parts.join("")}</filter>`);
|
|
182
|
+
return ` filter="url(#${id})"`;
|
|
183
|
+
}
|
|
184
|
+
function clipPath(clip, context) {
|
|
185
|
+
if (!clip)
|
|
186
|
+
return "";
|
|
187
|
+
const id = nextId(context, "clip");
|
|
188
|
+
if (clip.type === "rect") {
|
|
189
|
+
context.defs.push(`<clipPath id="${id}" clipPathUnits="userSpaceOnUse"><rect x="${clip.x}" y="${clip.y}" width="${clip.width}" height="${clip.height}" rx="${Number(clip.radius ?? 0)}"/></clipPath>`);
|
|
190
|
+
}
|
|
191
|
+
else if (clip.type === "circle") {
|
|
192
|
+
context.defs.push(`<clipPath id="${id}" clipPathUnits="userSpaceOnUse"><circle cx="${clip.cx}" cy="${clip.cy}" r="${clip.radius}"/></clipPath>`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
context.defs.push(`<clipPath id="${id}" clipPathUnits="userSpaceOnUse"><path d="${escapeAttr(clip.d)}"/></clipPath>`);
|
|
196
|
+
}
|
|
197
|
+
return ` clip-path="url(#${id})"`;
|
|
198
|
+
}
|
|
199
|
+
function maskPath(mask, context) {
|
|
200
|
+
if (!mask)
|
|
201
|
+
return "";
|
|
202
|
+
const id = nextId(context, "mask");
|
|
203
|
+
const opacity = mask.opacity === undefined ? "" : ` opacity="${Number(mask.opacity)}"`;
|
|
204
|
+
if (mask.type === "rect") {
|
|
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
|
+
}
|
|
213
|
+
return ` mask="url(#${id})"`;
|
|
214
|
+
}
|
|
215
|
+
function elementTransform(element) {
|
|
216
|
+
const rotation = Number(element.rotation ?? 0);
|
|
217
|
+
const scaleX = Number(element.scaleX ?? element.scale ?? 1);
|
|
218
|
+
const scaleY = Number(element.scaleY ?? element.scale ?? 1);
|
|
219
|
+
if (rotation === 0 && scaleX === 1 && scaleY === 1)
|
|
220
|
+
return "";
|
|
221
|
+
const origin = originPoint(element);
|
|
222
|
+
const transforms = [];
|
|
223
|
+
if (rotation !== 0)
|
|
224
|
+
transforms.push(`rotate(${rotation} ${origin[0]} ${origin[1]})`);
|
|
225
|
+
if (scaleX !== 1 || scaleY !== 1)
|
|
226
|
+
transforms.push(`translate(${origin[0]} ${origin[1]}) scale(${scaleX} ${scaleY}) translate(${-origin[0]} ${-origin[1]})`);
|
|
227
|
+
return transforms.length ? ` transform="${transforms.join(" ")}"` : "";
|
|
228
|
+
}
|
|
229
|
+
function groupTransform(element) {
|
|
230
|
+
const base = `translate(${element.x} ${element.y})`;
|
|
231
|
+
const rotation = Number(element.rotation ?? 0);
|
|
232
|
+
const scaleX = Number(element.scaleX ?? element.scale ?? 1);
|
|
233
|
+
const scaleY = Number(element.scaleY ?? element.scale ?? 1);
|
|
234
|
+
const origin = groupOrigin(element);
|
|
235
|
+
const transforms = [base];
|
|
236
|
+
if (rotation !== 0)
|
|
237
|
+
transforms.push(`rotate(${rotation} ${origin[0]} ${origin[1]})`);
|
|
238
|
+
if (scaleX !== 1 || scaleY !== 1)
|
|
239
|
+
transforms.push(`translate(${origin[0]} ${origin[1]}) scale(${scaleX} ${scaleY}) translate(${-origin[0]} ${-origin[1]})`);
|
|
240
|
+
return ` transform="${transforms.join(" ")}"`;
|
|
241
|
+
}
|
|
242
|
+
function originPoint(element) {
|
|
243
|
+
if ((0, utils_1.isPoint2)(element.origin))
|
|
244
|
+
return element.origin;
|
|
245
|
+
const box = (0, utils_1.elementBox)(element);
|
|
246
|
+
if (!box)
|
|
247
|
+
return [0, 0];
|
|
248
|
+
return (0, utils_1.anchorPoint)(box, typeof element.origin === "string" ? element.origin : "center");
|
|
249
|
+
}
|
|
250
|
+
function groupOrigin(element) {
|
|
251
|
+
if ((0, utils_1.isPoint2)(element.origin))
|
|
252
|
+
return [element.origin[0] - element.x, element.origin[1] - element.y];
|
|
253
|
+
const width = Number(element.width ?? 0);
|
|
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
|
+
}
|
|
257
|
+
function imageFit(fit) {
|
|
258
|
+
if (fit === "contain")
|
|
259
|
+
return "xMidYMid meet";
|
|
260
|
+
if (fit === "cover")
|
|
261
|
+
return "xMidYMid slice";
|
|
262
|
+
return "none";
|
|
263
|
+
}
|
|
264
|
+
function nextId(context, prefix) {
|
|
265
|
+
const id = `sketchmark-${prefix}-${context.nextId}`;
|
|
266
|
+
context.nextId += 1;
|
|
267
|
+
return id;
|
|
268
|
+
}
|
|
269
|
+
function escapeAttr(value) {
|
|
270
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
271
|
+
}
|
|
272
|
+
function escapeText(value) {
|
|
273
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
274
|
+
}
|
|
275
|
+
function clamp(value, min, max) {
|
|
276
|
+
return Math.max(min, Math.min(max, value));
|
|
277
|
+
}
|