@dcg-overseas/number-line 0.1.7 → 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 +263 -115
- package/dist/index.js +263 -115
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -489,6 +489,47 @@ 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
|
+
}
|
|
507
|
+
function computeHorizontalLayout(minVal, maxVal) {
|
|
508
|
+
const gutter = 16;
|
|
509
|
+
const charWidth = 8;
|
|
510
|
+
const leftLabel = String(Math.round(minVal));
|
|
511
|
+
const rightLabel = String(Math.round(maxVal));
|
|
512
|
+
return {
|
|
513
|
+
padLeft: gutter,
|
|
514
|
+
padRight: gutter,
|
|
515
|
+
labelInsetLeft: Math.max(8, Math.ceil(leftLabel.length * charWidth / 2) + 2),
|
|
516
|
+
labelInsetRight: Math.max(12, Math.ceil(rightLabel.length * charWidth / 2) + 4)
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function createValueToX(viewMin, viewMax, viewWidth, layout) {
|
|
520
|
+
const range = Math.max(viewMax - viewMin, 1e-6);
|
|
521
|
+
const usable = Math.max(viewWidth - layout.padLeft - layout.padRight, 1);
|
|
522
|
+
const innerUsable = Math.max(
|
|
523
|
+
usable - layout.labelInsetLeft - layout.labelInsetRight,
|
|
524
|
+
1
|
|
525
|
+
);
|
|
526
|
+
return (value) => layout.padLeft + layout.labelInsetLeft + (value - viewMin) / range * innerUsable;
|
|
527
|
+
}
|
|
528
|
+
function edgeTextAnchor(value, domainMin, domainMax) {
|
|
529
|
+
if (Math.abs(value - domainMin) < 1e-9) return "start";
|
|
530
|
+
if (Math.abs(value - domainMax) < 1e-9) return "end";
|
|
531
|
+
return "middle";
|
|
532
|
+
}
|
|
492
533
|
const ARC_COLORS = [
|
|
493
534
|
"#7c3aed",
|
|
494
535
|
"#0891b2",
|
|
@@ -496,6 +537,9 @@ const ARC_COLORS = [
|
|
|
496
537
|
"#be185d",
|
|
497
538
|
"#16a34a"
|
|
498
539
|
];
|
|
540
|
+
const ARC_RY_RATIO = 0.9;
|
|
541
|
+
const ARC_ANIMATION_STAGGER_MS = 400;
|
|
542
|
+
const ARC_ANIMATION_DRAW_MS = 500;
|
|
499
543
|
function gcd(a, b) {
|
|
500
544
|
let x = Math.max(1, Math.abs(Math.round(a)));
|
|
501
545
|
let y = Math.max(1, Math.abs(Math.round(b)));
|
|
@@ -504,11 +548,11 @@ function gcd(a, b) {
|
|
|
504
548
|
}
|
|
505
549
|
const AxisLayer = React.memo(function AxisLayer2({
|
|
506
550
|
min,
|
|
551
|
+
max,
|
|
507
552
|
viewMin,
|
|
508
553
|
viewMax,
|
|
509
554
|
axisY,
|
|
510
|
-
|
|
511
|
-
padRight,
|
|
555
|
+
horizontalLayout,
|
|
512
556
|
viewWidth,
|
|
513
557
|
containerWidth,
|
|
514
558
|
groupSize,
|
|
@@ -520,15 +564,17 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
520
564
|
}) {
|
|
521
565
|
const range = viewMax - viewMin;
|
|
522
566
|
if (range <= 0) return null;
|
|
523
|
-
const
|
|
524
|
-
const
|
|
567
|
+
const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
|
|
568
|
+
const axisStartX = toX(viewMin);
|
|
569
|
+
const axisEndX = toX(viewMax);
|
|
525
570
|
const step = Math.max(1, Math.round(tickStep));
|
|
526
571
|
const rawLabelStep = computeLabelStep(containerWidth, range);
|
|
527
572
|
const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
|
|
573
|
+
const minorStep = showMinorTicks ? computeMinorTickStep(step, containerWidth, range) : 0;
|
|
528
574
|
const MAX_TICKS = 500;
|
|
529
575
|
const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
|
|
530
576
|
const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
|
|
531
|
-
const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
|
|
577
|
+
const iterStep = showMinorTicks && minorStep > 0 ? Math.max(capStep, gcd(minorStep, capStep)) : Math.max(capStep, visibleStep);
|
|
532
578
|
const startVal = Math.floor(viewMin / iterStep) * iterStep;
|
|
533
579
|
const allTicks = [];
|
|
534
580
|
for (let v = startVal; v <= viewMax; v += iterStep) {
|
|
@@ -538,9 +584,9 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
538
584
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
539
585
|
"line",
|
|
540
586
|
{
|
|
541
|
-
x1:
|
|
587
|
+
x1: axisStartX - 4,
|
|
542
588
|
y1: axisY,
|
|
543
|
-
x2:
|
|
589
|
+
x2: axisEndX,
|
|
544
590
|
y2: axisY,
|
|
545
591
|
stroke: "#374151",
|
|
546
592
|
strokeWidth: 2
|
|
@@ -550,16 +596,18 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
550
596
|
const x = toX(v);
|
|
551
597
|
const offset = v - min;
|
|
552
598
|
const isMajor = offset % step === 0;
|
|
599
|
+
const isOnMinorGrid = minorStep > 0 && Math.abs(offset % minorStep) < 1e-9;
|
|
553
600
|
const maxGroupBoundary = min + groupCount * groupSize;
|
|
554
601
|
const isGroupBoundary = groupSize > 0 && offset % groupSize === 0 && v <= maxGroupBoundary;
|
|
555
|
-
const
|
|
602
|
+
const isMinorTick = isOnMinorGrid && !isMajor && !isGroupBoundary;
|
|
603
|
+
const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || isMinorTick && showMinorTicks;
|
|
556
604
|
if (!visible) return null;
|
|
557
605
|
const renderAsGroup = isGroupBoundary && showGroupTicks;
|
|
558
606
|
const renderAsMajor = !renderAsGroup && isMajor && showMajorTicks;
|
|
559
607
|
const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
|
|
560
608
|
const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : 4;
|
|
561
|
-
const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.
|
|
562
|
-
const color = renderAsGroup || renderAsMajor ? "#374151" : "#
|
|
609
|
+
const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.75;
|
|
610
|
+
const color = renderAsGroup || renderAsMajor ? "#374151" : "#6b7280";
|
|
563
611
|
return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
564
612
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
565
613
|
"line",
|
|
@@ -577,7 +625,7 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
577
625
|
{
|
|
578
626
|
x,
|
|
579
627
|
y: axisY + 22,
|
|
580
|
-
textAnchor:
|
|
628
|
+
textAnchor: edgeTextAnchor(v, min, max),
|
|
581
629
|
fontSize: 11,
|
|
582
630
|
fill: "#374151",
|
|
583
631
|
pointerEvents: "none",
|
|
@@ -589,6 +637,173 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
589
637
|
})
|
|
590
638
|
] });
|
|
591
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
|
+
}
|
|
592
807
|
const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
593
808
|
min,
|
|
594
809
|
max,
|
|
@@ -597,8 +812,7 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
597
812
|
groupSize,
|
|
598
813
|
groupCount,
|
|
599
814
|
axisY,
|
|
600
|
-
|
|
601
|
-
padRight,
|
|
815
|
+
horizontalLayout,
|
|
602
816
|
viewWidth,
|
|
603
817
|
arcMaxHeight,
|
|
604
818
|
tool,
|
|
@@ -608,10 +822,8 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
608
822
|
arcColors = ARC_COLORS
|
|
609
823
|
}) {
|
|
610
824
|
if (groupSize <= 0 || groupCount <= 0) return null;
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
const toX = (v) => padLeft + (v - viewMin) / range * usable;
|
|
614
|
-
const clipId = React.useId().replace(/:/g, "_") + "-arcs-clip";
|
|
825
|
+
const shouldAnimate = groupSize > 0 && groupCount > 0;
|
|
826
|
+
const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
|
|
615
827
|
const arcs = [];
|
|
616
828
|
for (let g = 0; g < groupCount; g++) {
|
|
617
829
|
if (deletedArcIndices.has(g)) continue;
|
|
@@ -622,105 +834,41 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
622
834
|
const x1 = toX(start);
|
|
623
835
|
const x2 = toX(end);
|
|
624
836
|
const rx = (x2 - x1) / 2;
|
|
625
|
-
const ry = Math.min(rx *
|
|
837
|
+
const ry = Math.min(rx * ARC_RY_RATIO, arcMaxHeight);
|
|
626
838
|
const color = arcColors[g % arcColors.length];
|
|
627
|
-
const arrowHeadH = 7;
|
|
628
|
-
const arrowHeadHW = 4;
|
|
629
839
|
const MIN_ARROW_SPAN = 18;
|
|
630
840
|
const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
|
|
631
841
|
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
632
842
|
const isSelected = selectedArcIndices.has(g);
|
|
633
843
|
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
634
844
|
arcs.push(
|
|
635
|
-
/* @__PURE__ */ jsxRuntime.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
pointerEvents: "none"
|
|
655
|
-
}
|
|
656
|
-
),
|
|
657
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
658
|
-
"path",
|
|
659
|
-
{
|
|
660
|
-
d: arcPath,
|
|
661
|
-
fill: "none",
|
|
662
|
-
stroke: "transparent",
|
|
663
|
-
strokeWidth: 20,
|
|
664
|
-
strokeLinecap: "round",
|
|
665
|
-
pointerEvents: "stroke",
|
|
666
|
-
style: { cursor: hitCursor },
|
|
667
|
-
onClick: (e) => {
|
|
668
|
-
if (tool === "eraser" || tool === "none") {
|
|
669
|
-
e.stopPropagation();
|
|
670
|
-
onArcClick(g, e);
|
|
671
|
-
}
|
|
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);
|
|
672
864
|
}
|
|
673
865
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
{
|
|
678
|
-
x: x2,
|
|
679
|
-
y: textY,
|
|
680
|
-
textAnchor: "middle",
|
|
681
|
-
fontSize: 10,
|
|
682
|
-
fill: isSelected ? "#1d4ed8" : color,
|
|
683
|
-
fontWeight: "600",
|
|
684
|
-
pointerEvents: "none",
|
|
685
|
-
style: { userSelect: "none", WebkitUserSelect: "none" },
|
|
686
|
-
children: end
|
|
687
|
-
}
|
|
688
|
-
),
|
|
689
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
690
|
-
"line",
|
|
691
|
-
{
|
|
692
|
-
x1: x2,
|
|
693
|
-
y1: textY + 4,
|
|
694
|
-
x2,
|
|
695
|
-
y2: axisY - arrowHeadH,
|
|
696
|
-
stroke: isSelected ? "#1d4ed8" : color,
|
|
697
|
-
strokeWidth: 1.5,
|
|
698
|
-
pointerEvents: "none"
|
|
699
|
-
}
|
|
700
|
-
),
|
|
701
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
702
|
-
"polygon",
|
|
703
|
-
{
|
|
704
|
-
points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
|
|
705
|
-
fill: isSelected ? "#1d4ed8" : color,
|
|
706
|
-
pointerEvents: "none"
|
|
707
|
-
}
|
|
708
|
-
)
|
|
709
|
-
] }, g)
|
|
866
|
+
},
|
|
867
|
+
g
|
|
868
|
+
)
|
|
710
869
|
);
|
|
711
870
|
}
|
|
712
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
713
|
-
/* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
714
|
-
"rect",
|
|
715
|
-
{
|
|
716
|
-
x: padLeft,
|
|
717
|
-
y: 0,
|
|
718
|
-
width: usable,
|
|
719
|
-
height: axisY + 1
|
|
720
|
-
}
|
|
721
|
-
) }) }),
|
|
722
|
-
/* @__PURE__ */ jsxRuntime.jsx("g", { clipPath: `url(#${clipId})`, children: arcs })
|
|
723
|
-
] });
|
|
871
|
+
return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
724
872
|
});
|
|
725
873
|
function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
|
|
726
874
|
return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
|
|
@@ -788,9 +936,6 @@ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick
|
|
|
788
936
|
)
|
|
789
937
|
] });
|
|
790
938
|
}
|
|
791
|
-
const PAD_LEFT_LEGACY = 30;
|
|
792
|
-
const PAD_RIGHT_LEGACY = 40;
|
|
793
|
-
const PAD_X = (PAD_LEFT_LEGACY + PAD_RIGHT_LEGACY) / 2;
|
|
794
939
|
const LABEL_SPACE_BELOW_AXIS = 42;
|
|
795
940
|
const LABEL_ABOVE_AXIS_SLOP = 32;
|
|
796
941
|
const ARC_TOP_PADDING = 16;
|
|
@@ -829,12 +974,16 @@ function NumberLine({ className }) {
|
|
|
829
974
|
} = useNumberLineContext();
|
|
830
975
|
const w = containerWidth;
|
|
831
976
|
const h = containerHeight;
|
|
977
|
+
const horizontalLayout = computeHorizontalLayout(min, max);
|
|
832
978
|
const range = Math.max(viewMax - viewMin, 1e-6);
|
|
833
|
-
const usable = Math.max(
|
|
979
|
+
const usable = Math.max(
|
|
980
|
+
w - horizontalLayout.padLeft - horizontalLayout.padRight - horizontalLayout.labelInsetLeft - horizontalLayout.labelInsetRight,
|
|
981
|
+
1
|
|
982
|
+
);
|
|
834
983
|
let ryTypical = MIN_ARC_HEIGHT * 0.5;
|
|
835
984
|
if (groupSize > 0 && groupCount > 0) {
|
|
836
985
|
const rx = usable * groupSize / range / 2;
|
|
837
|
-
ryTypical = Math.min(rx *
|
|
986
|
+
ryTypical = Math.min(rx * ARC_RY_RATIO, h * 0.48);
|
|
838
987
|
}
|
|
839
988
|
const minAxisY = MIN_ARC_HEIGHT + ARC_TOP_PADDING;
|
|
840
989
|
const maxAxisY = h - LABEL_SPACE_BELOW_AXIS;
|
|
@@ -986,7 +1135,8 @@ function NumberLine({ className }) {
|
|
|
986
1135
|
display: "block",
|
|
987
1136
|
outline: enableZoom && isZoomActive ? "2px solid rgba(59, 130, 246, 0.5)" : "none",
|
|
988
1137
|
cursor: svgCursor,
|
|
989
|
-
touchAction: "none"
|
|
1138
|
+
touchAction: "none",
|
|
1139
|
+
overflow: "visible"
|
|
990
1140
|
},
|
|
991
1141
|
onPointerDown: handlePointerDown,
|
|
992
1142
|
onPointerMove: handlePointerMove,
|
|
@@ -1006,8 +1156,7 @@ function NumberLine({ className }) {
|
|
|
1006
1156
|
viewMin,
|
|
1007
1157
|
viewMax,
|
|
1008
1158
|
axisY,
|
|
1009
|
-
|
|
1010
|
-
padRight: PAD_X,
|
|
1159
|
+
horizontalLayout,
|
|
1011
1160
|
viewWidth: w,
|
|
1012
1161
|
containerWidth: w,
|
|
1013
1162
|
groupSize,
|
|
@@ -1028,8 +1177,7 @@ function NumberLine({ className }) {
|
|
|
1028
1177
|
groupSize,
|
|
1029
1178
|
groupCount,
|
|
1030
1179
|
axisY,
|
|
1031
|
-
|
|
1032
|
-
padRight: PAD_X,
|
|
1180
|
+
horizontalLayout,
|
|
1033
1181
|
viewWidth: w,
|
|
1034
1182
|
arcMaxHeight,
|
|
1035
1183
|
tool,
|
package/dist/index.js
CHANGED
|
@@ -487,6 +487,47 @@ 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
|
+
}
|
|
505
|
+
function computeHorizontalLayout(minVal, maxVal) {
|
|
506
|
+
const gutter = 16;
|
|
507
|
+
const charWidth = 8;
|
|
508
|
+
const leftLabel = String(Math.round(minVal));
|
|
509
|
+
const rightLabel = String(Math.round(maxVal));
|
|
510
|
+
return {
|
|
511
|
+
padLeft: gutter,
|
|
512
|
+
padRight: gutter,
|
|
513
|
+
labelInsetLeft: Math.max(8, Math.ceil(leftLabel.length * charWidth / 2) + 2),
|
|
514
|
+
labelInsetRight: Math.max(12, Math.ceil(rightLabel.length * charWidth / 2) + 4)
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function createValueToX(viewMin, viewMax, viewWidth, layout) {
|
|
518
|
+
const range = Math.max(viewMax - viewMin, 1e-6);
|
|
519
|
+
const usable = Math.max(viewWidth - layout.padLeft - layout.padRight, 1);
|
|
520
|
+
const innerUsable = Math.max(
|
|
521
|
+
usable - layout.labelInsetLeft - layout.labelInsetRight,
|
|
522
|
+
1
|
|
523
|
+
);
|
|
524
|
+
return (value) => layout.padLeft + layout.labelInsetLeft + (value - viewMin) / range * innerUsable;
|
|
525
|
+
}
|
|
526
|
+
function edgeTextAnchor(value, domainMin, domainMax) {
|
|
527
|
+
if (Math.abs(value - domainMin) < 1e-9) return "start";
|
|
528
|
+
if (Math.abs(value - domainMax) < 1e-9) return "end";
|
|
529
|
+
return "middle";
|
|
530
|
+
}
|
|
490
531
|
const ARC_COLORS = [
|
|
491
532
|
"#7c3aed",
|
|
492
533
|
"#0891b2",
|
|
@@ -494,6 +535,9 @@ const ARC_COLORS = [
|
|
|
494
535
|
"#be185d",
|
|
495
536
|
"#16a34a"
|
|
496
537
|
];
|
|
538
|
+
const ARC_RY_RATIO = 0.9;
|
|
539
|
+
const ARC_ANIMATION_STAGGER_MS = 400;
|
|
540
|
+
const ARC_ANIMATION_DRAW_MS = 500;
|
|
497
541
|
function gcd(a, b) {
|
|
498
542
|
let x = Math.max(1, Math.abs(Math.round(a)));
|
|
499
543
|
let y = Math.max(1, Math.abs(Math.round(b)));
|
|
@@ -502,11 +546,11 @@ function gcd(a, b) {
|
|
|
502
546
|
}
|
|
503
547
|
const AxisLayer = React.memo(function AxisLayer2({
|
|
504
548
|
min,
|
|
549
|
+
max,
|
|
505
550
|
viewMin,
|
|
506
551
|
viewMax,
|
|
507
552
|
axisY,
|
|
508
|
-
|
|
509
|
-
padRight,
|
|
553
|
+
horizontalLayout,
|
|
510
554
|
viewWidth,
|
|
511
555
|
containerWidth,
|
|
512
556
|
groupSize,
|
|
@@ -518,15 +562,17 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
518
562
|
}) {
|
|
519
563
|
const range = viewMax - viewMin;
|
|
520
564
|
if (range <= 0) return null;
|
|
521
|
-
const
|
|
522
|
-
const
|
|
565
|
+
const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
|
|
566
|
+
const axisStartX = toX(viewMin);
|
|
567
|
+
const axisEndX = toX(viewMax);
|
|
523
568
|
const step = Math.max(1, Math.round(tickStep));
|
|
524
569
|
const rawLabelStep = computeLabelStep(containerWidth, range);
|
|
525
570
|
const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
|
|
571
|
+
const minorStep = showMinorTicks ? computeMinorTickStep(step, containerWidth, range) : 0;
|
|
526
572
|
const MAX_TICKS = 500;
|
|
527
573
|
const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
|
|
528
574
|
const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
|
|
529
|
-
const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
|
|
575
|
+
const iterStep = showMinorTicks && minorStep > 0 ? Math.max(capStep, gcd(minorStep, capStep)) : Math.max(capStep, visibleStep);
|
|
530
576
|
const startVal = Math.floor(viewMin / iterStep) * iterStep;
|
|
531
577
|
const allTicks = [];
|
|
532
578
|
for (let v = startVal; v <= viewMax; v += iterStep) {
|
|
@@ -536,9 +582,9 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
536
582
|
/* @__PURE__ */ jsx(
|
|
537
583
|
"line",
|
|
538
584
|
{
|
|
539
|
-
x1:
|
|
585
|
+
x1: axisStartX - 4,
|
|
540
586
|
y1: axisY,
|
|
541
|
-
x2:
|
|
587
|
+
x2: axisEndX,
|
|
542
588
|
y2: axisY,
|
|
543
589
|
stroke: "#374151",
|
|
544
590
|
strokeWidth: 2
|
|
@@ -548,16 +594,18 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
548
594
|
const x = toX(v);
|
|
549
595
|
const offset = v - min;
|
|
550
596
|
const isMajor = offset % step === 0;
|
|
597
|
+
const isOnMinorGrid = minorStep > 0 && Math.abs(offset % minorStep) < 1e-9;
|
|
551
598
|
const maxGroupBoundary = min + groupCount * groupSize;
|
|
552
599
|
const isGroupBoundary = groupSize > 0 && offset % groupSize === 0 && v <= maxGroupBoundary;
|
|
553
|
-
const
|
|
600
|
+
const isMinorTick = isOnMinorGrid && !isMajor && !isGroupBoundary;
|
|
601
|
+
const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || isMinorTick && showMinorTicks;
|
|
554
602
|
if (!visible) return null;
|
|
555
603
|
const renderAsGroup = isGroupBoundary && showGroupTicks;
|
|
556
604
|
const renderAsMajor = !renderAsGroup && isMajor && showMajorTicks;
|
|
557
605
|
const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
|
|
558
606
|
const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : 4;
|
|
559
|
-
const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.
|
|
560
|
-
const color = renderAsGroup || renderAsMajor ? "#374151" : "#
|
|
607
|
+
const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.75;
|
|
608
|
+
const color = renderAsGroup || renderAsMajor ? "#374151" : "#6b7280";
|
|
561
609
|
return /* @__PURE__ */ jsxs("g", { children: [
|
|
562
610
|
/* @__PURE__ */ jsx(
|
|
563
611
|
"line",
|
|
@@ -575,7 +623,7 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
575
623
|
{
|
|
576
624
|
x,
|
|
577
625
|
y: axisY + 22,
|
|
578
|
-
textAnchor:
|
|
626
|
+
textAnchor: edgeTextAnchor(v, min, max),
|
|
579
627
|
fontSize: 11,
|
|
580
628
|
fill: "#374151",
|
|
581
629
|
pointerEvents: "none",
|
|
@@ -587,6 +635,173 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
587
635
|
})
|
|
588
636
|
] });
|
|
589
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
|
+
}
|
|
590
805
|
const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
591
806
|
min,
|
|
592
807
|
max,
|
|
@@ -595,8 +810,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
595
810
|
groupSize,
|
|
596
811
|
groupCount,
|
|
597
812
|
axisY,
|
|
598
|
-
|
|
599
|
-
padRight,
|
|
813
|
+
horizontalLayout,
|
|
600
814
|
viewWidth,
|
|
601
815
|
arcMaxHeight,
|
|
602
816
|
tool,
|
|
@@ -606,10 +820,8 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
606
820
|
arcColors = ARC_COLORS
|
|
607
821
|
}) {
|
|
608
822
|
if (groupSize <= 0 || groupCount <= 0) return null;
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
const toX = (v) => padLeft + (v - viewMin) / range * usable;
|
|
612
|
-
const clipId = React.useId().replace(/:/g, "_") + "-arcs-clip";
|
|
823
|
+
const shouldAnimate = groupSize > 0 && groupCount > 0;
|
|
824
|
+
const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
|
|
613
825
|
const arcs = [];
|
|
614
826
|
for (let g = 0; g < groupCount; g++) {
|
|
615
827
|
if (deletedArcIndices.has(g)) continue;
|
|
@@ -620,105 +832,41 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
620
832
|
const x1 = toX(start);
|
|
621
833
|
const x2 = toX(end);
|
|
622
834
|
const rx = (x2 - x1) / 2;
|
|
623
|
-
const ry = Math.min(rx *
|
|
835
|
+
const ry = Math.min(rx * ARC_RY_RATIO, arcMaxHeight);
|
|
624
836
|
const color = arcColors[g % arcColors.length];
|
|
625
|
-
const arrowHeadH = 7;
|
|
626
|
-
const arrowHeadHW = 4;
|
|
627
837
|
const MIN_ARROW_SPAN = 18;
|
|
628
838
|
const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
|
|
629
839
|
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
630
840
|
const isSelected = selectedArcIndices.has(g);
|
|
631
841
|
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
632
842
|
arcs.push(
|
|
633
|
-
/* @__PURE__ */
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
pointerEvents: "none"
|
|
653
|
-
}
|
|
654
|
-
),
|
|
655
|
-
/* @__PURE__ */ jsx(
|
|
656
|
-
"path",
|
|
657
|
-
{
|
|
658
|
-
d: arcPath,
|
|
659
|
-
fill: "none",
|
|
660
|
-
stroke: "transparent",
|
|
661
|
-
strokeWidth: 20,
|
|
662
|
-
strokeLinecap: "round",
|
|
663
|
-
pointerEvents: "stroke",
|
|
664
|
-
style: { cursor: hitCursor },
|
|
665
|
-
onClick: (e) => {
|
|
666
|
-
if (tool === "eraser" || tool === "none") {
|
|
667
|
-
e.stopPropagation();
|
|
668
|
-
onArcClick(g, e);
|
|
669
|
-
}
|
|
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);
|
|
670
862
|
}
|
|
671
863
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
{
|
|
676
|
-
x: x2,
|
|
677
|
-
y: textY,
|
|
678
|
-
textAnchor: "middle",
|
|
679
|
-
fontSize: 10,
|
|
680
|
-
fill: isSelected ? "#1d4ed8" : color,
|
|
681
|
-
fontWeight: "600",
|
|
682
|
-
pointerEvents: "none",
|
|
683
|
-
style: { userSelect: "none", WebkitUserSelect: "none" },
|
|
684
|
-
children: end
|
|
685
|
-
}
|
|
686
|
-
),
|
|
687
|
-
/* @__PURE__ */ jsx(
|
|
688
|
-
"line",
|
|
689
|
-
{
|
|
690
|
-
x1: x2,
|
|
691
|
-
y1: textY + 4,
|
|
692
|
-
x2,
|
|
693
|
-
y2: axisY - arrowHeadH,
|
|
694
|
-
stroke: isSelected ? "#1d4ed8" : color,
|
|
695
|
-
strokeWidth: 1.5,
|
|
696
|
-
pointerEvents: "none"
|
|
697
|
-
}
|
|
698
|
-
),
|
|
699
|
-
/* @__PURE__ */ jsx(
|
|
700
|
-
"polygon",
|
|
701
|
-
{
|
|
702
|
-
points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
|
|
703
|
-
fill: isSelected ? "#1d4ed8" : color,
|
|
704
|
-
pointerEvents: "none"
|
|
705
|
-
}
|
|
706
|
-
)
|
|
707
|
-
] }, g)
|
|
864
|
+
},
|
|
865
|
+
g
|
|
866
|
+
)
|
|
708
867
|
);
|
|
709
868
|
}
|
|
710
|
-
return /* @__PURE__ */
|
|
711
|
-
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx(
|
|
712
|
-
"rect",
|
|
713
|
-
{
|
|
714
|
-
x: padLeft,
|
|
715
|
-
y: 0,
|
|
716
|
-
width: usable,
|
|
717
|
-
height: axisY + 1
|
|
718
|
-
}
|
|
719
|
-
) }) }),
|
|
720
|
-
/* @__PURE__ */ jsx("g", { clipPath: `url(#${clipId})`, children: arcs })
|
|
721
|
-
] });
|
|
869
|
+
return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
722
870
|
});
|
|
723
871
|
function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
|
|
724
872
|
return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
|
|
@@ -786,9 +934,6 @@ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick
|
|
|
786
934
|
)
|
|
787
935
|
] });
|
|
788
936
|
}
|
|
789
|
-
const PAD_LEFT_LEGACY = 30;
|
|
790
|
-
const PAD_RIGHT_LEGACY = 40;
|
|
791
|
-
const PAD_X = (PAD_LEFT_LEGACY + PAD_RIGHT_LEGACY) / 2;
|
|
792
937
|
const LABEL_SPACE_BELOW_AXIS = 42;
|
|
793
938
|
const LABEL_ABOVE_AXIS_SLOP = 32;
|
|
794
939
|
const ARC_TOP_PADDING = 16;
|
|
@@ -827,12 +972,16 @@ function NumberLine({ className }) {
|
|
|
827
972
|
} = useNumberLineContext();
|
|
828
973
|
const w = containerWidth;
|
|
829
974
|
const h = containerHeight;
|
|
975
|
+
const horizontalLayout = computeHorizontalLayout(min, max);
|
|
830
976
|
const range = Math.max(viewMax - viewMin, 1e-6);
|
|
831
|
-
const usable = Math.max(
|
|
977
|
+
const usable = Math.max(
|
|
978
|
+
w - horizontalLayout.padLeft - horizontalLayout.padRight - horizontalLayout.labelInsetLeft - horizontalLayout.labelInsetRight,
|
|
979
|
+
1
|
|
980
|
+
);
|
|
832
981
|
let ryTypical = MIN_ARC_HEIGHT * 0.5;
|
|
833
982
|
if (groupSize > 0 && groupCount > 0) {
|
|
834
983
|
const rx = usable * groupSize / range / 2;
|
|
835
|
-
ryTypical = Math.min(rx *
|
|
984
|
+
ryTypical = Math.min(rx * ARC_RY_RATIO, h * 0.48);
|
|
836
985
|
}
|
|
837
986
|
const minAxisY = MIN_ARC_HEIGHT + ARC_TOP_PADDING;
|
|
838
987
|
const maxAxisY = h - LABEL_SPACE_BELOW_AXIS;
|
|
@@ -984,7 +1133,8 @@ function NumberLine({ className }) {
|
|
|
984
1133
|
display: "block",
|
|
985
1134
|
outline: enableZoom && isZoomActive ? "2px solid rgba(59, 130, 246, 0.5)" : "none",
|
|
986
1135
|
cursor: svgCursor,
|
|
987
|
-
touchAction: "none"
|
|
1136
|
+
touchAction: "none",
|
|
1137
|
+
overflow: "visible"
|
|
988
1138
|
},
|
|
989
1139
|
onPointerDown: handlePointerDown,
|
|
990
1140
|
onPointerMove: handlePointerMove,
|
|
@@ -1004,8 +1154,7 @@ function NumberLine({ className }) {
|
|
|
1004
1154
|
viewMin,
|
|
1005
1155
|
viewMax,
|
|
1006
1156
|
axisY,
|
|
1007
|
-
|
|
1008
|
-
padRight: PAD_X,
|
|
1157
|
+
horizontalLayout,
|
|
1009
1158
|
viewWidth: w,
|
|
1010
1159
|
containerWidth: w,
|
|
1011
1160
|
groupSize,
|
|
@@ -1026,8 +1175,7 @@ function NumberLine({ className }) {
|
|
|
1026
1175
|
groupSize,
|
|
1027
1176
|
groupCount,
|
|
1028
1177
|
axisY,
|
|
1029
|
-
|
|
1030
|
-
padRight: PAD_X,
|
|
1178
|
+
horizontalLayout,
|
|
1031
1179
|
viewWidth: w,
|
|
1032
1180
|
arcMaxHeight,
|
|
1033
1181
|
tool,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcg-overseas/number-line",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
|
@@ -23,12 +23,6 @@
|
|
|
23
23
|
"dist"
|
|
24
24
|
],
|
|
25
25
|
"sideEffects": false,
|
|
26
|
-
"scripts": {
|
|
27
|
-
"build": "vite build",
|
|
28
|
-
"build:watch": "vite build --watch",
|
|
29
|
-
"typecheck": "tsc --noEmit",
|
|
30
|
-
"clean": "rm -rf dist *.tsbuildinfo"
|
|
31
|
-
},
|
|
32
26
|
"peerDependencies": {
|
|
33
27
|
"react": "^18.0.0",
|
|
34
28
|
"react-dom": "^18.0.0"
|
|
@@ -43,5 +37,11 @@
|
|
|
43
37
|
"publishConfig": {
|
|
44
38
|
"access": "public",
|
|
45
39
|
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "vite build",
|
|
43
|
+
"build:watch": "vite build --watch",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
}
|