sketchmark 2.1.2 → 2.1.4
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/bin/editor-ui.cjs +368 -123
- package/bin/editor-ui.d.ts +11 -0
- package/bin/vendor/mp4-muxer.LICENSE.txt +21 -0
- package/bin/vendor/mp4-muxer.mjs +1879 -0
- package/dist/src/browser-export.d.ts +10 -0
- package/dist/src/browser-export.js +220 -0
- package/dist/src/edit.js +39 -0
- package/package.json +59 -46
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { VisualDocument } from "./types";
|
|
2
|
+
export type BrowserExportFormat = "svg" | "png" | "jpg" | "html" | "json" | "mp4" | "webm";
|
|
3
|
+
export type BrowserExportOptions = {
|
|
4
|
+
format: BrowserExportFormat;
|
|
5
|
+
title: string;
|
|
6
|
+
time?: number;
|
|
7
|
+
sourceDocument?: VisualDocument;
|
|
8
|
+
onProgress?: (progress: number) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare function exportVisualInBrowser(document: VisualDocument, options: BrowserExportOptions): Promise<void>;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.exportVisualInBrowser = exportVisualInBrowser;
|
|
37
|
+
const html_1 = require("./render/html");
|
|
38
|
+
const svg_1 = require("./render/svg");
|
|
39
|
+
async function exportVisualInBrowser(document, options) {
|
|
40
|
+
const format = options.format;
|
|
41
|
+
const time = Math.max(0, Number(options.time ?? 0));
|
|
42
|
+
const title = safeFileName(options.title);
|
|
43
|
+
if (format === "json") {
|
|
44
|
+
downloadBlob(new Blob([JSON.stringify(options.sourceDocument ?? document, null, 2)], { type: "application/json" }), `${title}.json`);
|
|
45
|
+
options.onProgress?.(100);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (format === "svg") {
|
|
49
|
+
downloadBlob(new Blob([(0, svg_1.renderToSvg)(document, { time })], { type: "image/svg+xml;charset=utf-8" }), `${title}.svg`);
|
|
50
|
+
options.onProgress?.(100);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (format === "html") {
|
|
54
|
+
downloadBlob(new Blob([(0, html_1.renderToHtml)(document, { time })], { type: "text/html;charset=utf-8" }), `${title}.html`);
|
|
55
|
+
options.onProgress?.(100);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (format === "png" || format === "jpg") {
|
|
59
|
+
await exportRasterFrame(document, { format, title, time });
|
|
60
|
+
options.onProgress?.(100);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (format === "mp4") {
|
|
64
|
+
await exportMp4(document, { title, onProgress: options.onProgress });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
throw new Error("Browser WebM export is not implemented yet. Use MP4 or a server exporter.");
|
|
68
|
+
}
|
|
69
|
+
async function exportRasterFrame(document, options) {
|
|
70
|
+
const { width, height } = canvasSize(document);
|
|
71
|
+
const canvas = documentCanvas(width, height);
|
|
72
|
+
const svg = (0, svg_1.renderToSvg)(document, { time: options.time });
|
|
73
|
+
await drawSvgToCanvas(svg, canvas, width, height);
|
|
74
|
+
const mimeType = options.format === "jpg" ? "image/jpeg" : "image/png";
|
|
75
|
+
const blob = await canvasToBlob(canvas, mimeType, options.format === "jpg" ? 0.92 : undefined);
|
|
76
|
+
downloadBlob(blob, `${options.title}.${options.format}`);
|
|
77
|
+
}
|
|
78
|
+
async function exportMp4(document, options) {
|
|
79
|
+
const globalApi = globalThis;
|
|
80
|
+
const VideoEncoderCtor = globalApi.VideoEncoder;
|
|
81
|
+
const VideoFrameCtor = globalApi.VideoFrame;
|
|
82
|
+
if (!VideoEncoderCtor || !VideoFrameCtor) {
|
|
83
|
+
throw new Error("Browser MP4 export requires WebCodecs. Try Chrome or Edge, or use a server exporter.");
|
|
84
|
+
}
|
|
85
|
+
const duration = Number(document.canvas.duration ?? 0);
|
|
86
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
87
|
+
throw new Error("MP4 export requires a positive canvas.duration.");
|
|
88
|
+
}
|
|
89
|
+
const fps = Math.max(1, Math.round(Number(document.canvas.fps ?? 30) || 30));
|
|
90
|
+
const { width, height } = canvasSize(document);
|
|
91
|
+
const encodeWidth = evenDimension(width);
|
|
92
|
+
const encodeHeight = evenDimension(height);
|
|
93
|
+
const totalFrames = Math.max(1, Math.ceil(duration * fps));
|
|
94
|
+
const { Muxer, ArrayBufferTarget } = await loadMp4Muxer();
|
|
95
|
+
const target = new ArrayBufferTarget();
|
|
96
|
+
const muxer = new Muxer({
|
|
97
|
+
target,
|
|
98
|
+
video: { codec: "avc", width: encodeWidth, height: encodeHeight },
|
|
99
|
+
fastStart: "in-memory"
|
|
100
|
+
});
|
|
101
|
+
let encoderError;
|
|
102
|
+
const encoder = new VideoEncoderCtor({
|
|
103
|
+
output: (chunk, metadata) => muxer.addVideoChunk(chunk, metadata),
|
|
104
|
+
error: (error) => {
|
|
105
|
+
encoderError = error;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
encoder.configure({
|
|
109
|
+
codec: "avc1.640028",
|
|
110
|
+
width: encodeWidth,
|
|
111
|
+
height: encodeHeight,
|
|
112
|
+
bitrate: 5000000,
|
|
113
|
+
framerate: fps
|
|
114
|
+
});
|
|
115
|
+
const canvas = documentCanvas(encodeWidth, encodeHeight);
|
|
116
|
+
try {
|
|
117
|
+
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex += 1) {
|
|
118
|
+
const frameTime = Math.min(duration, frameIndex / fps);
|
|
119
|
+
const svg = (0, svg_1.renderToSvg)(document, { time: frameTime });
|
|
120
|
+
await drawSvgToCanvas(svg, canvas, encodeWidth, encodeHeight);
|
|
121
|
+
const frame = new VideoFrameCtor(canvas, {
|
|
122
|
+
timestamp: Math.round((frameIndex / fps) * 1000000),
|
|
123
|
+
duration: Math.round((1 / fps) * 1000000)
|
|
124
|
+
});
|
|
125
|
+
encoder.encode(frame, { keyFrame: frameIndex % Math.max(1, fps * 2) === 0 });
|
|
126
|
+
frame.close();
|
|
127
|
+
if (encoderError)
|
|
128
|
+
throw encoderError;
|
|
129
|
+
if (frameIndex % 5 === 0 || frameIndex === totalFrames - 1) {
|
|
130
|
+
options.onProgress?.(Math.round(((frameIndex + 1) / totalFrames) * 100));
|
|
131
|
+
await yieldToBrowser();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await encoder.flush();
|
|
135
|
+
if (encoderError)
|
|
136
|
+
throw encoderError;
|
|
137
|
+
encoder.close();
|
|
138
|
+
muxer.finalize();
|
|
139
|
+
downloadBlob(new Blob([target.buffer], { type: "video/mp4" }), `${options.title}.mp4`);
|
|
140
|
+
options.onProgress?.(100);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
try {
|
|
144
|
+
encoder.close();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Ignore close failures after encoder errors.
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function loadMp4Muxer() {
|
|
153
|
+
return Promise.resolve().then(() => __importStar(require("mp4-muxer")));
|
|
154
|
+
}
|
|
155
|
+
function documentCanvas(width, height) {
|
|
156
|
+
const canvas = document.createElement("canvas");
|
|
157
|
+
canvas.width = width;
|
|
158
|
+
canvas.height = height;
|
|
159
|
+
return canvas;
|
|
160
|
+
}
|
|
161
|
+
async function drawSvgToCanvas(svg, canvas, width, height) {
|
|
162
|
+
const context = canvas.getContext("2d");
|
|
163
|
+
if (!context)
|
|
164
|
+
throw new Error("Could not create a 2D canvas context.");
|
|
165
|
+
const image = await loadSvgImage(svg);
|
|
166
|
+
context.clearRect(0, 0, width, height);
|
|
167
|
+
context.drawImage(image, 0, 0, width, height);
|
|
168
|
+
}
|
|
169
|
+
function loadSvgImage(svg) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
|
|
172
|
+
const url = URL.createObjectURL(blob);
|
|
173
|
+
const image = new Image();
|
|
174
|
+
image.onload = () => {
|
|
175
|
+
URL.revokeObjectURL(url);
|
|
176
|
+
resolve(image);
|
|
177
|
+
};
|
|
178
|
+
image.onerror = () => {
|
|
179
|
+
URL.revokeObjectURL(url);
|
|
180
|
+
reject(new Error("Could not rasterize the SVG frame in the browser."));
|
|
181
|
+
};
|
|
182
|
+
image.src = url;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function canvasToBlob(canvas, type, quality) {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
canvas.toBlob((blob) => {
|
|
188
|
+
if (blob)
|
|
189
|
+
resolve(blob);
|
|
190
|
+
else
|
|
191
|
+
reject(new Error("Could not export the canvas frame. Cross-origin images can block browser raster export."));
|
|
192
|
+
}, type, quality);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function canvasSize(document) {
|
|
196
|
+
return {
|
|
197
|
+
width: Math.max(1, Math.round(Number(document.canvas.width) || 1)),
|
|
198
|
+
height: Math.max(1, Math.round(Number(document.canvas.height) || 1))
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function evenDimension(value) {
|
|
202
|
+
const rounded = Math.max(2, Math.round(value));
|
|
203
|
+
return rounded % 2 === 0 ? rounded : rounded + 1;
|
|
204
|
+
}
|
|
205
|
+
function downloadBlob(blob, filename) {
|
|
206
|
+
const url = URL.createObjectURL(blob);
|
|
207
|
+
const anchor = document.createElement("a");
|
|
208
|
+
anchor.href = url;
|
|
209
|
+
anchor.download = filename;
|
|
210
|
+
document.body.appendChild(anchor);
|
|
211
|
+
anchor.click();
|
|
212
|
+
anchor.remove();
|
|
213
|
+
URL.revokeObjectURL(url);
|
|
214
|
+
}
|
|
215
|
+
function safeFileName(value) {
|
|
216
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "sketchmark";
|
|
217
|
+
}
|
|
218
|
+
function yieldToBrowser() {
|
|
219
|
+
return new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
220
|
+
}
|
package/dist/src/edit.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.listTimelineTracks = listTimelineTracks;
|
|
|
9
9
|
exports.roundedRectClipPath = roundedRectClipPath;
|
|
10
10
|
exports.imageRoundedClip = imageRoundedClip;
|
|
11
11
|
const animatable_1 = require("./animatable");
|
|
12
|
+
const keyframes_1 = require("./keyframes");
|
|
12
13
|
const utils_1 = require("./utils");
|
|
13
14
|
const validate_1 = require("./validate");
|
|
14
15
|
function listElementReferences(document) {
|
|
@@ -29,6 +30,7 @@ function findElementById(document, id) {
|
|
|
29
30
|
}
|
|
30
31
|
function setElementProperty(document, id, property, value) {
|
|
31
32
|
const next = (0, utils_1.clone)(document);
|
|
33
|
+
repairLegacyTimelineCurves(next);
|
|
32
34
|
const element = requireElement(next, id);
|
|
33
35
|
applyProperty(element, property, value);
|
|
34
36
|
assertValid(next);
|
|
@@ -39,6 +41,7 @@ function setTimelineKeyframe(document, id, property, time, value, options = {})
|
|
|
39
41
|
if (!(0, utils_1.isFiniteNumber)(time) || time < 0)
|
|
40
42
|
throw new Error("Keyframe time must be a non-negative finite number.");
|
|
41
43
|
const next = (0, utils_1.clone)(document);
|
|
44
|
+
repairLegacyTimelineCurves(next);
|
|
42
45
|
const element = requireElement(next, id);
|
|
43
46
|
element.timeline ?? (element.timeline = {});
|
|
44
47
|
(_a = element.timeline).tracks ?? (_a.tracks = {});
|
|
@@ -157,6 +160,42 @@ function assertValid(document) {
|
|
|
157
160
|
throw new Error(first ? `${first.path}: ${first.message}` : "Invalid visual document.");
|
|
158
161
|
}
|
|
159
162
|
}
|
|
163
|
+
function repairLegacyTimelineCurves(document) {
|
|
164
|
+
visitElements(document.elements ?? [], (element) => {
|
|
165
|
+
for (const track of Object.values(element.timeline?.tracks ?? {})) {
|
|
166
|
+
repairTrackCurve(track);
|
|
167
|
+
for (const frame of track.keyframes ?? [])
|
|
168
|
+
repairKeyframeCurve(frame);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function repairTrackCurve(track) {
|
|
173
|
+
const record = track;
|
|
174
|
+
if (typeof record.curve === "string") {
|
|
175
|
+
const curve = legacyCurve(record.curve);
|
|
176
|
+
if (curve)
|
|
177
|
+
record.curve = curve;
|
|
178
|
+
else
|
|
179
|
+
delete record.curve;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function repairKeyframeCurve(frame) {
|
|
183
|
+
if (Array.isArray(frame))
|
|
184
|
+
return;
|
|
185
|
+
const record = frame;
|
|
186
|
+
for (const key of ["in", "out", "interpolation"]) {
|
|
187
|
+
if (typeof record[key] !== "string")
|
|
188
|
+
continue;
|
|
189
|
+
const curve = legacyCurve(record[key]);
|
|
190
|
+
if (curve)
|
|
191
|
+
record[key] = curve;
|
|
192
|
+
else
|
|
193
|
+
delete record[key];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function legacyCurve(value) {
|
|
197
|
+
return (0, keyframes_1.timelineCurvePreset)(value);
|
|
198
|
+
}
|
|
160
199
|
function finiteOrZero(value) {
|
|
161
200
|
return (0, utils_1.isFiniteNumber)(value) ? value : 0;
|
|
162
201
|
}
|
package/package.json
CHANGED
|
@@ -1,46 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "sketchmark",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "Render kernel for Sketchmark visual documents.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "commonjs",
|
|
7
|
-
"main": "./dist/src/index.js",
|
|
8
|
-
"types": "./dist/src/index.d.ts",
|
|
9
|
-
"bin": {
|
|
10
|
-
"sketchmark": "./bin/sketchmark.cjs"
|
|
11
|
-
},
|
|
12
|
-
"exports": {
|
|
13
|
-
".": {
|
|
14
|
-
"types": "./dist/src/index.d.ts",
|
|
15
|
-
"require": "./dist/src/index.js",
|
|
16
|
-
"default": "./dist/src/index.js"
|
|
17
|
-
},
|
|
18
|
-
"./presets": {
|
|
19
|
-
"types": "./dist/src/presets/index.d.ts",
|
|
20
|
-
"require": "./dist/src/presets/index.js",
|
|
21
|
-
"default": "./dist/src/presets/index.js"
|
|
22
|
-
},
|
|
23
|
-
"./
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "sketchmark",
|
|
3
|
+
"version": "2.1.4",
|
|
4
|
+
"description": "Render kernel for Sketchmark visual documents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "./dist/src/index.js",
|
|
8
|
+
"types": "./dist/src/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"sketchmark": "./bin/sketchmark.cjs"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"require": "./dist/src/index.js",
|
|
16
|
+
"default": "./dist/src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./presets": {
|
|
19
|
+
"types": "./dist/src/presets/index.d.ts",
|
|
20
|
+
"require": "./dist/src/presets/index.js",
|
|
21
|
+
"default": "./dist/src/presets/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./browser-export": {
|
|
24
|
+
"types": "./dist/src/browser-export.d.ts",
|
|
25
|
+
"require": "./dist/src/browser-export.js",
|
|
26
|
+
"default": "./dist/src/browser-export.js"
|
|
27
|
+
},
|
|
28
|
+
"./editor": {
|
|
29
|
+
"types": "./bin/editor-ui.d.ts",
|
|
30
|
+
"require": "./bin/editor-ui.cjs",
|
|
31
|
+
"default": "./bin/editor-ui.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./schema": {
|
|
34
|
+
"default": "./schema/visual.schema.json"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "node scripts/build.cjs",
|
|
39
|
+
"render": "node bin/sketchmark.cjs render",
|
|
40
|
+
"test": "npm run build && node dist/tests/run.js",
|
|
41
|
+
"prepublishOnly": "npm test"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"bin",
|
|
45
|
+
"dist",
|
|
46
|
+
"schema",
|
|
47
|
+
"README.md"
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"mp4-muxer": "^5.2.2"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"typescript": "^5.9.3"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18"
|
|
57
|
+
},
|
|
58
|
+
"sideEffects": false
|
|
59
|
+
}
|