From bbfb3614a22c4a3488130e7d4934775407184b3d Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Mon, 15 Sep 2025 17:56:32 -0500 Subject: [PATCH] fix: correct column indexing and add boundary checks for interactive abacus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix column indexing bug where tens/ones columns were swapped - Add boundary checks to prevent values exceeding abacus capacity - Improve earth and heaven bead click logic with proper place value mapping 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 3 +- apps/web/package.json | 1 + apps/web/src/components/InteractiveAbacus.tsx | 39 +++++++++++++++---- pnpm-lock.yaml | 25 ++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 675c5765..839ef9fe 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -99,7 +99,8 @@ "WebFetch(domain:www.radix-ui.com)", "Read(//Users/antialias/**)", "Bash(turbo run:*)", - "Bash(npx turbo run:*)" + "Bash(npx turbo run:*)", + "Bash(open http://localhost:3003/guide)" ], "deny": [], "ask": [] diff --git a/apps/web/package.json b/apps/web/package.json index 3bf09598..feee0064 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,7 @@ "@myriaddreamin/typst-ts-renderer": "0.6.1-rc3", "@myriaddreamin/typst-ts-web-compiler": "0.6.1-rc3", "@myriaddreamin/typst.ts": "0.6.1-rc3", + "@number-flow/react": "^0.5.10", "@pandacss/dev": "^0.20.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", diff --git a/apps/web/src/components/InteractiveAbacus.tsx b/apps/web/src/components/InteractiveAbacus.tsx index 1b091425..41fb1f9f 100644 --- a/apps/web/src/components/InteractiveAbacus.tsx +++ b/apps/web/src/components/InteractiveAbacus.tsx @@ -24,6 +24,7 @@ export function InteractiveAbacus({ }: InteractiveAbacusProps) { const [currentValue, setCurrentValue] = useState(initialValue) const [isChanging, setIsChanging] = useState(false) + const [previousValue, setPreviousValue] = useState(initialValue) const svgRef = useRef(null) // Animated value display @@ -39,6 +40,13 @@ export function InteractiveAbacus({ config: { tension: 400, friction: 25 } }) + // Crossfade animation between old and new SVG states + const crossfadeSpring = useSpring({ + opacity: isChanging ? 0.7 : 1, + transform: isChanging ? 'scale(0.98)' : 'scale(1)', + config: { tension: 300, friction: 30 } + }) + @@ -60,12 +68,15 @@ export function InteractiveAbacus({ if (beadType === 'earth') { const position = parseInt(beadPosition || '0') - const columnPower = Math.pow(10, beadColumn) + const placeValue = columns - 1 - beadColumn + const columnPower = Math.pow(10, placeValue) const currentDigit = Math.floor(currentValue / columnPower) % 10 const heavenContribution = Math.floor(currentDigit / 5) * 5 const earthContribution = currentDigit % 5 console.log('Earth bead analysis:', { position, + beadColumn, + placeValue, columnPower, currentDigit, heavenContribution, @@ -75,20 +86,27 @@ export function InteractiveAbacus({ if (beadType === 'heaven') { // Toggle heaven bead (worth 5) - const columnPower = Math.pow(10, beadColumn) + // Column indexing: 0=leftmost, but place values are rightmost=ones + // For 3 columns: col 0=hundreds(10^2), col 1=tens(10^1), col 2=ones(10^0) + const placeValue = columns - 1 - beadColumn + const columnPower = Math.pow(10, placeValue) const heavenValue = 5 * columnPower + const maxValue = Math.pow(10, columns) - 1 + if (isActive) { // Deactivate heaven bead - subtract 5 from this column setCurrentValue(prev => Math.max(0, prev - heavenValue)) } else { // Activate heaven bead - add 5 to this column - setCurrentValue(prev => prev + heavenValue) + setCurrentValue(prev => Math.min(prev + heavenValue, maxValue)) } } else if (beadType === 'earth' && beadPosition) { // Toggle earth bead (worth 1 each) const position = parseInt(beadPosition) // 0-3 where 0 is top (closest to bar), 3 is bottom - const columnPower = Math.pow(10, beadColumn) + // Column indexing: 0=leftmost, but place values are rightmost=ones + const placeValue = columns - 1 - beadColumn + const columnPower = Math.pow(10, placeValue) // Calculate current digit in this column const currentDigit = Math.floor(currentValue / columnPower) % 10 @@ -129,12 +147,19 @@ export function InteractiveAbacus({ const columnContribution = Math.floor(currentValue / columnPower) % 10 * columnPower const newValue = currentValue - columnContribution + (newDigit * columnPower) - setCurrentValue(Math.max(0, newValue)) + // Ensure value doesn't exceed maximum for this number of columns + const maxValue = Math.pow(10, columns) - 1 + setCurrentValue(Math.max(0, Math.min(newValue, maxValue))) } - // Visual feedback + // Visual feedback with extended timing for smoother transition setIsChanging(true) - setTimeout(() => setIsChanging(false), 150) + + // Update previous value for crossfade effect + setPreviousValue(currentValue) + + // Extended timing to allow for smoother crossfade + setTimeout(() => setIsChanging(false), 300) }, [currentValue]) // Add click event listener for bead interactions diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20c922a4..46af31cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@myriaddreamin/typst.ts': specifier: 0.6.1-rc3 version: 0.6.1-rc3(@myriaddreamin/typst-ts-renderer@0.6.1-rc3)(@myriaddreamin/typst-ts-web-compiler@0.6.1-rc3) + '@number-flow/react': + specifier: ^0.5.10 + version: 0.5.10(react-dom@18.0.0)(react@18.0.0) '@pandacss/dev': specifier: ^0.20.0 version: 0.20.1(jsdom@27.0.0)(typescript@5.0.2) @@ -1377,6 +1380,18 @@ packages: engines: {node: '>=12.4.0'} dev: true + /@number-flow/react@0.5.10(react-dom@18.0.0)(react@18.0.0): + resolution: {integrity: sha512-a8Wh5eNITn7Km4xbddAH7QH8eNmnduR6k34ER1hkHSGO4H2yU1DDnuAWLQM99vciGInFODemSc0tdxrXkJEpbA==} + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + dependencies: + esm-env: 1.2.2 + number-flow: 0.5.8 + react: 18.0.0 + react-dom: 18.0.0(react@18.0.0) + dev: false + /@pandacss/config@0.20.1: resolution: {integrity: sha512-RG+WWK2NAZhmikChYsER61YIHiR4IsLH52guJdhuerk+nT5dpSrs3RkGM5F8Fs18Pkcj+gWwcS+g+g41ictf1w==} dependencies: @@ -4388,6 +4403,10 @@ packages: - supports-color dev: true + /esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + dev: false + /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5618,6 +5637,12 @@ packages: path-key: 4.0.0 dev: true + /number-flow@0.5.8: + resolution: {integrity: sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==} + dependencies: + esm-env: 1.2.2 + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'}