@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.
- package/README.md +317 -317
- package/dist/index.cjs +224 -88
- package/dist/index.js +224 -88
- 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 -
|
|
587
|
+
x1: axisStartX - 10,
|
|
569
588
|
y1: axisY,
|
|
570
|
-
x2: axisEndX,
|
|
589
|
+
x2: axisEndX + 10,
|
|
571
590
|
y2: axisY,
|
|
572
|
-
stroke: "#
|
|
573
|
-
strokeWidth:
|
|
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
|
|
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 ?
|
|
589
|
-
const color = renderAsGroup || renderAsMajor ? "#374151" : "#
|
|
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 +
|
|
627
|
+
y: axisY + 32,
|
|
607
628
|
textAnchor: edgeTextAnchor(v, min, max),
|
|
608
|
-
fontSize:
|
|
609
|
-
fill: "#
|
|
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 *
|
|
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.
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
699
|
-
|
|
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 *
|
|
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;
|