canvu-react 0.3.37 → 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/react.cjs +181 -14
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +181 -14
- package/dist/react.js.map +1 -1
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -195,12 +195,23 @@ declare class SvgVectorRenderer {
|
|
|
195
195
|
private readonly svg;
|
|
196
196
|
private readonly rootG;
|
|
197
197
|
private readonly itemNodeCache;
|
|
198
|
+
private liveOverlay;
|
|
198
199
|
private readonly resizeObserver;
|
|
199
200
|
constructor(options: SvgVectorRendererOptions);
|
|
200
201
|
/**
|
|
201
202
|
* Reads container size, culls items, and updates the SVG (incrementally when possible).
|
|
202
203
|
*/
|
|
203
204
|
render(): void;
|
|
205
|
+
/**
|
|
206
|
+
* Updates only the in-progress (live) stroke node without re-culling or
|
|
207
|
+
* re-syncing the committed scene items. Drawing tools call this on every
|
|
208
|
+
* pointer move, so it must stay O(1) regardless of how many items the
|
|
209
|
+
* board already contains.
|
|
210
|
+
*
|
|
211
|
+
* Pass `null` to remove the live overlay (e.g. when the stroke is committed).
|
|
212
|
+
*/
|
|
213
|
+
renderLiveItem(item: VectorSceneItem | null): void;
|
|
214
|
+
private keepLiveOverlayOnTop;
|
|
204
215
|
private syncVisibleItems;
|
|
205
216
|
destroy(): void;
|
|
206
217
|
/** Toggle whether the scene SVG receives pointer events (vs overlay handling them). */
|
package/dist/index.d.ts
CHANGED
|
@@ -195,12 +195,23 @@ declare class SvgVectorRenderer {
|
|
|
195
195
|
private readonly svg;
|
|
196
196
|
private readonly rootG;
|
|
197
197
|
private readonly itemNodeCache;
|
|
198
|
+
private liveOverlay;
|
|
198
199
|
private readonly resizeObserver;
|
|
199
200
|
constructor(options: SvgVectorRendererOptions);
|
|
200
201
|
/**
|
|
201
202
|
* Reads container size, culls items, and updates the SVG (incrementally when possible).
|
|
202
203
|
*/
|
|
203
204
|
render(): void;
|
|
205
|
+
/**
|
|
206
|
+
* Updates only the in-progress (live) stroke node without re-culling or
|
|
207
|
+
* re-syncing the committed scene items. Drawing tools call this on every
|
|
208
|
+
* pointer move, so it must stay O(1) regardless of how many items the
|
|
209
|
+
* board already contains.
|
|
210
|
+
*
|
|
211
|
+
* Pass `null` to remove the live overlay (e.g. when the stroke is committed).
|
|
212
|
+
*/
|
|
213
|
+
renderLiveItem(item: VectorSceneItem | null): void;
|
|
214
|
+
private keepLiveOverlayOnTop;
|
|
204
215
|
private syncVisibleItems;
|
|
205
216
|
destroy(): void;
|
|
206
217
|
/** Toggle whether the scene SVG receives pointer events (vs overlay handling them). */
|
package/dist/index.js
CHANGED
|
@@ -407,6 +407,78 @@ async function loadImageFileAsRasterSceneSource(file, maxDimensionOrOptions) {
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
// src/input/pan-momentum.ts
|
|
411
|
+
var VELOCITY_SAMPLE_WINDOW = 80;
|
|
412
|
+
var FRICTION = 0.94;
|
|
413
|
+
var MIN_VELOCITY = 0.3;
|
|
414
|
+
function createPanMomentumController(camera, onUpdate, sensitivity) {
|
|
415
|
+
const samples = [];
|
|
416
|
+
let animationFrameId = null;
|
|
417
|
+
const cancel = () => {
|
|
418
|
+
if (animationFrameId !== null) {
|
|
419
|
+
cancelAnimationFrame(animationFrameId);
|
|
420
|
+
animationFrameId = null;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const reset = () => {
|
|
424
|
+
cancel();
|
|
425
|
+
samples.length = 0;
|
|
426
|
+
};
|
|
427
|
+
const recordMove = (dx, dy) => {
|
|
428
|
+
const now = performance.now();
|
|
429
|
+
samples.push({ dx, dy, time: now });
|
|
430
|
+
const cutoff = now - VELOCITY_SAMPLE_WINDOW;
|
|
431
|
+
let oldestSample = samples[0];
|
|
432
|
+
while (oldestSample && oldestSample.time < cutoff) {
|
|
433
|
+
samples.shift();
|
|
434
|
+
oldestSample = samples[0];
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
const computeReleaseVelocity = () => {
|
|
438
|
+
if (samples.length < 2) return { vx: 0, vy: 0 };
|
|
439
|
+
const first = samples[0];
|
|
440
|
+
const last = samples[samples.length - 1];
|
|
441
|
+
if (!first || !last) return { vx: 0, vy: 0 };
|
|
442
|
+
const elapsed = last.time - first.time;
|
|
443
|
+
if (elapsed < 4) return { vx: 0, vy: 0 };
|
|
444
|
+
let totalDx = 0;
|
|
445
|
+
let totalDy = 0;
|
|
446
|
+
for (const sample of samples) {
|
|
447
|
+
totalDx += sample.dx;
|
|
448
|
+
totalDy += sample.dy;
|
|
449
|
+
}
|
|
450
|
+
const msPerFrame = 1e3 / 60;
|
|
451
|
+
return {
|
|
452
|
+
vx: totalDx / elapsed * msPerFrame * sensitivity,
|
|
453
|
+
vy: totalDy / elapsed * msPerFrame * sensitivity
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
const startMomentum = () => {
|
|
457
|
+
cancel();
|
|
458
|
+
const { vx, vy } = computeReleaseVelocity();
|
|
459
|
+
samples.length = 0;
|
|
460
|
+
if (Math.abs(vx) < MIN_VELOCITY && Math.abs(vy) < MIN_VELOCITY) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
let currentVx = vx;
|
|
464
|
+
let currentVy = vy;
|
|
465
|
+
const animate = () => {
|
|
466
|
+
currentVx *= FRICTION;
|
|
467
|
+
currentVy *= FRICTION;
|
|
468
|
+
if (Math.abs(currentVx) < MIN_VELOCITY && Math.abs(currentVy) < MIN_VELOCITY) {
|
|
469
|
+
animationFrameId = null;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
camera.x += currentVx;
|
|
473
|
+
camera.y += currentVy;
|
|
474
|
+
onUpdate();
|
|
475
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
476
|
+
};
|
|
477
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
478
|
+
};
|
|
479
|
+
return { recordMove, startMomentum, cancel, reset };
|
|
480
|
+
}
|
|
481
|
+
|
|
410
482
|
// src/input/apple-pencil-navigation.ts
|
|
411
483
|
var DRAWING_LIKE_TOOLS = /* @__PURE__ */ new Set([
|
|
412
484
|
"draw",
|
|
@@ -437,6 +509,7 @@ function attachApplePencilNavigation(options) {
|
|
|
437
509
|
let pinchStartDist = 0;
|
|
438
510
|
let pinchStartZoom = 1;
|
|
439
511
|
let panLast = null;
|
|
512
|
+
let touchMomentum = null;
|
|
440
513
|
const shouldIntercept = (e) => {
|
|
441
514
|
if (e.pointerType !== "touch") return false;
|
|
442
515
|
const tool = getCurrentToolId();
|
|
@@ -455,6 +528,10 @@ function attachApplePencilNavigation(options) {
|
|
|
455
528
|
pointers.clear();
|
|
456
529
|
mode = "idle";
|
|
457
530
|
panLast = null;
|
|
531
|
+
if (touchMomentum) {
|
|
532
|
+
touchMomentum.cancel();
|
|
533
|
+
touchMomentum = null;
|
|
534
|
+
}
|
|
458
535
|
return;
|
|
459
536
|
}
|
|
460
537
|
if (e.pointerType === "touch" && activePenPointerIds.size > 0) {
|
|
@@ -464,10 +541,18 @@ function attachApplePencilNavigation(options) {
|
|
|
464
541
|
return;
|
|
465
542
|
}
|
|
466
543
|
if (!shouldIntercept(e)) return;
|
|
544
|
+
if (touchMomentum) {
|
|
545
|
+
touchMomentum.cancel();
|
|
546
|
+
}
|
|
467
547
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
468
548
|
if (pointers.size === 1) {
|
|
469
549
|
mode = "pan";
|
|
470
550
|
panLast = { x: e.clientX, y: e.clientY };
|
|
551
|
+
touchMomentum = createPanMomentumController(
|
|
552
|
+
camera,
|
|
553
|
+
onUpdate,
|
|
554
|
+
touchPanSensitivity
|
|
555
|
+
);
|
|
471
556
|
} else if (pointers.size === 2) {
|
|
472
557
|
const vals = Array.from(pointers.values());
|
|
473
558
|
const a = vals[0];
|
|
@@ -495,6 +580,7 @@ function attachApplePencilNavigation(options) {
|
|
|
495
580
|
panLast = { x: e.clientX, y: e.clientY };
|
|
496
581
|
camera.x += dx * touchPanSensitivity;
|
|
497
582
|
camera.y += dy * touchPanSensitivity;
|
|
583
|
+
touchMomentum?.recordMove(dx, dy);
|
|
498
584
|
onUpdate();
|
|
499
585
|
e.preventDefault();
|
|
500
586
|
e.stopImmediatePropagation();
|
|
@@ -541,12 +627,20 @@ function attachApplePencilNavigation(options) {
|
|
|
541
627
|
if (!pointers.has(e.pointerId)) return;
|
|
542
628
|
pointers.delete(e.pointerId);
|
|
543
629
|
if (pointers.size === 0) {
|
|
630
|
+
const wasPanning = mode === "pan";
|
|
544
631
|
mode = "idle";
|
|
545
632
|
panLast = null;
|
|
633
|
+
if (wasPanning && touchMomentum) {
|
|
634
|
+
touchMomentum.startMomentum();
|
|
635
|
+
touchMomentum = null;
|
|
636
|
+
}
|
|
546
637
|
} else if (pointers.size === 1 && mode === "pinch") {
|
|
547
638
|
mode = "pan";
|
|
548
639
|
const r = Array.from(pointers.values())[0];
|
|
549
640
|
panLast = r ?? null;
|
|
641
|
+
if (touchMomentum) {
|
|
642
|
+
touchMomentum.reset();
|
|
643
|
+
}
|
|
550
644
|
}
|
|
551
645
|
e.preventDefault();
|
|
552
646
|
e.stopImmediatePropagation();
|
|
@@ -556,6 +650,10 @@ function attachApplePencilNavigation(options) {
|
|
|
556
650
|
element.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
557
651
|
element.addEventListener("pointercancel", onPointerUp, { capture: true });
|
|
558
652
|
return () => {
|
|
653
|
+
if (touchMomentum) {
|
|
654
|
+
touchMomentum.cancel();
|
|
655
|
+
touchMomentum = null;
|
|
656
|
+
}
|
|
559
657
|
element.removeEventListener("pointerdown", onPointerDown, { capture: true });
|
|
560
658
|
element.removeEventListener("pointermove", onPointerMove, { capture: true });
|
|
561
659
|
element.removeEventListener("pointerup", onPointerUp, { capture: true });
|
|
@@ -608,6 +706,7 @@ function attachViewportInput(options) {
|
|
|
608
706
|
let pinchStartZoom = 1;
|
|
609
707
|
let panLast = null;
|
|
610
708
|
let mousePanButton = null;
|
|
709
|
+
let touchMomentum = null;
|
|
611
710
|
const onWheel = (e) => {
|
|
612
711
|
if (e.ctrlKey || e.metaKey) {
|
|
613
712
|
e.preventDefault();
|
|
@@ -634,6 +733,9 @@ function attachViewportInput(options) {
|
|
|
634
733
|
if (touchHandledElsewhere && e.pointerType === "touch") {
|
|
635
734
|
return;
|
|
636
735
|
}
|
|
736
|
+
if (touchMomentum) {
|
|
737
|
+
touchMomentum.cancel();
|
|
738
|
+
}
|
|
637
739
|
const panOk = allowPrimaryPointerPan();
|
|
638
740
|
if (e.pointerType === "mouse" && e.button === 0) {
|
|
639
741
|
if (!panOk) {
|
|
@@ -659,6 +761,11 @@ function attachViewportInput(options) {
|
|
|
659
761
|
if (panOk) {
|
|
660
762
|
mode = "pan";
|
|
661
763
|
panLast = { x: e.clientX, y: e.clientY };
|
|
764
|
+
touchMomentum = createPanMomentumController(
|
|
765
|
+
camera,
|
|
766
|
+
onUpdate,
|
|
767
|
+
touchPanSensitivity
|
|
768
|
+
);
|
|
662
769
|
e.preventDefault();
|
|
663
770
|
}
|
|
664
771
|
} else if (pointers.size === 2) {
|
|
@@ -695,6 +802,7 @@ function attachViewportInput(options) {
|
|
|
695
802
|
panLast = { x: e.clientX, y: e.clientY };
|
|
696
803
|
camera.x += dx * touchPanSensitivity;
|
|
697
804
|
camera.y += dy * touchPanSensitivity;
|
|
805
|
+
touchMomentum?.recordMove(dx, dy);
|
|
698
806
|
onUpdate();
|
|
699
807
|
e.preventDefault();
|
|
700
808
|
return;
|
|
@@ -746,15 +854,27 @@ function attachViewportInput(options) {
|
|
|
746
854
|
}
|
|
747
855
|
pointers.delete(e.pointerId);
|
|
748
856
|
if (pointers.size === 0) {
|
|
857
|
+
const wasPanning = mode === "pan";
|
|
749
858
|
mode = "idle";
|
|
750
859
|
panLast = null;
|
|
860
|
+
if (wasPanning && touchMomentum) {
|
|
861
|
+
touchMomentum.startMomentum();
|
|
862
|
+
touchMomentum = null;
|
|
863
|
+
}
|
|
751
864
|
} else if (pointers.size === 1 && mode === "pinch") {
|
|
752
865
|
mode = "pan";
|
|
753
866
|
const remaining = Array.from(pointers.values())[0];
|
|
754
867
|
panLast = remaining ?? null;
|
|
868
|
+
if (touchMomentum) {
|
|
869
|
+
touchMomentum.reset();
|
|
870
|
+
}
|
|
755
871
|
}
|
|
756
872
|
};
|
|
757
873
|
const onPointerCancel = (e) => {
|
|
874
|
+
if (touchMomentum) {
|
|
875
|
+
touchMomentum.cancel();
|
|
876
|
+
touchMomentum = null;
|
|
877
|
+
}
|
|
758
878
|
onPointerUp(e);
|
|
759
879
|
};
|
|
760
880
|
wheelTarget.addEventListener("wheel", onWheel, { passive: false });
|
|
@@ -763,6 +883,10 @@ function attachViewportInput(options) {
|
|
|
763
883
|
element.addEventListener("pointerup", onPointerUp);
|
|
764
884
|
element.addEventListener("pointercancel", onPointerCancel);
|
|
765
885
|
return () => {
|
|
886
|
+
if (touchMomentum) {
|
|
887
|
+
touchMomentum.cancel();
|
|
888
|
+
touchMomentum = null;
|
|
889
|
+
}
|
|
766
890
|
wheelTarget.removeEventListener("wheel", onWheel);
|
|
767
891
|
element.removeEventListener("pointerdown", onPointerDown);
|
|
768
892
|
element.removeEventListener("pointermove", onPointerMove);
|
|
@@ -2475,6 +2599,7 @@ var SvgVectorRenderer = class {
|
|
|
2475
2599
|
svg;
|
|
2476
2600
|
rootG;
|
|
2477
2601
|
itemNodeCache = /* @__PURE__ */ new Map();
|
|
2602
|
+
liveOverlay = null;
|
|
2478
2603
|
resizeObserver;
|
|
2479
2604
|
constructor(options) {
|
|
2480
2605
|
this.container = options.container;
|
|
@@ -2509,6 +2634,50 @@ var SvgVectorRenderer = class {
|
|
|
2509
2634
|
const items = cullItemsByViewport(this.scene.getItems(), visible);
|
|
2510
2635
|
this.syncVisibleItems(items);
|
|
2511
2636
|
this.rootG.setAttribute("transform", formatCameraTransform(this.camera));
|
|
2637
|
+
this.keepLiveOverlayOnTop();
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Updates only the in-progress (live) stroke node without re-culling or
|
|
2641
|
+
* re-syncing the committed scene items. Drawing tools call this on every
|
|
2642
|
+
* pointer move, so it must stay O(1) regardless of how many items the
|
|
2643
|
+
* board already contains.
|
|
2644
|
+
*
|
|
2645
|
+
* Pass `null` to remove the live overlay (e.g. when the stroke is committed).
|
|
2646
|
+
*/
|
|
2647
|
+
renderLiveItem(item) {
|
|
2648
|
+
if (!item) {
|
|
2649
|
+
if (this.liveOverlay) {
|
|
2650
|
+
this.liveOverlay.g.remove();
|
|
2651
|
+
this.liveOverlay = null;
|
|
2652
|
+
}
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
if (!this.liveOverlay) {
|
|
2656
|
+
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
2657
|
+
g.setAttribute("data-live-overlay", "true");
|
|
2658
|
+
this.liveOverlay = {
|
|
2659
|
+
g,
|
|
2660
|
+
lastChildrenSvg: "",
|
|
2661
|
+
lastTransform: ""
|
|
2662
|
+
};
|
|
2663
|
+
this.rootG.appendChild(g);
|
|
2664
|
+
}
|
|
2665
|
+
const cached = this.liveOverlay;
|
|
2666
|
+
const t = formatItemPlacementTransform(item);
|
|
2667
|
+
if (cached.lastTransform !== t) {
|
|
2668
|
+
cached.g.setAttribute("transform", t);
|
|
2669
|
+
cached.lastTransform = t;
|
|
2670
|
+
}
|
|
2671
|
+
if (cached.lastChildrenSvg !== item.childrenSvg) {
|
|
2672
|
+
cached.g.innerHTML = item.childrenSvg;
|
|
2673
|
+
cached.lastChildrenSvg = item.childrenSvg;
|
|
2674
|
+
}
|
|
2675
|
+
this.keepLiveOverlayOnTop();
|
|
2676
|
+
}
|
|
2677
|
+
keepLiveOverlayOnTop() {
|
|
2678
|
+
if (this.liveOverlay && this.rootG.lastChild !== this.liveOverlay.g) {
|
|
2679
|
+
this.rootG.appendChild(this.liveOverlay.g);
|
|
2680
|
+
}
|
|
2512
2681
|
}
|
|
2513
2682
|
syncVisibleItems(items) {
|
|
2514
2683
|
const visibleIds = /* @__PURE__ */ new Set();
|
|
@@ -2553,6 +2722,7 @@ var SvgVectorRenderer = class {
|
|
|
2553
2722
|
destroy() {
|
|
2554
2723
|
this.resizeObserver.disconnect();
|
|
2555
2724
|
this.itemNodeCache.clear();
|
|
2725
|
+
this.liveOverlay = null;
|
|
2556
2726
|
this.svg.remove();
|
|
2557
2727
|
}
|
|
2558
2728
|
/** Toggle whether the scene SVG receives pointer events (vs overlay handling them). */
|