lite-image-preview 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js ADDED
@@ -0,0 +1,719 @@
1
+ //#region src/util.ts
2
+ /**
3
+ * Clamp a number into the given range.
4
+ */
5
+ function clamp(n, min, max) {
6
+ return Math.min(max, Math.max(min, n));
7
+ }
8
+ /**
9
+ * Return whether a value is a finite number.
10
+ */
11
+ function isFiniteNumber(n) {
12
+ return Number.isFinite(n);
13
+ }
14
+ /**
15
+ * Compute the Euclidean distance between two points.
16
+ */
17
+ function dist(a, b) {
18
+ return Math.hypot(a.x - b.x, a.y - b.y);
19
+ }
20
+ /**
21
+ * Compute the midpoint between two points.
22
+ */
23
+ function mid(a, b) {
24
+ return {
25
+ x: (a.x + b.x) / 2,
26
+ y: (a.y + b.y) / 2
27
+ };
28
+ }
29
+ /**
30
+ * Read a safe client rect. Width and height are never returned as zero.
31
+ */
32
+ function safeRect(el) {
33
+ const rect = el.getBoundingClientRect();
34
+ return {
35
+ left: rect.left,
36
+ top: rect.top,
37
+ width: rect.width || 1,
38
+ height: rect.height || 1
39
+ };
40
+ }
41
+ /**
42
+ * Parse a numeric SVG/CSS length value.
43
+ * Returns null when the value is missing or invalid.
44
+ */
45
+ function parseLength(value) {
46
+ if (!value) return null;
47
+ const n = Number.parseFloat(value);
48
+ return Number.isFinite(n) && n > 0 ? n : null;
49
+ }
50
+ /**
51
+ * Measure the base SVG size used to initialize the preview viewBox.
52
+ */
53
+ function measureSvgBaseViewBox(svg) {
54
+ const vb = svg.viewBox.baseVal;
55
+ if (vb && vb.width > 0 && vb.height > 0) return {
56
+ x: vb.x,
57
+ y: vb.y,
58
+ w: vb.width,
59
+ h: vb.height
60
+ };
61
+ const attrW = parseLength(svg.getAttribute("width"));
62
+ const attrH = parseLength(svg.getAttribute("height"));
63
+ if (attrW && attrH) return {
64
+ x: 0,
65
+ y: 0,
66
+ w: attrW,
67
+ h: attrH
68
+ };
69
+ try {
70
+ const bbox = svg.getBBox();
71
+ if (bbox.width > 0 && bbox.height > 0) return {
72
+ x: bbox.x,
73
+ y: bbox.y,
74
+ w: bbox.width,
75
+ h: bbox.height
76
+ };
77
+ } catch {}
78
+ return {
79
+ x: 0,
80
+ y: 0,
81
+ w: 100,
82
+ h: 100
83
+ };
84
+ }
85
+ /**
86
+ * Wait until an HTML image is fully loaded.
87
+ */
88
+ function waitForImageLoad(img) {
89
+ return new Promise((resolve, reject) => {
90
+ if (img.complete) {
91
+ if (img.naturalWidth > 0) resolve();
92
+ else reject(/* @__PURE__ */ new Error(`Image failed to load: ${img.currentSrc || img.src}`));
93
+ return;
94
+ }
95
+ img.addEventListener("load", () => resolve(), { once: true });
96
+ img.addEventListener("error", () => reject(/* @__PURE__ */ new Error(`Image failed to load: ${img.currentSrc || img.src}`)), { once: true });
97
+ });
98
+ }
99
+ //#endregion
100
+ //#region src/preview.ts
101
+ /**
102
+ * Open a modal preview dialog for arbitrary content.
103
+ *
104
+ * The dialog container handles lifecycle and gestures, while the adapter
105
+ * implements the actual rendering backend.
106
+ *
107
+ * @param content - The DOM element to preview.
108
+ * @param initAdapter - Factory that creates a preview adapter after the stage is mounted.
109
+ * @param dispose - Optional cleanup callback called after the dialog closes.
110
+ * @returns A function that closes the preview dialog.
111
+ */
112
+ function createPreview(content, initAdapter, dispose) {
113
+ const body = document.body;
114
+ const previousOverflow = body.style.overflow;
115
+ const dialog = document.createElement("dialog");
116
+ const stage = document.createElement("div");
117
+ const resetBtn = document.createElement("button");
118
+ const closeBtn = document.createElement("button");
119
+ let adapter = null;
120
+ let gestureBinder = null;
121
+ let closed = false;
122
+ dialog.closedBy = "closeRequest";
123
+ dialog.style.cssText = `
124
+ width: 100%;
125
+ height: 100%;
126
+ max-width: 100%;
127
+ max-height: 100%;
128
+ box-sizing: border-box;
129
+ padding: 0;
130
+ border: none;
131
+ background: rgba(255, 255, 255, 0.8);
132
+ overflow: hidden;
133
+ `;
134
+ stage.style.cssText = `
135
+ position: relative;
136
+ width: 100%;
137
+ height: 100%;
138
+ overflow: hidden;
139
+ touch-action: none;
140
+ user-select: none;
141
+ `;
142
+ content.style.cssText += `
143
+ position: absolute;
144
+ left: 0;
145
+ top: 0;
146
+ cursor: grab;
147
+ `;
148
+ content.setAttribute("autofocus", "");
149
+ resetBtn.type = "button";
150
+ resetBtn.textContent = "Reset";
151
+ resetBtn.setAttribute("aria-label", "Reset to fit");
152
+ resetBtn.style.cssText = `
153
+ position: absolute;
154
+ top: 16px;
155
+ right: 64px;
156
+ z-index: 2;
157
+ min-width: 72px;
158
+ height: 40px;
159
+ padding: 0 14px;
160
+ border: 0;
161
+ border-radius: 999px;
162
+ background: rgba(0, 0, 0, 0.5);
163
+ color: #fff;
164
+ font-size: 14px;
165
+ cursor: pointer;
166
+ `;
167
+ resetBtn.disabled = true;
168
+ closeBtn.type = "button";
169
+ closeBtn.textContent = "×";
170
+ closeBtn.setAttribute("aria-label", "Close");
171
+ closeBtn.style.cssText = `
172
+ position: absolute;
173
+ top: 16px;
174
+ right: 16px;
175
+ z-index: 2;
176
+ width: 40px;
177
+ height: 40px;
178
+ border: 0;
179
+ border-radius: 50%;
180
+ background: rgba(0, 0, 0, 0.5);
181
+ color: #fff;
182
+ font-size: 28px;
183
+ line-height: 40px;
184
+ cursor: pointer;
185
+ `;
186
+ stage.appendChild(content);
187
+ stage.appendChild(resetBtn);
188
+ stage.appendChild(closeBtn);
189
+ dialog.appendChild(stage);
190
+ body.appendChild(dialog);
191
+ body.style.overflow = "hidden";
192
+ const onWheel = (e) => {
193
+ adapter?.zoomWithWheel(e);
194
+ };
195
+ const cleanup = () => {
196
+ if (closed) return;
197
+ closed = true;
198
+ stage.removeEventListener("wheel", onWheel);
199
+ gestureBinder?.destroy();
200
+ gestureBinder = null;
201
+ adapter?.destroy();
202
+ adapter?.resetStyle();
203
+ adapter = null;
204
+ dialog.remove();
205
+ body.style.overflow = previousOverflow;
206
+ dispose?.();
207
+ };
208
+ closeBtn.addEventListener("click", (e) => {
209
+ e.stopPropagation();
210
+ dialog.close();
211
+ });
212
+ resetBtn.addEventListener("click", (e) => {
213
+ e.preventDefault();
214
+ e.stopPropagation();
215
+ adapter?.fitToStage(stage);
216
+ });
217
+ dialog.addEventListener("close", cleanup);
218
+ dialog.style.visibility = "hidden";
219
+ dialog.showModal();
220
+ requestAnimationFrame(() => {
221
+ adapter = initAdapter(stage);
222
+ gestureBinder = bindGestures(content, adapter);
223
+ adapter.fitToStage(stage);
224
+ resetBtn.disabled = false;
225
+ requestAnimationFrame(() => {
226
+ adapter?.fitToStage(stage);
227
+ dialog.style.visibility = "visible";
228
+ });
229
+ });
230
+ stage.addEventListener("wheel", onWheel, { passive: false });
231
+ return () => dialog.close();
232
+ }
233
+ /**
234
+ * Build the default transform-based adapter used for raster images.
235
+ */
236
+ function createTransformAdapter(content, stage, baseWidth, baseHeight, options) {
237
+ const minScale = options?.minScale ?? .1;
238
+ const maxScale = options?.maxScale ?? 8;
239
+ const fitPadding = options?.fitPadding ?? 32;
240
+ const fitMaxScale = options?.fitMaxScale ?? 1;
241
+ const prev = {
242
+ transform: content.style.transform,
243
+ transformOrigin: content.style.transformOrigin,
244
+ width: content.style.width,
245
+ height: content.style.height,
246
+ cursor: content.style.cursor,
247
+ touchAction: content.style.touchAction,
248
+ userSelect: content.style.userSelect,
249
+ display: content.style.display,
250
+ maxWidth: content.style.maxWidth,
251
+ maxHeight: content.style.maxHeight,
252
+ position: content.style.position,
253
+ left: content.style.left,
254
+ top: content.style.top
255
+ };
256
+ content.style.transformOrigin = "0 0";
257
+ content.style.touchAction = "none";
258
+ content.style.userSelect = "none";
259
+ content.style.cursor = "grab";
260
+ content.style.display = "block";
261
+ content.style.maxWidth = "none";
262
+ content.style.maxHeight = "none";
263
+ content.style.position = "absolute";
264
+ content.style.left = "0";
265
+ content.style.top = "0";
266
+ content.style.width = `${baseWidth}px`;
267
+ content.style.height = `${baseHeight}px`;
268
+ const state = {
269
+ scale: 1,
270
+ x: 0,
271
+ y: 0
272
+ };
273
+ let pinchStart = null;
274
+ function apply() {
275
+ content.style.transform = `translate3d(${state.x}px, ${state.y}px, 0) scale(${state.scale})`;
276
+ }
277
+ function fitToStage() {
278
+ const rect = safeRect(stage);
279
+ const availW = Math.max(rect.width - fitPadding, 1);
280
+ const availH = Math.max(rect.height - fitPadding, 1);
281
+ const raw = Math.min(availW / baseWidth, availH / baseHeight, fitMaxScale);
282
+ const fitScale = clamp(isFiniteNumber(raw) && raw > 0 ? raw : 1, minScale, maxScale);
283
+ state.scale = fitScale;
284
+ state.x = (rect.width - baseWidth * fitScale) / 2;
285
+ state.y = (rect.height - baseHeight * fitScale) / 2;
286
+ apply();
287
+ }
288
+ function panBy(dx, dy) {
289
+ if (!isFiniteNumber(dx) || !isFiniteNumber(dy)) return;
290
+ state.x += dx;
291
+ state.y += dy;
292
+ apply();
293
+ }
294
+ function zoomAt(clientX, clientY, factor) {
295
+ if (!isFiniteNumber(factor) || factor <= 0) return;
296
+ const nextScale = clamp(state.scale * factor, minScale, maxScale);
297
+ if (nextScale === state.scale) return;
298
+ const contentX = (clientX - state.x) / state.scale;
299
+ const contentY = (clientY - state.y) / state.scale;
300
+ state.scale = nextScale;
301
+ state.x = clientX - contentX * nextScale;
302
+ state.y = clientY - contentY * nextScale;
303
+ apply();
304
+ }
305
+ function beginPinch(points) {
306
+ const [p1, p2] = points;
307
+ const midpoint = mid(p1, p2);
308
+ pinchStart = {
309
+ state: { ...state },
310
+ distance: dist(p1, p2),
311
+ anchor: {
312
+ x: (midpoint.x - state.x) / state.scale,
313
+ y: (midpoint.y - state.y) / state.scale
314
+ }
315
+ };
316
+ }
317
+ function updatePinch(points) {
318
+ if (!pinchStart) return;
319
+ const [p1, p2] = points;
320
+ const currentDistance = dist(p1, p2);
321
+ if (!isFiniteNumber(currentDistance) || currentDistance <= 0) return;
322
+ const currentMid = mid(p1, p2);
323
+ const factor = currentDistance / pinchStart.distance;
324
+ const nextScale = clamp(pinchStart.state.scale * factor, minScale, maxScale);
325
+ state.scale = nextScale;
326
+ state.x = currentMid.x - pinchStart.anchor.x * nextScale;
327
+ state.y = currentMid.y - pinchStart.anchor.y * nextScale;
328
+ apply();
329
+ }
330
+ function zoomWithWheel(e) {
331
+ e.preventDefault();
332
+ zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * .0015));
333
+ }
334
+ function destroy() {
335
+ pinchStart = null;
336
+ }
337
+ function resetStyle() {
338
+ content.style.transform = prev.transform;
339
+ content.style.transformOrigin = prev.transformOrigin;
340
+ content.style.width = prev.width;
341
+ content.style.height = prev.height;
342
+ content.style.cursor = prev.cursor;
343
+ content.style.touchAction = prev.touchAction;
344
+ content.style.userSelect = prev.userSelect;
345
+ content.style.display = prev.display;
346
+ content.style.maxWidth = prev.maxWidth;
347
+ content.style.maxHeight = prev.maxHeight;
348
+ content.style.position = prev.position;
349
+ content.style.left = prev.left;
350
+ content.style.top = prev.top;
351
+ }
352
+ return {
353
+ fitToStage,
354
+ panBy,
355
+ zoomAt,
356
+ beginPinch,
357
+ updatePinch,
358
+ zoomWithWheel,
359
+ destroy,
360
+ resetStyle
361
+ };
362
+ }
363
+ /**
364
+ * Bind mouse, pen, and touch gestures to the preview content.
365
+ *
366
+ * Touch input is routed through Touch Events to avoid browser-specific
367
+ * pointer gesture quirks on some mobile browsers.
368
+ */
369
+ function bindGestures(content, adapter) {
370
+ const pointers = /* @__PURE__ */ new Map();
371
+ const lastPointers = /* @__PURE__ */ new Map();
372
+ let pinchActive = false;
373
+ let pinchRafId = null;
374
+ function getPrimaryTwoPoints() {
375
+ const pair = [...pointers.entries()].sort((a, b) => a[0] - b[0]).slice(0, 2).map(([, p]) => p);
376
+ if (pair.length !== 2) return null;
377
+ return [pair[0], pair[1]];
378
+ }
379
+ function ensurePinchStarted() {
380
+ const pair = getPrimaryTwoPoints();
381
+ if (!pair) return;
382
+ adapter.beginPinch(pair);
383
+ pinchActive = true;
384
+ }
385
+ function schedulePinchUpdate() {
386
+ if (pinchRafId !== null) return;
387
+ pinchRafId = requestAnimationFrame(() => {
388
+ pinchRafId = null;
389
+ if (pointers.size < 2) return;
390
+ const pair = getPrimaryTwoPoints();
391
+ if (!pair) return;
392
+ if (!pinchActive) {
393
+ adapter.beginPinch(pair);
394
+ pinchActive = true;
395
+ }
396
+ adapter.updatePinch(pair);
397
+ });
398
+ }
399
+ function stopPinchIfNeeded() {
400
+ if (pointers.size < 2) pinchActive = false;
401
+ }
402
+ function onPointerDown(e) {
403
+ if (e.pointerType === "touch") return;
404
+ if (!content.isConnected) return;
405
+ e.preventDefault();
406
+ try {
407
+ content.setPointerCapture(e.pointerId);
408
+ } catch {}
409
+ const point = {
410
+ x: e.clientX,
411
+ y: e.clientY
412
+ };
413
+ pointers.set(e.pointerId, point);
414
+ lastPointers.set(e.pointerId, point);
415
+ if (pointers.size >= 2) ensurePinchStarted();
416
+ }
417
+ function onPointerMove(e) {
418
+ if (e.pointerType === "touch") return;
419
+ if (!pointers.has(e.pointerId)) return;
420
+ if (!content.isConnected) return;
421
+ e.preventDefault();
422
+ const current = {
423
+ x: e.clientX,
424
+ y: e.clientY
425
+ };
426
+ const previous = lastPointers.get(e.pointerId) ?? current;
427
+ pointers.set(e.pointerId, current);
428
+ lastPointers.set(e.pointerId, current);
429
+ if (pointers.size === 1) {
430
+ adapter.panBy(current.x - previous.x, current.y - previous.y);
431
+ return;
432
+ }
433
+ if (pointers.size >= 2) {
434
+ if (!pinchActive) ensurePinchStarted();
435
+ schedulePinchUpdate();
436
+ }
437
+ }
438
+ function onPointerUp(e) {
439
+ if (e.pointerType === "touch") return;
440
+ pointers.delete(e.pointerId);
441
+ lastPointers.delete(e.pointerId);
442
+ if (pointers.size >= 2) {
443
+ ensurePinchStarted();
444
+ schedulePinchUpdate();
445
+ } else stopPinchIfNeeded();
446
+ if (pinchRafId !== null) {
447
+ cancelAnimationFrame(pinchRafId);
448
+ pinchRafId = null;
449
+ }
450
+ }
451
+ function onTouchStart(e) {
452
+ if (!content.isConnected) return;
453
+ e.preventDefault();
454
+ for (const touch of Array.from(e.changedTouches)) {
455
+ const point = {
456
+ x: touch.clientX,
457
+ y: touch.clientY
458
+ };
459
+ pointers.set(touch.identifier, point);
460
+ lastPointers.set(touch.identifier, point);
461
+ }
462
+ if (pointers.size >= 2) ensurePinchStarted();
463
+ }
464
+ function onTouchMove(e) {
465
+ if (!content.isConnected) return;
466
+ e.preventDefault();
467
+ for (const touch of Array.from(e.changedTouches)) {
468
+ const current = {
469
+ x: touch.clientX,
470
+ y: touch.clientY
471
+ };
472
+ const previous = lastPointers.get(touch.identifier) ?? current;
473
+ pointers.set(touch.identifier, current);
474
+ lastPointers.set(touch.identifier, current);
475
+ if (pointers.size === 1) adapter.panBy(current.x - previous.x, current.y - previous.y);
476
+ }
477
+ if (pointers.size >= 2) {
478
+ if (!pinchActive) ensurePinchStarted();
479
+ schedulePinchUpdate();
480
+ }
481
+ }
482
+ function onTouchEnd(e) {
483
+ for (const touch of Array.from(e.changedTouches)) {
484
+ pointers.delete(touch.identifier);
485
+ lastPointers.delete(touch.identifier);
486
+ }
487
+ if (pointers.size >= 2) {
488
+ ensurePinchStarted();
489
+ schedulePinchUpdate();
490
+ } else stopPinchIfNeeded();
491
+ if (pinchRafId !== null) {
492
+ cancelAnimationFrame(pinchRafId);
493
+ pinchRafId = null;
494
+ }
495
+ }
496
+ content.addEventListener("pointerdown", onPointerDown);
497
+ content.addEventListener("pointermove", onPointerMove);
498
+ content.addEventListener("pointerup", onPointerUp);
499
+ content.addEventListener("pointercancel", onPointerUp);
500
+ content.addEventListener("lostpointercapture", onPointerUp);
501
+ content.addEventListener("touchstart", onTouchStart, { passive: false });
502
+ content.addEventListener("touchmove", onTouchMove, { passive: false });
503
+ content.addEventListener("touchend", onTouchEnd);
504
+ content.addEventListener("touchcancel", onTouchEnd);
505
+ return { destroy() {
506
+ content.removeEventListener("pointerdown", onPointerDown);
507
+ content.removeEventListener("pointermove", onPointerMove);
508
+ content.removeEventListener("pointerup", onPointerUp);
509
+ content.removeEventListener("pointercancel", onPointerUp);
510
+ content.removeEventListener("lostpointercapture", onPointerUp);
511
+ content.removeEventListener("touchstart", onTouchStart);
512
+ content.removeEventListener("touchmove", onTouchMove);
513
+ content.removeEventListener("touchend", onTouchEnd);
514
+ content.removeEventListener("touchcancel", onTouchEnd);
515
+ pointers.clear();
516
+ lastPointers.clear();
517
+ if (pinchRafId !== null) {
518
+ cancelAnimationFrame(pinchRafId);
519
+ pinchRafId = null;
520
+ }
521
+ } };
522
+ }
523
+ /**
524
+ * Open an image preview dialog.
525
+ *
526
+ * The returned promise resolves to a close handle when the preview is ready.
527
+ * If the image fails to load, the promise resolves to null.
528
+ */
529
+ async function previewImage(url, dispose) {
530
+ const img = document.createElement("img");
531
+ img.src = url;
532
+ img.alt = "";
533
+ img.draggable = false;
534
+ img.style.cssText = `
535
+ display: block;
536
+ max-width: none;
537
+ max-height: none;
538
+ `;
539
+ try {
540
+ await waitForImageLoad(img);
541
+ } catch {
542
+ dispose?.();
543
+ return null;
544
+ }
545
+ return createPreview(img, (stage) => {
546
+ return createTransformAdapter(img, stage, img.naturalWidth || 1, img.naturalHeight || 1, {
547
+ minScale: .1,
548
+ maxScale: 8,
549
+ fitPadding: 32,
550
+ fitMaxScale: 1
551
+ });
552
+ }, dispose);
553
+ }
554
+ /**
555
+ * Open an SVG preview dialog.
556
+ *
557
+ * The returned promise resolves to a close handle when the preview is ready.
558
+ */
559
+ async function previewSvg(svg, dispose) {
560
+ const cloned = svg.cloneNode(true);
561
+ cloned.removeAttribute("width");
562
+ cloned.removeAttribute("height");
563
+ cloned.style.cssText = `
564
+ display: block;
565
+ max-width: none;
566
+ max-height: none;
567
+ overflow: visible;
568
+ `;
569
+ return createPreview(cloned, () => createSvgViewBoxAdapter(cloned), dispose);
570
+ }
571
+ /**
572
+ * Build the SVG adapter that uses viewBox for crisp scaling.
573
+ */
574
+ function createSvgViewBoxAdapter(svg) {
575
+ const prev = {
576
+ transform: svg.style.transform,
577
+ transformOrigin: svg.style.transformOrigin,
578
+ width: svg.style.width,
579
+ height: svg.style.height,
580
+ cursor: svg.style.cursor,
581
+ touchAction: svg.style.touchAction,
582
+ userSelect: svg.style.userSelect,
583
+ display: svg.style.display,
584
+ maxWidth: svg.style.maxWidth,
585
+ maxHeight: svg.style.maxHeight,
586
+ position: svg.style.position,
587
+ left: svg.style.left,
588
+ top: svg.style.top,
589
+ overflow: svg.style.overflow,
590
+ preserveAspectRatio: svg.getAttribute("preserveAspectRatio"),
591
+ viewBox: svg.getAttribute("viewBox"),
592
+ widthAttr: svg.getAttribute("width"),
593
+ heightAttr: svg.getAttribute("height")
594
+ };
595
+ const baseViewBox = measureSvgBaseViewBox(svg);
596
+ let viewBox = { ...baseViewBox };
597
+ let pinchStart = null;
598
+ function applyViewBox() {
599
+ svg.setAttribute("viewBox", `${viewBox.x} ${viewBox.y} ${viewBox.w} ${viewBox.h}`);
600
+ }
601
+ function fitToStage() {
602
+ svg.style.width = "100%";
603
+ svg.style.height = "100%";
604
+ svg.style.display = "block";
605
+ svg.style.maxWidth = "none";
606
+ svg.style.maxHeight = "none";
607
+ svg.style.overflow = "visible";
608
+ svg.style.position = "absolute";
609
+ svg.style.left = "0";
610
+ svg.style.top = "0";
611
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
612
+ viewBox = { ...baseViewBox };
613
+ applyViewBox();
614
+ }
615
+ function getRectSafe() {
616
+ const rect = svg.getBoundingClientRect();
617
+ return {
618
+ left: rect.left,
619
+ top: rect.top,
620
+ width: rect.width || 1,
621
+ height: rect.height || 1
622
+ };
623
+ }
624
+ function panBy(dx, dy) {
625
+ if (!isFiniteNumber(dx) || !isFiniteNumber(dy)) return;
626
+ const rect = getRectSafe();
627
+ const factor = Math.max(viewBox.w / rect.width, viewBox.h / rect.height);
628
+ viewBox.x -= dx * factor;
629
+ viewBox.y -= dy * factor;
630
+ applyViewBox();
631
+ }
632
+ function zoomAt(clientX, clientY, factor) {
633
+ if (!isFiniteNumber(factor) || factor <= 0) return;
634
+ const rect = getRectSafe();
635
+ const cx = (clientX - rect.left) / rect.width;
636
+ const cy = (clientY - rect.top) / rect.height;
637
+ const newW = viewBox.w * factor;
638
+ const newH = viewBox.h * factor;
639
+ viewBox.x += (viewBox.w - newW) * cx;
640
+ viewBox.y += (viewBox.h - newH) * cy;
641
+ viewBox.w = newW;
642
+ viewBox.h = newH;
643
+ applyViewBox();
644
+ }
645
+ function beginPinch(points) {
646
+ const [p1, p2] = points;
647
+ const midpoint = mid(p1, p2);
648
+ pinchStart = {
649
+ viewBox: { ...viewBox },
650
+ distance: dist(p1, p2),
651
+ midpoint
652
+ };
653
+ }
654
+ function updatePinch(points) {
655
+ if (!pinchStart) return;
656
+ const [p1, p2] = points;
657
+ const currentDistance = dist(p1, p2);
658
+ if (!isFiniteNumber(currentDistance) || currentDistance <= 0) return;
659
+ const currentMid = mid(p1, p2);
660
+ const scale = pinchStart.distance / currentDistance;
661
+ const newW = pinchStart.viewBox.w * scale;
662
+ const newH = pinchStart.viewBox.h * scale;
663
+ const rect = getRectSafe();
664
+ const startCx = (pinchStart.midpoint.x - rect.left) / rect.width;
665
+ const startCy = (pinchStart.midpoint.y - rect.top) / rect.height;
666
+ const currentCx = (currentMid.x - rect.left) / rect.width;
667
+ const currentCy = (currentMid.y - rect.top) / rect.height;
668
+ const anchorX = pinchStart.viewBox.x + pinchStart.viewBox.w * startCx;
669
+ const anchorY = pinchStart.viewBox.y + pinchStart.viewBox.h * startCy;
670
+ viewBox.w = newW;
671
+ viewBox.h = newH;
672
+ viewBox.x = anchorX - newW * currentCx;
673
+ viewBox.y = anchorY - newH * currentCy;
674
+ applyViewBox();
675
+ }
676
+ function zoomWithWheel(e) {
677
+ e.preventDefault();
678
+ zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * .0015));
679
+ }
680
+ function destroy() {
681
+ pinchStart = null;
682
+ }
683
+ function resetStyle() {
684
+ if (prev.transform !== void 0) svg.style.transform = prev.transform;
685
+ if (prev.transformOrigin !== void 0) svg.style.transformOrigin = prev.transformOrigin;
686
+ if (prev.width !== void 0) svg.style.width = prev.width;
687
+ if (prev.height !== void 0) svg.style.height = prev.height;
688
+ if (prev.cursor !== void 0) svg.style.cursor = prev.cursor;
689
+ if (prev.touchAction !== void 0) svg.style.touchAction = prev.touchAction;
690
+ if (prev.userSelect !== void 0) svg.style.userSelect = prev.userSelect;
691
+ if (prev.display !== void 0) svg.style.display = prev.display;
692
+ if (prev.maxWidth !== void 0) svg.style.maxWidth = prev.maxWidth;
693
+ if (prev.maxHeight !== void 0) svg.style.maxHeight = prev.maxHeight;
694
+ if (prev.position !== void 0) svg.style.position = prev.position;
695
+ if (prev.left !== void 0) svg.style.left = prev.left;
696
+ if (prev.top !== void 0) svg.style.top = prev.top;
697
+ if (prev.overflow !== void 0) svg.style.overflow = prev.overflow;
698
+ if (prev.preserveAspectRatio === null) svg.removeAttribute("preserveAspectRatio");
699
+ else svg.setAttribute("preserveAspectRatio", prev.preserveAspectRatio);
700
+ if (prev.viewBox === null) svg.removeAttribute("viewBox");
701
+ else svg.setAttribute("viewBox", prev.viewBox);
702
+ if (prev.widthAttr === null) svg.removeAttribute("width");
703
+ else svg.setAttribute("width", prev.widthAttr);
704
+ if (prev.heightAttr === null) svg.removeAttribute("height");
705
+ else svg.setAttribute("height", prev.heightAttr);
706
+ }
707
+ return {
708
+ fitToStage,
709
+ panBy,
710
+ zoomAt,
711
+ beginPinch,
712
+ updatePinch,
713
+ zoomWithWheel,
714
+ destroy,
715
+ resetStyle
716
+ };
717
+ }
718
+ //#endregion
719
+ export { createPreview, previewImage, previewSvg };