nordic-skating / mistral-medium-35-littlejs / files / LITTLEJS_MIGRATION_STEPS.md
Branch mistral-medium-35-littlejs · default branch · all branches
LITTLEJS_MIGRATION_STEPS.md
185c6976 2026-10-19 Pavel Safronov | 1 | # LittleJS Migration: Step-by-Step Implementation Guide |
| 2 | ||
| 3 | This is a **practical, actionable guide** to migrate Nordic Skating to LittleJS. Follow these steps in order. | |
| 4 | ||
| 5 | --- | |
| 6 | ||
| 7 | ## Phase 0: Preparation (1-2 hours) | |
| 8 | ||
| 9 | ### Step 0.1: Install LittleJS | |
| 10 | ```bash | |
| 11 | cd /Users/pavel/code/nordic-skating | |
| 12 | npm install littlejsengine | |
| 13 | ``` | |
| 14 | ||
| 15 | ### Step 0.2: Back up current code | |
| 16 | ```bash | |
| 17 | cp src/main.ts src/main.ts.backup | |
| 18 | cp index.html index.html.backup | |
| 19 | ``` | |
| 20 | ||
| 21 | ### Step 0.3: Test POC (Optional but recommended) | |
| 22 | ```bash | |
| 23 | cd LITTLEJS_POC | |
| 24 | npm install | |
| 25 | npm run dev | |
| 26 | # Test in browser, then Ctrl+C | |
| 27 | cd .. | |
| 28 | ``` | |
| 29 | ||
| 30 | --- | |
| 31 | ||
| 32 | ## Phase 1: Minimal Integration - Input + Game Loop (1-2 days) | |
| 33 | ||
| 34 | ### Step 1.1: Create new entry point | |
| 35 | Create `src/main-littlejs.ts`: | |
| 36 | ||
| 37 | ```typescript | |
| 38 | import * as THREE from 'three' | |
| 39 | import { | |
| 40 | engineInit, | |
| 41 | engine, | |
| 42 | inputGetState, | |
| 43 | inputGetPressed, | |
| 44 | inputGetAxis, | |
| 45 | setThreeRenderer, | |
| 46 | vec2, | |
| 47 | colorWhite, | |
| 48 | drawTextScreen | |
| 49 | } from 'littlejsengine' | |
| 50 | ||
| 51 | // Import all existing modules | |
| 52 | import skateIcon from '../assets/skate-shoes.png' | |
| 53 | import soundOffIcon from '../assets/sound-off.png' | |
| 54 | import soundOnIcon from '../assets/sound-on.png' | |
| 55 | import tiltOffIcon from '../assets/tilt-off.png' | |
| 56 | import tiltOnIcon from '../assets/tilt-on.png' | |
| 57 | import { IceSound } from './audio/iceSound.js' | |
| 58 | import { Input as OldInput } from './input.js' | |
| 59 | import { Drifts } from './render/drifts.js' | |
| 60 | import { Ice } from './render/ice.js' | |
| 61 | import { Marks } from './render/marks.js' | |
| 62 | import { Shores } from './render/shores.js' | |
| 63 | import { LOOKS, Sky, TIME_NAMES, type TimeName } from './render/sky.js' | |
| 64 | import { Snow } from './render/snow.js' | |
| 65 | import { WEATHER, WEATHERS, type Weather, anyLake, loadSettings, saveSettings, timeOf, weatherOf, windOf } from './settings.js' | |
| 66 | import { BREAKING_THICKNESS, CRACKING_THICKNESS } from './ice/strength.js' | |
| 67 | import { PACES, PACE_NAMES, type Pace, UNDER_SECONDS, createSkater, pushSeconds, sincePush, stepSkater } from './skater.js' | |
| 68 | import { createLake } from './world/lake.js' | |
| 69 | ||
| 70 | // Make THREE available globally | |
| 71 | if (typeof window !== 'undefined') { | |
| 72 | (window as any).THREE = THREE | |
| 73 | } | |
| 74 | ||
| 75 | class NordicSkating { | |
| 76 | // All existing fields from main.ts | |
| 77 | private lake: any | |
| 78 | private skater: any | |
| 79 | private scene: THREE.Scene | |
| 80 | private camera: THREE.PerspectiveCamera | |
| 81 | private renderer: THREE.WebGLRenderer | |
| 82 | // ... etc (copy all from main.ts) | |
| 83 | ||
| 84 | constructor() { | |
| 85 | // Get canvas | |
| 86 | const canvas = document.querySelector<HTMLCanvasElement>('#scene')! | |
| 87 | ||
| 88 | // Configure LittleJS engine | |
| 89 | engine.canvas = canvas | |
| 90 | engine.canvasSize = vec2(canvas.width, canvas.height) | |
| 91 | ||
| 92 | // Initialize Three.js renderer | |
| 93 | this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true }) | |
| 94 | this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) | |
| 95 | ||
| 96 | // Set Three.js renderer for LittleJS | |
| 97 | setThreeRenderer(THREE) | |
| 98 | ||
| 99 | // Now run existing initialization code | |
| 100 | this.initializeGame() | |
| 101 | ||
| 102 | // Initialize LittleJS | |
| 103 | engineInit( | |
| 104 | () => this.gameInit(), | |
| 105 | () => this.gameUpdate(), | |
| 106 | () => this.gameUpdatePost(), | |
| 107 | () => this.gameRender(), | |
| 108 | () => this.gameRenderPost() | |
| 109 | ) | |
| 110 | } | |
| 111 | ||
| 112 | private initializeGame(): void { | |
| 113 | // Copy all initialization code from main.ts | |
| 114 | // But replace custom input with LittleJS input | |
| 115 | ||
| 116 | // ... (all existing init code) | |
| 117 | ||
| 118 | // REPLACE: Remove old Input initialization | |
| 119 | // OLD: const input = new Input(canvas) | |
| 120 | // NEW: Use LittleJS input system directly | |
| 121 | } | |
| 122 | ||
| 123 | gameInit(): void { | |
| 124 | // Initialization that needs to happen in LittleJS context | |
| 125 | } | |
| 126 | ||
| 127 | gameUpdate(): void { | |
| 128 | // NEW: Handle input using LittleJS | |
| 129 | this.handleLittleJSInput() | |
| 130 | ||
| 131 | // EXISTING: All update logic from main.ts animation loop | |
| 132 | const passed = engine.timing.lastTimeStep | |
| 133 | const dt = this.waiting() ? 0 : passed | |
| 134 | ||
| 135 | // ... rest of update logic | |
| 136 | } | |
| 137 | ||
| 138 | private handleLittleJSInput(): void { | |
| 139 | // Map LittleJS input to existing input system | |
| 140 | ||
| 141 | // Steering | |
| 142 | let steer = 0 | |
| 143 | steer += inputGetState('arrowLeft') ? -1 : 0 | |
| 144 | steer += inputGetState('arrowRight') ? 1 : 0 | |
| 145 | steer += inputGetState('a') ? -1 : 0 | |
| 146 | steer += inputGetState('d') ? 1 : 0 | |
| 147 | steer += inputGetAxis('mouseX') * 0.5 | |
| 148 | ||
| 149 | // Store for use in existing code | |
| 150 | this.currentSteer = steer | |
| 151 | ||
| 152 | // Button presses | |
| 153 | if (inputGetPressed('m')) { | |
| 154 | this.toggleMute() | |
| 155 | } | |
| 156 | if (inputGetPressed('r')) { | |
| 157 | this.randomize() | |
| 158 | } | |
| 159 | if (inputGetPressed('arrowUp') || inputGetPressed('w')) { | |
| 160 | this.stepPace(1, true) | |
| 161 | } | |
| 162 | if (inputGetPressed('arrowDown') || inputGetPressed('s')) { | |
| 163 | this.stepPace(-1, true) | |
| 164 | } | |
| 165 | ||
| 166 | // Gamepad | |
| 167 | for (const button of this.pollLittleJSPads()) { | |
| 168 | // ... handle gamepad buttons | |
| 169 | } | |
| 170 | ||
| 171 | // Start sound on any input | |
| 172 | if (Math.abs(steer) > 0 || | |
| 173 | inputGetPressed('m') || | |
| 174 | inputGetPressed('r') || | |
| 175 | inputGetPressed('arrowUp') || | |
| 176 | inputGetPressed('arrowDown')) { | |
| 177 | this.startSound() | |
| 178 | } | |
| 179 | } | |
| 180 | ||
| 181 | private pollLittleJSPads(): number[] { | |
| 182 | // Implement gamepad polling using LittleJS | |
| 183 | const pressed: number[] = [] | |
| 184 | const down = new Set<number>() | |
| 185 | ||
| 186 | for (const pad of navigator.getGamepads?.() ?? []) { | |
| 187 | if (!pad) continue | |
| 188 | ||
| 189 | // Check buttons | |
| 190 | pad.buttons.forEach((button, n) => { | |
| 191 | if (button.pressed) { | |
| 192 | down.add(n) | |
| 193 | if (!this.padDown.has(n)) pressed.push(n) | |
| 194 | } | |
| 195 | }) | |
| 196 | } | |
| 197 | ||
| 198 | this.padDown = down | |
| 199 | return pressed | |
| 200 | } | |
| 201 | ||
| 202 | gameUpdatePost(): void { | |
| 203 | // Post-update logic | |
| 204 | } | |
| 205 | ||
| 206 | gameRender(): void { | |
| 207 | // Render the Three.js scene | |
| 208 | this.renderer.render(this.scene, this.camera) | |
| 209 | } | |
| 210 | ||
| 211 | gameRenderPost(): void { | |
| 212 | // Draw HUD using LittleJS (optional) | |
| 213 | // Can keep existing DOM-based UI or use LittleJS | |
| 214 | } | |
| 215 | } | |
| 216 | ||
| 217 | // Initialize when ready | |
| 218 | new NordicSkating() | |
| 219 | ``` | |
| 220 | ||
| 221 | ### Step 1.2: Update index.html | |
| 222 | Replace the script tag: | |
| 223 | ```html | |
| 224 | <!-- OLD --> | |
| 225 | <script type="module" src="/src/main.ts"></script> | |
| 226 | ||
| 227 | <!-- NEW --> | |
| 228 | <script type="module" src="/src/main-littlejs.ts"></script> | |
| 229 | ``` | |
| 230 | ||
| 231 | ### Step 1.3: Update vite.config.ts (if needed) | |
| 232 | Ensure LittleJS can be resolved: | |
| 233 | ```typescript | |
| 234 | import { defineConfig } from 'vite' | |
| 235 | ||
| 236 | export default defineConfig({ | |
| 237 | resolve: { | |
| 238 | alias: { | |
| 239 | littlejsengine: 'littlejsengine/dist/littlejs.esm.js' | |
| 240 | } | |
| 241 | } | |
| 242 | }) | |
| 243 | ``` | |
| 244 | ||
| 245 | ### Step 1.4: Test basic functionality | |
| 246 | ```bash | |
| 247 | npm run dev | |
| 248 | ``` | |
| 249 | ||
| 250 | **Test checklist:** | |
| 251 | - [ ] Page loads without errors | |
| 252 | - [ ] Keyboard steering works | |
| 253 | - [ ] Mouse/touch steering works | |
| 254 | - [ ] Gamepad works (if available) | |
| 255 | - [ ] Pace changes work | |
| 256 | - [ ] Random lake works | |
| 257 | - [ ] UI still functions | |
| 258 | - [ ] Audio works | |
| 259 | ||
| 260 | --- | |
| 261 | ||
| 262 | ## Phase 2: Input System Migration (1 day) | |
| 263 | ||
| 264 | ### Step 2.1: Update tilt steering | |
| 265 | Replace custom device orientation with LittleJS: | |
| 266 | ```typescript | |
| 267 | // OLD: Custom tilt handling in Input class | |
| 268 | // NEW: Use LittleJS device orientation | |
| 269 | ||
| 270 | // In gameUpdate(): | |
| 271 | const tiltX = inputGetAxis('deviceOrientationGamma') | |
| 272 | const tiltY = inputGetAxis('deviceOrientationBeta') | |
| 273 | // Process tilt... | |
| 274 | ``` | |
| 275 | ||
| 276 | ### Step 2.2: Remove old Input class usage | |
| 277 | Gradually replace all calls to the old Input class with LittleJS input functions. | |
| 278 | ||
| 279 | ### Step 2.3: Test on mobile | |
| 280 | - Test on iOS and Android devices | |
| 281 | - Verify tilt steering works | |
| 282 | - Check touch input | |
| 283 | ||
| 284 | --- | |
| 285 | ||
| 286 | ## Phase 3: Game Loop Migration (1 day) | |
| 287 | ||
| 288 | ### Step 3.1: Remove custom animation loop | |
| 289 | Remove the `renderer.setAnimationLoop()` call from main.ts and replace with LittleJS game loop. | |
| 290 | ||
| 291 | ### Step 3.2: Convert timing to use delta time | |
| 292 | Replace all `clock.getDelta()` with `engine.timing.lastTimeStep`: | |
| 293 | ```typescript | |
| 294 | // OLD | |
| 295 | const passed = clock.getDelta() | |
| 296 | ||
| 297 | // NEW | |
| 298 | const passed = engine.timing.lastTimeStep | |
| 299 | ``` | |
| 300 | ||
| 301 | ### Step 3.3: Test frame rate independence | |
| 302 | - Verify physics work the same at different frame rates | |
| 303 | - Test on low-end devices | |
| 304 | ||
| 305 | --- | |
| 306 | ||
| 307 | ## Phase 4: Camera and Rendering (2-3 days) | |
| 308 | ||
| 309 | ### Step 4.1: Keep existing rendering code | |
| 310 | The Three.js rendering code can remain largely unchanged. | |
| 311 | ||
| 312 | ### Step 4.2: Integrate with LittleJS render loop | |
| 313 | Ensure the Three.js renderer is properly integrated with LittleJS. | |
| 314 | ||
| 315 | ### Step 4.3: Add LittleJS HUD (optional) | |
| 316 | Replace DOM-based HUD with LittleJS text rendering: | |
| 317 | ```typescript | |
| 318 | gameRenderPost(): void { | |
| 319 | const x = 20 | |
| 320 | let y = 40 | |
| 321 | ||
| 322 | drawTextScreen( | |
| 323 | `Speed: ${Math.round(this.skater.speed * 3.6)} km/h`, | |
| 324 | vec2(x, y), | |
| 325 | 24, | |
| 326 | colorWhite | |
| 327 | ) | |
| 328 | // ... more HUD elements | |
| 329 | } | |
| 330 | ``` | |
| 331 | ||
| 332 | --- | |
| 333 | ||
| 334 | ## Phase 5: Audio Integration (1-2 days) | |
| 335 | ||
| 336 | ### Step 5.1: Keep existing audio system | |
| 337 | The custom Web Audio code is excellent and should be kept. | |
| 338 | ||
| 339 | ### Step 5.2: Trigger audio from game loop | |
| 340 | ```typescript | |
| 341 | // In gameUpdate(): | |
| 342 | if (this.sounding() && this.skater.under === 0) { | |
| 343 | const h = this.lake.thickness(this.skater.x, this.skater.s) | |
| 344 | const lying = this.lake.snow(this.skater.x, this.skater.s) | |
| 345 | ||
| 346 | if (Math.floor(this.skater.stride) !== this.strides) { | |
| 347 | this.sound?.strike(h, undefined, (0.75 + 0.5 * Math.random()) * (1 - 0.9 * lying)) | |
| 348 | } | |
| 349 | ||
| 350 | this.sound?.glide(h, Math.exp(-sincePush(this.skater) / 0.5), undefined, lying) | |
| 351 | } | |
| 352 | ``` | |
| 353 | ||
| 354 | ### Step 5.3: Handle audio context | |
| 355 | Use LittleJS to manage audio context state if needed, or keep existing approach. | |
| 356 | ||
| 357 | --- | |
| 358 | ||
| 359 | ## Phase 6: UI Migration (2-3 days) | |
| 360 | ||
| 361 | ### Step 6.1: Keep DOM-based UI (easiest) | |
| 362 | The existing dialog-based UI works well. Just trigger it from LittleJS input. | |
| 363 | ||
| 364 | ### Step 6.2: Or migrate to LittleJS UI (optional) | |
| 365 | Use LittleJS UI system for buttons and dialogs: | |
| 366 | ```typescript | |
| 367 | // Create buttons | |
| 368 | const soundButton = new Button( | |
| 369 | vec2(100, 50), | |
| 370 | vec2(200, 60), | |
| 371 | 'Sound', | |
| 372 | () => this.toggleMute() | |
| 373 | ) | |
| 374 | ``` | |
| 375 | ||
| 376 | --- | |
| 377 | ||
| 378 | ## Phase 7: Polish and Testing (3-5 days) | |
| 379 | ||
| 380 | ### Step 7.1: Full regression testing | |
| 381 | Test every feature against the current implementation. | |
| 382 | ||
| 383 | ### Step 7.2: Performance testing | |
| 384 | - Compare FPS with current implementation | |
| 385 | - Check memory usage | |
| 386 | - Test on all target devices | |
| 387 | ||
| 388 | ### Step 7.3: User testing | |
| 389 | - Get feedback on controls feel | |
| 390 | - Verify audio quality | |
| 391 | - Check visual quality | |
| 392 | ||
| 393 | ### Step 7.4: Bug fixing | |
| 394 | Fix any issues discovered during testing. | |
| 395 | ||
| 396 | --- | |
| 397 | ||
| 398 | ## Phase 8: Cleanup (1-2 days) | |
| 399 | ||
| 400 | ### Step 8.1: Remove deprecated code | |
| 401 | - Remove old Input class | |
| 402 | - Remove custom animation loop code | |
| 403 | - Remove any other replaced systems | |
| 404 | ||
| 405 | ### Step 8.2: Update documentation | |
| 406 | - Update README | |
| 407 | - Update build instructions | |
| 408 | - Document any changes | |
| 409 | ||
| 410 | ### Step 8.3: Final testing | |
| 411 | One more round of full testing. | |
| 412 | ||
| 413 | --- | |
| 414 | ||
| 415 | ## Migration Checklist | |
| 416 | ||
| 417 | Use this checklist to track progress: | |
| 418 | ||
| 419 | ### Phase 1: Setup | |
| 420 | - [ ] LittleJS installed | |
| 421 | - [ ] New entry point created | |
| 422 | - [ ] index.html updated | |
| 423 | - [ ] Vite config updated (if needed) | |
| 424 | - [ ] Basic page loads | |
| 425 | ||
| 426 | ### Phase 2: Input | |
| 427 | - [ ] Keyboard input works | |
| 428 | - [ ] Mouse/touch input works | |
| 429 | - [ ] Gamepad input works | |
| 430 | - [ ] Tilt input works | |
| 431 | - [ ] All button mappings correct | |
| 432 | ||
| 433 | ### Phase 3: Game Loop | |
| 434 | - [ ] Custom animation loop removed | |
| 435 | - [ ] Delta time integrated | |
| 436 | - [ ] Physics work correctly | |
| 437 | - [ ] Frame rate independent | |
| 438 | ||
| 439 | ### Phase 4: Rendering | |
| 440 | - [ ] Three.js rendering works | |
| 441 | - [ ] Camera follows correctly | |
| 442 | - [ ] All visuals correct | |
| 443 | - [ ] HUD works (DOM or LittleJS) | |
| 444 | ||
| 445 | ### Phase 5: Audio | |
| 446 | - [ ] Audio triggers correctly | |
| 447 | - [ ] Audio context handled | |
| 448 | - [ ] All sounds work | |
| 449 | - [ ] Volume/mute works | |
| 450 | ||
| 451 | ### Phase 6: UI | |
| 452 | - [ ] Settings dialog works | |
| 453 | - [ ] About dialog works | |
| 454 | - [ ] Buttons work | |
| 455 | - [ ] Responsive design maintained | |
| 456 | ||
| 457 | ### Phase 7: Testing | |
| 458 | - [ ] All features tested | |
| 459 | - [ ] Performance acceptable | |
| 460 | - [ ] Mobile devices tested | |
| 461 | - [ ] No regressions | |
| 462 | ||
| 463 | ### Phase 8: Cleanup | |
| 464 | - [ ] Deprecated code removed | |
| 465 | - [ ] Documentation updated | |
| 466 | - [ ] Final testing complete | |
| 467 | ||
| 468 | --- | |
| 469 | ||
| 470 | ## Rollback Plan | |
| 471 | ||
| 472 | If you need to rollback at any point: | |
| 473 | ||
| 474 | ```bash | |
| 475 | # Restore original files | |
| 476 | cp src/main.ts.backup src/main.ts | |
| 477 | cp index.html.backup index.html | |
| 478 | ||
| 479 | # Remove LittleJS | |
| 480 | npm uninstall littlejsengine | |
| 481 | ||
| 482 | # Reinstall original dependencies | |
| 483 | npm install | |
| 484 | ``` | |
| 485 | ||
| 486 | --- | |
| 487 | ||
| 488 | ## Tips and Tricks | |
| 489 | ||
| 490 | ### 1. Debugging Input | |
| 491 | If input isn't working: | |
| 492 | ```typescript | |
| 493 | // Add debug logging | |
| 494 | console.log('Keyboard states:', { | |
| 495 | left: inputGetState('arrowLeft'), | |
| 496 | right: inputGetState('arrowRight'), | |
| 497 | a: inputGetState('a'), | |
| 498 | d: inputGetState('d') | |
| 499 | }) | |
| 500 | ``` | |
| 501 | ||
| 502 | ### 2. Checking Delta Time | |
| 503 | ```typescript | |
| 504 | console.log('Delta time:', engine.timing.lastTimeStep) | |
| 505 | console.log('FPS:', engine.timing.fps) | |
| 506 | ``` | |
| 507 | ||
| 508 | ### 3. Device Orientation Debug | |
| 509 | ```typescript | |
| 510 | console.log('Orientation:', { | |
| 511 | gamma: inputGetAxis('deviceOrientationGamma'), | |
| 512 | beta: inputGetAxis('deviceOrientationBeta') | |
| 513 | }) | |
| 514 | ``` | |
| 515 | ||
| 516 | ### 4. Canvas Issues | |
| 517 | If canvas isn't rendering: | |
| 518 | - Check that `engine.canvas` is set correctly | |
| 519 | - Verify Three.js renderer is using the same canvas | |
| 520 | - Ensure `setThreeRenderer(THREE)` is called | |
| 521 | ||
| 522 | ### 5. Performance Issues | |
| 523 | - Check for double rendering (Three.js + LittleJS) | |
| 524 | - Verify only one render loop is running | |
| 525 | - Use Chrome DevTools to profile | |
| 526 | ||
| 527 | --- | |
| 528 | ||
| 529 | ## Common Pitfalls | |
| 530 | ||
| 531 | ### 1. Input Names Don't Match | |
| 532 | LittleJS uses specific key names. Check the [LittleJS Input Documentation](https://github.com/KilledByAPixel/LittleJS/blob/main/REFERENCE.md#input). | |
| 533 | ||
| 534 | **Solution**: Use the exact key names LittleJS expects: | |
| 535 | - `arrowLeft`, `arrowRight`, `arrowUp`, `arrowDown` | |
| 536 | - `a`, `d`, `w`, `s` | |
| 537 | - `m`, `r` | |
| 538 | ||
| 539 | ### 2. Canvas Not Found | |
| 540 | **Solution**: Ensure the canvas element exists before initialization: | |
| 541 | ```typescript | |
| 542 | const canvas = document.getElementById('scene') | |
| 543 | if (!canvas) { | |
| 544 | console.error('Canvas not found!') | |
| 545 | return | |
| 546 | } | |
| 547 | ``` | |
| 548 | ||
| 549 | ### 3. THREE Not Available | |
| 550 | **Solution**: Make THREE globally available: | |
| 551 | ```typescript | |
| 552 | (window as any).THREE = THREE | |
| 553 | ``` | |
| 554 | ||
| 555 | ### 4. Audio Context Issues | |
| 556 | **Solution**: Audio must be triggered by user interaction. Use any input to start it: | |
| 557 | ```typescript | |
| 558 | if (anyInputDetected) { | |
| 559 | if (audio.state === 'suspended') { | |
| 560 | audio.resume() | |
| 561 | } | |
| 562 | } | |
| 563 | ``` | |
| 564 | ||
| 565 | ### 5. Mobile Input Not Working | |
| 566 | **Solution**: | |
| 567 | - Check for touch event handling | |
| 568 | - Ensure canvas has proper touch action settings | |
| 569 | - Test on actual device (simulators may not work) | |
| 570 | ||
| 571 | --- | |
| 572 | ||
| 573 | ## Success Criteria Checklist | |
| 574 | ||
| 575 | Before declaring the migration complete, verify: | |
| 576 | ||
| 577 | ### Functionality | |
| 578 | - [ ] All current features work exactly as before | |
| 579 | - [ ] No features are broken or missing | |
| 580 | - [ ] Controls feel the same or better | |
| 581 | - [ ] Audio quality unchanged | |
| 582 | - [ ] Visual quality unchanged | |
| 583 | ||
| 584 | ### Performance | |
| 585 | - [ ] Frame rate >= original implementation | |
| 586 | - [ ] Memory usage <= original implementation | |
| 587 | - [ ] Load time acceptable | |
| 588 | - [ ] Works on all target devices | |
| 589 | ||
| 590 | ### Code Quality | |
| 591 | - [ ] Code is cleaner and more maintainable | |
| 592 | - [ ] No unnecessary code left behind | |
| 593 | - [ ] Documentation updated | |
| 594 | - [ ] TypeScript types correct | |
| 595 | ||
| 596 | ### User Experience | |
| 597 | - [ ] No noticeable difference for users | |
| 598 | - [ ] Mobile experience improved (if applicable) | |
| 599 | - [ ] All input methods work | |
| 600 | - [ ] No new bugs introduced | |
| 601 | ||
| 602 | --- | |
| 603 | ||
| 604 | ## Final Notes | |
| 605 | ||
| 606 | This migration is **low-risk and high-reward** due to: | |
| 607 | ||
| 608 | 1. **Three.js plugin** means rendering code stays the same | |
| 609 | 2. **Incremental approach** allows validating each step | |
| 610 | 3. **Minimal integration** provides immediate benefits | |
| 611 | 4. **Well-documented** API with good examples | |
| 612 | 5. **Active community** for support | |
| 613 | ||
| 614 | **Estimated time for full migration**: 17-26 days (part-time) or 8-13 days (full-time) | |
| 615 | ||
| 616 | **Estimated time for minimal integration**: 3-5 days (part-time) or 1-2 days (full-time) | |
| 617 | ||
| 618 | **Recommendation**: Start with minimal integration to gain immediate benefits, then proceed with full migration if successful. |