25 KiB
@soroban/abacus-react
A comprehensive React component for rendering interactive Soroban (Japanese abacus) visualizations with advanced customization and tutorial capabilities.
Features
- 🎯 Interactive beads - Click to toggle or use directional gestures
- 🎨 Complete visual customization - Style every element individually
- 📱 Responsive scaling - Configurable scale factor for different sizes
- 🌈 Multiple color schemes - Monochrome, place-value, alternating, heaven-earth
- 🎭 Flexible shapes - Diamond, square, or circle beads
- ⚡ React Spring animations - Smooth bead movements and transitions
- 🔧 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
- 🚀 Server Component support - AbacusStatic works in React Server Components (Next.js App Router)
Installation
npm install @soroban/abacus-react
# or
pnpm add @soroban/abacus-react
# or
yarn add @soroban/abacus-react
Quick Start
Basic Usage
Simple abacus showing a number
<AbacusReact value={123} columns={3} showNumbers={true} scaleFactor={1.0} />
Interactive Mode
Clickable abacus with animations
<AbacusReact
value={456}
columns={3}
interactive={true}
animated={true}
showNumbers={true}
callbacks={{
onValueChange: (newValue) => console.log("New value:", newValue),
onBeadClick: (event) => console.log("Bead clicked:", event),
}}
/>
Custom Styling
Personalized colors and highlights
<AbacusReact
value={789}
columns={3}
colorScheme="place-value"
beadShape="circle"
customStyles={{
heavenBeads: { fill: "#ff6b35" },
earthBeads: { fill: "#3498db" },
numerals: { color: "#2c3e50", fontWeight: "bold" },
}}
highlightBeads={[{ columnIndex: 1, beadType: "heaven" }]}
/>
Theme Presets
Use pre-defined themes for quick styling:
import { AbacusReact, ABACUS_THEMES } from '@soroban/abacus-react';
// Available themes: 'light', 'dark', 'trophy', 'translucent', 'solid', 'traditional'
<AbacusReact
value={123}
columns={3}
customStyles={ABACUS_THEMES.dark}
/>
<AbacusReact
value={456}
columns={3}
customStyles={ABACUS_THEMES.trophy} // Golden frame for achievements
/>
<AbacusReact
value={789}
columns={3}
customStyles={ABACUS_THEMES.traditional} // Brown wooden appearance
/>
Available Themes:
light- Solid white frame with subtle gray accents (best for light backgrounds)dark- Translucent white with subtle glow (best for dark backgrounds)trophy- Golden frame with warm tones (best for achievements/rewards)translucent- Nearly invisible frame (best for inline/minimal UI)solid- Black frame (best for high contrast/educational contexts)traditional- Brown wooden appearance (best for traditional soroban aesthetic)
Static Display (Server Components)
For static, non-interactive displays that work in React Server Components:
// IMPORTANT: Use /static import path for RSC compatibility!
import { AbacusStatic } from "@soroban/abacus-react/static";
// ✅ Works in React Server Components - no "use client" needed!
// ✅ No JavaScript sent to client
// ✅ Perfect for SSG, SSR, and static previews
<AbacusStatic value={123} columns="auto" hideInactiveBeads compact />;
Import paths:
@soroban/abacus-react- Full package (client components with hooks/animations)@soroban/abacus-react/static- Server-compatible components only (no client code)
Guaranteed Visual Consistency:
Both AbacusStatic and AbacusReact share the same underlying layout engine. Same props = same exact SVG output. This ensures:
- Static previews match interactive versions pixel-perfect
- Server-rendered abaci look identical to client-rendered ones
- PDF generation produces accurate representations
- No visual discrepancies between environments
Architecture: How We Guarantee Consistency
The package uses a shared rendering architecture with dependency injection:
┌─────────────────────────────────────────────┐
│ Shared Utilities (AbacusUtils.ts) │
│ • calculateStandardDimensions() - Single │
│ source of truth for all layout dimensions│
│ • calculateBeadPosition() - Exact bead │
│ positioning using shared formulas │
└────────────┬────────────────────────────────┘
│
├──────────────────────────────────┐
↓ ↓
┌─────────────────┐ ┌─────────────────┐
│ AbacusStatic │ │ AbacusReact │
│ (Server/Static) │ │ (Interactive) │
└────────┬────────┘ └────────┬────────┘
│ │
└────────────┬───────────────────┘
↓
┌────────────────────────┐
│ AbacusSVGRenderer │
│ • Pure SVG structure │
│ • Dependency injection │
│ • Bead component prop │
└────────────────────────┘
↓
┌───────────────┴───────────────┐
↓ ↓
┌──────────────┐ ┌──────────────────┐
│ AbacusStatic │ │ AbacusAnimated │
│ Bead │ │ Bead │
│ (Simple SVG) │ │ (react-spring) │
└──────────────┘ └──────────────────┘
Key Components:
calculateStandardDimensions()- Returns complete layout dimensions (bar position, bead sizes, gaps, etc.)calculateBeadPosition()- Calculates exact x,y coordinates for any beadAbacusSVGRenderer- Shared SVG rendering component that accepts a bead component via dependency injectionAbacusStaticBead- Simple SVG shapes for static display (no hooks, RSC-compatible)AbacusAnimatedBead- Client component with react-spring animations and gesture handling
This architecture eliminates code duplication (~560 lines removed in the refactor) while guaranteeing pixel-perfect consistency.
When to use AbacusStatic vs AbacusReact:
| Feature | AbacusStatic | AbacusReact |
|---|---|---|
| React Server Components | ✅ Yes | ❌ No (requires "use client") |
| Client-side JavaScript | ❌ None | ✅ Yes |
| User interaction | ❌ No | ✅ Click/drag beads |
| Animations | ❌ No | ✅ Smooth transitions |
| Sound effects | ❌ No | ✅ Optional sounds |
| 3D effects | ❌ No | ✅ Yes |
| Visual output | ✅ Identical | ✅ Identical |
| Bundle size | 📦 Minimal | 📦 Full-featured |
| Use cases | Preview cards, thumbnails, static pages, PDFs | Interactive tutorials, games, tools |
// Example: Server Component with static abacus cards
// app/flashcards/page.tsx
import { AbacusStatic } from "@soroban/abacus-react/static";
export default function FlashcardsPage() {
const numbers = [1, 5, 10, 25, 50, 100];
return (
<div className="grid grid-cols-3 gap-4">
{numbers.map((num) => (
<div key={num} className="card">
<AbacusStatic value={num} columns="auto" compact />
<p>{num}</p>
</div>
))}
</div>
);
}
Compact/Inline Display
Create mini abacus displays for inline use:
// Compact mode - automatically hides frame and optimizes spacing
<AbacusReact
value={7}
columns={1}
compact={true}
hideInactiveBeads={true}
scaleFactor={0.7}
/>
// Or manually control frame visibility
<AbacusReact
value={42}
columns={2}
frameVisible={false} // Hide column posts and reckoning bar
/>
Tutorial System
Educational guidance with tooltips and column highlighting
<AbacusReact
value={42}
columns={3}
interactive={true}
// Highlight the tens column with a label
highlightColumns={[1]} // Highlight column index 1 (tens)
columnLabels={["ones", "tens", "hundreds"]} // Add labels to columns
overlays={[
{
id: "tip",
type: "tooltip",
target: {
type: "bead",
columnIndex: 1,
beadType: "earth",
beadPosition: 1,
},
content: <div>Click this bead in the tens column!</div>,
offset: { x: 0, y: -30 },
},
]}
callbacks={{
onBeadClick: (event) => {
if (
event.columnIndex === 1 &&
event.beadType === "earth" &&
event.position === 1
) {
console.log("Correct! You clicked the tens column.");
}
},
}}
/>
Column Highlighting:
highlightColumns- Array of column indices to highlight (e.g.,[0, 2]highlights first and third columns)columnLabels- Optional labels displayed above each column (indexed left to right)
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.
<AbacusReact
value={12345}
columns={5}
enhanced3d="subtle"
interactive
animated
/>
Realistic Mode
Material-based rendering with lighting effects and textures.
<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 highlightssatin- Balanced shine (default)matte- Subtle shading, no shine
Lighting:
top-down- Balanced directional light from aboveambient- Soft light from all directionsdramatic- Strong directional light for high contrast
Delightful Mode
Maximum satisfaction with enhanced physics and interactive effects.
<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
Basic Props
interface AbacusConfig {
// Display
value?: number; // 0-99999, number to display
columns?: number | "auto"; // Number of columns or auto-calculate
showNumbers?: boolean; // Show place value numbers
scaleFactor?: number; // 0.5 - 3.0, size multiplier
// Appearance
beadShape?: "diamond" | "square" | "circle";
colorScheme?: "monochrome" | "place-value" | "alternating" | "heaven-earth";
colorPalette?: "default" | "colorblind" | "mnemonic" | "grayscale" | "nature";
hideInactiveBeads?: boolean; // Hide/show inactive beads
// Layout & Frame
frameVisible?: boolean; // Show/hide column posts and reckoning bar
compact?: boolean; // Compact layout (implies frameVisible=false)
// Interaction
interactive?: boolean; // Enable user interactions
animated?: boolean; // Enable animations
gestures?: boolean; // Enable drag gestures
// Tutorial Features
highlightColumns?: number[]; // Highlight specific columns by index
columnLabels?: string[]; // Optional labels for columns
}
Event Callbacks
interface AbacusCallbacks {
onValueChange?: (newValue: number) => void;
onBeadClick?: (event: BeadClickEvent) => void;
onBeadHover?: (event: BeadClickEvent) => void;
onBeadLeave?: (event: BeadClickEvent) => void;
onColumnClick?: (columnIndex: number) => void;
onNumeralClick?: (columnIndex: number, value: number) => void;
onBeadRef?: (bead: BeadConfig, element: SVGElement | null) => void;
}
interface BeadClickEvent {
columnIndex: number; // 0, 1, 2...
beadType: "heaven" | "earth"; // Type of bead
position: number; // Position within type (0-3 for earth)
active: boolean; // Current state
value: number; // Numeric value (1 or 5)
bead: BeadConfig; // Full bead configuration
}
Advanced Customization
Granular Styling
Target any visual element with precise control:
const customStyles = {
// Global defaults
heavenBeads: { fill: "#ff6b35" },
earthBeads: { fill: "#3498db" },
activeBeads: { opacity: 1.0 },
inactiveBeads: { opacity: 0.3 },
// Column-specific overrides
columns: {
0: {
// Hundreds column
heavenBeads: { fill: "#e74c3c" },
earthBeads: { fill: "#2ecc71" },
},
},
// Individual bead targeting
beads: {
1: {
// Middle column
heaven: { fill: "#f39c12" },
earth: {
0: { fill: "#1abc9c" }, // First earth bead
3: { fill: "#e67e22" }, // Fourth earth bead
},
},
},
// UI elements
reckoningBar: { stroke: "#34495e", strokeWidth: 3 },
columnPosts: { stroke: "#7f8c8d" },
numerals: {
color: "#2c3e50",
fontSize: "14px",
fontFamily: "monospace",
},
};
<AbacusReact customStyles={customStyles} />;
Tutorial and Overlay System
Create interactive educational experiences:
const overlays = [
{
id: "welcome-tooltip",
type: "tooltip",
target: {
type: "bead",
columnIndex: 0,
beadType: "earth",
beadPosition: 0,
},
content: (
<div
style={{
background: "#333",
color: "white",
padding: "8px",
borderRadius: "4px",
}}
>
Click me to start!
</div>
),
offset: { x: 0, y: -30 },
},
];
<AbacusReact
overlays={overlays}
highlightBeads={[{ columnIndex: 0, beadType: "earth", position: 0 }]}
callbacks={{
onBeadClick: (event) => {
if (
event.columnIndex === 0 &&
event.beadType === "earth" &&
event.position === 0
) {
console.log("Tutorial step completed!");
}
},
}}
/>;
Bead Reference System
Access individual bead DOM elements for advanced positioning:
function AdvancedExample() {
const beadRefs = useRef(new Map<string, SVGElement>());
const handleBeadRef = (bead: BeadConfig, element: SVGElement | null) => {
const key = `${bead.columnIndex}-${bead.type}-${bead.position}`;
if (element) {
beadRefs.current.set(key, element);
// Now you can position tooltips, highlights, etc. precisely
const rect = element.getBoundingClientRect();
console.log(`Bead at column ${bead.columnIndex} is at:`, rect);
}
};
return (
<AbacusReact
callbacks={{ onBeadRef: handleBeadRef }}
// ... other props
/>
);
}
Hooks
useAbacusDiff
Calculate bead differences between values for tutorials and animations:
import { useAbacusDiff } from "@soroban/abacus-react";
function Tutorial() {
const [currentValue, setCurrentValue] = useState(5);
const targetValue = 15;
// Get diff information: which beads need to move
const diff = useAbacusDiff(currentValue, targetValue);
return (
<div>
<p>{diff.summary}</p> {/* "add heaven bead in tens column, then..." */}
<AbacusReact
value={currentValue}
stepBeadHighlights={diff.highlights} // Highlight beads that need to change
interactive
onValueChange={setCurrentValue}
/>
<p>Changes needed: {diff.changes.length}</p>
</div>
);
}
Returns:
changes- Array of bead movements with direction and orderhighlights- Bead highlight data for stepBeadHighlights prophasChanges- Boolean indicating if any changes neededsummary- Human-readable description of changes (e.g., "add heaven bead in ones column")
useAbacusState
Convert numbers to abacus bead states:
import { useAbacusState } from "@soroban/abacus-react";
function BeadAnalyzer() {
const value = 123;
const state = useAbacusState(value);
// Check bead positions
const onesHasHeaven = state[0].heavenActive; // false (3 < 5)
const tensEarthCount = state[1].earthActive; // 2 (20 = 2 tens)
return (
<div>Ones column heaven bead: {onesHasHeaven ? "active" : "inactive"}</div>
);
}
useAbacusDimensions
Get exact sizing information for layout planning:
import { useAbacusDimensions } from "@soroban/abacus-react";
function MyComponent() {
const dimensions = useAbacusDimensions(3, 1.2); // 3 columns, 1.2x scale
return (
<div style={{ width: dimensions.width, height: dimensions.height }}>
<AbacusReact columns={3} scaleFactor={1.2} />
</div>
);
}
Utility Functions
Low-level functions for working with abacus states and calculations:
numberToAbacusState
Convert a number to bead positions:
import { numberToAbacusState } from "@soroban/abacus-react";
const state = numberToAbacusState(123, 5); // 5 columns
// Returns: {
// 0: { heavenActive: false, earthActive: 3 }, // ones = 3
// 1: { heavenActive: false, earthActive: 2 }, // tens = 2
// 2: { heavenActive: true, earthActive: 0 }, // hundreds = 1
// ...
// }
abacusStateToNumber
Convert bead positions back to a number:
import { abacusStateToNumber } from "@soroban/abacus-react";
const state = {
0: { heavenActive: false, earthActive: 3 },
1: { heavenActive: false, earthActive: 2 },
2: { heavenActive: true, earthActive: 0 },
};
const value = abacusStateToNumber(state); // 123
calculateBeadDiff
Calculate the exact bead movements needed between two states:
import { calculateBeadDiff, numberToAbacusState } from "@soroban/abacus-react";
const fromState = numberToAbacusState(5);
const toState = numberToAbacusState(15);
const diff = calculateBeadDiff(fromState, toState);
console.log(diff.summary); // "add heaven bead in tens column"
console.log(diff.changes); // Detailed array of movements with order
calculateBeadDiffFromValues
Convenience wrapper for calculating diff from numbers:
import { calculateBeadDiffFromValues } from "@soroban/abacus-react";
const diff = calculateBeadDiffFromValues(42, 57);
// Equivalent to: calculateBeadDiff(numberToAbacusState(42), numberToAbacusState(57))
validateAbacusValue
Check if a value is within the supported range:
import { validateAbacusValue } from "@soroban/abacus-react";
const result = validateAbacusValue(123456, 5); // 5 columns max
console.log(result.isValid); // false
console.log(result.error); // "Value exceeds maximum for 5 columns (max: 99999)"
areStatesEqual
Compare two abacus states:
import { areStatesEqual, numberToAbacusState } from "@soroban/abacus-react";
const state1 = numberToAbacusState(123);
const state2 = numberToAbacusState(123);
const isEqual = areStatesEqual(state1, state2); // true
calculateStandardDimensions
⚡ Core Architecture Function - Calculate complete layout dimensions for consistent rendering.
This is the single source of truth for all layout dimensions, used internally by both AbacusStatic and AbacusReact to guarantee pixel-perfect consistency.
import { calculateStandardDimensions } from "@soroban/abacus-react";
const dimensions = calculateStandardDimensions({
columns: 3,
scaleFactor: 1.5,
showNumbers: true,
columnLabels: ["ones", "tens", "hundreds"],
});
// Returns complete layout info:
// {
// width, height, // SVG canvas size
// beadSize, // 12 * scaleFactor (standard bead size)
// rodSpacing, // 25 * scaleFactor (column spacing)
// rodWidth, // 3 * scaleFactor
// barThickness, // 2 * scaleFactor
// barY, // Reckoning bar Y position (30 * scaleFactor + labels)
// heavenY, earthY, // Inactive bead rest positions
// activeGap, // 1 * scaleFactor (gap to bar when active)
// inactiveGap, // 8 * scaleFactor (gap between active/inactive)
// adjacentSpacing, // 0.5 * scaleFactor (spacing between adjacent beads)
// padding, labelHeight, numbersHeight, totalColumns
// }
Why this matters: Same input parameters = same exact layout dimensions = pixel-perfect visual consistency across static and interactive displays.
calculateBeadPosition
⚡ Core Architecture Function - Calculate exact x,y coordinates for any bead.
Used internally by AbacusSVGRenderer to position all beads consistently in both static and interactive modes.
import {
calculateBeadPosition,
calculateStandardDimensions,
} from "@soroban/abacus-react";
const dimensions = calculateStandardDimensions({ columns: 3, scaleFactor: 1 });
const bead = {
type: "heaven",
active: true,
position: 0,
placeValue: 1, // tens column
};
const position = calculateBeadPosition(bead, dimensions);
// Returns: { x: 25, y: 29 } // exact pixel coordinates
Useful for custom rendering or positioning tooltips/overlays relative to specific beads.
Educational Use Cases
Interactive Math Lessons
function MathLesson() {
const [problem, setProblem] = useState({ a: 23, b: 45 });
const [step, setStep] = useState("show-first");
return (
<div>
<h3>
Add {problem.a} + {problem.b}
</h3>
<AbacusReact
value={step === "show-first" ? problem.a : 0}
interactive={step === "add-second"}
callbacks={{
onValueChange: (value) => {
if (value === problem.a + problem.b) {
celebrate();
}
},
}}
/>
</div>
);
}
Assessment Tools
function AbacusQuiz() {
const [answers, setAnswers] = useState([]);
const checkAnswer = (event: BeadClickEvent) => {
const isCorrect = validateBeadClick(event, expectedAnswer);
recordAnswer(event, isCorrect);
if (isCorrect) {
showSuccessFeedback();
} else {
showHint(event);
}
};
return (
<AbacusReact
interactive={true}
callbacks={{ onBeadClick: checkAnswer }}
customStyles={getAnswerHighlighting(answers)}
/>
);
}
TypeScript Support
Full TypeScript definitions included:
import {
// Components
AbacusReact,
// Hooks
useAbacusDiff,
useAbacusState,
useAbacusDimensions,
// Utility Functions
numberToAbacusState,
abacusStateToNumber,
calculateBeadDiff,
calculateBeadDiffFromValues,
validateAbacusValue,
areStatesEqual,
calculateStandardDimensions, // NEW: Shared layout calculator
calculateBeadPosition, // NEW: Bead position calculator
// Theme Presets
ABACUS_THEMES,
// Types
AbacusConfig,
BeadConfig,
BeadClickEvent,
AbacusCustomStyles,
AbacusOverlay,
AbacusCallbacks,
AbacusState,
BeadState,
BeadDiffResult,
BeadDiffOutput,
AbacusThemeName,
AbacusLayoutDimensions, // NEW: Complete layout dimensions type
BeadPositionConfig, // NEW: Bead config for position calculation
} from "@soroban/abacus-react";
// All interfaces fully typed for excellent developer experience
Contributing
Contributions welcome! Please see our contributing guidelines and feel free to submit issues or pull requests.
License
MIT License - see LICENSE file for details.