@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/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 === true) {
50
- const from = merged.note && typeof merged.note === "object" && merged.note.position ? merged.note.position : "top-right";
51
- merged.arrow = { from, to: "edge", style: "curved" };
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,
@@ -458,21 +472,14 @@ function isBroken(fraction, breaks) {
458
472
  function createBorderBreaks(perimeter, config, random) {
459
473
  if (!config || perimeter <= 0) return [];
460
474
  const settings = config === true ? {} : config;
461
- const count = Math.max(
462
- 1,
463
- Math.round(settings.count ?? 2 + random() * 3)
464
- );
475
+ const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
465
476
  const min = Math.max(2, settings.min ?? 6);
466
477
  const max = Math.max(min, settings.max ?? 30);
467
478
  const slice = perimeter / count;
468
479
  let budget = perimeter * 0.35;
469
480
  const breaks = [];
470
481
  for (let i = 0; i < count; i++) {
471
- const length = Math.min(
472
- min + random() * (max - min),
473
- slice * 0.7,
474
- budget
475
- );
482
+ const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
476
483
  if (length < 2) break;
477
484
  budget -= length;
478
485
  const start = i * slice + random() * Math.max(0, slice - length);
@@ -572,14 +579,86 @@ function drawBorder(svg, rect, options, seed) {
572
579
  });
573
580
  }
574
581
  }
582
+ function cubicAt(p0, c1, c2, p3, t) {
583
+ const u = 1 - t;
584
+ const a = u * u * u;
585
+ const b = 3 * u * u * t;
586
+ const c = 3 * u * t * t;
587
+ const d = t * t * t;
588
+ return {
589
+ x: a * p0.x + b * c1.x + c * c2.x + d * p3.x,
590
+ y: a * p0.y + b * c1.y + c * c2.y + d * p3.y
591
+ };
592
+ }
593
+ function cubicTangentAt(p0, c1, c2, p3, t) {
594
+ const u = 1 - t;
595
+ const a = 3 * u * u;
596
+ const b = 6 * u * t;
597
+ const c = 3 * t * t;
598
+ return {
599
+ x: a * (c1.x - p0.x) + b * (c2.x - c1.x) + c * (p3.x - c2.x),
600
+ y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
601
+ };
602
+ }
603
+ function loopedCurvePoints(start, c1, c2, end, radius, random) {
604
+ const at = (t) => cubicAt(start, c1, c2, end, t);
605
+ const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
606
+ const knot = 0.46 + random() * 0.12;
607
+ const points = [];
608
+ const lead = Math.max(2, Math.round(knot * 16));
609
+ for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
610
+ const pivot = at(knot);
611
+ const heading = tangentAt(knot);
612
+ const normal = { x: -heading.y, y: heading.x };
613
+ const side = random() < 0.5 ? 1 : -1;
614
+ const centre = {
615
+ x: pivot.x + normal.x * radius * side,
616
+ y: pivot.y + normal.y * radius * side
617
+ };
618
+ const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
619
+ const turns = Math.PI * 2 * (0.97 + random() * 0.08);
620
+ const steps = 18;
621
+ for (let i = 1; i <= steps; i++) {
622
+ const progress = i / steps;
623
+ const angle = from + turns * progress * side;
624
+ const drift = radius * 0.55 * progress;
625
+ points.push({
626
+ x: centre.x + Math.cos(angle) * radius + heading.x * drift,
627
+ y: centre.y + Math.sin(angle) * radius + heading.y * drift
628
+ });
629
+ }
630
+ const tail = 14;
631
+ for (let i = 1; i <= tail; i++) {
632
+ points.push(at(knot + (1 - knot) * (i / tail)));
633
+ }
634
+ return points;
635
+ }
575
636
  function normalize(vector) {
576
637
  const length = Math.hypot(vector.x, vector.y);
577
638
  if (!length) return { x: 0, y: 0 };
578
639
  return { x: vector.x / length, y: vector.y / length };
579
640
  }
580
- function arrowStartFromNote(origin, target) {
641
+ var NOTE_CLEARANCE = 10;
642
+ var TIP_GAP = 11;
643
+ function arrowStartFromNote(origin, target, from) {
581
644
  const cx = origin.x + origin.width / 2;
582
645
  const cy = origin.y + origin.height / 2;
646
+ if (from && from !== "note") {
647
+ const point = pointFromPosition(from, origin);
648
+ const away = directionFromPosition(from);
649
+ if (!away.x && !away.y) {
650
+ const edge = pointOnBoxToward(origin, target);
651
+ const outward = normalize({ x: edge.x - cx, y: edge.y - cy });
652
+ return {
653
+ x: edge.x + outward.x * NOTE_CLEARANCE,
654
+ y: edge.y + outward.y * NOTE_CLEARANCE
655
+ };
656
+ }
657
+ return {
658
+ x: point.x + away.x * NOTE_CLEARANCE,
659
+ y: point.y + away.y * NOTE_CLEARANCE
660
+ };
661
+ }
583
662
  const dx = target.x - cx;
584
663
  const dy = target.y - cy;
585
664
  const hw = origin.width / 2 || 1e-3;
@@ -587,15 +666,16 @@ function arrowStartFromNote(origin, target) {
587
666
  const sx = Math.sign(dx) || 1;
588
667
  const sy = Math.sign(dy) || 1;
589
668
  if (Math.abs(dy) / hh >= Math.abs(dx) / hw) {
590
- return { x: cx - sx * hw * 0.72, y: cy + sy * (hh + 7) };
669
+ return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
591
670
  }
592
- return { x: cx + sx * (hw + 7), y: cy + sy * hh * 0.2 };
671
+ return { x: cx + sx * (hw + 8), y: cy + sy * (hh + 6) };
593
672
  }
594
673
  function arrowEndpoints(rect, arrowOptions, random, origin) {
595
674
  const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
675
+ const from = arrowOptions.from || "note";
596
676
  const anchored = Boolean(origin && origin.width > 0 && origin.height > 0);
597
- let start = anchored ? arrowStartFromNote(origin, center) : annotationAnchor(
598
- arrowOptions.from || "top-right",
677
+ let start = anchored ? arrowStartFromNote(origin, center, from) : annotationAnchor(
678
+ from === "note" ? "top-right" : from,
599
679
  rect,
600
680
  38 + random() * 12
601
681
  );
@@ -607,7 +687,7 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
607
687
  const near = nearestPointOnRect(rect, start);
608
688
  let outward = normalize({ x: start.x - near.x, y: start.y - near.y });
609
689
  if (!outward.x && !outward.y) outward = { x: 0, y: -1 };
610
- const gap = 6 + random() * 3;
690
+ const gap = TIP_GAP + random() * 5;
611
691
  end = { x: near.x + outward.x * gap, y: near.y + outward.y * gap };
612
692
  } else {
613
693
  end = pointFromPosition(toPos, rect);
@@ -620,6 +700,35 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
620
700
  }
621
701
  return { start, end };
622
702
  }
703
+ function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
704
+ const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
705
+ const spread = 0.46 + random() * 0.1;
706
+ for (const side of [-1, 1]) {
707
+ const armAngle = angle + spread * side;
708
+ const tail = {
709
+ x: tip.x - length * Math.cos(armAngle),
710
+ y: tip.y - length * Math.sin(armAngle)
711
+ };
712
+ appendPath(
713
+ svg,
714
+ roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
715
+ steps: 2,
716
+ bow: 0.35 * side
717
+ }),
718
+ { ...style, dashed: false }
719
+ );
720
+ }
721
+ }
722
+ function boundsOf(points) {
723
+ const xs = points.map((p) => p.x);
724
+ const ys = points.map((p) => p.y);
725
+ return {
726
+ x: Math.min(...xs),
727
+ y: Math.min(...ys),
728
+ width: Math.max(...xs) - Math.min(...xs),
729
+ height: Math.max(...ys) - Math.min(...ys)
730
+ };
731
+ }
623
732
  function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
