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>
This commit is contained in:
Thomas Hallock 2025-11-03 14:47:51 -06:00
parent 096104b094
commit 5ac55cc149
1 changed files with 212 additions and 21 deletions

View File

@ -1247,6 +1247,13 @@ interface BeadProps {
colorScheme?: string;
colorPalette?: string;
totalColumns?: number;
// 3D Enhancement
enhanced3d?: boolean | "subtle" | "realistic" | "delightful";
material3d?: Abacus3DMaterial;
physics3d?: Abacus3DPhysics;
columnIndex?: number;
mousePosition?: { x: number; y: number };
containerBounds?: { x: number; y: number };
}
const Bead: React.FC<BeadProps> = ({
@ -1275,18 +1282,50 @@ const Bead: React.FC<BeadProps> = ({
colorScheme = "monochrome",
colorPalette = "default",
totalColumns = 1,
enhanced3d,
material3d,
physics3d,
columnIndex,
mousePosition,
containerBounds,
}) => {
// 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
}));
// Track velocity for wobble effect (delightful mode only)
const velocityRef = useRef(0);
const lastYRef = useRef(y);
// Calculate parallax offset for hover effect
const parallaxOffset = React.useMemo(() => {
if (enhanced3d === 'delightful' && physics3d?.hoverParallax && mousePosition && containerBounds) {
return Abacus3DUtils.calculateParallaxOffset(
x,
y,
mousePosition.x,
mousePosition.y,
containerBounds.x,
containerBounds.y,
0.5
);
}
return { x: 0, y: 0, z: 0 };
}, [enhanced3d, physics3d?.hoverParallax, mousePosition, containerBounds, x, y]);
// Arrow pulse animation for urgency indication
const [{ arrowPulse }, arrowApi] = useSpring(() => ({
arrowPulse: 1,
@ -1363,11 +1402,16 @@ const Bead: React.FC<BeadProps> = ({
React.useEffect(() => {
if (enableAnimation) {
api.start({ x, y, config: { tension: 400, friction: 30, mass: 0.8 } });
// Calculate velocity for wobble effect
const deltaY = y - lastYRef.current;
velocityRef.current = deltaY;
lastYRef.current = y;
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 +1440,22 @@ const Bead: React.FC<BeadProps> = ({
const renderShape = () => {
const halfSize = size / 2;
// Determine fill - use gradient for realistic/delightful modes, otherwise use color
let fillValue = color;
if ((enhanced3d === 'realistic' || enhanced3d === 'delightful') && 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 +1465,7 @@ const Bead: React.FC<BeadProps> = ({
<rect
width={size}
height={size}
fill={color}
fill={fillValue}
stroke="#000"
strokeWidth="0.5"
rx="1"
@ -1424,7 +1478,7 @@ const Bead: React.FC<BeadProps> = ({
cx={halfSize}
cy={halfSize}
r={halfSize}
fill={color}
fill={fillValue}
stroke="#000"
strokeWidth="0.5"
/>
@ -1454,13 +1508,31 @@ const Bead: React.FC<BeadProps> = ({
};
// Build style object based on animation mode
const wobbleEnabled = enhanced3d === 'delightful' && physics3d?.wobble;
const parallaxEnabled = enhanced3d === 'delightful' && physics3d?.hoverParallax;
const beadStyle: any = enableAnimation
? {
transform: to(
[springX, springY],
(sx, sy) =>
`translate(${sx - getXOffset()}px, ${sy - getYOffset()}px)`,
(sx, sy) => {
const translate = `translate(${sx - getXOffset() + parallaxOffset.x}px, ${sy - getYOffset() + parallaxOffset.y}px)`;
const transforms = [translate];
// Add parallax Z translation
if (parallaxEnabled && parallaxOffset.z > 0) {
transforms.push(`translateZ(${parallaxOffset.z}px)`);
}
// Add wobble rotation
if (wobbleEnabled && velocityRef.current !== 0) {
transforms.push(Abacus3DUtils.getWobbleRotation(velocityRef.current, 'x'));
}
return transforms.join(' ');
},
),
transformOrigin: 'center center',
transformStyle: 'preserve-3d',
cursor: enableGestures ? "grab" : onClick ? "pointer" : "default",
touchAction: "none" as const,
transition: "opacity 0.2s ease-in-out",
@ -1992,14 +2064,36 @@ 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,
physics3d?.hoverParallax
);
// 3D Enhancement: Track mouse position for parallax
const containerRef = useRef<HTMLDivElement>(null);
const [mousePos, setMousePos] = React.useState({ x: 0, y: 0 });
const [containerBounds, setContainerBounds] = React.useState({ x: 0, y: 0 });
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (enhanced3d === 'delightful' && physics3d?.hoverParallax && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setMousePos({ x: e.clientX, y: e.clientY });
setContainerBounds({ x: rect.left, y: rect.top });
}
}, [enhanced3d, physics3d?.hoverParallax]);
return (
<div
className="abacus-container"
ref={containerRef}
className={containerClasses}
style={{
display: "inline-block",
textAlign: "center",
position: "relative",
}}
onMouseMove={handleMouseMove}
tabIndex={
finalConfig.interactive && finalConfig.showNumbers ? 0 : undefined
}
@ -2053,6 +2147,68 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
opacity: 0 !important;
}
`}</style>
{/* 3D Enhancement: Material gradients for beads */}
{(enhanced3d === 'realistic' || enhanced3d === 'delightful') && 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 +2276,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' || enhanced3d === 'delightful') && 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 +2316,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' || enhanced3d === 'delightful') && 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 +2514,12 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
colorScheme={finalConfig.colorScheme}
colorPalette={finalConfig.colorPalette}
totalColumns={effectiveColumns}
enhanced3d={enhanced3d}
material3d={material3d}
physics3d={physics3d}
columnIndex={colIndex}
mousePosition={mousePos}
containerBounds={containerBounds}
/>
);
}),