soroban-abacus-flashcards/packages/abacus-react/README.md

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

Basic Usage
<AbacusReact value={123} columns={3} showNumbers={true} scaleFactor={1.0} />

Interactive Mode

Clickable abacus with animations

Interactive Mode
<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

Custom Styling
<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:

  1. calculateStandardDimensions() - Returns complete layout dimensions (bar position, bead sizes, gaps, etc.)
  2. calculateBeadPosition() - Calculates exact x,y coordinates for any bead
  3. AbacusSVGRenderer - Shared SVG rendering component that accepts a bead component via dependency injection
  4. AbacusStaticBead - Simple SVG shapes for static display (no hooks, RSC-compatible)
  5. 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

Tutorial System
<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 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.

<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 order
  • highlights - Bead highlight data for stepBeadHighlights prop
  • hasChanges - Boolean indicating if any changes needed
  • summary - 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.