@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.js
CHANGED
|
@@ -46,9 +46,16 @@ function mergeOptions(options = {}) {
|
|
|
46
46
|
if (merged.note && typeof merged.note === "string") {
|
|
47
47
|
merged.note = { text: merged.note, position: "top-right" };
|
|
48
48
|
}
|
|
49
|
-
if (merged.arrow
|
|
50
|
-
const
|
|
51
|
-
merged.arrow = {
|
|
49
|
+
if (merged.arrow) {
|
|
50
|
+
const config = merged.arrow === true ? {} : merged.arrow;
|
|
51
|
+
merged.arrow = {
|
|
52
|
+
// "note" launches from the annotation, a position name from that side of
|
|
53
|
+
// the element. Normalised for every arrow, not just `arrow: true`, so an
|
|
54
|
+
// explicit `from` is honoured whether or not there is a note.
|
|
55
|
+
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
56
|
+
to: config.to ?? "edge",
|
|
57
|
+
style: config.style ?? "curved"
|
|
58
|
+
};
|
|
52
59
|
}
|
|
53
60
|
if (merged.decorations === true) {
|
|
54
61
|
merged.decorations = { count: null, types: null };
|
|
@@ -193,10 +200,6 @@ function intersectionArea(a, b) {
|
|
|
193
200
|
const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
194
201
|
return w > 0 && h > 0 ? w * h : 0;
|
|
195
202
|
}
|
|
196
|
-
function outsideArea(box, bounds) {
|
|
197
|
-
if (!bounds) return 0;
|
|
198
|
-
return Math.max(0, box.width * box.height - intersectionArea(box, bounds));
|
|
199
|
-
}
|
|
200
203
|
function nearestPointOnRect(rect, point) {
|
|
201
204
|
return {
|
|
202
205
|
x: clamp(point.x, rect.x, rect.x + rect.width),
|
|
@@ -209,6 +212,17 @@ function distanceToRoundedRect(point, rect, radius = 0) {
|
|
|
209
212
|
const cy = clamp(point.y, rect.y + r, rect.y + rect.height - r);
|
|
210
213
|
return Math.hypot(point.x - cx, point.y - cy) - r;
|
|
211
214
|
}
|
|
215
|
+
function pointOnBoxToward(box, target) {
|
|
216
|
+
const cx = box.x + box.width / 2;
|
|
217
|
+
const cy = box.y + box.height / 2;
|
|
218
|
+
const dx = target.x - cx;
|
|
219
|
+
const dy = target.y - cy;
|
|
220
|
+
if (dx === 0 && dy === 0) return { x: cx, y: cy };
|
|
221
|
+
const hw = box.width / 2 || 1e-3;
|
|
222
|
+
const hh = box.height / 2 || 1e-3;
|
|
223
|
+
const scale = 1 / Math.max(Math.abs(dx) / hw, Math.abs(dy) / hh);
|
|
224
|
+
return { x: cx + dx * scale, y: cy + dy * scale };
|
|
225
|
+
}
|
|
212
226
|
function directionFromPosition(position) {
|
|
213
227
|
return {
|
|
214
228
|
x: position.includes("right") ? 1 : position.includes("left") ? -1 : 0,
|
|
@@ -458,21 +472,14 @@ function isBroken(fraction, breaks) {
|
|
|
458
472
|
function createBorderBreaks(perimeter, config, random) {
|
|
459
473
|
if (!config || perimeter <= 0) return [];
|
|
460
474
|
const settings = config === true ? {} : config;
|
|
461
|
-
const count = Math.max(
|
|
462
|
-
1,
|
|
463
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
464
|
-
);
|
|
475
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
465
476
|
const min = Math.max(2, settings.min ?? 6);
|
|
466
477
|
const max = Math.max(min, settings.max ?? 30);
|
|
467
478
|
const slice = perimeter / count;
|
|
468
479
|
let budget = perimeter * 0.35;
|
|
469
480
|
const breaks = [];
|
|
470
481
|
for (let i = 0; i < count; i++) {
|
|
471
|
-
const length = Math.min(
|
|
472
|
-
min + random() * (max - min),
|
|
473
|
-
slice * 0.7,
|
|
474
|
-
budget
|
|
475
|
-
);
|
|
482
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
476
483
|
if (length < 2) break;
|
|
477
484
|
budget -= length;
|
|
478
485
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -572,14 +579,86 @@ function drawBorder(svg, rect, options, seed) {
|
|
|
572
579
|
});
|
|
573
580
|
}
|
|
574
581
|
}
|
|
582
|
+
function cubicAt(p0, c1, c2, p3, t) {
|
|
583
|
+
const u = 1 - t;
|
|
584
|
+
const a = u * u * u;
|
|
585
|
+
const b = 3 * u * u * t;
|
|
586
|
+
const c = 3 * u * t * t;
|
|
587
|
+
const d = t * t * t;
|
|
588
|
+
return {
|
|
589
|
+
x: a * p0.x + b * c1.x + c * c2.x + d * p3.x,
|
|
590
|
+
y: a * p0.y + b * c1.y + c * c2.y + d * p3.y
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
594
|
+
const u = 1 - t;
|
|
595
|
+
const a = 3 * u * u;
|
|
596
|
+
const b = 6 * u * t;
|
|
597
|
+
const c = 3 * t * t;
|
|
598
|
+
return {
|
|
599
|
+
x: a * (c1.x - p0.x) + b * (c2.x - c1.x) + c * (p3.x - c2.x),
|
|
600
|
+
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
604
|
+
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
605
|
+
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
606
|
+
const knot = 0.46 + random() * 0.12;
|
|
607
|
+
const points = [];
|
|
608
|
+
const lead = Math.max(2, Math.round(knot * 16));
|
|
609
|
+
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
610
|
+
const pivot = at(knot);
|
|
611
|
+
const heading = tangentAt(knot);
|
|
612
|
+
const normal = { x: -heading.y, y: heading.x };
|
|
613
|
+
const side = random() < 0.5 ? 1 : -1;
|
|
614
|
+
const centre = {
|
|
615
|
+
x: pivot.x + normal.x * radius * side,
|
|
616
|
+
y: pivot.y + normal.y * radius * side
|
|
617
|
+
};
|
|
618
|
+
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
619
|
+
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
620
|
+
const steps = 18;
|
|
621
|
+
for (let i = 1; i <= steps; i++) {
|
|
622
|
+
const progress = i / steps;
|
|
623
|
+
const angle = from + turns * progress * side;
|
|
624
|
+
const drift = radius * 0.55 * progress;
|
|
625
|
+
points.push({
|
|
626
|
+
x: centre.x + Math.cos(angle) * radius + heading.x * drift,
|
|
627
|
+
y: centre.y + Math.sin(angle) * radius + heading.y * drift
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const tail = 14;
|
|
631
|
+
for (let i = 1; i <= tail; i++) {
|
|
632
|
+
points.push(at(knot + (1 - knot) * (i / tail)));
|
|
633
|
+
}
|
|
634
|
+
return points;
|
|
635
|
+
}
|
|
575
636
|
function normalize(vector) {
|
|
576
637
|
const length = Math.hypot(vector.x, vector.y);
|
|
577
638
|
if (!length) return { x: 0, y: 0 };
|
|
578
639
|
return { x: vector.x / length, y: vector.y / length };
|
|
579
640
|
}
|
|
580
|
-
|
|
641
|
+
var NOTE_CLEARANCE = 10;
|
|
642
|
+
var TIP_GAP = 11;
|
|
643
|
+
function arrowStartFromNote(origin, target, from) {
|
|
581
644
|
const cx = origin.x + origin.width / 2;
|
|
582
645
|
const cy = origin.y + origin.height / 2;
|
|
646
|
+
if (from && from !== "note") {
|
|
647
|
+
const point = pointFromPosition(from, origin);
|
|
648
|
+
const away = directionFromPosition(from);
|
|
649
|
+
if (!away.x && !away.y) {
|
|
650
|
+
const edge = pointOnBoxToward(origin, target);
|
|
651
|
+
const outward = normalize({ x: edge.x - cx, y: edge.y - cy });
|
|
652
|
+
return {
|
|
653
|
+
x: edge.x + outward.x * NOTE_CLEARANCE,
|
|
654
|
+
y: edge.y + outward.y * NOTE_CLEARANCE
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
x: point.x + away.x * NOTE_CLEARANCE,
|
|
659
|
+
y: point.y + away.y * NOTE_CLEARANCE
|
|
660
|
+
};
|
|
661
|
+
}
|
|
583
662
|
const dx = target.x - cx;
|
|
584
663
|
const dy = target.y - cy;
|
|
585
664
|
const hw = origin.width / 2 || 1e-3;
|
|
@@ -587,15 +666,16 @@ function arrowStartFromNote(origin, target) {
|
|
|
587
666
|
const sx = Math.sign(dx) || 1;
|
|
588
667
|
const sy = Math.sign(dy) || 1;
|
|
589
668
|
if (Math.abs(dy) / hh >= Math.abs(dx) / hw) {
|
|
590
|
-
return { x: cx - sx * hw
|
|
669
|
+
return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
|
|
591
670
|
}
|
|
592
|
-
return { x: cx + sx * (hw +
|
|
671
|
+
return { x: cx + sx * (hw + 8), y: cy + sy * (hh + 6) };
|
|
593
672
|
}
|
|
594
673
|
function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
595
674
|
const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
675
|
+
const from = arrowOptions.from || "note";
|
|
596
676
|
const anchored = Boolean(origin && origin.width > 0 && origin.height > 0);
|
|
597
|
-
let start = anchored ? arrowStartFromNote(origin, center) : annotationAnchor(
|
|
598
|
-
|
|
677
|
+
let start = anchored ? arrowStartFromNote(origin, center, from) : annotationAnchor(
|
|
678
|
+
from === "note" ? "top-right" : from,
|
|
599
679
|
rect,
|
|
600
680
|
38 + random() * 12
|
|
601
681
|
);
|
|
@@ -607,7 +687,7 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
607
687
|
const near = nearestPointOnRect(rect, start);
|
|
608
688
|
let outward = normalize({ x: start.x - near.x, y: start.y - near.y });
|
|
609
689
|
if (!outward.x && !outward.y) outward = { x: 0, y: -1 };
|
|
610
|
-
const gap =
|
|
690
|
+
const gap = TIP_GAP + random() * 5;
|
|
611
691
|
end = { x: near.x + outward.x * gap, y: near.y + outward.y * gap };
|
|
612
692
|
} else {
|
|
613
693
|
end = pointFromPosition(toPos, rect);
|
|
@@ -620,6 +700,35 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
620
700
|
}
|
|
621
701
|
return { start, end };
|
|
622
702
|
}
|
|
703
|
+
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
704
|
+
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
705
|
+
const spread = 0.46 + random() * 0.1;
|
|
706
|
+
for (const side of [-1, 1]) {
|
|
707
|
+
const armAngle = angle + spread * side;
|
|
708
|
+
const tail = {
|
|
709
|
+
x: tip.x - length * Math.cos(armAngle),
|
|
710
|
+
y: tip.y - length * Math.sin(armAngle)
|
|
711
|
+
};
|
|
712
|
+
appendPath(
|
|
713
|
+
svg,
|
|
714
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
715
|
+
steps: 2,
|
|
716
|
+
bow: 0.35 * side
|
|
717
|
+
}),
|
|
718
|
+
{ ...style, dashed: false }
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function boundsOf(points) {
|
|
723
|
+
const xs = points.map((p) => p.x);
|
|
724
|
+
const ys = points.map((p) => p.y);
|
|
725
|
+
return {
|
|
726
|
+
x: Math.min(...xs),
|
|
727
|
+
y: Math.min(...ys),
|
|
728
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
729
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
730
|
+
};
|
|
731
|
+
}
|
|
623
732
|
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
624
733
|
const random = createRandom(seed + 17);
|
|
625
734
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
@@ -641,13 +750,33 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
641
750
|
x: rect.x + rect.width / 2 - mid.x,
|
|
642
751
|
y: rect.y + rect.height / 2 - mid.y
|
|
643
752
|
};
|
|
644
|
-
const
|
|
753
|
+
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
754
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
|
|
645
755
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
646
756
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
647
757
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
648
758
|
const c2x = start.x + dx * 0.72 + perpendicular.x * bow * 0.85;
|
|
649
759
|
const c2y = start.y + dy * 0.72 + perpendicular.y * bow * 0.85;
|
|
650
760
|
hull.push({ x: c1x, y: c1y }, { x: c2x, y: c2y });
|
|
761
|
+
if (arrowOptions.style === "looped") {
|
|
762
|
+
const radius = clamp(distance * 0.11, 7, 17);
|
|
763
|
+
const points = loopedCurvePoints(
|
|
764
|
+
start,
|
|
765
|
+
{ x: c1x, y: c1y },
|
|
766
|
+
{ x: c2x, y: c2y },
|
|
767
|
+
end,
|
|
768
|
+
radius,
|
|
769
|
+
random
|
|
770
|
+
);
|
|
771
|
+
shaft = catmullRomPath(points, false);
|
|
772
|
+
hull.push(...points);
|
|
773
|
+
const last = points[points.length - 1];
|
|
774
|
+
const prior = points[points.length - 2] ?? start;
|
|
775
|
+
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
776
|
+
appendPath(svg, shaft, { ...style, dashed });
|
|
777
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
778
|
+
return { start, end, bounds: boundsOf(hull) };
|
|
779
|
+
}
|
|
651
780
|
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
652
781
|
if (style.roughness >= 1.1) {
|
|
653
782
|
const drift = () => (random() - 0.5) * 1.4;
|
|
@@ -662,31 +791,8 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
662
791
|
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
663
792
|
}
|
|
664
793
|
appendPath(svg, shaft, { ...style, dashed });
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
for (const side of [-1, 1]) {
|
|
668
|
-
const angle = tipAngle + spread * side;
|
|
669
|
-
const tail = {
|
|
670
|
-
x: end.x - headLength * Math.cos(angle),
|
|
671
|
-
y: end.y - headLength * Math.sin(angle)
|
|
672
|
-
};
|
|
673
|
-
appendPath(
|
|
674
|
-
svg,
|
|
675
|
-
roughLine(tail.x, tail.y, end.x, end.y, style.roughness * 0.5, random, {
|
|
676
|
-
steps: 2,
|
|
677
|
-
bow: 0.35 * side
|
|
678
|
-
}),
|
|
679
|
-
{ ...style, dashed: false }
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
const xs = hull.map((p) => p.x);
|
|
683
|
-
const ys = hull.map((p) => p.y);
|
|
684
|
-
const bounds = {
|
|
685
|
-
x: Math.min(...xs),
|
|
686
|
-
y: Math.min(...ys),
|
|
687
|
-
width: Math.max(...xs) - Math.min(...xs),
|
|
688
|
-
height: Math.max(...ys) - Math.min(...ys)
|
|
689
|
-
};
|
|
794
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
795
|
+
const bounds = boundsOf(hull);
|
|
690
796
|
return { start, end, bounds };
|
|
691
797
|
}
|
|
692
798
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
@@ -724,11 +830,38 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
724
830
|
}
|
|
725
831
|
);
|
|
726
832
|
}
|
|
727
|
-
|
|
833
|
+
var DEFAULT_WORDS_PER_LINE = 2;
|
|
834
|
+
function wrapWords(text, perLine) {
|
|
835
|
+
const lines = [];
|
|
836
|
+
for (const paragraph of String(text).split(/\r?\n/)) {
|
|
837
|
+
const words = paragraph.trim().split(/\s+/).filter(Boolean);
|
|
838
|
+
if (!words.length) continue;
|
|
839
|
+
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
840
|
+
for (let i = 0; i < words.length; i += size) {
|
|
841
|
+
lines.push(words.slice(i, i + size).join(" "));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return lines.length ? lines : [String(text)];
|
|
845
|
+
}
|
|
846
|
+
function measureWidth(element, content, fontSize) {
|
|
847
|
+
try {
|
|
848
|
+
const box = element.getBBox();
|
|
849
|
+
if (box && box.width > 0) return box.width;
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
const measured = element.getComputedTextLength();
|
|
854
|
+
if (Number.isFinite(measured) && measured > 0) return measured;
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
return content.length * fontSize * 0.45;
|
|
858
|
+
}
|
|
859
|
+
function createNoteText(svg, text, style, options = {}) {
|
|
728
860
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
861
|
+
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
862
|
+
const lineHeight = fontSize * 1.05;
|
|
729
863
|
const group = document.createElementNS(SVG_NS, "g");
|
|
730
864
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
731
|
-
textEl.textContent = text;
|
|
732
865
|
textEl.setAttribute("x", "0");
|
|
733
866
|
textEl.setAttribute("y", "0");
|
|
734
867
|
textEl.setAttribute("fill", style.color);
|
|
@@ -739,31 +872,43 @@ function createNoteText(svg, text, style) {
|
|
|
739
872
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
740
873
|
textEl.setAttribute("text-anchor", "start");
|
|
741
874
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
875
|
+
const tspans = lines.map((line, index) => {
|
|
876
|
+
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
877
|
+
tspan.textContent = line;
|
|
878
|
+
tspan.setAttribute("x", "0");
|
|
879
|
+
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
880
|
+
textEl.appendChild(tspan);
|
|
881
|
+
return tspan;
|
|
882
|
+
});
|
|
742
883
|
group.appendChild(textEl);
|
|
743
884
|
svg.appendChild(group);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
} catch {
|
|
749
|
-
}
|
|
750
|
-
if (!width) {
|
|
751
|
-
try {
|
|
752
|
-
const measured = textEl.getComputedTextLength();
|
|
753
|
-
if (Number.isFinite(measured) && measured > 0) width = measured;
|
|
754
|
-
} catch {
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
if (!width) width = text.length * fontSize * 0.45;
|
|
885
|
+
const lineWidths = tspans.map(
|
|
886
|
+
(tspan, index) => measureWidth(tspan, lines[index], fontSize)
|
|
887
|
+
);
|
|
888
|
+
const width = Math.max(...lineWidths);
|
|
758
889
|
const ascent = fontSize * 0.74;
|
|
759
890
|
const descent = fontSize * 0.26;
|
|
760
|
-
|
|
891
|
+
const height = (lines.length - 1) * lineHeight + ascent + descent;
|
|
892
|
+
return {
|
|
893
|
+
group,
|
|
894
|
+
textEl,
|
|
895
|
+
tspans,
|
|
896
|
+
lines,
|
|
897
|
+
lineWidths,
|
|
898
|
+
lineHeight,
|
|
899
|
+
width,
|
|
900
|
+
height,
|
|
901
|
+
ascent,
|
|
902
|
+
descent,
|
|
903
|
+
fontSize
|
|
904
|
+
};
|
|
761
905
|
}
|
|
762
906
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
763
907
|
const random = createRandom(seed + 55);
|
|
764
908
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
765
909
|
note.textEl.setAttribute("x", fmt(box.x));
|
|
766
910
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
911
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
|
|
767
912
|
const cx = box.x + box.width / 2;
|
|
768
913
|
const cy = box.y + box.height / 2;
|
|
769
914
|
note.group.setAttribute(
|
|
@@ -771,11 +916,12 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
771
916
|
`rotate(${tilt.toFixed(2)} ${fmt(cx)} ${fmt(cy)})`
|
|
772
917
|
);
|
|
773
918
|
if (options.underline !== false) {
|
|
919
|
+
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
774
920
|
drawUnderline(
|
|
775
921
|
note.group,
|
|
776
922
|
box.x + 1,
|
|
777
923
|
box.y + box.height + 2,
|
|
778
|
-
|
|
924
|
+
lastWidth - 2,
|
|
779
925
|
{
|
|
780
926
|
color: style.color,
|
|
781
927
|
strokeWidth: style.strokeWidth || 1.4,
|
|
@@ -807,25 +953,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
807
953
|
|
|
808
954
|
// src/annotations.js
|
|
809
955
|
var NOTE_GAP = 14;
|
|
810
|
-
function mirror(position, axis) {
|
|
811
|
-
if (axis === "x") {
|
|
812
|
-
if (position.includes("right")) return position.replace("right", "left");
|
|
813
|
-
if (position.includes("left")) return position.replace("left", "right");
|
|
814
|
-
return position;
|
|
815
|
-
}
|
|
816
|
-
if (position.includes("top")) return position.replace("top", "bottom");
|
|
817
|
-
if (position.includes("bottom")) return position.replace("bottom", "top");
|
|
818
|
-
return position;
|
|
819
|
-
}
|
|
820
|
-
function candidatePositions(position) {
|
|
821
|
-
const candidates = [
|
|
822
|
-
position,
|
|
823
|
-
mirror(position, "x"),
|
|
824
|
-
mirror(position, "y"),
|
|
825
|
-
mirror(mirror(position, "x"), "y")
|
|
826
|
-
];
|
|
827
|
-
return candidates.filter((value, index) => candidates.indexOf(value) === index);
|
|
828
|
-
}
|
|
829
956
|
function boxForPosition(rect, position, size, gap) {
|
|
830
957
|
const direction = directionFromPosition(position);
|
|
831
958
|
const guard = inflateRect(rect, gap);
|
|
@@ -844,75 +971,67 @@ function boxForPosition(rect, position, size, gap) {
|
|
|
844
971
|
}
|
|
845
972
|
return { x, y, width: size.width, height: size.height };
|
|
846
973
|
}
|
|
847
|
-
function
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
if (!best || penalty < best.penalty) {
|
|
854
|
-
best = { box, penalty, position: candidate };
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
return best;
|
|
858
|
-
}
|
|
859
|
-
function clampToViewport(box, guard, viewport) {
|
|
860
|
-
if (!viewport || viewport.width <= 0 || viewport.height <= 0) return box;
|
|
861
|
-
const shifted = {
|
|
862
|
-
...box,
|
|
863
|
-
x: clamp(
|
|
864
|
-
box.x,
|
|
865
|
-
viewport.x,
|
|
866
|
-
Math.max(viewport.x, viewport.x + viewport.width - box.width)
|
|
867
|
-
),
|
|
868
|
-
y: clamp(
|
|
869
|
-
box.y,
|
|
870
|
-
viewport.y,
|
|
871
|
-
Math.max(viewport.y, viewport.y + viewport.height - box.height)
|
|
872
|
-
)
|
|
873
|
-
};
|
|
874
|
-
if (intersectionArea(shifted, guard) > intersectionArea(box, guard)) {
|
|
875
|
-
return box;
|
|
974
|
+
function limitAgainstElement(box, guard, dx) {
|
|
975
|
+
const level = box.y < guard.y + guard.height && box.y + box.height > guard.y;
|
|
976
|
+
if (!level) return dx;
|
|
977
|
+
if (dx < 0) {
|
|
978
|
+
const room2 = box.x - (guard.x + guard.width);
|
|
979
|
+
return room2 <= 0 ? 0 : -Math.min(-dx, room2);
|
|
876
980
|
}
|
|
877
|
-
|
|
981
|
+
const room = guard.x - (box.x + box.width);
|
|
982
|
+
return room <= 0 ? 0 : Math.min(dx, room);
|
|
983
|
+
}
|
|
984
|
+
function nudgeIntoBand(box, guard, band) {
|
|
985
|
+
if (!band || band.width <= 0) return box;
|
|
986
|
+
const overhangLeft = band.x - box.x;
|
|
987
|
+
const overhangRight = box.x + box.width - (band.x + band.width);
|
|
988
|
+
let dx = 0;
|
|
989
|
+
if (overhangLeft > 0) dx = overhangLeft;
|
|
990
|
+
else if (overhangRight > 0) dx = -overhangRight;
|
|
991
|
+
if (!dx) return box;
|
|
992
|
+
dx = limitAgainstElement(box, guard, dx);
|
|
993
|
+
if (!dx) return box;
|
|
994
|
+
return { ...box, x: box.x + dx };
|
|
878
995
|
}
|
|
879
996
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
880
997
|
if (!noteOptions?.text) return null;
|
|
881
|
-
const
|
|
998
|
+
const band = layout.band ?? null;
|
|
882
999
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
883
1000
|
const random = createRandom(seed + 9);
|
|
884
|
-
const note = createNoteText(svg, noteOptions.text, style
|
|
1001
|
+
const note = createNoteText(svg, noteOptions.text, style, {
|
|
1002
|
+
wordsPerLine: noteOptions.wordsPerLine
|
|
1003
|
+
});
|
|
885
1004
|
const tilt = (random() - 0.45) * 7;
|
|
886
1005
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
887
1006
|
const size = {
|
|
888
1007
|
width: note.width,
|
|
889
|
-
height: note.
|
|
1008
|
+
height: note.height + (noteOptions.underline === false ? 0 : 5)
|
|
890
1009
|
};
|
|
891
1010
|
const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
|
|
892
|
-
const placed =
|
|
1011
|
+
const placed = boxForPosition(
|
|
893
1012
|
rect,
|
|
894
1013
|
position,
|
|
895
1014
|
{ width: size.width, height: size.height + tiltPad },
|
|
896
|
-
gap
|
|
897
|
-
viewport
|
|
1015
|
+
gap
|
|
898
1016
|
);
|
|
1017
|
+
const shifted = {
|
|
1018
|
+
...placed,
|
|
1019
|
+
x: placed.x + (Number(noteOptions.offset?.x) || 0),
|
|
1020
|
+
y: placed.y + (Number(noteOptions.offset?.y) || 0)
|
|
1021
|
+
};
|
|
899
1022
|
const guard = inflateRect(rect, gap * 0.7);
|
|
900
|
-
const outer =
|
|
901
|
-
{ ...placed.box, height: size.height + tiltPad },
|
|
902
|
-
guard,
|
|
903
|
-
viewport
|
|
904
|
-
);
|
|
1023
|
+
const outer = nudgeIntoBand(shifted, guard, band);
|
|
905
1024
|
const box = {
|
|
906
1025
|
x: outer.x,
|
|
907
1026
|
y: outer.y + tiltPad / 2,
|
|
908
1027
|
width: size.width,
|
|
909
|
-
height: note.
|
|
1028
|
+
height: note.height
|
|
910
1029
|
};
|
|
911
1030
|
placeNoteText(note, box, style, seed, {
|
|
912
1031
|
tilt,
|
|
913
1032
|
underline: noteOptions.underline !== false
|
|
914
1033
|
});
|
|
915
|
-
return { group: note.group, box: inflateRect(outer, 4), position
|
|
1034
|
+
return { group: note.group, box: inflateRect(outer, 4), position };
|
|
916
1035
|
}
|
|
917
1036
|
|
|
918
1037
|
// src/decorations.js
|
|
@@ -1330,14 +1449,11 @@ function ensureHandwrittenFont() {
|
|
|
1330
1449
|
|
|
1331
1450
|
// src/doodle.js
|
|
1332
1451
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1333
|
-
function
|
|
1452
|
+
function localBand(overlayRect, inset = 8) {
|
|
1334
1453
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1335
|
-
const height = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1336
1454
|
return {
|
|
1337
1455
|
x: -overlayRect.x + inset,
|
|
1338
|
-
|
|
1339
|
-
width: Math.max(0, width - inset * 2),
|
|
1340
|
-
height: Math.max(0, height - inset * 2)
|
|
1456
|
+
width: Math.max(0, width - inset * 2)
|
|
1341
1457
|
};
|
|
1342
1458
|
}
|
|
1343
1459
|
var DoodleOverlay = class {
|
|
@@ -1436,8 +1552,8 @@ var DoodleOverlay = class {
|
|
|
1436
1552
|
};
|
|
1437
1553
|
const noteGroups = [];
|
|
1438
1554
|
const noteLayout = {
|
|
1439
|
-
|
|
1440
|
-
gap: this.options.arrow ?
|
|
1555
|
+
band: localBand(overlayRect),
|
|
1556
|
+
gap: this.options.arrow ? 40 : 14
|
|
1441
1557
|
};
|
|
1442
1558
|
targets.forEach((target, index) => {
|
|
1443
1559
|
const absolute = absoluteRects[index];
|
|
@@ -1655,7 +1771,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1655
1771
|
ref: mergeRefs(innerRef, forwardedRef),
|
|
1656
1772
|
className,
|
|
1657
1773
|
// inline-flex shrink-wraps the child so the doodle hugs the component
|
|
1658
|
-
// rather than a full-width block. Flex specifically
|
|
1774
|
+
// rather than a full-width block. Flex specifically - inline-block would
|
|
1659
1775
|
// add baseline leading under an inline-level child like a <button>,
|
|
1660
1776
|
// leaving the frame taller than what it wraps. Override via `style`/`as`.
|
|
1661
1777
|
style: { display: "inline-flex", ...style }
|
package/package.json
CHANGED