624
733
  const random = createRandom(seed + 17);
625
734
  const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
@@ -641,13 +750,33 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
641
750
  x: rect.x + rect.width / 2 - mid.x,
642
751
  y: rect.y + rect.height / 2 - mid.y
643
752
  };
644
- const sign = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y > 0 ? -1 : 1;
753
+ const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
754
+ const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
645
755
  const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
646
756
  const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
647
757
  const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
648
758
  const c2x = start.x + dx * 0.72 + perpendicular.x * bow * 0.85;
649
759
  const c2y = start.y + dy * 0.72 + perpendicular.y * bow * 0.85;
650
760
  hull.push({ x: c1x, y: c1y }, { x: c2x, y: c2y });
761
+ if (arrowOptions.style === "looped") {
762
+ const radius = clamp(distance * 0.11, 7, 17);
763
+ const points = loopedCurvePoints(
764
+ start,
765
+ { x: c1x, y: c1y },
766
+ { x: c2x, y: c2y },
767
+ end,
768
+ radius,
769
+ random
770
+ );
771
+ shaft = catmullRomPath(points, false);
772
+ hull.push(...points);
773
+ const last = points[points.length - 1];
774
+ const prior = points[points.length - 2] ?? start;
775
+ tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
776
+ appendPath(svg, shaft, { ...style, dashed });
777
+ drawArrowHead(svg, end, tipAngle, style, random, distance);
778
+ return { start, end, bounds: boundsOf(hull) };
779
+ }
651
780
  shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
