@aznabee/freehand-ui 0.1.2 → 0.1.3
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 +53 -11
- package/dist/freehand-ui.cjs +249 -126
- package/dist/freehand-ui.js +249 -126
- package/dist/index.d.ts +19 -3
- package/dist/react.cjs +249 -126
- package/dist/react.js +249 -126
- 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,
|
|
@@ -589,14 +603,86 @@ function drawBorder(svg, rect, options, seed) {
|
|
|
589
603
|
});
|
|
590
604
|
}
|
|
591
605
|
}
|
|
606
|
+
function cubicAt(p0, c1, c2, p3, t) {
|
|
607
|
+
const u = 1 - t;
|
|
608
|
+
const a = u * u * u;
|
|
609
|
+
const b = 3 * u * u * t;
|
|
610
|
+
const c = 3 * u * t * t;
|
|
611
|
+
const d = t * t * t;
|
|
612
|
+
return {
|
|
613
|
+
x: a * p0.x + b * c1.x + c * c2.x + d * p3.x,
|
|
614
|
+
y: a * p0.y + b * c1.y + c * c2.y + d * p3.y
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
618
|
+
const u = 1 - t;
|
|
619
|
+
const a = 3 * u * u;
|
|
620
|
+
const b = 6 * u * t;
|
|
621
|
+
const c = 3 * t * t;
|
|
622
|
+
return {
|
|
623
|
+
x: a * (c1.x - p0.x) + b * (c2.x - c1.x) + c * (p3.x - c2.x),
|
|
624
|
+
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
628
|
+
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
629
|
+
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
630
|
+
const knot = 0.46 + random() * 0.12;
|
|
631
|
+
const points = [];
|
|
632
|
+
const lead = Math.max(2, Math.round(knot * 16));
|
|
633
|
+
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
634
|
+
const pivot = at(knot);
|
|
635
|
+
const heading = tangentAt(knot);
|
|
636
|
+
const normal = { x: -heading.y, y: heading.x };
|
|
637
|
+
const side = random() < 0.5 ? 1 : -1;
|
|
638
|
+
const centre = {
|
|
639
|
+
x: pivot.x + normal.x * radius * side,
|
|
640
|
+
y: pivot.y + normal.y * radius * side
|
|
641
|
+
};
|
|
642
|
+
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
643
|
+
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
644
|
+
const steps = 18;
|
|
645
|
+
for (let i = 1; i <= steps; i++) {
|
|
646
|
+
const progress = i / steps;
|
|
647
|
+
const angle = from + turns * progress * side;
|
|
648
|
+
const drift = radius * 0.55 * progress;
|
|
649
|
+
points.push({
|
|
650
|
+
x: centre.x + Math.cos(angle) * radius + heading.x * drift,
|
|
651
|
+
y: centre.y + Math.sin(angle) * radius + heading.y * drift
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const tail = 14;
|
|
655
|
+
for (let i = 1; i <= tail; i++) {
|
|
656
|
+
points.push(at(knot + (1 - knot) * (i / tail)));
|
|
657
|
+
}
|
|
658
|
+
return points;
|
|
659
|
+
}
|
|
592
660
|
function normalize(vector) {
|
|
593
661
|
const length = Math.hypot(vector.x, vector.y);
|
|
594
662
|
if (!length) return { x: 0, y: 0 };
|
|
595
663
|
return { x: vector.x / length, y: vector.y / length };
|
|
596
664
|
}
|
|
597
|
-
|
|
665
|
+
var NOTE_CLEARANCE = 10;
|
|
666
|
+
var TIP_GAP = 11;
|
|
667
|
+
function arrowStartFromNote(origin, target, from) {
|
|
598
668
|
const cx = origin.x + origin.width / 2;
|
|
599
669
|
const cy = origin.y + origin.height / 2;
|
|
670
|
+
if (from && from !== "note") {
|
|
671
|
+
const point = pointFromPosition(from, origin);
|
|
672
|
+
const away = directionFromPosition(from);
|
|
673
|
+
if (!away.x && !away.y) {
|
|
674
|
+
const edge = pointOnBoxToward(origin, target);
|
|
675
|
+
const outward = normalize({ x: edge.x - cx, y: edge.y - cy });
|
|
676
|
+
return {
|
|
677
|
+
x: edge.x + outward.x * NOTE_CLEARANCE,
|
|
678
|
+
y: edge.y + outward.y * NOTE_CLEARANCE
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
x: point.x + away.x * NOTE_CLEARANCE,
|
|
683
|
+
y: point.y + away.y * NOTE_CLEARANCE
|
|
684
|
+
};
|
|
685
|
+
}
|
|
600
686
|
const dx = target.x - cx;
|
|
601
687
|
const dy = target.y - cy;
|
|
602
688
|
const hw = origin.width / 2 || 1e-3;
|
|
@@ -604,15 +690,16 @@ function arrowStartFromNote(origin, target) {
|
|
|
604
690
|
const sx = Math.sign(dx) || 1;
|
|
605
691
|
const sy = Math.sign(dy) || 1;
|
|
606
692
|
if (Math.abs(dy) / hh >= Math.abs(dx) / hw) {
|
|
607
|
-
return { x: cx - sx * hw
|
|
693
|
+
return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
|
|
608
694
|
}
|
|
609
|
-
return { x: cx + sx * (hw +
|
|
695
|
+
return { x: cx + sx * (hw + 8), y: cy + sy * (hh + 6) };
|
|
610
696
|
}
|
|
611
697
|
function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
612
698
|
const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
699
|
+
const from = arrowOptions.from || "note";
|
|
613
700
|
const anchored = Boolean(origin && origin.width > 0 && origin.height > 0);
|
|
614
|
-
let start = anchored ? arrowStartFromNote(origin, center) : annotationAnchor(
|
|
615
|
-
|
|
701
|
+
let start = anchored ? arrowStartFromNote(origin, center, from) : annotationAnchor(
|
|
702
|
+
from === "note" ? "top-right" : from,
|
|
616
703
|
rect,
|
|
617
704
|
38 + random() * 12
|
|
618
705
|
);
|
|
@@ -624,7 +711,7 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
624
711
|
const near = nearestPointOnRect(rect, start);
|
|
625
712
|
let outward = normalize({ x: start.x - near.x, y: start.y - near.y });
|
|
626
713
|
if (!outward.x && !outward.y) outward = { x: 0, y: -1 };
|
|
627
|
-
const gap =
|
|
714
|
+
const gap = TIP_GAP + random() * 5;
|
|
628
715
|
end = { x: near.x + outward.x * gap, y: near.y + outward.y * gap };
|
|
629
716
|
} else {
|
|
630
717
|
end = pointFromPosition(toPos, rect);
|
|
@@ -637,6 +724,35 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
637
724
|
}
|
|
638
725
|
return { start, end };
|
|
639
726
|
}
|
|
727
|
+
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
728
|
+
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
729
|
+
const spread = 0.46 + random() * 0.1;
|
|
730
|
+
for (const side of [-1, 1]) {
|
|
731
|
+
const armAngle = angle + spread * side;
|
|
732
|
+
const tail = {
|
|
733
|
+
x: tip.x - length * Math.cos(armAngle),
|
|
734
|
+
y: tip.y - length * Math.sin(armAngle)
|
|
735
|
+
};
|
|
736
|
+
appendPath(
|
|
737
|
+
svg,
|
|
738
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
739
|
+
steps: 2,
|
|
740
|
+
bow: 0.35 * side
|
|
741
|
+
}),
|
|
742
|
+
{ ...style, dashed: false }
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function boundsOf(points) {
|
|
747
|
+
const xs = points.map((p) => p.x);
|
|
748
|
+
const ys = points.map((p) => p.y);
|
|
749
|
+
return {
|
|
750
|
+
x: Math.min(...xs),
|
|
751
|
+
y: Math.min(...ys),
|
|
752
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
753
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
640
756
|
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
641
757
|
const random = createRandom(seed + 17);
|
|
642
758
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
@@ -658,13 +774,33 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
658
774
|
x: rect.x + rect.width / 2 - mid.x,
|
|
659
775
|
y: rect.y + rect.height / 2 - mid.y
|
|
660
776
|
};
|
|
661
|
-
const
|
|
777
|
+
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
778
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
|
|
662
779
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
663
780
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
664
781
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
665
782
|
const c2x = start.x + dx * 0.72 + perpendicular.x * bow * 0.85;
|
|
666
783
|
const c2y = start.y + dy * 0.72 + perpendicular.y * bow * 0.85;
|
|
667
784
|
hull.push({ x: c1x, y: c1y }, { x: c2x, y: c2y });
|
|
785
|
+
if (arrowOptions.style === "looped") {
|
|
786
|
+
const radius = clamp(distance * 0.11, 7, 17);
|
|
787
|
+
const points = loopedCurvePoints(
|
|
788
|
+
start,
|
|
789
|
+
{ x: c1x, y: c1y },
|
|
790
|
+
{ x: c2x, y: c2y },
|
|
791
|
+
end,
|
|
792
|
+
radius,
|
|
793
|
+
random
|
|
794
|
+
);
|
|
795
|
+
shaft = catmullRomPath(points, false);
|
|
796
|
+
hull.push(...points);
|
|
797
|
+
const last = points[points.length - 1];
|
|
798
|
+
const prior = points[points.length - 2] ?? start;
|
|
799
|
+
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
800
|
+
appendPath(svg, shaft, { ...style, dashed });
|
|
801
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
802
|
+
return { start, end, bounds: boundsOf(hull) };
|
|
803
|
+
}
|
|
668
804
|
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
669
805
|
if (style.roughness >= 1.1) {
|
|
670
806
|
const drift = () => (random() - 0.5) * 1.4;
|
|
@@ -679,31 +815,8 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
679
815
|
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
680
816
|
}
|
|
681
817
|
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
|
-
};
|
|
818
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
819
|
+
const bounds = boundsOf(hull);
|
|
707
820
|
return { start, end, bounds };
|
|
708
821
|
}
|
|
709
822
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
@@ -741,11 +854,38 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
741
854
|
}
|
|
742
855
|
);
|
|
743
856
|
}
|
|
744
|
-
|
|
857
|
+
var DEFAULT_WORDS_PER_LINE = 2;
|
|
858
|
+
function wrapWords(text, perLine) {
|
|
859
|
+
const lines = [];
|
|
860
|
+
for (const paragraph of String(text).split(/\r?\n/)) {
|
|
861
|
+
const words = paragraph.trim().split(/\s+/).filter(Boolean);
|
|
862
|
+
if (!words.length) continue;
|
|
863
|
+
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
864
|
+
for (let i = 0; i < words.length; i += size) {
|
|
865
|
+
lines.push(words.slice(i, i + size).join(" "));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return lines.length ? lines : [String(text)];
|
|
869
|
+
}
|
|
870
|
+
function measureWidth(element, content, fontSize) {
|
|
871
|
+
try {
|
|
872
|
+
const box = element.getBBox();
|
|
873
|
+
if (box && box.width > 0) return box.width;
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
const measured = element.getComputedTextLength();
|
|
878
|
+
if (Number.isFinite(measured) && measured > 0) return measured;
|
|
879
|
+
} catch {
|
|
880
|
+
}
|
|
881
|
+
return content.length * fontSize * 0.45;
|
|
882
|
+
}
|
|
883
|
+
function createNoteText(svg, text, style, options = {}) {
|
|
745
884
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
885
|
+
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
886
|
+
const lineHeight = fontSize * 1.05;
|
|
746
887
|
const group = document.createElementNS(SVG_NS, "g");
|
|
747
888
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
748
|
-
textEl.textContent = text;
|
|
749
889
|
textEl.setAttribute("x", "0");
|
|
750
890
|
textEl.setAttribute("y", "0");
|
|
751
891
|
textEl.setAttribute("fill", style.color);
|
|
@@ -756,31 +896,43 @@ function createNoteText(svg, text, style) {
|
|
|
756
896
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
757
897
|
textEl.setAttribute("text-anchor", "start");
|
|
758
898
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
899
|
+
const tspans = lines.map((line, index) => {
|
|
900
|
+
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
901
|
+
tspan.textContent = line;
|
|
902
|
+
tspan.setAttribute("x", "0");
|
|
903
|
+
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
904
|
+
textEl.appendChild(tspan);
|
|
905
|
+
return tspan;
|
|
906
|
+
});
|
|
759
907
|
group.appendChild(textEl);
|
|
760
908
|
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;
|
|
909
|
+
const lineWidths = tspans.map(
|
|
910
|
+
(tspan, index) => measureWidth(tspan, lines[index], fontSize)
|
|
911
|
+
);
|
|
912
|
+
const width = Math.max(...lineWidths);
|
|
775
913
|
const ascent = fontSize * 0.74;
|
|
776
914
|
const descent = fontSize * 0.26;
|
|
777
|
-
|
|
915
|
+
const height = (lines.length - 1) * lineHeight + ascent + descent;
|
|
916
|
+
return {
|
|
917
|
+
group,
|
|
918
|
+
textEl,
|
|
919
|
+
tspans,
|
|
920
|
+
lines,
|
|
921
|
+
lineWidths,
|
|
922
|
+
lineHeight,
|
|
923
|
+
width,
|
|
924
|
+
height,
|
|
925
|
+
ascent,
|
|
926
|
+
descent,
|
|
927
|
+
fontSize
|
|
928
|
+
};
|
|
778
929
|
}
|
|
779
930
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
780
931
|
const random = createRandom(seed + 55);
|
|
781
932
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
782
933
|
note.textEl.setAttribute("x", fmt(box.x));
|
|
783
934
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
935
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
|
|
784
936
|
const cx = box.x + box.width / 2;
|
|
785
937
|
const cy = box.y + box.height / 2;
|
|
786
938
|
note.group.setAttribute(
|
|
@@ -788,11 +940,12 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
788
940
|
`rotate(${tilt.toFixed(2)} ${fmt(cx)} ${fmt(cy)})`
|
|
789
941
|
);
|
|
790
942
|
if (options.underline !== false) {
|
|
943
|
+
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
791
944
|
drawUnderline(
|
|
792
945
|
note.group,
|
|
793
946
|
box.x + 1,
|
|
794
947
|
box.y + box.height + 2,
|
|
795
|
-
|
|
948
|
+
lastWidth - 2,
|
|
796
949
|
{
|
|
797
950
|
color: style.color,
|
|
798
951
|
strokeWidth: style.strokeWidth || 1.4,
|
|
@@ -824,25 +977,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
824
977
|
|
|
825
978
|
// src/annotations.js
|
|
826
979
|
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
980
|
function boxForPosition(rect, position, size, gap) {
|
|
847
981
|
const direction = directionFromPosition(position);
|
|
848
982
|
const guard = inflateRect(rect, gap);
|
|
@@ -861,75 +995,67 @@ function boxForPosition(rect, position, size, gap) {
|
|
|
861
995
|
}
|
|
862
996
|
return { x, y, width: size.width, height: size.height };
|
|
863
997
|
}
|
|
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;
|
|
998
|
+
function limitAgainstElement(box, guard, dx) {
|
|
999
|
+
const level = box.y < guard.y + guard.height && box.y + box.height > guard.y;
|
|
1000
|
+
if (!level) return dx;
|
|
1001
|
+
if (dx < 0) {
|
|
1002
|
+
const room2 = box.x - (guard.x + guard.width);
|
|
1003
|
+
return room2 <= 0 ? 0 : -Math.min(-dx, room2);
|
|
893
1004
|
}
|
|
894
|
-
|
|
1005
|
+
const room = guard.x - (box.x + box.width);
|
|
1006
|
+
return room <= 0 ? 0 : Math.min(dx, room);
|
|
1007
|
+
}
|
|
1008
|
+
function nudgeIntoBand(box, guard, band) {
|
|
1009
|
+
if (!band || band.width <= 0) return box;
|
|
1010
|
+
const overhangLeft = band.x - box.x;
|
|
1011
|
+
const overhangRight = box.x + box.width - (band.x + band.width);
|
|
1012
|
+
let dx = 0;
|
|
1013
|
+
if (overhangLeft > 0) dx = overhangLeft;
|
|
1014
|
+
else if (overhangRight > 0) dx = -overhangRight;
|
|
1015
|
+
if (!dx) return box;
|
|
1016
|
+
dx = limitAgainstElement(box, guard, dx);
|
|
1017
|
+
if (!dx) return box;
|
|
1018
|
+
return { ...box, x: box.x + dx };
|
|
895
1019
|
}
|
|
896
1020
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
897
1021
|
if (!noteOptions?.text) return null;
|
|
898
|
-
const
|
|
1022
|
+
const band = layout.band ?? null;
|
|
899
1023
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
900
1024
|
const random = createRandom(seed + 9);
|
|
901
|
-
const note = createNoteText(svg, noteOptions.text, style
|
|
1025
|
+
const note = createNoteText(svg, noteOptions.text, style, {
|
|
1026
|
+
wordsPerLine: noteOptions.wordsPerLine
|
|
1027
|
+
});
|
|
902
1028
|
const tilt = (random() - 0.45) * 7;
|
|
903
1029
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
904
1030
|
const size = {
|
|
905
1031
|
width: note.width,
|
|
906
|
-
height: note.
|
|
1032
|
+
height: note.height + (noteOptions.underline === false ? 0 : 5)
|
|
907
1033
|
};
|
|
908
1034
|
const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
|
|
909
|
-
const placed =
|
|
1035
|
+
const placed = boxForPosition(
|
|
910
1036
|
rect,
|
|
911
1037
|
position,
|
|
912
1038
|
{ width: size.width, height: size.height + tiltPad },
|
|
913
|
-
gap
|
|
914
|
-
viewport
|
|
1039
|
+
gap
|
|
915
1040
|
);
|
|
1041
|
+
const shifted = {
|
|
1042
|
+
...placed,
|
|
1043
|
+
x: placed.x + (Number(noteOptions.offset?.x) || 0),
|
|
1044
|
+
y: placed.y + (Number(noteOptions.offset?.y) || 0)
|
|
1045
|
+
};
|
|
916
1046
|
const guard = inflateRect(rect, gap * 0.7);
|
|
917
|
-
const outer =
|
|
918
|
-
{ ...placed.box, height: size.height + tiltPad },
|
|
919
|
-
guard,
|
|
920
|
-
viewport
|
|
921
|
-
);
|
|
1047
|
+
const outer = nudgeIntoBand(shifted, guard, band);
|
|
922
1048
|
const box = {
|
|
923
1049
|
x: outer.x,
|
|
924
1050
|
y: outer.y + tiltPad / 2,
|
|
925
1051
|
width: size.width,
|
|
926
|
-
height: note.
|
|
1052
|
+
height: note.height
|
|
927
1053
|
};
|
|
928
1054
|
placeNoteText(note, box, style, seed, {
|
|
929
1055
|
tilt,
|
|
930
1056
|
underline: noteOptions.underline !== false
|
|
931
1057
|
});
|
|
932
|
-
return { group: note.group, box: inflateRect(outer, 4), position
|
|
1058
|
+
return { group: note.group, box: inflateRect(outer, 4), position };
|
|
933
1059
|
}
|
|
934
1060
|
|
|
935
1061
|
// src/decorations.js
|
|
@@ -1347,14 +1473,11 @@ function ensureHandwrittenFont() {
|
|
|
1347
1473
|
|
|
1348
1474
|
// src/doodle.js
|
|
1349
1475
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1350
|
-
function
|
|
1476
|
+
function localBand(overlayRect, inset = 8) {
|
|
1351
1477
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1352
|
-
const height = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1353
1478
|
return {
|
|
1354
1479
|
x: -overlayRect.x + inset,
|
|
1355
|
-
|
|
1356
|
-
width: Math.max(0, width - inset * 2),
|
|
1357
|
-
height: Math.max(0, height - inset * 2)
|
|
1480
|
+
width: Math.max(0, width - inset * 2)
|
|
1358
1481
|
};
|
|
1359
1482
|
}
|
|
1360
1483
|
var DoodleOverlay = class {
|
|
@@ -1453,8 +1576,8 @@ var DoodleOverlay = class {
|
|
|
1453
1576
|
};
|
|
1454
1577
|
const noteGroups = [];
|
|
1455
1578
|
const noteLayout = {
|
|
1456
|
-
|
|
1457
|
-
gap: this.options.arrow ?
|
|
1579
|
+
band: localBand(overlayRect),
|
|
1580
|
+
gap: this.options.arrow ? 40 : 14
|
|
1458
1581
|
};
|
|
1459
1582
|
targets.forEach((target, index) => {
|
|
1460
1583
|
const absolute = absoluteRects[index];
|