@fieldnotes/core 0.54.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 +305 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -3
- package/dist/index.d.ts +145 -3
- package/dist/index.js +300 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8675,6 +8675,176 @@ var RemoteLaserOverlay = class {
|
|
|
8675
8675
|
}
|
|
8676
8676
|
};
|
|
8677
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
|
+
|
|
8678
8848
|
// src/tools/hand-tool.ts
|
|
8679
8849
|
var HandTool = class {
|
|
8680
8850
|
name = "hand";
|
|
@@ -8936,7 +9106,7 @@ function erasePoints(points, eraser, radius) {
|
|
|
8936
9106
|
}
|
|
8937
9107
|
|
|
8938
9108
|
// src/tools/eraser-tool.ts
|
|
8939
|
-
var
|
|
9109
|
+
var DEFAULT_RADIUS2 = 20;
|
|
8940
9110
|
function makeEraserCursor(radius) {
|
|
8941
9111
|
const size = radius * 2;
|
|
8942
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>`;
|
|
@@ -8949,7 +9119,7 @@ var EraserTool = class {
|
|
|
8949
9119
|
cursor;
|
|
8950
9120
|
mode;
|
|
8951
9121
|
constructor(options = {}) {
|
|
8952
|
-
this.radius = options.radius ??
|
|
9122
|
+
this.radius = options.radius ?? DEFAULT_RADIUS2;
|
|
8953
9123
|
this.cursor = makeEraserCursor(this.radius);
|
|
8954
9124
|
this.mode = options.mode ?? "partial";
|
|
8955
9125
|
}
|
|
@@ -11231,7 +11401,7 @@ var TemplateTool = class {
|
|
|
11231
11401
|
};
|
|
11232
11402
|
|
|
11233
11403
|
// src/tools/laser-tool.ts
|
|
11234
|
-
var
|
|
11404
|
+
var DEFAULT_COLOR3 = "#ff3b30";
|
|
11235
11405
|
var DEFAULT_WIDTH2 = 4;
|
|
11236
11406
|
var DEFAULT_FADE_MS2 = 1200;
|
|
11237
11407
|
var LaserTool = class {
|
|
@@ -11247,7 +11417,7 @@ var LaserTool = class {
|
|
|
11247
11417
|
pendingEmission = [];
|
|
11248
11418
|
constructor(options = {}) {
|
|
11249
11419
|
this.name = options.name ?? "laser";
|
|
11250
|
-
this.color = options.color ??
|
|
11420
|
+
this.color = options.color ?? DEFAULT_COLOR3;
|
|
11251
11421
|
this.width = options.width ?? DEFAULT_WIDTH2;
|
|
11252
11422
|
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
|
|
11253
11423
|
}
|
|
@@ -11374,8 +11544,128 @@ var LaserTool = class {
|
|
|
11374
11544
|
}
|
|
11375
11545
|
};
|
|
11376
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
|
+
|
|
11377
11667
|
// src/index.ts
|
|
11378
|
-
var VERSION = "0.
|
|
11668
|
+
var VERSION = "0.55.0";
|
|
11379
11669
|
export {
|
|
11380
11670
|
ArrowTool,
|
|
11381
11671
|
AutoSave,
|
|
@@ -11394,8 +11684,11 @@ export {
|
|
|
11394
11684
|
MeasureTool,
|
|
11395
11685
|
MemoryAdapter,
|
|
11396
11686
|
NoteTool,
|
|
11687
|
+
PING_PRESENCE_KIND,
|
|
11397
11688
|
PencilTool,
|
|
11689
|
+
PingTool,
|
|
11398
11690
|
RemoteLaserOverlay,
|
|
11691
|
+
RemotePingOverlay,
|
|
11399
11692
|
SelectTool,
|
|
11400
11693
|
ShapeTool,
|
|
11401
11694
|
TemplateTool,
|
|
@@ -11433,12 +11726,14 @@ export {
|
|
|
11433
11726
|
getHexDistance,
|
|
11434
11727
|
isLaserTrailPresence,
|
|
11435
11728
|
isNearBezier,
|
|
11729
|
+
isPingPresence,
|
|
11436
11730
|
setFontSize,
|
|
11437
11731
|
smartSnap,
|
|
11438
11732
|
snapPoint,
|
|
11439
11733
|
snapToHexCenter,
|
|
11440
11734
|
styleToPatch,
|
|
11441
11735
|
toLaserTrailPresence,
|
|
11736
|
+
toPingPresence,
|
|
11442
11737
|
toggleBold,
|
|
11443
11738
|
toggleItalic,
|
|
11444
11739
|
toggleStrikethrough,
|