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.
@@ -0,0 +1,688 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderToEmbedHtml = renderToEmbedHtml;
4
+ const mp4_muxer_source_1 = require("../mp4-muxer-source");
5
+ const utils_1 = require("../utils");
6
+ const svg_1 = require("./svg");
7
+ function renderToEmbedHtml(document, options = {}) {
8
+ const title = String(options.title ?? "Sketchmark Embed");
9
+ const duration = Math.max(0, Number(document.canvas.duration ?? 0) || 0);
10
+ const initialTime = (0, utils_1.clamp)(Number(options.time ?? 0) || 0, 0, duration);
11
+ const fps = normalizePositiveInteger(options.fps, normalizePositiveInteger(document.canvas.fps, 24));
12
+ const maxFrames = normalizePositiveInteger(options.maxFrames, 180);
13
+ const frameCount = sampledFrameCount(duration, fps, maxFrames);
14
+ const frameTimes = Array.from({ length: frameCount }, (_, index) => frameTimeAt(index, frameCount, duration));
15
+ const frames = frameTimes.map((time) => (0, svg_1.renderToSvg)(document, {
16
+ time,
17
+ transparent: options.transparent
18
+ }));
19
+ const initialFrameIndex = frameIndexForTime(initialTime, frameTimes);
20
+ const initialFrame = frames[initialFrameIndex] ?? frames[0] ?? (0, svg_1.renderToSvg)(document, options);
21
+ const chromeBackground = escapeHtml(String(options.chromeBackground ?? "transparent"));
22
+ const statusLabel = escapeHtml(title || "Sketchmark embed");
23
+ const payload = {
24
+ title,
25
+ fileBase: safeFileName(title),
26
+ canvas: {
27
+ width: Math.max(1, Math.round(Number(document.canvas.width) || 1)),
28
+ height: Math.max(1, Math.round(Number(document.canvas.height) || 1))
29
+ },
30
+ document,
31
+ duration,
32
+ initialTime,
33
+ frameTimes,
34
+ autoplay: options.autoplay ?? duration > 0,
35
+ loop: options.loop ?? true,
36
+ includeExportControls: options.includeExportControls !== false,
37
+ frames
38
+ };
39
+ return `<!doctype html>
40
+ <html>
41
+ <head>
42
+ <meta charset="utf-8" />
43
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
44
+ <title>${escapeHtml(title)} - Sketchmark Embed</title>
45
+ <style>
46
+ html, body {
47
+ margin: 0;
48
+ width: 100%;
49
+ height: 100%;
50
+ background: ${chromeBackground};
51
+ color-scheme: light dark;
52
+ color: #0f172a;
53
+ font: 12px/1.4 Roboto, Arial, sans-serif;
54
+ }
55
+ body {
56
+ display: grid;
57
+ grid-template-rows: minmax(0, 1fr) auto;
58
+ overflow: hidden;
59
+ --embed-surface: rgba(255, 255, 255, 0.78);
60
+ --embed-surface-strong: rgba(255, 255, 255, 0.94);
61
+ --embed-border: rgba(15, 23, 42, 0.14);
62
+ --embed-border-strong: rgba(15, 23, 42, 0.18);
63
+ --embed-text: #0f172a;
64
+ --embed-muted: #475569;
65
+ --embed-button: rgba(255, 255, 255, 0.72);
66
+ --embed-button-hover: rgba(255, 255, 255, 0.92);
67
+ --embed-shadow: 0 18px 50px rgba(15, 23, 42, 0.14);
68
+ --embed-accent: #2563eb;
69
+ }
70
+ @media (prefers-color-scheme: dark) {
71
+ body {
72
+ --embed-surface: rgba(15, 23, 42, 0.74);
73
+ --embed-surface-strong: rgba(15, 23, 42, 0.92);
74
+ --embed-border: rgba(255, 255, 255, 0.12);
75
+ --embed-border-strong: rgba(255, 255, 255, 0.16);
76
+ --embed-text: #e5edf7;
77
+ --embed-muted: #b6c2d1;
78
+ --embed-button: rgba(255, 255, 255, 0.08);
79
+ --embed-button-hover: rgba(255, 255, 255, 0.14);
80
+ --embed-shadow: 0 18px 50px rgba(2, 6, 23, 0.32);
81
+ --embed-accent: #7dd3fc;
82
+ }
83
+ }
84
+ #stage {
85
+ min-height: 0;
86
+ display: grid;
87
+ place-items: center;
88
+ padding: 14px;
89
+ box-sizing: border-box;
90
+ }
91
+ #stage svg {
92
+ display: block;
93
+ max-width: 100%;
94
+ max-height: 100%;
95
+ width: auto;
96
+ height: auto;
97
+ box-shadow: 0 18px 60px rgba(15, 23, 42, 0.28);
98
+ }
99
+ #controls {
100
+ display: flex;
101
+ flex-wrap: wrap;
102
+ align-items: center;
103
+ gap: 10px;
104
+ margin: 0 14px 14px;
105
+ padding: 10px 12px;
106
+ border: 1px solid var(--embed-border);
107
+ border-radius: 14px;
108
+ background: var(--embed-surface);
109
+ color: var(--embed-text);
110
+ backdrop-filter: blur(16px) saturate(140%);
111
+ -webkit-backdrop-filter: blur(16px) saturate(140%);
112
+ box-shadow: var(--embed-shadow);
113
+ box-sizing: border-box;
114
+ }
115
+ button,
116
+ summary,
117
+ input {
118
+ font: inherit;
119
+ }
120
+ button,
121
+ summary {
122
+ border: 1px solid var(--embed-border-strong);
123
+ border-radius: 8px;
124
+ background: var(--embed-button);
125
+ color: inherit;
126
+ cursor: pointer;
127
+ transition: background 120ms ease, border-color 120ms ease;
128
+ }
129
+ button {
130
+ padding: 8px 12px;
131
+ }
132
+ button:hover,
133
+ summary:hover {
134
+ background: var(--embed-button-hover);
135
+ }
136
+ button:disabled {
137
+ opacity: 0.45;
138
+ cursor: default;
139
+ }
140
+ #play {
141
+ min-width: 68px;
142
+ }
143
+ #time {
144
+ flex: 1 1 180px;
145
+ min-width: 140px;
146
+ accent-color: var(--embed-accent);
147
+ }
148
+ #clock {
149
+ min-width: 110px;
150
+ font-variant-numeric: tabular-nums;
151
+ color: var(--embed-muted);
152
+ text-align: right;
153
+ }
154
+ #meta {
155
+ min-width: 0;
156
+ flex: 1 1 160px;
157
+ color: var(--embed-muted);
158
+ white-space: nowrap;
159
+ overflow: hidden;
160
+ text-overflow: ellipsis;
161
+ text-align: right;
162
+ }
163
+ details {
164
+ position: relative;
165
+ }
166
+ summary {
167
+ list-style: none;
168
+ padding: 8px 12px;
169
+ user-select: none;
170
+ }
171
+ summary::-webkit-details-marker {
172
+ display: none;
173
+ }
174
+ .exportMenu {
175
+ position: absolute;
176
+ right: 0;
177
+ bottom: calc(100% + 8px);
178
+ display: grid;
179
+ gap: 6px;
180
+ min-width: 110px;
181
+ padding: 8px;
182
+ border: 1px solid var(--embed-border);
183
+ border-radius: 10px;
184
+ background: var(--embed-surface-strong);
185
+ box-shadow: var(--embed-shadow);
186
+ backdrop-filter: blur(18px) saturate(150%);
187
+ -webkit-backdrop-filter: blur(18px) saturate(150%);
188
+ }
189
+ .exportMenu button {
190
+ width: 100%;
191
+ text-align: left;
192
+ padding: 8px 10px;
193
+ }
194
+ @media (max-width: 720px) {
195
+ #controls {
196
+ gap: 8px;
197
+ }
198
+ #meta {
199
+ order: 10;
200
+ width: 100%;
201
+ text-align: left;
202
+ }
203
+ details {
204
+ margin-left: auto;
205
+ }
206
+ }
207
+ </style>
208
+ </head>
209
+ <body>
210
+ <div id="stage">${initialFrame}</div>
211
+ <div id="controls">
212
+ <button id="play" type="button">Play</button>
213
+ <input id="time" type="range" min="0" max="${Math.max(duration, 0.001)}" step="0.01" value="${initialTime}" />
214
+ <div id="clock">0.00s / ${duration.toFixed(2)}s</div>
215
+ ${options.includeExportControls !== false ? `<details id="exportRoot">
216
+ <summary>Export</summary>
217
+ <div class="exportMenu">
218
+ <button type="button" data-export-format="svg">SVG</button>
219
+ <button type="button" data-export-format="png">PNG</button>
220
+ <button type="button" data-export-format="jpg">JPG</button>
221
+ <button type="button" data-export-format="html">HTML</button>
222
+ <button type="button" data-export-format="json">JSON</button>
223
+ <button type="button" data-export-format="mp4">MP4</button>
224
+ </div>
225
+ </details>` : ""}
226
+ <div id="meta">${statusLabel}</div>
227
+ </div>
228
+ <script>
229
+ const payload = ${serializeForScript(payload)};
230
+ const mp4MuxerSource = ${serializeForScript(mp4_muxer_source_1.MP4_MUXER_SOURCE)};
231
+ const stage = document.getElementById("stage");
232
+ const playButton = document.getElementById("play");
233
+ const slider = document.getElementById("time");
234
+ const clock = document.getElementById("clock");
235
+ const meta = document.getElementById("meta");
236
+ const exportRoot = document.getElementById("exportRoot");
237
+ const defaultMeta = meta ? meta.textContent || "" : "";
238
+ const frameTimes = Array.isArray(payload.frameTimes) ? payload.frameTimes : [];
239
+ const state = {
240
+ time: clampTime(payload.initialTime),
241
+ playing: false,
242
+ raf: 0,
243
+ lastTick: 0
244
+ };
245
+ let metaTimer = 0;
246
+ let exportBusy = false;
247
+ let mp4MuxerModulePromise = null;
248
+
249
+ function clampTime(value) {
250
+ if (!payload.duration) return 0;
251
+ const number = Number(value) || 0;
252
+ return Math.max(0, Math.min(payload.duration, number));
253
+ }
254
+
255
+ function frameIndexForTimeValue(time) {
256
+ if (!frameTimes.length) return 0;
257
+ const clamped = clampTime(time);
258
+ let bestIndex = 0;
259
+ let bestDelta = Number.POSITIVE_INFINITY;
260
+ for (let index = 0; index < frameTimes.length; index += 1) {
261
+ const delta = Math.abs(Number(frameTimes[index] || 0) - clamped);
262
+ if (delta <= bestDelta) {
263
+ bestIndex = index;
264
+ bestDelta = delta;
265
+ } else if (Number(frameTimes[index] || 0) > clamped) {
266
+ break;
267
+ }
268
+ }
269
+ return bestIndex;
270
+ }
271
+
272
+ function currentFrameSvg() {
273
+ const frames = Array.isArray(payload.frames) ? payload.frames : [];
274
+ return frames[frameIndexForTimeValue(state.time)] || frames[0] || "";
275
+ }
276
+
277
+ function timeLabel(value) {
278
+ return clampTime(value).toFixed(2).replace(".", "-");
279
+ }
280
+
281
+ function flashMeta(message) {
282
+ if (!meta) return;
283
+ meta.textContent = message;
284
+ if (metaTimer) window.clearTimeout(metaTimer);
285
+ metaTimer = window.setTimeout(() => {
286
+ meta.textContent = defaultMeta;
287
+ }, 2200);
288
+ }
289
+
290
+ function notifyRendered() {
291
+ if (window.parent && window.parent !== window) {
292
+ window.parent.postMessage(
293
+ {
294
+ type: "sketchmark-rendered",
295
+ title: payload.title,
296
+ duration: payload.duration,
297
+ time: state.time
298
+ },
299
+ "*"
300
+ );
301
+ }
302
+ }
303
+
304
+ function render() {
305
+ stage.innerHTML = currentFrameSvg();
306
+ slider.max = String(Math.max(payload.duration, 0.001));
307
+ slider.value = String(clampTime(state.time));
308
+ slider.disabled = exportBusy || payload.duration <= 0;
309
+ playButton.disabled = exportBusy || payload.duration <= 0;
310
+ playButton.textContent = state.playing ? "Pause" : "Play";
311
+ clock.textContent = payload.duration > 0
312
+ ? state.time.toFixed(2) + "s / " + payload.duration.toFixed(2) + "s"
313
+ : "Static preview";
314
+ }
315
+
316
+ function pause() {
317
+ state.playing = false;
318
+ if (state.raf) {
319
+ window.cancelAnimationFrame(state.raf);
320
+ state.raf = 0;
321
+ }
322
+ render();
323
+ }
324
+
325
+ function play() {
326
+ if (payload.duration <= 0 || state.playing) return;
327
+ state.playing = true;
328
+ state.lastTick = performance.now();
329
+ render();
330
+ state.raf = window.requestAnimationFrame(tick);
331
+ }
332
+
333
+ function seek(time, notify) {
334
+ state.time = clampTime(time);
335
+ render();
336
+ if (notify) notifyRendered();
337
+ }
338
+
339
+ function tick(now) {
340
+ if (!state.playing) return;
341
+ const delta = (now - state.lastTick) / 1000;
342
+ state.lastTick = now;
343
+ let nextTime = state.time + delta;
344
+
345
+ if (payload.duration > 0 && nextTime > payload.duration) {
346
+ if (payload.loop) nextTime = nextTime % payload.duration;
347
+ else {
348
+ nextTime = payload.duration;
349
+ state.playing = false;
350
+ }
351
+ }
352
+
353
+ state.time = clampTime(nextTime);
354
+ render();
355
+ if (state.playing) state.raf = window.requestAnimationFrame(tick);
356
+ else state.raf = 0;
357
+ }
358
+
359
+ function downloadBlob(blob, filename) {
360
+ const url = URL.createObjectURL(blob);
361
+ const anchor = document.createElement("a");
362
+ anchor.href = url;
363
+ anchor.download = filename;
364
+ document.body.appendChild(anchor);
365
+ anchor.click();
366
+ anchor.remove();
367
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
368
+ }
369
+
370
+ function downloadText(filename, text, mimeType) {
371
+ downloadBlob(new Blob([text], { type: mimeType }), filename);
372
+ }
373
+
374
+ function loadSvgImage(svg) {
375
+ const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
376
+ const url = URL.createObjectURL(blob);
377
+ return loadImage(url).finally(() => URL.revokeObjectURL(url));
378
+ }
379
+
380
+ function loadImage(url) {
381
+ return new Promise((resolve, reject) => {
382
+ const image = new Image();
383
+ image.onload = () => resolve(image);
384
+ image.onerror = () => reject(new Error("Could not rasterize the current SVG frame."));
385
+ image.src = url;
386
+ });
387
+ }
388
+
389
+ function canvasToBlob(canvas, type, quality) {
390
+ return new Promise((resolve, reject) => {
391
+ canvas.toBlob((blob) => {
392
+ if (blob) resolve(blob);
393
+ else reject(new Error("Could not export the current frame."));
394
+ }, type, quality);
395
+ });
396
+ }
397
+
398
+ function yieldToBrowser() {
399
+ return new Promise((resolve) => window.setTimeout(resolve, 0));
400
+ }
401
+
402
+ function evenDimension(value) {
403
+ const rounded = Math.max(2, Math.round(Number(value) || 0));
404
+ return rounded % 2 === 0 ? rounded : rounded + 1;
405
+ }
406
+
407
+ function sampleFrameDuration(index) {
408
+ if (frameTimes.length >= 2) {
409
+ if (index < frameTimes.length - 1) {
410
+ return Math.max(1 / 240, Number(frameTimes[index + 1]) - Number(frameTimes[index]));
411
+ }
412
+ return Math.max(1 / 240, Number(frameTimes[index]) - Number(frameTimes[index - 1]));
413
+ }
414
+ const fallbackFps = Math.max(1, Number(payload.document && payload.document.canvas && payload.document.canvas.fps || 24));
415
+ return 1 / fallbackFps;
416
+ }
417
+
418
+ function sampleFps() {
419
+ if (payload.duration > 0 && frameTimes.length >= 2) {
420
+ return Math.max(1, Math.round((frameTimes.length - 1) / payload.duration));
421
+ }
422
+ return Math.max(1, Number(payload.document && payload.document.canvas && payload.document.canvas.fps || 24));
423
+ }
424
+
425
+ async function drawSvgToCanvas(svg, canvas, width, height) {
426
+ const context = canvas.getContext("2d");
427
+ if (!context) throw new Error("Could not create a 2D canvas context.");
428
+ const image = await loadSvgImage(svg);
429
+ context.clearRect(0, 0, width, height);
430
+ context.drawImage(image, 0, 0, width, height);
431
+ }
432
+
433
+ async function loadMp4Muxer() {
434
+ if (!mp4MuxerModulePromise) {
435
+ const blob = new Blob([mp4MuxerSource], { type: "text/javascript;charset=utf-8" });
436
+ const url = URL.createObjectURL(blob);
437
+ mp4MuxerModulePromise = import(url)
438
+ .catch((error) => {
439
+ mp4MuxerModulePromise = null;
440
+ throw error;
441
+ })
442
+ .finally(() => URL.revokeObjectURL(url));
443
+ }
444
+ return mp4MuxerModulePromise;
445
+ }
446
+
447
+ async function rasterBlob(format) {
448
+ const canvas = document.createElement("canvas");
449
+ canvas.width = payload.canvas.width;
450
+ canvas.height = payload.canvas.height;
451
+ await drawSvgToCanvas(currentFrameSvg(), canvas, canvas.width, canvas.height);
452
+ return canvasToBlob(canvas, format === "jpg" ? "image/jpeg" : "image/png", format === "jpg" ? 0.92 : undefined);
453
+ }
454
+
455
+ async function exportMp4() {
456
+ const globalApi = globalThis;
457
+ const VideoEncoderCtor = globalApi.VideoEncoder;
458
+ const VideoFrameCtor = globalApi.VideoFrame;
459
+ if (!VideoEncoderCtor || !VideoFrameCtor) {
460
+ throw new Error("MP4 export requires WebCodecs. Try Chrome or Edge.");
461
+ }
462
+ if (!(payload.duration > 0)) {
463
+ throw new Error("MP4 export requires a positive duration.");
464
+ }
465
+
466
+ const frames = Array.isArray(payload.frames) ? payload.frames : [];
467
+ if (!frames.length) {
468
+ throw new Error("No frames are available for MP4 export.");
469
+ }
470
+
471
+ const { Muxer, ArrayBufferTarget } = await loadMp4Muxer();
472
+ const encodeWidth = evenDimension(payload.canvas.width);
473
+ const encodeHeight = evenDimension(payload.canvas.height);
474
+ const fps = sampleFps();
475
+ const target = new ArrayBufferTarget();
476
+ const muxer = new Muxer({
477
+ target,
478
+ video: { codec: "avc", width: encodeWidth, height: encodeHeight },
479
+ fastStart: "in-memory"
480
+ });
481
+
482
+ let encoderError = null;
483
+ const encoder = new VideoEncoderCtor({
484
+ output: (chunk, metadata) => muxer.addVideoChunk(chunk, metadata),
485
+ error: (error) => {
486
+ encoderError = error;
487
+ }
488
+ });
489
+
490
+ encoder.configure({
491
+ codec: "avc1.640028",
492
+ width: encodeWidth,
493
+ height: encodeHeight,
494
+ bitrate: 5_000_000,
495
+ framerate: fps
496
+ });
497
+
498
+ const canvas = document.createElement("canvas");
499
+ canvas.width = encodeWidth;
500
+ canvas.height = encodeHeight;
501
+
502
+ try {
503
+ for (let index = 0; index < frames.length; index += 1) {
504
+ await drawSvgToCanvas(frames[index], canvas, encodeWidth, encodeHeight);
505
+ const frameTime = frameTimes.length ? Number(frameTimes[index] || 0) : Math.min(payload.duration, index / fps);
506
+ const frameDuration = sampleFrameDuration(index);
507
+ const frame = new VideoFrameCtor(canvas, {
508
+ timestamp: Math.max(0, Math.round(frameTime * 1_000_000)),
509
+ duration: Math.max(1, Math.round(frameDuration * 1_000_000))
510
+ });
511
+
512
+ encoder.encode(frame, { keyFrame: index % Math.max(1, fps * 2) === 0 });
513
+ frame.close();
514
+
515
+ if (encoderError) throw encoderError;
516
+ if (index % 5 === 0 || index === frames.length - 1) {
517
+ if (meta) meta.textContent = "Encoding MP4 " + Math.round(((index + 1) / frames.length) * 100) + "%";
518
+ await yieldToBrowser();
519
+ }
520
+ }
521
+
522
+ await encoder.flush();
523
+ if (encoderError) throw encoderError;
524
+ encoder.close();
525
+ muxer.finalize();
526
+ downloadBlob(new Blob([target.buffer], { type: "video/mp4" }), payload.fileBase + ".mp4");
527
+ } catch (error) {
528
+ try {
529
+ encoder.close();
530
+ } catch {}
531
+ throw error;
532
+ }
533
+ }
534
+
535
+ async function exportCurrent(format) {
536
+ if (exportBusy) return;
537
+ const resumePlayback = state.playing;
538
+ if (resumePlayback) pause();
539
+ exportBusy = true;
540
+ render();
541
+ try {
542
+ if (format === "svg") {
543
+ downloadText(payload.fileBase + "-t" + timeLabel(state.time) + ".svg", currentFrameSvg(), "image/svg+xml;charset=utf-8");
544
+ flashMeta("Saved SVG");
545
+ return;
546
+ }
547
+ if (format === "json") {
548
+ downloadText(payload.fileBase + ".visual.json", JSON.stringify(payload.document, null, 2) + "\\n", "application/json;charset=utf-8");
549
+ flashMeta("Saved JSON");
550
+ return;
551
+ }
552
+ if (format === "html") {
553
+ downloadText(payload.fileBase + ".embed.html", "<!doctype html>\\n" + document.documentElement.outerHTML, "text/html;charset=utf-8");
554
+ flashMeta("Saved HTML");
555
+ return;
556
+ }
557
+ if (format === "png" || format === "jpg") {
558
+ const blob = await rasterBlob(format);
559
+ downloadBlob(blob, payload.fileBase + "-t" + timeLabel(state.time) + "." + format);
560
+ flashMeta("Saved " + format.toUpperCase());
561
+ return;
562
+ }
563
+ if (format === "mp4") {
564
+ await exportMp4();
565
+ flashMeta("Saved MP4");
566
+ return;
567
+ }
568
+ throw new Error("Unsupported export format: " + format);
569
+ } catch (error) {
570
+ flashMeta(error && error.message ? error.message : "Export failed.");
571
+ throw error;
572
+ } finally {
573
+ exportBusy = false;
574
+ render();
575
+ if (resumePlayback && !state.playing) play();
576
+ if (exportRoot) exportRoot.open = false;
577
+ }
578
+ }
579
+
580
+ playButton.addEventListener("click", () => {
581
+ if (state.playing) pause();
582
+ else play();
583
+ });
584
+
585
+ slider.addEventListener("input", () => {
586
+ seek(Number(slider.value || 0), false);
587
+ });
588
+
589
+ document.addEventListener("click", (event) => {
590
+ const target = event.target;
591
+ const button = target && typeof target.closest === "function"
592
+ ? target.closest("[data-export-format]")
593
+ : null;
594
+ const format = button && button.getAttribute ? button.getAttribute("data-export-format") : "";
595
+ if (format) {
596
+ exportCurrent(format).catch(() => {});
597
+ }
598
+ });
599
+
600
+ window.addEventListener("message", (event) => {
601
+ const message = event.data || {};
602
+ if (message.type === "sketchmark-show" && typeof message.time === "number") {
603
+ seek(message.time, true);
604
+ return;
605
+ }
606
+ if (message.type === "sketchmark-play") {
607
+ play();
608
+ return;
609
+ }
610
+ if (message.type === "sketchmark-pause") {
611
+ pause();
612
+ return;
613
+ }
614
+ if (message.type === "sketchmark-export" && typeof message.format === "string") {
615
+ exportCurrent(message.format).catch(() => {});
616
+ }
617
+ });
618
+
619
+ window.__SKETCHMARK_EMBED__ = {
620
+ play,
621
+ pause,
622
+ seek,
623
+ export: exportCurrent,
624
+ getState: () => ({
625
+ time: state.time,
626
+ duration: payload.duration,
627
+ playing: state.playing,
628
+ frameCount: Array.isArray(payload.frames) ? payload.frames.length : 0
629
+ })
630
+ };
631
+
632
+ render();
633
+ notifyRendered();
634
+ if (payload.autoplay && payload.duration > 0) play();
635
+ </script>
636
+ </body>
637
+ </html>`;
638
+ }
639
+ function normalizePositiveInteger(value, fallback) {
640
+ const parsed = Math.round(Number(value));
641
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
642
+ }
643
+ function sampledFrameCount(duration, fps, maxFrames) {
644
+ if (!(duration > 0))
645
+ return 1;
646
+ return Math.max(2, Math.min(maxFrames, Math.ceil(duration * fps) + 1));
647
+ }
648
+ function frameTimeAt(index, frameCount, duration) {
649
+ if (frameCount <= 1 || duration <= 0)
650
+ return 0;
651
+ return (index / (frameCount - 1)) * duration;
652
+ }
653
+ function frameIndexForTime(time, frameTimes) {
654
+ if (!frameTimes.length)
655
+ return 0;
656
+ const clamped = (0, utils_1.clamp)(time, 0, Math.max(0, Number(frameTimes[frameTimes.length - 1] ?? 0)));
657
+ let bestIndex = 0;
658
+ let bestDelta = Number.POSITIVE_INFINITY;
659
+ for (let index = 0; index < frameTimes.length; index += 1) {
660
+ const delta = Math.abs(Number(frameTimes[index] ?? 0) - clamped);
661
+ if (delta <= bestDelta) {
662
+ bestIndex = index;
663
+ bestDelta = delta;
664
+ }
665
+ else if (Number(frameTimes[index] ?? 0) > clamped) {
666
+ break;
667
+ }
668
+ }
669
+ return bestIndex;
670
+ }
671
+ function safeFileName(value) {
672
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "sketchmark";
673
+ }
674
+ function escapeHtml(value) {
675
+ return value
676
+ .replace(/&/g, "&amp;")
677
+ .replace(/</g, "&lt;")
678
+ .replace(/>/g, "&gt;")
679
+ .replace(/"/g, "&quot;");
680
+ }
681
+ function serializeForScript(value) {
682
+ return JSON.stringify(value)
683
+ .replace(/</g, "\\u003c")
684
+ .replace(/>/g, "\\u003e")
685
+ .replace(/&/g, "\\u0026")
686
+ .replace(/\u2028/g, "\\u2028")
687
+ .replace(/\u2029/g, "\\u2029");
688
+ }