@fieldnotes/core 0.2.0 → 0.2.1
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 +188 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -6
- package/dist/index.d.ts +21 -6
- package/dist/index.js +188 -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,69 @@ 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
|
+
window.addEventListener("keydown", this.onInteractKeyDown);
|
|
1511
|
+
window.addEventListener("pointerdown", this.onInteractPointerDown);
|
|
1512
|
+
}
|
|
1513
|
+
stopInteracting() {
|
|
1514
|
+
if (!this.interactingElementId) return;
|
|
1515
|
+
const node = this.domNodes.get(this.interactingElementId);
|
|
1516
|
+
if (node) {
|
|
1517
|
+
node.style.pointerEvents = "none";
|
|
1518
|
+
}
|
|
1519
|
+
this.interactingElementId = null;
|
|
1520
|
+
window.removeEventListener("keydown", this.onInteractKeyDown);
|
|
1521
|
+
window.removeEventListener("pointerdown", this.onInteractPointerDown);
|
|
1522
|
+
}
|
|
1523
|
+
onInteractKeyDown = (e) => {
|
|
1524
|
+
if (e.key === "Escape") {
|
|
1525
|
+
this.stopInteracting();
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
onInteractPointerDown = (e) => {
|
|
1529
|
+
if (!this.interactingElementId) return;
|
|
1530
|
+
const target = e.target;
|
|
1531
|
+
if (!target) return;
|
|
1532
|
+
const node = this.domNodes.get(this.interactingElementId);
|
|
1533
|
+
if (node && !node.contains(target)) {
|
|
1534
|
+
this.stopInteracting();
|
|
1535
|
+
}
|
|
1412
1536
|
};
|
|
1413
1537
|
onDragOver = (e) => {
|
|
1414
1538
|
e.preventDefault();
|
|
@@ -1506,7 +1630,8 @@ var Viewport = class {
|
|
|
1506
1630
|
if (content) {
|
|
1507
1631
|
node.dataset["initialized"] = "true";
|
|
1508
1632
|
Object.assign(node.style, {
|
|
1509
|
-
overflow: "hidden"
|
|
1633
|
+
overflow: "hidden",
|
|
1634
|
+
pointerEvents: "none"
|
|
1510
1635
|
});
|
|
1511
1636
|
node.appendChild(content);
|
|
1512
1637
|
}
|
|
@@ -1609,15 +1734,19 @@ var HandTool = class {
|
|
|
1609
1734
|
|
|
1610
1735
|
// src/tools/pencil-tool.ts
|
|
1611
1736
|
var MIN_POINTS_FOR_STROKE = 2;
|
|
1737
|
+
var DEFAULT_SMOOTHING = 1.5;
|
|
1738
|
+
var DEFAULT_PRESSURE = 0.5;
|
|
1612
1739
|
var PencilTool = class {
|
|
1613
1740
|
name = "pencil";
|
|
1614
1741
|
drawing = false;
|
|
1615
1742
|
points = [];
|
|
1616
1743
|
color;
|
|
1617
1744
|
width;
|
|
1745
|
+
smoothing;
|
|
1618
1746
|
constructor(options = {}) {
|
|
1619
1747
|
this.color = options.color ?? "#000000";
|
|
1620
1748
|
this.width = options.width ?? 2;
|
|
1749
|
+
this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
|
|
1621
1750
|
}
|
|
1622
1751
|
onActivate(ctx) {
|
|
1623
1752
|
ctx.setCursor?.("crosshair");
|
|
@@ -1628,16 +1757,19 @@ var PencilTool = class {
|
|
|
1628
1757
|
setOptions(options) {
|
|
1629
1758
|
if (options.color !== void 0) this.color = options.color;
|
|
1630
1759
|
if (options.width !== void 0) this.width = options.width;
|
|
1760
|
+
if (options.smoothing !== void 0) this.smoothing = options.smoothing;
|
|
1631
1761
|
}
|
|
1632
1762
|
onPointerDown(state, ctx) {
|
|
1633
1763
|
this.drawing = true;
|
|
1634
1764
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
1635
|
-
|
|
1765
|
+
const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
|
|
1766
|
+
this.points = [{ x: world.x, y: world.y, pressure }];
|
|
1636
1767
|
}
|
|
1637
1768
|
onPointerMove(state, ctx) {
|
|
1638
1769
|
if (!this.drawing) return;
|
|
1639
1770
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
1640
|
-
|
|
1771
|
+
const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
|
|
1772
|
+
this.points.push({ x: world.x, y: world.y, pressure });
|
|
1641
1773
|
ctx.requestRender();
|
|
1642
1774
|
}
|
|
1643
1775
|
onPointerUp(_state, ctx) {
|
|
@@ -1647,8 +1779,9 @@ var PencilTool = class {
|
|
|
1647
1779
|
this.points = [];
|
|
1648
1780
|
return;
|
|
1649
1781
|
}
|
|
1782
|
+
const simplified = simplifyPoints(this.points, this.smoothing);
|
|
1650
1783
|
const stroke = createStroke({
|
|
1651
|
-
points:
|
|
1784
|
+
points: simplified,
|
|
1652
1785
|
color: this.color,
|
|
1653
1786
|
width: this.width
|
|
1654
1787
|
});
|
|
@@ -1660,19 +1793,18 @@ var PencilTool = class {
|
|
|
1660
1793
|
if (!this.drawing || this.points.length < 2) return;
|
|
1661
1794
|
ctx.save();
|
|
1662
1795
|
ctx.strokeStyle = this.color;
|
|
1663
|
-
ctx.lineWidth = this.width;
|
|
1664
1796
|
ctx.lineCap = "round";
|
|
1665
1797
|
ctx.lineJoin = "round";
|
|
1666
1798
|
ctx.globalAlpha = 0.8;
|
|
1667
|
-
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1799
|
+
const segments = smoothToSegments(this.points);
|
|
1800
|
+
for (const seg of segments) {
|
|
1801
|
+
const w = (pressureToWidth(seg.start.pressure, this.width) + pressureToWidth(seg.end.pressure, this.width)) / 2;
|
|
1802
|
+
ctx.lineWidth = w;
|
|
1803
|
+
ctx.beginPath();
|
|
1804
|
+
ctx.moveTo(seg.start.x, seg.start.y);
|
|
1805
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
1806
|
+
ctx.stroke();
|
|
1674
1807
|
}
|
|
1675
|
-
ctx.stroke();
|
|
1676
1808
|
ctx.restore();
|
|
1677
1809
|
}
|
|
1678
1810
|
};
|
|
@@ -1960,7 +2092,7 @@ var SelectTool = class {
|
|
|
1960
2092
|
handleResize(world, ctx) {
|
|
1961
2093
|
if (this.mode.type !== "resizing") return;
|
|
1962
2094
|
const el = ctx.store.getById(this.mode.elementId);
|
|
1963
|
-
if (!el || !("size" in el)) return;
|
|
2095
|
+
if (!el || !("size" in el) || el.locked) return;
|
|
1964
2096
|
const { handle } = this.mode;
|
|
1965
2097
|
const dx = world.x - this.lastWorld.x;
|
|
1966
2098
|
const dy = world.y - this.lastWorld.y;
|
|
@@ -2167,6 +2299,10 @@ var ArrowTool = class {
|
|
|
2167
2299
|
this.color = options.color ?? "#000000";
|
|
2168
2300
|
this.width = options.width ?? 2;
|
|
2169
2301
|
}
|
|
2302
|
+
setOptions(options) {
|
|
2303
|
+
if (options.color !== void 0) this.color = options.color;
|
|
2304
|
+
if (options.width !== void 0) this.width = options.width;
|
|
2305
|
+
}
|
|
2170
2306
|
onPointerDown(state, ctx) {
|
|
2171
2307
|
this.drawing = true;
|
|
2172
2308
|
this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
@@ -2231,6 +2367,10 @@ var NoteTool = class {
|
|
|
2231
2367
|
this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
|
|
2232
2368
|
this.size = options.size ?? { w: 200, h: 100 };
|
|
2233
2369
|
}
|
|
2370
|
+
setOptions(options) {
|
|
2371
|
+
if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
|
|
2372
|
+
if (options.size !== void 0) this.size = options.size;
|
|
2373
|
+
}
|
|
2234
2374
|
onPointerDown(_state, _ctx) {
|
|
2235
2375
|
}
|
|
2236
2376
|
onPointerMove(_state, _ctx) {
|
|
@@ -2280,7 +2420,7 @@ var ImageTool = class {
|
|
|
2280
2420
|
};
|
|
2281
2421
|
|
|
2282
2422
|
// src/index.ts
|
|
2283
|
-
var VERSION = "0.1
|
|
2423
|
+
var VERSION = "0.2.1";
|
|
2284
2424
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2285
2425
|
0 && (module.exports = {
|
|
2286
2426
|
AddElementCommand,
|