@aznabee/freehand-ui 0.1.2 → 0.1.4
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 +212 -157
- package/dist/freehand-ui.cjs +251 -135
- package/dist/freehand-ui.js +251 -135
- package/dist/index.d.ts +22 -6
- package/dist/react.cjs +252 -136
- package/dist/react.d.ts +3 -3
- package/dist/react.js +252 -136
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -63,9 +63,16 @@ function mergeOptions(options = {}) {
|
|
|
63
63
|
if (merged.note && typeof merged.note === "string") {
|
|
64
64
|
merged.note = { text: merged.note, position: "top-right" };
|
|
65
65
|
}
|
|
66
|
-
if (merged.arrow
|
|
67
|
-
const
|
|
68
|
-
merged.arrow = {
|
|
66
|
+
if (merged.arrow) {
|
|
67
|
+
const config = merged.arrow === true ? {} : merged.arrow;
|
|
68
|
+
merged.arrow = {
|
|
69
|
+
// "note" launches from the annotation, a position name from that side of
|
|
70
|
+
// the element. Normalised for every arrow, not just `arrow: true`, so an
|
|
71
|
+
// explicit `from` is honoured whether or not there is a note.
|
|
72
|
+
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
73
|
+
to: config.to ?? "edge",
|
|
74
|
+
style: config.style ?? "curved"
|
|
75
|
+
};
|
|
69
76
|
}
|
|
70
77
|
if (merged.decorations === true) {
|
|
71
78
|
merged.decorations = { count: null, types: null };
|
|
@@ -210,10 +217,6 @@ function intersectionArea(a, b) {
|
|
|
210
217
|
const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
211
218
|
return w > 0 && h > 0 ? w * h : 0;
|
|
212
219
|
}
|
|
213
|
-
function outsideArea(box, bounds) {
|
|
214
|
-
if (!bounds) return 0;
|
|
215
|
-
return Math.max(0, box.width * box.height - intersectionArea(box, bounds));
|
|
216
|
-
}
|
|
217
220
|
function nearestPointOnRect(rect, point) {
|
|
218
221
|
return {
|
|
219
222
|
x: clamp(point.x, rect.x, rect.x + rect.width),
|
|
@@ -226,6 +229,17 @@ function distanceToRoundedRect(point, rect, radius = 0) {
|
|
|
226
229
|
const cy = clamp(point.y, rect.y + r, rect.y + rect.height - r);
|
|
227
230
|
return Math.hypot(point.x - cx, point.y - cy) - r;
|
|
228
231
|
}
|
|
232
|
+
function pointOnBoxToward(box, target) {
|
|
233
|
+
const cx = box.x + box.width / 2;
|
|
234
|
+
const cy = box.y + box.height / 2;
|
|
235
|
+
const dx = target.x - cx;
|
|
236
|
+
const dy = target.y - cy;
|
|
237
|
+
if (dx === 0 && dy === 0) return { x: cx, y: cy };
|
|
238
|
+
const hw = box.width / 2 || 1e-3;
|
|
239
|
+
const hh = box.height / 2 || 1e-3;
|
|
240
|
+
const scale = 1 / Math.max(Math.abs(dx) / hw, Math.abs(dy) / hh);
|
|
241
|
+
return { x: cx + dx * scale, y: cy + dy * scale };
|
|
242
|
+
}
|
|
229
243
|
function directionFromPosition(position) {
|
|
230
244
|
return {
|
|
231
245
|
x: position.includes("right") ? 1 : position.includes("left") ? -1 : 0,
|
|
@@ -475,21 +489,14 @@ function isBroken(fraction, breaks) {
|
|
|
475
489
|
function createBorderBreaks(perimeter, config, random) {
|
|
476
490
|
if (!config || perimeter <= 0) return [];
|
|
477
491
|
const settings = config === true ? {} : config;
|
|
478
|
-
const count = Math.max(
|
|
479
|
-
1,
|
|
480
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
481
|
-
);
|
|
492
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
482
493
|
const min = Math.max(2, settings.min ?? 6);
|
|
483
494
|
const max = Math.max(min, settings.max ?? 30);
|
|
484
495
|
const slice = perimeter / count;
|
|
485
496
|
let budget = perimeter * 0.35;
|
|
486
497
|
const breaks = [];
|
|
487
498
|
for (let i = 0; i < count; i++) {
|
|
488
|
-
const length = Math.min(
|
|
489
|
-
min + random() * (max - min),
|
|
490
|
-
slice * 0.7,
|
|
491
|
-
budget
|
|
492
|
-
);
|
|
499
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
493
500
|
if (length < 2) break;
|
|
494
501
|
budget -= length;
|
|
495
502
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -589,14 +596,86 @@ function drawBorder(svg, rect, options, seed) {
|
|
|
589
596
|
});
|
|
590
597
|
}
|
|
591
598
|
}
|
|
599
|
+
function cubicAt(p0, c1, c2, p3, t) {
|
|
600
|
+
const u = 1 - t;
|
|
601
|
+
const a = u * u * u;
|
|
602
|
+
const b = 3 * u * u * t;
|
|
603
|
+
const c = 3 * u * t * t;
|
|
604
|
+
const d = t * t * t;
|
|
605
|
+
return {
|
|
606
|
+
x: a * p0.x + b * c1.x + c * c2.x + d * p3.x,
|
|
607
|
+
y: a * p0.y + b * c1.y + c * c2.y + d * p3.y
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
611
|
+
const u = 1 - t;
|
|
612
|
+
const a = 3 * u * u;
|
|
613
|
+
const b = 6 * u * t;
|
|
614
|
+
const c = 3 * t * t;
|
|
615
|
+
return {
|
|
616
|
+
x: a * (c1.x - p0.x) + b * (c2.x - c1.x) + c * (p3.x - c2.x),
|
|
617
|
+
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
621
|
+
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
622
|
+
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
623
|
+
const knot = 0.46 + random() * 0.12;
|
|
624
|
+
const points = [];
|
|
625
|
+
const lead = Math.max(2, Math.round(knot * 16));
|
|
626
|
+
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
627
|
+
const pivot = at(knot);
|
|
628
|
+
const heading = tangentAt(knot);
|
|
629
|
+
const normal = { x: -heading.y, y: heading.x };
|
|
630
|
+
const side = random() < 0.5 ? 1 : -1;
|
|
631
|
+
const centre = {
|
|
632
|
+
x: pivot.x + normal.x * radius * side,
|
|
633
|
+
y: pivot.y + normal.y * radius * side
|
|
634
|
+
};
|
|
635
|
+
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
636
|
+
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
637
|
+
const steps = 18;
|
|
638
|
+
for (let i = 1; i <= steps; i++) {
|
|
639
|
+
const progress = i / steps;
|
|
640
|
+
const angle = from + turns * progress * side;
|
|
641
|
+
const drift = radius * 0.55 * progress;
|
|
642
|
+
points.push({
|
|
643
|
+
x: centre.x + Math.cos(angle) * radius + heading.x * drift,
|
|
644
|
+
y: centre.y + Math.sin(angle) * radius + heading.y * drift
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
const tail = 14;
|
|
648
|
+
for (let i = 1; i <= tail; i++) {
|
|
649
|
+
points.push(at(knot + (1 - knot) * (i / tail)));
|
|
650
|
+
}
|
|
651
|
+
return points;
|
|
652
|
+
}
|
|
592
653
|
function normalize(vector) {
|
|
593
654
|
const length = Math.hypot(vector.x, vector.y);
|
|
594
655
|
if (!length) return { x: 0, y: 0 };
|
|
595
656
|
return { x: vector.x / length, y: vector.y / length };
|
|
596
657
|
}
|
|
597
|
-
|
|
658
|
+
var NOTE_CLEARANCE = 10;
|
|
659
|
+
var TIP_GAP = 11;
|
|
660
|
+
function arrowStartFromNote(origin, target, from) {
|
|
598
661
|
const cx = origin.x + origin.width / 2;
|
|
599
662
|
const cy = origin.y + origin.height / 2;
|
|
663
|
+
if (from && from !== "note") {
|
|
664
|
+
const point = pointFromPosition(from, origin);
|
|
665
|
+
const away = directionFromPosition(from);
|
|
666
|
+
if (!away.x && !away.y) {
|
|
667
|
+
const edge = pointOnBoxToward(origin, target);
|
|
668
|
+
const outward = normalize({ x: edge.x - cx, y: edge.y - cy });
|
|
669
|
+
return {
|
|
670
|
+
x: edge.x + outward.x * NOTE_CLEARANCE,
|
|
671
|
+
y: edge.y + outward.y * NOTE_CLEARANCE
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
x: point.x + away.x * NOTE_CLEARANCE,
|
|
676
|
+
y: point.y + away.y * NOTE_CLEARANCE
|
|
677
|
+
};
|
|
678
|
+
}
|
|
600
679
|
const dx = target.x - cx;
|
|
601
680
|
const dy = target.y - cy;
|
|
602
681
|
const hw = origin.width / 2 || 1e-3;
|
|
@@ -604,15 +683,16 @@ function arrowStartFromNote(origin, target) {
|
|
|
604
683
|
const sx = Math.sign(dx) || 1;
|
|
605
684
|
const sy = Math.sign(dy) || 1;
|
|
606
685
|
if (Math.abs(dy) / hh >= Math.abs(dx) / hw) {
|
|
607
|
-
return { x: cx - sx * hw
|
|
686
|
+
return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
|
|
608
687
|
}
|
|
609
|
-
return { x: cx + sx * (hw +
|
|
688
|
+
return { x: cx + sx * (hw + 8), y: cy + sy * (hh + 6) };
|
|
610
689
|
}
|
|
611
690
|
function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
612
691
|
const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
692
|
+
const from = arrowOptions.from || "note";
|
|
613
693
|
const anchored = Boolean(origin && origin.width > 0 && origin.height > 0);
|
|
614
|
-
let start = anchored ? arrowStartFromNote(origin, center) : annotationAnchor(
|
|
615
|
-
|
|
694
|
+
let start = anchored ? arrowStartFromNote(origin, center, from) : annotationAnchor(
|
|
695
|
+
from === "note" ? "top-right" : from,
|
|
616
696
|
rect,
|
|
617
697
|
38 + random() * 12
|
|
618
698
|
);
|
|
@@ -624,7 +704,7 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
624
704
|
const near = nearestPointOnRect(rect, start);
|
|
625
705
|
let outward = normalize({ x: start.x - near.x, y: start.y - near.y });
|
|
626
706
|
if (!outward.x && !outward.y) outward = { x: 0, y: -1 };
|
|
627
|
-
const gap =
|
|
707
|
+
const gap = TIP_GAP + random() * 5;
|
|
628
708
|
end = { x: near.x + outward.x * gap, y: near.y + outward.y * gap };
|
|
629
709
|
} else {
|
|
630
710
|
end = pointFromPosition(toPos, rect);
|
|
@@ -637,6 +717,35 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
637
717
|
}
|
|
638
718
|
return { start, end };
|
|
639
719
|
}
|
|
720
|
+
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
721
|
+
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
722
|
+
const spread = 0.46 + random() * 0.1;
|
|
723
|
+
for (const side of [-1, 1]) {
|
|
724
|
+
const armAngle = angle + spread * side;
|
|
725
|
+
const tail = {
|
|
726
|
+
x: tip.x - length * Math.cos(armAngle),
|
|
727
|
+
y: tip.y - length * Math.sin(armAngle)
|
|
728
|
+
};
|
|
729
|
+
appendPath(
|
|
730
|
+
svg,
|
|
731
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
732
|
+
steps: 2,
|
|
733
|
+
bow: 0.35 * side
|
|
734
|
+
}),
|
|
735
|
+
{ ...style, dashed: false }
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function boundsOf(points) {
|
|
740
|
+
const xs = points.map((p) => p.x);
|
|
741
|
+
const ys = points.map((p) => p.y);
|
|
742
|
+
return {
|
|
743
|
+
x: Math.min(...xs),
|
|
744
|
+
y: Math.min(...ys),
|
|
745
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
746
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
747
|
+
};
|
|
748
|
+
}
|
|
640
749
|
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
641
750
|
const random = createRandom(seed + 17);
|
|
642
751
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
@@ -658,13 +767,33 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
658
767
|
x: rect.x + rect.width / 2 - mid.x,
|
|
659
768
|
y: rect.y + rect.height / 2 - mid.y
|
|
660
769
|
};
|
|
661
|
-
const
|
|
770
|
+
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
771
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
|
|
662
772
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
663
773
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
664
774
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
665
775
|
const c2x = start.x + dx * 0.72 + perpendicular.x * bow * 0.85;
|
|
666
776
|
const c2y = start.y + dy * 0.72 + perpendicular.y * bow * 0.85;
|
|
667
777
|
hull.push({ x: c1x, y: c1y }, { x: c2x, y: c2y });
|
|
778
|
+
if (arrowOptions.style === "looped") {
|
|
779
|
+
const radius = clamp(distance * 0.11, 7, 17);
|
|
780
|
+
const points = loopedCurvePoints(
|
|
781
|
+
start,
|
|
782
|
+
{ x: c1x, y: c1y },
|
|
783
|
+
{ x: c2x, y: c2y },
|
|
784
|
+
end,
|
|
785
|
+
radius,
|
|
786
|
+
random
|
|
787
|
+
);
|
|
788
|
+
shaft = catmullRomPath(points, false);
|
|
789
|
+
hull.push(...points);
|
|
790
|
+
const last = points[points.length - 1];
|
|
791
|
+
const prior = points[points.length - 2] ?? start;
|
|
792
|
+
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
793
|
+
appendPath(svg, shaft, { ...style, dashed });
|
|
794
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
795
|
+
return { start, end, bounds: boundsOf(hull) };
|
|
796
|
+
}
|
|
668
797
|
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
669
798
|
if (style.roughness >= 1.1) {
|
|
670
799
|
const drift = () => (random() - 0.5) * 1.4;
|
|
@@ -679,31 +808,8 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
679
808
|
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
680
809
|
}
|
|
681
810
|
appendPath(svg, shaft, { ...style, dashed });
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
for (const side of [-1, 1]) {
|
|
685
|
-
const angle = tipAngle + spread * side;
|
|
686
|
-
const tail = {
|
|
687
|
-
x: end.x - headLength * Math.cos(angle),
|
|
688
|
-
y: end.y - headLength * Math.sin(angle)
|
|
689
|
-
};
|
|
690
|
-
appendPath(
|
|
691
|
-
svg,
|
|
692
|
-
roughLine(tail.x, tail.y, end.x, end.y, style.roughness * 0.5, random, {
|
|
693
|
-
steps: 2,
|
|
694
|
-
bow: 0.35 * side
|
|
695
|
-
}),
|
|
696
|
-
{ ...style, dashed: false }
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
const xs = hull.map((p) => p.x);
|
|
700
|
-
const ys = hull.map((p) => p.y);
|
|
701
|
-
const bounds = {
|
|
702
|
-
x: Math.min(...xs),
|
|
703
|
-
y: Math.min(...ys),
|
|
704
|
-
width: Math.max(...xs) - Math.min(...xs),
|
|
705
|
-
height: Math.max(...ys) - Math.min(...ys)
|
|
706
|
-
};
|
|
811
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
812
|
+
const bounds = boundsOf(hull);
|
|
707
813
|
return { start, end, bounds };
|
|
708
814
|
}
|
|
709
815
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
@@ -741,11 +847,38 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
741
847
|
}
|
|
742
848
|
);
|
|
743
849
|
}
|
|
744
|
-
|
|
850
|
+
var DEFAULT_WORDS_PER_LINE = 2;
|
|
851
|
+
function wrapWords(text, perLine) {
|
|
852
|
+
const lines = [];
|
|
853
|
+
for (const paragraph of String(text).split(/\r?\n/)) {
|
|
854
|
+
const words = paragraph.trim().split(/\s+/).filter(Boolean);
|
|
855
|
+
if (!words.length) continue;
|
|
856
|
+
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
857
|
+
for (let i = 0; i < words.length; i += size) {
|
|
858
|
+
lines.push(words.slice(i, i + size).join(" "));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return lines.length ? lines : [String(text)];
|
|
862
|
+
}
|
|
863
|
+
function measureWidth(element, content, fontSize) {
|
|
864
|
+
try {
|
|
865
|
+
const box = element.getBBox();
|
|
866
|
+
if (box && box.width > 0) return box.width;
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
const measured = element.getComputedTextLength();
|
|
871
|
+
if (Number.isFinite(measured) && measured > 0) return measured;
|
|
872
|
+
} catch {
|
|
873
|
+
}
|
|
874
|
+
return content.length * fontSize * 0.45;
|
|
875
|
+
}
|
|
876
|
+
function createNoteText(svg, text, style, options = {}) {
|
|
745
877
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
878
|
+
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
879
|
+
const lineHeight = fontSize * 1.05;
|
|
746
880
|
const group = document.createElementNS(SVG_NS, "g");
|
|
747
881
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
748
|
-
textEl.textContent = text;
|
|
749
882
|
textEl.setAttribute("x", "0");
|
|
750
883
|
textEl.setAttribute("y", "0");
|
|
751
884
|
textEl.setAttribute("fill", style.color);
|
|
@@ -756,31 +889,43 @@ function createNoteText(svg, text, style) {
|
|
|
756
889
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
757
890
|
textEl.setAttribute("text-anchor", "start");
|
|
758
891
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
892
|
+
const tspans = lines.map((line, index) => {
|
|
893
|
+
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
894
|
+
tspan.textContent = line;
|
|
895
|
+
tspan.setAttribute("x", "0");
|
|
896
|
+
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
897
|
+
textEl.appendChild(tspan);
|
|
898
|
+
return tspan;
|
|
899
|
+
});
|
|
759
900
|
group.appendChild(textEl);
|
|
760
901
|
svg.appendChild(group);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
} catch {
|
|
766
|
-
}
|
|
767
|
-
if (!width) {
|
|
768
|
-
try {
|
|
769
|
-
const measured = textEl.getComputedTextLength();
|
|
770
|
-
if (Number.isFinite(measured) && measured > 0) width = measured;
|
|
771
|
-
} catch {
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
if (!width) width = text.length * fontSize * 0.45;
|
|
902
|
+
const lineWidths = tspans.map(
|
|
903
|
+
(tspan, index) => measureWidth(tspan, lines[index], fontSize)
|
|
904
|
+
);
|
|
905
|
+
const width = Math.max(...lineWidths);
|
|
775
906
|
const ascent = fontSize * 0.74;
|
|
776
907
|
const descent = fontSize * 0.26;
|
|
777
|
-
|
|
908
|
+
const height = (lines.length - 1) * lineHeight + ascent + descent;
|
|
909
|
+
return {
|
|
910
|
+
group,
|
|
911
|
+
textEl,
|
|
912
|
+
tspans,
|
|
913
|
+
lines,
|
|
914
|
+
lineWidths,
|
|
915
|
+
lineHeight,
|
|
916
|
+
width,
|
|
917
|
+
height,
|
|
918
|
+
ascent,
|
|
919
|
+
descent,
|
|
920
|
+
fontSize
|
|
921
|
+
};
|
|
778
922
|
}
|
|
779
923
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
780
924
|
const random = createRandom(seed + 55);
|
|
781
925
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
782
926
|
note.textEl.setAttribute("x", fmt(box.x));
|
|
783
927
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
928
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
|
|
784
929
|
const cx = box.x + box.width / 2;
|
|
785
930
|
const cy = box.y + box.height / 2;
|
|
786
931
|
note.group.setAttribute(
|
|
@@ -788,11 +933,12 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
788
933
|
`rotate(${tilt.toFixed(2)} ${fmt(cx)} ${fmt(cy)})`
|
|
789
934
|
);
|
|
790
935
|
if (options.underline !== false) {
|
|
936
|
+
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
791
937
|
drawUnderline(
|
|
792
938
|
note.group,
|
|
793
939
|
box.x + 1,
|
|
794
940
|
box.y + box.height + 2,
|
|
795
|
-
|
|
941
|
+
lastWidth - 2,
|
|
796
942
|
{
|
|
797
943
|
color: style.color,
|
|
798
944
|
strokeWidth: style.strokeWidth || 1.4,
|
|
@@ -824,25 +970,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
824
970
|
|
|
825
971
|
// src/annotations.js
|
|
826
972
|
var NOTE_GAP = 14;
|
|
827
|
-
function mirror(position, axis) {
|
|
828
|
-
if (axis === "x") {
|
|
829
|
-
if (position.includes("right")) return position.replace("right", "left");
|
|
830
|
-
if (position.includes("left")) return position.replace("left", "right");
|
|
831
|
-
return position;
|
|
832
|
-
}
|
|
833
|
-
if (position.includes("top")) return position.replace("top", "bottom");
|
|
834
|
-
if (position.includes("bottom")) return position.replace("bottom", "top");
|
|
835
|
-
return position;
|
|
836
|
-
}
|
|
837
|
-
function candidatePositions(position) {
|
|
838
|
-
const candidates = [
|
|
839
|
-
position,
|
|
840
|
-
mirror(position, "x"),
|
|
841
|
-
mirror(position, "y"),
|
|
842
|
-
mirror(mirror(position, "x"), "y")
|
|
843
|
-
];
|
|
844
|
-
return candidates.filter((value, index) => candidates.indexOf(value) === index);
|
|
845
|
-
}
|
|
846
973
|
function boxForPosition(rect, position, size, gap) {
|
|
847
974
|
const direction = directionFromPosition(position);
|
|
848
975
|
const guard = inflateRect(rect, gap);
|
|
@@ -861,75 +988,67 @@ function boxForPosition(rect, position, size, gap) {
|
|
|
861
988
|
}
|
|
862
989
|
return { x, y, width: size.width, height: size.height };
|
|
863
990
|
}
|
|
864
|
-
function
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
if (!best || penalty < best.penalty) {
|
|
871
|
-
best = { box, penalty, position: candidate };
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
|
-
return best;
|
|
875
|
-
}
|
|
876
|
-
function clampToViewport(box, guard, viewport) {
|
|
877
|
-
if (!viewport || viewport.width <= 0 || viewport.height <= 0) return box;
|
|
878
|
-
const shifted = {
|
|
879
|
-
...box,
|
|
880
|
-
x: clamp(
|
|
881
|
-
box.x,
|
|
882
|
-
viewport.x,
|
|
883
|
-
Math.max(viewport.x, viewport.x + viewport.width - box.width)
|
|
884
|
-
),
|
|
885
|
-
y: clamp(
|
|
886
|
-
box.y,
|
|
887
|
-
viewport.y,
|
|
888
|
-
Math.max(viewport.y, viewport.y + viewport.height - box.height)
|
|
889
|
-
)
|
|
890
|
-
};
|
|
891
|
-
if (intersectionArea(shifted, guard) > intersectionArea(box, guard)) {
|
|
892
|
-
return box;
|
|
991
|
+
function limitAgainstElement(box, guard, dx) {
|
|
992
|
+
const level = box.y < guard.y + guard.height && box.y + box.height > guard.y;
|
|
993
|
+
if (!level) return dx;
|
|
994
|
+
if (dx < 0) {
|
|
995
|
+
const room2 = box.x - (guard.x + guard.width);
|
|
996
|
+
return room2 <= 0 ? 0 : -Math.min(-dx, room2);
|
|
893
997
|
}
|
|
894
|
-
|
|
998
|
+
const room = guard.x - (box.x + box.width);
|
|
999
|
+
return room <= 0 ? 0 : Math.min(dx, room);
|
|
1000
|
+
}
|
|
1001
|
+
function nudgeIntoBand(box, guard, band) {
|
|
1002
|
+
if (!band || band.width <= 0) return box;
|
|
1003
|
+
const overhangLeft = band.x - box.x;
|
|
1004
|
+
const overhangRight = box.x + box.width - (band.x + band.width);
|
|
1005
|
+
let dx = 0;
|
|
1006
|
+
if (overhangLeft > 0) dx = overhangLeft;
|
|
1007
|
+
else if (overhangRight > 0) dx = -overhangRight;
|
|
1008
|
+
if (!dx) return box;
|
|
1009
|
+
dx = limitAgainstElement(box, guard, dx);
|
|
1010
|
+
if (!dx) return box;
|
|
1011
|
+
return { ...box, x: box.x + dx };
|
|
895
1012
|
}
|
|
896
1013
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
897
1014
|
if (!noteOptions?.text) return null;
|
|
898
|
-
const
|
|
1015
|
+
const band = layout.band ?? null;
|
|
899
1016
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
900
1017
|
const random = createRandom(seed + 9);
|
|
901
|
-
const note = createNoteText(svg, noteOptions.text, style
|
|
1018
|
+
const note = createNoteText(svg, noteOptions.text, style, {
|
|
1019
|
+
wordsPerLine: noteOptions.wordsPerLine
|
|
1020
|
+
});
|
|
902
1021
|
const tilt = (random() - 0.45) * 7;
|
|
903
1022
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
904
1023
|
const size = {
|
|
905
1024
|
width: note.width,
|
|
906
|
-
height: note.
|
|
1025
|
+
height: note.height + (noteOptions.underline === false ? 0 : 5)
|
|
907
1026
|
};
|
|
908
1027
|
const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
|
|
909
|
-
const placed =
|
|
1028
|
+
const placed = boxForPosition(
|
|
910
1029
|
rect,
|
|
911
1030
|
position,
|
|
912
1031
|
{ width: size.width, height: size.height + tiltPad },
|
|
913
|
-
gap
|
|
914
|
-
viewport
|
|
1032
|
+
gap
|
|
915
1033
|
);
|
|
1034
|
+
const shifted = {
|
|
1035
|
+
...placed,
|
|
1036
|
+
x: placed.x + (Number(noteOptions.offset?.x) || 0),
|
|
1037
|
+
y: placed.y + (Number(noteOptions.offset?.y) || 0)
|
|
1038
|
+
};
|
|
916
1039
|
const guard = inflateRect(rect, gap * 0.7);
|
|
917
|
-
const outer =
|
|
918
|
-
{ ...placed.box, height: size.height + tiltPad },
|
|
919
|
-
guard,
|
|
920
|
-
viewport
|
|
921
|
-
);
|
|
1040
|
+
const outer = nudgeIntoBand(shifted, guard, band);
|
|
922
1041
|
const box = {
|
|
923
1042
|
x: outer.x,
|
|
924
1043
|
y: outer.y + tiltPad / 2,
|
|
925
1044
|
width: size.width,
|
|
926
|
-
height: note.
|
|
1045
|
+
height: note.height
|
|
927
1046
|
};
|
|
928
1047
|
placeNoteText(note, box, style, seed, {
|
|
929
1048
|
tilt,
|
|
930
1049
|
underline: noteOptions.underline !== false
|
|
931
1050
|
});
|
|
932
|
-
return { group: note.group, box: inflateRect(outer, 4), position
|
|
1051
|
+
return { group: note.group, box: inflateRect(outer, 4), position };
|
|
933
1052
|
}
|
|
934
1053
|
|
|
935
1054
|
// src/decorations.js
|
|
@@ -1347,14 +1466,11 @@ function ensureHandwrittenFont() {
|
|
|
1347
1466
|
|
|
1348
1467
|
// src/doodle.js
|
|
1349
1468
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1350
|
-
function
|
|
1469
|
+
function localBand(overlayRect, inset = 8) {
|
|
1351
1470
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1352
|
-
const height = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1353
1471
|
return {
|
|
1354
1472
|
x: -overlayRect.x + inset,
|
|
1355
|
-
|
|
1356
|
-
width: Math.max(0, width - inset * 2),
|
|
1357
|
-
height: Math.max(0, height - inset * 2)
|
|
1473
|
+
width: Math.max(0, width - inset * 2)
|
|
1358
1474
|
};
|
|
1359
1475
|
}
|
|
1360
1476
|
var DoodleOverlay = class {
|
|
@@ -1453,8 +1569,8 @@ var DoodleOverlay = class {
|
|
|
1453
1569
|
};
|
|
1454
1570
|
const noteGroups = [];
|
|
1455
1571
|
const noteLayout = {
|
|
1456
|
-
|
|
1457
|
-
gap: this.options.arrow ?
|
|
1572
|
+
band: localBand(overlayRect),
|
|
1573
|
+
gap: this.options.arrow ? 40 : 14
|
|
1458
1574
|
};
|
|
1459
1575
|
targets.forEach((target, index) => {
|
|
1460
1576
|
const absolute = absoluteRects[index];
|
|
@@ -1672,7 +1788,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1672
1788
|
ref: mergeRefs(innerRef, forwardedRef),
|
|
1673
1789
|
className,
|
|
1674
1790
|
// inline-flex shrink-wraps the child so the doodle hugs the component
|
|
1675
|
-
// rather than a full-width block. Flex specifically
|
|
1791
|
+
// rather than a full-width block. Flex specifically - inline-block would
|
|
1676
1792
|
// add baseline leading under an inline-level child like a <button>,
|
|
1677
1793
|
// leaving the frame taller than what it wraps. Override via `style`/`as`.
|
|
1678
1794
|
style: { display: "inline-flex", ...style }
|
package/dist/react.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface DoodleOwnProps extends DoodleOptionProps {
|
|
|
24
24
|
children?: ReactNode;
|
|
25
25
|
/** Wrapper element to render. Default `"span"`. */
|
|
26
26
|
as?: ElementType;
|
|
27
|
-
/** Skip drawing entirely
|
|
27
|
+
/** Skip drawing entirely - useful for reduced-motion or feature flags. */
|
|
28
28
|
disabled?: boolean;
|
|
29
29
|
/**
|
|
30
30
|
* Selector for decorating descendants instead of the wrapper as a whole.
|
|
@@ -52,7 +52,7 @@ export type DoodleProps<T extends ElementType = "span"> = DoodleOwnProps &
|
|
|
52
52
|
* decorated individually unless you opt in with `childSelector`.
|
|
53
53
|
*/
|
|
54
54
|
declare const Doodle: <T extends ElementType = "span">(
|
|
55
|
-
props: DoodleProps<T
|
|
55
|
+
props: DoodleProps<T>,
|
|
56
56
|
) => JSX.Element;
|
|
57
57
|
|
|
58
58
|
export default Doodle;
|
|
@@ -62,5 +62,5 @@ export { Doodle };
|
|
|
62
62
|
export function useDoodle(
|
|
63
63
|
ref: RefObject<Element | null>,
|
|
64
64
|
options?: DoodleOptions,
|
|
65
|
-
enabled?: boolean
|
|
65
|
+
enabled?: boolean,
|
|
66
66
|
): void;
|