sketchmark 2.1.6 → 2.1.8
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 +903 -556
- package/bin/editor-ui.d.ts +8 -6
- package/bin/sketchmark.cjs +84 -28
- package/dist/src/authoring/compose.d.ts +4 -0
- package/dist/src/authoring/compose.js +49 -0
- package/dist/src/authoring/index.d.ts +7 -0
- package/dist/src/authoring/index.js +45 -0
- package/dist/src/authoring/layout.d.ts +43 -0
- package/dist/src/authoring/layout.js +89 -0
- package/dist/src/authoring/motion.d.ts +53 -0
- package/dist/src/authoring/motion.js +93 -0
- package/dist/src/authoring/pose.d.ts +20 -0
- package/dist/src/authoring/pose.js +73 -0
- package/dist/src/authoring/states.d.ts +17 -0
- package/dist/src/authoring/states.js +28 -0
- package/dist/src/authoring/types.d.ts +14 -0
- package/dist/src/authoring/types.js +2 -0
- package/dist/src/browser-editor.d.ts +15 -0
- package/dist/src/browser-editor.js +450 -0
- package/dist/src/edit.d.ts +33 -0
- package/dist/src/edit.js +216 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +5 -0
- package/dist/tests/run.js +52 -0
- package/package.json +68 -59
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.at = at;
|
|
4
|
+
exports.spec = spec;
|
|
5
|
+
exports.compile = compile;
|
|
6
|
+
const keyframes_1 = require("../keyframes");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
function at(time, set, options = {}) {
|
|
9
|
+
return {
|
|
10
|
+
time,
|
|
11
|
+
set: (0, utils_1.clone)(set),
|
|
12
|
+
...(options.curve ? { curve: (0, utils_1.clone)(options.curve) } : {}),
|
|
13
|
+
...(options.offsets ? { offsets: (0, utils_1.clone)(options.offsets) } : {})
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function spec(value, options = {}) {
|
|
17
|
+
return {
|
|
18
|
+
value: (0, utils_1.clone)(value),
|
|
19
|
+
...(options.curve ? { curve: (0, utils_1.clone)(options.curve) } : {}),
|
|
20
|
+
...(options.offset !== undefined ? { offset: options.offset } : {}),
|
|
21
|
+
...(options.in ? { in: (0, utils_1.clone)(options.in) } : {}),
|
|
22
|
+
...(options.out ? { out: (0, utils_1.clone)(options.out) } : {}),
|
|
23
|
+
...(options.interpolation ? { interpolation: (0, utils_1.clone)(options.interpolation) } : {})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function compile(document, steps, options = {}) {
|
|
27
|
+
return (0, keyframes_1.compileKeyframeStates)(document, steps, options);
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CompileKeyframeStateOptions, KeyframeState } from "../keyframes";
|
|
2
|
+
import type { VisualCanvas, VisualDocument } from "../types";
|
|
3
|
+
import type { ApplyPresetOptions, PresetFragment } from "../presets/types";
|
|
4
|
+
export type AuthoringFragment = PresetFragment;
|
|
5
|
+
export type AuthoringStep = PresetFragment | KeyframeState;
|
|
6
|
+
export type AuthoringInput = AuthoringStep | AuthoringStep[];
|
|
7
|
+
export interface CreateDocumentOptions extends ApplyPresetOptions {
|
|
8
|
+
}
|
|
9
|
+
export interface ApplyAuthoringOptions extends ApplyPresetOptions, CompileKeyframeStateOptions {
|
|
10
|
+
}
|
|
11
|
+
export interface CreatedVisualDocument extends VisualDocument {
|
|
12
|
+
version: 1;
|
|
13
|
+
canvas: VisualCanvas;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VisualDocument } from "./types";
|
|
2
|
+
export interface BrowserEditorOptions {
|
|
3
|
+
document: VisualDocument;
|
|
4
|
+
title?: string;
|
|
5
|
+
autoSaveDelay?: number | false;
|
|
6
|
+
onChange?: (document: VisualDocument) => void;
|
|
7
|
+
onSave?: (document: VisualDocument) => void | Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface BrowserEditorInstance {
|
|
10
|
+
getDocument(): VisualDocument;
|
|
11
|
+
setDocument(document: VisualDocument): void;
|
|
12
|
+
save(): Promise<void>;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createSketchmarkBrowserEditor(root: HTMLElement, options: BrowserEditorOptions): BrowserEditorInstance;
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSketchmarkBrowserEditor = createSketchmarkBrowserEditor;
|
|
4
|
+
const animatable_1 = require("./animatable");
|
|
5
|
+
const browser_export_1 = require("./browser-export");
|
|
6
|
+
const edit_1 = require("./edit");
|
|
7
|
+
const normalize_1 = require("./normalize");
|
|
8
|
+
const svg_1 = require("./render/svg");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
const validate_1 = require("./validate");
|
|
11
|
+
const STYLE_ID = "sketchmark-browser-editor-style";
|
|
12
|
+
function createSketchmarkBrowserEditor(root, options) {
|
|
13
|
+
ensureStyle();
|
|
14
|
+
const document = (0, utils_1.clone)(options.document);
|
|
15
|
+
assertValid(document);
|
|
16
|
+
const refs = (0, edit_1.listElementReferences)(document);
|
|
17
|
+
const state = {
|
|
18
|
+
root,
|
|
19
|
+
options,
|
|
20
|
+
document,
|
|
21
|
+
selectedId: refs[0]?.id ?? "",
|
|
22
|
+
currentTime: 0,
|
|
23
|
+
playing: false,
|
|
24
|
+
raf: 0,
|
|
25
|
+
saveTimer: 0,
|
|
26
|
+
lastFrame: 0
|
|
27
|
+
};
|
|
28
|
+
root.classList.add("sketchmark-browser-editor");
|
|
29
|
+
root.innerHTML = [
|
|
30
|
+
"<aside class='skme-tree'></aside>",
|
|
31
|
+
"<main class='skme-main'>",
|
|
32
|
+
" <div class='skme-toolbar'></div>",
|
|
33
|
+
" <div class='skme-stage-wrap'><div class='skme-stage'></div></div>",
|
|
34
|
+
" <div class='skme-playbar'></div>",
|
|
35
|
+
"</main>",
|
|
36
|
+
"<aside class='skme-inspector'></aside>"
|
|
37
|
+
].join("");
|
|
38
|
+
renderAll(state);
|
|
39
|
+
return {
|
|
40
|
+
getDocument: () => (0, utils_1.clone)(state.document),
|
|
41
|
+
setDocument: (next) => {
|
|
42
|
+
assertValid(next);
|
|
43
|
+
state.document = (0, utils_1.clone)(next);
|
|
44
|
+
if (state.selectedId && !(0, edit_1.findElementById)(state.document, state.selectedId))
|
|
45
|
+
state.selectedId = "";
|
|
46
|
+
renderAll(state);
|
|
47
|
+
},
|
|
48
|
+
save: () => saveNow(state),
|
|
49
|
+
destroy: () => {
|
|
50
|
+
window.clearTimeout(state.saveTimer);
|
|
51
|
+
window.cancelAnimationFrame(state.raf);
|
|
52
|
+
root.innerHTML = "";
|
|
53
|
+
root.classList.remove("sketchmark-browser-editor");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function renderAll(state) {
|
|
58
|
+
renderTree(state);
|
|
59
|
+
renderToolbar(state);
|
|
60
|
+
renderStage(state);
|
|
61
|
+
renderPlaybar(state);
|
|
62
|
+
renderInspector(state);
|
|
63
|
+
}
|
|
64
|
+
function renderTree(state) {
|
|
65
|
+
const tree = requirePart(state.root, ".skme-tree");
|
|
66
|
+
const refs = (0, edit_1.listElementReferences)(state.document);
|
|
67
|
+
tree.innerHTML = `<div class="skme-panel-title">Elements</div>${refs.map((ref) => {
|
|
68
|
+
const selected = ref.id === state.selectedId ? " selected" : "";
|
|
69
|
+
const pad = Math.max(0, ref.depth) * 12;
|
|
70
|
+
return `<button class="skme-tree-row${selected}" data-select="${escapeAttr(ref.id)}" style="padding-left:${pad + 6}px"><span>${escapeHtml(ref.id)}</span><small>${escapeHtml(ref.type)}</small></button>`;
|
|
71
|
+
}).join("") || "<p class='skme-muted'>No elements.</p>"}`;
|
|
72
|
+
tree.querySelectorAll("[data-select]").forEach((button) => {
|
|
73
|
+
button.onclick = () => {
|
|
74
|
+
state.selectedId = button.dataset.select ?? "";
|
|
75
|
+
renderTree(state);
|
|
76
|
+
renderStage(state);
|
|
77
|
+
renderInspector(state);
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function renderToolbar(state) {
|
|
82
|
+
const toolbar = requirePart(state.root, ".skme-toolbar");
|
|
83
|
+
toolbar.innerHTML = [
|
|
84
|
+
`<strong>${escapeHtml(state.options.title ?? "Sketchmark Editor")}</strong>`,
|
|
85
|
+
"<span class='skme-spacer'></span>",
|
|
86
|
+
"<button data-save>Save</button>",
|
|
87
|
+
"<button data-export='svg'>SVG</button>",
|
|
88
|
+
"<button data-export='png'>PNG</button>",
|
|
89
|
+
"<button data-export='jpg'>JPG</button>",
|
|
90
|
+
"<button data-export='html'>HTML</button>",
|
|
91
|
+
"<button data-export='json'>JSON</button>",
|
|
92
|
+
"<button data-export='mp4'>MP4</button>",
|
|
93
|
+
"<span class='skme-status' data-status></span>"
|
|
94
|
+
].join("");
|
|
95
|
+
toolbar.querySelector("[data-save]").onclick = () => saveNow(state);
|
|
96
|
+
toolbar.querySelectorAll("[data-export]").forEach((button) => {
|
|
97
|
+
button.onclick = () => exportDocument(state, button.dataset.export);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function renderStage(state) {
|
|
101
|
+
const stage = requirePart(state.root, ".skme-stage");
|
|
102
|
+
try {
|
|
103
|
+
const resolved = (0, normalize_1.resolveVisualFrame)(state.document, state.currentTime);
|
|
104
|
+
stage.innerHTML = (0, svg_1.renderResolvedSvg)(resolved);
|
|
105
|
+
const svg = stage.querySelector("svg");
|
|
106
|
+
if (!svg)
|
|
107
|
+
return;
|
|
108
|
+
svg.addEventListener("pointerdown", (event) => beginDrag(state, event));
|
|
109
|
+
svg.querySelectorAll("[id]").forEach((element) => {
|
|
110
|
+
element.style.cursor = "pointer";
|
|
111
|
+
element.addEventListener("click", (event) => {
|
|
112
|
+
event.stopPropagation();
|
|
113
|
+
const id = event.currentTarget.id;
|
|
114
|
+
if (id) {
|
|
115
|
+
state.selectedId = id;
|
|
116
|
+
renderTree(state);
|
|
117
|
+
renderStage(state);
|
|
118
|
+
renderInspector(state);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
if (state.selectedId) {
|
|
123
|
+
const selected = svg.querySelector(`#${cssEscape(state.selectedId)}`);
|
|
124
|
+
if (selected)
|
|
125
|
+
drawSelection(svg, selected);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
stage.innerHTML = `<pre class="skme-error">${escapeHtml(errorMessage(error))}</pre>`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function renderPlaybar(state) {
|
|
133
|
+
const playbar = requirePart(state.root, ".skme-playbar");
|
|
134
|
+
const duration = Math.max(0, Number(state.document.canvas.duration ?? 0));
|
|
135
|
+
playbar.innerHTML = [
|
|
136
|
+
`<button data-play>${state.playing ? "Pause" : "Play"}</button>`,
|
|
137
|
+
`<input data-time type="range" min="0" max="${duration || 1}" step="0.001" value="${state.currentTime}">`,
|
|
138
|
+
`<span>${state.currentTime.toFixed(2)}s / ${duration.toFixed(2)}s</span>`
|
|
139
|
+
].join("");
|
|
140
|
+
playbar.querySelector("[data-play]").onclick = () => togglePlayback(state);
|
|
141
|
+
const slider = playbar.querySelector("[data-time]");
|
|
142
|
+
slider.oninput = () => {
|
|
143
|
+
state.currentTime = Number(slider.value);
|
|
144
|
+
renderStage(state);
|
|
145
|
+
renderPlaybar(state);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function renderInspector(state) {
|
|
149
|
+
const inspector = requirePart(state.root, ".skme-inspector");
|
|
150
|
+
const element = state.selectedId ? (0, edit_1.findElementById)(state.document, state.selectedId) : undefined;
|
|
151
|
+
if (!element) {
|
|
152
|
+
inspector.innerHTML = "<div class='skme-panel-title'>Inspector</div><p class='skme-muted'>Select an element.</p>";
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const tracks = (0, edit_1.listTimelineTracks)(state.document, element.id ?? "");
|
|
156
|
+
inspector.innerHTML = [
|
|
157
|
+
"<div class='skme-panel-title'>Inspector</div>",
|
|
158
|
+
fieldText("ID", element.id ?? "", true),
|
|
159
|
+
fieldText("Type", element.type, true),
|
|
160
|
+
numberField("x", numberProp(element, "x")),
|
|
161
|
+
numberField("y", numberProp(element, "y")),
|
|
162
|
+
numberField("width", numberProp(element, "width")),
|
|
163
|
+
numberField("height", numberProp(element, "height")),
|
|
164
|
+
numberField("opacity", numberProp(element, "opacity")),
|
|
165
|
+
numberField("rotation", numberProp(element, "rotation")),
|
|
166
|
+
numberField("scale", numberProp(element, "scale")),
|
|
167
|
+
paintField("fill", element.fill),
|
|
168
|
+
paintField("stroke", element.stroke),
|
|
169
|
+
numberField("strokeWidth", numberProp(element, "strokeWidth")),
|
|
170
|
+
textAreaField("text", element.type === "text" ? element.text ?? "" : undefined),
|
|
171
|
+
"<hr>",
|
|
172
|
+
renderKeyframeEditor(element),
|
|
173
|
+
"<hr>",
|
|
174
|
+
"<div class='skme-panel-title small'>Timeline</div>",
|
|
175
|
+
tracks.length ? tracks.map((track) => `<div class='skme-track'><b>${escapeHtml(track.property)}</b><span>${track.keyframes.length} keyframes</span></div>`).join("") : "<p class='skme-muted'>No tracks.</p>"
|
|
176
|
+
].join("");
|
|
177
|
+
inspector.querySelectorAll("[data-prop]").forEach((input) => {
|
|
178
|
+
input.oninput = () => {
|
|
179
|
+
const property = input.dataset.prop;
|
|
180
|
+
const value = input.dataset.kind === "number" ? Number(input.value) : input.value;
|
|
181
|
+
if (input.dataset.kind === "number" && !Number.isFinite(value))
|
|
182
|
+
return;
|
|
183
|
+
updateProperty(state, element.id, property, value);
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
const propertySelect = inspector.querySelector("[data-key-prop]");
|
|
187
|
+
const keyButton = inspector.querySelector("[data-add-key]");
|
|
188
|
+
if (propertySelect && keyButton) {
|
|
189
|
+
keyButton.onclick = () => addKeyframe(state, element, propertySelect.value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function renderKeyframeEditor(element) {
|
|
193
|
+
const id = element.id;
|
|
194
|
+
if (!id)
|
|
195
|
+
return "";
|
|
196
|
+
const options = (0, animatable_1.animatablePropertiesForElement)(element)
|
|
197
|
+
.map((spec) => `<option value="${escapeAttr(spec.property)}">${escapeHtml(spec.property)}</option>`)
|
|
198
|
+
.join("");
|
|
199
|
+
return [
|
|
200
|
+
"<div class='skme-panel-title small'>Keyframe</div>",
|
|
201
|
+
"<div class='skme-row'>",
|
|
202
|
+
`<select data-key-prop>${options}</select>`,
|
|
203
|
+
"<button data-add-key>Set</button>",
|
|
204
|
+
"</div>"
|
|
205
|
+
].join("");
|
|
206
|
+
}
|
|
207
|
+
function updateProperty(state, id, property, value) {
|
|
208
|
+
try {
|
|
209
|
+
state.document = (0, edit_1.setElementProperty)(state.document, id, property, value);
|
|
210
|
+
markChanged(state);
|
|
211
|
+
renderStage(state);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
setStatus(state, errorMessage(error), true);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function addKeyframe(state, element, property) {
|
|
218
|
+
if (!element.id)
|
|
219
|
+
return;
|
|
220
|
+
const value = (0, animatable_1.getPropertyValue)(element, property) ?? (0, animatable_1.baseValueForProperty)(element, property);
|
|
221
|
+
if (value === undefined) {
|
|
222
|
+
setStatus(state, `No value for ${property}`, true);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
state.document = (0, edit_1.setTimelineKeyframe)(state.document, element.id, property, state.currentTime, value);
|
|
227
|
+
markChanged(state);
|
|
228
|
+
renderInspector(state);
|
|
229
|
+
renderStage(state);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
setStatus(state, errorMessage(error), true);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function beginDrag(state, event) {
|
|
236
|
+
const target = event.target;
|
|
237
|
+
const id = closestSvgId(target);
|
|
238
|
+
if (!id)
|
|
239
|
+
return;
|
|
240
|
+
const element = (0, edit_1.findElementById)(state.document, id);
|
|
241
|
+
if (!element || !("x" in element) || !("y" in element))
|
|
242
|
+
return;
|
|
243
|
+
const point = svgPoint(event);
|
|
244
|
+
if (!point)
|
|
245
|
+
return;
|
|
246
|
+
state.selectedId = id;
|
|
247
|
+
state.drag = { id, start: point, baseX: Number(element.x ?? 0), baseY: Number(element.y ?? 0) };
|
|
248
|
+
const svg = event.currentTarget;
|
|
249
|
+
svg.setPointerCapture(event.pointerId);
|
|
250
|
+
svg.addEventListener("pointermove", dragMove);
|
|
251
|
+
svg.addEventListener("pointerup", dragEnd);
|
|
252
|
+
svg.addEventListener("pointercancel", dragEnd);
|
|
253
|
+
renderTree(state);
|
|
254
|
+
renderInspector(state);
|
|
255
|
+
function dragMove(moveEvent) {
|
|
256
|
+
if (!state.drag)
|
|
257
|
+
return;
|
|
258
|
+
const nextPoint = svgPoint(moveEvent);
|
|
259
|
+
if (!nextPoint)
|
|
260
|
+
return;
|
|
261
|
+
const current = (0, edit_1.findElementById)(state.document, state.drag.id);
|
|
262
|
+
if (!current || !("x" in current) || !("y" in current))
|
|
263
|
+
return;
|
|
264
|
+
current.x = state.drag.baseX + nextPoint[0] - state.drag.start[0];
|
|
265
|
+
current.y = state.drag.baseY + nextPoint[1] - state.drag.start[1];
|
|
266
|
+
renderStage(state);
|
|
267
|
+
renderInspector(state);
|
|
268
|
+
}
|
|
269
|
+
function dragEnd(endEvent) {
|
|
270
|
+
svg.releasePointerCapture(endEvent.pointerId);
|
|
271
|
+
svg.removeEventListener("pointermove", dragMove);
|
|
272
|
+
svg.removeEventListener("pointerup", dragEnd);
|
|
273
|
+
svg.removeEventListener("pointercancel", dragEnd);
|
|
274
|
+
state.drag = undefined;
|
|
275
|
+
markChanged(state);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function togglePlayback(state) {
|
|
279
|
+
state.playing = !state.playing;
|
|
280
|
+
state.lastFrame = performance.now();
|
|
281
|
+
renderPlaybar(state);
|
|
282
|
+
if (state.playing)
|
|
283
|
+
tick(state);
|
|
284
|
+
}
|
|
285
|
+
function tick(state) {
|
|
286
|
+
if (!state.playing)
|
|
287
|
+
return;
|
|
288
|
+
const now = performance.now();
|
|
289
|
+
const delta = Math.max(0, (now - state.lastFrame) / 1000);
|
|
290
|
+
state.lastFrame = now;
|
|
291
|
+
const duration = Math.max(0, Number(state.document.canvas.duration ?? 0));
|
|
292
|
+
state.currentTime = duration > 0 ? (state.currentTime + delta) % duration : 0;
|
|
293
|
+
renderStage(state);
|
|
294
|
+
renderPlaybar(state);
|
|
295
|
+
state.raf = window.requestAnimationFrame(() => tick(state));
|
|
296
|
+
}
|
|
297
|
+
function markChanged(state) {
|
|
298
|
+
state.options.onChange?.((0, utils_1.clone)(state.document));
|
|
299
|
+
setStatus(state, "Changed");
|
|
300
|
+
if (state.options.autoSaveDelay === false || !state.options.onSave)
|
|
301
|
+
return;
|
|
302
|
+
window.clearTimeout(state.saveTimer);
|
|
303
|
+
state.saveTimer = window.setTimeout(() => {
|
|
304
|
+
saveNow(state).catch((error) => setStatus(state, errorMessage(error), true));
|
|
305
|
+
}, state.options.autoSaveDelay ?? 800);
|
|
306
|
+
}
|
|
307
|
+
async function saveNow(state) {
|
|
308
|
+
if (!state.options.onSave) {
|
|
309
|
+
setStatus(state, "No save handler", true);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
await state.options.onSave((0, utils_1.clone)(state.document));
|
|
313
|
+
setStatus(state, "Saved");
|
|
314
|
+
}
|
|
315
|
+
async function exportDocument(state, format) {
|
|
316
|
+
try {
|
|
317
|
+
setStatus(state, format === "mp4" ? "Exporting MP4..." : "Exporting...");
|
|
318
|
+
await (0, browser_export_1.exportVisualInBrowser)(state.document, {
|
|
319
|
+
format,
|
|
320
|
+
title: state.options.title ?? "sketchmark",
|
|
321
|
+
time: state.currentTime,
|
|
322
|
+
sourceDocument: state.document,
|
|
323
|
+
onProgress: (progress) => setStatus(state, `Export ${progress}%`)
|
|
324
|
+
});
|
|
325
|
+
setStatus(state, "Exported");
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
setStatus(state, errorMessage(error), true);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function assertValid(document) {
|
|
332
|
+
const result = (0, validate_1.validateVisualDocument)(document);
|
|
333
|
+
if (!result.ok)
|
|
334
|
+
throw new Error(result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n"));
|
|
335
|
+
}
|
|
336
|
+
function fieldText(label, value, readonly) {
|
|
337
|
+
return `<label class="skme-field"><span>${label}</span><input value="${escapeAttr(value)}"${readonly ? " readonly" : ""}></label>`;
|
|
338
|
+
}
|
|
339
|
+
function numberField(property, value) {
|
|
340
|
+
if (value === undefined)
|
|
341
|
+
return "";
|
|
342
|
+
return `<label class="skme-field"><span>${property}</span><input data-prop="${escapeAttr(property)}" data-kind="number" type="number" step="0.1" value="${Number(value)}"></label>`;
|
|
343
|
+
}
|
|
344
|
+
function paintField(property, value) {
|
|
345
|
+
if (typeof value !== "string")
|
|
346
|
+
return "";
|
|
347
|
+
return `<label class="skme-field"><span>${property}</span><input data-prop="${escapeAttr(property)}" value="${escapeAttr(value)}"></label>`;
|
|
348
|
+
}
|
|
349
|
+
function textAreaField(property, value) {
|
|
350
|
+
if (value === undefined)
|
|
351
|
+
return "";
|
|
352
|
+
return `<label class="skme-field"><span>${property}</span><textarea data-prop="${escapeAttr(property)}">${escapeHtml(value)}</textarea></label>`;
|
|
353
|
+
}
|
|
354
|
+
function numberProp(element, property) {
|
|
355
|
+
const value = element[property];
|
|
356
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
357
|
+
}
|
|
358
|
+
function closestSvgId(target) {
|
|
359
|
+
let current = target;
|
|
360
|
+
while (current && current instanceof SVGElement) {
|
|
361
|
+
if (current.id)
|
|
362
|
+
return current.id;
|
|
363
|
+
current = current.parentElement;
|
|
364
|
+
}
|
|
365
|
+
return "";
|
|
366
|
+
}
|
|
367
|
+
function svgPoint(event) {
|
|
368
|
+
const svg = event.currentTarget ?? event.target.ownerSVGElement;
|
|
369
|
+
if (!svg)
|
|
370
|
+
return undefined;
|
|
371
|
+
const point = svg.createSVGPoint();
|
|
372
|
+
point.x = event.clientX;
|
|
373
|
+
point.y = event.clientY;
|
|
374
|
+
const matrix = svg.getScreenCTM();
|
|
375
|
+
if (!matrix)
|
|
376
|
+
return undefined;
|
|
377
|
+
const mapped = point.matrixTransform(matrix.inverse());
|
|
378
|
+
return [mapped.x, mapped.y];
|
|
379
|
+
}
|
|
380
|
+
function drawSelection(svg, selected) {
|
|
381
|
+
const box = selected.getBBox();
|
|
382
|
+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
383
|
+
rect.setAttribute("x", String(box.x));
|
|
384
|
+
rect.setAttribute("y", String(box.y));
|
|
385
|
+
rect.setAttribute("width", String(box.width));
|
|
386
|
+
rect.setAttribute("height", String(box.height));
|
|
387
|
+
rect.setAttribute("fill", "none");
|
|
388
|
+
rect.setAttribute("stroke", "#0b63ff");
|
|
389
|
+
rect.setAttribute("stroke-width", "1.5");
|
|
390
|
+
rect.setAttribute("stroke-dasharray", "5 4");
|
|
391
|
+
rect.setAttribute("pointer-events", "none");
|
|
392
|
+
svg.appendChild(rect);
|
|
393
|
+
}
|
|
394
|
+
function setStatus(state, message, isError = false) {
|
|
395
|
+
const status = state.root.querySelector("[data-status]");
|
|
396
|
+
if (!status)
|
|
397
|
+
return;
|
|
398
|
+
status.textContent = message;
|
|
399
|
+
status.classList.toggle("error", isError);
|
|
400
|
+
}
|
|
401
|
+
function requirePart(root, selector) {
|
|
402
|
+
const part = root.querySelector(selector);
|
|
403
|
+
if (!part)
|
|
404
|
+
throw new Error(`Missing editor part ${selector}.`);
|
|
405
|
+
return part;
|
|
406
|
+
}
|
|
407
|
+
function errorMessage(error) {
|
|
408
|
+
return error instanceof Error ? error.message : String(error);
|
|
409
|
+
}
|
|
410
|
+
function escapeHtml(value) {
|
|
411
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
412
|
+
}
|
|
413
|
+
function escapeAttr(value) {
|
|
414
|
+
return escapeHtml(value);
|
|
415
|
+
}
|
|
416
|
+
function cssEscape(value) {
|
|
417
|
+
const css = globalThis.CSS;
|
|
418
|
+
if (css?.escape)
|
|
419
|
+
return css.escape(value);
|
|
420
|
+
return value.replace(/[^A-Za-z0-9_-]/g, (char) => `\\${char}`);
|
|
421
|
+
}
|
|
422
|
+
function ensureStyle() {
|
|
423
|
+
if (document.getElementById(STYLE_ID))
|
|
424
|
+
return;
|
|
425
|
+
const style = document.createElement("style");
|
|
426
|
+
style.id = STYLE_ID;
|
|
427
|
+
style.textContent = `
|
|
428
|
+
.sketchmark-browser-editor{height:100%;min-height:100vh;display:grid;grid-template-columns:240px 1fr 320px;background:#c0c0c0;color:#111;font:13px Arial,sans-serif;overflow:hidden}
|
|
429
|
+
.skme-tree,.skme-inspector{overflow:auto;background:#f2f2f2;border-right:1px solid #888;padding:8px}
|
|
430
|
+
.skme-inspector{border-right:0;border-left:1px solid #888}
|
|
431
|
+
.skme-main{min-width:0;display:grid;grid-template-rows:auto 1fr auto}
|
|
432
|
+
.skme-toolbar,.skme-playbar{display:flex;align-items:center;gap:6px;padding:6px;background:#e5e5e5;border-bottom:1px solid #999}
|
|
433
|
+
.skme-playbar{border-top:1px solid #999;border-bottom:0}
|
|
434
|
+
.skme-playbar input{flex:1}
|
|
435
|
+
.skme-stage-wrap{min-width:0;min-height:0;display:grid;place-items:center;overflow:auto;background:#fff}
|
|
436
|
+
.skme-stage svg{display:block;max-width:100%;max-height:calc(100vh - 88px);background:#fff;border:1px solid #333;overflow:visible}
|
|
437
|
+
.skme-panel-title{font-weight:bold;margin:0 0 8px}.skme-panel-title.small{font-size:12px;margin-top:8px}
|
|
438
|
+
.skme-tree-row{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;border:1px solid transparent;background:transparent;text-align:left;padding:3px 6px;cursor:pointer}
|
|
439
|
+
.skme-tree-row small{color:#555}.skme-tree-row.selected{background:#003399;color:#fff}.skme-tree-row.selected small{color:#dbeafe}
|
|
440
|
+
.skme-field{display:grid;grid-template-columns:88px 1fr;gap:6px;align-items:center;margin:5px 0}
|
|
441
|
+
.skme-field input,.skme-field textarea,.skme-field select{width:100%;box-sizing:border-box;font:13px Arial,sans-serif}
|
|
442
|
+
.skme-field textarea{min-height:88px}
|
|
443
|
+
.skme-row{display:grid;grid-template-columns:1fr auto;gap:6px}
|
|
444
|
+
.skme-track{display:flex;justify-content:space-between;border:1px solid #aaa;background:#e9e9e9;margin:4px 0;padding:4px}
|
|
445
|
+
.skme-spacer{flex:1}.skme-muted{color:#555}.skme-error,.skme-status.error{color:#900}.skme-status{min-width:90px;color:#333}
|
|
446
|
+
button{font:13px Arial,sans-serif;padding:3px 8px}
|
|
447
|
+
hr{border:0;border-top:1px solid #bbb;margin:10px 0}
|
|
448
|
+
`;
|
|
449
|
+
document.head.appendChild(style);
|
|
450
|
+
}
|
package/dist/src/edit.d.ts
CHANGED
|
@@ -12,8 +12,41 @@ export interface SetKeyframeOptions {
|
|
|
12
12
|
curve?: TimelineCurve;
|
|
13
13
|
ease?: string;
|
|
14
14
|
}
|
|
15
|
+
export type ElementPresetKind = "text" | "rectangle" | "circle" | "line" | "path" | "point" | "group";
|
|
16
|
+
export interface InsertElementPresetOptions {
|
|
17
|
+
id?: string;
|
|
18
|
+
parentId?: string | null;
|
|
19
|
+
index?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface InsertElementPresetResult {
|
|
22
|
+
document: VisualDocument;
|
|
23
|
+
element: VisualElement;
|
|
24
|
+
parentId?: string;
|
|
25
|
+
}
|
|
26
|
+
export type ReorderElementDirection = "backward" | "forward" | "back" | "front";
|
|
27
|
+
export interface ReorderElementOptions {
|
|
28
|
+
direction?: ReorderElementDirection | string;
|
|
29
|
+
toIndex?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ReorderElementResult {
|
|
32
|
+
document: VisualDocument;
|
|
33
|
+
id: string;
|
|
34
|
+
index: number;
|
|
35
|
+
previousIndex: number;
|
|
36
|
+
parentId?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface DeleteElementResult {
|
|
39
|
+
document: VisualDocument;
|
|
40
|
+
element: VisualElement;
|
|
41
|
+
id: string;
|
|
42
|
+
index: number;
|
|
43
|
+
parentId?: string;
|
|
44
|
+
}
|
|
15
45
|
export declare function listElementReferences(document: VisualDocument): ElementReference[];
|
|
16
46
|
export declare function findElementById(document: VisualDocument, id: string): VisualElement | undefined;
|
|
47
|
+
export declare function insertElementPreset(document: VisualDocument, preset: ElementPresetKind | string, options?: InsertElementPresetOptions): InsertElementPresetResult;
|
|
48
|
+
export declare function reorderElement(document: VisualDocument, id: string, options?: ReorderElementOptions): ReorderElementResult;
|
|
49
|
+
export declare function deleteElement(document: VisualDocument, id: string): DeleteElementResult;
|
|
17
50
|
export declare function setElementProperty(document: VisualDocument, id: string, property: string, value: MotionValue): VisualDocument;
|
|
18
51
|
export declare function setTimelineKeyframe(document: VisualDocument, id: string, property: string, time: number, value: MotionValue, options?: SetKeyframeOptions): VisualDocument;
|
|
19
52
|
export declare function removeTimelineKeyframe(document: VisualDocument, id: string, property: string, time: number): VisualDocument;
|