Compare commits

..

8 Commits

Author SHA1 Message Date
semantic-release-bot
ee53bb9a9d chore(abacus-react): release v2.4.0 [skip ci]
# [2.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.3.0...abacus-react-v2.4.0) (2025-11-03)

### Bug Fixes

* remove distracting parallax and wobble 3D effects ([28a2d40](28a2d40996))
* remove wobble physics and enhance wood grain visibility ([5d97673](5d97673406))
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](26bdb11237))
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](096104b094))

### Features

* complete 3D enhancement integration for all three proposals ([5ac55cc](5ac55cc149))
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](37e330f26e))
2025-11-03 21:42:52 +00:00
Thomas Hallock
28a2d40996 fix: remove distracting parallax and wobble 3D effects
Remove all parallax hover effects and wobble physics from 3D enhancement system
as they were distracting and worsened the user experience.

Changes:
- Remove Abacus3DPhysics interface completely
- Remove physics3d prop from AbacusConfig and component
- Remove calculateParallaxOffset utility function
- Remove mouse tracking infrastructure (containerRef, mousePos, handleMouseMove)
- Update enhanced3d type to only support 'subtle' | 'realistic' (removed 'delightful')
- Update all 3D utilities to remove delightful mode support
- Remove all Delightful stories from Storybook
- Update Interactive Playground to remove parallax controls
- Change MyAbacus from delightful to realistic mode

The 3D enhancement system now focuses purely on visual improvements
(materials, lighting, textures) without any motion-based effects.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
37e330f26e feat: enable 3D enhancement on hero/open MyAbacus modes
Added delightful 3D mode to the hero and open states of MyAbacus:
- Glossy heaven beads + satin earth beads for premium feel
- Dramatic lighting for impact
- Wood grain texture on golden frame
- Hover parallax enabled for interactive depth
- Only applies to hero/open modes (not button mode)

The giant hero abacus on the home page now has satisfying
material rendering and interactive parallax effects.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
cc96802df8 docs: add 3D enhancement documentation to README
Added comprehensive 3D enhancement section covering:
- Subtle mode (CSS perspective + shadows)
- Realistic mode (materials, lighting, wood grain)
- Delightful mode (physics + parallax)

Includes code examples and explanations of all options.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
5d97673406 fix: remove wobble physics and enhance wood grain visibility
**Changes:**
- Removed wobble physics feature (was janky and distracting)
- Increased wood grain opacity from 0.15 → 0.4 (realistic) and 0.45 (delightful)
- Enhanced wood grain pattern with bolder strokes and more visible knots
- Removed getWobbleRotation utility function
- Simplified Abacus3DPhysics interface to only hoverParallax
- Updated all stories to remove wobble references
- Removed velocity tracking code from Bead component

Wood grain is now much more visible on frame elements without
affecting bead spacing or layout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
26bdb11237 fix: rewrite 3D stories to use props instead of CSS wrappers
The original stories were only applying CSS classes externally,
which meant none of the actual 3D features were working:
- No material gradients (glossy/satin/matte)
- No wood grain textures
- No wobble physics
- No hover parallax

Now properly passing enhanced3d, material3d, and physics3d props
to showcase all features:

**Stories added:**
- Compare All Levels (side-by-side)
- Material showcases (glossy, satin, matte, mixed)
- Wood grain frame demo
- Lighting comparison (top-down, ambient, dramatic)
- Wobble physics demo
- Hover parallax demo
- Interactive Playground with live controls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
5ac55cc149 feat: complete 3D enhancement integration for all three proposals
Fully integrated the 3D enhancement system into AbacusReact component:

**Proposal 1: Subtle (CSS Perspective + Shadows)**
- Container classes with perspective transforms
- CSS-based depth shadows on beads and frame

**Proposal 2: Realistic (Lighting + Materials)**
- Material gradients (glossy/satin/matte) for beads via SVG radial gradients
- Wood grain texture overlays for frame elements (rods & reckoning bar)
- Lighting filter effects (top-down/ambient/dramatic)
- Enhanced physics config for realistic motion

**Proposal 3: Delightful (Physics + Micro-interactions)**
- Advanced physics with overshoot and bounce
- Wobble rotation on bead movement based on velocity tracking
- Hover parallax tracking with Z-depth lift
- Mouse position tracking for interactive parallax effects

Implementation details:
- Pass enhanced3d, material3d, physics3d props down to Bead component
- Generate SVG gradient defs for material rendering (realistic/delightful)
- Apply gradients to bead shapes based on material type
- Calculate parallax offsets using Abacus3DUtils
- Track velocity for wobble rotation effects
- Add wood grain texture pattern to frame elements
- Enhanced React Spring physics config per enhancement level
- Container ref and mouse tracking for parallax

All three proposals work end-to-end with existing configurations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
096104b094 fix: use absolute positioning for hero abacus to eliminate scroll lag
Replace laggy JavaScript scroll tracking with proper CSS positioning:

Before:
- Used position: fixed with JavaScript scroll listener
- Calculated top position dynamically: calc(60vh - ${scrollY}px)
- Caused noticeable lag on mobile and slower browsers

After:
- Hero mode: position: absolute (scrolls naturally with document)
- Button mode: position: fixed (stays in viewport at bottom-right)
- Open mode: position: fixed (stays in viewport at center)
- Zero JavaScript scroll tracking needed

Result: Buttery smooth scrolling on all devices with zero lag.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
10 changed files with 628 additions and 782 deletions

View File

@@ -161,11 +161,14 @@
"Bash(printenv:*)",
"Bash(typst:*)",
"Bash(npx tsx:*)",
"Bash(sort:*)"
"Bash(sort:*)",
"Bash(scp:*)"
],
"deny": [],
"ask": []
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": ["sqlite"]
"enabledMcpjsonServers": [
"sqlite"
]
}

View File

