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/react.js
CHANGED
|
@@ -4963,6 +4963,78 @@ var Camera2D = class {
|
|
|
4963
4963
|
}
|
|
4964
4964
|
};
|
|
4965
4965
|
|
|
4966
|
+
// src/input/pan-momentum.ts
|
|
4967
|
+
var VELOCITY_SAMPLE_WINDOW = 80;
|
|
4968
|
+
var FRICTION = 0.94;
|
|
4969
|
+
var MIN_VELOCITY = 0.3;
|
|
4970
|
+
function createPanMomentumController(camera, onUpdate, sensitivity) {
|
|
4971
|
+
const samples = [];
|
|
4972
|
+
let animationFrameId = null;
|
|
4973
|
+
const cancel = () => {
|
|
4974
|
+
if (animationFrameId !== null) {
|
|
4975
|
+
cancelAnimationFrame(animationFrameId);
|
|
4976
|
+
animationFrameId = null;
|
|
4977
|
+
}
|
|
4978
|
+
};
|
|
4979
|
+
const reset = () => {
|
|
4980
|
+
cancel();
|
|
4981
|
+
samples.length = 0;
|
|
4982
|
+
};
|
|
4983
|
+
const recordMove = (dx, dy) => {
|
|
4984
|
+
const now = performance.now();
|
|
4985
|
+
samples.push({ dx, dy, time: now });
|
|
4986
|
+
const cutoff = now - VELOCITY_SAMPLE_WINDOW;
|
|
4987
|
+
let oldestSample = samples[0];
|
|
4988
|
+
while (oldestSample && oldestSample.time < cutoff) {
|
|
4989
|
+
samples.shift();
|
|
4990
|
+
oldestSample = samples[0];
|
|
4991
|
+
}
|
|
4992
|
+
};
|
|
4993
|
+
const computeReleaseVelocity = () => {
|
|
4994
|
+
if (samples.length < 2) return { vx: 0, vy: 0 };
|
|
4995
|
+
const first = samples[0];
|
|
4996
|
+
const last = samples[samples.length - 1];
|
|
4997
|
+
if (!first || !last) return { vx: 0, vy: 0 };
|
|
4998
|
+
const elapsed = last.time - first.time;
|
|
4999
|
+
if (elapsed < 4) return { vx: 0, vy: 0 };
|
|
5000
|
+
let totalDx = 0;
|
|
5001
|
+
let totalDy = 0;
|
|
5002
|
+
for (const sample of samples) {
|
|
5003
|
+
totalDx += sample.dx;
|
|
5004
|
+
totalDy += sample.dy;
|
|
5005
|
+
}
|
|
5006
|
+
const msPerFrame = 1e3 / 60;
|
|
5007
|
+
return {
|
|
5008
|
+
vx: totalDx / elapsed * msPerFrame * sensitivity,
|
|
5009
|
+
vy: totalDy / elapsed * msPerFrame * sensitivity
|
|
5010
|
+
};
|
|
5011
|
+
};
|
|
5012
|
+
const startMomentum = () => {
|
|
5013
|
+
cancel();
|
|
5014
|
+
const { vx, vy } = computeReleaseVelocity();
|
|
5015
|
+
samples.length = 0;
|
|
5016
|
+
if (Math.abs(vx) < MIN_VELOCITY && Math.abs(vy) < MIN_VELOCITY) {
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
let currentVx = vx;
|
|
5020
|
+
let currentVy = vy;
|
|
5021
|
+
const animate = () => {
|
|
5022
|
+
currentVx *= FRICTION;
|
|
5023
|
+
currentVy *= FRICTION;
|
|
5024
|
+
if (Math.abs(currentVx) < MIN_VELOCITY && Math.abs(currentVy) < MIN_VELOCITY) {
|
|
5025
|
+
animationFrameId = null;
|
|
5026
|
+
return;
|
|
5027
|
+
}
|
|
5028
|
+
camera.x += currentVx;
|
|
5029
|
+
camera.y += currentVy;
|
|
5030
|
+
onUpdate();
|
|
5031
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
5032
|
+
};
|
|
5033
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
5034
|
+
};
|
|
5035
|
+
return { recordMove, startMomentum, cancel, reset };
|
|
5036
|
+
}
|
|
5037
|
+
|
|
4966
5038
|
// src/input/apple-pencil-navigation.ts
|
|
4967
5039
|
var DRAWING_LIKE_TOOLS = /* @__PURE__ */ new Set([
|
|
4968
5040
|
"draw",
|
|
@@ -4993,6 +5065,7 @@ function attachApplePencilNavigation(options) {
|
|
|
4993
5065
|
let pinchStartDist = 0;
|
|
4994
5066
|
let pinchStartZoom = 1;
|
|
4995
5067
|
let panLast = null;
|
|
5068
|
+
let touchMomentum = null;
|
|
4996
5069
|
const shouldIntercept = (e) => {
|
|
4997
5070
|
if (e.pointerType !== "touch") return false;
|
|
4998
5071
|
const tool = getCurrentToolId();
|
|
@@ -5011,6 +5084,10 @@ function attachApplePencilNavigation(options) {
|
|
|
5011
5084
|
pointers.clear();
|
|
5012
5085
|
mode = "idle";
|
|
5013
5086
|
panLast = null;
|
|
5087
|
+
if (touchMomentum) {
|
|
5088
|
+
touchMomentum.cancel();
|
|
5089
|
+
touchMomentum = null;
|
|
5090
|
+
}
|
|
5014
5091
|
return;
|
|
5015
5092
|
}
|
|
5016
5093
|
if (e.pointerType === "touch" && activePenPointerIds.size > 0) {
|
|
@@ -5020,10 +5097,18 @@ function attachApplePencilNavigation(options) {
|
|
|
5020
5097
|
return;
|
|
5021
5098
|
}
|
|
5022
5099
|
if (!shouldIntercept(e)) return;
|
|
5100
|
+
if (touchMomentum) {
|
|
5101
|
+
touchMomentum.cancel();
|
|
5102
|
+
}
|
|
5023
5103
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
5024
5104
|
if (pointers.size === 1) {
|
|
5025
5105
|
mode = "pan";
|
|
5026
5106
|
panLast = { x: e.clientX, y: e.clientY };
|
|
5107
|
+
touchMomentum = createPanMomentumController(
|
|
5108
|
+
camera,
|
|
5109
|
+
onUpdate,
|
|
5110
|
+
touchPanSensitivity
|
|
5111
|
+
);
|
|
5027
5112
|
} else if (pointers.size === 2) {
|
|
5028
5113
|
const vals = Array.from(pointers.values());
|
|
5029
5114
|
const a = vals[0];
|
|
@@ -5051,6 +5136,7 @@ function attachApplePencilNavigation(options) {
|
|
|
5051
5136
|
panLast = { x: e.clientX, y: e.clientY };
|
|
5052
5137
|
camera.x += dx * touchPanSensitivity;
|
|
5053
5138
|
camera.y += dy * touchPanSensitivity;
|
|
5139
|
+
touchMomentum?.recordMove(dx, dy);
|
|
5054
5140
|
onUpdate();
|
|
5055
5141
|
e.preventDefault();
|
|
5056
5142
|
e.stopImmediatePropagation();
|
|
@@ -5097,12 +5183,20 @@ function attachApplePencilNavigation(options) {
|
|
|
5097
5183
|
if (!pointers.has(e.pointerId)) return;
|
|
5098
5184
|
pointers.delete(e.pointerId);
|
|
5099
5185
|
if (pointers.size === 0) {
|
|
5186
|
+
const wasPanning = mode === "pan";
|
|
5100
5187
|
mode = "idle";
|
|
5101
5188
|
panLast = null;
|
|
5189
|
+
if (wasPanning && touchMomentum) {
|
|
5190
|
+
touchMomentum.startMomentum();
|
|
5191
|
+
touchMomentum = null;
|
|
5192
|
+
}
|
|
5102
5193
|
} else if (pointers.size === 1 && mode === "pinch") {
|
|
5103
5194
|
mode = "pan";
|
|
5104
5195
|
const r = Array.from(pointers.values())[0];
|
|
5105
5196
|
panLast = r ?? null;
|
|
5197
|
+
if (touchMomentum) {
|
|
5198
|
+
touchMomentum.reset();
|
|
5199
|
+
}
|
|
5106
5200
|
}
|
|
5107
5201
|
e.preventDefault();
|
|
5108
5202
|
e.stopImmediatePropagation();
|
|
@@ -5112,6 +5206,10 @@ function attachApplePencilNavigation(options) {
|
|
|
5112
5206
|
element.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
5113
5207
|
element.addEventListener("pointercancel", onPointerUp, { capture: true });
|
|
5114
5208
|
return () => {
|
|
5209
|
+
if (touchMomentum) {
|
|
5210
|
+
touchMomentum.cancel();
|
|
5211
|
+
touchMomentum = null;
|
|
5212
|
+
}
|
|
5115
5213
|
element.removeEventListener("pointerdown", onPointerDown, { capture: true });
|
|
5116
5214
|
element.removeEventListener("pointermove", onPointerMove, { capture: true });
|
|
5117
5215
|
element.removeEventListener("pointerup", onPointerUp, { capture: true });
|
|
@@ -5164,6 +5262,7 @@ function attachViewportInput(options) {
|
|
|
5164
5262
|
let pinchStartZoom = 1;
|
|
5165
5263
|
let panLast = null;
|
|
5166
5264
|
let mousePanButton = null;
|
|
5265
|
+
let touchMomentum = null;
|
|
5167
5266
|
const onWheel = (e) => {
|
|
5168
5267
|
if (e.ctrlKey || e.metaKey) {
|
|
5169
5268
|
e.preventDefault();
|
|
@@ -5190,6 +5289,9 @@ function attachViewportInput(options) {
|
|
|
5190
5289
|
if (touchHandledElsewhere && e.pointerType === "touch") {
|
|
5191
5290
|
return;
|
|
5192
5291
|
}
|
|
5292
|
+
if (touchMomentum) {
|
|
5293
|
+
touchMomentum.cancel();
|
|
5294
|
+
}
|
|
5193
5295
|
const panOk = allowPrimaryPointerPan();
|
|
5194
5296
|
if (e.pointerType === "mouse" && e.button === 0) {
|
|
5195
5297
|
if (!panOk) {
|
|
@@ -5215,6 +5317,11 @@ function attachViewportInput(options) {
|
|
|
5215
5317
|
if (panOk) {
|
|
5216
5318
|
mode = "pan";
|
|
5217
5319
|
panLast = { x: e.clientX, y: e.clientY };
|
|
5320
|
+
touchMomentum = createPanMomentumController(
|
|
5321
|
+
camera,
|
|
5322
|
+
onUpdate,
|
|
5323
|
+
touchPanSensitivity
|
|
5324
|
+
);
|
|
5218
5325
|
e.preventDefault();
|
|
5219
5326
|
}
|
|
5220
5327
|
} else if (pointers.size === 2) {
|
|
@@ -5251,6 +5358,7 @@ function attachViewportInput(options) {
|
|
|
5251
5358
|
panLast = { x: e.clientX, y: e.clientY };
|
|
5252
5359
|
camera.x += dx * touchPanSensitivity;
|
|
5253
5360
|
camera.y += dy * touchPanSensitivity;
|
|
5361
|
+
touchMomentum?.recordMove(dx, dy);
|
|
5254
5362
|
onUpdate();
|
|
5255
5363
|
e.preventDefault();
|
|
5256
5364
|
return;
|
|
@@ -5302,15 +5410,27 @@ function attachViewportInput(options) {
|
|
|
5302
5410
|
}
|
|
5303
5411
|
pointers.delete(e.pointerId);
|
|
5304
5412
|
if (pointers.size === 0) {
|
|
5413
|
+
const wasPanning = mode === "pan";
|
|
5305
5414
|
mode = "idle";
|
|
5306
5415
|
panLast = null;
|
|
5416
|
+
if (wasPanning && touchMomentum) {
|
|
5417
|
+
touchMomentum.startMomentum();
|
|
5418
|
+
touchMomentum = null;
|
|
5419
|
+
}
|
|
5307
5420
|
} else if (pointers.size === 1 && mode === "pinch") {
|
|
5308
5421
|
mode = "pan";
|
|
5309
5422
|
const remaining = Array.from(pointers.values())[0];
|
|
5310
5423
|
panLast = remaining ?? null;
|
|
5424
|
+
if (touchMomentum) {
|
|
5425
|
+
touchMomentum.reset();
|
|
5426
|
+
}
|
|
5311
5427
|
}
|
|
5312
5428
|
};
|
|
5313
5429
|
const onPointerCancel = (e) => {
|
|
5430
|
+
if (touchMomentum) {
|
|
5431
|
+
touchMomentum.cancel();
|
|
5432
|
+
touchMomentum = null;
|
|
5433
|
+
}
|
|
5314
5434
|
onPointerUp(e);
|
|
5315
5435
|
};
|
|
5316
5436
|
wheelTarget.addEventListener("wheel", onWheel, { passive: false });
|
|
@@ -5319,6 +5439,10 @@ function attachViewportInput(options) {
|
|
|
5319
5439
|
element.addEventListener("pointerup", onPointerUp);
|
|
5320
5440
|
element.addEventListener("pointercancel", onPointerCancel);
|
|
5321
5441
|
return () => {
|
|
5442
|
+
if (touchMomentum) {
|
|
5443
|
+
touchMomentum.cancel();
|
|
5444
|
+
touchMomentum = null;
|
|
5445
|
+
}
|
|
5322
5446
|
wheelTarget.removeEventListener("wheel", onWheel);
|
|
5323
5447
|
element.removeEventListener("pointerdown", onPointerDown);
|
|
5324
5448
|
element.removeEventListener("pointermove", onPointerMove);
|
|
@@ -6200,6 +6324,7 @@ var SvgVectorRenderer = class {
|
|
|
6200
6324
|
svg;
|
|
6201
6325
|
rootG;
|
|
6202
6326
|
itemNodeCache = /* @__PURE__ */ new Map();
|
|
6327
|
+
liveOverlay = null;
|
|
6203
6328
|
resizeObserver;
|
|
6204
6329
|
constructor(options) {
|
|
6205
6330
|
this.container = options.container;
|
|
@@ -6234,6 +6359,50 @@ var SvgVectorRenderer = class {
|
|
|
6234
6359
|
const items = cullItemsByViewport(this.scene.getItems(), visible);
|
|
6235
6360
|
this.syncVisibleItems(items);
|
|
6236
6361
|
this.rootG.setAttribute("transform", formatCameraTransform(this.camera));
|
|
6362
|
+
this.keepLiveOverlayOnTop();
|
|
6363
|
+
}
|
|
6364
|
+
/**
|
|
6365
|
+
* Updates only the in-progress (live) stroke node without re-culling or
|
|
6366
|
+
* re-syncing the committed scene items. Drawing tools call this on every
|
|
6367
|
+
* pointer move, so it must stay O(1) regardless of how many items the
|
|
6368
|
+
* board already contains.
|
|
6369
|
+
*
|
|
6370
|
+
* Pass `null` to remove the live overlay (e.g. when the stroke is committed).
|
|
6371
|
+
*/
|
|
6372
|
+
renderLiveItem(item) {
|
|
6373
|
+
if (!item) {
|
|
6374
|
+
if (this.liveOverlay) {
|
|
6375
|
+
this.liveOverlay.g.remove();
|
|
6376
|
+
this.liveOverlay = null;
|
|
6377
|
+
}
|
|
6378
|
+
return;
|
|
6379
|
+
}
|
|
6380
|
+
if (!this.liveOverlay) {
|
|
6381
|
+
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
6382
|
+
g.setAttribute("data-live-overlay", "true");
|
|
6383
|
+
this.liveOverlay = {
|
|
6384
|
+
g,
|
|
6385
|
+
lastChildrenSvg: "",
|
|
6386
|
+
lastTransform: ""
|
|
6387
|
+
};
|
|
6388
|
+
this.rootG.appendChild(g);
|
|
6389
|
+
}
|
|
6390
|
+
const cached = this.liveOverlay;
|
|
6391
|
+
const t = formatItemPlacementTransform(item);
|
|
6392
|
+
if (cached.lastTransform !== t) {
|
|
6393
|
+
cached.g.setAttribute("transform", t);
|
|
6394
|
+
cached.lastTransform = t;
|
|
6395
|
+
}
|
|
6396
|
+
if (cached.lastChildrenSvg !== item.childrenSvg) {
|
|
6397
|
+
cached.g.innerHTML = item.childrenSvg;
|
|
6398
|
+
cached.lastChildrenSvg = item.childrenSvg;
|
|
6399
|
+
}
|
|
6400
|
+
this.keepLiveOverlayOnTop();
|
|
6401
|
+
}
|
|
6402
|
+
keepLiveOverlayOnTop() {
|
|
6403
|
+
if (this.liveOverlay && this.rootG.lastChild !== this.liveOverlay.g) {
|
|
6404
|
+
this.rootG.appendChild(this.liveOverlay.g);
|
|
6405
|
+
}
|
|
6237
6406
|
}
|
|
6238
6407
|
syncVisibleItems(items) {
|
|
6239
6408
|
const visibleIds = /* @__PURE__ */ new Set();
|
|
@@ -6278,6 +6447,7 @@ var SvgVectorRenderer = class {
|
|
|
6278
6447
|
destroy() {
|
|
6279
6448
|
this.resizeObserver.disconnect();
|
|
6280
6449
|
this.itemNodeCache.clear();
|
|
6450
|
+
this.liveOverlay = null;
|
|
6281
6451
|
this.svg.remove();
|
|
6282
6452
|
}
|
|
6283
6453
|
/** Toggle whether the scene SVG receives pointer events (vs overlay handling them). */
|
|
@@ -7754,8 +7924,6 @@ var VectorViewport = forwardRef(
|
|
|
7754
7924
|
const hiddenIds = new Set(eraserPreviewIds);
|
|
7755
7925
|
return resolvedItems.filter((it) => !hiddenIds.has(it.id));
|
|
7756
7926
|
}, [eraserPreviewIds, resolvedItems]);
|
|
7757
|
-
const resolvedSceneItemsRef = useRef(resolvedSceneItems);
|
|
7758
|
-
resolvedSceneItemsRef.current = resolvedSceneItems;
|
|
7759
7927
|
const livePenStrokeItemRef = useRef(null);
|
|
7760
7928
|
const [eraserActive, setEraserActive] = useState(false);
|
|
7761
7929
|
const [editingTextId, setEditingTextId] = useState(null);
|
|
@@ -7976,12 +8144,9 @@ var VectorViewport = forwardRef(
|
|
|
7976
8144
|
const renderSceneWithLivePenStroke = useCallback(
|
|
7977
8145
|
(item) => {
|
|
7978
8146
|
livePenStrokeItemRef.current = item;
|
|
7979
|
-
const scene = sceneRef.current;
|
|
7980
8147
|
const renderer = rendererRef.current;
|
|
7981
|
-
if (!
|
|
7982
|
-
|
|
7983
|
-
scene.setItems(item ? [...base2, item] : base2);
|
|
7984
|
-
renderer.render();
|
|
8148
|
+
if (!renderer) return;
|
|
8149
|
+
renderer.renderLiveItem(item);
|
|
7985
8150
|
},
|
|
7986
8151
|
[]
|
|
7987
8152
|
);
|
|
@@ -8212,16 +8377,15 @@ var VectorViewport = forwardRef(
|
|
|
8212
8377
|
}, []);
|
|
8213
8378
|
useEffect(() => {
|
|
8214
8379
|
const scene = sceneRef.current;
|
|
8380
|
+
const renderer = rendererRef.current;
|
|
8215
8381
|
if (scene) {
|
|
8216
8382
|
const live = livePenStrokeItemRef.current;
|
|
8217
8383
|
if (live && resolvedSceneItems.some((it) => it.id === live.id)) {
|
|
8218
8384
|
livePenStrokeItemRef.current = null;
|
|
8219
8385
|
}
|
|
8220
|
-
|
|
8221
|
-
scene.setItems(
|
|
8222
|
-
currentLive ? [...resolvedSceneItems, currentLive] : resolvedSceneItems
|
|
8223
|
-
);
|
|
8386
|
+
scene.setItems(resolvedSceneItems);
|
|
8224
8387
|
renderFrame();
|
|
8388
|
+
renderer?.renderLiveItem(livePenStrokeItemRef.current);
|
|
8225
8389
|
}
|
|
8226
8390
|
}, [resolvedSceneItems, renderFrame]);
|
|
8227
8391
|
useEffect(() => {
|
|
@@ -8797,11 +8961,12 @@ var VectorViewport = forwardRef(
|
|
|
8797
8961
|
setEditingTextId(null);
|
|
8798
8962
|
}, []);
|
|
8799
8963
|
const placeImageFilesAtWorld = useCallback(
|
|
8800
|
-
async (files, worldX, worldY) => {
|
|
8964
|
+
async (files, worldX, worldY, options) => {
|
|
8801
8965
|
const change = onItemsChangeRef.current;
|
|
8802
8966
|
if (!change || files.length === 0) return;
|
|
8803
8967
|
const store = imageStoreRef.current;
|
|
8804
8968
|
if (!store) return;
|
|
8969
|
+
const stackBelowExistingItems = options?.stackBelowExistingItems ?? true;
|
|
8805
8970
|
try {
|
|
8806
8971
|
const pdfFiles = files.filter((file) => file.type === "application/pdf");
|
|
8807
8972
|
if (pdfFiles.length > 0) {
|
|
@@ -8827,7 +8992,7 @@ var VectorViewport = forwardRef(
|
|
|
8827
8992
|
}
|
|
8828
8993
|
const result = await ingestAssetFilesToSceneItems({
|
|
8829
8994
|
files,
|
|
8830
|
-
existingItems: itemsRef.current,
|
|
8995
|
+
existingItems: stackBelowExistingItems ? itemsRef.current : [],
|
|
8831
8996
|
worldCenter: {
|
|
8832
8997
|
x: worldX,
|
|
8833
8998
|
y: worldY
|
|
@@ -8914,7 +9079,9 @@ var VectorViewport = forwardRef(
|
|
|
8914
9079
|
);
|
|
8915
9080
|
if (files.length === 0) return;
|
|
8916
9081
|
const { worldX, worldY } = screenToWorld(e.clientX, e.clientY);
|
|
8917
|
-
await placeImageFilesAtWorld(files, worldX, worldY
|
|
9082
|
+
await placeImageFilesAtWorld(files, worldX, worldY, {
|
|
9083
|
+
stackBelowExistingItems: false
|
|
9084
|
+
});
|
|
8918
9085
|
},
|
|
8919
9086
|
[screenToWorld, placeImageFilesAtWorld]
|
|
8920
9087
|
);
|