@fieldnotes/core 0.54.0 → 0.56.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 CHANGED
@@ -37,8 +37,12 @@ __export(index_exports, {
37
37
  MeasureTool: () => MeasureTool,
38
38
  MemoryAdapter: () => MemoryAdapter,
39
39
  NoteTool: () => NoteTool,
40
+ PING_PRESENCE_KIND: () => PING_PRESENCE_KIND,
40
41
  PencilTool: () => PencilTool,
42
+ PingInput: () => PingInput,
43
+ PingTool: () => PingTool,
41
44
  RemoteLaserOverlay: () => RemoteLaserOverlay,
45
+ RemotePingOverlay: () => RemotePingOverlay,
42
46
  SelectTool: () => SelectTool,
43
47
  ShapeTool: () => ShapeTool,
44
48
  TemplateTool: () => TemplateTool,
@@ -76,12 +80,14 @@ __export(index_exports, {
76
80
  getHexDistance: () => getHexDistance,
77
81
  isLaserTrailPresence: () => isLaserTrailPresence,
78
82
  isNearBezier: () => isNearBezier,
83
+ isPingPresence: () => isPingPresence,
79
84
  setFontSize: () => setFontSize,
80
85
  smartSnap: () => smartSnap,
81
86
  snapPoint: () => snapPoint,
82
87
  snapToHexCenter: () => snapToHexCenter,
83
88
  styleToPatch: () => styleToPatch,
84
89
  toLaserTrailPresence: () => toLaserTrailPresence,
90
+ toPingPresence: () => toPingPresence,
85
91
  toggleBold: () => toggleBold,
86
92
  toggleItalic: () => toggleItalic,
87
93
  toggleStrikethrough: () => toggleStrikethrough,
@@ -8766,6 +8772,361 @@ var RemoteLaserOverlay = class {
8766
8772
  }
8767
8773
  };
8768
8774
 
8775
+ // src/canvas/ping-pulse.ts
8776
+ var RIPPLE_OFFSETS = [0, 0.25];
8777
+ function easeOutCubic(t) {
8778
+ const inv = 1 - t;
8779
+ return 1 - inv * inv * inv;
8780
+ }
8781
+ function renderPingPulse(ctx, x, y, ageMs, style) {
8782
+ if (ageMs < 0 || ageMs >= style.durationMs) return;
8783
+ ctx.save();
8784
+ ctx.strokeStyle = style.color;
8785
+ ctx.fillStyle = style.color;
8786
+ for (const offset of RIPPLE_OFFSETS) {
8787
+ const rippleSpan = style.durationMs * (1 - offset);
8788
+ const rippleAge = ageMs - style.durationMs * offset;
8789
+ if (rippleAge < 0) continue;
8790
+ const progress = Math.min(1, rippleAge / rippleSpan);
8791
+ const rippleRadius = style.radius * easeOutCubic(progress);
8792
+ if (rippleRadius <= 0) continue;
8793
+ ctx.globalAlpha = Math.max(0, 1 - progress);
8794
+ ctx.lineWidth = Math.max(1.5, style.radius / 12);
8795
+ ctx.beginPath();
8796
+ ctx.arc(x, y, rippleRadius, 0, Math.PI * 2);
8797
+ ctx.stroke();
8798
+ }
8799
+ const dotProgress = ageMs / style.durationMs;
8800
+ ctx.globalAlpha = Math.max(0, 1 - Math.max(0, dotProgress - 0.5) * 2);
8801
+ ctx.beginPath();
8802
+ ctx.arc(x, y, Math.max(2, style.radius / 8), 0, Math.PI * 2);
8803
+ ctx.fill();
8804
+ ctx.restore();
8805
+ }
8806
+
8807
+ // src/canvas/remote-ping-overlay.ts
8808
+ var PING_PRESENCE_KIND = "ping";
8809
+ function isPingPresence(data) {
8810
+ if (typeof data !== "object" || data === null) return false;
8811
+ const payload = data;
8812
+ if (payload.kind !== PING_PRESENCE_KIND) return false;
8813
+ if (typeof payload.x !== "number" || !Number.isFinite(payload.x)) return false;
8814
+ if (typeof payload.y !== "number" || !Number.isFinite(payload.y)) return false;
8815
+ if (payload.color !== void 0 && typeof payload.color !== "string") return false;
8816
+ if (payload.durationMs !== void 0 && !(typeof payload.durationMs === "number" && Number.isFinite(payload.durationMs) && payload.durationMs > 0)) {
8817
+ return false;
8818
+ }
8819
+ if (payload.radius !== void 0 && !(typeof payload.radius === "number" && Number.isFinite(payload.radius) && payload.radius > 0)) {
8820
+ return false;
8821
+ }
8822
+ return true;
8823
+ }
8824
+ function toPingPresence(emission) {
8825
+ return {
8826
+ kind: PING_PRESENCE_KIND,
8827
+ x: emission.x,
8828
+ y: emission.y,
8829
+ color: emission.color,
8830
+ durationMs: emission.durationMs,
8831
+ radius: emission.radius
8832
+ };
8833
+ }
8834
+ var DEFAULT_COLOR2 = "#ff3b30";
8835
+ var DEFAULT_DURATION_MS = 1800;
8836
+ var DEFAULT_RADIUS = 48;
8837
+ var DEFAULT_MAX_PINGS = 8;
8838
+ var RemotePingOverlay = class {
8839
+ host;
8840
+ color;
8841
+ durationMs;
8842
+ radius;
8843
+ maxPingsPerSender;
8844
+ pings = /* @__PURE__ */ new Map();
8845
+ unregister;
8846
+ rafId = null;
8847
+ disposed = false;
8848
+ constructor(host, options = {}) {
8849
+ this.host = host;
8850
+ this.color = options.color ?? DEFAULT_COLOR2;
8851
+ this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS;
8852
+ this.radius = options.radius ?? DEFAULT_RADIUS;
8853
+ this.maxPingsPerSender = options.maxPingsPerSender ?? DEFAULT_MAX_PINGS;
8854
+ this.unregister = host.registerOverlay((ctx) => this.renderPings(ctx));
8855
+ }
8856
+ now() {
8857
+ return performance.now();
8858
+ }
8859
+ /**
8860
+ * Applies a presence payload from `sender` (any opaque per-sender key, e.g.
8861
+ * the envelope `from`). Non-ping or malformed payloads are ignored and
8862
+ * reported as `false`, so hosts can feed every presence frame through.
8863
+ */
8864
+ apply(sender, data) {
8865
+ if (this.disposed || !isPingPresence(data)) return false;
8866
+ let senderPings = this.pings.get(sender);
8867
+ if (!senderPings) {
8868
+ senderPings = [];
8869
+ this.pings.set(sender, senderPings);
8870
+ }
8871
+ senderPings.push({
8872
+ x: data.x,
8873
+ y: data.y,
8874
+ t: this.now(),
8875
+ color: data.color ?? this.color,
8876
+ durationMs: data.durationMs ?? this.durationMs,
8877
+ radius: data.radius ?? this.radius
8878
+ });
8879
+ const excess = senderPings.length - this.maxPingsPerSender;
8880
+ if (excess > 0) senderPings.splice(0, excess);
8881
+ this.ensureAnimating();
8882
+ this.host.requestRender();
8883
+ return true;
8884
+ }
8885
+ /** Removes a sender's pings immediately (presence-leave/disconnect). */
8886
+ remove(sender) {
8887
+ if (this.pings.delete(sender)) this.host.requestRender();
8888
+ }
8889
+ /** Removes every ping immediately. */
8890
+ clear() {
8891
+ if (this.pings.size === 0) return;
8892
+ this.pings.clear();
8893
+ this.host.requestRender();
8894
+ }
8895
+ /** Number of senders with a live (unexpired) ping. */
8896
+ get activeSenderCount() {
8897
+ return this.pings.size;
8898
+ }
8899
+ /** Unregisters the overlay and stops the animation loop. Idempotent. */
8900
+ dispose() {
8901
+ if (this.disposed) return;
8902
+ this.disposed = true;
8903
+ if (this.rafId !== null) {
8904
+ cancelAnimationFrame(this.rafId);
8905
+ this.rafId = null;
8906
+ }
8907
+ this.pings.clear();
8908
+ this.unregister?.();
8909
+ this.unregister = null;
8910
+ }
8911
+ ensureAnimating() {
8912
+ if (this.rafId === null) {
8913
+ this.rafId = requestAnimationFrame(() => this.tick());
8914
+ }
8915
+ }
8916
+ tick() {
8917
+ if (this.disposed) return;
8918
+ const now = this.now();
8919
+ for (const [sender, senderPings] of this.pings) {
8920
+ const live = senderPings.filter((ping) => now - ping.t < ping.durationMs);
8921
+ if (live.length === 0) {
8922
+ this.pings.delete(sender);
8923
+ } else {
8924
+ this.pings.set(sender, live);
8925
+ }
8926
+ }
8927
+ this.host.requestRender();
8928
+ this.rafId = this.pings.size > 0 ? requestAnimationFrame(() => this.tick()) : null;
8929
+ }
8930
+ renderPings(ctx) {
8931
+ if (this.pings.size === 0) return;
8932
+ const now = this.now();
8933
+ for (const senderPings of this.pings.values()) {
8934
+ for (const ping of senderPings) {
8935
+ renderPingPulse(ctx, ping.x, ping.y, now - ping.t, {
8936
+ color: ping.color,
8937
+ durationMs: ping.durationMs,
8938
+ radius: ping.radius
8939
+ });
8940
+ }
8941
+ }
8942
+ }
8943
+ };
8944
+
8945
+ // src/canvas/ping-input.ts
8946
+ var DEFAULT_LONG_PRESS_MS = 600;
8947
+ var DEFAULT_SLOP_PX = 8;
8948
+ var DEFAULT_COLOR3 = "#ff3b30";
8949
+ var DEFAULT_DURATION_MS2 = 1800;
8950
+ var DEFAULT_RADIUS2 = 48;
8951
+ var DEFAULT_MIN_INTERVAL_MS = 300;
8952
+ var PingInput = class {
8953
+ element;
8954
+ host;
8955
+ longPressEnabled;
8956
+ longPressMs;
8957
+ slopPx;
8958
+ color;
8959
+ durationMs;
8960
+ radius;
8961
+ minIntervalMs;
8962
+ shouldPing;
8963
+ press = null;
8964
+ downPointers = /* @__PURE__ */ new Set();
8965
+ lastPointerScreen = null;
8966
+ lastEmitAt = -Infinity;
8967
+ disposed = false;
8968
+ optionListeners = /* @__PURE__ */ new Set();
8969
+ pingListeners = /* @__PURE__ */ new Set();
8970
+ handlePointerDown = (e) => this.onPointerDown(e);
8971
+ handlePointerMove = (e) => this.onPointerMove(e);
8972
+ handlePointerUp = (e) => this.onPointerEnd(e);
8973
+ handlePointerCancel = (e) => this.onPointerEnd(e);
8974
+ handlePointerLeave = (e) => this.onPointerEnd(e);
8975
+ constructor(element, host, options = {}) {
8976
+ this.element = element;
8977
+ this.host = host;
8978
+ this.longPressEnabled = options.longPressEnabled ?? false;
8979
+ this.longPressMs = options.longPressMs ?? DEFAULT_LONG_PRESS_MS;
8980
+ this.slopPx = options.slopPx ?? DEFAULT_SLOP_PX;
8981
+ this.color = options.color ?? DEFAULT_COLOR3;
8982
+ this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
8983
+ this.radius = options.radius ?? DEFAULT_RADIUS2;
8984
+ this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
8985
+ this.shouldPing = options.shouldPing;
8986
+ const opts = { passive: true };
8987
+ element.addEventListener("pointerdown", this.handlePointerDown, opts);
8988
+ element.addEventListener("pointermove", this.handlePointerMove, opts);
8989
+ element.addEventListener("pointerup", this.handlePointerUp, opts);
8990
+ element.addEventListener("pointercancel", this.handlePointerCancel, opts);
8991
+ element.addEventListener("pointerleave", this.handlePointerLeave, opts);
8992
+ }
8993
+ now() {
8994
+ return performance.now();
8995
+ }
8996
+ getOptions() {
8997
+ return {
8998
+ longPressEnabled: this.longPressEnabled,
8999
+ longPressMs: this.longPressMs,
9000
+ slopPx: this.slopPx,
9001
+ color: this.color,
9002
+ durationMs: this.durationMs,
9003
+ radius: this.radius,
9004
+ minIntervalMs: this.minIntervalMs,
9005
+ ...this.shouldPing ? { shouldPing: this.shouldPing } : {}
9006
+ };
9007
+ }
9008
+ setOptions(options) {
9009
+ if (options.longPressEnabled !== void 0) {
9010
+ this.longPressEnabled = options.longPressEnabled;
9011
+ if (!this.longPressEnabled) this.cancelPress();
9012
+ }
9013
+ if (options.longPressMs !== void 0) this.longPressMs = options.longPressMs;
9014
+ if (options.slopPx !== void 0) this.slopPx = options.slopPx;
9015
+ if (options.color !== void 0) this.color = options.color;
9016
+ if (options.durationMs !== void 0) this.durationMs = options.durationMs;
9017
+ if (options.radius !== void 0) this.radius = options.radius;
9018
+ if (options.minIntervalMs !== void 0) this.minIntervalMs = options.minIntervalMs;
9019
+ if (options.shouldPing !== void 0) this.shouldPing = options.shouldPing;
9020
+ for (const listener of this.optionListeners) listener();
9021
+ }
9022
+ onOptionsChange(listener) {
9023
+ this.optionListeners.add(listener);
9024
+ return () => this.optionListeners.delete(listener);
9025
+ }
9026
+ /**
9027
+ * Subscribes to emitted pings. Listeners must not throw; a throwing
9028
+ * listener is isolated so it cannot break input handling or other
9029
+ * listeners.
9030
+ */
9031
+ onPing(listener) {
9032
+ this.pingListeners.add(listener);
9033
+ return () => this.pingListeners.delete(listener);
9034
+ }
9035
+ /**
9036
+ * Pings at the last tracked pointer position (hover moves and presses),
9037
+ * world-converted at call time. Returns `false` when no pointer has been
9038
+ * seen yet, the host vetoes, or the rate limit drops the ping.
9039
+ */
9040
+ pingAtPointer() {
9041
+ if (this.disposed || this.lastPointerScreen === null) return false;
9042
+ return this.emit(this.host.screenToWorld(this.lastPointerScreen));
9043
+ }
9044
+ /** Pings a world position directly. Same veto and rate limit as every path. */
9045
+ pingAt(world) {
9046
+ if (this.disposed) return false;
9047
+ return this.emit(world);
9048
+ }
9049
+ /** Removes all DOM listeners and cancels any pending press. Idempotent. */
9050
+ dispose() {
9051
+ if (this.disposed) return;
9052
+ this.disposed = true;
9053
+ this.cancelPress();
9054
+ this.downPointers.clear();
9055
+ this.element.removeEventListener("pointerdown", this.handlePointerDown);
9056
+ this.element.removeEventListener("pointermove", this.handlePointerMove);
9057
+ this.element.removeEventListener("pointerup", this.handlePointerUp);
9058
+ this.element.removeEventListener("pointercancel", this.handlePointerCancel);
9059
+ this.element.removeEventListener("pointerleave", this.handlePointerLeave);
9060
+ this.optionListeners.clear();
9061
+ this.pingListeners.clear();
9062
+ }
9063
+ toLocal(e) {
9064
+ const rect = this.element.getBoundingClientRect();
9065
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
9066
+ }
9067
+ onPointerDown(e) {
9068
+ const local = this.toLocal(e);
9069
+ this.lastPointerScreen = local;
9070
+ const hadPointers = this.downPointers.size > 0;
9071
+ this.downPointers.add(e.pointerId);
9072
+ if (hadPointers) {
9073
+ this.cancelPress();
9074
+ return;
9075
+ }
9076
+ if (!this.longPressEnabled) return;
9077
+ if (e.pointerType === "mouse" && e.button !== 0) return;
9078
+ this.press = {
9079
+ pointerId: e.pointerId,
9080
+ x: local.x,
9081
+ y: local.y,
9082
+ timer: setTimeout(() => this.firePress(), this.longPressMs)
9083
+ };
9084
+ }
9085
+ onPointerMove(e) {
9086
+ const local = this.toLocal(e);
9087
+ this.lastPointerScreen = local;
9088
+ if (this.press === null || e.pointerId !== this.press.pointerId) return;
9089
+ const dx = local.x - this.press.x;
9090
+ const dy = local.y - this.press.y;
9091
+ if (Math.hypot(dx, dy) > this.slopPx) this.cancelPress();
9092
+ }
9093
+ onPointerEnd(e) {
9094
+ this.downPointers.delete(e.pointerId);
9095
+ if (this.press !== null && e.pointerId === this.press.pointerId) this.cancelPress();
9096
+ }
9097
+ cancelPress() {
9098
+ if (this.press === null) return;
9099
+ clearTimeout(this.press.timer);
9100
+ this.press = null;
9101
+ }
9102
+ firePress() {
9103
+ if (this.press === null) return;
9104
+ const screen = { x: this.press.x, y: this.press.y };
9105
+ this.press = null;
9106
+ this.emit(this.host.screenToWorld(screen));
9107
+ }
9108
+ emit(world) {
9109
+ if (this.shouldPing && !this.shouldPing()) return false;
9110
+ const t = this.now();
9111
+ if (t - this.lastEmitAt < this.minIntervalMs) return false;
9112
+ this.lastEmitAt = t;
9113
+ const emission = {
9114
+ x: world.x,
9115
+ y: world.y,
9116
+ color: this.color,
9117
+ durationMs: this.durationMs,
9118
+ radius: this.radius
9119
+ };
9120
+ for (const listener of this.pingListeners) {
9121
+ try {
9122
+ listener(emission);
9123
+ } catch {
9124
+ }
9125
+ }
9126
+ return true;
9127
+ }
9128
+ };
9129
+
8769
9130
  // src/tools/hand-tool.ts
8770
9131
  var HandTool = class {
8771
9132
  name = "hand";
@@ -9027,7 +9388,7 @@ function erasePoints(points, eraser, radius) {
9027
9388
  }
9028
9389
 
9029
9390
  // src/tools/eraser-tool.ts
9030
- var DEFAULT_RADIUS = 20;
9391
+ var DEFAULT_RADIUS3 = 20;
9031
9392
  function makeEraserCursor(radius) {
9032
9393
  const size = radius * 2;
9033
9394
  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>`;
@@ -9040,7 +9401,7 @@ var EraserTool = class {
9040
9401
  cursor;
9041
9402
  mode;
9042
9403
  constructor(options = {}) {
9043
- this.radius = options.radius ?? DEFAULT_RADIUS;
9404
+ this.radius = options.radius ?? DEFAULT_RADIUS3;
9044
9405
  this.cursor = makeEraserCursor(this.radius);
9045
9406
  this.mode = options.mode ?? "partial";
9046
9407
  }
@@ -11322,7 +11683,7 @@ var TemplateTool = class {
11322
11683
  };
11323
11684
 
11324
11685
  // src/tools/laser-tool.ts
11325
- var DEFAULT_COLOR2 = "#ff3b30";
11686
+ var DEFAULT_COLOR4 = "#ff3b30";
11326
11687
  var DEFAULT_WIDTH2 = 4;
11327
11688
  var DEFAULT_FADE_MS2 = 1200;
11328
11689
  var LaserTool = class {
@@ -11338,7 +11699,7 @@ var LaserTool = class {
11338
11699
  pendingEmission = [];
11339
11700
  constructor(options = {}) {
11340
11701
  this.name = options.name ?? "laser";
11341
- this.color = options.color ?? DEFAULT_COLOR2;
11702
+ this.color = options.color ?? DEFAULT_COLOR4;
11342
11703
  this.width = options.width ?? DEFAULT_WIDTH2;
11343
11704
  this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
11344
11705
  }
@@ -11465,8 +11826,128 @@ var LaserTool = class {
11465
11826
  }
11466
11827
  };
11467
11828
 
11829
+ // src/tools/ping-tool.ts
11830
+ var DEFAULT_COLOR5 = "#ff3b30";
11831
+ var DEFAULT_DURATION_MS3 = 1800;
11832
+ var DEFAULT_RADIUS4 = 48;
11833
+ var DEFAULT_MIN_INTERVAL_MS2 = 300;
11834
+ var PingTool = class {
11835
+ name;
11836
+ color;
11837
+ durationMs;
11838
+ radius;
11839
+ minIntervalMs;
11840
+ pings = [];
11841
+ lastEmitAt = -Infinity;
11842
+ rafId = null;
11843
+ optionListeners = /* @__PURE__ */ new Set();
11844
+ pingListeners = /* @__PURE__ */ new Set();
11845
+ constructor(options = {}) {
11846
+ this.name = options.name ?? "ping";
11847
+ this.color = options.color ?? DEFAULT_COLOR5;
11848
+ this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
11849
+ this.radius = options.radius ?? DEFAULT_RADIUS4;
11850
+ this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
11851
+ }
11852
+ now() {
11853
+ return performance.now();
11854
+ }
11855
+ onActivate(ctx) {
11856
+ ctx.setCursor?.("crosshair");
11857
+ }
11858
+ onDeactivate(ctx) {
11859
+ if (this.rafId !== null) {
11860
+ cancelAnimationFrame(this.rafId);
11861
+ this.rafId = null;
11862
+ }
11863
+ this.pings = [];
11864
+ ctx.setCursor?.("default");
11865
+ ctx.requestRender();
11866
+ }
11867
+ getOptions() {
11868
+ return {
11869
+ name: this.name,
11870
+ color: this.color,
11871
+ durationMs: this.durationMs,
11872
+ radius: this.radius,
11873
+ minIntervalMs: this.minIntervalMs
11874
+ };
11875
+ }
11876
+ setOptions(options) {
11877
+ if (options.color !== void 0) this.color = options.color;
11878
+ if (options.durationMs !== void 0) this.durationMs = options.durationMs;
11879
+ if (options.radius !== void 0) this.radius = options.radius;
11880
+ if (options.minIntervalMs !== void 0) this.minIntervalMs = options.minIntervalMs;
11881
+ this.notifyOptionsChange();
11882
+ }
11883
+ onOptionsChange(listener) {
11884
+ this.optionListeners.add(listener);
11885
+ return () => this.optionListeners.delete(listener);
11886
+ }
11887
+ /**
11888
+ * Subscribes to accepted pings. Listeners must not throw; a throwing
11889
+ * listener is isolated so it cannot break the tap handling or other
11890
+ * listeners.
11891
+ */
11892
+ onPing(listener) {
11893
+ this.pingListeners.add(listener);
11894
+ return () => this.pingListeners.delete(listener);
11895
+ }
11896
+ onPointerDown(state, ctx) {
11897
+ const t = this.now();
11898
+ if (t - this.lastEmitAt < this.minIntervalMs) return;
11899
+ this.lastEmitAt = t;
11900
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
11901
+ this.pings.push({ x: world.x, y: world.y, t });
11902
+ const emission = {
11903
+ x: world.x,
11904
+ y: world.y,
11905
+ color: this.color,
11906
+ durationMs: this.durationMs,
11907
+ radius: this.radius
11908
+ };
11909
+ for (const listener of this.pingListeners) {
11910
+ try {
11911
+ listener(emission);
11912
+ } catch {
11913
+ }
11914
+ }
11915
+ this.ensureAnimating(ctx);
11916
+ ctx.requestRender();
11917
+ }
11918
+ onPointerMove(_state, _ctx) {
11919
+ }
11920
+ onPointerUp(_state, _ctx) {
11921
+ }
11922
+ renderOverlay(ctx) {
11923
+ if (this.pings.length === 0) return;
11924
+ const now = this.now();
11925
+ for (const ping of this.pings) {
11926
+ renderPingPulse(ctx, ping.x, ping.y, now - ping.t, {
11927
+ color: this.color,
11928
+ durationMs: this.durationMs,
11929
+ radius: this.radius
11930
+ });
11931
+ }
11932
+ }
11933
+ ensureAnimating(ctx) {
11934
+ if (this.rafId === null) {
11935
+ this.rafId = requestAnimationFrame(() => this.tick(ctx));
11936
+ }
11937
+ }
11938
+ tick(ctx) {
11939
+ const cutoff = this.now() - this.durationMs;
11940
+ this.pings = this.pings.filter((ping) => ping.t > cutoff);
11941
+ ctx.requestRender();
11942
+ this.rafId = this.pings.length > 0 ? requestAnimationFrame(() => this.tick(ctx)) : null;
11943
+ }
11944
+ notifyOptionsChange() {
11945
+ for (const listener of this.optionListeners) listener();
11946
+ }
11947
+ };
11948
+
11468
11949
  // src/index.ts
11469
- var VERSION = "0.54.0";
11950
+ var VERSION = "0.56.0";
11470
11951
  // Annotate the CommonJS export names for ESM import in node:
11471
11952
  0 && (module.exports = {
11472
11953
  ArrowTool,
@@ -11486,8 +11967,12 @@ var VERSION = "0.54.0";
11486
11967
  MeasureTool,
11487
11968
  MemoryAdapter,
11488
11969
  NoteTool,
11970
+ PING_PRESENCE_KIND,
11489
11971
  PencilTool,
11972
+ PingInput,
11973
+ PingTool,
11490
11974
  RemoteLaserOverlay,
11975
+ RemotePingOverlay,
11491
11976
  SelectTool,
11492
11977
  ShapeTool,
11493
11978
  TemplateTool,
@@ -11525,12 +12010,14 @@ var VERSION = "0.54.0";
11525
12010
  getHexDistance,
11526
12011
  isLaserTrailPresence,
11527
12012
  isNearBezier,
12013
+ isPingPresence,
11528
12014
  setFontSize,
11529
12015
  smartSnap,
11530
12016
  snapPoint,
11531
12017
  snapToHexCenter,
11532
12018
  styleToPatch,
11533
12019
  toLaserTrailPresence,
12020
+ toPingPresence,
11534
12021
  toggleBold,
11535
12022
  toggleItalic,
11536
12023
  toggleStrikethrough,