@@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { writeFileSync, readFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'

View File

@@ -6,7 +6,6 @@ import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { css } from '../../styled-system/css'
import { useMyAbacus } from '@/contexts/MyAbacusContext'
import { HomeHeroContext } from '@/contexts/HomeHeroContext'
import { Z_INDEX } from '@/constants/zIndex'
export function MyAbacus() {
const { isOpen, close, toggle } = useMyAbacus()
@@ -31,21 +30,6 @@ export function MyAbacus() {
const isHeroVisible = homeHeroContext?.isHeroVisible ?? false
const isHeroMode = isOnHomePage && isHeroVisible && !isOpen
// Track scroll position for hero mode
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
if (!isHeroMode) return
const handleScroll = () => {
setScrollY(window.scrollY)
}
handleScroll() // Initial position
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [isHeroMode])
// Close on Escape key
useEffect(() => {
if (!isOpen) return
@@ -114,7 +98,7 @@ export function MyAbacus() {
inset: 0,
bg: 'rgba(0, 0, 0, 0.8)',
backdropFilter: 'blur(12px)',
zIndex: Z_INDEX.MY_ABACUS_BACKDROP,
zIndex: 101,
animation: 'backdropFadeIn 0.4s ease-out',
})}
onClick={close}
@@ -144,7 +128,7 @@ export function MyAbacus() {
fontWeight: 'bold',
cursor: 'pointer',
transition: 'all 0.2s',
zIndex: Z_INDEX.MY_ABACUS + 1,
zIndex: 103,
animation: 'fadeIn 0.3s ease-out 0.2s both',
_hover: {
bg: 'rgba(255, 255, 255, 0.2)',
@@ -162,36 +146,28 @@ export function MyAbacus() {
data-component="my-abacus"
data-mode={isOpen ? 'open' : isHeroMode ? 'hero' : 'button'}
onClick={isOpen || isHeroMode ? undefined : toggle}
style={
isHeroMode
? {
// Hero mode: position accounts for scroll to flow with page (subtract scroll to move up with content)
// Positioned lower (60vh instead of 50vh) to avoid covering subtitle
top: `calc(60vh - ${scrollY}px)`,
}
: undefined
}
className={css({
position: 'fixed',
zIndex: Z_INDEX.MY_ABACUS,
position: isHeroMode ? 'absolute' : 'fixed',
zIndex: 102,
cursor: isOpen || isHeroMode ? 'default' : 'pointer',
transition: isHeroMode ? 'none' : 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
// Three modes: hero (inline with content), button (bottom-right), open (center)
transition: 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
// Three modes: hero (absolute - scrolls with document), button (fixed), open (fixed)
...(isOpen
? {
// Open mode: center of screen
// Open mode: fixed to center of viewport
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}
: isHeroMode
? {
// Hero mode: centered horizontally, top handled by inline style
// Hero mode: absolute positioning - scrolls naturally with document
top: '60vh',
left: '50%',
transform: 'translate(-50%, -50%)',
}
: {
// Button mode: bottom-right corner
// Button mode: fixed to bottom-right corner
bottom: { base: '4', md: '6' },
right: { base: '4', md: '6' },
transform: 'translate(0, 0)',
@@ -259,58 +235,22 @@ export function MyAbacus() {
animated={isOpen || isHeroMode}
customStyles={isHeroMode ? structuralStyles : trophyStyles}
onValueChange={setAbacusValue}
// 3D Enhancement - realistic mode for hero and open states
enhanced3d={isOpen || isHeroMode ? 'realistic' : undefined}
material3d={
isOpen || isHeroMode
? {
heavenBeads: 'glossy',
earthBeads: 'satin',
lighting: 'dramatic',
woodGrain: true,
}
: undefined
}
/>
</div>
</div>
{/* Title and achievement info - only visible when open */}
{isOpen && (
<div
className={css({
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
mt: { base: '16', md: '20', lg: '24' },
textAlign: 'center',
animation: 'fadeIn 0.5s ease-out 0.3s both',
maxW: '600px',
px: '8',
})}
>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl', lg: '4xl' },
fontWeight: 'bold',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
mb: '3',
})}
>
My Abacus
</h2>
<p
className={css({
fontSize: { base: 'md', md: 'lg' },
color: 'gray.300',
mb: '4',
fontWeight: 'medium',
})}
>
Your personal abacus grows with you
</p>
<p
className={css({
fontSize: { base: 'sm', md: 'md' },
color: 'gray.400',
lineHeight: '1.6',
})}
>
Complete tutorials, play games, and earn achievements to unlock higher place values
</p>
</div>
)}
</div>
{/* Keyframes for animations */}

View File

@@ -9,7 +9,9 @@
"Bash(git add:*)",
"Bash(git rm:*)",
"Bash(git commit:*)",
"Bash(npm run build:*)"
"Bash(npm run build:*)",
"Bash(git reset:*)",
"Bash(cat:*)"
]
},
"enableAllProjectMcpServers": true,

View File

@@ -1,3 +1,19 @@
# [2.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.3.0...abacus-react-v2.4.0) (2025-11-03)
### Bug Fixes
* remove distracting parallax and wobble 3D effects ([28a2d40](https://github.com/antialias/soroban-abacus-flashcards/commit/28a2d40996256700bf19cd80130b26e24441949f))
* remove wobble physics and enhance wood grain visibility ([5d97673](https://github.com/antialias/soroban-abacus-flashcards/commit/5d976734062eb3d943bfdfdd125473c56b533759))
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](https://github.com/antialias/soroban-abacus-flashcards/commit/26bdb112370cece08634e3d693d15336111fc70f))
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](https://github.com/antialias/soroban-abacus-flashcards/commit/096104b094b45aa584f2b9d47a440a8c14d82fc0))
### Features
* complete 3D enhancement integration for all three proposals ([5ac55cc](https://github.com/antialias/soroban-abacus-flashcards/commit/5ac55cc14980b778f9be32f0833f8760aa16b631))
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](https://github.com/antialias/soroban-abacus-flashcards/commit/37e330f26e5398c2358599361cd417b4aeefac7d))
# [2.3.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.2.0...abacus-react-v2.3.0) (2025-11-03)

View File

@@ -13,6 +13,7 @@ A comprehensive React component for rendering interactive Soroban (Japanese abac
- 🔧 **Developer-friendly** - Comprehensive hooks and callback system
- 🎓 **Tutorial system** - Built-in overlay and guidance capabilities
- 🧩 **Framework-free SVG** - Complete control over rendering
-**3D Enhancement** - Three levels of progressive 3D effects for immersive visuals
## Installation
@@ -113,6 +114,82 @@ Educational guidance with tooltips
/>
```
## 3D Enhancement
Make the abacus feel tangible and satisfying with three progressive levels of 3D effects.
### Subtle Mode
Light depth shadows and perspective for subtle dimensionality.
```tsx
<AbacusReact
value={12345}
columns={5}
enhanced3d="subtle"
interactive
animated
/>
```
### Realistic Mode
Material-based rendering with lighting effects and textures.
```tsx
<AbacusReact
value={7890}
columns={4}
enhanced3d="realistic"
material3d={{
heavenBeads: 'glossy', // 'glossy' | 'satin' | 'matte'
earthBeads: 'satin',
lighting: 'top-down', // 'top-down' | 'ambient' | 'dramatic'
woodGrain: true // Add wood texture to frame
}}
interactive
animated
/>
```
**Materials:**
- `glossy` - High shine with strong highlights
- `satin` - Balanced shine (default)
- `matte` - Subtle shading, no shine
**Lighting:**
- `top-down` - Balanced directional light from above
- `ambient` - Soft light from all directions
- `dramatic` - Strong directional light for high contrast
### Delightful Mode
Maximum satisfaction with enhanced physics and interactive effects.
```tsx
<AbacusReact
value={8642}
columns={4}
enhanced3d="delightful"
material3d={{
heavenBeads: 'glossy',
earthBeads: 'satin',
lighting: 'dramatic',
woodGrain: true
}}
physics3d={{
hoverParallax: true // Beads lift on hover with Z-depth
}}
interactive
animated
soundEnabled
/>
```
**Physics Options:**
- `hoverParallax` - Beads near mouse cursor lift up with depth perception
All 3D modes work with existing configurations and preserve exact geometry.
## Core API

View File

@@ -103,7 +103,7 @@
/* Wood grain texture overlay */
.abacus-3d-container.enhanced-realistic .frame-wood {
opacity: 0.15;
opacity: 0.4;
mix-blend-mode: multiply;
pointer-events: none;
}
@@ -293,11 +293,6 @@
z-index: -1;
}
/* Wobble physics - applied via inline styles from React Spring */
.bead-wobble {
/* transform-origin set dynamically */
transform-style: preserve-3d;
}
/* Frame depth enhancement */
.abacus-3d-container.enhanced-delightful rect[class*="column-post"],
@@ -307,25 +302,11 @@
drop-shadow(0 0 2px rgba(0, 0, 0, 0.1));
}
/* Wood grain texture - enhanced */
.frame-wood-enhanced {
background-image:
repeating-linear-gradient(
90deg,
transparent,
transparent 2px,
rgba(139, 90, 43, 0.03) 2px,
rgba(139, 90, 43, 0.03) 4px
),
repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(101, 67, 33, 0.02) 1px,
rgba(101, 67, 33, 0.02) 2px
);
opacity: 0.2;
/* Wood grain texture - enhanced for delightful mode */
.abacus-3d-container.enhanced-delightful .frame-wood {
opacity: 0.45;
mix-blend-mode: multiply;
pointer-events: none;
}
/* Accessibility - Reduced motion */

View File

@@ -124,7 +124,7 @@ export function getLightingFilter(lighting: LightingStyle = "top-down"): string
* Calculate Z-depth for a bead based on enhancement level and state
*/
export function getBeadZDepth(
enhanced3d: boolean | "subtle" | "realistic" | "delightful",
enhanced3d: boolean | "subtle" | "realistic",
active: boolean
): number {
if (!enhanced3d || enhanced3d === true) return 0;
@@ -136,77 +136,28 @@ export function getBeadZDepth(
return 6;
case "realistic":
return 10;
case "delightful":
return 12;
default:
return 0;
}
}
/**
* Generate wobble rotation based on velocity (for delightful mode)
*/
export function getWobbleRotation(velocity: number, axis: "x" | "y" = "x"): string {
const maxRotation = 3; // degrees
const rotation = Math.max(-maxRotation, Math.min(maxRotation, velocity * -2));
if (axis === "x") {
return `rotateX(${rotation}deg)`;
}
return `rotateY(${rotation}deg)`;
}
/**
* Calculate parallax offset based on mouse position
*/
export function calculateParallaxOffset(
beadX: number,
beadY: number,
mouseX: number,
mouseY: number,
containerX: number,
containerY: number,
intensity: number = 0.5
): { x: number; y: number; z: number } {
// Calculate distance from bead center to mouse
const dx = (mouseX - containerX) - beadX;
const dy = (mouseY - containerY) - beadY;
const distance = Math.sqrt(dx * dx + dy * dy);
// Max influence radius (pixels)
const maxRadius = 150;
if (distance > maxRadius) {
return { x: 0, y: 0, z: 0 };
}
// Calculate lift amount (inverse square falloff)
const influence = Math.max(0, 1 - (distance / maxRadius));
const lift = influence * influence * intensity;
return {
x: dx * lift * 0.1,
y: dy * lift * 0.1,
z: lift * 8
};
}
/**
* Generate wood grain texture SVG pattern
*/
export function getWoodGrainPattern(id: string): string {
return `
<pattern id="${id}" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
<rect width="100" height="100" fill="#8B5A2B" opacity="0.3"/>
<!-- Grain lines -->
<path d="M 0 10 Q 25 8 50 10 T 100 10" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
<path d="M 0 30 Q 25 28 50 30 T 100 30" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.3"/>
<path d="M 0 50 Q 25 48 50 50 T 100 50" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
<path d="M 0 70 Q 25 68 50 70 T 100 70" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.3"/>
<path d="M 0 90 Q 25 88 50 90 T 100 90" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
<!-- Knots -->
<ellipse cx="20" cy="25" rx="8" ry="6" fill="#654321" opacity="0.2"/>
<ellipse cx="75" cy="65" rx="6" ry="8" fill="#654321" opacity="0.2"/>
<rect width="100" height="100" fill="#8B5A2B" opacity="0.5"/>
<!-- Grain lines - more visible -->
<path d="M 0 10 Q 25 8 50 10 T 100 10" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
<path d="M 0 30 Q 25 28 50 30 T 100 30" stroke="#654321" stroke-width="1" fill="none" opacity="0.5"/>
<path d="M 0 50 Q 25 48 50 50 T 100 50" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
<path d="M 0 70 Q 25 68 50 70 T 100 70" stroke="#654321" stroke-width="1" fill="none" opacity="0.5"/>
<path d="M 0 90 Q 25 88 50 90 T 100 90" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
<!-- Knots - more prominent -->
<ellipse cx="20" cy="25" rx="8" ry="6" fill="#654321" opacity="0.35"/>
<ellipse cx="75" cy="65" rx="6" ry="8" fill="#654321" opacity="0.35"/>
<ellipse cx="45" cy="82" rx="5" ry="7" fill="#654321" opacity="0.3"/>
</pattern>
`;
}
@@ -215,9 +166,8 @@ export function getWoodGrainPattern(id: string): string {
* Get container class names for 3D enhancement level
*/
export function get3DContainerClasses(
enhanced3d: boolean | "subtle" | "realistic" | "delightful" | undefined,
lighting?: LightingStyle,
parallaxEnabled?: boolean
enhanced3d: boolean | "subtle" | "realistic" | undefined,
lighting?: LightingStyle
): string {
const classes: string[] = ["abacus-3d-container"];
@@ -228,8 +178,6 @@ export function get3DContainerClasses(
classes.push("enhanced-subtle");
} else if (enhanced3d === "realistic") {
classes.push("enhanced-realistic");
} else if (enhanced3d === "delightful") {
classes.push("enhanced-delightful");
}
// Add lighting class
@@ -237,11 +185,6 @@ export function get3DContainerClasses(
classes.push(`lighting-${lighting}`);
}
// Add parallax class
if (parallaxEnabled && enhanced3d === "delightful") {
classes.push("parallax-enabled");
}
return classes.join(" ");
}
@@ -260,7 +203,7 @@ export function getBeadGradientId(
/**
* Physics config for different enhancement levels
*/
export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic" | "delightful") {
export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic") {
const base = {
tension: 300,
friction: 22,
@@ -272,20 +215,11 @@ export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic" |
return { ...base, clamp: true };
}
if (enhanced3d === "realistic") {
return {
tension: 320,
friction: 24,
mass: 0.6,
clamp: false
};
}
// delightful
// realistic
return {
tension: 280,
friction: 20,
mass: 0.7,
clamp: false, // Allow overshoot for satisfying settle
tension: 320,
friction: 24,
mass: 0.6,
clamp: false
};
}

View File

@@ -1,7 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AbacusReact } from './AbacusReact';
import React, { useEffect, useRef } from 'react';
import './Abacus3D.css';
import React from 'react';
const meta: Meta<typeof AbacusReact> = {
title: 'Soroban/3D Effects Showcase',
@@ -13,29 +12,20 @@ const meta: Meta<typeof AbacusReact> = {
component: `
# 3D Enhancement Showcase
Three levels of progressive 3D enhancement for the abacus to make interactions feel satisfying and real.
Two levels of progressive 3D enhancement for the abacus to make interactions feel satisfying and real.
## Proposal 1: Subtle (CSS Perspective + Shadows)
## Subtle (CSS Perspective + Shadows)
- Light perspective tilt
- Depth shadows on active beads
- Smooth transitions
- **Zero performance cost**
## Proposal 2: Realistic (Lighting + Materials)
- Everything from Proposal 1 +
- Realistic lighting effects
- Material-based bead rendering (glossy/satin/matte)
- Ambient occlusion
- Frame depth
## Proposal 3: Delightful (Physics + Micro-interactions)
- Everything from Proposal 2 +
- Enhanced physics with satisfying bounce
- Clack ripple effects when beads snap
- Hover parallax
- Maximum satisfaction
**Note:** Currently these are CSS-only demos. Full integration with React Spring physics coming next!
## Realistic (Lighting + Materials)
- Everything from Subtle +
- Realistic lighting effects with material gradients
- Glossy/Satin/Matte bead materials
- Wood grain textures on frame
- Enhanced physics for realistic motion
`
}
}
@@ -46,596 +36,387 @@ Three levels of progressive 3D enhancement for the abacus to make interactions f
export default meta;
type Story = StoryObj<typeof meta>;
// Wrapper component to apply 3D CSS classes
const Wrapper3D: React.FC<{
children: React.ReactNode;
level: 'subtle' | 'realistic' | 'delightful';
lighting?: 'top-down' | 'ambient' | 'dramatic';
}> = ({ children, level, lighting }) => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (containerRef.current) {
const svg = containerRef.current.querySelector('.abacus-svg');
const beads = containerRef.current.querySelectorAll('.abacus-bead');
// Add classes to container
containerRef.current.classList.add('abacus-3d-container');
containerRef.current.classList.add(`enhanced-${level}`);
if (lighting) {
containerRef.current.classList.add(`lighting-${lighting}`);
}
// Apply will-change for performance
if (level === 'delightful') {
beads.forEach(bead => {
(bead as HTMLElement).style.willChange = 'transform, filter';
});
}
}
}, [level, lighting]);
return <div ref={containerRef}>{children}</div>;
};
// ============================================
// PROPOSAL 1: SUBTLE
// SIDE-BY-SIDE COMPARISON
// ============================================
export const Subtle_Static: Story = {
name: '1. Subtle - Static Display',
export const CompareAllLevels: Story = {
name: '🎯 Compare All Levels',
render: () => (
<Wrapper3D level="subtle">
<AbacusReact
value={12345}
columns={5}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Subtle 3D with light perspective tilt and depth shadows. Notice the slight elevation of active beads.'
}
}
}
};
export const Subtle_Interactive: Story = {
name: '1. Subtle - Interactive',
render: () => (
<Wrapper3D level="subtle">
<AbacusReact
value={678}
columns={3}
showNumbers
interactive
animated
soundEnabled
colorScheme="heaven-earth"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Subtle 3D + interaction. Click beads to see depth shadows change. Notice how the perspective gives a sense of physicality.'
}
}
}
};
export const Subtle_Tutorial: Story = {
name: '1. Subtle - Tutorial Mode',
render: () => {
const [step, setStep] = React.useState(0);
const highlights = [
{ placeValue: 0, beadType: 'earth' as const, position: 2 },
{ placeValue: 1, beadType: 'heaven' as const },
{ placeValue: 2, beadType: 'earth' as const, position: 0 },
];
return (
<div style={{ textAlign: 'center' }}>
<Wrapper3D level="subtle">
<AbacusReact
value={123}
columns={3}
showNumbers
interactive
animated
highlightBeads={[highlights[step]]}
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
<div style={{ marginTop: '20px' }}>
<button onClick={() => setStep((step + 1) % 3)} style={{ padding: '8px 16px' }}>
Next Step ({step + 1}/3)
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '60px', alignItems: 'center' }}>
<div>
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>No Enhancement</h3>
<AbacusReact
value={4242}
columns={4}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
/>
</div>
);
},
parameters: {
docs: {
description: {
story: 'Tutorial mode with subtle 3D effects. The depth helps highlight which bead to focus on.'
}
}
}
};
// ============================================
// PROPOSAL 2: REALISTIC
// ============================================
export const Realistic_TopDown: Story = {
name: '2. Realistic - Top-Down Lighting',
render: () => (
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={24680}
columns={5}
showNumbers
colorScheme="place-value"
beadShape="circle"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D with top-down lighting. Notice the enhanced shadows and sense of illumination from above.'
}
}
}
};
export const Realistic_Ambient: Story = {
name: '2. Realistic - Ambient Lighting',
render: () => (
<Wrapper3D level="realistic" lighting="ambient">
<AbacusReact
value={13579}
columns={5}
showNumbers
colorScheme="place-value"
beadShape="diamond"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D with ambient lighting. Softer, more even illumination creates a cozy feel.'
}
}
}
};
export const Realistic_Dramatic: Story = {
name: '2. Realistic - Dramatic Lighting',
render: () => (
<Wrapper3D level="realistic" lighting="dramatic">
<AbacusReact
value={99999}
columns={5}
showNumbers
colorScheme="heaven-earth"
beadShape="square"
colorPalette="colorblind"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D with dramatic lighting. Strong directional light creates bold shadows and depth.'
}
}
}
};
export const Realistic_Interactive: Story = {
name: '2. Realistic - Interactive',
render: () => (
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={555}
columns={3}
showNumbers
interactive
animated
soundEnabled
colorScheme="place-value"
colorPalette="nature"
scaleFactor={1.3}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D + interaction. Click beads and watch the enhanced shadows and lighting respond. Feel that satisfaction!'
}
}
}
};
export const Realistic_AllShapes: Story = {
name: '2. Realistic - All Bead Shapes',
render: () => (
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap', justifyContent: 'center' }}>
<div style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={777}
columns={3}
showNumbers
beadShape="diamond"
colorScheme="place-value"
/>
</Wrapper3D>
<p style={{ marginTop: '12px', fontSize: '14px' }}>Diamond</p>
<div>
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>Subtle</h3>
<AbacusReact
value={4242}
columns={4}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="subtle"
/>
</div>
<div style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={777}
columns={3}
showNumbers
beadShape="circle"
colorScheme="place-value"
/>
</Wrapper3D>
<p style={{ marginTop: '12px', fontSize: '14px' }}>Circle</p>
</div>
<div style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={777}
columns={3}
showNumbers
beadShape="square"
colorScheme="place-value"
/>
</Wrapper3D>
<p style={{ marginTop: '12px', fontSize: '14px' }}>Square</p>
<div>
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>Realistic (Satin Beads + Wood Frame)</h3>
<AbacusReact
value={4242}
columns={4}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="realistic"
material3d={{
heavenBeads: 'satin',
earthBeads: 'satin',
lighting: 'top-down',
woodGrain: true
}}
/>
</div>
</div>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D works beautifully with all three bead shapes.'
story: 'Side-by-side comparison of both enhancement levels. **Click beads** to see how they move!'
}
}
}
};
// ============================================
// PROPOSAL 3: DELIGHTFUL
// PROPOSAL 1: SUBTLE
// ============================================
export const Delightful_Static: Story = {
name: '3. Delightful - Maximum Depth',
render: () => (
<Wrapper3D level="delightful">
<AbacusReact
value={11111}
columns={5}
showNumbers
colorScheme="alternating"
beadShape="circle"
colorPalette="mnemonic"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Delightful 3D with maximum depth and richness. The beads really pop off the page!'
}
}
}
};
export const Delightful_Interactive: Story = {
name: '3. Delightful - Interactive (Physics Ready)',
render: () => (
<Wrapper3D level="delightful">
<AbacusReact
value={987}
columns={3}
showNumbers
interactive
animated
soundEnabled
colorScheme="place-value"
scaleFactor={1.3}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Delightful 3D + interaction. This is the CSS foundation - physics effects (wobble, clack ripple) will be added in the next iteration. Already feels great!'
}
}
}
};
export const Delightful_LargeScale: Story = {
name: '3. Delightful - Large Scale',
render: () => (
<Wrapper3D level="delightful">
<AbacusReact
value={9876543210}
columns={10}
showNumbers
colorScheme="place-value"
scaleFactor={1}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Delightful 3D scales beautifully even with many columns. The depth hierarchy helps organize the visual.'
}
}
}
};
// ============================================
// COMPARISON VIEWS
// ============================================
export const CompareAllLevels: Story = {
name: 'Compare All Three Levels',
render: () => {
const value = 4242;
const columns = 4;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '60px', padding: '20px' }}>
{/* No 3D */}
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
No Enhancement (Current)
</h3>
<AbacusReact
value={value}
columns={columns}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</div>
{/* Subtle */}
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
Proposal 1: Subtle 😊
</h3>
<Wrapper3D level="subtle">
<AbacusReact
value={value}
columns={columns}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
Light tilt + depth shadows
</p>
</div>
{/* Realistic */}
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
Proposal 2: Realistic 😍
</h3>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={value}
columns={columns}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
Lighting + materials + ambient occlusion
</p>
</div>
{/* Delightful */}
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
Proposal 3: Delightful 🤩
</h3>
<Wrapper3D level="delightful">
<AbacusReact
value={value}
columns={columns}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
Maximum depth + enhanced lighting (physics effects coming next!)
</p>
</div>
</div>
);
export const Subtle_Basic: Story = {
name: '1⃣ Subtle - Basic',
args: {
value: 12345,
columns: 5,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'place-value',
scaleFactor: 1.2,
enhanced3d: 'subtle'
},
parameters: {
docs: {
description: {
story: 'Side-by-side comparison of all three enhancement levels. Which feels best to you?'
story: 'Subtle 3D with light perspective tilt and depth shadows. Click beads to interact!'
}
}
}
};
export const CompareInteractive: Story = {
name: 'Compare Interactive (Side-by-Side)',
render: () => {
const [value1, setValue1] = React.useState(123);
const [value2, setValue2] = React.useState(456);
const [value3, setValue3] = React.useState(789);
// ============================================
// PROPOSAL 2: REALISTIC (Materials)
// ============================================
return (
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap', justifyContent: 'center' }}>
<div style={{ textAlign: 'center' }}>
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Subtle</h4>
<Wrapper3D level="subtle">
<AbacusReact
value={value1}
onValueChange={(v) => setValue1(Number(v))}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
/>
</Wrapper3D>
</div>
<div style={{ textAlign: 'center' }}>
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Realistic</h4>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={value2}
onValueChange={(v) => setValue2(Number(v))}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
/>
</Wrapper3D>
</div>
<div style={{ textAlign: 'center' }}>
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Delightful</h4>
<Wrapper3D level="delightful">
<AbacusReact
value={value3}
onValueChange={(v) => setValue3(Number(v))}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
/>
</Wrapper3D>
</div>
</div>
);
export const Realistic_GlossyBeads: Story = {
name: '2⃣ Realistic - Glossy Beads',
args: {
value: 7890,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'heaven-earth',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'glossy',
earthBeads: 'glossy',
lighting: 'top-down'
}
},
parameters: {
docs: {
description: {
story: 'Try all three side-by-side! Click beads and feel the difference in satisfaction.'
story: '**Glossy material** with high shine and strong highlights. Notice the radial gradients on the beads!'
}
}
}
};
export const Realistic_SatinBeads: Story = {
name: '2⃣ Realistic - Satin Beads',
args: {
value: 7890,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'heaven-earth',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'satin',
earthBeads: 'satin',
lighting: 'top-down'
}
},
parameters: {
docs: {
description: {
story: '**Satin material** (default) with balanced shine. Medium highlights, smooth appearance.'
}
}
}
};
export const Realistic_MatteBeads: Story = {
name: '2⃣ Realistic - Matte Beads',
args: {
value: 7890,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'heaven-earth',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'matte',
earthBeads: 'matte',
lighting: 'ambient'
}
},
parameters: {
docs: {
description: {
story: '**Matte material** with subtle shading, no shine. Flat, understated appearance.'
}
}
}
};
export const Realistic_MixedMaterials: Story = {
name: '2⃣ Realistic - Mixed Materials',
args: {
value: 5678,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'heaven-earth',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'glossy', // Heaven beads are shiny
earthBeads: 'matte', // Earth beads are flat
lighting: 'dramatic'
}
},
parameters: {
docs: {
description: {
story: '**Mixed materials**: Glossy heaven beads (5-value) + Matte earth beads (1-value). Different visual weight!'
}
}
}
};
export const Realistic_WoodGrain: Story = {
name: '2⃣ Realistic - Wood Grain Frame',
args: {
value: 3456,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'monochrome',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'satin',
earthBeads: 'satin',
lighting: 'top-down',
woodGrain: true // Enable wood texture on frame
}
},
parameters: {
docs: {
description: {
story: '**Wood grain texture** overlaid on the frame (rods and reckoning bar). Traditional soroban aesthetic!'
}
}
}
};
export const Realistic_LightingComparison: Story = {
name: '2⃣ Realistic - Lighting Comparison',
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '40px', alignItems: 'center' }}>
<div>
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Top-Down Lighting</h4>
<AbacusReact
value={999}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="realistic"
material3d={{
heavenBeads: 'glossy',
earthBeads: 'glossy',
lighting: 'top-down'
}}
/>
</div>
<div>
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Ambient Lighting</h4>
<AbacusReact
value={999}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="realistic"
material3d={{
heavenBeads: 'glossy',
earthBeads: 'glossy',
lighting: 'ambient'
}}
/>
</div>
<div>
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Dramatic Lighting</h4>
<AbacusReact
value={999}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="realistic"
material3d={{
heavenBeads: 'glossy',
earthBeads: 'glossy',
lighting: 'dramatic'
}}
/>
</div>
</div>
),
parameters: {
docs: {
description: {
story: 'Compare different **lighting styles**: top-down (balanced), ambient (soft all around), dramatic (strong directional).'
}
}
}
};
// ============================================
// FEATURE TESTS
// INTERACTIVE PLAYGROUND
// ============================================
export const ColorSchemes_With3D: Story = {
name: '3D Works With All Color Schemes',
export const Playground: Story = {
name: '🎮 Interactive Playground',
render: () => {
const value = 333;
const schemes: Array<'monochrome' | 'place-value' | 'alternating' | 'heaven-earth'> = [
'monochrome',
'place-value',
'alternating',
'heaven-earth'
];
const [level, setLevel] = React.useState<'subtle' | 'realistic'>('realistic');
const [material, setMaterial] = React.useState<'glossy' | 'satin' | 'matte'>('glossy');
const [lighting, setLighting] = React.useState<'top-down' | 'ambient' | 'dramatic'>('dramatic');
const [woodGrain, setWoodGrain] = React.useState(true);
return (
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap', justifyContent: 'center' }}>
{schemes.map(scheme => (
<div key={scheme} style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={value}
columns={3}
showNumbers
colorScheme={scheme}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', marginTop: '8px', textTransform: 'capitalize' }}>
{scheme}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '30px', alignItems: 'center' }}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '20px',
padding: '20px',
background: '#f5f5f5',
borderRadius: '8px',
maxWidth: '500px'
}}>
<div>
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Enhancement Level</label>
<select value={level} onChange={e => setLevel(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
<option value="subtle">Subtle</option>
<option value="realistic">Realistic</option>
</select>
</div>
))}
</div>
);
},
parameters: {
docs: {
description: {
story: 'The 3D effects work seamlessly with all existing color schemes.'
}
}
}
};
export const ColorPalettes_With3D: Story = {
name: '3D Works With All Palettes',
render: () => {
const value = 555;
const palettes: Array<'default' | 'colorblind' | 'mnemonic' | 'grayscale' | 'nature'> = [
'default',
'colorblind',
'mnemonic',
'grayscale',
'nature'
];
return (
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap', justifyContent: 'center' }}>
{palettes.map(palette => (
<div key={palette} style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={value}
columns={3}
showNumbers
colorScheme="place-value"
colorPalette={palette}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', marginTop: '8px', textTransform: 'capitalize' }}>
{palette}
</p>
<div>
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Bead Material</label>
<select value={material} onChange={e => setMaterial(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
<option value="glossy">Glossy</option>
<option value="satin">Satin</option>
<option value="matte">Matte</option>
</select>
</div>
))}
<div>
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Lighting</label>
<select value={lighting} onChange={e => setLighting(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
<option value="top-down">Top-Down</option>
<option value="ambient">Ambient</option>
<option value="dramatic">Dramatic</option>
</select>
</div>
<div>
<label style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<input type="checkbox" checked={woodGrain} onChange={e => setWoodGrain(e.target.checked)} />
<span>Wood Grain</span>
</label>
</div>
</div>
<AbacusReact
value={6789}
columns={4}
showNumbers
interactive
animated
soundEnabled
colorScheme="rainbow"
scaleFactor={1.4}
enhanced3d={level}
material3d={{
heavenBeads: material,
earthBeads: material,
lighting: lighting,
woodGrain: woodGrain
}}
/>
<p style={{ maxWidth: '500px', textAlign: 'center', color: '#666' }}>
Click beads to interact! Try different combinations above to find your favorite look and feel.
</p>
</div>
);
},
parameters: {
docs: {
description: {
story: 'The 3D effects enhance all color palettes beautifully.'
story: 'Experiment with all the 3D options! Mix and match materials, lighting, and physics to find your perfect configuration.'
}
}
}

View File

@@ -253,14 +253,6 @@ export interface Abacus3DMaterial {
woodGrain?: boolean; // Add wood texture to frame
}
export interface Abacus3DPhysics {
wobble?: boolean; // Beads rotate slightly during movement
clackEffect?: boolean; // Visual ripple when beads snap
hoverParallax?: boolean; // Beads lift on hover
particleSnap?: "off" | "subtle" | "sparkle"; // Particle effects on snap
hapticFeedback?: boolean; // Trigger haptic feedback on mobile
}
export interface AbacusConfig {
// Basic configuration
value?: number | bigint;
@@ -279,9 +271,8 @@ export interface AbacusConfig {
soundVolume?: number;
// 3D Enhancement
enhanced3d?: boolean | "subtle" | "realistic" | "delightful";
enhanced3d?: boolean | "subtle" | "realistic";
material3d?: Abacus3DMaterial;
physics3d?: Abacus3DPhysics;
// Advanced customization
customStyles?: AbacusCustomStyles;
@@ -1247,6 +1238,10 @@ interface BeadProps {
colorScheme?: string;
colorPalette?: string;
totalColumns?: number;
// 3D Enhancement
enhanced3d?: boolean | "subtle" | "realistic";
material3d?: Abacus3DMaterial;
columnIndex?: number;
}
const Bead: React.FC<BeadProps> = ({
@@ -1275,16 +1270,25 @@ const Bead: React.FC<BeadProps> = ({
colorScheme = "monochrome",
colorPalette = "default",
totalColumns = 1,
enhanced3d,
material3d,
columnIndex,
}) => {
// Detect server-side rendering
const isServer = typeof window === 'undefined';
// Use springs only if not on server and animations are enabled
// Even on server, we must call hooks unconditionally, so we provide static values
// Enhanced physics config for 3D modes
const physicsConfig = React.useMemo(() => {
if (!enableAnimation || isServer) return { duration: 0 };
if (!enhanced3d || enhanced3d === true || enhanced3d === 'subtle') return config.default;
return Abacus3DUtils.getPhysicsConfig(enhanced3d);
}, [enableAnimation, isServer, enhanced3d]);
const [{ x: springX, y: springY }, api] = useSpring(() => ({
x,
y,
config: enableAnimation && !isServer ? config.default : { duration: 0 }
config: physicsConfig
}));
// Arrow pulse animation for urgency indication
@@ -1363,11 +1367,11 @@ const Bead: React.FC<BeadProps> = ({
React.useEffect(() => {
if (enableAnimation) {
api.start({ x, y, config: { tension: 400, friction: 30, mass: 0.8 } });
api.start({ x, y, config: physicsConfig });
} else {
api.set({ x, y });
}
}, [x, y, enableAnimation, api]);
}, [x, y, enableAnimation, api, physicsConfig]);
// Pulse animation for direction arrows to indicate urgency
React.useEffect(() => {
@@ -1396,12 +1400,22 @@ const Bead: React.FC<BeadProps> = ({
const renderShape = () => {
const halfSize = size / 2;
// Determine fill - use gradient for realistic mode, otherwise use color
let fillValue = color;
if (enhanced3d === 'realistic' && columnIndex !== undefined) {
if (bead.type === 'heaven') {
fillValue = `url(#bead-gradient-${columnIndex}-heaven)`;
} else {
fillValue = `url(#bead-gradient-${columnIndex}-earth-${bead.position})`;
}
}
switch (shape) {
case "diamond":
return (
<polygon
points={`${size * 0.7},0 ${size * 1.4},${halfSize} ${size * 0.7},${size} 0,${halfSize}`}
fill={color}
fill={fillValue}
stroke="#000"
strokeWidth="0.5"
/>
@@ -1411,7 +1425,7 @@ const Bead: React.FC<BeadProps> = ({
<rect
width={size}
height={size}
fill={color}
fill={fillValue}
stroke="#000"
strokeWidth="0.5"
rx="1"
@@ -1424,7 +1438,7 @@ const Bead: React.FC<BeadProps> = ({
cx={halfSize}
cy={halfSize}
r={halfSize}
fill={color}
fill={fillValue}
stroke="#000"
strokeWidth="0.5"
/>
@@ -1458,8 +1472,7 @@ const Bead: React.FC<BeadProps> = ({
? {
transform: to(
[springX, springY],
(sx, sy) =>
`translate(${sx - getXOffset()}px, ${sy - getYOffset()}px)`,
(sx, sy) => `translate(${sx - getXOffset()}px, ${sy - getYOffset()}px)`,
),
cursor: enableGestures ? "grab" : onClick ? "pointer" : "default",
touchAction: "none" as const,
@@ -1571,7 +1584,6 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
// 3D enhancement props
enhanced3d,
material3d,
physics3d,
// Advanced customization props
customStyles,
callbacks,
@@ -1992,9 +2004,15 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
// console.log(`🎯 activeColumn changed to: ${activeColumn}`);
}, [activeColumn]);
// 3D Enhancement: Calculate container classes
const containerClasses = Abacus3DUtils.get3DContainerClasses(
enhanced3d,
material3d?.lighting
);
return (
<div
className="abacus-container"
className={containerClasses}
style={{
display: "inline-block",
textAlign: "center",
@@ -2053,6 +2071,68 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
opacity: 0 !important;
}
`}</style>
{/* 3D Enhancement: Material gradients for beads */}
{enhanced3d === 'realistic' && material3d && (
<>
{/* Generate gradients for all beads based on material type */}
{Array.from({ length: effectiveColumns }, (_, colIndex) => {
const placeValue = (effectiveColumns - 1 - colIndex) as ValidPlaceValues;
// Create dummy beads to get their colors
const heavenBead: BeadConfig = {
type: 'heaven',
value: 5,
active: true,
position: 0,
placeValue
};
const earthBead: BeadConfig = {
type: 'earth',
value: 1,
active: true,
position: 0,
placeValue
};
const heavenColor = getBeadColor(heavenBead, effectiveColumns, finalConfig.colorScheme, finalConfig.colorPalette, false);
const earthColor = getBeadColor(earthBead, effectiveColumns, finalConfig.colorScheme, finalConfig.colorPalette, false);
return (
<React.Fragment key={`gradients-col-${colIndex}`}>
{/* Heaven bead gradient */}
<defs dangerouslySetInnerHTML={{
__html: Abacus3DUtils.getBeadGradient(
`bead-gradient-${colIndex}-heaven`,
heavenColor,
material3d.heavenBeads || 'satin',
true
)
}} />
{/* Earth bead gradients */}
{[0, 1, 2, 3].map(pos => (
<defs key={`earth-${pos}`} dangerouslySetInnerHTML={{
__html: Abacus3DUtils.getBeadGradient(
`bead-gradient-${colIndex}-earth-${pos}`,
earthColor,
material3d.earthBeads || 'satin',
true
)
}} />
))}
</React.Fragment>
);
}).filter(Boolean)}
{/* Wood grain texture pattern */}
{material3d.woodGrain && (
<defs dangerouslySetInnerHTML={{
__html: Abacus3DUtils.getWoodGrainPattern('wood-grain-pattern')
}} />
)}
</>
)}
</defs>
{/* Background glow effects - rendered behind everything */}
@@ -2120,17 +2200,31 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
};
return (
<rect
key={`rod-pv${placeValue}`}
x={x - dimensions.rodWidth / 2}
y={rodStartY}
width={dimensions.rodWidth}
height={rodEndY - rodStartY}
fill={rodStyle.fill}
stroke={rodStyle.stroke}
strokeWidth={rodStyle.strokeWidth}
opacity={rodStyle.opacity}
/>
<React.Fragment key={`rod-pv${placeValue}`}>
<rect
x={x - dimensions.rodWidth / 2}
y={rodStartY}
width={dimensions.rodWidth}
height={rodEndY - rodStartY}
fill={rodStyle.fill}
stroke={rodStyle.stroke}
strokeWidth={rodStyle.strokeWidth}
opacity={rodStyle.opacity}
className="column-post"
/>
{/* Wood grain texture overlay for column posts */}
{enhanced3d === 'realistic' && material3d?.woodGrain && (
<rect
x={x - dimensions.rodWidth / 2}
y={rodStartY}
width={dimensions.rodWidth}
height={rodEndY - rodStartY}
fill="url(#wood-grain-pattern)"
className="frame-wood"
style={{ pointerEvents: 'none' }}
/>
)}
</React.Fragment>
);
})}
@@ -2146,7 +2240,22 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
stroke={customStyles?.reckoningBar?.stroke || "none"}
strokeWidth={customStyles?.reckoningBar?.strokeWidth ?? 0}
opacity={customStyles?.reckoningBar?.opacity ?? 1}
className="reckoning-bar"
/>
{/* Wood grain texture overlay for reckoning bar */}
{enhanced3d === 'realistic' && material3d?.woodGrain && (
<rect
x={dimensions.rodSpacing / 2 - dimensions.beadSize / 2}
y={barY}
width={
(effectiveColumns - 1) * dimensions.rodSpacing + dimensions.beadSize
}
height={dimensions.barThickness}
fill="url(#wood-grain-pattern)"
className="frame-wood"
style={{ pointerEvents: 'none' }}
/>
)}
{/* Beads */}
{beadStates.map((columnBeads, colIndex) =>
@@ -2329,6 +2438,9 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
colorScheme={finalConfig.colorScheme}
colorPalette={finalConfig.colorPalette}
totalColumns={effectiveColumns}
enhanced3d={enhanced3d}
material3d={material3d}
columnIndex={colIndex}
/>
);
}),