@dcg-overseas/number-line 0.1.8 → 0.1.9

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.
Files changed (3) hide show
  1. package/dist/index.cjs +217 -81
  2. package/dist/index.js +217 -81
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -489,6 +489,21 @@ function computeLabelStep(containerWidth, range) {
489
489
  const candidates = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500];
490
490
  return candidates.find((c) => c >= raw) ?? candidates[candidates.length - 1];
491
491
  }
492
+ function computeMinorTickStep(majorStep, containerWidth, range) {
493
+ if (majorStep <= 1 || range <= 0 || containerWidth <= 0) return 0;
494
+ const pxPerMajor = containerWidth / range * majorStep;
495
+ const MIN_MINOR_PX = 8;
496
+ for (const divisions of [10, 5, 4, 2]) {
497
+ if (majorStep % divisions !== 0) continue;
498
+ const minorStep = majorStep / divisions;
499
+ if (minorStep < 1) continue;
500
+ if (pxPerMajor / divisions >= MIN_MINOR_PX) return minorStep;
501
+ }
502
+ if (majorStep >= 2 && pxPerMajor / Math.min(majorStep - 1, 5) >= MIN_MINOR_PX) {
503
+ return 1;
504
+ }
505
+ return 0;
506
+ }
492
507
  function computeHorizontalLayout(minVal, maxVal) {
493
508
  const gutter = 16;
494
509
  const charWidth = 8;
@@ -522,6 +537,9 @@ const ARC_COLORS = [
522
537
  "#be185d",
523
538
  "#16a34a"
524
539
  ];
540
+ const ARC_RY_RATIO = 0.9;
541
+ const ARC_ANIMATION_STAGGER_MS = 400;
542
+ const ARC_ANIMATION_DRAW_MS = 500;
525
543
  function gcd(a, b) {
526
544
  let x = Math.max(1, Math.abs(Math.round(a)));
527
545
  let y = Math.max(1, Math.abs(Math.round(b)));
@@ -552,10 +570,11 @@ const AxisLayer = React.memo(function AxisLayer2({
552
570
  const step = Math.max(1, Math.round(tickStep));
553
571
  const rawLabelStep = computeLabelStep(containerWidth, range);
554
572
  const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
573
+ const minorStep = showMinorTicks ? computeMinorTickStep(step, containerWidth, range) : 0;
555
574
  const MAX_TICKS = 500;
556
575
  const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
557
576
  const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
558
- const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
577
+ const iterStep = showMinorTicks && minorStep > 0 ? Math.max(capStep, gcd(minorStep, capStep)) : Math.max(capStep, visibleStep);
559
578
  const startVal = Math.floor(viewMin / iterStep) * iterStep;
560
579
  const allTicks = [];
561
580
  for (let v = startVal; v <= viewMax; v += iterStep) {
@@ -577,16 +596,18 @@ const AxisLayer = React.memo(function AxisLayer2({
577
596
  const x = toX(v);
578
597
  const offset = v - min;
579
598
  const isMajor = offset % step === 0;
599
+ const isOnMinorGrid = minorStep > 0 && Math.abs(offset % minorStep) < 1e-9;
580
600
  const maxGroupBoundary = min + groupCount * groupSize;
581
601
  const isGroupBoundary = groupSize > 0 && offset % groupSize === 0 && v <= maxGroupBoundary;
582
- const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || !isGroupBoundary && !isMajor && showMinorTicks;
602
+ const isMinorTick = isOnMinorGrid && !isMajor && !isGroupBoundary;
603
+ const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || isMinorTick && showMinorTicks;
583
604
  if (!visible) return null;
584
605
  const renderAsGroup = isGroupBoundary && showGroupTicks;
585
606
  const renderAsMajor = !renderAsGroup && isMajor && showMajorTicks;
586
607
  const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
587
608
  const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : 4;
588
- const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.5;
589
- const color = renderAsGroup || renderAsMajor ? "#374151" : "#9ca3af";
609
+ const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.75;
610
+ const color = renderAsGroup || renderAsMajor ? "#374151" : "#6b7280";
590
611
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
591
612
  /* @__PURE__ */ jsxRuntime.jsx(
592
613
  "line",
@@ -616,6 +637,173 @@ const AxisLayer = React.memo(function AxisLayer2({
616
637
  })
617
638
  ] });
618
639
  });
640
+ function AnimatedArc({
641
+ groupIndex,
642
+ arcPath,
643
+ color,
644
+ isSelected,
645
+ x2,
646
+ textY,
647
+ axisY,
648
+ end,
649
+ min,
650
+ max,
651
+ hitCursor,
652
+ shouldAnimate,
653
+ onArcClick
654
+ }) {
655
+ const measureRef = React.useRef(null);
656
+ const animatedRef = React.useRef(false);
657
+ const [pathLen, setPathLen] = React.useState(null);
658
+ const [dashOffset, setDashOffset] = React.useState(null);
659
+ const [isDrawing, setIsDrawing] = React.useState(false);
660
+ const [showLabels, setShowLabels] = React.useState(!shouldAnimate);
661
+ const [showHitArea, setShowHitArea] = React.useState(!shouldAnimate);
662
+ const strokeColor = isSelected ? "#1d4ed8" : color;
663
+ const arrowHeadH = 7;
664
+ const arrowHeadHW = 4;
665
+ React.useLayoutEffect(() => {
666
+ var _a;
667
+ const len = ((_a = measureRef.current) == null ? void 0 : _a.getTotalLength()) ?? 0;
668
+ if (len <= 0) return;
669
+ setPathLen(len);
670
+ if (!shouldAnimate || animatedRef.current) {
671
+ setDashOffset(0);
672
+ setShowLabels(true);
673
+ setShowHitArea(true);
674
+ setIsDrawing(false);
675
+ return;
676
+ }
677
+ setDashOffset(len);
678
+ setShowLabels(false);
679
+ setShowHitArea(false);
680
+ setIsDrawing(false);
681
+ const delay = groupIndex * ARC_ANIMATION_STAGGER_MS;
682
+ let drawTimer = 0;
683
+ let labelTimer = 0;
684
+ let completeTimer = 0;
685
+ const rafId = requestAnimationFrame(() => {
686
+ requestAnimationFrame(() => {
687
+ drawTimer = window.setTimeout(() => {
688
+ setIsDrawing(true);
689
+ setDashOffset(0);
690
+ }, delay);
691
+ labelTimer = window.setTimeout(
692
+ () => setShowLabels(true),
693
+ delay + ARC_ANIMATION_DRAW_MS
694
+ );
695
+ completeTimer = window.setTimeout(() => {
696
+ animatedRef.current = true;
697
+ setIsDrawing(false);
698
+ setShowHitArea(true);
699
+ }, delay + ARC_ANIMATION_DRAW_MS);
700
+ });
701
+ });
702
+ return () => {
703
+ cancelAnimationFrame(rafId);
704
+ window.clearTimeout(drawTimer);
705
+ window.clearTimeout(labelTimer);
706
+ window.clearTimeout(completeTimer);
707
+ };
708
+ }, [arcPath, groupIndex, shouldAnimate]);
709
+ const strokeTransition = isDrawing ? `stroke-dashoffset ${ARC_ANIMATION_DRAW_MS}ms ease-out` : "none";
710
+ const arcVisible = pathLen !== null && dashOffset !== null;
711
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
712
+ /* @__PURE__ */ jsxRuntime.jsx(
713
+ "path",
714
+ {
715
+ ref: measureRef,
716
+ d: arcPath,
717
+ fill: "none",
718
+ stroke: "none",
719
+ visibility: "hidden",
720
+ pointerEvents: "none"
721
+ }
722
+ ),
723
+ arcVisible && isSelected && /* @__PURE__ */ jsxRuntime.jsx(
724
+ "path",
725
+ {
726
+ d: arcPath,
727
+ fill: "none",
728
+ stroke: "rgba(0,0,0,0.15)",
729
+ strokeWidth: 14,
730
+ strokeLinecap: "round",
731
+ pointerEvents: "none"
732
+ }
733
+ ),
734
+ arcVisible && /* @__PURE__ */ jsxRuntime.jsx(
735
+ "path",
736
+ {
737
+ d: arcPath,
738
+ fill: "none",
739
+ stroke: strokeColor,
740
+ strokeWidth: isSelected ? 3 : 2,
741
+ strokeDasharray: pathLen,
742
+ strokeDashoffset: dashOffset,
743
+ style: { transition: strokeTransition },
744
+ pointerEvents: "none"
745
+ }
746
+ ),
747
+ arcVisible && showHitArea && /* @__PURE__ */ jsxRuntime.jsx(
748
+ "path",
749
+ {
750
+ d: arcPath,
751
+ fill: "none",
752
+ stroke: "transparent",
753
+ strokeWidth: 20,
754
+ strokeLinecap: "round",
755
+ pointerEvents: "stroke",
756
+ style: { cursor: hitCursor },
757
+ onClick: (e) => {
758
+ onArcClick(groupIndex, e);
759
+ }
760
+ }
761
+ ),
762
+ arcVisible && /* @__PURE__ */ jsxRuntime.jsxs(
763
+ "g",
764
+ {
765
+ opacity: showLabels ? 1 : 0,
766
+ style: { transition: "opacity 220ms ease-out" },
767
+ children: [
768
+ /* @__PURE__ */ jsxRuntime.jsx(
769
+ "text",
770
+ {
771
+ x: x2,
772
+ y: textY,
773
+ textAnchor: edgeTextAnchor(end, min, max),
774
+ fontSize: 10,
775
+ fill: strokeColor,
776
+ fontWeight: "600",
777
+ pointerEvents: "none",
778
+ style: { userSelect: "none", WebkitUserSelect: "none" },
779
+ children: end
780
+ }
781
+ ),
782
+ /* @__PURE__ */ jsxRuntime.jsx(
783
+ "line",
784
+ {
785
+ x1: x2,
786
+ y1: textY + 4,
787
+ x2,
788
+ y2: axisY - arrowHeadH,
789
+ stroke: strokeColor,
790
+ strokeWidth: 1.5,
791
+ pointerEvents: "none"
792
+ }
793
+ ),
794
+ /* @__PURE__ */ jsxRuntime.jsx(
795
+ "polygon",
796
+ {
797
+ points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
798
+ fill: strokeColor,
799
+ pointerEvents: "none"
800
+ }
801
+ )
802
+ ]
803
+ }
804
+ )
805
+ ] });
806
+ }
619
807
  const GroupArcsLayer = React.memo(function GroupArcsLayer2({
620
808
  min,
621
809
  max,
@@ -634,6 +822,7 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
634
822
  arcColors = ARC_COLORS
635
823
  }) {
636
824
  if (groupSize <= 0 || groupCount <= 0) return null;
825
+ const shouldAnimate = groupSize > 0 && groupCount > 0;
637
826
  const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
638
827
  const arcs = [];
639
828
  for (let g = 0; g < groupCount; g++) {
@@ -645,91 +834,38 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
645
834
  const x1 = toX(start);
646
835
  const x2 = toX(end);
647
836
  const rx = (x2 - x1) / 2;
648
- const ry = Math.min(rx * 0.7, arcMaxHeight);
837
+ const ry = Math.min(rx * ARC_RY_RATIO, arcMaxHeight);
649
838
  const color = arcColors[g % arcColors.length];
650
- const arrowHeadH = 7;
651
- const arrowHeadHW = 4;
652
839
  const MIN_ARROW_SPAN = 18;
653
840
  const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
654
841
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
655
842
  const isSelected = selectedArcIndices.has(g);
656
843
  const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
657
844
  arcs.push(
658
- /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
659
- isSelected && /* @__PURE__ */ jsxRuntime.jsx(
660
- "path",
661
- {
662
- d: arcPath,
663
- fill: "none",
664
- stroke: "rgba(0,0,0,0.15)",
665
- strokeWidth: 14,
666
- strokeLinecap: "round",
667
- pointerEvents: "none"
668
- }
669
- ),
670
- /* @__PURE__ */ jsxRuntime.jsx(
671
- "path",
672
- {
673
- d: arcPath,
674
- fill: "none",
675
- stroke: isSelected ? "#1d4ed8" : color,
676
- strokeWidth: isSelected ? 3 : 2,
677
- pointerEvents: "none"
678
- }
679
- ),
680
- /* @__PURE__ */ jsxRuntime.jsx(
681
- "path",
682
- {
683
- d: arcPath,
684
- fill: "none",
685
- stroke: "transparent",
686
- strokeWidth: 20,
687
- strokeLinecap: "round",
688
- pointerEvents: "stroke",
689
- style: { cursor: hitCursor },
690
- onClick: (e) => {
691
- if (tool === "eraser" || tool === "none") {
692
- e.stopPropagation();
693
- onArcClick(g, e);
694
- }
845
+ /* @__PURE__ */ jsxRuntime.jsx(
846
+ AnimatedArc,
847
+ {
848
+ groupIndex: g,
849
+ arcPath,
850
+ color,
851
+ isSelected,
852
+ x2,
853
+ textY,
854
+ axisY,
855
+ end,
856
+ min,
857
+ max,
858
+ hitCursor,
859
+ shouldAnimate,
860
+ onArcClick: (index, e) => {
861
+ if (tool === "eraser" || tool === "none") {
862
+ e.stopPropagation();
863
+ onArcClick(index, e);
695
864
  }
696
865
  }
697
- ),
698
- /* @__PURE__ */ jsxRuntime.jsx(
699
- "text",
700
- {
701
- x: x2,
702
- y: textY,
703
- textAnchor: edgeTextAnchor(end, min, max),
704
- fontSize: 10,
705
- fill: isSelected ? "#1d4ed8" : color,
706
- fontWeight: "600",
707
- pointerEvents: "none",
708
- style: { userSelect: "none", WebkitUserSelect: "none" },
709
- children: end
710
- }
711
- ),
712
- /* @__PURE__ */ jsxRuntime.jsx(
713
- "line",
714
- {
715
- x1: x2,
716
- y1: textY + 4,
717
- x2,
718
- y2: axisY - arrowHeadH,
719
- stroke: isSelected ? "#1d4ed8" : color,
720
- strokeWidth: 1.5,
721
- pointerEvents: "none"
722
- }
723
- ),
724
- /* @__PURE__ */ jsxRuntime.jsx(
725
- "polygon",
726
- {
727
- points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
728
- fill: isSelected ? "#1d4ed8" : color,
729
- pointerEvents: "none"
730
- }
731
- )
732
- ] }, g)
866
+ },
867
+ g
868
+ )
733
869
  );
734
870
  }
735
871
  return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
@@ -847,7 +983,7 @@ function NumberLine({ className }) {
847
983
  let ryTypical = MIN_ARC_HEIGHT * 0.5;
848
984
  if (groupSize > 0 && groupCount > 0) {
849
985
  const rx = usable * groupSize / range / 2;
850
- ryTypical = Math.min(rx * 0.7, h * 0.48);
986
+ ryTypical = Math.min(rx * ARC_RY_RATIO, h * 0.48);
851
987
  }
852
988
  const minAxisY = MIN_ARC_HEIGHT + ARC_TOP_PADDING;
853
989
  const maxAxisY = h - LABEL_SPACE_BELOW_AXIS;
package/dist/index.js CHANGED
@@ -487,6 +487,21 @@ function computeLabelStep(containerWidth, range) {
487
487
  const candidates = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500];
488
488
  return candidates.find((c) => c >= raw) ?? candidates[candidates.length - 1];
489
489
  }
490
+ function computeMinorTickStep(majorStep, containerWidth, range) {
491
+ if (majorStep <= 1 || range <= 0 || containerWidth <= 0) return 0;
492
+ const pxPerMajor = containerWidth / range * majorStep;
493
+ const MIN_MINOR_PX = 8;
494
+ for (const divisions of [10, 5, 4, 2]) {
495
+ if (majorStep % divisions !== 0) continue;
496
+ const minorStep = majorStep / divisions;
497
+ if (minorStep < 1) continue;
498
+ if (pxPerMajor / divisions >= MIN_MINOR_PX) return minorStep;
499
+ }
500
+ if (majorStep >= 2 && pxPerMajor / Math.min(majorStep - 1, 5) >= MIN_MINOR_PX) {
501
+ return 1;
502
+ }
503
+ return 0;
504
+ }
490
505
  function computeHorizontalLayout(minVal, maxVal) {
491
506
  const gutter = 16;
492
507
  const charWidth = 8;
@@ -520,6 +535,9 @@ const ARC_COLORS = [
520
535
  "#be185d",
521
536
  "#16a34a"
522
537
  ];
538
+ const ARC_RY_RATIO = 0.9;
539
+ const ARC_ANIMATION_STAGGER_MS = 400;
540
+ const ARC_ANIMATION_DRAW_MS = 500;
523
541
  function gcd(a, b) {
524
542
  let x = Math.max(1, Math.abs(Math.round(a)));
525
543
  let y = Math.max(1, Math.abs(Math.round(b)));
@@ -550,10 +568,11 @@ const AxisLayer = React.memo(function AxisLayer2({
550
568
  const step = Math.max(1, Math.round(tickStep));
551
569
  const rawLabelStep = computeLabelStep(containerWidth, range);
552
570
  const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
571
+ const minorStep = showMinorTicks ? computeMinorTickStep(step, containerWidth, range) : 0;
553
572
  const MAX_TICKS = 500;
554
573
  const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
555
574
  const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
556
- const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
575
+ const iterStep = showMinorTicks && minorStep > 0 ? Math.max(capStep, gcd(minorStep, capStep)) : Math.max(capStep, visibleStep);
557
576
  const startVal = Math.floor(viewMin / iterStep) * iterStep;
558
577
  const allTicks = [];
559
578
  for (let v = startVal; v <= viewMax; v += iterStep) {
@@ -575,16 +594,18 @@ const AxisLayer = React.memo(function AxisLayer2({
575
594
  const x = toX(v);
576
595
  const offset = v - min;
577
596
  const isMajor = offset % step === 0;
597
+ const isOnMinorGrid = minorStep > 0 && Math.abs(offset % minorStep) < 1e-9;
578
598
  const maxGroupBoundary = min + groupCount * groupSize;
579
599
  const isGroupBoundary = groupSize > 0 && offset % groupSize === 0 && v <= maxGroupBoundary;
580
- const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || !isGroupBoundary && !isMajor && showMinorTicks;
600
+ const isMinorTick = isOnMinorGrid && !isMajor && !isGroupBoundary;
601
+ const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || isMinorTick && showMinorTicks;
581
602
  if (!visible) return null;
582
603
  const renderAsGroup = isGroupBoundary && showGroupTicks;
583
604
  const renderAsMajor = !renderAsGroup && isMajor && showMajorTicks;
584
605
  const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
585
606
  const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : 4;
586
- const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.5;
587
- const color = renderAsGroup || renderAsMajor ? "#374151" : "#9ca3af";
607
+ const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.75;
608
+ const color = renderAsGroup || renderAsMajor ? "#374151" : "#6b7280";
588
609
  return /* @__PURE__ */ jsxs("g", { children: [
589
610
  /* @__PURE__ */ jsx(
590
611
  "line",
@@ -614,6 +635,173 @@ const AxisLayer = React.memo(function AxisLayer2({
614
635
  })
615
636
  ] });
616
637
  });
638
+ function AnimatedArc({
639
+ groupIndex,
640
+ arcPath,
641
+ color,
642
+ isSelected,
643
+ x2,
644
+ textY,
645
+ axisY,
646
+ end,
647
+ min,
648
+ max,
649
+ hitCursor,
650
+ shouldAnimate,
651
+ onArcClick
652
+ }) {
653
+ const measureRef = useRef(null);
654
+ const animatedRef = useRef(false);
655
+ const [pathLen, setPathLen] = useState(null);
656
+ const [dashOffset, setDashOffset] = useState(null);
657
+ const [isDrawing, setIsDrawing] = useState(false);
658
+ const [showLabels, setShowLabels] = useState(!shouldAnimate);
659
+ const [showHitArea, setShowHitArea] = useState(!shouldAnimate);
660
+ const strokeColor = isSelected ? "#1d4ed8" : color;
661
+ const arrowHeadH = 7;
662
+ const arrowHeadHW = 4;
663
+ useLayoutEffect(() => {
664
+ var _a;
665
+ const len = ((_a = measureRef.current) == null ? void 0 : _a.getTotalLength()) ?? 0;
666
+ if (len <= 0) return;
667
+ setPathLen(len);
668
+ if (!shouldAnimate || animatedRef.current) {
669
+ setDashOffset(0);
670
+ setShowLabels(true);
671
+ setShowHitArea(true);
672
+ setIsDrawing(false);
673
+ return;
674
+ }
675
+ setDashOffset(len);
676
+ setShowLabels(false);
677
+ setShowHitArea(false);
678
+ setIsDrawing(false);
679
+ const delay = groupIndex * ARC_ANIMATION_STAGGER_MS;
680
+ let drawTimer = 0;
681
+ let labelTimer = 0;
682
+ let completeTimer = 0;
683
+ const rafId = requestAnimationFrame(() => {
684
+ requestAnimationFrame(() => {
685
+ drawTimer = window.setTimeout(() => {
686
+ setIsDrawing(true);
687
+ setDashOffset(0);
688
+ }, delay);
689
+ labelTimer = window.setTimeout(
690
+ () => setShowLabels(true),
691
+ delay + ARC_ANIMATION_DRAW_MS
692
+ );
693
+ completeTimer = window.setTimeout(() => {
694
+ animatedRef.current = true;
695
+ setIsDrawing(false);
696
+ setShowHitArea(true);
697
+ }, delay + ARC_ANIMATION_DRAW_MS);
698
+ });
699
+ });
700
+ return () => {
701
+ cancelAnimationFrame(rafId);
702
+ window.clearTimeout(drawTimer);
703
+ window.clearTimeout(labelTimer);
704
+ window.clearTimeout(completeTimer);
705
+ };
706
+ }, [arcPath, groupIndex, shouldAnimate]);
707
+ const strokeTransition = isDrawing ? `stroke-dashoffset ${ARC_ANIMATION_DRAW_MS}ms ease-out` : "none";
708
+ const arcVisible = pathLen !== null && dashOffset !== null;
709
+ return /* @__PURE__ */ jsxs("g", { children: [
710
+ /* @__PURE__ */ jsx(
711
+ "path",
712
+ {
713
+ ref: measureRef,
714
+ d: arcPath,
715
+ fill: "none",
716
+ stroke: "none",
717
+ visibility: "hidden",
718
+ pointerEvents: "none"
719
+ }
720
+ ),
721
+ arcVisible && isSelected && /* @__PURE__ */ jsx(
722
+ "path",
723
+ {
724
+ d: arcPath,
725
+ fill: "none",
726
+ stroke: "rgba(0,0,0,0.15)",
727
+ strokeWidth: 14,
728
+ strokeLinecap: "round",
729
+ pointerEvents: "none"
730
+ }
731
+ ),
732
+ arcVisible && /* @__PURE__ */ jsx(
733
+ "path",
734
+ {
735
+ d: arcPath,
736
+ fill: "none",
737
+ stroke: strokeColor,
738
+ strokeWidth: isSelected ? 3 : 2,
739
+ strokeDasharray: pathLen,
740
+ strokeDashoffset: dashOffset,
741
+ style: { transition: strokeTransition },
742
+ pointerEvents: "none"
743
+ }
744
+ ),
745
+ arcVisible && showHitArea && /* @__PURE__ */ jsx(
746
+ "path",
747
+ {
748
+ d: arcPath,
749
+ fill: "none",
750
+ stroke: "transparent",
751
+ strokeWidth: 20,
752
+ strokeLinecap: "round",
753
+ pointerEvents: "stroke",
754
+ style: { cursor: hitCursor },
755
+ onClick: (e) => {
756
+ onArcClick(groupIndex, e);
757
+ }
758
+ }
759
+ ),
760
+ arcVisible && /* @__PURE__ */ jsxs(
761
+ "g",
762
+ {
763
+ opacity: showLabels ? 1 : 0,
764
+ style: { transition: "opacity 220ms ease-out" },
765
+ children: [
766
+ /* @__PURE__ */ jsx(
767
+ "text",
768
+ {
769
+ x: x2,
770
+ y: textY,
771
+ textAnchor: edgeTextAnchor(end, min, max),
772
+ fontSize: 10,
773
+ fill: strokeColor,
774
+ fontWeight: "600",
775
+ pointerEvents: "none",
776
+ style: { userSelect: "none", WebkitUserSelect: "none" },
777
+ children: end
778
+ }
779
+ ),
780
+ /* @__PURE__ */ jsx(
781
+ "line",
782
+ {
783
+ x1: x2,
784
+ y1: textY + 4,
785
+ x2,
786
+ y2: axisY - arrowHeadH,
787
+ stroke: strokeColor,
788
+ strokeWidth: 1.5,
789
+ pointerEvents: "none"
790
+ }
791
+ ),
792
+ /* @__PURE__ */ jsx(
793
+ "polygon",
794
+ {
795
+ points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
796
+ fill: strokeColor,
797
+ pointerEvents: "none"
798
+ }
799
+ )
800
+ ]
801
+ }
802
+ )
803
+ ] });
804
+ }
617
805
  const GroupArcsLayer = memo(function GroupArcsLayer2({
618
806
  min,
619
807
  max,
@@ -632,6 +820,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
632
820
  arcColors = ARC_COLORS
633
821
  }) {
634
822
  if (groupSize <= 0 || groupCount <= 0) return null;
823
+ const shouldAnimate = groupSize > 0 && groupCount > 0;
635
824
  const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
636
825
  const arcs = [];
637
826
  for (let g = 0; g < groupCount; g++) {
@@ -643,91 +832,38 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
643
832
  const x1 = toX(start);
644
833
  const x2 = toX(end);
645
834
  const rx = (x2 - x1) / 2;
646
- const ry = Math.min(rx * 0.7, arcMaxHeight);
835
+ const ry = Math.min(rx * ARC_RY_RATIO, arcMaxHeight);
647
836
  const color = arcColors[g % arcColors.length];
648
- const arrowHeadH = 7;
649
- const arrowHeadHW = 4;
650
837
  const MIN_ARROW_SPAN = 18;
651
838
  const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
652
839
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
653
840
  const isSelected = selectedArcIndices.has(g);
654
841
  const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
655
842
  arcs.push(
656
- /* @__PURE__ */ jsxs("g", { children: [
657
- isSelected && /* @__PURE__ */ jsx(
658
- "path",
659
- {
660
- d: arcPath,
661
- fill: "none",
662
- stroke: "rgba(0,0,0,0.15)",
663
- strokeWidth: 14,
664
- strokeLinecap: "round",
665
- pointerEvents: "none"
666
- }
667
- ),
668
- /* @__PURE__ */ jsx(
669
- "path",
670
- {
671
- d: arcPath,
672
- fill: "none",
673
- stroke: isSelected ? "#1d4ed8" : color,
674
- strokeWidth: isSelected ? 3 : 2,
675
- pointerEvents: "none"
676
- }
677
- ),
678
- /* @__PURE__ */ jsx(
679
- "path",
680
- {
681
- d: arcPath,
682
- fill: "none",
683
- stroke: "transparent",
684
- strokeWidth: 20,
685
- strokeLinecap: "round",
686
- pointerEvents: "stroke",
687
- style: { cursor: hitCursor },
688
- onClick: (e) => {
689
- if (tool === "eraser" || tool === "none") {
690
- e.stopPropagation();
691
- onArcClick(g, e);
692
- }
843
+ /* @__PURE__ */ jsx(
844
+ AnimatedArc,
845
+ {
846
+ groupIndex: g,
847
+ arcPath,
848
+ color,
849
+ isSelected,
850
+ x2,
851
+ textY,
852
+ axisY,
853
+ end,
854
+ min,
855
+ max,
856
+ hitCursor,
857
+ shouldAnimate,
858
+ onArcClick: (index, e) => {
859
+ if (tool === "eraser" || tool === "none") {
860
+ e.stopPropagation();
861
+ onArcClick(index, e);
693
862
  }
694
863
  }
695
- ),
696
- /* @__PURE__ */ jsx(
697
- "text",
698
- {
699
- x: x2,
700
- y: textY,
701
- textAnchor: edgeTextAnchor(end, min, max),
702
- fontSize: 10,
703
- fill: isSelected ? "#1d4ed8" : color,
704
- fontWeight: "600",
705
- pointerEvents: "none",
706
- style: { userSelect: "none", WebkitUserSelect: "none" },
707
- children: end
708
- }
709
- ),
710
- /* @__PURE__ */ jsx(
711
- "line",
712
- {
713
- x1: x2,
714
- y1: textY + 4,
715
- x2,
716
- y2: axisY - arrowHeadH,
717
- stroke: isSelected ? "#1d4ed8" : color,
718
- strokeWidth: 1.5,
719
- pointerEvents: "none"
720
- }
721
- ),
722
- /* @__PURE__ */ jsx(
723
- "polygon",
724
- {
725
- points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
726
- fill: isSelected ? "#1d4ed8" : color,
727
- pointerEvents: "none"
728
- }
729
- )
730
- ] }, g)
864
+ },
865
+ g
866
+ )
731
867
  );
732
868
  }
733
869
  return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
@@ -845,7 +981,7 @@ function NumberLine({ className }) {
845
981
  let ryTypical = MIN_ARC_HEIGHT * 0.5;
846
982
  if (groupSize > 0 && groupCount > 0) {
847
983
  const rx = usable * groupSize / range / 2;
848
- ryTypical = Math.min(rx * 0.7, h * 0.48);
984
+ ryTypical = Math.min(rx * ARC_RY_RATIO, h * 0.48);
849
985
  }
850
986
  const minAxisY = MIN_ARC_HEIGHT + ARC_TOP_PADDING;
851
987
  const maxAxisY = h - LABEL_SPACE_BELOW_AXIS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcg-overseas/number-line",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Interactive number line component with group arcs and freehand drawing",
5
5
  "type": "module",
6
6
  "license": "MIT",