@embedpdf/web 3.0.0-next.5 → 3.0.0-next.7
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/index.cjs +843 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +342 -1
- package/dist/index.d.ts +342 -1
- package/dist/index.js +836 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -235,7 +235,847 @@ function sanitizeExternalUri(uri, allowedSchemes = DEFAULT_ALLOWED_SCHEMES) {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
//#endregion
|
|
238
|
+
//#region src/painted-image.ts
|
|
239
|
+
/**
|
|
240
|
+
* Bind a raster source to an <img> without ever painting its incomplete-image
|
|
241
|
+
* placeholder. The source becomes visible on load; retained coverage is
|
|
242
|
+
* released only after the browser has had one full frame to present it.
|
|
243
|
+
*
|
|
244
|
+
* Call this from the framework's pre-paint mount hook and invoke the returned
|
|
245
|
+
* cleanup on unmount. Keeping this DOM lifecycle here lets every framework
|
|
246
|
+
* adapter remain a ref plus one bind/unbind hook.
|
|
247
|
+
*/
|
|
248
|
+
function bindPaintedImage(image, source, callbacks) {
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
let revoke;
|
|
251
|
+
let frame = 0;
|
|
252
|
+
let painted = false;
|
|
253
|
+
image.style.visibility = "hidden";
|
|
254
|
+
const onLoad = () => {
|
|
255
|
+
image.style.visibility = "visible";
|
|
256
|
+
frame = requestAnimationFrame(() => {
|
|
257
|
+
frame = requestAnimationFrame(() => {
|
|
258
|
+
frame = 0;
|
|
259
|
+
painted = true;
|
|
260
|
+
callbacks.onPainted();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
image.addEventListener("load", onLoad, { once: true });
|
|
265
|
+
source.objectUrl(controller.signal).then((objectUrl) => {
|
|
266
|
+
if (controller.signal.aborted) {
|
|
267
|
+
objectUrl.revoke();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
revoke = objectUrl.revoke;
|
|
271
|
+
image.src = objectUrl.url;
|
|
272
|
+
}, () => {});
|
|
273
|
+
return () => {
|
|
274
|
+
controller.abort();
|
|
275
|
+
if (frame) cancelAnimationFrame(frame);
|
|
276
|
+
image.removeEventListener("load", onLoad);
|
|
277
|
+
image.style.visibility = "hidden";
|
|
278
|
+
image.removeAttribute("src");
|
|
279
|
+
revoke?.();
|
|
280
|
+
if (painted) callbacks.onUnpainted();
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/feedback.ts
|
|
285
|
+
const vibrate = (pattern) => {
|
|
286
|
+
if (typeof navigator !== "undefined") navigator.vibrate?.(pattern);
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* The default web provider: the Vibration API, mapped onto the three haptic
|
|
290
|
+
* families (selection = the shortest tick; impact scales with weight; notify
|
|
291
|
+
* uses patterns). A no-op wherever vibration is unsupported — notably iOS
|
|
292
|
+
* Safari — so it is always safe to install.
|
|
293
|
+
*/
|
|
294
|
+
const vibrationFeedback = {
|
|
295
|
+
selection: () => vibrate(8),
|
|
296
|
+
impact: (weight = "light") => vibrate(weight === "heavy" ? 30 : weight === "medium" ? 20 : 10),
|
|
297
|
+
notify: (kind) => vibrate(kind === "error" ? [
|
|
298
|
+
12,
|
|
299
|
+
60,
|
|
300
|
+
12,
|
|
301
|
+
60,
|
|
302
|
+
12
|
|
303
|
+
] : kind === "warning" ? [
|
|
304
|
+
15,
|
|
305
|
+
70,
|
|
306
|
+
15
|
|
307
|
+
] : [
|
|
308
|
+
10,
|
|
309
|
+
50,
|
|
310
|
+
10
|
|
311
|
+
])
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* A provider for native iOS shells: forwards each call to a WKWebView script
|
|
315
|
+
* message handler the HOST APP installed under `handlerName` — an explicit
|
|
316
|
+
* contract between the embedder and their own app, never a global this library
|
|
317
|
+
* sniffs on its own. The message shape is
|
|
318
|
+
* `{ family: 'selection' } | { family: 'impact', weight } | { family: 'notify', kind }`;
|
|
319
|
+
* the native side maps families onto the matching `UIFeedbackGenerator`.
|
|
320
|
+
* Calls fall back to `fallback` (default: {@link vibrationFeedback}) when the
|
|
321
|
+
* handler is absent, so one provider serves a hybrid web/app deployment.
|
|
322
|
+
*/
|
|
323
|
+
const wkFeedback = (handlerName, fallback = vibrationFeedback) => {
|
|
324
|
+
const handler = () => {
|
|
325
|
+
if (typeof window === "undefined") return void 0;
|
|
326
|
+
return window.webkit?.messageHandlers?.[handlerName];
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
selection: () => {
|
|
330
|
+
const h = handler();
|
|
331
|
+
if (h) h.postMessage({ family: "selection" });
|
|
332
|
+
else fallback.selection();
|
|
333
|
+
},
|
|
334
|
+
impact: (weight = "light") => {
|
|
335
|
+
const h = handler();
|
|
336
|
+
if (h) h.postMessage({
|
|
337
|
+
family: "impact",
|
|
338
|
+
weight
|
|
339
|
+
});
|
|
340
|
+
else fallback.impact(weight);
|
|
341
|
+
},
|
|
342
|
+
notify: (kind) => {
|
|
343
|
+
const h = handler();
|
|
344
|
+
if (h) h.postMessage({
|
|
345
|
+
family: "notify",
|
|
346
|
+
kind
|
|
347
|
+
});
|
|
348
|
+
else fallback.notify(kind);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/wheel.ts
|
|
354
|
+
/** The per-notch step for discrete wheels — the zoom buttons' ratio. */
|
|
355
|
+
const NOTCH = 1.2;
|
|
356
|
+
/** Zoom factor for a ctrl/cmd wheel event (the caller decides zoom vs pan). */
|
|
357
|
+
function wheelZoomFactor(e) {
|
|
358
|
+
if (e.deltaMode !== 0) return Math.pow(NOTCH, -Math.sign(e.deltaY));
|
|
359
|
+
if (e.ctrlKey) {
|
|
360
|
+
if (Math.abs(e.deltaY) >= 100) return Math.pow(NOTCH, -Math.sign(e.deltaY));
|
|
361
|
+
return Math.exp(-e.deltaY / 100);
|
|
362
|
+
}
|
|
363
|
+
return Math.exp(-e.deltaY * .0015);
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/stage-gestures.ts
|
|
367
|
+
/**
|
|
368
|
+
* Stage gesture controller — the ONE DOM input binding for a Stage surface,
|
|
369
|
+
* shared by every framework adapter (React, Angular, …) so the feel can never
|
|
370
|
+
* drift between them.
|
|
371
|
+
*
|
|
372
|
+
* The premise (see the stage plugin's camera doctrine): desktop inputs arrive
|
|
373
|
+
* with physics already applied by the OS — momentum lives in the wheel stream,
|
|
374
|
+
* a trackpad pinch is one pre-arbitrated gesture stream. Touch arrives RAW:
|
|
375
|
+
* with `touch-action: none` the platform scroller is out of the loop, so the
|
|
376
|
+
* ballistics (velocity → fling) and the arbitration (is this contact a scroll,
|
|
377
|
+
* a pinch, a tap, or a tool gesture?) must be synthesized here.
|
|
378
|
+
*
|
|
379
|
+
* Arbitration is MODALITY-AWARE:
|
|
380
|
+
* - touch — navigation-first: one finger pans (whatever tool is armed), two
|
|
381
|
+
* fingers pinch-zoom around their centroid, release velocity flings, a tap
|
|
382
|
+
* forwards as a click, a double-tap zoom-toggles, a long-press hands the
|
|
383
|
+
* gesture to the interaction hub (text selection), and a second finger
|
|
384
|
+
* landing mid-tool-gesture CANCELS it into a pinch (the Notes/Procreate
|
|
385
|
+
* convention).
|
|
386
|
+
* - mouse/pen — tool-first, exactly the pre-existing behavior: with a hub,
|
|
387
|
+
* every down/move/up forwards (pan is the pan tool's job); without one,
|
|
388
|
+
* dragging pans. Wheel and Safari-trackpad gesture events are unchanged.
|
|
389
|
+
*
|
|
390
|
+
* Camera writes are rAF-COALESCED: pointer events only update gesture state;
|
|
391
|
+
* one animation-frame tick applies at most one pan and one zoom per frame,
|
|
392
|
+
* inside the host's begin/endGesture transaction. Events may arrive at 120 Hz;
|
|
393
|
+
* the camera moves at display rate.
|
|
394
|
+
*
|
|
395
|
+
* Dependency note: this module speaks to the stage through the STRUCTURAL
|
|
396
|
+
* {@link StageGestureHost} interface (satisfied by `StageCapability`) and to
|
|
397
|
+
* the interaction hub through {@link StageGestureSink} (a closure the adapter
|
|
398
|
+
* builds) — @embedpdf/web stays free of plugin imports, per the layering law.
|
|
399
|
+
*/
|
|
400
|
+
/**
|
|
401
|
+
* Release velocity from a trail of pointer samples: the mean velocity over the
|
|
402
|
+
* trailing `windowMs` (first-to-last inside the window). Null when the trail is
|
|
403
|
+
* too thin or too stale to trust — the standard "held still, then let go"
|
|
404
|
+
* case, which must NOT fling. Pure; exported for tests.
|
|
405
|
+
*/
|
|
406
|
+
function computeReleaseVelocity(samples, now, windowMs = 100) {
|
|
407
|
+
let firstIdx = -1;
|
|
408
|
+
for (let i = 0; i < samples.length; i++) if (now - samples[i].t <= windowMs) {
|
|
409
|
+
firstIdx = i;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
if (firstIdx < 0 || firstIdx === samples.length - 1) return null;
|
|
413
|
+
const first = samples[firstIdx];
|
|
414
|
+
const last = samples[samples.length - 1];
|
|
415
|
+
const dt = last.t - first.t;
|
|
416
|
+
if (dt < 10) return null;
|
|
417
|
+
return {
|
|
418
|
+
vx: (last.x - first.x) / dt * 1e3,
|
|
419
|
+
vy: (last.y - first.y) / dt * 1e3
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/** Attach the gesture controller to a Stage container. Returns the detach fn. */
|
|
423
|
+
function createStageGestureController(el, host, options) {
|
|
424
|
+
const zoomGestures = options.zoomGestures ?? true;
|
|
425
|
+
const wheelZoom = options.wheelZoomFactor ?? wheelZoomFactor;
|
|
426
|
+
const sink = options.sink ?? null;
|
|
427
|
+
const LONG_PRESS_MS = options.longPressMs ?? 450;
|
|
428
|
+
const SLOP = options.tapSlopPx ?? 10;
|
|
429
|
+
const DOUBLE_TAP_MS = options.doubleTapMs ?? 300;
|
|
430
|
+
const DOUBLE_TAP_RADIUS = 25;
|
|
431
|
+
const FLING_MIN = options.flingMinVelocity ?? 50;
|
|
432
|
+
const MIN_PINCH_SPAN = 20;
|
|
433
|
+
const PINCH_RELEASE_GRACE_MS = 160;
|
|
434
|
+
const SETTLE_SPEED = .12;
|
|
435
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
436
|
+
let mode = "idle";
|
|
437
|
+
let began = false;
|
|
438
|
+
let suppressTap = false;
|
|
439
|
+
let touchToolGesture = false;
|
|
440
|
+
let downEvent = null;
|
|
441
|
+
let longPressTimer = null;
|
|
442
|
+
let panId = -1;
|
|
443
|
+
let panArmed = true;
|
|
444
|
+
let panGraceUntil = 0;
|
|
445
|
+
let graceSample = {
|
|
446
|
+
t: 0,
|
|
447
|
+
x: 0,
|
|
448
|
+
y: 0
|
|
449
|
+
};
|
|
450
|
+
let lastTouchAt = -Infinity;
|
|
451
|
+
let frame = 0;
|
|
452
|
+
let dirty = false;
|
|
453
|
+
let lastApplied = {
|
|
454
|
+
x: 0,
|
|
455
|
+
y: 0
|
|
456
|
+
};
|
|
457
|
+
let lastSpan = 0;
|
|
458
|
+
let trail = [];
|
|
459
|
+
let spanTrail = [];
|
|
460
|
+
let lastTapT = 0;
|
|
461
|
+
let lastTapX = 0;
|
|
462
|
+
let lastTapY = 0;
|
|
463
|
+
let mLast = 0;
|
|
464
|
+
let mX = 0;
|
|
465
|
+
let mY = 0;
|
|
466
|
+
let mCount = 0;
|
|
467
|
+
const clickCount = (e) => {
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
mCount = now - mLast <= 400 && Math.hypot(e.clientX - mX, e.clientY - mY) <= 6 ? mCount + 1 : 1;
|
|
470
|
+
mLast = now;
|
|
471
|
+
mX = e.clientX;
|
|
472
|
+
mY = e.clientY;
|
|
473
|
+
return mCount;
|
|
474
|
+
};
|
|
475
|
+
const vpt = (clientX, clientY) => {
|
|
476
|
+
const r = el.getBoundingClientRect();
|
|
477
|
+
return {
|
|
478
|
+
x: clientX - r.left,
|
|
479
|
+
y: clientY - r.top
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
const touches = () => {
|
|
483
|
+
const out = [];
|
|
484
|
+
pointers.forEach((p) => {
|
|
485
|
+
if (p.kind === "touch") out.push(p);
|
|
486
|
+
});
|
|
487
|
+
return out;
|
|
488
|
+
};
|
|
489
|
+
const clearLongPress = () => {
|
|
490
|
+
if (longPressTimer !== null) {
|
|
491
|
+
clearTimeout(longPressTimer);
|
|
492
|
+
longPressTimer = null;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const stopFrames = () => {
|
|
496
|
+
if (frame) {
|
|
497
|
+
cancelAnimationFrame(frame);
|
|
498
|
+
frame = 0;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const begin = (elastic = false) => {
|
|
502
|
+
if (!began) {
|
|
503
|
+
began = true;
|
|
504
|
+
host.beginGesture(elastic ? { elastic: true } : void 0);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const end = () => {
|
|
508
|
+
if (began) {
|
|
509
|
+
began = false;
|
|
510
|
+
host.endGesture();
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const toIdle = () => {
|
|
514
|
+
mode = "idle";
|
|
515
|
+
downEvent = null;
|
|
516
|
+
touchToolGesture = false;
|
|
517
|
+
trail = [];
|
|
518
|
+
spanTrail = [];
|
|
519
|
+
dirty = false;
|
|
520
|
+
panId = -1;
|
|
521
|
+
panArmed = true;
|
|
522
|
+
panGraceUntil = 0;
|
|
523
|
+
clearLongPress();
|
|
524
|
+
stopFrames();
|
|
525
|
+
};
|
|
526
|
+
const pushSample = (x, y) => {
|
|
527
|
+
trail.push({
|
|
528
|
+
t: performance.now(),
|
|
529
|
+
x,
|
|
530
|
+
y
|
|
531
|
+
});
|
|
532
|
+
if (trail.length > 16) trail.shift();
|
|
533
|
+
};
|
|
534
|
+
const maybeFling = () => {
|
|
535
|
+
const v = computeReleaseVelocity(trail, performance.now());
|
|
536
|
+
if (v && Math.hypot(v.vx, v.vy) >= FLING_MIN) host.fling(v.vx, v.vy);
|
|
537
|
+
};
|
|
538
|
+
const flushPanTo = (x, y) => {
|
|
539
|
+
const dx = x - lastApplied.x;
|
|
540
|
+
const dy = y - lastApplied.y;
|
|
541
|
+
if (dx !== 0 || dy !== 0) host.panBy(dx, dy);
|
|
542
|
+
lastApplied = {
|
|
543
|
+
x,
|
|
544
|
+
y
|
|
545
|
+
};
|
|
546
|
+
dirty = false;
|
|
547
|
+
};
|
|
548
|
+
const flushPinch = (ax, ay, bx, by) => {
|
|
549
|
+
const cx = (ax + bx) / 2;
|
|
550
|
+
const cy = (ay + by) / 2;
|
|
551
|
+
const span = Math.hypot(ax - bx, ay - by);
|
|
552
|
+
const dx = cx - lastApplied.x;
|
|
553
|
+
const dy = cy - lastApplied.y;
|
|
554
|
+
if (dx !== 0 || dy !== 0) host.panBy(dx, dy);
|
|
555
|
+
if (zoomGestures && span > MIN_PINCH_SPAN && lastSpan > MIN_PINCH_SPAN && span !== lastSpan) host.zoomAround(vpt(cx, cy), span / lastSpan);
|
|
556
|
+
pushSample(cx, cy);
|
|
557
|
+
spanTrail.push({
|
|
558
|
+
t: performance.now(),
|
|
559
|
+
x: span,
|
|
560
|
+
y: 0
|
|
561
|
+
});
|
|
562
|
+
if (spanTrail.length > 16) spanTrail.shift();
|
|
563
|
+
lastApplied = {
|
|
564
|
+
x: cx,
|
|
565
|
+
y: cy
|
|
566
|
+
};
|
|
567
|
+
lastSpan = span;
|
|
568
|
+
dirty = false;
|
|
569
|
+
};
|
|
570
|
+
const tick = () => {
|
|
571
|
+
frame = 0;
|
|
572
|
+
if (mode === "pan" || mode === "mousedrag") {
|
|
573
|
+
const p = pointers.get(panId);
|
|
574
|
+
if (p && dirty) flushPanTo(p.x, p.y);
|
|
575
|
+
frame = requestAnimationFrame(tick);
|
|
576
|
+
} else if (mode === "pinch") {
|
|
577
|
+
const [a, b] = touches();
|
|
578
|
+
if (a && b && dirty) flushPinch(a.x, a.y, b.x, b.y);
|
|
579
|
+
frame = requestAnimationFrame(tick);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
const ensureFrames = () => {
|
|
583
|
+
if (!frame) frame = requestAnimationFrame(tick);
|
|
584
|
+
};
|
|
585
|
+
const enterPinch = () => {
|
|
586
|
+
const [a, b] = touches();
|
|
587
|
+
if (!a || !b) return;
|
|
588
|
+
mode = "pinch";
|
|
589
|
+
lastApplied = {
|
|
590
|
+
x: (a.x + b.x) / 2,
|
|
591
|
+
y: (a.y + b.y) / 2
|
|
592
|
+
};
|
|
593
|
+
lastSpan = Math.hypot(a.x - b.x, a.y - b.y);
|
|
594
|
+
trail = [];
|
|
595
|
+
dirty = false;
|
|
596
|
+
ensureFrames();
|
|
597
|
+
};
|
|
598
|
+
const onLongPress = () => {
|
|
599
|
+
longPressTimer = null;
|
|
600
|
+
if (mode !== "pending" || !downEvent || !sink) return;
|
|
601
|
+
end();
|
|
602
|
+
mode = "tool";
|
|
603
|
+
touchToolGesture = true;
|
|
604
|
+
sink.longPress(downEvent);
|
|
605
|
+
};
|
|
606
|
+
const onDown = (e) => {
|
|
607
|
+
const kind = e.pointerType || "mouse";
|
|
608
|
+
if (kind === "touch") lastTouchAt = performance.now();
|
|
609
|
+
if (kind !== "touch") {
|
|
610
|
+
if (kind === "mouse" && e.button !== 0) return;
|
|
611
|
+
if (mode !== "idle") return;
|
|
612
|
+
pointers.set(e.pointerId, {
|
|
613
|
+
id: e.pointerId,
|
|
614
|
+
kind,
|
|
615
|
+
x: e.clientX,
|
|
616
|
+
y: e.clientY,
|
|
617
|
+
downX: e.clientX,
|
|
618
|
+
downY: e.clientY
|
|
619
|
+
});
|
|
620
|
+
if (sink) {
|
|
621
|
+
mode = "tool";
|
|
622
|
+
sink.down(e, clickCount(e));
|
|
623
|
+
} else {
|
|
624
|
+
mode = "mousedrag";
|
|
625
|
+
begin();
|
|
626
|
+
panId = e.pointerId;
|
|
627
|
+
lastApplied = {
|
|
628
|
+
x: e.clientX,
|
|
629
|
+
y: e.clientY
|
|
630
|
+
};
|
|
631
|
+
ensureFrames();
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
switch (mode) {
|
|
636
|
+
case "idle": {
|
|
637
|
+
pointers.set(e.pointerId, {
|
|
638
|
+
id: e.pointerId,
|
|
639
|
+
kind,
|
|
640
|
+
x: e.clientX,
|
|
641
|
+
y: e.clientY,
|
|
642
|
+
downX: e.clientX,
|
|
643
|
+
downY: e.clientY
|
|
644
|
+
});
|
|
645
|
+
const moving = host.cameraInMotion();
|
|
646
|
+
if (!moving && sink?.claimsPoint?.(e)) {
|
|
647
|
+
mode = "tool";
|
|
648
|
+
touchToolGesture = true;
|
|
649
|
+
sink.down(e, 1);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
mode = "pending";
|
|
653
|
+
downEvent = e;
|
|
654
|
+
suppressTap = moving;
|
|
655
|
+
begin(true);
|
|
656
|
+
trail = [];
|
|
657
|
+
pushSample(e.clientX, e.clientY);
|
|
658
|
+
clearLongPress();
|
|
659
|
+
if (sink && !suppressTap) longPressTimer = setTimeout(onLongPress, LONG_PRESS_MS);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case "pending":
|
|
663
|
+
case "pan":
|
|
664
|
+
pointers.set(e.pointerId, {
|
|
665
|
+
id: e.pointerId,
|
|
666
|
+
kind,
|
|
667
|
+
x: e.clientX,
|
|
668
|
+
y: e.clientY,
|
|
669
|
+
downX: e.clientX,
|
|
670
|
+
downY: e.clientY
|
|
671
|
+
});
|
|
672
|
+
clearLongPress();
|
|
673
|
+
enterPinch();
|
|
674
|
+
break;
|
|
675
|
+
case "tool":
|
|
676
|
+
if (!touchToolGesture) return;
|
|
677
|
+
sink?.cancel(e);
|
|
678
|
+
touchToolGesture = false;
|
|
679
|
+
pointers.set(e.pointerId, {
|
|
680
|
+
id: e.pointerId,
|
|
681
|
+
kind,
|
|
682
|
+
x: e.clientX,
|
|
683
|
+
y: e.clientY,
|
|
684
|
+
downX: e.clientX,
|
|
685
|
+
downY: e.clientY
|
|
686
|
+
});
|
|
687
|
+
begin(true);
|
|
688
|
+
enterPinch();
|
|
689
|
+
break;
|
|
690
|
+
case "pinch":
|
|
691
|
+
case "mousedrag": return;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
const onWindowMove = (e) => {
|
|
695
|
+
const p = pointers.get(e.pointerId);
|
|
696
|
+
if (!p) return;
|
|
697
|
+
if (p.kind === "touch") lastTouchAt = performance.now();
|
|
698
|
+
p.x = e.clientX;
|
|
699
|
+
p.y = e.clientY;
|
|
700
|
+
switch (mode) {
|
|
701
|
+
case "pending":
|
|
702
|
+
pushSample(p.x, p.y);
|
|
703
|
+
if (Math.hypot(p.x - p.downX, p.y - p.downY) > SLOP) {
|
|
704
|
+
clearLongPress();
|
|
705
|
+
mode = "pan";
|
|
706
|
+
panId = p.id;
|
|
707
|
+
lastApplied = {
|
|
708
|
+
x: p.x,
|
|
709
|
+
y: p.y
|
|
710
|
+
};
|
|
711
|
+
ensureFrames();
|
|
712
|
+
}
|
|
713
|
+
break;
|
|
714
|
+
case "pan":
|
|
715
|
+
if (p.id !== panId) break;
|
|
716
|
+
if (!panArmed) {
|
|
717
|
+
const nowT = performance.now();
|
|
718
|
+
if (nowT < panGraceUntil) {
|
|
719
|
+
const dt = nowT - graceSample.t;
|
|
720
|
+
if (dt >= 8) {
|
|
721
|
+
const speed = Math.hypot(p.x - graceSample.x, p.y - graceSample.y) / dt;
|
|
722
|
+
graceSample = {
|
|
723
|
+
t: nowT,
|
|
724
|
+
x: p.x,
|
|
725
|
+
y: p.y
|
|
726
|
+
};
|
|
727
|
+
if (speed < SETTLE_SPEED) panGraceUntil = 0;
|
|
728
|
+
}
|
|
729
|
+
p.downX = p.x;
|
|
730
|
+
p.downY = p.y;
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
if (Math.hypot(p.x - p.downX, p.y - p.downY) <= SLOP) break;
|
|
734
|
+
panArmed = true;
|
|
735
|
+
lastApplied = {
|
|
736
|
+
x: p.x,
|
|
737
|
+
y: p.y
|
|
738
|
+
};
|
|
739
|
+
trail = [];
|
|
740
|
+
spanTrail = [];
|
|
741
|
+
}
|
|
742
|
+
pushSample(p.x, p.y);
|
|
743
|
+
dirty = true;
|
|
744
|
+
break;
|
|
745
|
+
case "pinch":
|
|
746
|
+
dirty = true;
|
|
747
|
+
break;
|
|
748
|
+
case "tool":
|
|
749
|
+
sink?.move(e);
|
|
750
|
+
break;
|
|
751
|
+
case "mousedrag":
|
|
752
|
+
if (p.id === panId) dirty = true;
|
|
753
|
+
break;
|
|
754
|
+
case "idle": break;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
const onHoverMove = (e) => {
|
|
758
|
+
if (mode === "idle" && sink) sink.hover(e);
|
|
759
|
+
};
|
|
760
|
+
const backToSingleFinger = () => {
|
|
761
|
+
const rest = touches();
|
|
762
|
+
if (rest.length !== 1) return false;
|
|
763
|
+
mode = "pan";
|
|
764
|
+
panId = rest[0].id;
|
|
765
|
+
panArmed = false;
|
|
766
|
+
panGraceUntil = performance.now() + PINCH_RELEASE_GRACE_MS;
|
|
767
|
+
graceSample = {
|
|
768
|
+
t: performance.now(),
|
|
769
|
+
x: rest[0].x,
|
|
770
|
+
y: rest[0].y
|
|
771
|
+
};
|
|
772
|
+
rest[0].downX = rest[0].x;
|
|
773
|
+
rest[0].downY = rest[0].y;
|
|
774
|
+
lastApplied = {
|
|
775
|
+
x: rest[0].x,
|
|
776
|
+
y: rest[0].y
|
|
777
|
+
};
|
|
778
|
+
trail = [];
|
|
779
|
+
dirty = false;
|
|
780
|
+
return true;
|
|
781
|
+
};
|
|
782
|
+
const onUp = (e) => {
|
|
783
|
+
const p = pointers.get(e.pointerId);
|
|
784
|
+
if (!p) return;
|
|
785
|
+
if (p.kind === "touch") lastTouchAt = performance.now();
|
|
786
|
+
pointers.delete(e.pointerId);
|
|
787
|
+
switch (mode) {
|
|
788
|
+
case "pending": {
|
|
789
|
+
clearLongPress();
|
|
790
|
+
end();
|
|
791
|
+
const now = performance.now();
|
|
792
|
+
const isDouble = now - lastTapT <= DOUBLE_TAP_MS && Math.hypot(e.clientX - lastTapX, e.clientY - lastTapY) <= DOUBLE_TAP_RADIUS;
|
|
793
|
+
if (suppressTap) lastTapT = 0;
|
|
794
|
+
else if (zoomGestures && isDouble) {
|
|
795
|
+
lastTapT = 0;
|
|
796
|
+
host.doubleTapZoom(vpt(e.clientX, e.clientY));
|
|
797
|
+
} else {
|
|
798
|
+
lastTapT = now;
|
|
799
|
+
lastTapX = e.clientX;
|
|
800
|
+
lastTapY = e.clientY;
|
|
801
|
+
if (sink && downEvent) {
|
|
802
|
+
sink.down(downEvent, 1);
|
|
803
|
+
sink.up(e);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
toIdle();
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
case "pan":
|
|
810
|
+
if (!panArmed) {
|
|
811
|
+
const now = performance.now();
|
|
812
|
+
const v = computeReleaseVelocity(trail, now);
|
|
813
|
+
const vs = computeReleaseVelocity(spanTrail, now);
|
|
814
|
+
end();
|
|
815
|
+
if (v) {
|
|
816
|
+
const speed = Math.hypot(v.vx, v.vy);
|
|
817
|
+
if (speed >= FLING_MIN && speed > Math.abs(vs?.vx ?? 0)) host.fling(v.vx, v.vy);
|
|
818
|
+
}
|
|
819
|
+
toIdle();
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
flushPanTo(e.clientX, e.clientY);
|
|
823
|
+
pushSample(e.clientX, e.clientY);
|
|
824
|
+
end();
|
|
825
|
+
maybeFling();
|
|
826
|
+
toIdle();
|
|
827
|
+
break;
|
|
828
|
+
case "pinch": {
|
|
829
|
+
const rest = touches();
|
|
830
|
+
if (rest.length === 1) {
|
|
831
|
+
mode = "pan";
|
|
832
|
+
panId = rest[0].id;
|
|
833
|
+
panArmed = false;
|
|
834
|
+
panGraceUntil = performance.now() + PINCH_RELEASE_GRACE_MS;
|
|
835
|
+
graceSample = {
|
|
836
|
+
t: performance.now(),
|
|
837
|
+
x: rest[0].x,
|
|
838
|
+
y: rest[0].y
|
|
839
|
+
};
|
|
840
|
+
rest[0].downX = rest[0].x;
|
|
841
|
+
rest[0].downY = rest[0].y;
|
|
842
|
+
lastApplied = {
|
|
843
|
+
x: rest[0].x,
|
|
844
|
+
y: rest[0].y
|
|
845
|
+
};
|
|
846
|
+
dirty = false;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
end();
|
|
850
|
+
maybeFling();
|
|
851
|
+
toIdle();
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
case "tool":
|
|
855
|
+
sink?.up(e);
|
|
856
|
+
toIdle();
|
|
857
|
+
break;
|
|
858
|
+
case "mousedrag":
|
|
859
|
+
if (p.id !== panId) break;
|
|
860
|
+
flushPanTo(e.clientX, e.clientY);
|
|
861
|
+
end();
|
|
862
|
+
toIdle();
|
|
863
|
+
break;
|
|
864
|
+
case "idle": break;
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
const onCancel = (e) => {
|
|
868
|
+
if (!pointers.get(e.pointerId)) return;
|
|
869
|
+
pointers.delete(e.pointerId);
|
|
870
|
+
if (mode === "tool") {
|
|
871
|
+
sink?.cancel(e);
|
|
872
|
+
toIdle();
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (mode === "pinch" && backToSingleFinger()) return;
|
|
876
|
+
end();
|
|
877
|
+
toIdle();
|
|
878
|
+
};
|
|
879
|
+
const onWheel = (e) => {
|
|
880
|
+
e.preventDefault();
|
|
881
|
+
if (zoomGestures && (e.ctrlKey || e.metaKey)) host.zoomAround(vpt(e.clientX, e.clientY), wheelZoom(e));
|
|
882
|
+
else {
|
|
883
|
+
const dx = e.shiftKey ? e.deltaY : e.deltaX;
|
|
884
|
+
const dy = e.shiftKey ? e.deltaX : e.deltaY;
|
|
885
|
+
host.panBy(-dx, -dy);
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
let lastScale = 1;
|
|
889
|
+
const onGestureStart = (e) => {
|
|
890
|
+
e.preventDefault();
|
|
891
|
+
lastScale = e.scale ?? 1;
|
|
892
|
+
};
|
|
893
|
+
const onGestureChange = (e) => {
|
|
894
|
+
e.preventDefault();
|
|
895
|
+
if (touches().length > 0 || performance.now() - lastTouchAt < 500) return;
|
|
896
|
+
const g = e;
|
|
897
|
+
const scale = g.scale ?? 1;
|
|
898
|
+
if (zoomGestures && scale > 0) host.zoomAround(vpt(g.clientX, g.clientY), scale / lastScale);
|
|
899
|
+
lastScale = scale;
|
|
900
|
+
};
|
|
901
|
+
el.addEventListener("pointerdown", onDown);
|
|
902
|
+
el.addEventListener("pointermove", onHoverMove);
|
|
903
|
+
window.addEventListener("pointermove", onWindowMove);
|
|
904
|
+
window.addEventListener("pointerup", onUp);
|
|
905
|
+
window.addEventListener("pointercancel", onCancel);
|
|
906
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
907
|
+
const hasGestureEvents = "GestureEvent" in window;
|
|
908
|
+
if (hasGestureEvents) {
|
|
909
|
+
el.addEventListener("gesturestart", onGestureStart);
|
|
910
|
+
el.addEventListener("gesturechange", onGestureChange);
|
|
911
|
+
el.addEventListener("gestureend", onGestureStart);
|
|
912
|
+
}
|
|
913
|
+
return () => {
|
|
914
|
+
el.removeEventListener("pointerdown", onDown);
|
|
915
|
+
el.removeEventListener("pointermove", onHoverMove);
|
|
916
|
+
window.removeEventListener("pointermove", onWindowMove);
|
|
917
|
+
window.removeEventListener("pointerup", onUp);
|
|
918
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
919
|
+
el.removeEventListener("wheel", onWheel);
|
|
920
|
+
if (hasGestureEvents) {
|
|
921
|
+
el.removeEventListener("gesturestart", onGestureStart);
|
|
922
|
+
el.removeEventListener("gesturechange", onGestureChange);
|
|
923
|
+
el.removeEventListener("gestureend", onGestureStart);
|
|
924
|
+
}
|
|
925
|
+
clearLongPress();
|
|
926
|
+
stopFrames();
|
|
927
|
+
end();
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region src/selection-handles.ts
|
|
932
|
+
/** Bind one handle element. Returns the detach fn. */
|
|
933
|
+
function attachSelectionHandle(el, options) {
|
|
934
|
+
let active = null;
|
|
935
|
+
const onMove = (e) => {
|
|
936
|
+
if (!active || e.pointerId !== active.pointerId) return;
|
|
937
|
+
active.session.move({
|
|
938
|
+
x: active.base.x + (e.clientX - active.baseClient.x),
|
|
939
|
+
y: active.base.y + (e.clientY - active.baseClient.y)
|
|
940
|
+
});
|
|
941
|
+
};
|
|
942
|
+
const onUp = (e) => {
|
|
943
|
+
if (!active || e.pointerId !== active.pointerId) return;
|
|
944
|
+
const s = active.session;
|
|
945
|
+
active = null;
|
|
946
|
+
s.end();
|
|
947
|
+
};
|
|
948
|
+
const onDown = (e) => {
|
|
949
|
+
if (active) return;
|
|
950
|
+
const armed = options.arm();
|
|
951
|
+
if (!armed) return;
|
|
952
|
+
e.preventDefault();
|
|
953
|
+
e.stopPropagation();
|
|
954
|
+
try {
|
|
955
|
+
el.setPointerCapture(e.pointerId);
|
|
956
|
+
} catch {}
|
|
957
|
+
active = {
|
|
958
|
+
session: armed.session,
|
|
959
|
+
base: armed.base,
|
|
960
|
+
baseClient: {
|
|
961
|
+
x: e.clientX,
|
|
962
|
+
y: e.clientY
|
|
963
|
+
},
|
|
964
|
+
pointerId: e.pointerId
|
|
965
|
+
};
|
|
966
|
+
};
|
|
967
|
+
el.addEventListener("pointerdown", onDown);
|
|
968
|
+
el.addEventListener("pointermove", onMove);
|
|
969
|
+
el.addEventListener("pointerup", onUp);
|
|
970
|
+
el.addEventListener("pointercancel", onUp);
|
|
971
|
+
return () => {
|
|
972
|
+
el.removeEventListener("pointerdown", onDown);
|
|
973
|
+
el.removeEventListener("pointermove", onMove);
|
|
974
|
+
el.removeEventListener("pointerup", onUp);
|
|
975
|
+
el.removeEventListener("pointercancel", onUp);
|
|
976
|
+
active = null;
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/stage-surface.ts
|
|
981
|
+
/**
|
|
982
|
+
* Stage surface binding — everything DOM about attaching one element to one
|
|
983
|
+
* stage lens, shared by every framework adapter (React, Angular, …) so the
|
|
984
|
+
* binding can never drift between them:
|
|
985
|
+
*
|
|
986
|
+
* • ResizeObserver → `setViewport` (the shell only reports; placement is the
|
|
987
|
+
* stage plugin's job)
|
|
988
|
+
* • device-pixel-ratio observation → `setDevicePixelRatio` (dppx changes
|
|
989
|
+
* re-subscribe, since the media query's value itself moves)
|
|
990
|
+
* • pointer-sample normalization — the ONE place a PointerEvent becomes a
|
|
991
|
+
* page-resolved, source-stamped sample for the interaction hub
|
|
992
|
+
* • the gesture controller (wheel, Safari trackpad gestures, synthesized
|
|
993
|
+
* touch physics) from `./stage-gestures`
|
|
994
|
+
*
|
|
995
|
+
* The framework adapters keep only their reactive glue: refs/effects, page
|
|
996
|
+
* surfaces, cursor-style subscription, overlay slots.
|
|
997
|
+
*
|
|
998
|
+
* Dependency note: like the gesture controller, this module speaks to the
|
|
999
|
+
* stage and the hub through STRUCTURAL interfaces ({@link StageSurfaceHost},
|
|
1000
|
+
* {@link StageSurfaceHub}) — satisfied by `StageCapability` and
|
|
1001
|
+
* `InteractionCapability`, imported by neither. @embedpdf/web stays free of
|
|
1002
|
+
* plugin imports, per the layering law.
|
|
1003
|
+
*
|
|
1004
|
+
* Browser-only by nature (ResizeObserver, matchMedia): call from a mounted
|
|
1005
|
+
* effect / browser platform check, exactly like the gesture controller.
|
|
1006
|
+
*/
|
|
1007
|
+
/** Attach the full surface binding. Returns the detach fn. */
|
|
1008
|
+
function createStageSurface(el, stage, options = {}) {
|
|
1009
|
+
const hub = options.hub ?? null;
|
|
1010
|
+
const cleanups = [];
|
|
1011
|
+
const setViewport = () => stage.setViewport({
|
|
1012
|
+
width: el.clientWidth,
|
|
1013
|
+
height: el.clientHeight
|
|
1014
|
+
});
|
|
1015
|
+
const ro = new ResizeObserver(setViewport);
|
|
1016
|
+
ro.observe(el);
|
|
1017
|
+
setViewport();
|
|
1018
|
+
cleanups.push(() => ro.disconnect());
|
|
1019
|
+
let mq = null;
|
|
1020
|
+
const reportDpr = () => {
|
|
1021
|
+
stage.setDevicePixelRatio(window.devicePixelRatio || 1);
|
|
1022
|
+
mq?.removeEventListener("change", reportDpr);
|
|
1023
|
+
mq = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
1024
|
+
mq.addEventListener("change", reportDpr);
|
|
1025
|
+
};
|
|
1026
|
+
reportDpr();
|
|
1027
|
+
cleanups.push(() => mq?.removeEventListener("change", reportDpr));
|
|
1028
|
+
const sampleOf = (phase, e, clickCount = 1, gesture) => {
|
|
1029
|
+
const r = el.getBoundingClientRect();
|
|
1030
|
+
const viewport = {
|
|
1031
|
+
x: e.clientX - r.left,
|
|
1032
|
+
y: e.clientY - r.top
|
|
1033
|
+
};
|
|
1034
|
+
return {
|
|
1035
|
+
phase,
|
|
1036
|
+
viewport,
|
|
1037
|
+
page: stage.pageAt(viewport) ?? void 0,
|
|
1038
|
+
project: (pon) => stage.pointOnPage(pon, viewport),
|
|
1039
|
+
modifiers: {
|
|
1040
|
+
shift: e.shiftKey,
|
|
1041
|
+
alt: e.altKey,
|
|
1042
|
+
ctrl: e.ctrlKey,
|
|
1043
|
+
meta: e.metaKey
|
|
1044
|
+
},
|
|
1045
|
+
clickCount,
|
|
1046
|
+
pointerType: e.pointerType || "mouse",
|
|
1047
|
+
...gesture ? { gesture } : {},
|
|
1048
|
+
...options.source ? { source: options.source } : {}
|
|
1049
|
+
};
|
|
1050
|
+
};
|
|
1051
|
+
const forward = (phase, e, clickCount = 1, gesture) => {
|
|
1052
|
+
hub?.dispatch(sampleOf(phase, e, clickCount, gesture));
|
|
1053
|
+
};
|
|
1054
|
+
const sink = hub ? {
|
|
1055
|
+
down: (e, clickCount) => forward("down", e, clickCount),
|
|
1056
|
+
move: (e) => forward("move", e),
|
|
1057
|
+
up: (e) => forward("up", e),
|
|
1058
|
+
cancel: (e) => forward("cancel", e),
|
|
1059
|
+
hover: (e) => forward("move", e),
|
|
1060
|
+
longPress: (e) => forward("down", e, 2, "long-press"),
|
|
1061
|
+
claimsPoint: (e) => !!hub.activeTool().touchDirect || hub.wouldClaimTouch(sampleOf("down", e))
|
|
1062
|
+
} : null;
|
|
1063
|
+
cleanups.push(createStageGestureController(el, stage, {
|
|
1064
|
+
zoomGestures: options.zoomGestures,
|
|
1065
|
+
wheelZoomFactor: options.wheelZoomFactor,
|
|
1066
|
+
sink
|
|
1067
|
+
}));
|
|
1068
|
+
return () => {
|
|
1069
|
+
for (const fn of cleanups) fn();
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
//#endregion
|
|
1073
|
+
exports.attachSelectionHandle = attachSelectionHandle;
|
|
1074
|
+
exports.bindPaintedImage = bindPaintedImage;
|
|
1075
|
+
exports.computeReleaseVelocity = computeReleaseVelocity;
|
|
238
1076
|
exports.copySelection = copySelection;
|
|
1077
|
+
exports.createStageGestureController = createStageGestureController;
|
|
1078
|
+
exports.createStageSurface = createStageSurface;
|
|
239
1079
|
exports.observeClientGeometry = observeClientGeometry;
|
|
240
1080
|
exports.pickFile = pickFile;
|
|
241
1081
|
exports.pickImageFile = pickImageFile;
|
|
@@ -244,6 +1084,9 @@ exports.projectAnchoredTarget = projectAnchoredTarget;
|
|
|
244
1084
|
exports.sanitizeExternalUri = sanitizeExternalUri;
|
|
245
1085
|
exports.saveFile = saveFile;
|
|
246
1086
|
exports.svgCursor = svgCursor;
|
|
1087
|
+
exports.vibrationFeedback = vibrationFeedback;
|
|
1088
|
+
exports.wheelZoomFactor = wheelZoomFactor;
|
|
247
1089
|
exports.wireSelectionClipboard = wireSelectionClipboard;
|
|
1090
|
+
exports.wkFeedback = wkFeedback;
|
|
248
1091
|
|
|
249
1092
|
//# sourceMappingURL=index.cjs.map
|