@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/freehand-ui.cjs
CHANGED
|
@@ -58,9 +58,16 @@ function mergeOptions(options = {}) {
|
|
|
58
58
|
if (merged.note && typeof merged.note === "string") {
|
|
59
59
|
merged.note = { text: merged.note, position: "top-right" };
|
|
60
60
|
}
|
|
61
|
-
if (merged.arrow
|
|
62
|
-
const
|
|
63
|
-
merged.arrow = {
|
|
61
|
+
if (merged.arrow) {
|
|
62
|
+
const config = merged.arrow === true ? {} : merged.arrow;
|
|
63
|
+
merged.arrow = {
|
|
64
|
+
// "note" launches from the annotation, a position name from that side of
|
|
65
|
+
// the element. Normalised for every arrow, not just `arrow: true`, so an
|
|
66
|
+
// explicit `from` is honoured whether or not there is a note.
|
|
67
|
+
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
68
|
+
to: config.to ?? "edge",
|
|
69
|
+
style: config.style ?? "curved"
|
|
70
|
+
};
|
|
64
71
|
}
|
|
65
72
|
if (merged.decorations === true) {
|
|
66
73
|
merged.decorations = { count: null, types: null };
|
|
@@ -205,10 +212,6 @@ function intersectionArea(a, b) {
|
|
|
205
212
|
const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
206
213
|
return w > 0 && h > 0 ? w * h : 0;
|
|
207
214
|
}
|
|
208
|
-
function outsideArea(box, bounds) {
|
|
209
|
-
if (!bounds) return 0;
|
|
210
|
-
return Math.max(0, box.width * box.height - intersectionArea(box, bounds));
|
|
211
|
-
}
|
|
212
215
|
function nearestPointOnRect(rect, point) {
|
|
213
216
|
return {
|
|
214
217
|
x: clamp(point.x, rect.x, rect.x + rect.width),
|
|
@@ -221,6 +224,17 @@ function distanceToRoundedRect(point, rect, radius = 0) {
|
|
|
221
224
|
const cy = clamp(point.y, rect.y + r, rect.y + rect.height - r);
|
|
222
225
|
return Math.hypot(point.x - cx, point.y - cy) - r;
|
|
223
226
|
}
|
|
227
|
+
function pointOnBoxToward(box, target) {
|
|
228
|
+
const cx = box.x + box.width / 2;
|
|
229
|
+
const cy = box.y + box.height / 2;
|
|
230
|
+
const dx = target.x - cx;
|
|
231
|
+
const dy = target.y - cy;
|
|
232
|
+
if (dx === 0 && dy === 0) return { x: cx, y: cy };
|
|
233
|
+
const hw = box.width / 2 || 1e-3;
|
|
234
|
+
const hh = box.height / 2 || 1e-3;
|
|
235
|
+
const scale = 1 / Math.max(Math.abs(dx) / hw, Math.abs(dy) / hh);
|
|
236
|
+
return { x: cx + dx * scale, y: cy + dy * scale };
|
|
237
|
+
}
|
|
224
238
|
function directionFromPosition(position) {
|
|
225
239
|
return {
|
|
226
240
|
x: position.includes("right") ? 1 : position.includes("left") ? -1 : 0,
|
|
@@ -470,21 +484,14 @@ function isBroken(fraction, breaks) {
|
|
|
470
484
|
function createBorderBreaks(perimeter, config, random) {
|
|
471
485
|
if (!config || perimeter <= 0) return [];
|
|
472
486
|
const settings = config === true ? {} : config;
|
|
473
|
-
const count = Math.max(
|
|
474
|
-
1,
|
|
475
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
476
|
-
);
|
|
487
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
477
488
|
const min = Math.max(2, settings.min ?? 6);
|
|
478
489
|
const max = Math.max(min, settings.max ?? 30);
|
|
479
490
|
const slice = perimeter / count;
|
|
480
491
|
let budget = perimeter * 0.35;
|
|
481
492
|
const breaks = [];
|
|
482
493
|
for (let i = 0; i < count; i++) {
|
|
483
|
-
const length = Math.min(
|
|
484
|
-
min + random() * (max - min),
|
|
485
|
-
slice * 0.7,
|
|
486
|
-
budget
|
|
487
|
-
);
|
|
494
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
488
495
|
if (length < 2) break;
|
|
489
496
|
budget -= length;
|
|
490
497
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -584,14 +591,86 @@ function drawBorder(svg, rect, options, seed) {
|
|
|
584
591
|
});
|
|
585
592
|
}
|
|
586
593
|
}
|
|
594
|
+
function cubicAt(p0, c1, c2, p3, t) {
|
|
595
|
+
const u = 1 - t;
|
|
596
|
+
const a = u * u * u;
|
|
597
|
+
const b = 3 * u * u * t;
|
|
598
|
+
const c = 3 * u * t * t;
|
|
599
|
+
const d = t * t * t;
|
|
600
|
+
return {
|
|
601
|
+
x: a * p0.x + b * c1.x + c * c2.x + d * p3.x,
|
|
602
|
+
y: a * p0.y + b * c1.y + c * c2.y + d * p3.y
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
606
|
+
const u = 1 - t;
|
|
607
|
+
const a = 3 * u * u;
|
|
608
|
+
const b = 6 * u * t;
|
|
609
|
+
const c = 3 * t * t;
|
|
610
|
+
return {
|
|
611
|
+
x: a * (c1.x - p0.x) + b * (c2.x - c1.x) + c * (p3.x - c2.x),
|
|
612
|
+
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
616
|
+
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
617
|
+
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
618
|
+
const knot = 0.46 + random() * 0.12;
|
|
619
|
+
const points = [];
|
|
620
|
+
const lead = Math.max(2, Math.round(knot * 16));
|
|
621
|
+
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
622
|
+
const pivot = at(knot);
|
|
623
|
+
const heading = tangentAt(knot);
|
|
624
|
+
const normal = { x: -heading.y, y: heading.x };
|
|
625
|
+
const side = random() < 0.5 ? 1 : -1;
|
|
626
|
+
const centre = {
|
|
627
|
+
x: pivot.x + normal.x * radius * side,
|
|
628
|
+
y: pivot.y + normal.y * radius * side
|
|
629
|
+
};
|
|
630
|
+
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
631
|
+
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
632
|
+
const steps = 18;
|
|
633
|
+
for (let i = 1; i <= steps; i++) {
|
|
634
|
+
const progress = i / steps;
|
|
635
|
+
const angle = from + turns * progress * side;
|
|
636
|
+
const drift = radius * 0.55 * progress;
|
|
637
|
+
points.push({
|
|
638
|
+
x: centre.x + Math.cos(angle) * radius + heading.x * drift,
|
|
639
|
+
y: centre.y + Math.sin(angle) * radius + heading.y * drift
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const tail = 14;
|
|
643
|
+
for (let i = 1; i <= tail; i++) {
|
|
644
|
+
points.push(at(knot + (1 - knot) * (i / tail)));
|
|
645
|
+
}
|
|
646
|
+
return points;
|
|
647
|
+
}
|
|
587
648
|
function normalize(vector) {
|
|
588
649
|
const length = Math.hypot(vector.x, vector.y);
|
|
589
650
|
if (!length) return { x: 0, y: 0 };
|
|
590
651
|
return { x: vector.x / length, y: vector.y / length };
|
|
591
652
|
}
|
|
592
|
-
|
|
653
|
+
var NOTE_CLEARANCE = 10;
|
|
654
|
+
var TIP_GAP = 11;
|
|
655
|
+
function arrowStartFromNote(origin, target, from) {
|
|
593
656
|
const cx = origin.x + origin.width / 2;
|
|
594
657
|
const cy = origin.y + origin.height / 2;
|
|
658
|
+
if (from && from !== "note") {
|
|
659
|
+
const point = pointFromPosition(from, origin);
|
|
660
|
+
const away = directionFromPosition(from);
|
|
661
|
+
if (!away.x && !away.y) {
|
|
662
|
+
const edge = pointOnBoxToward(origin, target);
|
|
663
|
+
const outward = normalize({ x: edge.x - cx, y: edge.y - cy });
|
|
664
|
+
return {
|
|
665
|
+
x: edge.x + outward.x * NOTE_CLEARANCE,
|
|
666
|
+
y: edge.y + outward.y * NOTE_CLEARANCE
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
x: point.x + away.x * NOTE_CLEARANCE,
|
|
671
|
+
y: point.y + away.y * NOTE_CLEARANCE
|
|
672
|
+
};
|
|
673
|
+
}
|
|
595
674
|
const dx = target.x - cx;
|
|
596
675
|
const dy = target.y - cy;
|
|
597
676
|
const hw = origin.width / 2 || 1e-3;
|
|
@@ -599,15 +678,16 @@ function arrowStartFromNote(origin, target) {
|
|
|
599
678
|
const sx = Math.sign(dx) || 1;
|
|
600
679
|
const sy = Math.sign(dy) || 1;
|
|
601
680
|
if (Math.abs(dy) / hh >= Math.abs(dx) / hw) {
|
|
602
|
-
return { x: cx - sx * hw
|
|
681
|
+
return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
|
|
603
682
|
}
|
|
604
|
-
return { x: cx + sx * (hw +
|
|
683
|
+
return { x: cx + sx * (hw + 8), y: cy + sy * (hh + 6) };
|
|
605
684
|
}
|
|
606
685
|
function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
607
686
|
const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
687
|
+
const from = arrowOptions.from || "note";
|
|
608
688
|
const anchored = Boolean(origin && origin.width > 0 && origin.height > 0);
|
|
609
|
-
let start = anchored ? arrowStartFromNote(origin, center) : annotationAnchor(
|
|
610
|
-
|
|
689
|
+
let start = anchored ? arrowStartFromNote(origin, center, from) : annotationAnchor(
|
|
690
|
+
from === "note" ? "top-right" : from,
|
|
611
691
|
rect,
|
|
612
692
|
38 + random() * 12
|
|
613
693
|
);
|
|
@@ -619,7 +699,7 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
619
699
|
const near = nearestPointOnRect(rect, start);
|
|
620
700
|
let outward = normalize({ x: start.x - near.x, y: start.y - near.y });
|
|
621
701
|
if (!outward.x && !outward.y) outward = { x: 0, y: -1 };
|
|
622
|
-
const gap =
|
|
702
|
+
const gap = TIP_GAP + random() * 5;
|
|
623
703
|
end = { x: near.x + outward.x * gap, y: near.y + outward.y * gap };
|
|
624
704
|
} else {
|
|
625
705
|
end = pointFromPosition(toPos, rect);
|
|
@@ -632,6 +712,35 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
632
712
|
}
|
|
633
713
|
return { start, end };
|
|
634
714
|
}
|
|
715
|
+
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
716
|
+
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
717
|
+
const spread = 0.46 + random() * 0.1;
|
|
718
|
+
for (const side of [-1, 1]) {
|
|
719
|
+
const armAngle = angle + spread * side;
|
|
720
|
+
const tail = {
|
|
721
|
+
x: tip.x - length * Math.cos(armAngle),
|
|
722
|
+
y: tip.y - length * Math.sin(armAngle)
|
|
723
|
+
};
|
|
724
|
+
appendPath(
|
|
725
|
+
svg,
|
|
726
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
727
|
+
steps: 2,
|
|
728
|
+
bow: 0.35 * side
|
|
729
|
+
}),
|
|
730
|
+
{ ...style, dashed: false }
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function boundsOf(points) {
|
|
735
|
+
const xs = points.map((p) => p.x);
|
|
736
|
+
const ys = points.map((p) => p.y);
|
|
737
|
+
return {
|
|
738
|
+
x: Math.min(...xs),
|
|
739
|
+
y: Math.min(...ys),
|
|
740
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
741
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
742
|
+
};
|
|
743
|
+
}
|
|
635
744
|
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
636
745
|
const random = createRandom(seed + 17);
|
|
637
746
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
@@ -653,13 +762,33 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
653
762
|
x: rect.x + rect.width / 2 - mid.x,
|
|
654
763
|
y: rect.y + rect.height / 2 - mid.y
|
|
655
764
|
};
|
|
656
|
-
const
|
|
765
|
+
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
766
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
|
|
657
767
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
658
768
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
659
769
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
660
770
|
const c2x = start.x + dx * 0.72 + perpendicular.x * bow * 0.85;
|
|
661
771
|
const c2y = start.y + dy * 0.72 + perpendicular.y * bow * 0.85;
|
|
662
772
|
hull.push({ x: c1x, y: c1y }, { x: c2x, y: c2y });
|
|
773
|
+
if (arrowOptions.style === "looped") {
|
|
774
|
+
const radius = clamp(distance * 0.11, 7, 17);
|
|
775
|
+
const points = loopedCurvePoints(
|
|
776
|
+
start,
|
|
777
|
+
{ x: c1x, y: c1y },
|
|
778
|
+
{ x: c2x, y: c2y },
|
|
779
|
+
end,
|
|
780
|
+
radius,
|
|
781
|
+
random
|
|
782
|
+
);
|
|
783
|
+
shaft = catmullRomPath(points, false);
|
|
784
|
+
hull.push(...points);
|
|
785
|
+
const last = points[points.length - 1];
|
|
786
|
+
const prior = points[points.length - 2] ?? start;
|
|
787
|
+
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
788
|
+
appendPath(svg, shaft, { ...style, dashed });
|
|
789
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
790
|
+
return { start, end, bounds: boundsOf(hull) };
|
|
791
|
+
}
|
|
663
792
|
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
664
793
|
if (style.roughness >= 1.1) {
|
|
665
794
|
const drift = () => (random() - 0.5) * 1.4;
|
|
@@ -674,31 +803,8 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
674
803
|
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
675
804
|
}
|
|
676
805
|
appendPath(svg, shaft, { ...style, dashed });
|
|
677
|
-
|
|
678
|
-
const
|
|
679
|
-
for (const side of [-1, 1]) {
|
|
680
|
-
const angle = tipAngle + spread * side;
|
|
681
|
-
const tail = {
|
|
682
|
-
x: end.x - headLength * Math.cos(angle),
|
|
683
|
-
y: end.y - headLength * Math.sin(angle)
|
|
684
|
-
};
|
|
685
|
-
appendPath(
|
|
686
|
-
svg,
|
|
687
|
-
roughLine(tail.x, tail.y, end.x, end.y, style.roughness * 0.5, random, {
|
|
688
|
-
steps: 2,
|
|
689
|
-
bow: 0.35 * side
|
|
690
|
-
}),
|
|
691
|
-
{ ...style, dashed: false }
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
const xs = hull.map((p) => p.x);
|
|
695
|
-
const ys = hull.map((p) => p.y);
|
|
696
|
-
const bounds = {
|
|
697
|
-
x: Math.min(...xs),
|
|
698
|
-
y: Math.min(...ys),
|
|
699
|
-
width: Math.max(...xs) - Math.min(...xs),
|
|
700
|
-
height: Math.max(...ys) - Math.min(...ys)
|
|
701
|
-
};
|
|
806
|
+
drawArrowHead(svg, end, tipAngle, style, random, distance);
|
|
807
|
+
const bounds = boundsOf(hull);
|
|
702
808
|
return { start, end, bounds };
|
|
703
809
|
}
|
|
704
810
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
@@ -736,11 +842,38 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
736
842
|
}
|
|
737
843
|
);
|
|
738
844
|
}
|
|
739
|
-
|
|
845
|
+
var DEFAULT_WORDS_PER_LINE = 2;
|
|
846
|
+
function wrapWords(text, perLine) {
|
|
847
|
+
const lines = [];
|
|
848
|
+
for (const paragraph of String(text).split(/\r?\n/)) {
|
|
849
|
+
const words = paragraph.trim().split(/\s+/).filter(Boolean);
|
|
850
|
+
if (!words.length) continue;
|
|
851
|
+
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
852
|
+
for (let i = 0; i < words.length; i += size) {
|
|
853
|
+
lines.push(words.slice(i, i + size).join(" "));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return lines.length ? lines : [String(text)];
|
|
857
|
+
}
|
|
858
|
+
function measureWidth(element, content, fontSize) {
|
|
859
|
+
try {
|
|
860
|
+
const box = element.getBBox();
|
|
861
|
+
if (box && box.width > 0) return box.width;
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
const measured = element.getComputedTextLength();
|
|
866
|
+
if (Number.isFinite(measured) && measured > 0) return measured;
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
return content.length * fontSize * 0.45;
|
|
870
|
+
}
|
|
871
|
+
function createNoteText(svg, text, style, options = {}) {
|
|
740
872
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
873
|
+
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
874
|
+
const lineHeight = fontSize * 1.05;
|
|
741
875
|
const group = document.createElementNS(SVG_NS, "g");
|
|
742
876
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
743
|
-
textEl.textContent = text;
|
|
744
877
|
textEl.setAttribute("x", "0");
|
|
745
878
|
textEl.setAttribute("y", "0");
|
|
746
879
|
textEl.setAttribute("fill", style.color);
|
|
@@ -751,31 +884,43 @@ function createNoteText(svg, text, style) {
|
|
|
751
884
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
752
885
|
textEl.setAttribute("text-anchor", "start");
|
|
753
886
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
887
|
+
const tspans = lines.map((line, index) => {
|
|
888
|
+
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
889
|
+
tspan.textContent = line;
|
|
890
|
+
tspan.setAttribute("x", "0");
|
|
891
|
+
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
892
|
+
textEl.appendChild(tspan);
|
|
893
|
+
return tspan;
|
|
894
|
+
});
|
|
754
895
|
group.appendChild(textEl);
|
|
755
896
|
svg.appendChild(group);
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
} catch {
|
|
761
|
-
}
|
|
762
|
-
if (!width) {
|
|
763
|
-
try {
|
|
764
|
-
const measured = textEl.getComputedTextLength();
|
|
765
|
-
if (Number.isFinite(measured) && measured > 0) width = measured;
|
|
766
|
-
} catch {
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
if (!width) width = text.length * fontSize * 0.45;
|
|
897
|
+
const lineWidths = tspans.map(
|
|
898
|
+
(tspan, index) => measureWidth(tspan, lines[index], fontSize)
|
|
899
|
+
);
|
|
900
|
+
const width = Math.max(...lineWidths);
|
|
770
901
|
const ascent = fontSize * 0.74;
|
|
771
902
|
const descent = fontSize * 0.26;
|
|
772
|
-
|
|
903
|
+
const height = (lines.length - 1) * lineHeight + ascent + descent;
|
|
904
|
+
return {
|
|
905
|
+
group,
|
|
906
|
+
textEl,
|
|
907
|
+
tspans,
|
|
908
|
+
lines,
|
|
909
|
+
lineWidths,
|
|
910
|
+
lineHeight,
|
|
911
|
+
width,
|
|
912
|
+
height,
|
|
913
|
+
ascent,
|
|
914
|
+
descent,
|
|
915
|
+
fontSize
|
|
916
|
+
};
|
|
773
917
|
}
|
|
774
918
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
775
919
|
const random = createRandom(seed + 55);
|
|
776
920
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
777
921
|
note.textEl.setAttribute("x", fmt(box.x));
|
|
778
922
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
923
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
|
|
779
924
|
const cx = box.x + box.width / 2;
|
|
780
925
|
const cy = box.y + box.height / 2;
|
|
781
926
|
note.group.setAttribute(
|
|
@@ -783,11 +928,12 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
783
928
|
`rotate(${tilt.toFixed(2)} ${fmt(cx)} ${fmt(cy)})`
|
|
784
929
|
);
|
|
785
930
|
if (options.underline !== false) {
|
|
931
|
+
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
786
932
|
drawUnderline(
|
|
787
933
|
note.group,
|
|
788
934
|
box.x + 1,
|
|
789
935
|
box.y + box.height + 2,
|
|
790
|
-
|
|
936
|
+
lastWidth - 2,
|
|
791
937
|
{
|
|
792
938
|
color: style.color,
|
|
793
939
|
strokeWidth: style.strokeWidth || 1.4,
|
|
@@ -819,25 +965,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
819
965
|
|
|
820
966
|
// src/annotations.js
|
|
821
967
|
var NOTE_GAP = 14;
|
|
822
|
-
function mirror(position, axis) {
|
|
823
|
-
if (axis === "x") {
|
|
824
|
-
if (position.includes("right")) return position.replace("right", "left");
|
|
825
|
-
if (position.includes("left")) return position.replace("left", "right");
|
|
826
|
-
return position;
|
|
827
|
-
}
|
|
828
|
-
if (position.includes("top")) return position.replace("top", "bottom");
|
|
829
|
-
if (position.includes("bottom")) return position.replace("bottom", "top");
|
|
830
|
-
return position;
|
|
831
|
-
}
|
|
832
|
-
function candidatePositions(position) {
|
|
833
|
-
const candidates = [
|
|
834
|
-
position,
|
|
835
|
-
mirror(position, "x"),
|
|
836
|
-
mirror(position, "y"),
|
|
837
|
-
mirror(mirror(position, "x"), "y")
|
|
838
|
-
];
|
|
839
|
-
return candidates.filter((value, index) => candidates.indexOf(value) === index);
|
|
840
|
-
}
|
|
841
968
|
function boxForPosition(rect, position, size, gap) {
|
|
842
969
|
const direction = directionFromPosition(position);
|
|
843
970
|
const guard = inflateRect(rect, gap);
|
|
@@ -856,75 +983,67 @@ function boxForPosition(rect, position, size, gap) {
|
|
|
856
983
|
}
|
|
857
984
|
return { x, y, width: size.width, height: size.height };
|
|
858
985
|
}
|
|
859
|
-
function
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
if (!best || penalty < best.penalty) {
|
|
866
|
-
best = { box, penalty, position: candidate };
|
|
867
|
-
}
|
|
868
|
-
});
|
|
869
|
-
return best;
|
|
870
|
-
}
|
|
871
|
-
function clampToViewport(box, guard, viewport) {
|
|
872
|
-
if (!viewport || viewport.width <= 0 || viewport.height <= 0) return box;
|
|
873
|
-
const shifted = {
|
|
874
|
-
...box,
|
|
875
|
-
x: clamp(
|
|
876
|
-
box.x,
|
|
877
|
-
viewport.x,
|
|
878
|
-
Math.max(viewport.x, viewport.x + viewport.width - box.width)
|
|
879
|
-
),
|
|
880
|
-
y: clamp(
|
|
881
|
-
box.y,
|
|
882
|
-
viewport.y,
|
|
883
|
-
Math.max(viewport.y, viewport.y + viewport.height - box.height)
|
|
884
|
-
)
|
|
885
|
-
};
|
|
886
|
-
if (intersectionArea(shifted, guard) > intersectionArea(box, guard)) {
|
|
887
|
-
return box;
|
|
986
|
+
function limitAgainstElement(box, guard, dx) {
|
|
987
|
+
const level = box.y < guard.y + guard.height && box.y + box.height > guard.y;
|
|
988
|
+
if (!level) return dx;
|
|
989
|
+
if (dx < 0) {
|
|
990
|
+
const room2 = box.x - (guard.x + guard.width);
|
|
991
|
+
return room2 <= 0 ? 0 : -Math.min(-dx, room2);
|
|
888
992
|
}
|
|
889
|
-
|
|
993
|
+
const room = guard.x - (box.x + box.width);
|
|
994
|
+
return room <= 0 ? 0 : Math.min(dx, room);
|
|
995
|
+
}
|
|
996
|
+
function nudgeIntoBand(box, guard, band) {
|
|
997
|
+
if (!band || band.width <= 0) return box;
|
|
998
|
+
const overhangLeft = band.x - box.x;
|
|
999
|
+
const overhangRight = box.x + box.width - (band.x + band.width);
|
|
1000
|
+
let dx = 0;
|
|
1001
|
+
if (overhangLeft > 0) dx = overhangLeft;
|
|
1002
|
+
else if (overhangRight > 0) dx = -overhangRight;
|
|
1003
|
+
if (!dx) return box;
|
|
1004
|
+
dx = limitAgainstElement(box, guard, dx);
|
|
1005
|
+
if (!dx) return box;
|
|
1006
|
+
return { ...box, x: box.x + dx };
|
|
890
1007
|
}
|
|
891
1008
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
892
1009
|
if (!noteOptions?.text) return null;
|
|
893
|
-
const
|
|
1010
|
+
const band = layout.band ?? null;
|
|
894
1011
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
895
1012
|
const random = createRandom(seed + 9);
|
|
896
|
-
const note = createNoteText(svg, noteOptions.text, style
|
|
1013
|
+
const note = createNoteText(svg, noteOptions.text, style, {
|
|
1014
|
+
wordsPerLine: noteOptions.wordsPerLine
|
|
1015
|
+
});
|
|
897
1016
|
const tilt = (random() - 0.45) * 7;
|
|
898
1017
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
899
1018
|
const size = {
|
|
900
1019
|
width: note.width,
|
|
901
|
-
height: note.
|
|
1020
|
+
height: note.height + (noteOptions.underline === false ? 0 : 5)
|
|
902
1021
|
};
|
|
903
1022
|
const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
|
|
904
|
-
const placed =
|
|
1023
|
+
const placed = boxForPosition(
|
|
905
1024
|
rect,
|
|
906
1025
|
position,
|
|
907
1026
|
{ width: size.width, height: size.height + tiltPad },
|
|
908
|
-
gap
|
|
909
|
-
viewport
|
|
1027
|
+
gap
|
|
910
1028
|
);
|
|
1029
|
+
const shifted = {
|
|
1030
|
+
...placed,
|
|
1031
|
+
x: placed.x + (Number(noteOptions.offset?.x) || 0),
|
|
1032
|
+
y: placed.y + (Number(noteOptions.offset?.y) || 0)
|
|
1033
|
+
};
|
|
911
1034
|
const guard = inflateRect(rect, gap * 0.7);
|
|
912
|
-
const outer =
|
|
913
|
-
{ ...placed.box, height: size.height + tiltPad },
|
|
914
|
-
guard,
|
|
915
|
-
viewport
|
|
916
|
-
);
|
|
1035
|
+
const outer = nudgeIntoBand(shifted, guard, band);
|
|
917
1036
|
const box = {
|
|
918
1037
|
x: outer.x,
|
|
919
1038
|
y: outer.y + tiltPad / 2,
|
|
920
1039
|
width: size.width,
|
|
921
|
-
height: note.
|
|
1040
|
+
height: note.height
|
|
922
1041
|
};
|
|
923
1042
|
placeNoteText(note, box, style, seed, {
|
|
924
1043
|
tilt,
|
|
925
1044
|
underline: noteOptions.underline !== false
|
|
926
1045
|
});
|
|
927
|
-
return { group: note.group, box: inflateRect(outer, 4), position
|
|
1046
|
+
return { group: note.group, box: inflateRect(outer, 4), position };
|
|
928
1047
|
}
|
|
929
1048
|
|
|
930
1049
|
// src/decorations.js
|
|
@@ -1342,14 +1461,11 @@ function ensureHandwrittenFont() {
|
|
|
1342
1461
|
|
|
1343
1462
|
// src/doodle.js
|
|
1344
1463
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1345
|
-
function
|
|
1464
|
+
function localBand(overlayRect, inset = 8) {
|
|
1346
1465
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1347
|
-
const height = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1348
1466
|
return {
|
|
1349
1467
|
x: -overlayRect.x + inset,
|
|
1350
|
-
|
|
1351
|
-
width: Math.max(0, width - inset * 2),
|
|
1352
|
-
height: Math.max(0, height - inset * 2)
|
|
1468
|
+
width: Math.max(0, width - inset * 2)
|
|
1353
1469
|
};
|
|
1354
1470
|
}
|
|
1355
1471
|
var DoodleOverlay = class {
|
|
@@ -1448,8 +1564,8 @@ var DoodleOverlay = class {
|
|
|
1448
1564
|
};
|
|
1449
1565
|
const noteGroups = [];
|
|
1450
1566
|
const noteLayout = {
|
|
1451
|
-
|
|
1452
|
-
gap: this.options.arrow ?
|
|
1567
|
+
band: localBand(overlayRect),
|
|
1568
|
+
gap: this.options.arrow ? 40 : 14
|
|
1453
1569
|
};
|
|
1454
1570
|
targets.forEach((target, index) => {
|
|
1455
1571
|
const absolute = absoluteRects[index];
|