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.cjs
CHANGED
|
@@ -4970,6 +4970,78 @@ var Camera2D = class {
|
|
|
4970
4970
|
}
|
|
4971
4971
|
};
|
|
4972
4972
|
|
|
4973
|
+
// src/input/pan-momentum.ts
|
|
4974
|
+
var VELOCITY_SAMPLE_WINDOW = 80;
|
|
4975
|
+
var FRICTION = 0.94;
|
|
4976
|
+
var MIN_VELOCITY = 0.3;
|
|
4977
|
+
function createPanMomentumController(camera, onUpdate, sensitivity) {
|
|
4978
|
+
const samples = [];
|
|
4979
|
+
let animationFrameId = null;
|
|
4980
|
+
const cancel = () => {
|
|
4981
|
+
if (animationFrameId !== null) {
|
|
4982
|
+
cancelAnimationFrame(animationFrameId);
|
|
4983
|
+
animationFrameId = null;
|
|
4984
|
+
}
|
|
4985
|
+
};
|
|
4986
|
+
const reset = () => {
|
|
4987
|
+
cancel();
|
|
4988
|
+
samples.length = 0;
|
|
4989
|
+
};
|
|
4990
|
+
const recordMove = (dx, dy) => {
|
|
4991
|
+
const now = performance.now();
|
|
4992
|
+
samples.push({ dx, dy, time: now });
|
|
4993
|
+
const cutoff = now - VELOCITY_SAMPLE_WINDOW;
|
|
4994
|
+
let oldestSample = samples[0];
|
|
4995
|
+
while (oldestSample && oldestSample.time < cutoff) {
|
|
4996
|
+
samples.shift();
|
|
4997
|
+
oldestSample = samples[0];
|
|
4998
|
+
}
|
|
4999
|
+
};
|
|
5000
|
+
const computeReleaseVelocity = () => {
|
|
5001
|
+
if (samples.length < 2) return { vx: 0, vy: 0 };
|
|
5002
|
+
const first = samples[0];
|
|
5003
|
+
const last = samples[samples.length - 1];
|
|
5004
|
+
if (!first || !last) return { vx: 0, vy: 0 };
|
|
5005
|
+
const elapsed = last.time - first.time;
|
|
5006
|
+
if (elapsed < 4) return { vx: 0, vy: 0 };
|
|
5007
|
+
let totalDx = 0;
|
|
5008
|
+
let totalDy = 0;
|
|
5009
|
+
for (const sample of samples) {
|
|
5010
|
+
totalDx += sample.dx;
|
|
5011
|
+
totalDy += sample.dy;
|
|
5012
|
+
}
|
|
5013
|
+
const msPerFrame = 1e3 / 60;
|
|
5014
|
+
return {
|
|
5015
|
+
vx: totalDx / elapsed * msPerFrame * sensitivity,
|
|
5016
|
+
vy: totalDy / elapsed * msPerFrame * sensitivity
|
|
5017
|
+
};
|
|
5018
|
+
};
|
|
5019
|
+
const startMomentum = () => {
|
|
5020
|
+
cancel();
|
|
5021
|
+
const { vx, vy } = computeReleaseVelocity();
|
|
5022
|
+
samples.length = 0;
|
|
5023
|
+
if (Math.abs(vx) < MIN_VELOCITY && Math.abs(vy) < MIN_VELOCITY) {
|
|
5024
|
+
return;
|
|
5025
|
+
}
|
|
5026
|
+
let currentVx = vx;
|
|
5027
|
+
let currentVy = vy;
|
|
5028
|
+
const animate = () => {
|
|
5029
|
+
currentVx *= FRICTION;
|
|
5030
|
+
currentVy *= FRICTION;
|
|
5031
|
+
if (Math.abs(currentVx) < MIN_VELOCITY && Math.abs(currentVy) < MIN_VELOCITY) {
|
|
5032
|
+
animationFrameId = null;
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
camera.x += currentVx;
|
|
5036
|
+
camera.y += currentVy;
|
|
5037
|
+
onUpdate();
|
|
5038
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
5039
|
+
};
|
|
5040
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
5041
|
+
};
|
|
5042
|
+
return { recordMove, startMomentum, cancel, reset };
|
|
5043
|
+
}
|
|
5044
|
+
|
|
4973
5045
|
// src/input/apple-pencil-navigation.ts
|
|
4974
5046
|
var DRAWING_LIKE_TOOLS = /* @__PURE__ */ new Set([
|
|
4975
5047
|
"draw",
|
|
@@ -5000,6 +5072,7 @@ function attachApplePencilNavigation(options) {
|
|
|
5000
5072
|
let pinchStartDist = 0;
|
|
5001
5073
|
let pinchStartZoom = 1;
|
|
5002
5074
|
let panLast = null;
|
|
5075
|
+
let touchMomentum = null;
|
|
5003
5076
|
const shouldIntercept = (e) => {
|
|
5004
5077
|
if (e.pointerType !== "touch") return false;
|
|
5005
5078
|
const tool = getCurrentToolId();
|
|
@@ -5018,6 +5091,10 @@ function attachApplePencilNavigation(options) {
|
|
|
5018
5091
|
pointers.clear();
|
|
5019
5092
|
mode = "idle";
|
|
5020
5093
|
panLast = null;
|
|
5094
|
+
if (touchMomentum) {
|
|
5095
|
+
touchMomentum.cancel();
|
|
5096
|
+
touchMomentum = null;
|
|
5097
|
+
}
|
|
5021
5098
|
return;
|
|
5022
5099
|
}
|
|
5023
5100
|
if (e.pointerType === "touch" && activePenPointerIds.size > 0) {
|
|
@@ -5027,10 +5104,18 @@ function attachApplePencilNavigation(options) {
|
|
|
5027
5104
|
return;
|
|
5028
5105
|
}
|
|
5029
5106
|
if (!shouldIntercept(e)) return;
|
|
5107
|
+
if (touchMomentum) {
|
|
5108
|
+
touchMomentum.cancel();
|
|
5109
|
+
}
|
|
5030
5110
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
5031
5111
|
if (pointers.size === 1) {
|
|
5032
5112
|
mode = "pan";
|
|
5033
5113
|
panLast = { x: e.clientX, y: e.clientY };
|
|
5114
|
+
touchMomentum = createPanMomentumController(
|
|
5115
|
+
camera,
|
|
5116
|
+
onUpdate,
|
|
5117
|
+
touchPanSensitivity
|
|
5118
|
+
);
|
|
5034
5119
|
} else if (pointers.size === 2) {
|
|
5035
5120
|
const vals = Array.from(pointers.values());
|
|
5036
5121
|
const a = vals[0];
|
|
@@ -5058,6 +5143,7 @@ function attachApplePencilNavigation(options) {
|
|
|
5058
5143
|
panLast = { x: e.clientX, y: e.clientY };
|
|
5059
5144
|
camera.x += dx * touchPanSensitivity;
|
|
5060
5145
|
camera.y += dy * touchPanSensitivity;
|
|
5146
|
+
touchMomentum?.recordMove(dx, dy);
|
|
5061
5147
|
onUpdate();
|
|
5062
5148
|
e.preventDefault();
|
|
5063
5149
|
e.stopImmediatePropagation();
|
|
@@ -5104,12 +5190,20 @@ function attachApplePencilNavigation(options) {
|
|
|
5104
5190
|
if (!pointers.has(e.pointerId)) return;
|
|
5105
5191
|
pointers.delete(e.pointerId);
|
|
5106
5192
|
if (pointers.size === 0) {
|
|
5193
|
+
const wasPanning = mode === "pan";
|
|
5107
5194
|
mode = "idle";
|
|
5108
5195
|
panLast = null;
|
|
5196
|
+
if (wasPanning && touchMomentum) {
|
|
5197
|
+
touchMomentum.startMomentum();
|
|
5198
|
+
touchMomentum = null;
|
|
5199
|
+
}
|
|
5109
5200
|
} else if (pointers.size === 1 && mode === "pinch") {
|
|
5110
5201
|
mode = "pan";
|
|
5111
5202
|
const r = Array.from(pointers.values())[0];
|
|
5112
5203
|
panLast = r ?? null;
|
|
5204
|
+
if (touchMomentum) {
|
|
5205
|
+
touchMomentum.reset();
|
|
5206
|
+
}
|
|
5113
5207
|
}
|
|
5114
5208
|
e.preventDefault();
|
|
5115
5209
|
e.stopImmediatePropagation();
|
|
@@ -5119,6 +5213,10 @@ function attachApplePencilNavigation(options) {
|
|
|
5119
5213
|
element.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
5120
5214
|
element.addEventListener("pointercancel", onPointerUp, { capture: true });
|
|
5121
5215
|
return () => {
|
|
5216
|
+
if (touchMomentum) {
|
|
5217
|
+
touchMomentum.cancel();
|
|
5218
|
+
touchMomentum = null;
|
|
5219
|
+
}
|
|
5122
5220
|
element.removeEventListener("pointerdown", onPointerDown, { capture: true });
|
|
5123
5221
|
element.removeEventListener("pointermove", onPointerMove, { capture: true });
|
|
5124
5222
|
element.removeEventListener("pointerup", onPointerUp, { capture: true });
|
|
@@ -5171,6 +5269,7 @@ function attachViewportInput(options) {
|
|
|
5171
5269
|
let pinchStartZoom = 1;
|
|
5172
5270
|
let panLast = null;
|
|
5173
5271
|
let mousePanButton = null;
|
|
5272
|
+
let touchMomentum = null;
|
|
5174
5273
|
const onWheel = (e) => {
|
|
5175
5274
|
if (e.ctrlKey || e.metaKey) {
|
|
5176
5275
|
e.preventDefault();
|
|
@@ -5197,6 +5296,9 @@ function attachViewportInput(options) {
|
|
|
5197
5296
|
if (touchHandledElsewhere && e.pointerType === "touch") {
|
|
5198
5297
|
return;
|
|
5199
5298
|
}
|
|
5299
|
+
if (touchMomentum) {
|
|
5300
|
+
touchMomentum.cancel();
|
|
5301
|
+
}
|
|
5200
5302
|
const panOk = allowPrimaryPointerPan();
|
|
5201
5303
|
if (e.pointerType === "mouse" && e.button === 0) {
|
|
5202
5304
|
if (!panOk) {
|
|
@@ -5222,6 +5324,11 @@ function attachViewportInput(options) {
|
|
|
5222
5324
|
if (panOk) {
|
|
5223
5325
|
mode = "pan";
|
|
5224
5326
|
panLast = { x: e.clientX, y: e.clientY };
|
|
5327
|
+
touchMomentum = createPanMomentumController(
|
|
5328
|
+
camera,
|
|
5329
|
+
onUpdate,
|
|
5330
|
+
touchPanSensitivity
|
|
5331
|
+
);
|
|
5225
5332
|
e.preventDefault();
|
|
5226
5333
|
}
|
|
5227
5334
|
} else if (pointers.size === 2) {
|
|
@@ -5258,6 +5365,7 @@ function attachViewportInput(options) {
|
|
|
5258
5365
|
panLast = { x: e.clientX, y: e.clientY };
|
|
5259
5366
|
camera.x += dx * touchPanSensitivity;
|
|
5260
5367
|
camera.y += dy * touchPanSensitivity;
|
|
5368
|
+
touchMomentum?.recordMove(dx, dy);
|
|
5261
5369
|
onUpdate();
|
|
5262
5370
|
e.preventDefault();
|
|
5263
5371
|
return;
|
|
@@ -5309,15 +5417,27 @@ function attachViewportInput(options) {
|
|
|
5309
5417
|
}
|
|
5310
5418
|
pointers.delete(e.pointerId);
|
|
5311
5419
|
if (pointers.size === 0) {
|
|
5420
|
+
const wasPanning = mode === "pan";
|
|
5312
5421
|
mode = "idle";
|
|
5313
5422
|
panLast = null;
|
|
5423
|
+
if (wasPanning && touchMomentum) {
|
|
5424
|
+
touchMomentum.startMomentum();
|
|
5425
|
+
touchMomentum = null;
|
|
5426
|
+
}
|
|
5314
5427
|
} else if (pointers.size === 1 && mode === "pinch") {
|
|
5315
5428
|
mode = "pan";
|
|
5316
5429
|
const remaining = Array.from(pointers.values())[0];
|
|
5317
5430
|
panLast = remaining ?? null;
|
|
5431
|
+
if (touchMomentum) {
|
|
5432
|
+
touchMomentum.reset();
|
|
5433
|
+
}
|
|
5318
5434
|
}
|
|
5319
5435
|
};
|
|
5320
5436
|
const onPointerCancel = (e) => {
|
|
5437
|
+
if (touchMomentum) {
|
|
5438
|
+
touchMomentum.cancel();
|
|
5439
|
+
touchMomentum = null;
|
|
5440
|
+
}
|
|
5321
5441
|
onPointerUp(e);
|
|
5322
5442
|
};
|
|
5323
5443
|
wheelTarget.addEventListener("wheel", onWheel, { passive: false });
|
|
@@ -5326,6 +5446,10 @@ function attachViewportInput(options) {
|
|
|
5326
5446
|
element.addEventListener("pointerup", onPointerUp);
|
|
5327
5447
|
element.addEventListener("pointercancel", onPointerCancel);
|
|
5328
5448
|
return () => {
|
|
5449
|
+
if (touchMomentum) {
|
|
5450
|
+
touchMomentum.cancel();
|
|
5451
|
+
touchMomentum = null;
|
|
5452
|
+
}
|
|
5329
5453
|
wheelTarget.removeEventListener("wheel", onWheel);
|
|
5330
5454
|
element.removeEventListener("pointerdown", onPointerDown);
|
|
5331
5455
|
element.removeEventListener("pointermove", onPointerMove);
|
|
@@ -6207,6 +6331,7 @@ var SvgVectorRenderer = class {
|
|
|
6207
6331
|
svg;
|
|
6208
6332
|
rootG;
|
|
6209
6333
|
itemNodeCache = /* @__PURE__ */ new Map();
|
|
6334
|
+
liveOverlay = null;
|
|
6210
6335
|
resizeObserver;
|
|
6211
6336
|
constructor(options) {
|
|
6212
6337
|
this.container = options.container;
|
|
@@ -6241,6 +6366,50 @@ var SvgVectorRenderer = class {
|
|
|
6241
6366
|
const items = cullItemsByViewport(this.scene.getItems(), visible);
|
|
6242
6367
|
this.syncVisibleItems(items);
|
|
6243
6368
|
this.rootG.setAttribute("transform", formatCameraTransform(this.camera));
|
|
6369
|
+
this.keepLiveOverlayOnTop();
|
|
6370
|
+
}
|
|
6371
|
+
/**
|
|
6372
|
+
* Updates only the in-progress (live) stroke node without re-culling or
|
|
6373
|
+
* re-syncing the committed scene items. Drawing tools call this on every
|
|
6374
|
+
* pointer move, so it must stay O(1) regardless of how many items the
|
|
6375
|
+
* board already contains.
|
|
6376
|
+
*
|
|
6377
|
+
* Pass `null` to remove the live overlay (e.g. when the stroke is committed).
|
|
6378
|
+
*/
|
|
6379
|
+
renderLiveItem(item) {
|
|
6380
|
+
if (!item) {
|
|
6381
|
+
if (this.liveOverlay) {
|
|
6382
|
+
this.liveOverlay.g.remove();
|
|
6383
|
+
this.liveOverlay = null;
|
|
6384
|
+
}
|
|
6385
|
+
return;
|
|
6386
|
+
}
|
|
6387
|
+
if (!this.liveOverlay) {
|
|
6388
|
+
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
6389
|
+
g.setAttribute("data-live-overlay", "true");
|
|
6390
|
+
this.liveOverlay = {
|
|
6391
|
+
g,
|
|
6392
|
+
lastChildrenSvg: "",
|
|
6393
|
+
lastTransform: ""
|
|
6394
|
+
};
|
|
6395
|
+
this.rootG.appendChild(g);
|
|
6396
|
+
}
|
|
6397
|
+
const cached = this.liveOverlay;
|
|
6398
|
+
const t = formatItemPlacementTransform(item);
|
|
6399
|
+
if (cached.lastTransform !== t) {
|
|
6400
|
+
cached.g.setAttribute("transform", t);
|
|
6401
|
+
cached.lastTransform = t;
|
|
6402
|
+
}
|
|
6403
|
+
if (cached.lastChildrenSvg !== item.childrenSvg) {
|
|
6404
|
+
cached.g.innerHTML = item.childrenSvg;
|
|
6405
|
+
cached.lastChildrenSvg = item.childrenSvg;
|
|
6406
|
+
}
|
|
6407
|
+
this.keepLiveOverlayOnTop();
|
|
6408
|
+
}
|
|
6409
|
+
keepLiveOverlayOnTop() {
|
|
6410
|
+
if (this.liveOverlay && this.rootG.lastChild !== this.liveOverlay.g) {
|
|
6411
|
+
this.rootG.appendChild(this.liveOverlay.g);
|
|
6412
|
+
}
|
|
6244
6413
|
}
|
|
6245
6414
|
syncVisibleItems(items) {
|
|
6246
6415
|
const visibleIds = /* @__PURE__ */ new Set();
|
|
@@ -6285,6 +6454,7 @@ var SvgVectorRenderer = class {
|
|
|
6285
6454
|
destroy() {
|
|
6286
6455
|
this.resizeObserver.disconnect();
|
|
6287
6456
|
this.itemNodeCache.clear();
|
|
6457
|
+
this.liveOverlay = null;
|
|
6288
6458
|
this.svg.remove();
|
|
6289
6459
|
}
|
|
6290
6460
|
/** Toggle whether the scene SVG receives pointer events (vs overlay handling them). */
|
|
@@ -7761,8 +7931,6 @@ var VectorViewport = react.forwardRef(
|
|
|
7761
7931
|
const hiddenIds = new Set(eraserPreviewIds);
|
|
7762
7932
|
return resolvedItems.filter((it) => !hiddenIds.has(it.id));
|
|
7763
7933
|
}, [eraserPreviewIds, resolvedItems]);
|
|
7764
|
-
const resolvedSceneItemsRef = react.useRef(resolvedSceneItems);
|
|
7765
|
-
resolvedSceneItemsRef.current = resolvedSceneItems;
|
|
7766
7934
|
const livePenStrokeItemRef = react.useRef(null);
|
|
7767
7935
|
const [eraserActive, setEraserActive] = react.useState(false);
|
|
7768
7936
|
const [editingTextId, setEditingTextId] = react.useState(null);
|
|
@@ -7983,12 +8151,9 @@ var VectorViewport = react.forwardRef(
|
|
|
7983
8151
|
const renderSceneWithLivePenStroke = react.useCallback(
|
|
7984
8152
|
(item) => {
|
|
7985
8153
|
livePenStrokeItemRef.current = item;
|
|
7986
|
-
const scene = sceneRef.current;
|
|
7987
8154
|
const renderer = rendererRef.current;
|
|
7988
|
-
if (!
|
|
7989
|
-
|
|
7990
|
-
scene.setItems(item ? [...base2, item] : base2);
|
|
7991
|
-
renderer.render();
|
|
8155
|
+
if (!renderer) return;
|
|
8156
|
+
renderer.renderLiveItem(item);
|
|
7992
8157
|
},
|
|
7993
8158
|
[]
|
|
7994
8159
|
);
|
|
@@ -8219,16 +8384,15 @@ var VectorViewport = react.forwardRef(
|
|
|
8219
8384
|
}, []);
|
|
8220
8385
|
react.useEffect(() => {
|
|
8221
8386
|
const scene = sceneRef.current;
|
|
8387
|
+
const renderer = rendererRef.current;
|
|
8222
8388
|
if (scene) {
|
|
8223
8389
|
const live = livePenStrokeItemRef.current;
|
|
8224
8390
|
if (live && resolvedSceneItems.some((it) => it.id === live.id)) {
|
|
8225
8391
|
livePenStrokeItemRef.current = null;
|
|
8226
8392
|
}
|
|
8227
|
-
|
|
8228
|
-
scene.setItems(
|
|
8229
|
-
currentLive ? [...resolvedSceneItems, currentLive] : resolvedSceneItems
|
|
8230
|
-
);
|
|
8393
|
+
scene.setItems(resolvedSceneItems);
|
|
8231
8394
|
renderFrame();
|
|
8395
|
+
renderer?.renderLiveItem(livePenStrokeItemRef.current);
|
|
8232
8396
|
}
|
|
8233
8397
|
}, [resolvedSceneItems, renderFrame]);
|
|
8234
8398
|
react.useEffect(() => {
|
|
@@ -8804,11 +8968,12 @@ var VectorViewport = react.forwardRef(
|
|
|
8804
8968
|
setEditingTextId(null);
|
|
8805
8969
|
}, []);
|
|
8806
8970
|
const placeImageFilesAtWorld = react.useCallback(
|
|
8807
|
-
async (files, worldX, worldY) => {
|
|
8971
|
+
async (files, worldX, worldY, options) => {
|
|
8808
8972
|
const change = onItemsChangeRef.current;
|
|
8809
8973
|
if (!change || files.length === 0) return;
|
|
8810
8974
|
const store = imageStoreRef.current;
|
|
8811
8975
|
if (!store) return;
|
|
8976
|
+
const stackBelowExistingItems = options?.stackBelowExistingItems ?? true;
|
|
8812
8977
|
try {
|
|
8813
8978
|
const pdfFiles = files.filter((file) => file.type === "application/pdf");
|
|
8814
8979
|
if (pdfFiles.length > 0) {
|
|
@@ -8834,7 +8999,7 @@ var VectorViewport = react.forwardRef(
|
|
|
8834
8999
|
}
|
|
8835
9000
|
const result = await ingestAssetFilesToSceneItems({
|
|
8836
9001
|
files,
|
|
8837
|
-
existingItems: itemsRef.current,
|
|
9002
|
+
existingItems: stackBelowExistingItems ? itemsRef.current : [],
|
|
8838
9003
|
worldCenter: {
|
|
8839
9004
|
x: worldX,
|
|
8840
9005
|
y: worldY
|
|
@@ -8921,7 +9086,9 @@ var VectorViewport = react.forwardRef(
|
|
|
8921
9086
|
);
|
|
8922
9087
|
if (files.length === 0) return;
|
|
8923
9088
|
const { worldX, worldY } = screenToWorld(e.clientX, e.clientY);
|
|
8924
|
-
await placeImageFilesAtWorld(files, worldX, worldY
|
|
9089
|
+
await placeImageFilesAtWorld(files, worldX, worldY, {
|
|
9090
|
+
stackBelowExistingItems: false
|
|
9091
|
+
});
|
|
8925
9092
|
},
|
|
8926
9093
|
[screenToWorld, placeImageFilesAtWorld]
|
|
8927
9094
|
);
|