Repository navigation
Add 3D WebGL board rendering with Three.js - #196
Closed
adrienbrault wants to merge 29 commits into
Closed
adrienbrault wants to merge 29 commits into
adrienbrault wants to merge 29 commits into
Conversation
The board is getting a 3D scene with lighting, tile depth and particle effects. Three.js is loaded lazily by that renderer only, so the initial bundle and every other screen stay unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The WebGL board sits under the DOM grid and must place each tile exactly where its DOM cell is, gaps included, and resolve a pointer to the cell beneath it for hover effects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Gives the WebGL renderer one source for where each cell sits and which cell a pointer is over. Gap pixels resolve to the following cell so hover never flickers off while crossing a seam. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The 3D board animates what just happened: a digit popping in, one being erased, a row, column or box being completed. Those moments are derived from two snapshots rather than threaded through every game action. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The 3D board diffs each render against the previous one to trigger pop-in, pop-out and house-completion ripples. A house only counts once all nine digits are distinct, so a wrong digit that fills a row does not get celebrated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The settings popover and the board are separate components that both need the toggle, so the preference behaves like a tiny store rather than per-component state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
On by default, persisted in localStorage, and broadcast to every subscriber so the settings toggle and the board stay in step. The WebGL probe runs once and releases its context, and short-circuits where WebGL2 does not exist at all (jsdom, legacy browsers) so the board silently keeps its DOM rendering there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The settings popover flips the preference while the board is mounted; the board must pick it up without a reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
useSyncExternalStore keeps all mounted copies consistent, and the active flag folds in WebGL support so callers ask one question. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The flags were computed inline while rendering each DOM cell. The WebGL board needs the exact same states, so they now come from computeCellVisuals and both renderers read the one result. Behavior is unchanged; the Board tests cover it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Tile lifts, digit pops and the selection cursor all move on springs so interrupted animations retarget smoothly instead of restarting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Fixed sub-steps keep stiff springs stable when a frame is dropped, and kick() lets an effect add an impulse (a press, a bounce) while the resting position stays owned by the board state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The project enables erasableSyntaxOnly, which rejects constructor parameter properties; assign the fields explicitly instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Biome formatting for the test introduced by the parent commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
A self-contained WebGL renderer for the board: beveled glossy tiles on box plates, canvas-drawn glyphs and pencil notes, and contact shadows that grow as tiles lift. It reads the CSS design tokens, so dark mode and every digit palette (tinted, colors, emoji) follow the DOM board exactly. It is driven by board state alone and animates the difference between renders: digits pop in with a ripple and sparks, erased ones spin out, conflicts shake, a completed row, column or box lights up in a wave, and a solved board flips tile by tile under confetti. A selection cursor glides between cells and the board tilts toward the pointer. Rendering stops when nothing moves and idle pulses run at half rate, and prefers-reduced-motion drops the motion but keeps the state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The three.js scene is lazy-loaded under the existing grid, which turns see-through once the first 3D frame is on screen. Keeping the DOM grid on top means taps, drag-select, digit drags, keyboard focus and screen-reader semantics are untouched, and drop previews still draw over the scene. Without WebGL, or if the context is lost, the board simply stays DOM-rendered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Players on weak devices or who prefer the flat board need a way out, and the switch must not appear where WebGL cannot render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Sits with the other display preferences and is hidden when the browser has no WebGL, where it would do nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
With the 3D board, solving triggers a flip wave and confetti across the tiles; opening the result after 300ms covered it immediately. The flat board keeps its original timing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The scene file ran past the 300-line lint limit; expose the glyph cache as a field instead of a getter to bring it back under. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Free the WebGL context when the scene is disposed instead of waiting for GC; browsers cap live contexts and every game screen mounts a fresh board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The lazily loaded scene often arrives after the 600ms reveal window, so it skipped its entrance and tiles just appeared. Capture the reveal intent at mount and hand that to the scene's first frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Move theme painting of the board body into scene-layout so the scene file stays under the 300-line lint limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The landing demo renders a Board too, so the landing page was loading three.js and redrawing a scene the demo keeps animating. In software-rendered WebGL that starved the main thread badly enough that e2e clicks on the landing buttons timed out. Boards now opt in with threeD; solo and multiplayer games do, the demo stays DOM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
The first pass made every cell a thick glossy key with deep grooves and a grey falloff across its face, which read as a dated keyboard rather than a sudoku grid. Cells are now thin, square-edged and matte, butt up to the same 1px lines as the flat board, and are lit so their faces match the design tokens (three.js lights are scaled by pi, which the old intensities ignored). Depth is kept for interaction: selected, hovered and matching tiles lift further and cast a contact shadow, so the 3D shows up when something happens instead of on every cell at rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Deploying sudoku with
|
| Latest commit: |
d961e24
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f80cfc03.sudoku-4cc.pages.dev |
| Branch Preview URL: | https://claude-threejs-interactive-b.sudoku-4cc.pages.dev |
The three.js scene files need a WebGL context and 2D canvases that jsdom does not provide, so they read as 0% and pulled global coverage from ~95% to 77%, failing the CI gate. They are verified by the Playwright screenshots instead. spring.ts stays measured, as do the pure modules the renderer relies on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Without a GPU (blocklisted driver, some VMs, headless browsers) the browser still hands out a WebGL context but rasterizes on the CPU. The scene then renders on the main thread and taps stall for seconds; in e2e runs clicks on the board timed out. The probe now asks for failIfMajorPerformanceCaveat, so those players keep the fast DOM board. A "force" preference value opts back in, for capturing the 3D board in headless screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
Headless Chromium only offers software WebGL, which the app now refuses, so every existing scene shows the DOM board. These scenes force the WebGL board on, in light and dark, so layout and styling regressions in the renderer stay visible in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
…tion The glows kept breathing for players who asked the OS for reduced motion, and that pulse kept the render loop running at 30fps while the board sat idle. Freezing the pulse clock honors the setting and lets the loop stop between changes; on slow GPUs this was enough continuous work to delay taps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an optional 3D WebGL board that renders beneath the DOM grid, bringing depth, animation, and visual polish to the Sudoku experience. The 3D board is enabled by default but can be toggled off in settings, with graceful fallback to the 2D DOM board if WebGL is unavailable.
Key Changes
3D Board Engine (
src/lib/board-scene/): Complete WebGL rendering system built with Three.jsscene.ts: MainBoardSceneclass managing the 3D board lifecycle, camera, lighting, and frame looptile.ts: Individual cell tiles with spring-based animations for lift, scale, rotation, and visual effectsparticles.ts: CPU-simulated particle system for sparks and confetti effectseffects.ts: Cursor frame and ripple rings that respond to user interactionschoreography.ts: Animation sequences for reveal, placement, clearing, and victory celebrationsglyphs.ts: Canvas-rendered digit textures that follow the app's digit color mode and dark modetheme.ts: CSS design token resolution so the 3D board matches the DOM board's palette exactlyscene-layout.ts: Geometry and lighting setup; mirrors DOM grid pixel geometry so tiles land under cellsscene-state.ts: State diffing and tile painting logic; applies board changes as animationsspring.ts: Damped harmonic spring physics for smooth, interruptible animationsCell Visuals Extraction (
src/lib/cell-visuals.ts): Centralized computation of per-cell highlight state (selection, conflicts, hints, drag targets, same-number highlights). Both DOM and WebGL renderers now consume the sameCellVisual[]array, ensuring they never disagree about what is selected or highlighted.Board Event Diffing (
src/lib/board-events.ts): Detects which cells changed between board snapshots and which rows/columns/boxes became complete, enabling targeted animations instead of re-rendering the entire board.3D Board Preference (
src/lib/board-3d.ts): localStorage-backed toggle for the 3D board, with WebGL capability detection. Preference persists across sessions.React Integration
src/hooks/useBoard3D.ts: Hook exposing the 3D board preference and WebGL support statussrc/components/BoardSceneLayer.tsx: Lazy-loaded component hosting the WebGL canvas; handles context loss and first-frame signalingsrc/components/BoardDepthToggle.tsx: Settings UI to toggle the 3D board on/offBoard Component Updates (
src/components/Board.tsx): Refactored to use centralized cell visuals and conditionally render the 3D board layer. The DOM grid remains on top for all gestures, focus, and accessibility.Layout Utilities (
src/lib/board-layout.ts): Pixel geometry calculations (padding, gaps, cell offsets) shared between DOM and WebGL so they stay perfectly aligned.Tests: Added unit tests for spring physics, board diffing, board layout calculations, 3D preference persistence, and the useBoard3D hook.
Styling (
src/index.css): CSS for the 3D board container and grid transparency when the WebGL board is active.Dependencies: Added
three@^0.186.1to package.json.Implementation Details
lazyandSuspense), keeping the main bundle size unchanged for users who disable it.https://claude.ai/code/session_01PcxowZMreMKHPQKo4FB2HV