LittleJS Conversion Plan for Nordic Skating
Overview
This document outlines a comprehensive plan to migrate the Nordic Skating project from its current Three.js-based architecture to LittleJS, a tiny fast JavaScript game engine with 2D/3D capabilities.
Key Insight: LittleJS Three.js Plugin
LittleJS has a Three.js plugin that allows using Three.js as an alternative renderer. This means: - We can keep the existing Three.js rendering code largely intact - We gain LittleJS's game loop, input system, scene management, audio, and other features - We get built-in mobile support, touch controls, and better performance management - The engine handles the animation loop, delta time, and frame rate independence
Why Migrate?
- Simplified Architecture: LittleJS provides a unified framework for game development
- Better Input Handling: Built-in support for keyboard, mouse, touch, gamepads, and device orientation
- Audio System: Built-in audio with ZzFX sound generation (could supplement or replace current Web Audio)
- Scene Management: Built-in scene system for menus, game states, etc.
- Mobile Optimization: Designed for mobile devices with touch controls
- Performance: Optimized game loop and rendering
- Tooling: Built-in level editors, debug tools, particle systems
- Size: Small footprint (~135KB minified, but bundlers can tree-shake unused features)
Current Architecture Analysis
Current Tech Stack
- Rendering: Three.js (WebGL-based 3D rendering)
- Build: Vite with TypeScript
- Audio: Web Audio API (custom ice sound synthesis)
- Input: Custom implementation for keyboard, mouse, touch, gamepad, device orientation
- World Generation: Procedural lake generation with noise functions
- Physics: Custom skater physics and ice acoustics
- State Management: Custom history/state handling for lake seeds
Core Systems
-
World/Lake System (
src/world/lake.ts) - Procedural channel generation - Ice thickness calculation - Islet and island generation - Fork handling -
Skater System (
src/skater.ts) - Position, direction, speed management - Steering and turning physics - Autopilot logic - Stride and push mechanics -
Rendering System (
src/render/) - Ice rendering with dynamic textures - Shore/bank rendering - Sky and weather effects - Snow and drifts - Marks (tracks, cracks) -
Audio System (
src/audio/iceSound.ts) - Custom Web Audio synthesis - Ice singing, cracking, striking sounds - Wind and snow sounds - Physics-based sound generation -
Input System (
src/input.ts) - Keyboard, mouse, touch - Gamepad support - Device orientation (tilt steering) -
UI System - Settings dialog - About dialog - Sound, pace, tilt, randomize buttons - Responsive design
Conversion Strategy
Phase 1: Preparation and Setup (1-2 days)
1.1 Install LittleJS
npm install littlejsengine
1.2 Update package.json
{
"dependencies": {
"littlejsengine": "^latest",
"simplex-noise": "^4.0.3",
"three": "^0.180.0"
}
}
1.3 Create LittleJS Entry Point
Create src/littlejs-entry.ts as the new main entry point:
import { engineInit, gameInit, gameUpdate, gameRender } from 'littlejsengine'
import { main } from './main-littlejs'
// Initialize LittleJS with our game functions
engineInit(
() => main.init(),
() => main.update(),
() => main.updatePost(),
() => main.render(),
() => main.renderPost()
)
1.4 Update HTML Entry Point
Update index.html to use LittleJS:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Nordic Skating</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
</head>
<body>
<canvas id="littlejsCanvas"></canvas>
<script type="module" src="/src/littlejs-entry.ts"></script>
</body>
</html>
Phase 2: Core Engine Integration (2-3 days)
2.1 Initialize LittleJS with Three.js Plugin
// src/main-littlejs.ts
import * as THREE from 'three'
import { engine, setThreeRenderer } from 'littlejsengine'
import { Lake } from './world/lake'
import { Skater } from './skater'
class NordicSkatingGame {
private lake: Lake
private skater: Skater
private scene: THREE.Scene
private camera: THREE.PerspectiveCamera
private renderer: THREE.WebGLRenderer
init(): void {
// Initialize LittleJS
engineInit(this.gameInit, this.gameUpdate, this.gameUpdatePost,
this.gameRender, this.gameRenderPost)
// Set up Three.js renderer for LittleJS
setThreeRenderer(THREE)
// Create Three.js scene
this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1500)
this.camera.rotation.order = 'YXZ'
// Initialize world
this.lake = createLake(12345, { impossibleIce: false })
this.skater = createSkater(this.lake, 0)
// Set up fog (matches current implementation)
this.scene.fog = new THREE.Fog(0, 30, 600)
this.scene.background = this.scene.fog.color
}
gameInit(): void {
// Called by LittleJS on initialization
}
gameUpdate(): void {
// Handle input, update skater, world, etc.
}
gameRender(): void {
// Render the scene
renderer.render(this.scene, this.camera)
}
}
export const main = new NordicSkatingGame()
2.2 Map Input Systems
Current Input: - Keyboard (Arrow keys, WASD, M, R) - Mouse/Touch drag for steering - Gamepad buttons and sticks - Device orientation (tilt steering)
LittleJS Input Mapping:
// In gameUpdate()
const steer = inputGetState('arrowLeft') - inputGetState('arrowRight')
+ inputGetState('a') - inputGetState('d')
+ inputGetAxis('mouseX') * 0.1 // Mouse/touch
+ inputGetAxis('gamepadLeftX') // Gamepad
+ inputGetAxis('deviceOrientationGamma') * 0.05 // Tilt
const muted = inputGetPressed('m')
const randomize = inputGetPressed('r')
Note: LittleJS has built-in device orientation support that we can use instead of our custom implementation.
2.3 Integrate Audio System
Option A: Keep Current Web Audio (Recommended) The current audio system is highly specialized for ice physics. We can keep it and just trigger it from LittleJS's game loop.
Option B: Migrate to LittleJS Audio LittleJS has a simpler audio system. We'd need to port the ice acoustics synthesis.
Recommendation: Start with Option A, as the current audio is complex and physics-based. Later consider if LittleJS's audio effects can supplement it.
Phase 3: Scene and Rendering Migration (3-5 days)
3.1 Camera System
Current: - First-person camera following skater - Dynamic FOV based on speed - Pitch and heading calculations
LittleJS Integration:
gameRender(): void {
// Update camera position based on skater
const push = this.skater.under === 0 && sincePush(this.skater) < pushFor
? Math.sin((Math.PI * sincePush(this.skater)) / pushFor) : 0
const hurry = (this.skater.speed - PACES.cruise.speed) /
(PACES.olympic.speed - PACES.cruise.speed)
const crouch = hurry > 0 ? PACES.olympic.crouch * hurry :
PACES.meander.crouch * Math.min(1, -hurry * 2.3)
const fov = baseFov + 12 * Math.max(-0.4, Math.min(1.1, hurry))
camera.fov = fov
camera.aspect = engine.canvas.width / engine.canvas.height
camera.updateProjectionMatrix()
const along = Math.atan(this.skater.channel.slope(this.skater.s))
const look = this.skater.heading - 0.25 * Math.sin(this.skater.heading - along)
camera.position.set(
this.skater.x,
EYE - crouch + 0.03 * push - 1.3 * sunk,
this.skater.s
)
camera.rotation.set(-pitch, -look, -0.02 * this.skater.turn * this.skater.speed)
renderer.render(scene, camera)
}
3.2 World Rendering Components
Ice Rendering (src/render/ice.ts):
- Keep as Three.js mesh
- Integrate into LittleJS render loop
- Update texture coordinates based on skater position
Shores Rendering (src/render/shores.ts):
- Keep current Three.js implementation
- Update chunk management in gameUpdate()
Sky Rendering (src/render/sky.ts):
- Keep as Three.js mesh with shaders
- Update time-based animations in gameUpdate()
Snow and Drifts (src/render/snow.ts, src/render/drifts.ts):
- Keep current particle systems
- Integrate with LittleJS particle system if beneficial
Marks (src/render/marks.ts):
- Keep current implementation
- Update based on skater position
Phase 4: Game Logic Integration (2-3 days)
4.1 Skater Physics
Port the skater update logic to use LittleJS's delta time:
gameUpdate(): void {
const dt = engine.timing.lastTimeStep
// Update skater
const steer = this.getSteeringInput()
stepSkater(this.skater, this.lake, steer, dt)
// Update marks
marks.follow(this.skater.x, this.skater.s, dt)
if (dt > 0) marks.track(this.skater.x, this.skater.s)
// Check for pushes
const pushed = Math.floor(this.skater.stride) !== strides
if (pushed) {
marks.push(this.skater.x, this.skater.s, this.skater.heading,
Math.floor(this.skater.stride) % 2 ? 1 : -1)
}
}
4.2 Lake Generation
Keep the procedural lake generation as-is. It's already frame-independent and seed-based.
4.3 Weather and Time Systems
Integrate with LittleJS's timing system:
// In gameUpdate()
const weather = this.getCurrentWeather()
const timeOfDay = this.getCurrentTime()
// Update sky based on time
sky.update(engine.timing.time)
// Update wind
const blowing = weather.wind[0] +
(weather.wind[1] - weather.wind[0]) *
(0.5 + 0.5 * Math.sin(engine.timing.time * 0.23) *
Math.sin(engine.timing.time * 0.071 + 1))
Phase 5: UI Migration (2-3 days)
5.1 Settings and About Dialogs
Option A: Use LittleJS UI System LittleJS has a basic UI system with buttons, sliders, etc. We can use this for the settings dialog.
Option B: Keep Current HTML/CSS UI Keep the existing dialog elements and just trigger them from LittleJS input.
Recommendation: Start with Option B for faster migration, then consider Option A for future enhancements.
5.2 Buttons and Controls
Map current buttons to LittleJS:
- Sound button: inputGetPressed('m') or UI button
- Randomize: inputGetPressed('r') or UI button
- Pace: UI button or keyboard
- Tilt: UI button
- Settings: UI button
- About: UI button
5.3 HUD Elements
LittleJS has built-in text rendering:
gameRenderPost(): void {
// Draw HUD elements
drawTextScreen(`Speed: ${Math.round(this.skater.speed * 3.6)} km/h`,
vec2(10, 10), 20, colorWhite)
// Draw hints
if (this.showHint) {
drawTextScreen(this.hintText, vec2(centerX, 100), 30, colorWhite)
}
}
Phase 6: State Management (1-2 days)
6.1 Lake Seed and History
Current: Uses browser history API to manage lake seeds
LittleJS Integration: - Keep history API usage - Or use LittleJS's scene system for different "levels"
Recommendation: Keep current approach as it works well for sharing links.
6.2 Settings Persistence
Keep current localStorage approach, it's working well.
Phase 7: Audio Integration (2-3 days)
7.1 Ice Sound System
Keep Current Implementation: The ice sound synthesis is complex and physics-based. It should remain as Web Audio API code.
Integration:
// In gameInit()
audio = new AudioContext()
sound = new IceSound(audio)
// In gameUpdate()
if (sounding()) {
sound.wind(blowing)
sound.glide(thickness, pushAmount)
sound.strike(thickness)
// etc.
}
Note: Need to handle audio context suspend/resume based on user interaction.
7.2 Sound Button
// In gameUpdate()
if (inputGetPressed('m')) {
setMuted(!muted)
}
Phase 8: Testing and Optimization (3-5 days)
8.1 Input Testing
- Test all input methods (keyboard, mouse, touch, gamepad, tilt)
- Verify steering feel is preserved
- Test on mobile devices
8.2 Performance Testing
- Verify frame rate on desktop and mobile
- Check memory usage
- Test with various lake complexities
8.3 Audio Testing
- Verify all ice sounds work correctly
- Test wind and weather sounds
- Check audio on mobile devices
8.4 Visual Testing
- Verify ice rendering at all distances
- Check shore and island rendering
- Test sky and weather effects
- Verify marks (tracks, cracks) rendering
Phase 9: Cleanup and Documentation (1-2 days)
9.1 Code Cleanup
- Remove deprecated code
- Update comments
- Ensure consistent code style
9.2 Update Documentation
- Update README with new LittleJS-based instructions
- Update build and run instructions
- Document any changes in behavior
9.3 Final Testing
- Full regression testing
- Performance benchmarking
- Mobile device testing
Detailed File Migration Plan
Core Files to Create/Modify
-
src/littlejs-entry.ts(New) - LittleJS engine initialization - Entry point for the application -
src/main-littlejs.ts(New, replacessrc/main.ts) - Main game class - Initializes all systems - Manages game loop -
src/game-config.ts(New) - Game configuration constants - LittleJS engine settings
Files to Adapt
-
src/world/lake.ts(Minimal changes) - Keep as-is, may need to adjust imports - Ensure compatible with ESM -
src/skater.ts(Minimal changes) - Keep physics logic - May need to adjust timing to use LittleJS delta time -
src/render/*.ts(Minimal changes) - Keep Three.js rendering code - May need to adjust how they're instantiated -
src/audio/iceSound.ts(Minimal changes) - Keep Web Audio implementation - Adjust how it's triggered -
src/input.ts(Significant changes) - Replace with LittleJS input system - May keep some utility functions (tiltSteer, padSteer) -
src/settings.ts(Minimal changes) - Keep as-is - May adjust how settings are applied
Files to Deprecate
src/main.ts(Replace withmain-littlejs.ts)
Migration Order Recommendation
-
Setup Phase (Day 1) - Install LittleJS - Create basic entry point - Get LittleJS running with empty game
-
Core Integration (Days 2-3) - Set up Three.js with LittleJS - Port input system - Get basic rendering working
-
Game Logic (Days 4-5) - Integrate skater physics - Integrate lake generation - Basic movement working
-
Rendering (Days 6-7) - Integrate ice rendering - Integrate shores - Integrate sky
-
Audio (Days 8-9) - Integrate ice sounds - Test audio triggering
-
UI (Days 10-11) - Port settings dialog - Port about dialog - Add HUD elements
-
Polish (Days 12-13) - Weather system - Snow and drifts - Marks (tracks, cracks)
-
Testing (Days 14-15) - Full testing on all devices - Performance optimization - Bug fixes
Risk Assessment
Low Risk Items
- Lake generation (already modular)
- Settings system (already modular)
- Three.js rendering (can coexist with LittleJS)
Medium Risk Items
- Input system migration (need to test feel)
- Skater physics timing (delta time changes)
- Audio system integration
High Risk Items
- Mobile device testing (need physical devices)
- Performance on low-end devices
- Tilt steering on iOS (permission issues)
Rollback Plan
If the migration encounters significant issues:
- Partial Migration: Keep using Three.js for rendering while using LittleJS for input, audio, and game loop
- Incremental Migration: Migrate one system at a time, keeping the old code until new is proven
- Fallback: Can always revert to current implementation if needed
Success Criteria
- All current features work as before
- Frame rate >= current implementation
- Input feel is preserved or improved
- Audio quality is preserved
- Visual quality is preserved
- Mobile experience is improved
- Code is more maintainable
Estimated Timeline
| Phase | Duration | Start Date | End Date |
|---|---|---|---|
| Phase 1: Setup | 1-2 days | TBD | TBD |
| Phase 2: Core Integration | 2-3 days | TBD | TBD |
| Phase 3: Scene Migration | 3-5 days | TBD | TBD |
| Phase 4: Game Logic | 2-3 days | TBD | TBD |
| Phase 5: UI Migration | 2-3 days | TBD | TBD |
| Phase 6: State Management | 1-2 days | TBD | TBD |
| Phase 7: Audio Integration | 2-3 days | TBD | TBD |
| Phase 8: Testing | 3-5 days | TBD | TBD |
| Phase 9: Cleanup | 1-2 days | TBD | TBD |
| Total | 17-26 days |
Note: This is a part-time estimate. For full-time work, reduce by ~50%.
Next Steps
- Review this plan and provide feedback
- Set up development environment with LittleJS
- Create proof-of-concept with basic movement
- Proceed with migration phase by phase
- Test thoroughly at each phase
Additional Considerations
LittleJS Features to Leverage Later
- Particle System: Could enhance snow, drifts, and crack effects
- Level Editor: Could be used for testing specific lake configurations
- Pathfinding: Could be used for autopilot improvements
- Networking: Could enable multiplayer features
- Cloud Saves: Could save settings and favorite lakes
- Medals/Leaderboards: Could add competitive elements
Features That Might Be Simplified
- Input Handling: LittleJS handles many input complexities
- Audio Management: LittleJS can manage audio context
- Delta Time: Automatic handling of frame timing
- Canvas Management: LittleJS handles canvas sizing and pixel ratio
Features to Keep Custom
- Lake Generation: Too specialized for LittleJS's built-in systems
- Ice Acoustics: Too physics-specific
- Procedural World: Unique to Nordic Skating
Conclusion
The migration to LittleJS is feasible and beneficial, particularly because of the Three.js plugin. The migration can be done incrementally with minimal disruption to the existing codebase. The biggest benefits will be in input handling, game loop management, and mobile optimization.
Recommendation: Proceed with the migration as outlined in this plan.