@dcg-overseas/number-line 0.1.8 → 0.1.10

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 (4) hide show
  1. package/README.md +317 -317
  2. package/dist/index.cjs +224 -88
  3. package/dist/index.js +224 -88
  4. package/package.json +47 -47
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) {
@@ -565,28 +584,30 @@ const AxisLayer = React.memo(function AxisLayer2({
565
584
  /* @__PURE__ */ jsxRuntime.jsx(
566
585
  "line",
567
586
  {
568
- x1: axisStartX - 4,
587
+ x1: axisStartX - 10,
569
588
  y1: axisY,
570
- x2: axisEndX,
589
+ x2: axisEndX + 10,
571
590
  y2: axisY,
572
- stroke: "#374151",
573
- strokeWidth: 2
591
+ stroke: "#472E17",
592
+ strokeWidth: 4
574
593
  }
575
594
  ),
576
595
  allTicks.map((v) => {
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 ? 4 : renderAsMajor ? 2 : 1;
610
+ const color = renderAsGroup || renderAsMajor ? "#374151" : "#6b7280";
590
611
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
591
612
  /* @__PURE__ */ jsxRuntime.jsx(
592
613
  "line",
@@ -603,10 +624,10 @@ const AxisLayer = React.memo(function AxisLayer2({
603
624
  "text",
604
625
  {
605
626
  x,
606
- y: axisY + 22,
627
+ y: axisY + 32,
607
628
  textAnchor: edgeTextAnchor(v, min, max),
608
- fontSize: 11,
609
- fill: "#374151",
629
+ fontSize: 18,
630
+ fill: "#472E17",
610
631
  pointerEvents: "none",
611
632
  style: { userSelect: "none", WebkitUserSelect: "none" },
612
633
  children: v
@@ -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;