canvu-react 0.3.36 → 0.3.38
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 +170 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +170 -0
- package/dist/index.js.map +1 -1
- package/dist/native.cjs +1 -1
- package/dist/native.cjs.map +1 -1
- package/dist/native.js +1 -1
- package/dist/native.js.map +1 -1
- package/dist/react.cjs +183 -16
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +183 -16
- package/dist/react.js.map +1 -1
- package/dist/realtime.cjs +1 -1
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.js +1 -1
- package/dist/realtime.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -414,6 +414,78 @@ async function loadImageFileAsRasterSceneSource(file, maxDimensionOrOptions) {
|
|
|
414
414
|
}
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
// src/input/pan-momentum.ts
|
|
418
|
+
var VELOCITY_SAMPLE_WINDOW = 80;
|
|
419
|
+
var FRICTION = 0.94;
|
|
420
|
+
var MIN_VELOCITY = 0.3;
|
|
421
|
+
function createPanMomentumController(camera, onUpdate, sensitivity) {
|
|
422
|
+
const samples = [];
|
|
423
|
+
let animationFrameId = null;
|
|
424
|
+
const cancel = () => {
|
|
425
|
+
if (animationFrameId !== null) {
|
|
426
|
+
cancelAnimationFrame(animationFrameId);
|
|
427
|
+
animationFrameId = null;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const reset = () => {
|
|
431
|
+
cancel();
|
|
432
|
+
samples.length = 0;
|
|
433
|
+
};
|
|
434
|
+
const recordMove = (dx, dy) => {
|
|
435
|
+
const now = performance.now();
|
|
436
|
+
samples.push({ dx, dy, time: now });
|
|
437
|
+
const cutoff = now - VELOCITY_SAMPLE_WINDOW;
|
|
438
|
+
let oldestSample = samples[0];
|
|
439
|
+
while (oldestSample && oldestSample.time < cutoff) {
|
|
440
|
+
samples.shift();
|
|
441
|
+
oldestSample = samples[0];
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const computeReleaseVelocity = () => {
|
|
445
|
+
if (samples.length < 2) return { vx: 0, vy: 0 };
|
|
446
|
+
const first = samples[0];
|
|
447
|
+
const last = samples[samples.length - 1];
|
|
448
|
+
if (!first || !last) return { vx: 0, vy: 0 };
|
|
449
|
+
const elapsed = last.time - first.time;
|
|
450
|
+
if (elapsed < 4) return { vx: 0, vy: 0 };
|
|
451
|
+
let totalDx = 0;
|
|
452
|
+
let totalDy = 0;
|
|
453
|
+
for (const sample of samples) {
|
|
454
|
+
totalDx += sample.dx;
|
|
455
|
+
totalDy += sample.dy;
|
|
456
|
+
}
|
|
457
|
+
const msPerFrame = 1e3 / 60;
|
|
458
|
+
return {
|
|
459
|
+
vx: totalDx / elapsed * msPerFrame * sensitivity,
|
|
460
|
+
vy: totalDy / elapsed * msPerFrame * sensitivity
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
const startMomentum = () => {
|
|
464
|
+
cancel();
|
|
465
|
+
const { vx, vy } = computeReleaseVelocity();
|
|
466
|
+
samples.length = 0;
|
|
467
|
+
if (Math.abs(vx) < MIN_VELOCITY && Math.abs(vy) < MIN_VELOCITY) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
let currentVx = vx;
|
|
471
|
+
let currentVy = vy;
|
|
472
|
+
const animate = () => {
|
|
473
|
+
currentVx *= FRICTION;
|
|
474
|
+
currentVy *= FRICTION;
|
|
475
|
+
if (Math.abs(currentVx) < MIN_VELOCITY && Math.abs(currentVy) < MIN_VELOCITY) {
|
|
476
|
+
animationFrameId = null;
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
camera.x += currentVx;
|
|
480
|
+
camera.y += currentVy;
|
|
481
|
+
onUpdate();
|
|
482
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
483
|
+
};
|
|
484
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
485
|
+
};
|
|
486
|
+
return { recordMove, startMomentum, cancel, reset };
|
|
487
|
+
}
|
|
488
|
+
|
|
417
489
|
// src/input/apple-pencil-navigation.ts
|
|
418
490
|
var DRAWING_LIKE_TOOLS = /* @__PURE__ */ new Set([
|
|
419
491
|
"draw",
|
|
@@ -444,6 +516,7 @@ function attachApplePencilNavigation(options) {
|
|
|
444
516
|
let pinchStartDist = 0;
|
|
445
517
|
let pinchStartZoom = 1;
|
|
446
518
|
let panLast = null;
|
|
519
|
+
let touchMomentum = null;
|
|
447
520
|
const shouldIntercept = (e) => {
|
|
448
521
|
if (e.pointerType !== "touch") return false;
|
|
449
522
|
const tool = getCurrentToolId();
|
|
@@ -462,6 +535,10 @@ function attachApplePencilNavigation(options) {
|
|
|
462
535
|
pointers.clear();
|
|
463
536
|
mode = "idle";
|
|
464
537
|
panLast = null;
|
|
538
|
+
if (touchMomentum) {
|
|
539
|
+
touchMomentum.cancel();
|
|
540
|
+
touchMomentum = null;
|
|
541
|
+
}
|
|
465
542
|
return;
|
|
466
543
|
}
|
|
467
544
|
if (e.pointerType === "touch" && activePenPointerIds.size > 0) {
|
|
@@ -471,10 +548,18 @@ function attachApplePencilNavigation(options) {
|
|
|
471
548
|
return;
|
|
472
549
|
}
|
|
473
550
|
if (!shouldIntercept(e)) return;
|
|
551
|
+
if (touchMomentum) {
|
|
552
|
+
touchMomentum.cancel();
|
|
553
|
+
}
|
|
474
554
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
475
555
|
if (pointers.size === 1) {
|
|
476
556
|
mode = "pan";
|
|
477
557
|
panLast = { x: e.clientX, y: e.clientY };
|
|
558
|
+
touchMomentum = createPanMomentumController(
|
|
559
|
+
camera,
|
|
560
|
+
onUpdate,
|
|
561
|
+
touchPanSensitivity
|
|
562
|
+
);
|
|
478
563
|
} else if (pointers.size === 2) {
|
|
479
564
|
const vals = Array.from(pointers.values());
|
|
480
565
|
const a = vals[0];
|
|
@@ -502,6 +587,7 @@ function attachApplePencilNavigation(options) {
|
|
|
502
587
|
panLast = { x: e.clientX, y: e.clientY };
|
|
503
588
|
camera.x += dx * touchPanSensitivity;
|
|
504
589
|
camera.y += dy * touchPanSensitivity;
|
|
590
|
+
touchMomentum?.recordMove(dx, dy);
|
|
505
591
|
onUpdate();
|
|
506
592
|
e.preventDefault();
|
|
507
593
|
e.stopImmediatePropagation();
|
|
@@ -548,12 +634,20 @@ function attachApplePencilNavigation(options) {
|
|
|
548
634
|
if (!pointers.has(e.pointerId)) return;
|
|
549
635
|
pointers.delete(e.pointerId);
|
|
550
636
|
if (pointers.size === 0) {
|
|
637
|
+
const wasPanning = mode === "pan";
|
|
551
638
|
mode = "idle";
|
|
552
639
|
panLast = null;
|
|
640
|
+
if (wasPanning && touchMomentum) {
|
|
641
|
+
touchMomentum.startMomentum();
|
|
642
|
+
touchMomentum = null;
|
|
643
|
+
}
|
|
553
644
|
} else if (pointers.size === 1 && mode === "pinch") {
|
|
554
645
|
mode = "pan";
|
|
555
646
|
const r = Array.from(pointers.values())[0];
|
|
556
647
|
panLast = r ?? null;
|
|
648
|
+
if (touchMomentum) {
|
|
649
|
+
touchMomentum.reset();
|
|
650
|
+
}
|
|
557
651
|
}
|
|
558
652
|
e.preventDefault();
|
|
559
653
|
e.stopImmediatePropagation();
|
|
@@ -563,6 +657,10 @@ function attachApplePencilNavigation(options) {
|
|
|
563
657
|
element.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
564
658
|
element.addEventListener("pointercancel", onPointerUp, { capture: true });
|
|
565
659
|
return () => {
|
|
660
|
+
if (touchMomentum) {
|
|
661
|
+
touchMomentum.cancel();
|
|
662
|
+
touchMomentum = null;
|
|
663
|
+
}
|
|
566
664
|
element.removeEventListener("pointerdown", onPointerDown, { capture: true });
|
|
567
665
|
element.removeEventListener("pointermove", onPointerMove, { capture: true });
|
|
568
666
|
element.removeEventListener("pointerup", onPointerUp, { capture: true });
|
|
@@ -615,6 +713,7 @@ function attachViewportInput(options) {
|
|
|
615
713
|
let pinchStartZoom = 1;
|
|
616
714
|
let panLast = null;
|
|
617
715
|
let mousePanButton = null;
|
|
716
|
+
let touchMomentum = null;
|
|
618
717
|
const onWheel = (e) => {
|
|
619
718
|
if (e.ctrlKey || e.metaKey) {
|
|
620
719
|
e.preventDefault();
|
|
@@ -641,6 +740,9 @@ function attachViewportInput(options) {
|
|
|
641
740
|
if (touchHandledElsewhere && e.pointerType === "touch") {
|
|
642
741
|
return;
|
|
643
742
|
}
|
|
743
|
+
if (touchMomentum) {
|
|
744
|
+
touchMomentum.cancel();
|
|
745
|
+
}
|
|
644
746
|
const panOk = allowPrimaryPointerPan();
|
|
645
747
|
if (e.pointerType === "mouse" && e.button === 0) {
|
|
646
748
|
if (!panOk) {
|
|
@@ -666,6 +768,11 @@ function attachViewportInput(options) {
|
|
|
666
768
|
if (panOk) {
|
|
667
769
|
mode = "pan";
|
|
668
770
|
panLast = { x: e.clientX, y: e.clientY };
|
|
771
|
+
touchMomentum = createPanMomentumController(
|
|
772
|
+
camera,
|
|
773
|
+
onUpdate,
|
|
774
|
+
touchPanSensitivity
|
|
775
|
+
);
|
|
669
776
|
e.preventDefault();
|
|
670
777
|
}
|
|
671
778
|
} else if (pointers.size === 2) {
|
|
@@ -702,6 +809,7 @@ function attachViewportInput(options) {
|
|
|
702
809
|
panLast = { x: e.clientX, y: e.clientY };
|
|
703
810
|
camera.x += dx * touchPanSensitivity;
|
|
704
811
|
camera.y += dy * touchPanSensitivity;
|
|
812
|
+
touchMomentum?.recordMove(dx, dy);
|
|
705
813
|
onUpdate();
|
|
706
814
|
e.preventDefault();
|
|
707
815
|
return;
|
|
@@ -753,15 +861,27 @@ function attachViewportInput(options) {
|
|
|
753
861
|
}
|
|
754
862
|
pointers.delete(e.pointerId);
|
|
755
863
|
if (pointers.size === 0) {
|
|
864
|
+
const wasPanning = mode === "pan";
|
|
756
865
|
mode = "idle";
|
|
757
866
|
panLast = null;
|
|
867
|
+
if (wasPanning && touchMomentum) {
|
|
868
|
+
touchMomentum.startMomentum();
|
|
869
|
+
touchMomentum = null;
|
|
870
|
+
}
|
|
758
871
|
} else if (pointers.size === 1 && mode === "pinch") {
|
|
759
872
|
mode = "pan";
|
|
760
873
|
const remaining = Array.from(pointers.values())[0];
|
|
761
874
|
panLast = remaining ?? null;
|
|
875
|
+
if (touchMomentum) {
|
|
876
|
+
touchMomentum.reset();
|
|
877
|
+
}
|
|
762
878
|
}
|
|
763
879
|
};
|
|
764
880
|
const onPointerCancel = (e) => {
|
|
881
|
+
if (touchMomentum) {
|
|
882
|
+
touchMomentum.cancel();
|
|
883
|
+
touchMomentum = null;
|
|
884
|
+
}
|
|
765
885
|
onPointerUp(e);
|
|
766
886
|
};
|
|
767
887
|
wheelTarget.addEventListener("wheel", onWheel, { passive: false });
|
|
@@ -770,6 +890,10 @@ function attachViewportInput(options) {
|
|
|
770
890
|
element.addEventListener("pointerup", onPointerUp);
|
|
771
891
|
element.addEventListener("pointercancel", onPointerCancel);
|
|
772
892
|
return () => {
|
|
893
|
+
if (touchMomentum) {
|
|
894
|
+
touchMomentum.cancel();
|
|
895
|
+
touchMomentum = null;
|
|
896
|
+
}
|
|
773
897
|
wheelTarget.removeEventListener("wheel", onWheel);
|
|
774
898
|
element.removeEventListener("pointerdown", onPointerDown);
|
|
775
899
|
element.removeEventListener("pointermove", onPointerMove);
|
|
@@ -2482,6 +2606,7 @@ var SvgVectorRenderer = class {
|
|
|
2482
2606
|
svg;
|
|
2483
2607
|
rootG;
|
|
2484
2608
|
itemNodeCache = /* @__PURE__ */ new Map();
|
|
2609
|
+
liveOverlay = null;
|
|
2485
2610
|
resizeObserver;
|
|
2486
2611
|
constructor(options) {
|
|
2487
2612
|
this.container = options.container;
|
|
@@ -2516,6 +2641,50 @@ var SvgVectorRenderer = class {
|
|
|
2516
2641
|
const items = cullItemsByViewport(this.scene.getItems(), visible);
|
|
2517
2642
|
this.syncVisibleItems(items);
|
|
2518
2643
|
this.rootG.setAttribute("transform", formatCameraTransform(this.camera));
|
|
2644
|
+
this.keepLiveOverlayOnTop();
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Updates only the in-progress (live) stroke node without re-culling or
|
|
2648
|
+
* re-syncing the committed scene items. Drawing tools call this on every
|
|
2649
|
+
* pointer move, so it must stay O(1) regardless of how many items the
|
|
2650
|
+
* board already contains.
|
|
2651
|
+
*
|
|
2652
|
+
* Pass `null` to remove the live overlay (e.g. when the stroke is committed).
|
|
2653
|
+
*/
|
|
2654
|
+
renderLiveItem(item) {
|
|
2655
|
+
if (!item) {
|
|
2656
|
+
if (this.liveOverlay) {
|
|
2657
|
+
this.liveOverlay.g.remove();
|
|
2658
|
+
this.liveOverlay = null;
|
|
2659
|
+
}
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
if (!this.liveOverlay) {
|
|
2663
|
+
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
2664
|
+
g.setAttribute("data-live-overlay", "true");
|
|
2665
|
+
this.liveOverlay = {
|
|
2666
|
+
g,
|
|
2667
|
+
lastChildrenSvg: "",
|
|
2668
|
+
lastTransform: ""
|
|
2669
|
+
};
|
|
2670
|
+
this.rootG.appendChild(g);
|
|
2671
|
+
}
|
|
2672
|
+
const cached = this.liveOverlay;
|
|
2673
|
+
const t = formatItemPlacementTransform(item);
|
|
2674
|
+
if (cached.lastTransform !== t) {
|
|
2675
|
+
cached.g.setAttribute("transform", t);
|
|
2676
|
+
cached.lastTransform = t;
|
|
2677
|
+
}
|
|
2678
|
+
if (cached.lastChildrenSvg !== item.childrenSvg) {
|
|
2679
|
+
cached.g.innerHTML = item.childrenSvg;
|
|
2680
|
+
cached.lastChildrenSvg = item.childrenSvg;
|
|
2681
|
+
}
|
|
2682
|
+
this.keepLiveOverlayOnTop();
|
|
2683
|
+
}
|
|
2684
|
+
keepLiveOverlayOnTop() {
|
|
2685
|
+
if (this.liveOverlay && this.rootG.lastChild !== this.liveOverlay.g) {
|
|
2686
|
+
this.rootG.appendChild(this.liveOverlay.g);
|
|
2687
|
+
}
|
|
2519
2688
|
}
|
|
2520
2689
|
syncVisibleItems(items) {
|
|
2521
2690
|
const visibleIds = /* @__PURE__ */ new Set();
|
|
@@ -2560,6 +2729,7 @@ var SvgVectorRenderer = class {
|
|
|
2560
2729
|
destroy() {
|
|
2561
2730
|
this.resizeObserver.disconnect();
|
|
2562
2731
|
this.itemNodeCache.clear();
|
|
2732
|
+
this.liveOverlay = null;
|
|
2563
2733
|
this.svg.remove();
|
|
2564
2734
|
}
|
|
2565
2735
|
/** Toggle whether the scene SVG receives pointer events (vs overlay handling them). */
|