@fieldnotes/core 0.2.0 → 0.2.2
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/README.md +332 -273
- package/dist/index.cjs +193 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -6
- package/dist/index.d.ts +22 -6
- package/dist/index.js +193 -48
- package/dist/index.js.map +1 -1
- package/package.json +31 -31
package/dist/index.cjs
CHANGED
|
@@ -152,6 +152,13 @@ function migrateElement(obj) {
|
|
|
152
152
|
if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
|
|
153
153
|
obj["bend"] = 0;
|
|
154
154
|
}
|
|
155
|
+
if (obj["type"] === "stroke" && Array.isArray(obj["points"])) {
|
|
156
|
+
for (const pt of obj["points"]) {
|
|
157
|
+
if (typeof pt["pressure"] !== "number") {
|
|
158
|
+
pt["pressure"] = 0.5;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
// src/core/auto-save.ts
|
|
@@ -407,24 +414,8 @@ var InputHandler = class {
|
|
|
407
414
|
y: e.clientY - rect.top
|
|
408
415
|
});
|
|
409
416
|
};
|
|
410
|
-
isInteractiveHtmlContent(e) {
|
|
411
|
-
const target = e.target;
|
|
412
|
-
if (!target) return false;
|
|
413
|
-
const node = target.closest("[data-element-id]");
|
|
414
|
-
if (!node) return false;
|
|
415
|
-
const elementId = node.dataset["elementId"];
|
|
416
|
-
if (!elementId) return false;
|
|
417
|
-
const store = this.toolContext?.store;
|
|
418
|
-
if (!store) return false;
|
|
419
|
-
const element = store.getById(elementId);
|
|
420
|
-
if (!element || element.type !== "html") return false;
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
417
|
onPointerDown = (e) => {
|
|
424
418
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
425
|
-
if (this.isInteractiveHtmlContent(e)) {
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
419
|
this.element.setPointerCapture?.(e.pointerId);
|
|
429
420
|
if (this.activePointers.size === 2) {
|
|
430
421
|
this.startPinch();
|
|
@@ -750,6 +741,79 @@ function isNearLine(point, a, b, threshold) {
|
|
|
750
741
|
return Math.hypot(point.x - projX, point.y - projY) <= threshold;
|
|
751
742
|
}
|
|
752
743
|
|
|
744
|
+
// src/elements/stroke-smoothing.ts
|
|
745
|
+
var MIN_PRESSURE_SCALE = 0.2;
|
|
746
|
+
function pressureToWidth(pressure, baseWidth) {
|
|
747
|
+
return baseWidth * (MIN_PRESSURE_SCALE + (1 - MIN_PRESSURE_SCALE) * pressure);
|
|
748
|
+
}
|
|
749
|
+
function simplifyPoints(points, tolerance) {
|
|
750
|
+
if (points.length <= 2) return points.slice();
|
|
751
|
+
return rdp(points, 0, points.length - 1, tolerance);
|
|
752
|
+
}
|
|
753
|
+
function rdp(points, start, end, tolerance) {
|
|
754
|
+
const first = points[start];
|
|
755
|
+
const last = points[end];
|
|
756
|
+
if (!first || !last) return [];
|
|
757
|
+
if (end - start <= 1) return [first, last];
|
|
758
|
+
let maxDist = 0;
|
|
759
|
+
let maxIndex = start;
|
|
760
|
+
for (let i = start + 1; i < end; i++) {
|
|
761
|
+
const pt = points[i];
|
|
762
|
+
if (!pt) continue;
|
|
763
|
+
const dist = perpendicularDistance(pt, first, last);
|
|
764
|
+
if (dist > maxDist) {
|
|
765
|
+
maxDist = dist;
|
|
766
|
+
maxIndex = i;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (maxDist <= tolerance) return [first, last];
|
|
770
|
+
const left = rdp(points, start, maxIndex, tolerance);
|
|
771
|
+
const right = rdp(points, maxIndex, end, tolerance);
|
|
772
|
+
return left.concat(right.slice(1));
|
|
773
|
+
}
|
|
774
|
+
function perpendicularDistance(pt, lineStart, lineEnd) {
|
|
775
|
+
const dx = lineEnd.x - lineStart.x;
|
|
776
|
+
const dy = lineEnd.y - lineStart.y;
|
|
777
|
+
const lenSq = dx * dx + dy * dy;
|
|
778
|
+
if (lenSq === 0) {
|
|
779
|
+
const ex = pt.x - lineStart.x;
|
|
780
|
+
const ey = pt.y - lineStart.y;
|
|
781
|
+
return Math.sqrt(ex * ex + ey * ey);
|
|
782
|
+
}
|
|
783
|
+
const num = Math.abs(dy * pt.x - dx * pt.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x);
|
|
784
|
+
return num / Math.sqrt(lenSq);
|
|
785
|
+
}
|
|
786
|
+
function smoothToSegments(points) {
|
|
787
|
+
if (points.length < 2) return [];
|
|
788
|
+
if (points.length === 2) {
|
|
789
|
+
const p0 = points[0];
|
|
790
|
+
const p1 = points[1];
|
|
791
|
+
if (!p0 || !p1) return [];
|
|
792
|
+
const mx = (p0.x + p1.x) / 2;
|
|
793
|
+
const my = (p0.y + p1.y) / 2;
|
|
794
|
+
return [{ start: p0, cp1: { x: mx, y: my }, cp2: { x: mx, y: my }, end: p1 }];
|
|
795
|
+
}
|
|
796
|
+
const segments = [];
|
|
797
|
+
const n = points.length;
|
|
798
|
+
for (let i = 0; i < n - 1; i++) {
|
|
799
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
800
|
+
const p1 = points[i];
|
|
801
|
+
const p2 = points[i + 1];
|
|
802
|
+
const p3 = points[Math.min(n - 1, i + 2)];
|
|
803
|
+
if (!p0 || !p1 || !p2 || !p3) continue;
|
|
804
|
+
const cp1 = {
|
|
805
|
+
x: p1.x + (p2.x - p0.x) / 6,
|
|
806
|
+
y: p1.y + (p2.y - p0.y) / 6
|
|
807
|
+
};
|
|
808
|
+
const cp2 = {
|
|
809
|
+
x: p2.x - (p3.x - p1.x) / 6,
|
|
810
|
+
y: p2.y - (p3.y - p1.y) / 6
|
|
811
|
+
};
|
|
812
|
+
segments.push({ start: p1, cp1, cp2, end: p2 });
|
|
813
|
+
}
|
|
814
|
+
return segments;
|
|
815
|
+
}
|
|
816
|
+
|
|
753
817
|
// src/elements/element-renderer.ts
|
|
754
818
|
var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "image", "html"]);
|
|
755
819
|
var ARROWHEAD_LENGTH = 12;
|
|
@@ -773,22 +837,18 @@ var ElementRenderer = class {
|
|
|
773
837
|
ctx.save();
|
|
774
838
|
ctx.translate(stroke.position.x, stroke.position.y);
|
|
775
839
|
ctx.strokeStyle = stroke.color;
|
|
776
|
-
ctx.lineWidth = stroke.width;
|
|
777
840
|
ctx.lineCap = "round";
|
|
778
841
|
ctx.lineJoin = "round";
|
|
779
842
|
ctx.globalAlpha = stroke.opacity;
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
ctx.
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
ctx.lineTo(pt.x, pt.y);
|
|
789
|
-
}
|
|
843
|
+
const segments = smoothToSegments(stroke.points);
|
|
844
|
+
for (const seg of segments) {
|
|
845
|
+
const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
|
|
846
|
+
ctx.lineWidth = w;
|
|
847
|
+
ctx.beginPath();
|
|
848
|
+
ctx.moveTo(seg.start.x, seg.start.y);
|
|
849
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
850
|
+
ctx.stroke();
|
|
790
851
|
}
|
|
791
|
-
ctx.stroke();
|
|
792
852
|
ctx.restore();
|
|
793
853
|
}
|
|
794
854
|
renderArrow(ctx, arrow) {
|
|
@@ -931,6 +991,9 @@ var ToolManager = class {
|
|
|
931
991
|
register(tool) {
|
|
932
992
|
this.tools.set(tool.name, tool);
|
|
933
993
|
}
|
|
994
|
+
getTool(name) {
|
|
995
|
+
return this.tools.get(name);
|
|
996
|
+
}
|
|
934
997
|
setTool(name, ctx) {
|
|
935
998
|
const tool = this.tools.get(name);
|
|
936
999
|
if (!tool) return;
|
|
@@ -1290,6 +1353,7 @@ var Viewport = class {
|
|
|
1290
1353
|
needsRender = true;
|
|
1291
1354
|
domNodes = /* @__PURE__ */ new Map();
|
|
1292
1355
|
htmlContent = /* @__PURE__ */ new Map();
|
|
1356
|
+
interactingElementId = null;
|
|
1293
1357
|
get ctx() {
|
|
1294
1358
|
return this.canvasEl.getContext("2d");
|
|
1295
1359
|
}
|
|
@@ -1335,6 +1399,7 @@ var Viewport = class {
|
|
|
1335
1399
|
this.store.add(image);
|
|
1336
1400
|
this.historyRecorder.commit();
|
|
1337
1401
|
this.requestRender();
|
|
1402
|
+
return image.id;
|
|
1338
1403
|
}
|
|
1339
1404
|
addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
|
|
1340
1405
|
const el = createHtmlElement({ position, size });
|
|
@@ -1347,6 +1412,7 @@ var Viewport = class {
|
|
|
1347
1412
|
}
|
|
1348
1413
|
destroy() {
|
|
1349
1414
|
cancelAnimationFrame(this.animFrameId);
|
|
1415
|
+
this.stopInteracting();
|
|
1350
1416
|
this.noteEditor.destroy(this.store);
|
|
1351
1417
|
this.historyRecorder.destroy();
|
|
1352
1418
|
this.wrapper.removeEventListener("dblclick", this.onDblClick);
|
|
@@ -1404,11 +1470,74 @@ var Viewport = class {
|
|
|
1404
1470
|
}
|
|
1405
1471
|
onDblClick = (e) => {
|
|
1406
1472
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1473
|
+
const nodeEl = el?.closest("[data-element-id]");
|
|
1474
|
+
if (nodeEl) {
|
|
1475
|
+
const elementId = nodeEl.dataset["elementId"];
|
|
1476
|
+
if (elementId) {
|
|
1477
|
+
const element = this.store.getById(elementId);
|
|
1478
|
+
if (element?.type === "note") {
|
|
1479
|
+
this.startEditingNote(elementId);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const rect = this.wrapper.getBoundingClientRect();
|
|
1485
|
+
const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
1486
|
+
const world = this.camera.screenToWorld(screen);
|
|
1487
|
+
const hit = this.hitTestWorld(world);
|
|
1488
|
+
if (hit?.type === "html") {
|
|
1489
|
+
this.startInteracting(hit.id);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
hitTestWorld(world) {
|
|
1493
|
+
const elements = this.store.getAll().reverse();
|
|
1494
|
+
for (const el of elements) {
|
|
1495
|
+
if (!("size" in el)) continue;
|
|
1496
|
+
const { x, y } = el.position;
|
|
1497
|
+
const { w, h } = el.size;
|
|
1498
|
+
if (world.x >= x && world.x <= x + w && world.y >= y && world.y <= y + h) {
|
|
1499
|
+
return el;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
startInteracting(id) {
|
|
1505
|
+
this.stopInteracting();
|
|
1506
|
+
const node = this.domNodes.get(id);
|
|
1507
|
+
if (!node) return;
|
|
1508
|
+
this.interactingElementId = id;
|
|
1509
|
+
node.style.pointerEvents = "auto";
|
|
1510
|
+
node.addEventListener("pointerdown", this.onInteractNodePointerDown);
|
|
1511
|
+
window.addEventListener("keydown", this.onInteractKeyDown);
|
|
1512
|
+
window.addEventListener("pointerdown", this.onInteractPointerDown);
|
|
1513
|
+
}
|
|
1514
|
+
stopInteracting() {
|
|
1515
|
+
if (!this.interactingElementId) return;
|
|
1516
|
+
const node = this.domNodes.get(this.interactingElementId);
|
|
1517
|
+
if (node) {
|
|
1518
|
+
node.style.pointerEvents = "none";
|
|
1519
|
+
node.removeEventListener("pointerdown", this.onInteractNodePointerDown);
|
|
1520
|
+
}
|
|
1521
|
+
this.interactingElementId = null;
|
|
1522
|
+
window.removeEventListener("keydown", this.onInteractKeyDown);
|
|
1523
|
+
window.removeEventListener("pointerdown", this.onInteractPointerDown);
|
|
1524
|
+
}
|
|
1525
|
+
onInteractNodePointerDown = (e) => {
|
|
1526
|
+
e.stopPropagation();
|
|
1527
|
+
};
|
|
1528
|
+
onInteractKeyDown = (e) => {
|
|
1529
|
+
if (e.key === "Escape") {
|
|
1530
|
+
this.stopInteracting();
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
onInteractPointerDown = (e) => {
|
|
1534
|
+
if (!this.interactingElementId) return;
|
|
1535
|
+
const target = e.target;
|
|
1536
|
+
if (!target) return;
|
|
1537
|
+
const node = this.domNodes.get(this.interactingElementId);
|
|
1538
|
+
if (node && !node.contains(target)) {
|
|
1539
|
+
this.stopInteracting();
|
|
1540
|
+
}
|
|
1412
1541
|
};
|
|
1413
1542
|
onDragOver = (e) => {
|
|
1414
1543
|
e.preventDefault();
|
|
@@ -1506,7 +1635,8 @@ var Viewport = class {
|
|
|
1506
1635
|
if (content) {
|
|
1507
1636
|
node.dataset["initialized"] = "true";
|
|
1508
1637
|
Object.assign(node.style, {
|
|
1509
|
-
overflow: "hidden"
|
|
1638
|
+
overflow: "hidden",
|
|
1639
|
+
pointerEvents: "none"
|
|
1510
1640
|
});
|
|
1511
1641
|
node.appendChild(content);
|
|
1512
1642
|
}
|
|
@@ -1609,15 +1739,19 @@ var HandTool = class {
|
|
|
1609
1739
|
|
|
1610
1740
|
// src/tools/pencil-tool.ts
|
|
1611
1741
|
var MIN_POINTS_FOR_STROKE = 2;
|
|
1742
|
+
var DEFAULT_SMOOTHING = 1.5;
|
|
1743
|
+
var DEFAULT_PRESSURE = 0.5;
|
|
1612
1744
|
var PencilTool = class {
|
|
1613
1745
|
name = "pencil";
|
|
1614
1746
|
drawing = false;
|
|
1615
1747
|
points = [];
|
|
1616
1748
|
color;
|
|
1617
1749
|
width;
|
|
1750
|
+
smoothing;
|
|
1618
1751
|
constructor(options = {}) {
|
|
1619
1752
|
this.color = options.color ?? "#000000";
|
|
1620
1753
|
this.width = options.width ?? 2;
|
|
1754
|
+
this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
|
|
1621
1755
|
}
|
|
1622
1756
|
onActivate(ctx) {
|
|
1623
1757
|
ctx.setCursor?.("crosshair");
|
|
@@ -1628,16 +1762,19 @@ var PencilTool = class {
|
|
|
1628
1762
|
setOptions(options) {
|
|
1629
1763
|
if (options.color !== void 0) this.color = options.color;
|
|
1630
1764
|
if (options.width !== void 0) this.width = options.width;
|
|
1765
|
+
if (options.smoothing !== void 0) this.smoothing = options.smoothing;
|
|
1631
1766
|
}
|
|
1632
1767
|
onPointerDown(state, ctx) {
|
|
1633
1768
|
this.drawing = true;
|
|
1634
1769
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
1635
|
-
|
|
1770
|
+
const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
|
|
1771
|
+
this.points = [{ x: world.x, y: world.y, pressure }];
|
|
1636
1772
|
}
|
|
1637
1773
|
onPointerMove(state, ctx) {
|
|
1638
1774
|
if (!this.drawing) return;
|
|
1639
1775
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
1640
|
-
|
|
1776
|
+
const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
|
|
1777
|
+
this.points.push({ x: world.x, y: world.y, pressure });
|
|
1641
1778
|
ctx.requestRender();
|
|
1642
1779
|
}
|
|
1643
1780
|
onPointerUp(_state, ctx) {
|
|
@@ -1647,8 +1784,9 @@ var PencilTool = class {
|
|
|
1647
1784
|
this.points = [];
|
|
1648
1785
|
return;
|
|
1649
1786
|
}
|
|
1787
|
+
const simplified = simplifyPoints(this.points, this.smoothing);
|
|
1650
1788
|
const stroke = createStroke({
|
|
1651
|
-
points:
|
|
1789
|
+
points: simplified,
|
|
1652
1790
|
color: this.color,
|
|
1653
1791
|
width: this.width
|
|
1654
1792
|
});
|
|
@@ -1660,19 +1798,18 @@ var PencilTool = class {
|
|
|
1660
1798
|
if (!this.drawing || this.points.length < 2) return;
|
|
1661
1799
|
ctx.save();
|
|
1662
1800
|
ctx.strokeStyle = this.color;
|
|
1663
|
-
ctx.lineWidth = this.width;
|
|
1664
1801
|
ctx.lineCap = "round";
|
|
1665
1802
|
ctx.lineJoin = "round";
|
|
1666
1803
|
ctx.globalAlpha = 0.8;
|
|
1667
|
-
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1804
|
+
const segments = smoothToSegments(this.points);
|
|
1805
|
+
for (const seg of segments) {
|
|
1806
|
+
const w = (pressureToWidth(seg.start.pressure, this.width) + pressureToWidth(seg.end.pressure, this.width)) / 2;
|
|
1807
|
+
ctx.lineWidth = w;
|
|
1808
|
+
ctx.beginPath();
|
|
1809
|
+
ctx.moveTo(seg.start.x, seg.start.y);
|
|
1810
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
1811
|
+
ctx.stroke();
|
|
1674
1812
|
}
|
|
1675
|
-
ctx.stroke();
|
|
1676
1813
|
ctx.restore();
|
|
1677
1814
|
}
|
|
1678
1815
|
};
|
|
@@ -1960,7 +2097,7 @@ var SelectTool = class {
|
|
|
1960
2097
|
handleResize(world, ctx) {
|
|
1961
2098
|
if (this.mode.type !== "resizing") return;
|
|
1962
2099
|
const el = ctx.store.getById(this.mode.elementId);
|
|
1963
|
-
if (!el || !("size" in el)) return;
|
|
2100
|
+
if (!el || !("size" in el) || el.locked) return;
|
|
1964
2101
|
const { handle } = this.mode;
|
|
1965
2102
|
const dx = world.x - this.lastWorld.x;
|
|
1966
2103
|
const dy = world.y - this.lastWorld.y;
|
|
@@ -2167,6 +2304,10 @@ var ArrowTool = class {
|
|
|
2167
2304
|
this.color = options.color ?? "#000000";
|
|
2168
2305
|
this.width = options.width ?? 2;
|
|
2169
2306
|
}
|
|
2307
|
+
setOptions(options) {
|
|
2308
|
+
if (options.color !== void 0) this.color = options.color;
|
|
2309
|
+
if (options.width !== void 0) this.width = options.width;
|
|
2310
|
+
}
|
|
2170
2311
|
onPointerDown(state, ctx) {
|
|
2171
2312
|
this.drawing = true;
|
|
2172
2313
|
this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
@@ -2231,6 +2372,10 @@ var NoteTool = class {
|
|
|
2231
2372
|
this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
|
|
2232
2373
|
this.size = options.size ?? { w: 200, h: 100 };
|
|
2233
2374
|
}
|
|
2375
|
+
setOptions(options) {
|
|
2376
|
+
if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
|
|
2377
|
+
if (options.size !== void 0) this.size = options.size;
|
|
2378
|
+
}
|
|
2234
2379
|
onPointerDown(_state, _ctx) {
|
|
2235
2380
|
}
|
|
2236
2381
|
onPointerMove(_state, _ctx) {
|
|
@@ -2280,7 +2425,7 @@ var ImageTool = class {
|
|
|
2280
2425
|
};
|
|
2281
2426
|
|
|
2282
2427
|
// src/index.ts
|
|
2283
|
-
var VERSION = "0.
|
|
2428
|
+
var VERSION = "0.2.2";
|
|
2284
2429
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2285
2430
|
0 && (module.exports = {
|
|
2286
2431
|
AddElementCommand,
|