@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.
- package/dist/index.cjs +217 -81
- package/dist/index.js +217 -81
- 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
|
|
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.
|
|
589
|
-
const color = renderAsGroup || renderAsMajor ? "#374151" : "#
|
|
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 *
|
|
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;
|
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
|
|
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.
|
|
587
|
-
const color = renderAsGroup || renderAsMajor ? "#374151" : "#
|
|
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 *
|
|
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__ */
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
697
|
-
|
|
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 *
|
|
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;
|