@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/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 === true) {
67
- const from = merged.note && typeof merged.note === "object" && merged.note.position ? merged.note.position : "top-right";
68
- merged.arrow = { from, to: "edge", style: "curved" };
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
- function arrowStartFromNote(origin, target) {
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 * 0.72, y: cy + sy * (hh + 7) };
693
+ return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
608
694
  }
609
- return { x: cx + sx * (hw + 7), y: cy + sy * hh * 0.2 };
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
- arrowOptions.from || "top-right",
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 = 6 + random() * 3;
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 sign = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y > 0 ? -1 : 1;
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
- const headLength = 9 + style.strokeWidth * 1.4;
683
- const spread = 0.46 + random() * 0.1;
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
- function createNoteText(svg, text, style) {
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
- let width = 0;
762
- try {
763
- const box = textEl.getBBox();
764
- if (box && box.width > 0) width = box.width;
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
- return { group, textEl, width, ascent, descent, fontSize };
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
- box.width - 2,
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 placeNote(rect, position, size, gap, viewport) {
865
- const guard = inflateRect(rect, gap * 0.7);
866
- let best = null;
867
- candidatePositions(position).forEach((candidate, index) => {
868
- const box = boxForPosition(rect, candidate, size, gap);
869
- const penalty = intersectionArea(box, guard) * 6 + outsideArea(box, viewport) + index * 0.5;
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
- return shifted;
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 viewport = layout.viewport ?? null;
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.ascent + note.descent + (noteOptions.underline === false ? 0 : 5)
1032
+ height: note.height + (noteOptions.underline === false ? 0 : 5)
907
1033
  };
908
1034
  const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
909
- const placed = placeNote(
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 = clampToViewport(
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.ascent + note.descent
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: placed.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 localViewport(overlayRect, inset = 8) {
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
- y: -overlayRect.y + inset,
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
- viewport: localViewport(overlayRect),
1457
- gap: this.options.arrow ? 32 : 14
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];