@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/freehand-ui.js
CHANGED
|
@@ -33,9 +33,16 @@ function mergeOptions(options = {}) {
|
|
|
33
33
|
if (merged.note && typeof merged.note === "string") {
|
|
34
34
|
merged.note = { text: merged.note, position: "top-right" };
|
|
35
35
|
}
|
|
36
|
-
if (merged.arrow
|
|
37
|
-
const
|
|
38
|
-
merged.arrow = {
|
|
36
|
+
if (merged.arrow) {
|
|
37
|
+
const config = merged.arrow === true ? {} : merged.arrow;
|
|
38
|
+
merged.arrow = {
|
|
39
|
+
// "note" launches from the annotation, a position name from that side of
|
|
40
|
+
// the element. Normalised for every arrow, not just `arrow: true`, so an
|
|
41
|
+
// explicit `from` is honoured whether or not there is a note.
|
|
42
|
+
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
43
|
+
to: config.to ?? "edge",
|
|
44
|
+
style: config.style ?? "curved"
|
|
45
|
+
};
|
|
39
46
|
}
|
|
40
47
|
if (merged.decorations === true) {
|
|
41
48
|
merged.decorations = { count: null, types: null };
|
|
@@ -180,10 +187,6 @@ function intersectionArea(a, b) {
|
|
|
180
187
|
const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
181
188
|
return w > 0 && h > 0 ? w * h : 0;
|
|
182
189
|
}
|
|
183
|
-
function outsideArea(box, bounds) {
|
|
184
|
-
if (!bounds) return 0;
|
|
185
|
-
return Math.max(0, box.width * box.height - intersectionArea(box, bounds));
|
|
186
|
-
}
|
|
187
190
|
function nearestPointOnRect(rect, point) {
|
|
188
191
|
return {
|
|
189
192
|
x: clamp(point.x, rect.x, rect.x + rect.width),
|
|
@@ -196,6 +199,17 @@ function distanceToRoundedRect(point, rect, radius = 0) {
|
|
|
196
199
|
const cy = clamp(point.y, rect.y + r, rect.y + rect.height - r);
|
|
197
200
|
return Math.hypot(point.x - cx, point.y - cy) - r;
|
|
198
201
|
}
|
|
202
|
+
function pointOnBoxToward(box, target) {
|
|
203
|
+
const cx = box.x + box.width / 2;
|
|
204
|
+
const cy = box.y + box.height / 2;
|
|
205
|
+
const dx = target.x - cx;
|
|
206
|
+
const dy = target.y - cy;
|
|
207
|
+
if (dx === 0 && dy === 0) return { x: cx, y: cy };
|
|
208
|
+
const hw = box.width / 2 || 1e-3;
|
|
209
|
+
const hh = box.height / 2 || 1e-3;
|
|
210
|
+
const scale = 1 / Math.max(Math.abs(dx) / hw, Math.abs(dy) / hh);
|
|
211
|
+
return { x: cx + dx * scale, y: cy + dy * scale };
|
|
212
|
+
}
|
|
199
213
|
function directionFromPosition(position) {
|
|
200
214
|
return {
|
|
201
215
|
x: position.includes("right") ? 1 : position.includes("left") ? -1 : 0,
|
|
@@ -559,14 +573,86 @@ function drawBorder(svg, rect, options, seed) {
|
|
|
559
573
|
});
|
|
560
574
|
}
|
|
561
575
|
}
|
|
576
|
+
function cubicAt(p0, c1, c2, p3, t) {
|
|
577
|
+
const u = 1 - t;
|
|
578
|
+
const a = u * u * u;
|
|
579
|
+
const b = 3 * u * u * t;
|
|
580
|
+
const c = 3 * u * t * t;
|
|
581
|
+
const d = t * t * t;
|
|
582
|
+
return {
|
|
583
|
+
x: a * p0.x + b * c1.x + c * c2.x + d * p3.x,
|
|
584
|
+
y: a * p0.y + b * c1.y + c * c2.y + d * p3.y
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
588
|
+
const u = 1 - t;
|
|
589
|
+
const a = 3 * u * u;
|
|
590
|
+
const b = 6 * u * t;
|
|
591
|
+
const c = 3 * t * t;
|
|
592
|
+
return {
|
|
593
|
+
x: a * (c1.x - p0.x) + b * (c2.x - c1.x) + c * (p3.x - c2.x),
|
|
594
|
+
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
598
|
+
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
599
|
+
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
600
|
+
const knot = 0.46 + random() * 0.12;
|
|
601
|
+
const points = [];
|
|
602
|
+
const lead = Math.max(2, Math.round(knot * 16));
|
|
603
|
+
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
604
|
+
const pivot = at(knot);
|
|
605
|
+
const heading = tangentAt(knot);
|
|
606
|
+
const normal = { x: -heading.y, y: heading.x };
|
|
607
|
+
const side = random() < 0.5 ? 1 : -1;
|
|
608
|
+
const centre = {
|
|
609
|
+
x: pivot.x + normal.x * radius * side,
|
|
610
|
+
y: pivot.y + normal.y * radius * side
|
|
611
|
+
};
|
|
612
|
+
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
613
|
+
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
614
|
+
const steps = 18;
|
|
615
|
+
for (let i = 1; i <= steps; i++) {
|
|
616
|
+
const progress = i / steps;
|
|
617
|
+
const angle = from + turns * progress * side;
|
|
618
|
+
const drift = radius * 0.55 * progress;
|
|
619
|
+
points.push({
|
|
620
|
+
x: centre.x + Math.cos(angle) * radius + heading.x * drift,
|
|
621
|
+
y: centre.y + Math.sin(angle) * radius + heading.y * drift
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
const tail = 14;
|
|
625
|
+
for (let i = 1; i <= tail; i++) {
|
|
626
|
+
points.push(at(knot + (1 - knot) * (i / tail)));
|
|
627
|
+
}
|
|
628
|
+
return points;
|
|
629
|
+
}
|
|
562
630
|
function normalize(vector) {
|
|
563
631
|
const length = Math.hypot(vector.x, vector.y);
|
|
564
632
|
if (!length) return { x: 0, y: 0 };
|
|
565
633
|
return { x: vector.x / length, y: vector.y / length };
|
|
566
634
|
}
|
|
567
|
-
|
|
635
|
+
var NOTE_CLEARANCE = 10;
|
|
636
|
+
var TIP_GAP = 11;
|
|
637
|
+
function arrowStartFromNote(origin, target, from) {
|
|
568
638
|
const cx = origin.x + origin.width / 2;
|
|
569
639
|
const cy = origin.y + origin.height / 2;
|
|
640
|
+
if (from && from !== "note") {
|
|
641
|
+
const point = pointFromPosition(from, origin);
|
|
642
|
+
const away = directionFromPosition(from);
|
|
643
|
+
if (!away.x && !away.y) {
|
|
644
|
+
const edge = pointOnBoxToward(origin, target);
|
|
645
|
+
const outward = normalize({ x: edge.x - cx, y: edge.y - cy });
|
|
646
|
+
return {
|
|
647
|
+
x: edge.x + outward.x * NOTE_CLEARANCE,
|
|
648
|
+
y: edge.y + outward.y * NOTE_CLEARANCE
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
x: point.x + away.x * NOTE_CLEARANCE,
|
|
653
|
+
y: point.y + away.y * NOTE_CLEARANCE
|
|
654
|
+
};
|
|
655
|
+
}
|
|
570
656
|
const dx = target.x - cx;
|
|
571
657
|
const dy = target.y - cy;
|
|
572
658
|
const hw = origin.width / 2 || 1e-3;
|
|
@@ -574,15 +660,16 @@ function arrowStartFromNote(origin, target) {
|
|
|
574
660
|
const sx = Math.sign(dx) || 1;
|
|
575
661
|
const sy = Math.sign(dy) || 1;
|
|
576
662
|
if (Math.abs(dy) / hh >= Math.abs(dx) / hw) {
|
|
577
|
-
return { x: cx - sx * hw
|
|
663
|
+
return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
|
|
578
664
|
}
|
|
579
|
-
return { x: cx + sx * (hw +
|
|
665
|
+
return { x: cx + sx * (hw + 8), y: cy + sy * (hh + 6) };
|
|
580
666
|
}
|
|
581
667
|
function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
582
668
|
const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
669
|
+
const from = arrowOptions.from || "note";
|
|
583
670
|
const anchored = Boolean(origin && origin.width > 0 && origin.height > 0);
|
|
584
|
-
let start = anchored ? arrowStartFromNote(origin, center) : annotationAnchor(
|
|
585
|
-
|
|
671
|
+
let start = anchored ? arrowStartFromNote(origin, center, from) : annotationAnchor(
|
|
672
|
+
from === "note" ? "top-right" : from,
|
|
586
673
|
rect,
|
|
587
674
|
38 + random() * 12
|
|
588
675
|
);
|
|
@@ -594,7 +681,7 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
594
681
|
const near = nearestPointOnRect(rect, start);
|
|
595
682
|
let outward = normalize({ x: start.x - near.x, y: start.y - near.y });
|
|
596
683
|
if (!outward.x && !outward.y) outward = { x: 0, y: -1 };
|
|
597
|
-
const gap =
|
|
684
|
+
const gap = TIP_GAP + random() * 5;
|
|
598
685
|
end = { x: near.x + outward.x * gap, y: near.y + outward.y * gap };
|
|
599
686
|
} else {
|
|
600
687
|
end = pointFromPosition(toPos, rect);
|
|
@@ -607,6 +694,35 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
607
694
|
}
|
|
608
695
|
return { start, end };
|
|
609
696
|
}
|
|
697
|
+
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
698
|
+
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
699
|
+
const spread = 0.46 + random() * 0.1;
|
|
700
|
+
for (const side of [-1, 1]) {
|
|
701
|
+
const armAngle = angle + spread * side;
|
|
702
|
+
const tail = {
|
|
703
|
+
x: tip.x - length * Math.cos(armAngle),
|
|
704
|
+
y: tip.y - length * Math.sin(armAngle)
|
|
705
|
+
};
|
|
706
|
+
appendPath(
|
|
707
|
+
svg,
|
|
708
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
709
|
+
steps: 2,
|
|
710
|
+
bow: 0.35 * side
|
|
711
|
+
}),
|
|
712
|
+
{ ...style, dashed: false }
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function boundsOf(points) {
|
|
717
|
+
const xs = points.map((p) => p.x);
|
|
718
|
+
const ys = points.map((p) => p.y);
|
|
719
|
+
return {
|
|
720
|
+
x: Math.min(...xs),
|
|
721
|
+
y: Math.min(...ys),
|
|
722
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
723
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
724
|
+
};
|
|
725
|
+
}
|
|
610
726
|
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
611
727
|
const random = createRandom(seed + 17);
|
|
612
728
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
@@ -628,13 +744,33 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
628
744
|
x: rect.x + rect.width / 2 - mid.x,
|
|
629
745
|
y: rect.y + rect.height / 2 - mid.y
|
|
630
746
|
};
|
|
631
|
-
const
|
|
747
|
+
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
748
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
|
|
632
749
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
633
750
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
634
751
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
635
752
|
const c2x = start.x + dx * 0.72 + perpendicular.x * bow * 0.85;
|
|
636
753
|
const c2y = start.y + dy * 0.72 + perpendicular.y * bow * 0.85;
|
|
637
754
|
hull.push({ x: c1x, y: c1y }, { x: c2x, y: c2y });
|
|
755
|
+
if (arrowOptions.style === "looped") {
|
|
756
|
+
const radius = clamp(distance * 0.11, 7, 17);
|
|
757
|
+
const points = loopedCurvePoints(
|
|
758
|
+
start,
|
|
759
|
+
{ x: c1x, y: c1y },
|
|
760
|
+
{ x: c2x, y: c2y },
|
|
761
|
+
end,
|
|
762
|
+
radius,
|
|
763
|
+
random
|
|
764
|
+
);
|
|
765
|
+
shaft = catmullRomPath(points, false);
|
|
766
|
+
hull.push(...points);
|
|
767
|
+
const last = points[points.length - 1];
|
|
768
|
+
const prior = points[points.length - 2] ?? start;
|
|
769
|
+
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
770
|
+
appendPath(svg, shaft, { ...style, dashed });
|
|
771
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
772
|
+
return { start, end, bounds: boundsOf(hull) };
|
|
773
|
+
}
|
|
638
774
|
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
639
775
|
if (style.roughness >= 1.1) {
|
|
640
776
|
const drift = () => (random() - 0.5) * 1.4;
|
|
@@ -649,31 +785,8 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
649
785
|
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
650
786
|
}
|
|
651
787
|
appendPath(svg, shaft, { ...style, dashed });
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
for (const side of [-1, 1]) {
|
|
655
|
-
const angle = tipAngle + spread * side;
|
|
656
|
-
const tail = {
|
|
657
|
-
x: end.x - headLength * Math.cos(angle),
|
|
658
|
-
y: end.y - headLength * Math.sin(angle)
|
|
659
|
-
};
|
|
660
|
-
appendPath(
|
|
661
|
-
svg,
|
|
662
|
-
roughLine(tail.x, tail.y, end.x, end.y, style.roughness * 0.5, random, {
|
|
663
|
-
steps: 2,
|
|
664
|
-
bow: 0.35 * side
|
|
665
|
-
}),
|
|
666
|
-
{ ...style, dashed: false }
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
const xs = hull.map((p) => p.x);
|
|
670
|
-
const ys = hull.map((p) => p.y);
|
|
671
|
-
const bounds = {
|
|
672
|
-
x: Math.min(...xs),
|
|
673
|
-
y: Math.min(...ys),
|
|
674
|
-
width: Math.max(...xs) - Math.min(...xs),
|
|
675
|
-
height: Math.max(...ys) - Math.min(...ys)
|
|
676
|
-
};
|
|
788
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
789
|
+
const bounds = boundsOf(hull);
|
|
677
790
|
return { start, end, bounds };
|
|
678
791
|
}
|
|
679
792
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
@@ -711,11 +824,38 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
711
824
|
}
|
|
712
825
|
);
|
|
713
826
|
}
|
|
714
|
-
|
|
827
|
+
var DEFAULT_WORDS_PER_LINE = 2;
|
|
828
|
+
function wrapWords(text, perLine) {
|
|
829
|
+
const lines = [];
|
|
830
|
+
for (const paragraph of String(text).split(/\r?\n/)) {
|
|
831
|
+
const words = paragraph.trim().split(/\s+/).filter(Boolean);
|
|
832
|
+
if (!words.length) continue;
|
|
833
|
+
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
834
|
+
for (let i = 0; i < words.length; i += size) {
|
|
835
|
+
lines.push(words.slice(i, i + size).join(" "));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return lines.length ? lines : [String(text)];
|
|
839
|
+
}
|
|
840
|
+
function measureWidth(element, content, fontSize) {
|
|
841
|
+
try {
|
|
842
|
+
const box = element.getBBox();
|
|
843
|
+
if (box && box.width > 0) return box.width;
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
const measured = element.getComputedTextLength();
|
|
848
|
+
if (Number.isFinite(measured) && measured > 0) return measured;
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
return content.length * fontSize * 0.45;
|
|
852
|
+
}
|
|
853
|
+
function createNoteText(svg, text, style, options = {}) {
|
|
715
854
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
855
|
+
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
856
|
+
const lineHeight = fontSize * 1.05;
|
|
716
857
|
const group = document.createElementNS(SVG_NS, "g");
|
|
717
858
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
718
|
-
textEl.textContent = text;
|
|
719
859
|
textEl.setAttribute("x", "0");
|
|
720
860
|
textEl.setAttribute("y", "0");
|
|
721
861
|
textEl.setAttribute("fill", style.color);
|
|
@@ -726,31 +866,43 @@ function createNoteText(svg, text, style) {
|
|
|
726
866
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
727
867
|
textEl.setAttribute("text-anchor", "start");
|
|
728
868
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
869
|
+
const tspans = lines.map((line, index) => {
|
|
870
|
+
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
871
|
+
tspan.textContent = line;
|
|
872
|
+
tspan.setAttribute("x", "0");
|
|
873
|
+
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
874
|
+
textEl.appendChild(tspan);
|
|
875
|
+
return tspan;
|
|
876
|
+
});
|
|
729
877
|
group.appendChild(textEl);
|
|
730
878
|
svg.appendChild(group);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
} catch {
|
|
736
|
-
}
|
|
737
|
-
if (!width) {
|
|
738
|
-
try {
|
|
739
|
-
const measured = textEl.getComputedTextLength();
|
|
740
|
-
if (Number.isFinite(measured) && measured > 0) width = measured;
|
|
741
|
-
} catch {
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (!width) width = text.length * fontSize * 0.45;
|
|
879
|
+
const lineWidths = tspans.map(
|
|
880
|
+
(tspan, index) => measureWidth(tspan, lines[index], fontSize)
|
|
881
|
+
);
|
|
882
|
+
const width = Math.max(...lineWidths);
|
|
745
883
|
const ascent = fontSize * 0.74;
|
|
746
884
|
const descent = fontSize * 0.26;
|
|
747
|
-
|
|
885
|
+
const height = (lines.length - 1) * lineHeight + ascent + descent;
|
|
886
|
+
return {
|
|
887
|
+
group,
|
|
888
|
+
textEl,
|
|
889
|
+
tspans,
|
|
890
|
+
lines,
|
|
891
|
+
lineWidths,
|
|
892
|
+
lineHeight,
|
|
893
|
+
width,
|
|
894
|
+
height,
|
|
895
|
+
ascent,
|
|
896
|
+
descent,
|
|
897
|
+
fontSize
|
|
898
|
+
};
|
|
748
899
|
}
|
|
749
900
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
750
901
|
const random = createRandom(seed + 55);
|
|
751
902
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
752
903
|
note.textEl.setAttribute("x", fmt(box.x));
|
|
753
904
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
905
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
|
|
754
906
|
const cx = box.x + box.width / 2;
|
|
755
907
|
const cy = box.y + box.height / 2;
|
|
756
908
|
note.group.setAttribute(
|
|
@@ -758,11 +910,12 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
758
910
|
`rotate(${tilt.toFixed(2)} ${fmt(cx)} ${fmt(cy)})`
|
|
759
911
|
);
|
|
760
912
|
if (options.underline !== false) {
|
|
913
|
+
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
761
914
|
drawUnderline(
|
|
762
915
|
note.group,
|
|
763
916
|
box.x + 1,
|
|
764
917
|
box.y + box.height + 2,
|
|
765
|
-
|
|
918
|
+
lastWidth - 2,
|
|
766
919
|
{
|
|
767
920
|
color: style.color,
|
|
768
921
|
strokeWidth: style.strokeWidth || 1.4,
|
|
@@ -794,25 +947,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
794
947
|
|
|
795
948
|
// src/annotations.js
|
|
796
949
|
var NOTE_GAP = 14;
|
|
797
|
-
function mirror(position, axis) {
|
|
798
|
-
if (axis === "x") {
|
|
799
|
-
if (position.includes("right")) return position.replace("right", "left");
|
|
800
|
-
if (position.includes("left")) return position.replace("left", "right");
|
|
801
|
-
return position;
|
|
802
|
-
}
|
|
803
|
-
if (position.includes("top")) return position.replace("top", "bottom");
|
|
804
|
-
if (position.includes("bottom")) return position.replace("bottom", "top");
|
|
805
|
-
return position;
|
|
806
|
-
}
|
|
807
|
-
function candidatePositions(position) {
|
|
808
|
-
const candidates = [
|
|
809
|
-
position,
|
|
810
|
-
mirror(position, "x"),
|
|
811
|
-
mirror(position, "y"),
|
|
812
|
-
mirror(mirror(position, "x"), "y")
|
|
813
|
-
];
|
|
814
|
-
return candidates.filter((value, index) => candidates.indexOf(value) === index);
|
|
815
|
-
}
|
|
816
950
|
function boxForPosition(rect, position, size, gap) {
|
|
817
951
|
const direction = directionFromPosition(position);
|
|
818
952
|
const guard = inflateRect(rect, gap);
|
|
@@ -831,75 +965,67 @@ function boxForPosition(rect, position, size, gap) {
|
|
|
831
965
|
}
|
|
832
966
|
return { x, y, width: size.width, height: size.height };
|
|
833
967
|
}
|
|
834
|
-
function
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
if (!best || penalty < best.penalty) {
|
|
841
|
-
best = { box, penalty, position: candidate };
|
|
842
|
-
}
|
|
843
|
-
});
|
|
844
|
-
return best;
|
|
845
|
-
}
|
|
846
|
-
function clampToViewport(box, guard, viewport) {
|
|
847
|
-
if (!viewport || viewport.width <= 0 || viewport.height <= 0) return box;
|
|
848
|
-
const shifted = {
|
|
849
|
-
...box,
|
|
850
|
-
x: clamp(
|
|
851
|
-
box.x,
|
|
852
|
-
viewport.x,
|
|
853
|
-
Math.max(viewport.x, viewport.x + viewport.width - box.width)
|
|
854
|
-
),
|
|
855
|
-
y: clamp(
|
|
856
|
-
box.y,
|
|
857
|
-
viewport.y,
|
|
858
|
-
Math.max(viewport.y, viewport.y + viewport.height - box.height)
|
|
859
|
-
)
|
|
860
|
-
};
|
|
861
|
-
if (intersectionArea(shifted, guard) > intersectionArea(box, guard)) {
|
|
862
|
-
return box;
|
|
968
|
+
function limitAgainstElement(box, guard, dx) {
|
|
969
|
+
const level = box.y < guard.y + guard.height && box.y + box.height > guard.y;
|
|
970
|
+
if (!level) return dx;
|
|
971
|
+
if (dx < 0) {
|
|
972
|
+
const room2 = box.x - (guard.x + guard.width);
|
|
973
|
+
return room2 <= 0 ? 0 : -Math.min(-dx, room2);
|
|
863
974
|
}
|
|
864
|
-
|
|
975
|
+
const room = guard.x - (box.x + box.width);
|
|
976
|
+
return room <= 0 ? 0 : Math.min(dx, room);
|
|
977
|
+
}
|
|
978
|
+
function nudgeIntoBand(box, guard, band) {
|
|
979
|
+
if (!band || band.width <= 0) return box;
|
|
980
|
+
const overhangLeft = band.x - box.x;
|
|
981
|
+
const overhangRight = box.x + box.width - (band.x + band.width);
|
|
982
|
+
let dx = 0;
|
|
983
|
+
if (overhangLeft > 0) dx = overhangLeft;
|
|
984
|
+
else if (overhangRight > 0) dx = -overhangRight;
|
|
985
|
+
if (!dx) return box;
|
|
986
|
+
dx = limitAgainstElement(box, guard, dx);
|
|
987
|
+
if (!dx) return box;
|
|
988
|
+
return { ...box, x: box.x + dx };
|
|
865
989
|
}
|
|
866
990
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
867
991
|
if (!noteOptions?.text) return null;
|
|
868
|
-
const
|
|
992
|
+
const band = layout.band ?? null;
|
|
869
993
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
870
994
|
const random = createRandom(seed + 9);
|
|
871
|
-
const note = createNoteText(svg, noteOptions.text, style
|
|
995
|
+
const note = createNoteText(svg, noteOptions.text, style, {
|
|
996
|
+
wordsPerLine: noteOptions.wordsPerLine
|
|
997
|
+
});
|
|
872
998
|
const tilt = (random() - 0.45) * 7;
|
|
873
999
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
874
1000
|
const size = {
|
|
875
1001
|
width: note.width,
|
|
876
|
-
height: note.
|
|
1002
|
+
height: note.height + (noteOptions.underline === false ? 0 : 5)
|
|
877
1003
|
};
|
|
878
1004
|
const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
|
|
879
|
-
const placed =
|
|
1005
|
+
const placed = boxForPosition(
|
|
880
1006
|
rect,
|
|
881
1007
|
position,
|
|
882
1008
|
{ width: size.width, height: size.height + tiltPad },
|
|
883
|
-
gap
|
|
884
|
-
viewport
|
|
1009
|
+
gap
|
|
885
1010
|
);
|
|
1011
|
+
const shifted = {
|
|
1012
|
+
...placed,
|
|
1013
|
+
x: placed.x + (Number(noteOptions.offset?.x) || 0),
|
|
1014
|
+
y: placed.y + (Number(noteOptions.offset?.y) || 0)
|
|
1015
|
+
};
|
|
886
1016
|
const guard = inflateRect(rect, gap * 0.7);
|
|
887
|
-
const outer =
|
|
888
|
-
{ ...placed.box, height: size.height + tiltPad },
|
|
889
|
-
guard,
|
|
890
|
-
viewport
|
|
891
|
-
);
|
|
1017
|
+
const outer = nudgeIntoBand(shifted, guard, band);
|
|
892
1018
|
const box = {
|
|
893
1019
|
x: outer.x,
|
|
894
1020
|
y: outer.y + tiltPad / 2,
|
|
895
1021
|
width: size.width,
|
|
896
|
-
height: note.
|
|
1022
|
+
height: note.height
|
|
897
1023
|
};
|
|
898
1024
|
placeNoteText(note, box, style, seed, {
|
|
899
1025
|
tilt,
|
|
900
1026
|
underline: noteOptions.underline !== false
|
|
901
1027
|
});
|
|
902
|
-
return { group: note.group, box: inflateRect(outer, 4), position
|
|
1028
|
+
return { group: note.group, box: inflateRect(outer, 4), position };
|
|
903
1029
|
}
|
|
904
1030
|
|
|
905
1031
|
// src/decorations.js
|
|
@@ -1317,14 +1443,11 @@ function ensureHandwrittenFont() {
|
|
|
1317
1443
|
|
|
1318
1444
|
// src/doodle.js
|
|
1319
1445
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1320
|
-
function
|
|
1446
|
+
function localBand(overlayRect, inset = 8) {
|
|
1321
1447
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1322
|
-
const height = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1323
1448
|
return {
|
|
1324
1449
|
x: -overlayRect.x + inset,
|
|
1325
|
-
|
|
1326
|
-
width: Math.max(0, width - inset * 2),
|
|
1327
|
-
height: Math.max(0, height - inset * 2)
|
|
1450
|
+
width: Math.max(0, width - inset * 2)
|
|
1328
1451
|
};
|
|
1329
1452
|
}
|
|
1330
1453
|
var DoodleOverlay = class {
|
|
@@ -1423,8 +1546,8 @@ var DoodleOverlay = class {
|
|
|
1423
1546
|
};
|
|
1424
1547
|
const noteGroups = [];
|
|
1425
1548
|
const noteLayout = {
|
|
1426
|
-
|
|
1427
|
-
gap: this.options.arrow ?
|
|
1549
|
+
band: localBand(overlayRect),
|
|
1550
|
+
gap: this.options.arrow ? 40 : 14
|
|
1428
1551
|
};
|
|
1429
1552
|
targets.forEach((target, index) => {
|
|
1430
1553
|
const absolute = absoluteRects[index];
|
package/dist/index.d.ts
CHANGED
|
@@ -23,17 +23,33 @@ export type DecorationType =
|
|
|
23
23
|
|
|
24
24
|
export interface NoteOptions {
|
|
25
25
|
text: string;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Which side of the element the note sits on. Always honoured — a note that
|
|
28
|
+
* would run off screen slides horizontally to stay in view rather than
|
|
29
|
+
* moving to another side.
|
|
30
|
+
*/
|
|
27
31
|
position?: DoodlePosition;
|
|
28
32
|
/** Underline sized to the measured text. Default `true`. */
|
|
29
33
|
underline?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Nudge the note from where `position` put it, in px. The note is still kept
|
|
36
|
+
* in view from the offset spot, so a large `x` slides back rather than
|
|
37
|
+
* running off screen.
|
|
38
|
+
*/
|
|
39
|
+
offset?: { x?: number; y?: number };
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
export interface ArrowOptions {
|
|
33
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Where the arrow leaves. With a `note` this names a side of *the note* —
|
|
45
|
+
* the arrow always departs from the handwriting and travels to the element,
|
|
46
|
+
* however far an `offset` has moved it. `"note"` (the default) picks the side
|
|
47
|
+
* facing the element. Without a note it names a side of the element instead.
|
|
48
|
+
*/
|
|
49
|
+
from?: DoodlePosition | "note";
|
|
34
50
|
/** `"edge"` lands just outside the frame; `"center"` points into it. */
|
|
35
51
|
to?: DoodlePosition | "edge";
|
|
36
|
-
style?: "curved" | "straight" | "dotted";
|
|
52
|
+
style?: "curved" | "straight" | "dotted" | "looped";
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
export interface DecorationOptions {
|