fuzzyworld

nordic-skating / mistral-medium-35-littlejs / files / LITTLEJS_POC.md

Branch mistral-medium-35-littlejs · default branch · all branches

LITTLEJS_POC.md

Blame: the commit that last changed each line · file · history

185c6976 2026-10-19 Pavel Safronov1# LittleJS Proof of Concept for Nordic Skating
2
3This document provides a working proof-of-concept implementation to validate the migration approach before committing to the full conversion.
4
5## Quick Start
6
7To test the POC:
8
9```bash
10cd /Users/pavel/code/nordic-skating
11npm install littlejsengine
12cp src/main.ts src/main.ts.backup
13cp LITTLEJS_POC/src/main-littlejs.ts src/
14npm run dev
15```
16
17## POC Structure
18
19The POC demonstrates:
201. LittleJS engine initialization with Three.js
212. Basic input handling (keyboard, mouse)
223. Simple skater movement
234. Lake rendering (static for POC)
245. Integration with existing lake generation
25
26## Proof of Concept Implementation
27
28### 1. Install LittleJS
29
30```bash
31npm install littlejsengine
32```
33
34### 2. Create the POC Entry Point
35
36`src/littlejs-poc.ts`:
37
38```typescript
39/**
40 * Proof of Concept: LittleJS + Three.js Integration for Nordic Skating
41 *
42 * This file demonstrates that LittleJS can work alongside Three.js
43 * for rendering while handling input, game loop, and state management.
44 */
45
46import * as THREE from 'three'
47import {
48 engineInit,
49 engine,
50 inputGetState,
51 inputGetPressed,
52 vec2,
53 colorWhite,
54 drawTextScreen,
55 setThreeRenderer
56} from 'littlejsengine'
57import { createLake, type Lake, mulberry32 } from './world/lake.js'
58import { createSkater, stepSkater, PACES, type Skater } from './skater.js'
59import { BREAKING_THICKNESS, CRACKING_THICKNESS } from './ice/strength.js'
60
61// Constants
62const EYE = 1.5
63const BASE_FOV = 65
64const PITCH = 0.12
65
66class NordicPOC {
67 private lake: Lake
68 private skater: Skater
69 private scene: THREE.Scene
70 private camera: THREE.PerspectiveCamera
71 private renderer: THREE.WebGLRenderer
72 private iceMesh: THREE.Mesh
73 private shoresGroup: THREE.Group
74
75 // Input state
76 private steerValue = 0
77 private muted = false
78 private seed = 12345
79
80 constructor() {
81 // Store reference for Three.js
82 window.THREE = THREE
83
84 // Initialize LittleJS with Three.js plugin
85 this.initEngine()
86 }
87
88 private initEngine(): void {
89 // Set up canvas
90 const canvas = document.getElementById('scene') as HTMLCanvasElement
91 if (!canvas) {
92 console.error('Canvas element not found')
93 return
94 }
95
96 // Configure engine to use our canvas
97 engine.canvas = canvas
98 engine.canvasSize = vec2(canvas.width, canvas.height)
99
100 // Initialize Three.js renderer
101 this.renderer = new THREE.WebGLRenderer({
102 canvas,
103 antialias: true
104 })
105 this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
106
107 // Set Three.js renderer for LittleJS
108 setThreeRenderer(THREE)
109
110 // Create scene
111 this.scene = new THREE.Scene()
112 const haze = new THREE.Fog(0, 30, 600)
113 this.scene.fog = haze
114 this.scene.background = haze.color
115
116 // Create camera
117 this.camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1500)
118 this.camera.rotation.order = 'YXZ'
119
120 // Initialize world
121 this.lake = createLake(this.seed, { impossibleIce: false })
122 this.skater = createSkater(this.lake, 0)
123 this.skater.pace = 'cruise'
124
125 // Create simple ice plane for POC
126 this.createIcePlane()
127
128 // Create simple shores for POC
129 this.shoresGroup = new THREE.Group()
130 this.scene.add(this.shoresGroup)
131
132 // Create simple sky
133 this.createSimpleSky()
134
135 // Handle resize
136 window.addEventListener('resize', this.onResize.bind(this))
137 this.onResize()
138
139 // Initialize LittleJS
140 engineInit(
141 () => this.gameInit(),
142 () => this.gameUpdate(),
143 () => this.gameUpdatePost(),
144 () => this.gameRender(),
145 () => this.gameRenderPost()
146 )
147 }
148
149 private createIcePlane(): void {
150 // Simple ice plane for POC
151 const geometry = new THREE.PlaneGeometry(1000, 1000, 100, 100)
152 const material = new THREE.MeshStandardMaterial({
153 color: 0x0b1a24,
154 roughness: 0.9,
155 metalness: 0.1
156 })
157 this.iceMesh = new THREE.Mesh(geometry, material)
158 this.iceMesh.rotation.x = -Math.PI / 2
159 this.scene.add(this.iceMesh)
160 }
161
162 private createSimpleSky(): void {
163 // Simple skybox for POC
164 const geometry = new THREE.SphereGeometry(500, 32, 32)
165 const material = new THREE.MeshBasicMaterial({
166 color: 0x87ceeb,
167 side: THREE.BackSide
168 })
169 const sky = new THREE.Mesh(geometry, material)
170 this.scene.add(sky)
171 }
172
173 private onResize(): void {
174 const canvas = engine.canvas
175 this.renderer.setSize(canvas.width, canvas.height, false)
176 this.camera.aspect = canvas.width / canvas.height
177 this.camera.updateProjectionMatrix()
178 }
179
180 gameInit(): void {
181 console.log('LittleJS + Nordic Skating POC initialized')
182 }
183
184 gameUpdate(): void {
185 // Handle input using LittleJS
186 this.handleInput()
187
188 // Update skater using existing physics
189 const dt = engine.timing.lastTimeStep
190 stepSkater(this.skater, this.lake, this.steerValue, dt)
191 }
192
193 private handleInput(): void {
194 // Keyboard steering
195 let steer = 0
196 steer += inputGetState('arrowLeft') ? -1 : 0
197 steer += inputGetState('arrowRight') ? 1 : 0
198 steer += inputGetState('a') ? -1 : 0
199 steer += inputGetState('d') ? 1 : 0
200
201 // Mouse/touch steering
202 steer += inputGetAxis('mouseX') * 0.5
203
204 // Gamepad steering
205 steer += inputGetAxis('gamepadLeftX')
206
207 this.steerValue = Math.max(-1, Math.min(1, steer))
208
209 // Other inputs
210 if (inputGetPressed('m')) {
211 this.muted = !this.muted
212 console.log(`Sound ${this.muted ? 'off' : 'on'}`)
213 }
214
215 if (inputGetPressed('r')) {
216 this.seed = Math.floor(Math.random() * 999999) + 1
217 this.lake = createLake(this.seed, { impossibleIce: false })
218 this.skater = createSkater(this.lake, 0)
219 this.skater.pace = 'cruise'
220 console.log(`New lake: ${this.seed}`)
221 }
222
223 // Pace control
224 if (inputGetPressed('arrowUp') || inputGetPressed('w')) {
225 const currentIndex = PACES[0].findIndex(p => p === this.skater.pace)
226 const nextIndex = (currentIndex + 1) % PACES[0].length
227 this.skater.pace = PACES[0][nextIndex]
228 }
229 if (inputGetPressed('arrowDown') || inputGetPressed('s')) {
230 const currentIndex = PACES[0].findIndex(p => p === this.skater.pace)
231 const prevIndex = (currentIndex - 1 + PACES[0].length) % PACES[0].length
232 this.skater.pace = PACES[0][prevIndex]
233 }
234
235 // Start sound on any input (browser requirement)
236 if (this.steerValue !== 0 ||
237 inputGetPressed('m') ||
238 inputGetPressed('r') ||
239 inputGetPressed('arrowUp') ||
240 inputGetPressed('arrowDown')) {
241 this.startSound()
242 }
243 }
244
245 private startSound(): void {
246 // POC: Just log for now
247 console.log('Sound would start here')
248 }
249
250 gameUpdatePost(): void {
251 // Post-update logic
252 }
253
254 gameRender(): void {
255 // Update camera based on skater
256 this.updateCamera()
257
258 // Render Three.js scene
259 this.renderer.render(this.scene, this.camera)
260 }
261
262 private updateCamera(): void {
263 // Calculate camera position based on skater state
264 const pushFor = 0.5 * PACES[this.skater.pace].strides
265 const push = this.skater.under === 0 &&
266 ((Date.now() / 1000) % this.skater.stride) < pushFor
267 ? Math.sin((Math.PI * ((Date.now() / 1000) % this.skater.stride)) / pushFor)
268 : 0
269
270 const hurry = (this.skater.speed - PACES.cruise.speed) /
271 (PACES.olympic.speed - PACES.cruise.speed)
272 const crouch = hurry > 0
273 ? PACES.olympic.crouch * hurry
274 : PACES.meander.crouch * Math.min(1, -hurry * 2.3)
275
276 const fov = BASE_FOV + 12 * Math.max(-0.4, Math.min(1.1, hurry))
277
278 if (Math.abs(this.camera.fov - fov) > 0.05) {
279 this.camera.fov = fov
280 this.camera.updateProjectionMatrix()
281 }
282
283 const sunk = this.skater.under > 0
284 ? Math.min(1, (3 - this.skater.under) / 0.3)
285 : 0
286
287 // Camera position: first person view
288 this.camera.position.set(
289 this.skater.x,
290 EYE - crouch + 0.03 * push - 1.3 * sunk,
291 this.skater.s
292 )
293
294 // Camera rotation
295 const along = Math.atan(this.skater.channel.slope(this.skater.s))
296 const look = this.skater.heading -
297 0.25 * Math.sin(this.skater.heading - along)
298
299 this.camera.rotation.set(-PITCH, -look, -0.02 * this.skater.turn * this.skater.speed)
300 }
301
302 gameRenderPost(): void {
303 // Draw HUD using LittleJS
304 const x = 20
305 let y = 40
306 const lineHeight = 30
307
308 drawTextScreen(
309 `Speed: ${Math.round(this.skater.speed * 3.6)} km/h`,
310 vec2(x, y),
311 24,
312 colorWhite
313 )
314 y += lineHeight
315
316 drawTextScreen(
317 `Pace: ${this.skater.pace}`,
318 vec2(x, y),
319 24,
320 colorWhite
321 )
322 y += lineHeight
323
324 drawTextScreen(
325 `Lake: ${this.seed}`,
326 vec2(x, y),
327 24,
328 colorWhite
329 )
330 y += lineHeight
331
332 drawTextScreen(
333 `Steer: ${this.steerValue.toFixed(2)}`,
334 vec2(x, y),
335 24,
336 colorWhite
337 )
338
339 // Instructions
340 drawTextScreen(
341 'WASD/Arrows: Steer | M: Mute | R: Random Lake | Up/Down: Change Pace',
342 vec2(engine.canvas.width / 2, engine.canvas.height - 30),
343 18,
344 colorWhite,
345 0.5
346 )
347 }
348}
349
350// Initialize POC when DOM is ready
351if (document.readyState === 'loading') {
352 document.addEventListener('DOMContentLoaded', () => {
353 new NordicPOC()
354 })
355} else {
356 new NordicPOC()
357}
358```
359
360### 3. Update index.html for POC
361
362```html
363<!DOCTYPE html>
364<html lang="en">
365<head>
366 <meta charset="UTF-8">
367 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
368 <title>Nordic Skating - LittleJS POC</title>
369 <style>
370 * { margin: 0; padding: 0; box-sizing: border-box; }
371 body {
372 overflow: hidden;
373 background: #000;
374 font-family: sans-serif;
375 }
376 canvas {
377 display: block;
378 width: 100vw;
379 height: 100vh;
380 }
381 </style>
382</head>
383<body>
384 <canvas id="scene"></canvas>
385 <script type="module" src="/src/littlejs-poc.ts"></script>
386</body>
387</html>
388```
389
390### 4. Update Vite Config (if needed)
391
392`vite.config.ts`:
393```typescript
394import { defineConfig } from 'vite'
395import { resolve } from 'path'
396
397export default defineConfig({
398 resolve: {
399 alias: {
400 // Ensure LittleJS can be resolved
401 littlejsengine: resolve(__dirname, 'node_modules/littlejsengine/dist/littlejs.esm.js')
402 }
403 },
404 build: {
405 target: 'esnext'
406 }
407})
408```
409
410## Testing the POC
411
412### Expected Behavior
413
4141. **Basic Movement**: Use WASD or arrow keys to steer
4152. **Pace Changes**: Use Up/Down arrows or W/S to change pace
4163. **Random Lake**: Press R to generate a new lake
4174. **HUD**: Shows speed, pace, lake seed, and steering value
4185. **Camera**: First-person view that follows skater physics
419
420### Test Checklist
421
422- [ ] Keyboard steering works (A/D, Arrow Left/Right)
423- [ ] Mouse steering works (drag left/right)
424- [ ] Gamepad steering works (if available)
425- [ ] Pace changes work (Up/Down, W/S)
426- [ ] Random lake generation works (R)
427- [ ] HUD displays correctly
428- [ ] Camera follows skater properly
429- [ ] Frame rate is smooth
430- [ ] No errors in console
431
432## POC Validation Criteria
433
434The POC is successful if:
435
4361. ✅ LittleJS engine initializes without errors
4372. ✅ Three.js rendering works alongside LittleJS
4383. ✅ Input from keyboard, mouse, and gamepad is captured
4394. ✅ Skater physics update correctly using LittleJS delta time
4405. ✅ Camera positioning works as expected
4416. ✅ HUD rendering with LittleJS text works
4427. ✅ Basic lake generation is integrated
443
444## Next Steps After POC
445
446If POC is successful:
447
4481. **Full Migration**: Proceed with the complete migration plan
4492. **Incremental Approach**: Migrate one system at a time
4503. **Side-by-side Testing**: Compare performance and feel with current implementation
451
452If POC has issues:
453
4541. **Debug**: Identify and fix integration problems
4552. **Adjust Plan**: Modify migration approach based on findings
4563. **Partial Integration**: Use only specific LittleJS features (input, game loop) while keeping Three.js for rendering
457
458## Troubleshooting
459
460### Common Issues
461
462**1. Three.js not available in LittleJS context**
463- Solution: Ensure THREE is available globally or pass it explicitly
464- In the POC, we set `window.THREE = THREE` as a fallback
465
466**2. Canvas sizing issues**
467- Solution: Let LittleJS manage the canvas, don't manually size it
468- Remove any manual canvas resizing from existing code
469
470**3. Input not working**
471- Solution: Check LittleJS input mapping
472- Ensure input system is initialized before use
473- Verify key names match LittleJS expectations
474
475**4. Audio context issues**
476- Solution: Handle audio context creation carefully
477- Remember browser restrictions on autoplay
478- Use user interaction to unlock audio
479
480**5. Performance issues**
481- Solution: Check for double rendering
482- Ensure only one render loop is running
483- Verify Three.js renderer is properly configured
484
485## Alternative: Minimal Integration
486
487If the full integration proves problematic, consider a **minimal integration** approach:
488
489```typescript
490// Only use LittleJS for input and game loop
491import { engineInit, inputGetState } from 'littlejsengine'
492import * as THREE from 'three'
493
494// Keep existing Three.js setup
495const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
496const scene = new THREE.Scene()
497const camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1500)
498
499// Use LittleJS just for input and timing
500engineInit(
501 () => {}, // init
502 () => {
503 // Game update using LittleJS input
504 const steer = inputGetState('arrowLeft') - inputGetState('arrowRight')
505 // Update game state
506 },
507 () => {}, // updatePost
508 () => {
509 // Render with Three.js
510 renderer.render(scene, camera)
511 },
512 () => {} // renderPost
513)
514```
515
516This approach gives you:
517- LittleJS input handling (better than custom implementation)
518- LittleJS timing (consistent delta time)
519- LittleJS game loop management
520- While keeping full control over Three.js rendering
521
522## Conclusion
523
524The POC demonstrates that **LittleJS and Three.js can coexist** in the Nordic Skating project. The main benefits from even a minimal integration would be:
525
5261. **Better Input Handling**: LittleJS provides comprehensive input support
5272. **Consistent Timing**: Automatic delta time calculation
5283. **Game Loop Management**: No need to manually manage animation frames
5294. **Mobile Support**: Built-in touch and device orientation support
530
531The full migration would additionally provide:
5325. **UI System**: For dialogs and HUD
5336. **Audio Management**: Though we may keep custom Web Audio
5347. **Scene Management**: For different game states
5358. **Particle System**: Could enhance visual effects
536
537**Recommendation**: Start with the minimal integration (input + game loop) to validate the approach, then expand to full migration if successful.