@fieldnotes/core 0.53.0 → 0.55.0
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 +545 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +291 -34
- package/dist/index.d.ts +291 -34
- package/dist/index.js +536 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6835,6 +6835,7 @@ var RenderLoop = class {
|
|
|
6835
6835
|
// set on recenter/viewport-change; consumed by the grid block
|
|
6836
6836
|
stats = new RenderStats();
|
|
6837
6837
|
layerGroups = /* @__PURE__ */ new Map();
|
|
6838
|
+
overlays = /* @__PURE__ */ new Set();
|
|
6838
6839
|
gridCacheCanvas = null;
|
|
6839
6840
|
gridCacheCtx = null;
|
|
6840
6841
|
lastGridRefs = [];
|
|
@@ -6893,6 +6894,29 @@ var RenderLoop = class {
|
|
|
6893
6894
|
getStats() {
|
|
6894
6895
|
return this.stats.getSnapshot();
|
|
6895
6896
|
}
|
|
6897
|
+
/**
|
|
6898
|
+
* Registers a viewport overlay drawn beneath the active tool's own overlay.
|
|
6899
|
+
* Returns an idempotent unsubscribe that also erases the overlay's last
|
|
6900
|
+
* frame.
|
|
6901
|
+
*/
|
|
6902
|
+
registerOverlay(draw) {
|
|
6903
|
+
this.overlays.add(draw);
|
|
6904
|
+
this.requestRender();
|
|
6905
|
+
return () => {
|
|
6906
|
+
if (this.overlays.delete(draw)) this.requestRender();
|
|
6907
|
+
};
|
|
6908
|
+
}
|
|
6909
|
+
drawRegisteredOverlays(ctx) {
|
|
6910
|
+
for (const draw of this.overlays) {
|
|
6911
|
+
ctx.save();
|
|
6912
|
+
try {
|
|
6913
|
+
draw(ctx);
|
|
6914
|
+
} catch {
|
|
6915
|
+
} finally {
|
|
6916
|
+
ctx.restore();
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6896
6920
|
compositeLayerCache(ctx, layerId, opacity) {
|
|
6897
6921
|
const cached = this.layerCache.getCanvas(layerId);
|
|
6898
6922
|
const offset = this.marginViewport.compositeOffset(
|
|
@@ -7026,7 +7050,8 @@ var RenderLoop = class {
|
|
|
7026
7050
|
}
|
|
7027
7051
|
const activeTool = this.toolManager.activeTool;
|
|
7028
7052
|
const overlayOrder = visibleElements.length + 1;
|
|
7029
|
-
|
|
7053
|
+
const hasOverlay = activeTool?.renderOverlay !== void 0 || this.overlays.size > 0;
|
|
7054
|
+
if (hybridActive && hasOverlay) hybridOrders.add(overlayOrder);
|
|
7030
7055
|
this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
|
|
7031
7056
|
for (const [layerId, elements] of this.layerGroups) {
|
|
7032
7057
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
@@ -7136,7 +7161,7 @@ var RenderLoop = class {
|
|
|
7136
7161
|
hybridCtx.restore();
|
|
7137
7162
|
}
|
|
7138
7163
|
const overlayT0 = performance.now();
|
|
7139
|
-
if (hybridActive &&
|
|
7164
|
+
if (hybridActive && hasOverlay) {
|
|
7140
7165
|
const overlayCtx = this.hybridSurface.getContext(overlayOrder);
|
|
7141
7166
|
if (overlayCtx) {
|
|
7142
7167
|
overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
@@ -7144,11 +7169,13 @@ var RenderLoop = class {
|
|
|
7144
7169
|
overlayCtx.scale(dpr, dpr);
|
|
7145
7170
|
overlayCtx.translate(this.camera.position.x, this.camera.position.y);
|
|
7146
7171
|
overlayCtx.scale(this.camera.zoom, this.camera.zoom);
|
|
7147
|
-
|
|
7172
|
+
this.drawRegisteredOverlays(overlayCtx);
|
|
7173
|
+
if (activeTool?.renderOverlay) activeTool.renderOverlay(overlayCtx);
|
|
7148
7174
|
overlayCtx.restore();
|
|
7149
7175
|
}
|
|
7150
|
-
} else if (
|
|
7151
|
-
|
|
7176
|
+
} else if (hasOverlay) {
|
|
7177
|
+
this.drawRegisteredOverlays(ctx);
|
|
7178
|
+
if (activeTool?.renderOverlay) activeTool.renderOverlay(ctx);
|
|
7152
7179
|
}
|
|
7153
7180
|
const overlayMs = performance.now() - overlayT0;
|
|
7154
7181
|
ctx.restore();
|
|
@@ -8142,6 +8169,17 @@ var Viewport = class {
|
|
|
8142
8169
|
requestRender() {
|
|
8143
8170
|
this.renderLoop.requestRender();
|
|
8144
8171
|
}
|
|
8172
|
+
/**
|
|
8173
|
+
* Registers a world-space overlay drawn above elements on every frame,
|
|
8174
|
+
* regardless of the active tool — the surface for remote presence visuals
|
|
8175
|
+
* such as laser trails, cursors, pings, and shared rulers. Overlays draw
|
|
8176
|
+
* beneath the active tool's own `renderOverlay` and never touch elements,
|
|
8177
|
+
* history, or persisted state. Returns an idempotent unsubscribe that also
|
|
8178
|
+
* erases the overlay's last frame.
|
|
8179
|
+
*/
|
|
8180
|
+
registerOverlay(draw) {
|
|
8181
|
+
return this.renderLoop.registerOverlay(draw);
|
|
8182
|
+
}
|
|
8145
8183
|
exportState() {
|
|
8146
8184
|
return exportState(
|
|
8147
8185
|
this.store.snapshot(),
|
|
@@ -8489,6 +8527,324 @@ var Viewport = class {
|
|
|
8489
8527
|
}
|
|
8490
8528
|
};
|
|
8491
8529
|
|
|
8530
|
+
// src/canvas/remote-laser-overlay.ts
|
|
8531
|
+
var LASER_TRAIL_PRESENCE_KIND = "laser";
|
|
8532
|
+
function isFinitePoint(value) {
|
|
8533
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8534
|
+
const point = value;
|
|
8535
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
8536
|
+
}
|
|
8537
|
+
function isLaserTrailPresence(data) {
|
|
8538
|
+
if (typeof data !== "object" || data === null) return false;
|
|
8539
|
+
const payload = data;
|
|
8540
|
+
if (payload.kind !== LASER_TRAIL_PRESENCE_KIND) return false;
|
|
8541
|
+
if (!Array.isArray(payload.points) || !payload.points.every(isFinitePoint)) return false;
|
|
8542
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
8543
|
+
if (payload.width !== void 0 && !(typeof payload.width === "number" && payload.width > 0)) {
|
|
8544
|
+
return false;
|
|
8545
|
+
}
|
|
8546
|
+
if (payload.fadeMs !== void 0 && !(typeof payload.fadeMs === "number" && payload.fadeMs > 0)) {
|
|
8547
|
+
return false;
|
|
8548
|
+
}
|
|
8549
|
+
return true;
|
|
8550
|
+
}
|
|
8551
|
+
function toLaserTrailPresence(emission) {
|
|
8552
|
+
return {
|
|
8553
|
+
kind: LASER_TRAIL_PRESENCE_KIND,
|
|
8554
|
+
points: emission.points.map((point) => ({ x: point.x, y: point.y })),
|
|
8555
|
+
color: emission.color,
|
|
8556
|
+
width: emission.width,
|
|
8557
|
+
fadeMs: emission.fadeMs
|
|
8558
|
+
};
|
|
8559
|
+
}
|
|
8560
|
+
var DEFAULT_COLOR = "#ff3b30";
|
|
8561
|
+
var DEFAULT_WIDTH = 4;
|
|
8562
|
+
var DEFAULT_FADE_MS = 1200;
|
|
8563
|
+
var DEFAULT_MAX_POINTS = 512;
|
|
8564
|
+
var RemoteLaserOverlay = class {
|
|
8565
|
+
host;
|
|
8566
|
+
color;
|
|
8567
|
+
width;
|
|
8568
|
+
fadeMs;
|
|
8569
|
+
maxPointsPerSender;
|
|
8570
|
+
trails = /* @__PURE__ */ new Map();
|
|
8571
|
+
unregister;
|
|
8572
|
+
rafId = null;
|
|
8573
|
+
disposed = false;
|
|
8574
|
+
constructor(host, options = {}) {
|
|
8575
|
+
this.host = host;
|
|
8576
|
+
this.color = options.color ?? DEFAULT_COLOR;
|
|
8577
|
+
this.width = options.width ?? DEFAULT_WIDTH;
|
|
8578
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
|
|
8579
|
+
this.maxPointsPerSender = options.maxPointsPerSender ?? DEFAULT_MAX_POINTS;
|
|
8580
|
+
this.unregister = host.registerOverlay((ctx) => this.renderTrails(ctx));
|
|
8581
|
+
}
|
|
8582
|
+
now() {
|
|
8583
|
+
return performance.now();
|
|
8584
|
+
}
|
|
8585
|
+
/**
|
|
8586
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
8587
|
+
* the envelope `from`). Non-laser or malformed payloads are ignored and
|
|
8588
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
8589
|
+
*/
|
|
8590
|
+
apply(sender, data) {
|
|
8591
|
+
if (this.disposed || !isLaserTrailPresence(data)) return false;
|
|
8592
|
+
let trail = this.trails.get(sender);
|
|
8593
|
+
if (!trail) {
|
|
8594
|
+
trail = { points: [], color: this.color, width: this.width, fadeMs: this.fadeMs };
|
|
8595
|
+
this.trails.set(sender, trail);
|
|
8596
|
+
}
|
|
8597
|
+
trail.color = data.color ?? this.color;
|
|
8598
|
+
trail.width = data.width ?? this.width;
|
|
8599
|
+
trail.fadeMs = data.fadeMs ?? this.fadeMs;
|
|
8600
|
+
const t = this.now();
|
|
8601
|
+
for (const point of data.points) {
|
|
8602
|
+
trail.points.push({ x: point.x, y: point.y, t });
|
|
8603
|
+
}
|
|
8604
|
+
const excess = trail.points.length - this.maxPointsPerSender;
|
|
8605
|
+
if (excess > 0) trail.points.splice(0, excess);
|
|
8606
|
+
this.ensureAnimating();
|
|
8607
|
+
this.host.requestRender();
|
|
8608
|
+
return true;
|
|
8609
|
+
}
|
|
8610
|
+
/** Removes a sender's trail immediately (presence-leave/disconnect). */
|
|
8611
|
+
remove(sender) {
|
|
8612
|
+
if (this.trails.delete(sender)) this.host.requestRender();
|
|
8613
|
+
}
|
|
8614
|
+
/** Removes every trail immediately. */
|
|
8615
|
+
clear() {
|
|
8616
|
+
if (this.trails.size === 0) return;
|
|
8617
|
+
this.trails.clear();
|
|
8618
|
+
this.host.requestRender();
|
|
8619
|
+
}
|
|
8620
|
+
/** Number of senders with a live (unfaded) trail. */
|
|
8621
|
+
get activeSenderCount() {
|
|
8622
|
+
return this.trails.size;
|
|
8623
|
+
}
|
|
8624
|
+
/** Unregisters the overlay and stops the fade loop. Idempotent. */
|
|
8625
|
+
dispose() {
|
|
8626
|
+
if (this.disposed) return;
|
|
8627
|
+
this.disposed = true;
|
|
8628
|
+
if (this.rafId !== null) {
|
|
8629
|
+
cancelAnimationFrame(this.rafId);
|
|
8630
|
+
this.rafId = null;
|
|
8631
|
+
}
|
|
8632
|
+
this.trails.clear();
|
|
8633
|
+
this.unregister?.();
|
|
8634
|
+
this.unregister = null;
|
|
8635
|
+
}
|
|
8636
|
+
ensureAnimating() {
|
|
8637
|
+
if (this.rafId === null) {
|
|
8638
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
8639
|
+
}
|
|
8640
|
+
}
|
|
8641
|
+
tick() {
|
|
8642
|
+
if (this.disposed) return;
|
|
8643
|
+
const now = this.now();
|
|
8644
|
+
for (const [sender, trail] of this.trails) {
|
|
8645
|
+
const cutoff = now - trail.fadeMs;
|
|
8646
|
+
trail.points = trail.points.filter((point) => point.t >= cutoff);
|
|
8647
|
+
if (trail.points.length === 0) this.trails.delete(sender);
|
|
8648
|
+
}
|
|
8649
|
+
this.host.requestRender();
|
|
8650
|
+
this.rafId = this.trails.size > 0 ? requestAnimationFrame(() => this.tick()) : null;
|
|
8651
|
+
}
|
|
8652
|
+
renderTrails(ctx) {
|
|
8653
|
+
if (this.trails.size === 0) return;
|
|
8654
|
+
const now = this.now();
|
|
8655
|
+
for (const trail of this.trails.values()) {
|
|
8656
|
+
if (trail.points.length < 2) continue;
|
|
8657
|
+
ctx.save();
|
|
8658
|
+
ctx.strokeStyle = trail.color;
|
|
8659
|
+
ctx.lineWidth = trail.width;
|
|
8660
|
+
ctx.lineCap = "round";
|
|
8661
|
+
ctx.lineJoin = "round";
|
|
8662
|
+
for (let i = 0; i < trail.points.length - 1; i++) {
|
|
8663
|
+
const a = trail.points[i];
|
|
8664
|
+
const b = trail.points[i + 1];
|
|
8665
|
+
if (!a || !b) continue;
|
|
8666
|
+
const age = now - b.t;
|
|
8667
|
+
ctx.globalAlpha = Math.max(0, 1 - age / trail.fadeMs);
|
|
8668
|
+
ctx.beginPath();
|
|
8669
|
+
ctx.moveTo(a.x, a.y);
|
|
8670
|
+
ctx.lineTo(b.x, b.y);
|
|
8671
|
+
ctx.stroke();
|
|
8672
|
+
}
|
|
8673
|
+
ctx.restore();
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8676
|
+
};
|
|
8677
|
+
|
|
8678
|
+
// src/canvas/ping-pulse.ts
|
|
8679
|
+
var RIPPLE_OFFSETS = [0, 0.25];
|
|
8680
|
+
function easeOutCubic(t) {
|
|
8681
|
+
const inv = 1 - t;
|
|
8682
|
+
return 1 - inv * inv * inv;
|
|
8683
|
+
}
|
|
8684
|
+
function renderPingPulse(ctx, x, y, ageMs, style) {
|
|
8685
|
+
if (ageMs < 0 || ageMs >= style.durationMs) return;
|
|
8686
|
+
ctx.save();
|
|
8687
|
+
ctx.strokeStyle = style.color;
|
|
8688
|
+
ctx.fillStyle = style.color;
|
|
8689
|
+
for (const offset of RIPPLE_OFFSETS) {
|
|
8690
|
+
const rippleSpan = style.durationMs * (1 - offset);
|
|
8691
|
+
const rippleAge = ageMs - style.durationMs * offset;
|
|
8692
|
+
if (rippleAge < 0) continue;
|
|
8693
|
+
const progress = Math.min(1, rippleAge / rippleSpan);
|
|
8694
|
+
const rippleRadius = style.radius * easeOutCubic(progress);
|
|
8695
|
+
if (rippleRadius <= 0) continue;
|
|
8696
|
+
ctx.globalAlpha = Math.max(0, 1 - progress);
|
|
8697
|
+
ctx.lineWidth = Math.max(1.5, style.radius / 12);
|
|
8698
|
+
ctx.beginPath();
|
|
8699
|
+
ctx.arc(x, y, rippleRadius, 0, Math.PI * 2);
|
|
8700
|
+
ctx.stroke();
|
|
8701
|
+
}
|
|
8702
|
+
const dotProgress = ageMs / style.durationMs;
|
|
8703
|
+
ctx.globalAlpha = Math.max(0, 1 - Math.max(0, dotProgress - 0.5) * 2);
|
|
8704
|
+
ctx.beginPath();
|
|
8705
|
+
ctx.arc(x, y, Math.max(2, style.radius / 8), 0, Math.PI * 2);
|
|
8706
|
+
ctx.fill();
|
|
8707
|
+
ctx.restore();
|
|
8708
|
+
}
|
|
8709
|
+
|
|
8710
|
+
// src/canvas/remote-ping-overlay.ts
|
|
8711
|
+
var PING_PRESENCE_KIND = "ping";
|
|
8712
|
+
function isPingPresence(data) {
|
|
8713
|
+
if (typeof data !== "object" || data === null) return false;
|
|
8714
|
+
const payload = data;
|
|
8715
|
+
if (payload.kind !== PING_PRESENCE_KIND) return false;
|
|
8716
|
+
if (typeof payload.x !== "number" || !Number.isFinite(payload.x)) return false;
|
|
8717
|
+
if (typeof payload.y !== "number" || !Number.isFinite(payload.y)) return false;
|
|
8718
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
8719
|
+
if (payload.durationMs !== void 0 && !(typeof payload.durationMs === "number" && Number.isFinite(payload.durationMs) && payload.durationMs > 0)) {
|
|
8720
|
+
return false;
|
|
8721
|
+
}
|
|
8722
|
+
if (payload.radius !== void 0 && !(typeof payload.radius === "number" && Number.isFinite(payload.radius) && payload.radius > 0)) {
|
|
8723
|
+
return false;
|
|
8724
|
+
}
|
|
8725
|
+
return true;
|
|
8726
|
+
}
|
|
8727
|
+
function toPingPresence(emission) {
|
|
8728
|
+
return {
|
|
8729
|
+
kind: PING_PRESENCE_KIND,
|
|
8730
|
+
x: emission.x,
|
|
8731
|
+
y: emission.y,
|
|
8732
|
+
color: emission.color,
|
|
8733
|
+
durationMs: emission.durationMs,
|
|
8734
|
+
radius: emission.radius
|
|
8735
|
+
};
|
|
8736
|
+
}
|
|
8737
|
+
var DEFAULT_COLOR2 = "#ff3b30";
|
|
8738
|
+
var DEFAULT_DURATION_MS = 1800;
|
|
8739
|
+
var DEFAULT_RADIUS = 48;
|
|
8740
|
+
var DEFAULT_MAX_PINGS = 8;
|
|
8741
|
+
var RemotePingOverlay = class {
|
|
8742
|
+
host;
|
|
8743
|
+
color;
|
|
8744
|
+
durationMs;
|
|
8745
|
+
radius;
|
|
8746
|
+
maxPingsPerSender;
|
|
8747
|
+
pings = /* @__PURE__ */ new Map();
|
|
8748
|
+
unregister;
|
|
8749
|
+
rafId = null;
|
|
8750
|
+
disposed = false;
|
|
8751
|
+
constructor(host, options = {}) {
|
|
8752
|
+
this.host = host;
|
|
8753
|
+
this.color = options.color ?? DEFAULT_COLOR2;
|
|
8754
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS;
|
|
8755
|
+
this.radius = options.radius ?? DEFAULT_RADIUS;
|
|
8756
|
+
this.maxPingsPerSender = options.maxPingsPerSender ?? DEFAULT_MAX_PINGS;
|
|
8757
|
+
this.unregister = host.registerOverlay((ctx) => this.renderPings(ctx));
|
|
8758
|
+
}
|
|
8759
|
+
now() {
|
|
8760
|
+
return performance.now();
|
|
8761
|
+
}
|
|
8762
|
+
/**
|
|
8763
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
8764
|
+
* the envelope `from`). Non-ping or malformed payloads are ignored and
|
|
8765
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
8766
|
+
*/
|
|
8767
|
+
apply(sender, data) {
|
|
8768
|
+
if (this.disposed || !isPingPresence(data)) return false;
|
|
8769
|
+
let senderPings = this.pings.get(sender);
|
|
8770
|
+
if (!senderPings) {
|
|
8771
|
+
senderPings = [];
|
|
8772
|
+
this.pings.set(sender, senderPings);
|
|
8773
|
+
}
|
|
8774
|
+
senderPings.push({
|
|
8775
|
+
x: data.x,
|
|
8776
|
+
y: data.y,
|
|
8777
|
+
t: this.now(),
|
|
8778
|
+
color: data.color ?? this.color,
|
|
8779
|
+
durationMs: data.durationMs ?? this.durationMs,
|
|
8780
|
+
radius: data.radius ?? this.radius
|
|
8781
|
+
});
|
|
8782
|
+
const excess = senderPings.length - this.maxPingsPerSender;
|
|
8783
|
+
if (excess > 0) senderPings.splice(0, excess);
|
|
8784
|
+
this.ensureAnimating();
|
|
8785
|
+
this.host.requestRender();
|
|
8786
|
+
return true;
|
|
8787
|
+
}
|
|
8788
|
+
/** Removes a sender's pings immediately (presence-leave/disconnect). */
|
|
8789
|
+
remove(sender) {
|
|
8790
|
+
if (this.pings.delete(sender)) this.host.requestRender();
|
|
8791
|
+
}
|
|
8792
|
+
/** Removes every ping immediately. */
|
|
8793
|
+
clear() {
|
|
8794
|
+
if (this.pings.size === 0) return;
|
|
8795
|
+
this.pings.clear();
|
|
8796
|
+
this.host.requestRender();
|
|
8797
|
+
}
|
|
8798
|
+
/** Number of senders with a live (unexpired) ping. */
|
|
8799
|
+
get activeSenderCount() {
|
|
8800
|
+
return this.pings.size;
|
|
8801
|
+
}
|
|
8802
|
+
/** Unregisters the overlay and stops the animation loop. Idempotent. */
|
|
8803
|
+
dispose() {
|
|
8804
|
+
if (this.disposed) return;
|
|
8805
|
+
this.disposed = true;
|
|
8806
|
+
if (this.rafId !== null) {
|
|
8807
|
+
cancelAnimationFrame(this.rafId);
|
|
8808
|
+
this.rafId = null;
|
|
8809
|
+
}
|
|
8810
|
+
this.pings.clear();
|
|
8811
|
+
this.unregister?.();
|
|
8812
|
+
this.unregister = null;
|
|
8813
|
+
}
|
|
8814
|
+
ensureAnimating() {
|
|
8815
|
+
if (this.rafId === null) {
|
|
8816
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
8817
|
+
}
|
|
8818
|
+
}
|
|
8819
|
+
tick() {
|
|
8820
|
+
if (this.disposed) return;
|
|
8821
|
+
const now = this.now();
|
|
8822
|
+
for (const [sender, senderPings] of this.pings) {
|
|
8823
|
+
const live = senderPings.filter((ping) => now - ping.t < ping.durationMs);
|
|
8824
|
+
if (live.length === 0) {
|
|
8825
|
+
this.pings.delete(sender);
|
|
8826
|
+
} else {
|
|
8827
|
+
this.pings.set(sender, live);
|
|
8828
|
+
}
|
|
8829
|
+
}
|
|
8830
|
+
this.host.requestRender();
|
|
8831
|
+
this.rafId = this.pings.size > 0 ? requestAnimationFrame(() => this.tick()) : null;
|
|
8832
|
+
}
|
|
8833
|
+
renderPings(ctx) {
|
|
8834
|
+
if (this.pings.size === 0) return;
|
|
8835
|
+
const now = this.now();
|
|
8836
|
+
for (const senderPings of this.pings.values()) {
|
|
8837
|
+
for (const ping of senderPings) {
|
|
8838
|
+
renderPingPulse(ctx, ping.x, ping.y, now - ping.t, {
|
|
8839
|
+
color: ping.color,
|
|
8840
|
+
durationMs: ping.durationMs,
|
|
8841
|
+
radius: ping.radius
|
|
8842
|
+
});
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
};
|
|
8847
|
+
|
|
8492
8848
|
// src/tools/hand-tool.ts
|
|
8493
8849
|
var HandTool = class {
|
|
8494
8850
|
name = "hand";
|
|
@@ -8750,7 +9106,7 @@ function erasePoints(points, eraser, radius) {
|
|
|
8750
9106
|
}
|
|
8751
9107
|
|
|
8752
9108
|
// src/tools/eraser-tool.ts
|
|
8753
|
-
var
|
|
9109
|
+
var DEFAULT_RADIUS2 = 20;
|
|
8754
9110
|
function makeEraserCursor(radius) {
|
|
8755
9111
|
const size = radius * 2;
|
|
8756
9112
|
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${size}' height='${size}'><circle cx='${radius}' cy='${radius}' r='${radius - 1}' fill='none' stroke='%23666' stroke-width='1.5'/></svg>`;
|
|
@@ -8763,7 +9119,7 @@ var EraserTool = class {
|
|
|
8763
9119
|
cursor;
|
|
8764
9120
|
mode;
|
|
8765
9121
|
constructor(options = {}) {
|
|
8766
|
-
this.radius = options.radius ??
|
|
9122
|
+
this.radius = options.radius ?? DEFAULT_RADIUS2;
|
|
8767
9123
|
this.cursor = makeEraserCursor(this.radius);
|
|
8768
9124
|
this.mode = options.mode ?? "partial";
|
|
8769
9125
|
}
|
|
@@ -11045,9 +11401,9 @@ var TemplateTool = class {
|
|
|
11045
11401
|
};
|
|
11046
11402
|
|
|
11047
11403
|
// src/tools/laser-tool.ts
|
|
11048
|
-
var
|
|
11049
|
-
var
|
|
11050
|
-
var
|
|
11404
|
+
var DEFAULT_COLOR3 = "#ff3b30";
|
|
11405
|
+
var DEFAULT_WIDTH2 = 4;
|
|
11406
|
+
var DEFAULT_FADE_MS2 = 1200;
|
|
11051
11407
|
var LaserTool = class {
|
|
11052
11408
|
name;
|
|
11053
11409
|
color;
|
|
@@ -11057,11 +11413,13 @@ var LaserTool = class {
|
|
|
11057
11413
|
rafId = null;
|
|
11058
11414
|
drawing = false;
|
|
11059
11415
|
optionListeners = /* @__PURE__ */ new Set();
|
|
11416
|
+
trailListeners = /* @__PURE__ */ new Set();
|
|
11417
|
+
pendingEmission = [];
|
|
11060
11418
|
constructor(options = {}) {
|
|
11061
11419
|
this.name = options.name ?? "laser";
|
|
11062
|
-
this.color = options.color ??
|
|
11063
|
-
this.width = options.width ??
|
|
11064
|
-
this.fadeMs = options.fadeMs ??
|
|
11420
|
+
this.color = options.color ?? DEFAULT_COLOR3;
|
|
11421
|
+
this.width = options.width ?? DEFAULT_WIDTH2;
|
|
11422
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
|
|
11065
11423
|
}
|
|
11066
11424
|
now() {
|
|
11067
11425
|
return performance.now();
|
|
@@ -11076,6 +11434,7 @@ var LaserTool = class {
|
|
|
11076
11434
|
}
|
|
11077
11435
|
this.trail = [];
|
|
11078
11436
|
this.drawing = false;
|
|
11437
|
+
this.pendingEmission = [];
|
|
11079
11438
|
ctx.setCursor?.("default");
|
|
11080
11439
|
ctx.requestRender();
|
|
11081
11440
|
}
|
|
@@ -11091,6 +11450,16 @@ var LaserTool = class {
|
|
|
11091
11450
|
this.optionListeners.add(listener);
|
|
11092
11451
|
return () => this.optionListeners.delete(listener);
|
|
11093
11452
|
}
|
|
11453
|
+
/**
|
|
11454
|
+
* Subscribes to coalesced trail emissions. Points recorded by pointer input
|
|
11455
|
+
* are batched and delivered at most once per animation frame, keeping
|
|
11456
|
+
* outgoing presence packets small. Listeners must not throw; a throwing
|
|
11457
|
+
* listener is isolated so it cannot stall the fade loop.
|
|
11458
|
+
*/
|
|
11459
|
+
onTrail(listener) {
|
|
11460
|
+
this.trailListeners.add(listener);
|
|
11461
|
+
return () => this.trailListeners.delete(listener);
|
|
11462
|
+
}
|
|
11094
11463
|
setOptions(options) {
|
|
11095
11464
|
if (options.color !== void 0) this.color = options.color;
|
|
11096
11465
|
if (options.width !== void 0) this.width = options.width;
|
|
@@ -11099,14 +11468,18 @@ var LaserTool = class {
|
|
|
11099
11468
|
}
|
|
11100
11469
|
onPointerDown(state, ctx) {
|
|
11101
11470
|
this.drawing = true;
|
|
11102
|
-
|
|
11103
|
-
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
11104
|
-
this.ensureAnimating(ctx);
|
|
11471
|
+
this.recordPoint(state, ctx);
|
|
11105
11472
|
}
|
|
11106
11473
|
onPointerMove(state, ctx) {
|
|
11107
11474
|
if (!this.drawing) return;
|
|
11475
|
+
this.recordPoint(state, ctx);
|
|
11476
|
+
}
|
|
11477
|
+
recordPoint(state, ctx) {
|
|
11108
11478
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
11109
11479
|
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
11480
|
+
if (this.trailListeners.size > 0) {
|
|
11481
|
+
this.pendingEmission.push({ x: world.x, y: world.y });
|
|
11482
|
+
}
|
|
11110
11483
|
this.ensureAnimating(ctx);
|
|
11111
11484
|
}
|
|
11112
11485
|
onPointerUp(_state, _ctx) {
|
|
@@ -11138,7 +11511,24 @@ var LaserTool = class {
|
|
|
11138
11511
|
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
11139
11512
|
}
|
|
11140
11513
|
}
|
|
11514
|
+
flushEmission() {
|
|
11515
|
+
if (this.pendingEmission.length === 0) return;
|
|
11516
|
+
const emission = {
|
|
11517
|
+
points: this.pendingEmission,
|
|
11518
|
+
color: this.color,
|
|
11519
|
+
width: this.width,
|
|
11520
|
+
fadeMs: this.fadeMs
|
|
11521
|
+
};
|
|
11522
|
+
this.pendingEmission = [];
|
|
11523
|
+
for (const listener of this.trailListeners) {
|
|
11524
|
+
try {
|
|
11525
|
+
listener(emission);
|
|
11526
|
+
} catch {
|
|
11527
|
+
}
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11141
11530
|
tick(ctx) {
|
|
11531
|
+
this.flushEmission();
|
|
11142
11532
|
const cutoff = this.now() - this.fadeMs;
|
|
11143
11533
|
this.trail = this.trail.filter((p) => p.t >= cutoff);
|
|
11144
11534
|
if (this.trail.length > 0) {
|
|
@@ -11154,8 +11544,128 @@ var LaserTool = class {
|
|
|
11154
11544
|
}
|
|
11155
11545
|
};
|
|
11156
11546
|
|
|
11547
|
+
// src/tools/ping-tool.ts
|
|
11548
|
+
var DEFAULT_COLOR4 = "#ff3b30";
|
|
11549
|
+
var DEFAULT_DURATION_MS2 = 1800;
|
|
11550
|
+
var DEFAULT_RADIUS3 = 48;
|
|
11551
|
+
var DEFAULT_MIN_INTERVAL_MS = 300;
|
|
11552
|
+
var PingTool = class {
|
|
11553
|
+
name;
|
|
11554
|
+
color;
|
|
11555
|
+
durationMs;
|
|
11556
|
+
radius;
|
|
11557
|
+
minIntervalMs;
|
|
11558
|
+
pings = [];
|
|
11559
|
+
lastEmitAt = -Infinity;
|
|
11560
|
+
rafId = null;
|
|
11561
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
11562
|
+
pingListeners = /* @__PURE__ */ new Set();
|
|
11563
|
+
constructor(options = {}) {
|
|
11564
|
+
this.name = options.name ?? "ping";
|
|
11565
|
+
this.color = options.color ?? DEFAULT_COLOR4;
|
|
11566
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
|
|
11567
|
+
this.radius = options.radius ?? DEFAULT_RADIUS3;
|
|
11568
|
+
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
11569
|
+
}
|
|
11570
|
+
now() {
|
|
11571
|
+
return performance.now();
|
|
11572
|
+
}
|
|
11573
|
+
onActivate(ctx) {
|
|
11574
|
+
ctx.setCursor?.("crosshair");
|
|
11575
|
+
}
|
|
11576
|
+
onDeactivate(ctx) {
|
|
11577
|
+
if (this.rafId !== null) {
|
|
11578
|
+
cancelAnimationFrame(this.rafId);
|
|
11579
|
+
this.rafId = null;
|
|
11580
|
+
}
|
|
11581
|
+
this.pings = [];
|
|
11582
|
+
ctx.setCursor?.("default");
|
|
11583
|
+
ctx.requestRender();
|
|
11584
|
+
}
|
|
11585
|
+
getOptions() {
|
|
11586
|
+
return {
|
|
11587
|
+
name: this.name,
|
|
11588
|
+
color: this.color,
|
|
11589
|
+
durationMs: this.durationMs,
|
|
11590
|
+
radius: this.radius,
|
|
11591
|
+
minIntervalMs: this.minIntervalMs
|
|
11592
|
+
};
|
|
11593
|
+
}
|
|
11594
|
+
setOptions(options) {
|
|
11595
|
+
if (options.color !== void 0) this.color = options.color;
|
|
11596
|
+
if (options.durationMs !== void 0) this.durationMs = options.durationMs;
|
|
11597
|
+
if (options.radius !== void 0) this.radius = options.radius;
|
|
11598
|
+
if (options.minIntervalMs !== void 0) this.minIntervalMs = options.minIntervalMs;
|
|
11599
|
+
this.notifyOptionsChange();
|
|
11600
|
+
}
|
|
11601
|
+
onOptionsChange(listener) {
|
|
11602
|
+
this.optionListeners.add(listener);
|
|
11603
|
+
return () => this.optionListeners.delete(listener);
|
|
11604
|
+
}
|
|
11605
|
+
/**
|
|
11606
|
+
* Subscribes to accepted pings. Listeners must not throw; a throwing
|
|
11607
|
+
* listener is isolated so it cannot break the tap handling or other
|
|
11608
|
+
* listeners.
|
|
11609
|
+
*/
|
|
11610
|
+
onPing(listener) {
|
|
11611
|
+
this.pingListeners.add(listener);
|
|
11612
|
+
return () => this.pingListeners.delete(listener);
|
|
11613
|
+
}
|
|
11614
|
+
onPointerDown(state, ctx) {
|
|
11615
|
+
const t = this.now();
|
|
11616
|
+
if (t - this.lastEmitAt < this.minIntervalMs) return;
|
|
11617
|
+
this.lastEmitAt = t;
|
|
11618
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
11619
|
+
this.pings.push({ x: world.x, y: world.y, t });
|
|
11620
|
+
const emission = {
|
|
11621
|
+
x: world.x,
|
|
11622
|
+
y: world.y,
|
|
11623
|
+
color: this.color,
|
|
11624
|
+
durationMs: this.durationMs,
|
|
11625
|
+
radius: this.radius
|
|
11626
|
+
};
|
|
11627
|
+
for (const listener of this.pingListeners) {
|
|
11628
|
+
try {
|
|
11629
|
+
listener(emission);
|
|
11630
|
+
} catch {
|
|
11631
|
+
}
|
|
11632
|
+
}
|
|
11633
|
+
this.ensureAnimating(ctx);
|
|
11634
|
+
ctx.requestRender();
|
|
11635
|
+
}
|
|
11636
|
+
onPointerMove(_state, _ctx) {
|
|
11637
|
+
}
|
|
11638
|
+
onPointerUp(_state, _ctx) {
|
|
11639
|
+
}
|
|
11640
|
+
renderOverlay(ctx) {
|
|
11641
|
+
if (this.pings.length === 0) return;
|
|
11642
|
+
const now = this.now();
|
|
11643
|
+
for (const ping of this.pings) {
|
|
11644
|
+
renderPingPulse(ctx, ping.x, ping.y, now - ping.t, {
|
|
11645
|
+
color: this.color,
|
|
11646
|
+
durationMs: this.durationMs,
|
|
11647
|
+
radius: this.radius
|
|
11648
|
+
});
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11651
|
+
ensureAnimating(ctx) {
|
|
11652
|
+
if (this.rafId === null) {
|
|
11653
|
+
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
11654
|
+
}
|
|
11655
|
+
}
|
|
11656
|
+
tick(ctx) {
|
|
11657
|
+
const cutoff = this.now() - this.durationMs;
|
|
11658
|
+
this.pings = this.pings.filter((ping) => ping.t > cutoff);
|
|
11659
|
+
ctx.requestRender();
|
|
11660
|
+
this.rafId = this.pings.length > 0 ? requestAnimationFrame(() => this.tick(ctx)) : null;
|
|
11661
|
+
}
|
|
11662
|
+
notifyOptionsChange() {
|
|
11663
|
+
for (const listener of this.optionListeners) listener();
|
|
11664
|
+
}
|
|
11665
|
+
};
|
|
11666
|
+
|
|
11157
11667
|
// src/index.ts
|
|
11158
|
-
var VERSION = "0.
|
|
11668
|
+
var VERSION = "0.55.0";
|
|
11159
11669
|
export {
|
|
11160
11670
|
ArrowTool,
|
|
11161
11671
|
AutoSave,
|
|
@@ -11167,13 +11677,18 @@ export {
|
|
|
11167
11677
|
HistoryStack,
|
|
11168
11678
|
ImageTool,
|
|
11169
11679
|
IndexedDBAdapter,
|
|
11680
|
+
LASER_TRAIL_PRESENCE_KIND,
|
|
11170
11681
|
LaserTool,
|
|
11171
11682
|
LayerManager,
|
|
11172
11683
|
LocalStorageAdapter,
|
|
11173
11684
|
MeasureTool,
|
|
11174
11685
|
MemoryAdapter,
|
|
11175
11686
|
NoteTool,
|
|
11687
|
+
PING_PRESENCE_KIND,
|
|
11176
11688
|
PencilTool,
|
|
11689
|
+
PingTool,
|
|
11690
|
+
RemoteLaserOverlay,
|
|
11691
|
+
RemotePingOverlay,
|
|
11177
11692
|
SelectTool,
|
|
11178
11693
|
ShapeTool,
|
|
11179
11694
|
TemplateTool,
|
|
@@ -11209,12 +11724,16 @@ export {
|
|
|
11209
11724
|
getHexCellsInRectangle,
|
|
11210
11725
|
getHexCellsInSquare,
|
|
11211
11726
|
getHexDistance,
|
|
11727
|
+
isLaserTrailPresence,
|
|
11212
11728
|
isNearBezier,
|
|
11729
|
+
isPingPresence,
|
|
11213
11730
|
setFontSize,
|
|
11214
11731
|
smartSnap,
|
|
11215
11732
|
snapPoint,
|
|
11216
11733
|
snapToHexCenter,
|
|
11217
11734
|
styleToPatch,
|
|
11735
|
+
toLaserTrailPresence,
|
|
11736
|
+
toPingPresence,
|
|
11218
11737
|
toggleBold,
|
|
11219
11738
|
toggleItalic,
|
|
11220
11739
|
toggleStrikethrough,
|