652
781
  if (style.roughness >= 1.1) {
653
782
  const drift = () => (random() - 0.5) * 1.4;
@@ -662,31 +791,8 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
662
791
  tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
663
792
  }
664
793
  appendPath(svg, shaft, { ...style, dashed });
665
- const headLength = 9 + style.strokeWidth * 1.4;
666
- const spread = 0.46 + random() * 0.1;
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
- };
794
+ drawArrowHead(svg, end, tipAngle, style, random, distance);
795
+ const bounds = boundsOf(hull);
690
796
  return { start, end, bounds };
691
797
  }
692
798
  function drawUnderline(svg, x, y, width, style, random) {
@@ -724,11 +830,38 @@ function drawUnderline(svg, x, y, width, style, random) {
724
830
  }
725
831
  );
726
832
  }
727
- function createNoteText(svg, text, style) {
833
+ var DEFAULT_WORDS_PER_LINE = 2;
834
+ function wrapWords(text, perLine) {
835
+ const lines = [];
836
+ for (const paragraph of String(text).split(/\r?\n/)) {
837
+ const words = paragraph.trim().split(/\s+/).filter(Boolean);
838
+ if (!words.length) continue;
839
+ const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
840
+ for (let i = 0; i < words.length; i += size) {
841
+ lines.push(words.slice(i, i + size).join(" "));
842
+ }
843
+ }
844
+ return lines.length ? lines : [String(text)];
845
+ }
846
+ function measureWidth(element, content, fontSize) {
847
+ try {
848
+ const box = element.getBBox();
849
+ if (box && box.width > 0) return box.width;
850
+ } catch {
851
+ }
852
+ try {
853
+ const measured = element.getComputedTextLength();
854
+ if (Number.isFinite(measured) && measured > 0) return measured;
855
+ } catch {
856
+ }
857
+ return content.length * fontSize * 0.45;
858
+ }
859
+ function createNoteText(svg, text, style, options = {}) {
728
860
  const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
861
+ const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
862
+ const lineHeight = fontSize * 1.05;
729
863
  const group = document.createElementNS(SVG_NS, "g");
730
864
  const textEl = document.createElementNS(SVG_NS, "text");
731
- textEl.textContent = text;
732
865
  textEl.setAttribute("x", "0");
733
866
  textEl.setAttribute("y", "0");
734
867
  textEl.setAttribute("fill", style.color);
@@ -739,31 +872,43 @@ function createNoteText(svg, text, style) {
739
872
  textEl.setAttribute("letter-spacing", "0.4");
740
873
  textEl.setAttribute("text-anchor", "start");
741
874
  textEl.setAttribute("dominant-baseline", "auto");
875
+ const tspans = lines.map((line, index) => {
876
+ const tspan = document.createElementNS(SVG_NS, "tspan");
877
+ tspan.textContent = line;
878
+ tspan.setAttribute("x", "0");
879
+ tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
880
+ textEl.appendChild(tspan);
881
+ return tspan;
882
+ });
742
883
  group.appendChild(textEl);
743
884
  svg.appendChild(group);
744
- let width = 0;
745
- try {
746
- const box = textEl.getBBox();
747
- if (box && box.width > 0) width = box.width;
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;
885
+ const lineWidths = tspans.map(
886
+ (tspan, index) => measureWidth(tspan, lines[index], fontSize)
887
+ );
888
+ const width = Math.max(...lineWidths);
758
889
  const ascent = fontSize * 0.74;
759
890
  const descent = fontSize * 0.26;
760
- return { group, textEl, width, ascent, descent, fontSize };
891
+ const height = (lines.length - 1) * lineHeight + ascent + descent;
892
+ return {
893
+ group,
894
+ textEl,
895
+ tspans,
896
+ lines,
897
+ lineWidths,
898
+ lineHeight,
899
+ width,
900
+ height,
901
+ ascent,
902
+ descent,
903
+ fontSize
904
+ };
761
905
  }
762
906
  function placeNoteText(note, box, style, seed, options = {}) {
763
907
  const random = createRandom(seed + 55);
764
908
  const tilt = options.tilt ?? (random() - 0.5) * 7;
765
909
  note.textEl.setAttribute("x", fmt(box.x));
766
910
  note.textEl.setAttribute("y", fmt(box.y + note.ascent));
911
+ for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
767
912
  const cx = box.x + box.width / 2;
768
913
  const cy = box.y + box.height / 2;
769
914
  note.group.setAttribute(
@@ -771,11 +916,12 @@ function placeNoteText(note, box, style, seed, options = {}) {
771
916
  `rotate(${tilt.toFixed(2)} ${fmt(cx)} ${fmt(cy)})`
772
917
  );
773
918
  if (options.underline !== false) {
919
+ const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
774
920
  drawUnderline(
775
921
  note.group,
776
922
  box.x + 1,
777
923
  box.y + box.height + 2,
778
- box.width - 2,
924
+ lastWidth - 2,
779
925
  {
780
926
  color: style.color,
781
927
  strokeWidth: style.strokeWidth || 1.4,
@@ -807,25 +953,6 @@ function createOverlaySvg(left, top, width, height) {
807
953
 
808
954
  // src/annotations.js
809
955
  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
956
  function boxForPosition(rect, position, size, gap) {
830
957
  const direction = directionFromPosition(position);
831
958
  const guard = inflateRect(rect, gap);
@@ -844,75 +971,67 @@ function boxForPosition(rect, position, size, gap) {
844
971
  }
845
972
  return { x, y, width: size.width, height: size.height };
846
973
  }
847
- function placeNote(rect, position, size, gap, viewport) {
848
- const guard = inflateRect(rect, gap * 0.7);
849
- let best = null;
850
- candidatePositions(position).forEach((candidate, index) => {
851
- const box = boxForPosition(rect, candidate, size, gap);
852
- const penalty = intersectionArea(box, guard) * 6 + outsideArea(box, viewport) + index * 0.5;
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;
974
+ function limitAgainstElement(box, guard, dx) {
975
+ const level = box.y < guard.y + guard.height && box.y + box.height > guard.y;
976
+ if (!level) return dx;
977
+ if (dx < 0) {
978
+ const room2 = box.x - (guard.x + guard.width);
979
+ return room2 <= 0 ? 0 : -Math.min(-dx, room2);
876
980
  }
877
- return shifted;
981
+ const room = guard.x - (box.x + box.width);
982
+ return room <= 0 ? 0 : Math.min(dx, room);
983
+ }
984
+ function nudgeIntoBand(box, guard, band) {
985
+ if (!band || band.width <= 0) return box;
986
+ const overhangLeft = band.x - box.x;
987
+ const overhangRight = box.x + box.width - (band.x + band.width);
988
+ let dx = 0;
989
+ if (overhangLeft > 0) dx = overhangLeft;
990
+ else if (overhangRight > 0) dx = -overhangRight;
991
+ if (!dx) return box;
992
+ dx = limitAgainstElement(box, guard, dx);
993
+ if (!dx) return box;
994
+ return { ...box, x: box.x + dx };
878
995
  }
879
996
  function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
880
997
  if (!noteOptions?.text) return null;
881
- const viewport = layout.viewport ?? null;
998
+ const band = layout.band ?? null;
882
999
  const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
883
1000
  const random = createRandom(seed + 9);
884
- const note = createNoteText(svg, noteOptions.text, style);
1001
+ const note = createNoteText(svg, noteOptions.text, style, {
1002
+ wordsPerLine: noteOptions.wordsPerLine
1003
+ });
885
1004
  const tilt = (random() - 0.45) * 7;
886
1005
  const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
887
1006
  const size = {
888
1007
  width: note.width,
889
- height: note.ascent + note.descent + (noteOptions.underline === false ? 0 : 5)
1008
+ height: note.height + (noteOptions.underline === false ? 0 : 5)
890
1009
  };
891
1010
  const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
892
- const placed = placeNote(
1011
+ const placed = boxForPosition(
893
1012
  rect,
894
1013
  position,
895
1014
  { width: size.width, height: size.height + tiltPad },
896
- gap,
897
- viewport
1015
+ gap
898
1016
  );
1017
+ const shifted = {
1018
+ ...placed,
1019
+ x: placed.x + (Number(noteOptions.offset?.x) || 0),
1020
+ y: placed.y + (Number(noteOptions.offset?.y) || 0)
1021
+ };
899
1022
  const guard = inflateRect(rect, gap * 0.7);
900
- const outer = clampToViewport(
901
- { ...placed.box, height: size.height + tiltPad },
902
- guard,
903
- viewport
904
- );
1023
+ const outer = nudgeIntoBand(shifted, guard, band);
905
1024
  const box = {
906
1025
  x: outer.x,
907
1026
  y: outer.y + tiltPad / 2,
908
1027
  width: size.width,
909
- height: note.ascent + note.descent
1028
+ height: note.height
910
1029
  };
911
1030
  placeNoteText(note, box, style, seed, {
912
1031
  tilt,
913
1032
  underline: noteOptions.underline !== false
914
1033
  });
915
- return { group: note.group, box: inflateRect(outer, 4), position: placed.position };
1034
+ return { group: note.group, box: inflateRect(outer, 4), position };
916
1035
  }
917
1036
 
918
1037
  // src/decorations.js
@@ -1330,14 +1449,11 @@ function ensureHandwrittenFont() {
1330
1449
 
1331
1450
  // src/doodle.js
1332
1451
  var instances = /* @__PURE__ */ new WeakMap();
1333
- function localViewport(overlayRect, inset = 8) {
1452
+ function localBand(overlayRect, inset = 8) {
1334
1453
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1335
- const height = window.innerHeight || document.documentElement.clientHeight || 0;
1336
1454
  return {
1337
1455
  x: -overlayRect.x + inset,
1338
- y: -overlayRect.y + inset,
1339
- width: Math.max(0, width - inset * 2),
1340
- height: Math.max(0, height - inset * 2)
1456
+ width: Math.max(0, width - inset * 2)
1341
1457
  };
1342
1458
  }
1343
1459
  var DoodleOverlay = class {
@@ -1436,8 +1552,8 @@ var DoodleOverlay = class {
1436
1552
  };
1437
1553
  const noteGroups = [];
1438
1554
  const noteLayout = {
1439
- viewport: localViewport(overlayRect),
1440
- gap: this.options.arrow ? 32 : 14
1555
+ band: localBand(overlayRect),
1556
+ gap: this.options.arrow ? 40 : 14
1441
1557
  };
1442
1558
  targets.forEach((target, index) => {
1443
1559
  const absolute = absoluteRects[index];
@@ -1655,7 +1771,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
1655
1771
  ref: mergeRefs(innerRef, forwardedRef),
1656
1772
  className,
1657
1773
  // inline-flex shrink-wraps the child so the doodle hugs the component
1658
- // rather than a full-width block. Flex specifically — inline-block would
1774
+ // rather than a full-width block. Flex specifically - inline-block would
1659
1775
  // add baseline leading under an inline-level child like a <button>,
1660
1776
  // leaving the frame taller than what it wraps. Override via `style`/`as`.
1661
1777
  style: { display: "inline-flex", ...style }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aznabee/freehand-ui",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Wrap any existing web element with a beautiful, responsive, hand-drawn doodle layer",
5
5
  "type": "module",
6
6
  "main": "./dist/freehand-ui.cjs",