sketchmark 2.1.7 → 2.1.9
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 +62 -83
- package/bin/editor-ui.cjs +404 -329
- package/bin/editor-ui.d.ts +0 -1
- package/bin/sketchmark.cjs +84 -28
- package/dist/src/browser-export.d.ts +1 -1
- package/dist/src/browser-export.js +6 -0
- package/dist/src/edit.d.ts +33 -0
- package/dist/src/edit.js +216 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +6 -0
- package/dist/src/mp4-muxer-source.d.ts +1 -0
- package/dist/src/mp4-muxer-source.js +5 -0
- package/dist/src/render/embed.d.ts +11 -0
- package/dist/src/render/embed.js +688 -0
- package/dist/tests/run.js +82 -0
- package/package.json +6 -1
package/bin/editor-ui.cjs
CHANGED
|
@@ -8,33 +8,28 @@ function editorHtml(title, options = {}) {
|
|
|
8
8
|
const mp4MuxerUrl = options.mp4MuxerUrl || "";
|
|
9
9
|
const mp4MuxerSource = mp4MuxerUrl ? "" : resolveMp4MuxerSource(options.mp4MuxerSource);
|
|
10
10
|
const serverExportFallback = options.serverExportFallback !== false;
|
|
11
|
-
const canvasStageRender = options.canvasStageRender === true;
|
|
12
11
|
const localDocumentControls = options.localDocumentControls === true;
|
|
13
12
|
const bootstrapScript = typeof options.bootstrapScript === "string" ? options.bootstrapScript : "";
|
|
14
13
|
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sketchmark Editor - ${escapeHtml(title)}</title><style>
|
|
15
14
|
@font-face{font-family:'Roboto';src:url('/fonts/Roboto-Light.ttf') format('truetype');font-weight:300;font-style:normal;font-display:swap}
|
|
16
15
|
@font-face{font-family:'Roboto';src:url('/fonts/Roboto-Regular.ttf') format('truetype');font-weight:400;font-style:normal;font-display:swap}
|
|
17
16
|
@font-face{font-family:'Roboto';src:url('/fonts/Roboto-Bold.ttf') format('truetype');font-weight:700;font-style:normal;font-display:swap}
|
|
18
|
-
html,body{margin:0;width:100%;height:100%;font:13px Roboto,Arial,sans-serif;background:#c0c0c0;color:#000;overflow:hidden}
|
|
19
|
-
body{display:grid;grid-template-columns:
|
|
17
|
+
html,body{margin:0;width:100%;height:100%;font:13px Roboto,Arial,sans-serif;background:#c0c0c0;color:#000;overflow:hidden}
|
|
18
|
+
body{--tree-width:240px;display:grid;grid-template-columns:var(--tree-width) 6px minmax(0,1fr) 300px;grid-template-rows:1fr 165px;min-width:900px;overflow:hidden}
|
|
19
|
+
body.resizingSidebar{cursor:col-resize;user-select:none}
|
|
20
20
|
button,input,select,textarea{font:13px Roboto,Arial,sans-serif}
|
|
21
21
|
button{padding:3px 8px}
|
|
22
22
|
input,select,textarea{box-sizing:border-box;width:100%}
|
|
23
23
|
textarea{min-height:88px;padding:4px;resize:vertical}
|
|
24
|
-
#tree,#inspector,#timeline{background:#f3f4f6;border:1px solid #c6ccd6;overflow:auto;padding:6px;scrollbar-width:none;-ms-overflow-style:none}
|
|
25
|
-
#tree{grid-row:1/3}
|
|
26
|
-
#
|
|
27
|
-
#
|
|
24
|
+
#tree,#inspector,#timeline{background:#f3f4f6;border:1px solid #c6ccd6;overflow:auto;padding:6px;scrollbar-width:none;-ms-overflow-style:none}
|
|
25
|
+
#tree{grid-column:1;grid-row:1/3;display:flex;flex-direction:column;min-height:0;overflow:hidden}
|
|
26
|
+
#treeResizeHandle{grid-column:2;grid-row:1/3;background:#d1d5db;border-left:1px solid #aab2bf;border-right:1px solid #aab2bf;cursor:col-resize;z-index:5}
|
|
27
|
+
#treeResizeHandle:hover,body.resizingSidebar #treeResizeHandle{background:#9ca3af}
|
|
28
|
+
#stageWrap{position:relative;display:grid;place-items:center;min-width:0;min-height:0;padding:0;background:#fff;overflow:hidden;cursor:default}
|
|
29
|
+
#stageWrap{grid-column:3;grid-row:1}
|
|
30
|
+
#stageWrap.panning{cursor:grabbing}
|
|
28
31
|
#stage{display:grid;place-items:center;min-width:0;min-height:0;position:relative}
|
|
29
32
|
#stage svg{max-width:100%;max-height:calc(100vh - 190px);background:white;border:1px solid #333;overflow:visible}
|
|
30
|
-
${canvasStageRender ? `#stage.sketchmarkCanvasStage{display:block;position:relative;line-height:0;background:white;border:1px solid #333;box-sizing:content-box}
|
|
31
|
-
#stageCanvas{display:block;background:white}
|
|
32
|
-
#stage.sketchmarkCanvasStage>svg{position:absolute;left:0;top:0;width:100%;height:100%;max-width:none;max-height:none;background:transparent;border:0;box-sizing:border-box;z-index:2;pointer-events:auto}
|
|
33
|
-
#stage.sketchmarkCanvasStage>svg:not(#stageLiveSvg)>*:not(defs):not(#__sketchmark_handles):not(#__sketchmark_drag_preview){opacity:0}
|
|
34
|
-
#stage.sketchmarkCanvasStage>svg:not(#stageLiveSvg) #__sketchmark_handles,#stage.sketchmarkCanvasStage>svg:not(#stageLiveSvg) #__sketchmark_drag_preview{opacity:1}
|
|
35
|
-
#stage.sketchmarkCanvasStage>#stageLiveSvg{display:none;position:absolute;left:0;top:0;width:100%;height:100%;max-width:none;max-height:none;background:transparent;border:0;box-sizing:border-box;z-index:1;pointer-events:none;overflow:visible}
|
|
36
|
-
#stage.sketchmarkCanvasStage.liveSvgDrag>#stageCanvas{visibility:hidden}
|
|
37
|
-
#stage.sketchmarkCanvasStage.liveSvgDrag>#stageLiveSvg{display:block}` : ""}
|
|
38
33
|
${localDocumentControls ? `.browserFileGrid{display:grid;gap:6px}
|
|
39
34
|
.browserFileActions{display:grid;grid-template-columns:1fr 1fr;gap:4px}
|
|
40
35
|
.browserFileInput{font-size:12px}
|
|
@@ -42,25 +37,46 @@ ${localDocumentControls ? `.browserFileGrid{display:grid;gap:6px}
|
|
|
42
37
|
#viewportHud{position:absolute;right:12px;bottom:12px;display:flex;flex-direction:column;gap:6px;align-items:stretch;padding:6px;background:rgba(238,238,238,.95);border:1px solid #8f96a3;z-index:3}
|
|
43
38
|
#viewportHud button{padding:1px 8px;min-width:42px}
|
|
44
39
|
#zoomLabel{min-width:42px;text-align:center;font-weight:bold;color:#111827}
|
|
45
|
-
#
|
|
40
|
+
#inspector{grid-column:4;grid-row:1}
|
|
41
|
+
#timeline{grid-column:3/5;grid-row:2}
|
|
46
42
|
.row{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin:4px 0}
|
|
47
43
|
.row label{display:block}
|
|
48
44
|
.colorField{display:grid;grid-template-columns:34px 1fr;gap:6px;align-items:center}
|
|
49
45
|
.colorField input[type=color]{width:34px;height:28px;padding:0;border:1px solid #8f96a3;background:#fff;cursor:pointer}
|
|
50
46
|
.stack{display:grid;gap:5px}.section{margin:0 0 10px}.label{display:block;font-weight:bold;margin:0 0 3px}
|
|
51
|
-
.panelGroup{margin:0 0 8px;border:1px solid #c9cfdb;background:#f8fafc}
|
|
52
|
-
.panelGroup > summary{list-style:none;cursor:pointer;padding:6px 8px;font-weight:bold;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
53
|
-
.panelGroup > summary::-webkit-details-marker{display:none}
|
|
54
|
-
.panelGroup > summary::after{content:"+";font-weight:bold;color:#4b5563}
|
|
55
|
-
.panelGroup[open] > summary::after{content:"-"}
|
|
56
|
-
.panelGroup > summary .summaryMeta{font-size:11px;color:#4b5563;font-weight:normal;margin-left:auto}
|
|
57
|
-
.panelBody{padding:6px 8px;border-top:1px solid #d8dee8}
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
.
|
|
47
|
+
.panelGroup{margin:0 0 8px;border:1px solid #c9cfdb;background:#f8fafc}
|
|
48
|
+
.panelGroup > summary{list-style:none;cursor:pointer;padding:6px 8px;font-weight:bold;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
49
|
+
.panelGroup > summary::-webkit-details-marker{display:none}
|
|
50
|
+
.panelGroup > summary::after{content:"+";font-weight:bold;color:#4b5563}
|
|
51
|
+
.panelGroup[open] > summary::after{content:"-"}
|
|
52
|
+
.panelGroup > summary .summaryMeta{font-size:11px;color:#4b5563;font-weight:normal;margin-left:auto}
|
|
53
|
+
.panelBody{padding:6px 8px;border-top:1px solid #d8dee8}
|
|
54
|
+
#tree > .panelGroup{flex:0 0 auto}
|
|
55
|
+
#tree > .panelGroup[data-panel='tree-elements']{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;margin-bottom:0}
|
|
56
|
+
#tree > .panelGroup[data-panel='tree-elements']:not([open]){flex:0 0 auto}
|
|
57
|
+
#tree > .panelGroup[data-panel='tree-elements'] > summary{flex:0 0 auto}
|
|
58
|
+
#tree > .panelGroup[data-panel='tree-elements'] > .panelBody{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;overflow:hidden}
|
|
59
|
+
.elementsFixed{flex:0 0 auto}
|
|
60
|
+
.elementsTreeList{flex:1 1 auto;min-height:0;overflow:auto;padding-right:2px;scrollbar-width:none;-ms-overflow-style:none}
|
|
61
|
+
.subhead{display:block;font-size:11px;font-weight:bold;color:#374151;margin:6px 0 2px}
|
|
62
|
+
.treeRow{display:grid;grid-template-columns:18px 20px 20px 20px 1fr;gap:3px;align-items:center;margin:1px 0}
|
|
63
|
+
.treePad{display:block;width:18px;height:20px}
|
|
64
|
+
.treeCtl{height:20px;padding:0;border:1px solid #9aa1ad;background:#f8fafc;cursor:pointer;line-height:18px;font-size:11px}
|
|
65
|
+
.treeCtl.active{background:#003399;color:#fff;border-color:#003399}
|
|
66
|
+
.treeCtl:disabled{opacity:.35;cursor:default}
|
|
67
|
+
.treeFold{position:relative;border-color:transparent;background:transparent}
|
|
68
|
+
.treeFold:hover{background:#e5e7eb;border-color:#cbd5e1}
|
|
69
|
+
.treeFold::before{content:"";position:absolute;left:6px;top:5px;border-style:solid;border-width:5px 0 5px 7px;border-color:transparent transparent transparent #374151}
|
|
70
|
+
.treeFold.expanded::before{left:4px;top:7px;border-width:7px 5px 0 5px;border-color:#374151 transparent transparent transparent}
|
|
71
|
+
.layerBar{display:grid;gap:4px;margin:0 0 6px;padding:5px;border:1px solid #cbd5e1;background:#fff}
|
|
72
|
+
.layerMeta{font-size:11px;color:#374151;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
73
|
+
.layerActions{display:grid;grid-template-columns:repeat(5,1fr);gap:4px}
|
|
74
|
+
.layerActions button{padding:2px 0;min-width:0}
|
|
75
|
+
.treeDanger{color:#9f1239}
|
|
76
|
+
.treeDanger:hover:not(:disabled){background:#fee2e2;border-color:#f43f5e}
|
|
77
|
+
.insertBar{display:grid;grid-template-columns:1fr 34px;gap:4px;margin-bottom:6px}
|
|
78
|
+
.treeEmpty{font-size:11px;color:#555;padding:4px 6px}
|
|
79
|
+
.canvasError{color:#900;font-size:11px;min-height:14px}
|
|
64
80
|
.treeBtn{display:block;width:100%;text-align:left;margin:0;border:1px solid transparent;background:#f8fafc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:2px 6px}
|
|
65
81
|
.treeBtn.dim{opacity:0.6}
|
|
66
82
|
.treeBtn.selected{background:#003399;color:white}.muted{color:#555}.track{border:1px solid #888;background:#ddd;padding:5px;margin:4px 0}
|
|
@@ -87,14 +103,15 @@ ${localDocumentControls ? `.browserFileGrid{display:grid;gap:6px}
|
|
|
87
103
|
.curveModal{width:min(760px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;border:2px outset #ddd;background:#ececec;padding:8px;scrollbar-width:none;-ms-overflow-style:none}
|
|
88
104
|
.curveModalBar{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}
|
|
89
105
|
.curveModalContent .curvePanel{margin-top:0}
|
|
90
|
-
#tree::-webkit-scrollbar,#inspector::-webkit-scrollbar,#timeline::-webkit-scrollbar,.curveModal::-webkit-scrollbar{width:0;height:0}
|
|
106
|
+
#tree::-webkit-scrollbar,#elementsTree::-webkit-scrollbar,#inspector::-webkit-scrollbar,#timeline::-webkit-scrollbar,.curveModal::-webkit-scrollbar{width:0;height:0}
|
|
91
107
|
#error{color:#900;min-height:18px;margin-top:6px}.tiny{font-size:11px;color:#444}.toolbar{display:grid;grid-template-columns:auto 1fr auto auto;gap:6px;align-items:center}
|
|
92
108
|
.exportButtons{display:grid;grid-template-columns:1fr 1fr;gap:4px}
|
|
93
109
|
.exportButtons button{width:100%;text-align:left;padding:4px 6px}
|
|
94
110
|
.exportButtons button.exportWide{grid-column:1/3}
|
|
95
|
-
</style></head><body><aside id="tree"></aside><main id="stageWrap"><div id="stage"></div><div id="viewportHud"><button id="zoomOut" type="button" title="Zoom out">-</button><button id="zoomIn" type="button" title="Zoom in">+</button><button id="zoomFit" type="button" title="Reset zoom and pan">Fit</button><span id="zoomLabel">100%</span></div></main><aside id="inspector"></aside><section id="timeline"></section><div id="curveModalBackdrop" class="modalBackdrop hidden"><div id="curveModal" class="curveModal" role="dialog" aria-modal="true" aria-label="Interpolation Graph"><div class="curveModalBar"><strong>Interpolation Graph</strong><button id="curveModalClose" type="button">Close</button></div><div id="curveModalContent" class="curveModalContent"></div></div></div>${bootstrapScript ? `<script>\n${bootstrapScript}\n</script>` : ""}<script>
|
|
96
|
-
const tree = document.getElementById("tree");
|
|
97
|
-
const
|
|
111
|
+
</style></head><body><aside id="tree"></aside><div id="treeResizeHandle" title="Resize elements panel"></div><main id="stageWrap"><div id="stage"></div><div id="viewportHud"><button id="zoomOut" type="button" title="Zoom out">-</button><button id="zoomIn" type="button" title="Zoom in">+</button><button id="zoomFit" type="button" title="Reset zoom and pan">Fit</button><span id="zoomLabel">100%</span></div></main><aside id="inspector"></aside><section id="timeline"></section><div id="curveModalBackdrop" class="modalBackdrop hidden"><div id="curveModal" class="curveModal" role="dialog" aria-modal="true" aria-label="Interpolation Graph"><div class="curveModalBar"><strong>Interpolation Graph</strong><button id="curveModalClose" type="button">Close</button></div><div id="curveModalContent" class="curveModalContent"></div></div></div>${bootstrapScript ? `<script>\n${bootstrapScript}\n</script>` : ""}<script>
|
|
112
|
+
const tree = document.getElementById("tree");
|
|
113
|
+
const treeResizeHandle = document.getElementById("treeResizeHandle");
|
|
114
|
+
const stageWrap = document.getElementById("stageWrap");
|
|
98
115
|
const stage = document.getElementById("stage");
|
|
99
116
|
const zoomOut = document.getElementById("zoomOut");
|
|
100
117
|
const zoomIn = document.getElementById("zoomIn");
|
|
@@ -113,32 +130,44 @@ let currentTime = 0;
|
|
|
113
130
|
let playing = false;
|
|
114
131
|
let lastTick = 0;
|
|
115
132
|
let playHandle = 0;
|
|
116
|
-
let resolvedDoc = null;
|
|
117
|
-
let drawScheduled = false;
|
|
118
|
-
let drawInFlight = false;
|
|
119
|
-
let drawQueued = false;
|
|
120
|
-
let drag = null;
|
|
121
|
-
let suppressClick = false;
|
|
133
|
+
let resolvedDoc = null;
|
|
134
|
+
let drawScheduled = false;
|
|
135
|
+
let drawInFlight = false;
|
|
136
|
+
let drawQueued = false;
|
|
137
|
+
let drag = null;
|
|
138
|
+
let suppressClick = false;
|
|
122
139
|
const FONT_FAMILY_OPTIONS = [
|
|
123
140
|
{ label: "Roboto (Local)", value: "Roboto, Arial, sans-serif" }
|
|
124
141
|
];
|
|
125
|
-
const FONT_WEIGHT_OPTIONS = [
|
|
126
|
-
{ label: "300", value: "300" },
|
|
127
|
-
{ label: "400", value: "400" },
|
|
128
|
-
{ label: "500", value: "500" },
|
|
129
|
-
{ label: "600", value: "600" },
|
|
130
|
-
{ label: "700", value: "700" }
|
|
131
|
-
];
|
|
132
|
-
|
|
142
|
+
const FONT_WEIGHT_OPTIONS = [
|
|
143
|
+
{ label: "300", value: "300" },
|
|
144
|
+
{ label: "400", value: "400" },
|
|
145
|
+
{ label: "500", value: "500" },
|
|
146
|
+
{ label: "600", value: "600" },
|
|
147
|
+
{ label: "700", value: "700" }
|
|
148
|
+
];
|
|
149
|
+
const ELEMENT_PRESET_OPTIONS = [
|
|
150
|
+
{ label: "Text", value: "text" },
|
|
151
|
+
{ label: "Rectangle", value: "rectangle" },
|
|
152
|
+
{ label: "Circle", value: "circle" },
|
|
153
|
+
{ label: "Line", value: "line" },
|
|
154
|
+
{ label: "Path", value: "path" },
|
|
155
|
+
{ label: "Point", value: "point" },
|
|
156
|
+
{ label: "Group", value: "group" }
|
|
157
|
+
];
|
|
158
|
+
let collapsedGroups = new Set();
|
|
133
159
|
let hiddenIds = new Set();
|
|
134
|
-
let lockedIds = new Set();
|
|
135
|
-
let parentById = Object.create(null);
|
|
136
|
-
let childIdsById = Object.create(null);
|
|
137
|
-
let
|
|
138
|
-
let
|
|
139
|
-
let
|
|
140
|
-
let
|
|
141
|
-
let
|
|
160
|
+
let lockedIds = new Set();
|
|
161
|
+
let parentById = Object.create(null);
|
|
162
|
+
let childIdsById = Object.create(null);
|
|
163
|
+
let layerIndexById = Object.create(null);
|
|
164
|
+
let layerCountById = Object.create(null);
|
|
165
|
+
let typeById = Object.create(null);
|
|
166
|
+
let sidebarCommitTimers = Object.create(null);
|
|
167
|
+
let selectedSegment = null;
|
|
168
|
+
let panelOpenState = Object.create(null);
|
|
169
|
+
let insertPresetKind = "rectangle";
|
|
170
|
+
let viewport = { initialized: false, baseWidth: 1, baseHeight: 1, x: 0, y: 0, width: 1, height: 1 };
|
|
142
171
|
let viewportPan = null;
|
|
143
172
|
let spacePanActive = false;
|
|
144
173
|
const API_BASE = ${scriptJson(apiBase)};
|
|
@@ -146,171 +175,13 @@ const EDITOR_TITLE = ${scriptJson(title || "sketchmark")};
|
|
|
146
175
|
const MP4_MUXER_URL = ${scriptJson(mp4MuxerUrl)};
|
|
147
176
|
const MP4_MUXER_SOURCE = ${scriptJson(mp4MuxerSource)};
|
|
148
177
|
const SERVER_EXPORT_FALLBACK = ${scriptJson(serverExportFallback)};
|
|
149
|
-
const CANVAS_STAGE_RENDER = ${scriptJson(canvasStageRender)};
|
|
150
178
|
const LOCAL_DOCUMENT_CONTROLS = ${scriptJson(localDocumentControls)};
|
|
179
|
+
const TREE_WIDTH_KEY = "sketchmark.editor.treeWidth";
|
|
180
|
+
const DEFAULT_TREE_WIDTH = 240;
|
|
181
|
+
const MIN_TREE_WIDTH = 180;
|
|
182
|
+
const MAX_TREE_WIDTH = 620;
|
|
151
183
|
let mp4MuxerObjectUrl = "";
|
|
152
|
-
|
|
153
|
-
let canvasStageRenderToken = 0;
|
|
154
|
-
let canvasStageRenderScheduled = false;
|
|
155
|
-
let pendingCanvasStageCanvas = null;
|
|
156
|
-
let liveDragPreviewPendingClear = false;
|
|
157
|
-
|
|
158
|
-
function requestVisibleCanvasStageRender(canvas) {
|
|
159
|
-
if (!CANVAS_STAGE_RENDER || !canvas) return;
|
|
160
|
-
pendingCanvasStageCanvas = canvas;
|
|
161
|
-
syncCanvasStageLayout(canvas);
|
|
162
|
-
if (canvasStageRenderScheduled) return;
|
|
163
|
-
canvasStageRenderScheduled = true;
|
|
164
|
-
requestAnimationFrame(() => {
|
|
165
|
-
canvasStageRenderScheduled = false;
|
|
166
|
-
renderVisibleCanvasStage(pendingCanvasStageCanvas);
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function setStageOverlaySvg(svgMarkup) {
|
|
171
|
-
const template = document.createElement("template");
|
|
172
|
-
template.innerHTML = String(svgMarkup || "").trim();
|
|
173
|
-
const nextSvg = template.content.querySelector("svg");
|
|
174
|
-
if (!nextSvg) {
|
|
175
|
-
stage.innerHTML = String(svgMarkup || "");
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const previousSvg = stage.querySelector("svg:not(#stageLiveSvg)");
|
|
179
|
-
if (previousSvg) {
|
|
180
|
-
previousSvg.replaceWith(nextSvg);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const stageCanvas = document.getElementById("stageCanvas");
|
|
184
|
-
if (stageCanvas && stageCanvas.parentElement === stage) stageCanvas.after(nextSvg);
|
|
185
|
-
else stage.appendChild(nextSvg);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function syncCanvasStageLayout(canvas) {
|
|
189
|
-
const svg = currentSvg();
|
|
190
|
-
if (!CANVAS_STAGE_RENDER || !svg || !canvas) return null;
|
|
191
|
-
const size = canvasSize(canvas);
|
|
192
|
-
let stageCanvas = document.getElementById("stageCanvas");
|
|
193
|
-
if (!stageCanvas || stageCanvas.parentElement !== stage) {
|
|
194
|
-
stageCanvas = document.createElement("canvas");
|
|
195
|
-
stageCanvas.id = "stageCanvas";
|
|
196
|
-
stage.insertBefore(stageCanvas, svg);
|
|
197
|
-
} else if (stageCanvas.nextSibling !== svg) {
|
|
198
|
-
stage.insertBefore(stageCanvas, svg);
|
|
199
|
-
}
|
|
200
|
-
stage.classList.add("sketchmarkCanvasStage");
|
|
201
|
-
const display = canvasStageDisplaySize(size);
|
|
202
|
-
stage.style.width = display.width + "px";
|
|
203
|
-
stage.style.height = display.height + "px";
|
|
204
|
-
stageCanvas.style.width = display.width + "px";
|
|
205
|
-
stageCanvas.style.height = display.height + "px";
|
|
206
|
-
svg.style.width = display.width + "px";
|
|
207
|
-
svg.style.height = display.height + "px";
|
|
208
|
-
svg.style.maxWidth = "none";
|
|
209
|
-
svg.style.maxHeight = "none";
|
|
210
|
-
const liveSvg = document.getElementById("stageLiveSvg");
|
|
211
|
-
if (liveSvg && liveSvg.parentElement === stage) {
|
|
212
|
-
liveSvg.style.width = display.width + "px";
|
|
213
|
-
liveSvg.style.height = display.height + "px";
|
|
214
|
-
liveSvg.style.maxWidth = "none";
|
|
215
|
-
liveSvg.style.maxHeight = "none";
|
|
216
|
-
}
|
|
217
|
-
const pixelRatio = Math.max(1, Math.min(3, Number(window.devicePixelRatio || 1)));
|
|
218
|
-
const pixelWidth = Math.max(1, Math.round(display.width * pixelRatio));
|
|
219
|
-
const pixelHeight = Math.max(1, Math.round(display.height * pixelRatio));
|
|
220
|
-
return { svg, stageCanvas, display, pixelRatio, pixelWidth, pixelHeight };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function renderVisibleCanvasStage(canvas) {
|
|
224
|
-
const layout = syncCanvasStageLayout(canvas);
|
|
225
|
-
if (!layout) return;
|
|
226
|
-
const { svg, stageCanvas, display, pixelRatio, pixelWidth, pixelHeight } = layout;
|
|
227
|
-
const context = stageCanvas.getContext("2d");
|
|
228
|
-
if (!context) return;
|
|
229
|
-
const serialized = serializeStageSvgForCanvas(svg);
|
|
230
|
-
const token = ++canvasStageRenderToken;
|
|
231
|
-
const image = new Image();
|
|
232
|
-
const url = URL.createObjectURL(new Blob([serialized], { type: "image/svg+xml;charset=utf-8" }));
|
|
233
|
-
image.onload = () => {
|
|
234
|
-
URL.revokeObjectURL(url);
|
|
235
|
-
if (token !== canvasStageRenderToken || !stage.contains(stageCanvas)) return;
|
|
236
|
-
if (stageCanvas.width !== pixelWidth) stageCanvas.width = pixelWidth;
|
|
237
|
-
if (stageCanvas.height !== pixelHeight) stageCanvas.height = pixelHeight;
|
|
238
|
-
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
239
|
-
context.clearRect(0, 0, display.width, display.height);
|
|
240
|
-
context.drawImage(image, 0, 0, display.width, display.height);
|
|
241
|
-
if (liveDragPreviewPendingClear && !drag) clearLiveDragPreview();
|
|
242
|
-
};
|
|
243
|
-
image.onerror = () => {
|
|
244
|
-
URL.revokeObjectURL(url);
|
|
245
|
-
};
|
|
246
|
-
image.src = url;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function canvasStageDisplaySize(size) {
|
|
250
|
-
const availableWidth = Math.max(1, Number(stageWrap && stageWrap.clientWidth || size.width) - 2);
|
|
251
|
-
const availableHeight = Math.max(1, Number(stageWrap && stageWrap.clientHeight || size.height) - 2);
|
|
252
|
-
const scale = Math.min(1, availableWidth / size.width, availableHeight / size.height);
|
|
253
|
-
return {
|
|
254
|
-
width: Math.max(1, Math.round(size.width * scale)),
|
|
255
|
-
height: Math.max(1, Math.round(size.height * scale))
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function serializeStageSvgForCanvas(svg) {
|
|
260
|
-
const clone = svg.cloneNode(true);
|
|
261
|
-
const handles = clone.querySelector("#__sketchmark_handles");
|
|
262
|
-
if (handles) handles.remove();
|
|
263
|
-
const preview = clone.querySelector("#__sketchmark_drag_preview");
|
|
264
|
-
if (preview) preview.remove();
|
|
265
|
-
if (drag && drag.id) {
|
|
266
|
-
const dragged = clone.querySelector("#" + cssId(drag.id));
|
|
267
|
-
if (dragged) dragged.remove();
|
|
268
|
-
}
|
|
269
|
-
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
270
|
-
return new XMLSerializer().serializeToString(clone);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function showLiveDragPreview(target) {
|
|
274
|
-
if (!CANVAS_STAGE_RENDER || !target) return;
|
|
275
|
-
const svg = target.ownerSVGElement;
|
|
276
|
-
if (!svg) return;
|
|
277
|
-
syncLiveDragSvg(svg);
|
|
278
|
-
stage.classList.add("liveSvgDrag");
|
|
279
|
-
liveDragPreviewPendingClear = false;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function syncLiveDragSvg(svg) {
|
|
283
|
-
const clone = svg.cloneNode(true);
|
|
284
|
-
const handles = clone.querySelector("#__sketchmark_handles");
|
|
285
|
-
if (handles) handles.remove();
|
|
286
|
-
const preview = clone.querySelector("#__sketchmark_drag_preview");
|
|
287
|
-
if (preview) preview.remove();
|
|
288
|
-
clone.setAttribute("id", "stageLiveSvg");
|
|
289
|
-
clone.setAttribute("aria-hidden", "true");
|
|
290
|
-
let liveSvg = document.getElementById("stageLiveSvg");
|
|
291
|
-
if (liveSvg) liveSvg.replaceWith(clone);
|
|
292
|
-
else svg.after(clone);
|
|
293
|
-
clone.style.width = svg.style.width;
|
|
294
|
-
clone.style.height = svg.style.height;
|
|
295
|
-
clone.style.maxWidth = "none";
|
|
296
|
-
clone.style.maxHeight = "none";
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function releaseLiveDragPreview(waitForDraw) {
|
|
300
|
-
if (!CANVAS_STAGE_RENDER || !stage.classList.contains("liveSvgDrag")) return;
|
|
301
|
-
liveDragPreviewPendingClear = true;
|
|
302
|
-
if (waitForDraw) requestDraw();
|
|
303
|
-
else requestVisibleCanvasStageRender(doc && doc.canvas);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function clearLiveDragPreview() {
|
|
307
|
-
const preview = stage.querySelector("#__sketchmark_drag_preview");
|
|
308
|
-
if (preview) preview.remove();
|
|
309
|
-
const liveSvg = document.getElementById("stageLiveSvg");
|
|
310
|
-
if (liveSvg) liveSvg.remove();
|
|
311
|
-
stage.classList.remove("liveSvgDrag");
|
|
312
|
-
liveDragPreviewPendingClear = false;
|
|
313
|
-
}
|
|
184
|
+
let sidebarResize = null;
|
|
314
185
|
|
|
315
186
|
function browserStoragePanel() {
|
|
316
187
|
if (!LOCAL_DOCUMENT_CONTROLS) return "";
|
|
@@ -377,9 +248,66 @@ function showBrowserStorageStatus() {
|
|
|
377
248
|
if (box && api && api.storageStatus) box.textContent = api.storageStatus();
|
|
378
249
|
}
|
|
379
250
|
|
|
251
|
+
function initTreeResize() {
|
|
252
|
+
setTreeWidth(loadTreeWidth(), false);
|
|
253
|
+
if (!treeResizeHandle) return;
|
|
254
|
+
treeResizeHandle.addEventListener("pointerdown", beginTreeResize);
|
|
255
|
+
treeResizeHandle.addEventListener("dblclick", () => setTreeWidth(DEFAULT_TREE_WIDTH, true));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function beginTreeResize(event) {
|
|
259
|
+
sidebarResize = {
|
|
260
|
+
pointerId: event.pointerId,
|
|
261
|
+
startX: event.clientX,
|
|
262
|
+
startWidth: tree.getBoundingClientRect().width
|
|
263
|
+
};
|
|
264
|
+
document.body.classList.add("resizingSidebar");
|
|
265
|
+
treeResizeHandle.setPointerCapture?.(event.pointerId);
|
|
266
|
+
event.preventDefault();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function updateTreeResize(event) {
|
|
270
|
+
if (!sidebarResize) return;
|
|
271
|
+
setTreeWidth(sidebarResize.startWidth + event.clientX - sidebarResize.startX, false);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function endTreeResize() {
|
|
275
|
+
if (!sidebarResize) return;
|
|
276
|
+
sidebarResize = null;
|
|
277
|
+
document.body.classList.remove("resizingSidebar");
|
|
278
|
+
setTreeWidth(tree.getBoundingClientRect().width, true);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function loadTreeWidth() {
|
|
282
|
+
try {
|
|
283
|
+
const stored = Number(window.localStorage.getItem(TREE_WIDTH_KEY));
|
|
284
|
+
return Number.isFinite(stored) && stored > 0 ? stored : DEFAULT_TREE_WIDTH;
|
|
285
|
+
} catch {
|
|
286
|
+
return DEFAULT_TREE_WIDTH;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function setTreeWidth(width, persist) {
|
|
291
|
+
const next = clampTreeWidth(width);
|
|
292
|
+
document.body.style.setProperty("--tree-width", next + "px");
|
|
293
|
+
if (persist) {
|
|
294
|
+
try {
|
|
295
|
+
window.localStorage.setItem(TREE_WIDTH_KEY, String(Math.round(next)));
|
|
296
|
+
} catch {}
|
|
297
|
+
}
|
|
298
|
+
resizeElementsTree();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function clampTreeWidth(width) {
|
|
302
|
+
const value = Number(width);
|
|
303
|
+
const availableMax = Math.max(MIN_TREE_WIDTH, window.innerWidth - 520);
|
|
304
|
+
const max = Math.min(MAX_TREE_WIDTH, availableMax);
|
|
305
|
+
return Math.max(MIN_TREE_WIDTH, Math.min(max, Number.isFinite(value) ? value : DEFAULT_TREE_WIDTH));
|
|
306
|
+
}
|
|
307
|
+
|
|
380
308
|
function apiPath(path) {
|
|
381
|
-
return API_BASE + path;
|
|
382
|
-
}
|
|
309
|
+
return API_BASE + path;
|
|
310
|
+
}
|
|
383
311
|
|
|
384
312
|
curveModalClose.onclick = closeCurveModal;
|
|
385
313
|
curveModalBackdrop.onclick = (event) => {
|
|
@@ -410,18 +338,17 @@ async function load() {
|
|
|
410
338
|
requestDraw();
|
|
411
339
|
}
|
|
412
340
|
|
|
413
|
-
async function draw() {
|
|
414
|
-
if (drawInFlight) {
|
|
415
|
-
drawQueued = true;
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
drawInFlight = true;
|
|
419
|
-
const time = currentTime;
|
|
420
|
-
try {
|
|
421
|
-
const data = await api(apiPath("/frame") + "?time=" + encodeURIComponent(time));
|
|
422
|
-
resolvedDoc = data.resolved || null;
|
|
423
|
-
|
|
424
|
-
else stage.innerHTML = data.svg;
|
|
341
|
+
async function draw() {
|
|
342
|
+
if (drawInFlight) {
|
|
343
|
+
drawQueued = true;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
drawInFlight = true;
|
|
347
|
+
const time = currentTime;
|
|
348
|
+
try {
|
|
349
|
+
const data = await api(apiPath("/frame") + "?time=" + encodeURIComponent(time));
|
|
350
|
+
resolvedDoc = data.resolved || null;
|
|
351
|
+
stage.innerHTML = data.svg;
|
|
425
352
|
const svg = currentSvg();
|
|
426
353
|
if (svg) {
|
|
427
354
|
svg.style.overflow = "visible";
|
|
@@ -446,18 +373,18 @@ async function draw() {
|
|
|
446
373
|
}
|
|
447
374
|
}
|
|
448
375
|
|
|
449
|
-
function requestDraw() {
|
|
450
|
-
if (drawInFlight) {
|
|
451
|
-
drawQueued = true;
|
|
452
|
-
return;
|
|
376
|
+
function requestDraw() {
|
|
377
|
+
if (drawInFlight) {
|
|
378
|
+
drawQueued = true;
|
|
379
|
+
return;
|
|
453
380
|
}
|
|
454
381
|
if (drawScheduled) return;
|
|
455
382
|
drawScheduled = true;
|
|
456
|
-
requestAnimationFrame(() => {
|
|
457
|
-
drawScheduled = false;
|
|
458
|
-
draw().catch(showError);
|
|
459
|
-
});
|
|
460
|
-
}
|
|
383
|
+
requestAnimationFrame(() => {
|
|
384
|
+
drawScheduled = false;
|
|
385
|
+
draw().catch(showError);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
461
388
|
|
|
462
389
|
function canvasSize(canvas) {
|
|
463
390
|
const width = Math.max(1, Number(canvas && canvas.width || 1));
|
|
@@ -482,13 +409,12 @@ function ensureViewportState(canvas, forceReset) {
|
|
|
482
409
|
clampViewport();
|
|
483
410
|
}
|
|
484
411
|
|
|
485
|
-
function applyViewportToSvg(svg, canvas) {
|
|
486
|
-
if (!svg) return;
|
|
487
|
-
ensureViewportState(canvas, false);
|
|
412
|
+
function applyViewportToSvg(svg, canvas) {
|
|
413
|
+
if (!svg) return;
|
|
414
|
+
ensureViewportState(canvas, false);
|
|
488
415
|
svg.setAttribute("viewBox", viewport.x.toFixed(3) + " " + viewport.y.toFixed(3) + " " + viewport.width.toFixed(3) + " " + viewport.height.toFixed(3));
|
|
489
416
|
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
490
417
|
updateZoomLabel();
|
|
491
|
-
requestVisibleCanvasStageRender(canvas);
|
|
492
418
|
}
|
|
493
419
|
|
|
494
420
|
function updateZoomLabel() {
|
|
@@ -529,7 +455,7 @@ function zoomFactorFromWheel(event) {
|
|
|
529
455
|
}
|
|
530
456
|
|
|
531
457
|
function currentSvg() {
|
|
532
|
-
return stage.querySelector("svg
|
|
458
|
+
return stage.querySelector("svg");
|
|
533
459
|
}
|
|
534
460
|
|
|
535
461
|
function svgPointFromClient(svg, clientX, clientY) {
|
|
@@ -662,21 +588,118 @@ function panelDetails(panelId, title, body, options) {
|
|
|
662
588
|
"</details>";
|
|
663
589
|
}
|
|
664
590
|
|
|
665
|
-
function bindPanelStates(scope) {
|
|
666
|
-
const root = scope || document;
|
|
667
|
-
const panels = root.querySelectorAll("details[data-panel]");
|
|
668
|
-
for (const panel of panels) {
|
|
669
|
-
const panelId = panel.getAttribute("data-panel");
|
|
670
|
-
if (!panelId) continue;
|
|
671
|
-
panelOpenState[panelId] = panel.open;
|
|
672
|
-
panel.ontoggle = () => {
|
|
673
|
-
panelOpenState[panelId] = panel.open;
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
591
|
+
function bindPanelStates(scope) {
|
|
592
|
+
const root = scope || document;
|
|
593
|
+
const panels = root.querySelectorAll("details[data-panel]");
|
|
594
|
+
for (const panel of panels) {
|
|
595
|
+
const panelId = panel.getAttribute("data-panel");
|
|
596
|
+
if (!panelId) continue;
|
|
597
|
+
panelOpenState[panelId] = panel.open;
|
|
598
|
+
panel.ontoggle = () => {
|
|
599
|
+
panelOpenState[panelId] = panel.open;
|
|
600
|
+
resizeElementsTree();
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function elementInsertPanel() {
|
|
606
|
+
return "<div class='insertBar'><select id='insertPreset'>" + ELEMENT_PRESET_OPTIONS.map((item) =>
|
|
607
|
+
"<option value='" + escapeAttr(item.value) + "'" + (item.value === insertPresetKind ? " selected" : "") + ">" + escapeText(item.label) + "</option>"
|
|
608
|
+
).join("") + "</select><button id='insertTopElement' type='button' title='Add element'>+</button></div>" + selectedLayerPanel();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function bindElementInsertControls() {
|
|
612
|
+
const preset = document.getElementById("insertPreset");
|
|
613
|
+
if (preset) {
|
|
614
|
+
preset.onchange = () => {
|
|
615
|
+
insertPresetKind = String(preset.value || "rectangle");
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const add = document.getElementById("insertTopElement");
|
|
619
|
+
if (add) add.onclick = () => insertElement("");
|
|
620
|
+
bindLayerButton("layerBack", "back");
|
|
621
|
+
bindLayerButton("layerBackward", "backward");
|
|
622
|
+
bindLayerButton("layerForward", "forward");
|
|
623
|
+
bindLayerButton("layerFront", "front");
|
|
624
|
+
const remove = document.getElementById("layerDelete");
|
|
625
|
+
if (remove) remove.onclick = () => {
|
|
626
|
+
if (selectedId) deleteTreeElement(selectedId);
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function insertElement(parentId) {
|
|
631
|
+
try {
|
|
632
|
+
const data = await api(apiPath("/element"), {
|
|
633
|
+
method: "POST",
|
|
634
|
+
headers: { "content-type": "application/json" },
|
|
635
|
+
body: JSON.stringify({ preset: insertPresetKind, parentId: parentId || null })
|
|
636
|
+
});
|
|
637
|
+
doc = data.document;
|
|
638
|
+
refs = data.elements;
|
|
639
|
+
rebuildElementIndex();
|
|
640
|
+
if (parentId) collapsedGroups.delete(parentId);
|
|
641
|
+
selectedId = typeof data.insertedId === "string" ? data.insertedId : selectedId;
|
|
642
|
+
renderTree();
|
|
643
|
+
renderInspector();
|
|
644
|
+
renderTimeline();
|
|
645
|
+
requestDraw();
|
|
646
|
+
} catch (error) {
|
|
647
|
+
showError(error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function selectedLayerPanel() {
|
|
652
|
+
const id = selectedId && findElement(selectedId) ? selectedId : "";
|
|
653
|
+
const index = id ? Number(layerIndexById[id]) : -1;
|
|
654
|
+
const count = id ? Number(layerCountById[id]) : 0;
|
|
655
|
+
const locked = id ? isElementLocked(id) : true;
|
|
656
|
+
const canMove = Boolean(id) && !locked && Number.isFinite(index) && Number.isFinite(count) && count > 1;
|
|
657
|
+
const label = id ? "Layer: " + id : "Layer";
|
|
658
|
+
return "<div class='layerBar'>" +
|
|
659
|
+
"<div class='layerMeta' title='" + escapeAttr(label) + "'>" + escapeText(label) + "</div>" +
|
|
660
|
+
"<div class='layerActions'>" +
|
|
661
|
+
layerActionButton("layerBack", "<<", "Send to back", !canMove || index <= 0) +
|
|
662
|
+
layerActionButton("layerBackward", "<", "Send backward", !canMove || index <= 0) +
|
|
663
|
+
layerActionButton("layerForward", ">", "Bring forward", !canMove || index >= count - 1) +
|
|
664
|
+
layerActionButton("layerFront", ">>", "Bring to front", !canMove || index >= count - 1) +
|
|
665
|
+
layerActionButton("layerDelete", "X", "Delete element/group", !id || locked, " treeDanger") +
|
|
666
|
+
"</div></div>";
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function layerActionButton(id, label, title, disabled, className) {
|
|
670
|
+
return "<button id='" + escapeAttr(id) + "' class='treeCtl" + (className || "") + "' type='button' title='" + escapeAttr(title) + "'" + (disabled ? " disabled" : "") + ">" + escapeText(label) + "</button>";
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function bindLayerButton(id, direction) {
|
|
674
|
+
const button = document.getElementById(id);
|
|
675
|
+
if (button) button.onclick = () => {
|
|
676
|
+
if (selectedId) reorderLayer(selectedId, direction);
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function reorderLayer(id, direction) {
|
|
681
|
+
try {
|
|
682
|
+
if (!ensureElementEditable(id)) return;
|
|
683
|
+
await mutate(apiPath("/reorder"), { id, direction }, { refreshTimeline: false });
|
|
684
|
+
} catch (error) {
|
|
685
|
+
showError(error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function deleteTreeElement(id) {
|
|
690
|
+
try {
|
|
691
|
+
if (!ensureElementEditable(id)) return;
|
|
692
|
+
const isGroup = typeById[id] === "group";
|
|
693
|
+
const message = "Delete '" + id + "'" + (isGroup ? " and its children" : "") + "?";
|
|
694
|
+
if (!window.confirm(message)) return;
|
|
695
|
+
await mutate(apiPath("/delete-element"), { id });
|
|
696
|
+
} catch (error) {
|
|
697
|
+
showError(error);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function renderTree() {
|
|
702
|
+
const canvas = doc && doc.canvas ? doc.canvas : {};
|
|
680
703
|
const canvasSummary = Math.round(valueOr(canvas.width, 1)) + "x" + Math.round(valueOr(canvas.height, 1));
|
|
681
704
|
const canvasBody =
|
|
682
705
|
"<div class='row'><label>Width<input id='canvasWidth' type='number' step='1' min='1' value='" + escapeAttr(valueOr(canvas.width, 1)) + "'></label><label>Height<input id='canvasHeight' type='number' step='1' min='1' value='" + escapeAttr(valueOr(canvas.height, 1)) + "'></label></div>" +
|
|
@@ -686,27 +709,35 @@ function renderTree() {
|
|
|
686
709
|
tree.innerHTML =
|
|
687
710
|
browserStoragePanel() +
|
|
688
711
|
panelDetails("tree-canvas", "Canvas", canvasBody, { defaultOpen: false, meta: canvasSummary }) +
|
|
689
|
-
panelDetails("tree-elements", "Elements", "<div id='elementsTree'></div>", { defaultOpen: false, meta: refs.length + " items" });
|
|
712
|
+
panelDetails("tree-elements", "Elements", "<div class='elementsFixed'>" + elementInsertPanel() + "</div><div id='elementsTree' class='elementsTreeList'></div>", { defaultOpen: false, meta: refs.length + " items" });
|
|
690
713
|
bindPanelStates(tree);
|
|
691
714
|
bindCanvasInputs();
|
|
692
715
|
bindBrowserStoragePanel();
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const
|
|
716
|
+
bindElementInsertControls();
|
|
717
|
+
const treeRoot = document.getElementById("elementsTree");
|
|
718
|
+
if (!treeRoot) return;
|
|
719
|
+
if (!refs.length) {
|
|
720
|
+
const empty = document.createElement("div");
|
|
721
|
+
empty.className = "treeEmpty";
|
|
722
|
+
empty.textContent = "No elements yet.";
|
|
723
|
+
treeRoot.appendChild(empty);
|
|
724
|
+
}
|
|
725
|
+
for (const ref of refs) {
|
|
726
|
+
if (isInCollapsedBranch(ref.id)) continue;
|
|
727
|
+
const row = document.createElement("div");
|
|
698
728
|
row.className = "treeRow";
|
|
699
729
|
row.style.paddingLeft = 8 + ref.depth * 14 + "px";
|
|
700
|
-
const hasChildren = hasTreeChildren(ref.id) && typeById[ref.id] === "group";
|
|
701
|
-
if (hasChildren) {
|
|
702
|
-
const
|
|
703
|
-
fold
|
|
704
|
-
fold.
|
|
705
|
-
fold.title =
|
|
706
|
-
fold.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
730
|
+
const hasChildren = hasTreeChildren(ref.id) && typeById[ref.id] === "group";
|
|
731
|
+
if (hasChildren) {
|
|
732
|
+
const isCollapsed = collapsedGroups.has(ref.id);
|
|
733
|
+
const fold = document.createElement("button");
|
|
734
|
+
fold.className = "treeCtl treeFold" + (isCollapsed ? " collapsed" : " expanded");
|
|
735
|
+
fold.title = isCollapsed ? "Expand group" : "Collapse group";
|
|
736
|
+
fold.setAttribute("aria-label", fold.title);
|
|
737
|
+
fold.onclick = (event) => {
|
|
738
|
+
event.stopPropagation();
|
|
739
|
+
toggleCollapse(ref.id);
|
|
740
|
+
};
|
|
710
741
|
row.appendChild(fold);
|
|
711
742
|
} else {
|
|
712
743
|
const pad = document.createElement("span");
|
|
@@ -729,16 +760,48 @@ function renderTree() {
|
|
|
729
760
|
lock.onclick = (event) => {
|
|
730
761
|
event.stopPropagation();
|
|
731
762
|
toggleLocked(ref.id);
|
|
732
|
-
};
|
|
733
|
-
row.appendChild(lock);
|
|
734
|
-
|
|
735
|
-
|
|
763
|
+
};
|
|
764
|
+
row.appendChild(lock);
|
|
765
|
+
if (ref.type === "group") {
|
|
766
|
+
const add = document.createElement("button");
|
|
767
|
+
add.className = "treeCtl";
|
|
768
|
+
add.textContent = "+";
|
|
769
|
+
add.title = "Add element to group";
|
|
770
|
+
add.onclick = (event) => {
|
|
771
|
+
event.stopPropagation();
|
|
772
|
+
insertElement(ref.id);
|
|
773
|
+
};
|
|
774
|
+
row.appendChild(add);
|
|
775
|
+
} else {
|
|
776
|
+
const pad = document.createElement("span");
|
|
777
|
+
pad.className = "treePad";
|
|
778
|
+
row.appendChild(pad);
|
|
779
|
+
}
|
|
780
|
+
const button = document.createElement("button");
|
|
781
|
+
button.className = "treeBtn" + (ref.id === selectedId ? " selected" : "") + (isElementHidden(ref.id) ? " dim" : "");
|
|
736
782
|
button.textContent = ref.id + " " + ref.type;
|
|
737
783
|
button.onclick = () => select(ref.id);
|
|
738
|
-
row.appendChild(button);
|
|
739
|
-
treeRoot.appendChild(row);
|
|
740
|
-
}
|
|
741
|
-
|
|
784
|
+
row.appendChild(button);
|
|
785
|
+
treeRoot.appendChild(row);
|
|
786
|
+
}
|
|
787
|
+
resizeElementsTree();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function resizeElementsTree() {
|
|
791
|
+
const treeRoot = document.getElementById("elementsTree");
|
|
792
|
+
const panel = document.querySelector("[data-panel='tree-elements']");
|
|
793
|
+
if (!treeRoot || !panel || !panel.open) return;
|
|
794
|
+
const body = panel.querySelector(".panelBody");
|
|
795
|
+
const fixed = panel.querySelector(".elementsFixed");
|
|
796
|
+
if (!body || !fixed) return;
|
|
797
|
+
const treeRect = tree.getBoundingClientRect();
|
|
798
|
+
const bodyRect = body.getBoundingClientRect();
|
|
799
|
+
const fixedRect = fixed.getBoundingClientRect();
|
|
800
|
+
const styles = window.getComputedStyle(body);
|
|
801
|
+
const paddingBottom = Number.parseFloat(styles.paddingBottom || "0") || 0;
|
|
802
|
+
const available = treeRect.bottom - bodyRect.top - fixedRect.height - paddingBottom - 6;
|
|
803
|
+
treeRoot.style.maxHeight = Math.max(72, Math.floor(available)) + "px";
|
|
804
|
+
}
|
|
742
805
|
|
|
743
806
|
function bindCanvasInputs() {
|
|
744
807
|
const bind = (id, callback) => {
|
|
@@ -802,22 +865,28 @@ function showCanvasError(message) {
|
|
|
802
865
|
if (box) box.textContent = message || "";
|
|
803
866
|
}
|
|
804
867
|
|
|
805
|
-
function rebuildElementIndex() {
|
|
806
|
-
parentById = Object.create(null);
|
|
807
|
-
childIdsById = Object.create(null);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
868
|
+
function rebuildElementIndex() {
|
|
869
|
+
parentById = Object.create(null);
|
|
870
|
+
childIdsById = Object.create(null);
|
|
871
|
+
layerIndexById = Object.create(null);
|
|
872
|
+
layerCountById = Object.create(null);
|
|
873
|
+
typeById = Object.create(null);
|
|
874
|
+
const visit = (elements, parentId) => {
|
|
875
|
+
const list = elements || [];
|
|
876
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
877
|
+
const element = list[index];
|
|
878
|
+
const id = element && element.id;
|
|
879
|
+
const nextParent = id || parentId;
|
|
880
|
+
if (id) {
|
|
881
|
+
typeById[id] = element.type;
|
|
882
|
+
parentById[id] = parentId || "";
|
|
883
|
+
layerIndexById[id] = index;
|
|
884
|
+
layerCountById[id] = list.length;
|
|
885
|
+
childIdsById[id] = childIdsById[id] || [];
|
|
886
|
+
if (parentId) {
|
|
887
|
+
childIdsById[parentId] = childIdsById[parentId] || [];
|
|
888
|
+
childIdsById[parentId].push(id);
|
|
889
|
+
}
|
|
821
890
|
}
|
|
822
891
|
if (element && element.type === "group") visit(element.children || [], nextParent);
|
|
823
892
|
}
|
|
@@ -1324,12 +1393,13 @@ function renderTimeline() {
|
|
|
1324
1393
|
const element = findElement(selectedId);
|
|
1325
1394
|
const tracks = element && element.timeline && element.timeline.tracks ? element.timeline.tracks : {};
|
|
1326
1395
|
timeline.innerHTML = "<div class='toolbar'><button id='play'>" + (playing ? "Pause" : "Play") + "</button><input id='scrub' type='range' min='0' max='" + Math.max(Number(doc.canvas.duration || 0), 0.01) + "' step='0.005' value='" + currentTime + "'><strong id='timeLabel'>" + currentTime.toFixed(2) + "s</strong><button id='refresh'>Refresh</button></div>";
|
|
1327
|
-
document.getElementById("play").onclick = togglePlay;
|
|
1328
|
-
document.getElementById("refresh").onclick = load;
|
|
1329
|
-
document.getElementById("scrub")
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1396
|
+
document.getElementById("play").onclick = togglePlay;
|
|
1397
|
+
document.getElementById("refresh").onclick = load;
|
|
1398
|
+
const scrub = document.getElementById("scrub");
|
|
1399
|
+
scrub.oninput = (event) => {
|
|
1400
|
+
setCurrentTime(event.target.value);
|
|
1401
|
+
};
|
|
1402
|
+
const properties = Object.keys(tracks);
|
|
1333
1403
|
if (!properties.length) {
|
|
1334
1404
|
selectedSegment = null;
|
|
1335
1405
|
closeCurveModal();
|
|
@@ -1974,18 +2044,18 @@ async function applySegmentCurve(property, segmentIndex, curve) {
|
|
|
1974
2044
|
);
|
|
1975
2045
|
}
|
|
1976
2046
|
|
|
1977
|
-
function setCurrentTime(time) {
|
|
1978
|
-
const duration = Math.max(Number(doc && doc.canvas && doc.canvas.duration || 0), 0.01);
|
|
1979
|
-
const next = Math.max(0, Math.min(Number(time || 0), duration));
|
|
1980
|
-
currentTime = next;
|
|
2047
|
+
function setCurrentTime(time) {
|
|
2048
|
+
const duration = Math.max(Number(doc && doc.canvas && doc.canvas.duration || 0), 0.01);
|
|
2049
|
+
const next = Math.max(0, Math.min(Number(time || 0), duration));
|
|
2050
|
+
currentTime = next;
|
|
1981
2051
|
const scrub = document.getElementById("scrub");
|
|
1982
2052
|
if (scrub) scrub.value = String(next);
|
|
1983
2053
|
const label = document.getElementById("timeLabel");
|
|
1984
2054
|
if (label) label.textContent = next.toFixed(2) + "s";
|
|
1985
|
-
const kfTime = document.getElementById("kfTime");
|
|
1986
|
-
if (kfTime) kfTime.value = next.toFixed(2);
|
|
1987
|
-
requestDraw();
|
|
1988
|
-
}
|
|
2055
|
+
const kfTime = document.getElementById("kfTime");
|
|
2056
|
+
if (kfTime) kfTime.value = next.toFixed(2);
|
|
2057
|
+
requestDraw();
|
|
2058
|
+
}
|
|
1989
2059
|
|
|
1990
2060
|
function clearSidebarCommitTimers() {
|
|
1991
2061
|
for (const key of Object.keys(sidebarCommitTimers)) {
|
|
@@ -2236,12 +2306,20 @@ document.addEventListener("keyup", (event) => {
|
|
|
2236
2306
|
if (event.code === "Space") spacePanActive = false;
|
|
2237
2307
|
});
|
|
2238
2308
|
|
|
2239
|
-
window.addEventListener("blur", () => {
|
|
2240
|
-
spacePanActive = false;
|
|
2241
|
-
endViewportPan();
|
|
2242
|
-
});
|
|
2243
|
-
|
|
2244
|
-
|
|
2309
|
+
window.addEventListener("blur", () => {
|
|
2310
|
+
spacePanActive = false;
|
|
2311
|
+
endViewportPan();
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
window.addEventListener("resize", () => {
|
|
2315
|
+
setTreeWidth(tree.getBoundingClientRect().width || loadTreeWidth(), false);
|
|
2316
|
+
resizeElementsTree();
|
|
2317
|
+
});
|
|
2318
|
+
window.addEventListener("pointermove", updateTreeResize);
|
|
2319
|
+
window.addEventListener("pointerup", endTreeResize);
|
|
2320
|
+
window.addEventListener("pointercancel", endTreeResize);
|
|
2321
|
+
|
|
2322
|
+
stage.addEventListener("pointerdown", (event) => {
|
|
2245
2323
|
const handle = event.target.closest("[data-handle]");
|
|
2246
2324
|
if (handle && selectedId) {
|
|
2247
2325
|
if (isElementLocked(selectedId) || isElementHidden(selectedId)) return;
|
|
@@ -2317,7 +2395,6 @@ async function finishDrag() {
|
|
|
2317
2395
|
if (snapshot.transform) snapshot.target.setAttribute("transform", snapshot.transform);
|
|
2318
2396
|
else snapshot.target.removeAttribute("transform");
|
|
2319
2397
|
}
|
|
2320
|
-
releaseLiveDragPreview(committed);
|
|
2321
2398
|
}
|
|
2322
2399
|
}
|
|
2323
2400
|
|
|
@@ -2343,8 +2420,6 @@ function startDrag(event, target, mode) {
|
|
|
2343
2420
|
event.preventDefault();
|
|
2344
2421
|
event.stopPropagation();
|
|
2345
2422
|
stage.setPointerCapture?.(event.pointerId);
|
|
2346
|
-
showLiveDragPreview(target);
|
|
2347
|
-
requestVisibleCanvasStageRender(doc && doc.canvas);
|
|
2348
2423
|
}
|
|
2349
2424
|
|
|
2350
2425
|
async function commitDrag(snapshot) {
|
|
@@ -2444,7 +2519,6 @@ function parentPoint(event, target) {
|
|
|
2444
2519
|
function previewDraggedTransform(prefix) {
|
|
2445
2520
|
if (!drag || !drag.target) return;
|
|
2446
2521
|
drag.target.setAttribute("transform", prefix + (drag.transform ? " " + drag.transform : ""));
|
|
2447
|
-
showLiveDragPreview(drag.target);
|
|
2448
2522
|
}
|
|
2449
2523
|
function selectedTarget() {
|
|
2450
2524
|
if (!selectedId || isElementHidden(selectedId) || isElementLocked(selectedId)) return null;
|
|
@@ -2745,10 +2819,11 @@ function cap(value) { value = String(value); return value.charAt(0).toUpperCase(
|
|
|
2745
2819
|
function cssId(id) { return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\\\\]^\\\`{|}~])/g, "\\\\$1"); }
|
|
2746
2820
|
function escapeText(value) { return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); }
|
|
2747
2821
|
function escapeAttr(value) { return escapeText(value).replace(/"/g, """).replace(/'/g, "'"); }
|
|
2748
|
-
function showError(error) { const box = document.getElementById("error"); if (box) box.textContent = error.message || String(error); }
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2822
|
+
function showError(error) { const box = document.getElementById("error"); if (box) box.textContent = error.message || String(error); }
|
|
2823
|
+
initTreeResize();
|
|
2824
|
+
load().catch(showError);
|
|
2825
|
+
</script></body></html>`;
|
|
2826
|
+
}
|
|
2752
2827
|
|
|
2753
2828
|
|
|
2754
2829
|
function escapeHtml(value) {
|