@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.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,
|
|
@@ -572,14 +586,86 @@ function drawBorder(svg, rect, options, seed) {
|
|
|
572
586
|
});
|
|
573
587
|
}
|
|
574
588
|
}
|
|
589
|
+
function cubicAt(p0, c1, c2, p3, t) {
|
|
590
|
+
const u = 1 - t;
|
|
591
|
+
const a = u * u * u;
|
|
592
|
+
const b = 3 * u * u * t;
|
|
593
|
+
const c = 3 * u * t * t;
|
|
594
|
+
const d = t * t * t;
|
|
595
|
+
return {
|
|
596
|
+
x: a * p0.x + b * c1.x + c * c2.x + d * p3.x,
|
|
597
|
+
y: a * p0.y + b * c1.y + c * c2.y + d * p3.y
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
601
|
+
const u = 1 - t;
|
|
602
|
+
const a = 3 * u * u;
|
|
603
|
+
const b = 6 * u * t;
|
|
604
|
+
const c = 3 * t * t;
|
|
605
|
+
return {
|
|
606
|
+
x: a * (c1.x - p0.x) + b * (c2.x - c1.x) + c * (p3.x - c2.x),
|
|
607
|
+
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
611
|
+
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
612
|
+
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
613
|
+
const knot = 0.46 + random() * 0.12;
|
|
614
|
+
const points = [];
|
|
615
|
+
const lead = Math.max(2, Math.round(knot * 16));
|
|
616
|
+
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
617
|
+
const pivot = at(knot);
|
|
618
|
+
const heading = tangentAt(knot);
|
|
619
|
+
const normal = { x: -heading.y, y: heading.x };
|
|
620
|
+
const side = random() < 0.5 ? 1 : -1;
|
|
621
|
+
const centre = {
|
|
622
|
+
x: pivot.x + normal.x * radius * side,
|
|
623
|
+
y: pivot.y + normal.y * radius * side
|
|
624
|
+
};
|
|
625
|
+
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
626
|
+
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
627
|
+
const steps = 18;
|
|
628
|
+
for (let i = 1; i <= steps; i++) {
|
|
629
|
+
const progress = i / steps;
|
|
630
|
+
const angle = from + turns * progress * side;
|
|
631
|
+
const drift = radius * 0.55 * progress;
|
|
632
|
+
points.push({
|
|
633
|
+
x: centre.x + Math.cos(angle) * radius + heading.x * drift,
|
|
634
|
+
y: centre.y + Math.sin(angle) * radius + heading.y * drift
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
const tail = 14;
|
|
638
|
+
for (let i = 1; i <= tail; i++) {
|
|
639
|
+
points.push(at(knot + (1 - knot) * (i / tail)));
|
|
640
|
+
}
|
|
641
|
+
return points;
|
|
642
|
+
}
|
|
575
643
|
function normalize(vector) {
|
|
576
644
|
const length = Math.hypot(vector.x, vector.y);
|
|
577
645
|
if (!length) return { x: 0, y: 0 };
|
|
578
646
|
return { x: vector.x / length, y: vector.y / length };
|
|
579
647
|
}
|
|
580
|
-
|
|
648
|
+
var NOTE_CLEARANCE = 10;
|
|
649
|
+
var TIP_GAP = 11;
|
|
650
|
+
function arrowStartFromNote(origin, target, from) {
|
|
581
651
|
const cx = origin.x + origin.width / 2;
|
|
582
652
|
const cy = origin.y + origin.height / 2;
|
|
653
|
+
if (from && from !== "note") {
|
|
654
|
+
const point = pointFromPosition(from, origin);
|
|
655
|
+
const away = directionFromPosition(from);
|
|
656
|
+
if (!away.x && !away.y) {
|
|
657
|
+
const edge = pointOnBoxToward(origin, target);
|
|
658
|
+
const outward = normalize({ x: edge.x - cx, y: edge.y - cy });
|
|
659
|
+
return {
|
|
660
|
+
x: edge.x + outward.x * NOTE_CLEARANCE,
|
|
661
|
+
y: edge.y + outward.y * NOTE_CLEARANCE
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
x: point.x + away.x * NOTE_CLEARANCE,
|
|
666
|
+
y: point.y + away.y * NOTE_CLEARANCE
|
|
667
|
+
};
|
|
668
|
+
}
|
|
583
669
|
const dx = target.x - cx;
|
|
584
670
|
const dy = target.y - cy;
|
|
585
671
|
const hw = origin.width / 2 || 1e-3;
|
|
@@ -587,15 +673,16 @@ function arrowStartFromNote(origin, target) {
|
|
|
587
673
|
const sx = Math.sign(dx) || 1;
|
|
588
674
|
const sy = Math.sign(dy) || 1;
|
|
589
675
|
if (Math.abs(dy) / hh >= Math.abs(dx) / hw) {
|
|
590
|
-
return { x: cx - sx * hw
|
|
676
|
+
return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
|
|
591
677
|
}
|
|
592
|
-
return { x: cx + sx * (hw +
|
|
678
|
+
return { x: cx + sx * (hw + 8), y: cy + sy * (hh + 6) };
|
|
593
679
|
}
|
|
594
680
|
function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
595
681
|
const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
682
|
+
const from = arrowOptions.from || "note";
|
|
596
683
|
const anchored = Boolean(origin && origin.width > 0 && origin.height > 0);
|
|
597
|
-
let start = anchored ? arrowStartFromNote(origin, center) : annotationAnchor(
|
|
598
|
-
|
|
684
|
+
let start = anchored ? arrowStartFromNote(origin, center, from) : annotationAnchor(
|
|
685
|
+
from === "note" ? "top-right" : from,
|
|
599
686
|
rect,
|
|
600
687
|
38 + random() * 12
|
|
601
688
|
);
|
|
@@ -607,7 +694,7 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
607
694
|
const near = nearestPointOnRect(rect, start);
|
|
608
695
|
let outward = normalize({ x: start.x - near.x, y: start.y - near.y });
|
|
609
696
|
if (!outward.x && !outward.y) outward = { x: 0, y: -1 };
|
|
610
|
-
const gap =
|
|
697
|
+
const gap = TIP_GAP + random() * 5;
|
|
611
698
|
end = { x: near.x + outward.x * gap, y: near.y + outward.y * gap };
|
|
612
699
|
} else {
|
|
613
700
|
end = pointFromPosition(toPos, rect);
|
|
@@ -620,6 +707,35 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
620
707
|
}
|
|
621
708
|
return { start, end };
|
|
622
709
|
}
|
|
710
|
+
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
711
|
+
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
712
|
+
const spread = 0.46 + random() * 0.1;
|
|
713
|
+
for (const side of [-1, 1]) {
|
|
714
|
+
const armAngle = angle + spread * side;
|
|
715
|
+
const tail = {
|
|
716
|
+
x: tip.x - length * Math.cos(armAngle),
|
|
717
|
+
y: tip.y - length * Math.sin(armAngle)
|
|
718
|
+
};
|
|
719
|
+
appendPath(
|
|
720
|
+
svg,
|
|
721
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
722
|
+
steps: 2,
|
|
723
|
+
bow: 0.35 * side
|
|
724
|
+
}),
|
|
725
|
+
{ ...style, dashed: false }
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function boundsOf(points) {
|
|
730
|
+
const xs = points.map((p) => p.x);
|
|
731
|
+
const ys = points.map((p) => p.y);
|
|
732
|
+
return {
|
|
733
|
+
x: Math.min(...xs),
|
|
734
|
+
y: Math.min(...ys),
|
|
735
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
736
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
737
|
+
};
|
|
738
|
+
}
|
|
623
739
|
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
624
740
|
const random = createRandom(seed + 17);
|
|
625
741
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
@@ -641,13 +757,33 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
641
757
|
x: rect.x + rect.width / 2 - mid.x,
|
|
642
758
|
y: rect.y + rect.height / 2 - mid.y
|
|
643
759
|
};
|
|
644
|
-
const
|
|
760
|
+
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
761
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
|
|
645
762
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
646
763
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
647
764
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
648
765
|
const c2x = start.x + dx * 0.72 + perpendicular.x * bow * 0.85;
|
|
649
766
|
const c2y = start.y + dy * 0.72 + perpendicular.y * bow * 0.85;
|
|
650
767
|
hull.push({ x: c1x, y: c1y }, { x: c2x, y: c2y });
|
|
768
|
+
if (arrowOptions.style === "looped") {
|
|
769
|
+
const radius = clamp(distance * 0.11, 7, 17);
|
|
770
|
+
const points = loopedCurvePoints(
|
|
771
|
+
start,
|
|
772
|
+
{ x: c1x, y: c1y },
|
|
773
|
+
{ x: c2x, y: c2y },
|
|
774
|
+
end,
|
|
775
|
+
radius,
|
|
776
|
+
random
|
|
777
|
+
);
|
|
778
|
+
shaft = catmullRomPath(points, false);
|
|
779
|
+
hull.push(...points);
|
|
780
|
+
const last = points[points.length - 1];
|
|
781
|
+
const prior = points[points.length - 2] ?? start;
|
|
782
|
+
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
783
|
+
appendPath(svg, shaft, { ...style, dashed });
|
|
784
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
785
|
+
return { start, end, bounds: boundsOf(hull) };
|
|
786
|
+
}
|
|
651
787
|
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
652
788
|
if (style.roughness >= 1.1) {
|
|
653
789
|
const drift = () => (random() - 0.5) * 1.4;
|
|
@@ -662,31 +798,8 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
662
798
|
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
663
799
|
}
|
|
664
800
|
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
|
-
};
|
|
801
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
802
|
+
const bounds = boundsOf(hull);
|
|
690
803
|
return { start, end, bounds };
|
|
691
804
|
}
|
|
692
805
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
@@ -724,11 +837,38 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
724
837
|
}
|
|
725
838
|
);
|
|
726
839
|
}
|
|
727
|
-
|
|
840
|
+
var DEFAULT_WORDS_PER_LINE = 2;
|
|
841
|
+
function wrapWords(text, perLine) {
|
|
842
|
+
const lines = [];
|
|
843
|
+
for (const paragraph of String(text).split(/\r?\n/)) {
|
|
844
|
+
const words = paragraph.trim().split(/\s+/).filter(Boolean);
|
|
845
|
+
if (!words.length) continue;
|
|
846
|
+
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
847
|
+
for (let i = 0; i < words.length; i += size) {
|
|
848
|
+
lines.push(words.slice(i, i + size).join(" "));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return lines.length ? lines : [String(text)];
|
|
852
|
+
}
|
|
853
|
+
function measureWidth(element, content, fontSize) {
|
|
854
|
+
try {
|
|
855
|
+
const box = element.getBBox();
|
|
856
|
+
if (box && box.width > 0) return box.width;
|
|
857
|
+
} catch {
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
const measured = element.getComputedTextLength();
|
|
861
|
+
if (Number.isFinite(measured) && measured > 0) return measured;
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
return content.length * fontSize * 0.45;
|
|
865
|
+
}
|
|
866
|
+
function createNoteText(svg, text, style, options = {}) {
|
|
728
867
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
868
|
+
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
869
|
+
const lineHeight = fontSize * 1.05;
|
|
729
870
|
const group = document.createElementNS(SVG_NS, "g");
|
|
730
871
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
731
|
-
textEl.textContent = text;
|
|
732
872
|
textEl.setAttribute("x", "0");
|
|
733
873
|
textEl.setAttribute("y", "0");
|
|
734
874
|
textEl.setAttribute("fill", style.color);
|
|
@@ -739,31 +879,43 @@ function createNoteText(svg, text, style) {
|
|
|
739
879
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
740
880
|
textEl.setAttribute("text-anchor", "start");
|
|
741
881
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
882
|
+
const tspans = lines.map((line, index) => {
|
|
883
|
+
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
884
|
+
tspan.textContent = line;
|
|
885
|
+
tspan.setAttribute("x", "0");
|
|
886
|
+
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
887
|
+
textEl.appendChild(tspan);
|
|
888
|
+
return tspan;
|
|
889
|
+
});
|
|
742
890
|
group.appendChild(textEl);
|
|
743
891
|
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;
|
|
892
|
+
const lineWidths = tspans.map(
|
|
893
|
+
(tspan, index) => measureWidth(tspan, lines[index], fontSize)
|
|
894
|
+
);
|
|
895
|
+
const width = Math.max(...lineWidths);
|
|
758
896
|
const ascent = fontSize * 0.74;
|
|
759
897
|
const descent = fontSize * 0.26;
|
|
760
|
-
|
|
898
|
+
const height = (lines.length - 1) * lineHeight + ascent + descent;
|
|
899
|
+
return {
|
|
900
|
+
group,
|
|
901
|
+
textEl,
|
|
902
|
+
tspans,
|
|
903
|
+
lines,
|
|
904
|
+
lineWidths,
|
|
905
|
+
lineHeight,
|
|
906
|
+
width,
|
|
907
|
+
height,
|
|
908
|
+
ascent,
|
|
909
|
+
descent,
|
|
910
|
+
fontSize
|
|
911
|
+
};
|
|
761
912
|
}
|
|
762
913
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
763
914
|
const random = createRandom(seed + 55);
|
|
764
915
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
765
916
|
note.textEl.setAttribute("x", fmt(box.x));
|
|
766
917
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
918
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
|
|
767
919
|
const cx = box.x + box.width / 2;
|
|
768
920
|
const cy = box.y + box.height / 2;
|
|
769
921
|
note.group.setAttribute(
|
|
@@ -771,11 +923,12 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
771
923
|
`rotate(${tilt.toFixed(2)} ${fmt(cx)} ${fmt(cy)})`
|
|
772
924
|
);
|
|
773
925
|
if (options.underline !== false) {
|
|
926
|
+
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
774
927
|
drawUnderline(
|
|
775
928
|
note.group,
|
|
776
929
|
box.x + 1,
|
|
777
930
|
box.y + box.height + 2,
|
|
778
|
-
|
|
931
|
+
lastWidth - 2,
|
|
779
932
|
{
|
|
780
933
|
color: style.color,
|
|
781
934
|
strokeWidth: style.strokeWidth || 1.4,
|
|
@@ -807,25 +960,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
807
960
|
|
|
808
961
|
// src/annotations.js
|
|
809
962
|
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
963
|
function boxForPosition(rect, position, size, gap) {
|
|
830
964
|
const direction = directionFromPosition(position);
|
|
831
965
|
const guard = inflateRect(rect, gap);
|
|
@@ -844,75 +978,67 @@ function boxForPosition(rect, position, size, gap) {
|
|
|
844
978
|
}
|
|
845
979
|
return { x, y, width: size.width, height: size.height };
|
|
846
980
|
}
|
|
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;
|
|
981
|
+
function limitAgainstElement(box, guard, dx) {
|
|
982
|
+
const level = box.y < guard.y + guard.height && box.y + box.height > guard.y;
|
|
983
|
+
if (!level) return dx;
|
|
984
|
+
if (dx < 0) {
|
|
985
|
+
const room2 = box.x - (guard.x + guard.width);
|
|
986
|
+
return room2 <= 0 ? 0 : -Math.min(-dx, room2);
|
|
876
987
|
}
|
|
877
|
-
|
|
988
|
+
const room = guard.x - (box.x + box.width);
|
|
989
|
+
return room <= 0 ? 0 : Math.min(dx, room);
|
|
990
|
+
}
|
|
991
|
+
function nudgeIntoBand(box, guard, band) {
|
|
992
|
+
if (!band || band.width <= 0) return box;
|
|
993
|
+
const overhangLeft = band.x - box.x;
|
|
994
|
+
const overhangRight = box.x + box.width - (band.x + band.width);
|
|
995
|
+
let dx = 0;
|
|
996
|
+
if (overhangLeft > 0) dx = overhangLeft;
|
|
997
|
+
else if (overhangRight > 0) dx = -overhangRight;
|
|
998
|
+
if (!dx) return box;
|
|
999
|
+
dx = limitAgainstElement(box, guard, dx);
|
|
1000
|
+
if (!dx) return box;
|
|
1001
|
+
return { ...box, x: box.x + dx };
|
|
878
1002
|
}
|
|
879
1003
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
880
1004
|
if (!noteOptions?.text) return null;
|
|
881
|
-
const
|
|
1005
|
+
const band = layout.band ?? null;
|
|
882
1006
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
883
1007
|
const random = createRandom(seed + 9);
|
|
884
|
-
const note = createNoteText(svg, noteOptions.text, style
|
|
1008
|
+
const note = createNoteText(svg, noteOptions.text, style, {
|
|
1009
|
+
wordsPerLine: noteOptions.wordsPerLine
|
|
1010
|
+
});
|
|
885
1011
|
const tilt = (random() - 0.45) * 7;
|
|
886
1012
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
887
1013
|
const size = {
|
|
888
1014
|
width: note.width,
|
|
889
|
-
height: note.
|
|
1015
|
+
height: note.height + (noteOptions.underline === false ? 0 : 5)
|
|
890
1016
|
};
|
|
891
1017
|
const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
|
|
892
|
-
const placed =
|
|
1018
|
+
const placed = boxForPosition(
|
|
893
1019
|
rect,
|
|
894
1020
|
position,
|
|
895
1021
|
{ width: size.width, height: size.height + tiltPad },
|
|
896
|
-
gap
|
|
897
|
-
viewport
|
|
1022
|
+
gap
|
|
898
1023
|
);
|
|
1024
|
+
const shifted = {
|
|
1025
|
+
...placed,
|
|
1026
|
+
x: placed.x + (Number(noteOptions.offset?.x) || 0),
|
|
1027
|
+
y: placed.y + (Number(noteOptions.offset?.y) || 0)
|
|
1028
|
+
};
|
|
899
1029
|
const guard = inflateRect(rect, gap * 0.7);
|
|
900
|
-
const outer =
|
|
901
|
-
{ ...placed.box, height: size.height + tiltPad },
|
|
902
|
-
guard,
|
|
903
|
-
viewport
|
|
904
|
-
);
|
|
1030
|
+
const outer = nudgeIntoBand(shifted, guard, band);
|
|
905
1031
|
const box = {
|
|
906
1032
|
x: outer.x,
|
|
907
1033
|
y: outer.y + tiltPad / 2,
|
|
908
1034
|
width: size.width,
|
|
909
|
-
height: note.
|
|
1035
|
+
height: note.height
|
|
910
1036
|
};
|
|
911
1037
|
placeNoteText(note, box, style, seed, {
|
|
912
1038
|
tilt,
|
|
913
1039
|
underline: noteOptions.underline !== false
|
|
914
1040
|
});
|
|
915
|
-
return { group: note.group, box: inflateRect(outer, 4), position
|
|
1041
|
+
return { group: note.group, box: inflateRect(outer, 4), position };
|
|
916
1042
|
}
|
|
917
1043
|
|
|
918
1044
|
// src/decorations.js
|
|
@@ -1330,14 +1456,11 @@ function ensureHandwrittenFont() {
|
|
|
1330
1456
|
|
|
1331
1457
|
// src/doodle.js
|
|
1332
1458
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1333
|
-
function
|
|
1459
|
+
function localBand(overlayRect, inset = 8) {
|
|
1334
1460
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1335
|
-
const height = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1336
1461
|
return {
|
|
1337
1462
|
x: -overlayRect.x + inset,
|
|
1338
|
-
|
|
1339
|
-
width: Math.max(0, width - inset * 2),
|
|
1340
|
-
height: Math.max(0, height - inset * 2)
|
|
1463
|
+
width: Math.max(0, width - inset * 2)
|
|
1341
1464
|
};
|
|
1342
1465
|
}
|
|
1343
1466
|
var DoodleOverlay = class {
|
|
@@ -1436,8 +1559,8 @@ var DoodleOverlay = class {
|
|
|
1436
1559
|
};
|
|
1437
1560
|
const noteGroups = [];
|
|
1438
1561
|
const noteLayout = {
|
|
1439
|
-
|
|
1440
|
-
gap: this.options.arrow ?
|
|
1562
|
+
band: localBand(overlayRect),
|
|
1563
|
+
gap: this.options.arrow ? 40 : 14
|
|
1441
1564
|
};
|
|
1442
1565
|
targets.forEach((target, index) => {
|
|
1443
1566
|
const absolute = absoluteRects[index];
|
package/package.json
CHANGED