@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.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) {
@@ -563,28 +582,30 @@ const AxisLayer = React.memo(function AxisLayer2({
563
582
  /* @__PURE__ */ jsx(
564
583
  "line",
565
584
  {
566
- x1: axisStartX - 4,
585
+ x1: axisStartX - 10,
567
586
  y1: axisY,
568
- x2: axisEndX,
587
+ x2: axisEndX + 10,
569
588
  y2: axisY,
570
- stroke: "#374151",
571
- strokeWidth: 2
589
+ stroke: "#472E17",
590
+ strokeWidth: 4
572
591
  }
573
592
  ),
574
593
  allTicks.map((v) => {
575
594
  const x = toX(v);
576
595
  const offset = v - min;
577
596
  const isMajor = offset % step === 0;
597
+ const isOnMinorGrid = minorStep > 0 && Math.abs(offset % minorStep) < 1e-9;
578
598
  const maxGroupBoundary = min + groupCount * groupSize;
579
599
  const isGroupBoundary = groupSize > 0 && offset % groupSize === 0 && v <= maxGroupBoundary;
580
- const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || !isGroupBoundary && !isMajor && showMinorTicks;
600
+ const isMinorTick = isOnMinorGrid && !isMajor && !isGroupBoundary;
601
+ const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || isMinorTick && showMinorTicks;
581
602
  if (!visible) return null;
582
603
  const renderAsGroup = isGroupBoundary && showGroupTicks;
583
604
  const renderAsMajor = !renderAsGroup && isMajor && showMajorTicks;
584
605
  const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
585
606
  const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : 4;
586
- const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.5;
587
- const color = renderAsGroup || renderAsMajor ? "#374151" : "#9ca3af";
607
+ const strokeW = renderAsGroup ? 4 : renderAsMajor ? 2 : 1;
608
+ const color = renderAsGroup || renderAsMajor ? "#374151" : "#6b7280";
588
609
  return /* @__PURE__ */ jsxs("g", { children: [
589
610
  /* @__PURE__ */ jsx(
590
611
  "line",
@@ -601,10 +622,10 @@ const AxisLayer = React.memo(function AxisLayer2({
601
622
  "text",
602
623
  {
603
624
  x,
604
- y: axisY + 22,
625
+ y: axisY + 32,
605
626
  textAnchor: edgeTextAnchor(v, min, max),
606
- fontSize: 11,
607
- fill: "#374151",
627
+ fontSize: 18,
628
+ fill: "#472E17",
608
629
  pointerEvents: "none",
609
630
  style: { userSelect: "none", WebkitUserSelect: "none" },
610
631
  children: v
@@ -614,6 +635,173 @@ const AxisLayer = React.memo(function AxisLayer2({
614
635
  })
615
636
  ] });
616
637
  });
638
+ function AnimatedArc({
639
+ groupIndex,
640
+ arcPath,
641
+ color,
642
+ isSelected,
643
+ x2,
644
+ textY,
645
+ axisY,
646
+ end,
647
+ min,
648
+ max,
649
+ hitCursor,
650
+ shouldAnimate,
651
+ onArcClick
652
+ }) {
653
+ const measureRef = useRef(null);
654
+ const animatedRef = useRef(false);
655
+ const [pathLen, setPathLen] = useState(null);
656
+ const [dashOffset, setDashOffset] = useState(null);
657
+ const [isDrawing, setIsDrawing] = useState(false);
658
+ const [showLabels, setShowLabels] = useState(!shouldAnimate);
659
+ const [showHitArea, setShowHitArea] = useState(!shouldAnimate);
660
+ const strokeColor = isSelected ? "#1d4ed8" : color;
661
+ const arrowHeadH = 7;
662
+ const arrowHeadHW = 4;
663
+ useLayoutEffect(() => {
664
+ var _a;
665
+ const len = ((_a = measureRef.current) == null ? void 0 : _a.getTotalLength()) ?? 0;
666
+ if (len <= 0) return;
667
+ setPathLen(len);
668
+ if (!shouldAnimate || animatedRef.current) {
669
+ setDashOffset(0);
670
+ setShowLabels(true);
671
+ setShowHitArea(true);
672
+ setIsDrawing(false);
673
+ return;
674
+ }
675
+ setDashOffset(len);
676
+ setShowLabels(false);
677
+ setShowHitArea(false);
678
+ setIsDrawing(false);
679
+ const delay = groupIndex * ARC_ANIMATION_STAGGER_MS;
680
+ let drawTimer = 0;
681
+ let labelTimer = 0;
682
+ let completeTimer = 0;
683
+ const rafId = requestAnimationFrame(() => {
684
+ requestAnimationFrame(() => {
685
+ drawTimer = window.setTimeout(() => {
686
+ setIsDrawing(true);
687
+ setDashOffset(0);
688
+ }, delay);
689
+ labelTimer = window.setTimeout(
690
+ () => setShowLabels(true),
691
+ delay + ARC_ANIMATION_DRAW_MS
692
+ );
693
+ completeTimer = window.setTimeout(() => {
694
+ animatedRef.current = true;
695
+ setIsDrawing(false);
696
+ setShowHitArea(true);
697
+ }, delay + ARC_ANIMATION_DRAW_MS);
698
+ });
699
+ });
700
+ return () => {
701
+ cancelAnimationFrame(rafId);
702
+ window.clearTimeout(drawTimer);
703
+ window.clearTimeout(labelTimer);
704
+ window.clearTimeout(completeTimer);
705
+ };
706
+ }, [arcPath, groupIndex, shouldAnimate]);
707
+ const strokeTransition = isDrawing ? `stroke-dashoffset ${ARC_ANIMATION_DRAW_MS}ms ease-out` : "none";
708
+ const arcVisible = pathLen !== null && dashOffset !== null;
709
+ return /* @__PURE__ */ jsxs("g", { children: [
710
+ /* @__PURE__ */ jsx(
711
+ "path",
712
+ {
713
+ ref: measureRef,
714
+ d: arcPath,
715
+ fill: "none",
716
+ stroke: "none",
717
+ visibility: "hidden",
718
+ pointerEvents: "none"
719
+ }
720
+ ),
721
+ arcVisible && isSelected && /* @__PURE__ */ jsx(
722
+ "path",
723
+ {
724
+ d: arcPath,
725
+ fill: "none",
726
+ stroke: "rgba(0,0,0,0.15)",
727
+ strokeWidth: 14,
728
+ strokeLinecap: "round",
729
+ pointerEvents: "none"
730
+ }
731
+ ),
732
+ arcVisible && /* @__PURE__ */ jsx(
733
+ "path",
734
+ {
735
+ d: arcPath,
736
+ fill: "none",
737
+ stroke: strokeColor,
738
+ strokeWidth: isSelected ? 3 : 2,
739
+ strokeDasharray: pathLen,
740
+ strokeDashoffset: dashOffset,
741
+ style: { transition: strokeTransition },
742
+ pointerEvents: "none"
743
+ }
744
+ ),
745
+ arcVisible && showHitArea && /* @__PURE__ */ jsx(
746
+ "path",
747
+ {
748
+ d: arcPath,
749
+ fill: "none",
750
+ stroke: "transparent",
751
+ strokeWidth: 20,
752
+ strokeLinecap: "round",
753
+ pointerEvents: "stroke",
754
+ style: { cursor: hitCursor },
755
+ onClick: (e) => {
756
+ onArcClick(groupIndex, e);
757
+ }
758
+ }
759
+ ),
760
+ arcVisible && /* @__PURE__ */ jsxs(
761
+ "g",
762
+ {
763
+ opacity: showLabels ? 1 : 0,
764
+ style: { transition: "opacity 220ms ease-out" },
765
+ children: [
766
+ /* @__PURE__ */ jsx(
767
+ "text",
768
+ {
769
+ x: x2,
770
+ y: textY,
771
+ textAnchor: edgeTextAnchor(end, min, max),
772
+ fontSize: 10,
773
+ fill: strokeColor,
774
+ fontWeight: "600",
775
+ pointerEvents: "none",
776
+ style: { userSelect: "none", WebkitUserSelect: "none" },
777
+ children: end
778
+ }
779
+ ),
780
+ /* @__PURE__ */ jsx(
781
+ "line",
782
+ {
783
+ x1: x2,
784
+ y1: textY + 4,
785
+ x2,
786
+ y2: axisY - arrowHeadH,
787
+ stroke: strokeColor,
788
+ strokeWidth: 1.5,
789
+ pointerEvents: "none"
790
+ }
791
+ ),
792
+ /* @__PURE__ */ jsx(
793
+ "polygon",
794
+ {
795
+ points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
796
+ fill: strokeColor,
797
+ pointerEvents: "none"
798
+ }
799
+ )
800
+ ]
801
+ }
802
+ )
803
+ ] });
804
+ }
617
805
  const GroupArcsLayer = memo(function GroupArcsLayer2({
618
806
  min,
619
807
  max,
@@ -632,6 +820,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
632
820
  arcColors = ARC_COLORS
633
821
  }) {
634
822
  if (groupSize <= 0 || groupCount <= 0) return null;
823
+ const shouldAnimate = groupSize > 0 && groupCount > 0;
635
824
  const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
636
825
  const arcs = [];
637
826
  for (let g = 0; g < groupCount; g++) {
@@ -643,91 +832,38 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
643
832
  const x1 = toX(start);
644
833
  const x2 = toX(end);
645
834
  const rx = (x2 - x1) / 2;
646
- const ry = Math.min(rx * 0.7, arcMaxHeight);
835
+ const ry = Math.min(rx * ARC_RY_RATIO, arcMaxHeight);
647
836
  const color = arcColors[g % arcColors.length];
648
- const arrowHeadH = 7;
649
- const arrowHeadHW = 4;
650
837
  const MIN_ARROW_SPAN = 18;
651
838
  const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
652
839
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
653
840
  const isSelected = selectedArcIndices.has(g);
654
841
  const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
655
842
  arcs.push(
656
- /* @__PURE__ */ jsxs("g", { children: [
657
- isSelected && /* @__PURE__ */ jsx(
658
- "path",
659
- {
660
- d: arcPath,
661
- fill: "none",
662
- stroke: "rgba(0,0,0,0.15)",
663
- strokeWidth: 14,
664
- strokeLinecap: "round",
665
- pointerEvents: "none"
666
- }
667
- ),
668
- /* @__PURE__ */ jsx(
669
- "path",
670
- {
671
- d: arcPath,
672
- fill: "none",
673
- stroke: isSelected ? "#1d4ed8" : color,
674
- strokeWidth: isSelected ? 3 : 2,
675
- pointerEvents: "none"
676
- }
677
- ),
678
- /* @__PURE__ */ jsx(
679
- "path",
680
- {
681
- d: arcPath,
682
- fill: "none",
683
- stroke: "transparent",
684
- strokeWidth: 20,
685
- strokeLinecap: "round",
686
- pointerEvents: "stroke",
687
- style: { cursor: hitCursor },
688
- onClick: (e) => {
689
- if (tool === "eraser" || tool === "none") {
690
- e.stopPropagation();
691
- onArcClick(g, e);
692
- }
843
+ /* @__PURE__ */ jsx(
844
+ AnimatedArc,
845
+ {
846
+ groupIndex: g,
847
+ arcPath,
848
+ color,
849
+ isSelected,
850
+ x2,
851
+ textY,
852
+ axisY,
853
+ end,
854
+ min,
855
+ max,
856
+ hitCursor,
857
+ shouldAnimate,
858
+ onArcClick: (index, e) => {
859
+ if (tool === "eraser" || tool === "none") {
860
+ e.stopPropagation();
861
+ onArcClick(index, e);
693
862
  }
694
863
  }
695
- ),
696
- /* @__PURE__ */ jsx(
697
- "text",
698
- {
699
- x: x2,
700
- y: textY,
701
- textAnchor: edgeTextAnchor(end, min, max),
702
- fontSize: 10,
703
- fill: isSelected ? "#1d4ed8" : color,
704
- fontWeight: "600",
705
- pointerEvents: "none",
706
- style: { userSelect: "none", WebkitUserSelect: "none" },
707
- children: end
708
- }
709
- ),
710
- /* @__PURE__ */ jsx(
711
- "line",
712
- {
713
- x1: x2,
714
- y1: textY + 4,
715
- x2,
716
- y2: axisY - arrowHeadH,
717
- stroke: isSelected ? "#1d4ed8" : color,
718
- strokeWidth: 1.5,
719
- pointerEvents: "none"
720
- }
721
- ),
722
- /* @__PURE__ */ jsx(
723
- "polygon",
724
- {
725
- points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
726
- fill: isSelected ? "#1d4ed8" : color,
727
- pointerEvents: "none"
728
- }
729
- )
730
- ] }, g)
864
+ },
865
+ g
866
+ )
731
867
  );
732
868
  }
733
869
  return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
@@ -845,7 +981,7 @@ function NumberLine({ className }) {
845
981
  let ryTypical = MIN_ARC_HEIGHT * 0.5;
846
982
  if (groupSize > 0 && groupCount > 0) {
847
983
  const rx = usable * groupSize / range / 2;
848
- ryTypical = Math.min(rx * 0.7, h * 0.48);
984
+ ryTypical = Math.min(rx * ARC_RY_RATIO, h * 0.48);
849
985
  }
850
986
  const minAxisY = MIN_ARC_HEIGHT + ARC_TOP_PADDING;
851
987
  const maxAxisY = h - LABEL_SPACE_BELOW_AXIS;
package/package.json CHANGED
@@ -1,47 +1,47 @@
1
- {
2
- "name": "@dcg-overseas/number-line",
3
- "version": "0.1.8",
4
- "description": "Interactive number line component with group arcs and freehand drawing",
5
- "type": "module",
6
- "license": "MIT",
7
- "main": "./dist/index.cjs",
8
- "module": "./dist/index.js",
9
- "types": "./dist/index.d.ts",
10
- "exports": {
11
- ".": {
12
- "import": {
13
- "types": "./dist/index.d.ts",
14
- "default": "./dist/index.js"
15
- },
16
- "require": {
17
- "types": "./dist/index.d.cts",
18
- "default": "./dist/index.cjs"
19
- }
20
- }
21
- },
22
- "files": [
23
- "dist"
24
- ],
25
- "sideEffects": false,
26
- "peerDependencies": {
27
- "react": "^18.0.0",
28
- "react-dom": "^18.0.0"
29
- },
30
- "devDependencies": {
31
- "@types/react": "^18.3.3",
32
- "@types/react-dom": "^18.3.0",
33
- "@vitejs/plugin-react": "^4.3.1",
34
- "vite": "^5.3.1",
35
- "vite-plugin-dts": "^3.9.1"
36
- },
37
- "publishConfig": {
38
- "access": "public",
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
- }
47
- }
1
+ {
2
+ "name": "@dcg-overseas/number-line",
3
+ "version": "0.1.10",
4
+ "description": "Interactive number line component with group arcs and freehand drawing",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
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
+ "peerDependencies": {
33
+ "react": "^18.0.0",
34
+ "react-dom": "^18.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/react": "^18.3.3",
38
+ "@types/react-dom": "^18.3.0",
39
+ "@vitejs/plugin-react": "^4.3.1",
40
+ "vite": "^5.3.1",
41
+ "vite-plugin-dts": "^3.9.1"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "registry": "https://registry.npmjs.org/"
46
+ }
47
+ }