Interactive Applications
Flexium provides a complete toolkit for building interactive canvas-based applications and games with a proper animation loop, keyboard input, mouse input, and declarative rendering.
Why Flexium for Interactive Apps?
Traditional interactive development with canvas requires:
- Manual animation loop management
- Complex input state tracking
- Imperative rendering code
- Delta time calculations
- Fixed timestep for physics
Flexium handles all of this for you with a clean, reactive API.
Quick Example
import { createLoop, keyboard, Keys } from 'flexium/interactive'
import { Canvas, Circle } from 'flexium/canvas'
function SimpleGame() {
let x = 200
let y = 200
const speed = 200 // pixels per second
const keyboard = keyboard()
const loop = createLoop({
onUpdate: (delta) => {
// Handle input
if (keyboard.isPressed(Keys.ArrowRight)) x += speed * delta
if (keyboard.isPressed(Keys.ArrowLeft)) x -= speed * delta
if (keyboard.isPressed(Keys.ArrowUp)) y -= speed * delta
if (keyboard.isPressed(Keys.ArrowDown)) y += speed * delta
},
onRender: () => {
// Render will trigger canvas update
return (
<Canvas width={400} height={400}>
<Circle x={x} y={y} radius={20} fill="blue" />
</Canvas>
)
}
})
loop.start()
return null // Rendering happens in game loop
}Animation Loop
The animation loop is the heart of any interactive application. It handles timing, updates, and rendering.
createLoop()
Creates an animation loop with delta time and optional fixed timestep for physics.
import { createLoop } from 'flexium/interactive'
const loop = createLoop({
fixedFps: 60, // Target FPS for physics (default: 60)
onUpdate: (delta) => {
// Called every frame
// delta = time since last frame in seconds
},
onFixedUpdate: (fixedDelta) => {
// Called at fixed intervals
// fixedDelta = 1/fixedFps (e.g., 1/60 = 0.016666...)
},
onRender: (alpha) => {
// Called every frame for rendering
// alpha = interpolation factor (0-1) for smooth rendering
}
})
loop.start() // Start the loop
loop.stop() // Stop the loop
loop.isRunning() // Check if running
loop.getFps() // Get current FPSDelta Time
Delta time represents the time elapsed since the last frame in seconds. Use it to make movement frame-rate independent:
const speed = 100 // pixels per second
createLoop({
onUpdate: (delta) => {
// Without delta: moves 1 pixel per frame (varies with FPS)
x += 1
// With delta: moves 100 pixels per second (consistent)
x += speed * delta
}
})Fixed Timestep
For physics simulations, use onFixedUpdate to ensure deterministic behavior:
const loop = createLoop({
fixedFps: 60, // Physics runs at 60 FPS
onUpdate: (delta) => {
// Variable timestep - good for input and game logic
},
onFixedUpdate: (fixedDelta) => {
// Fixed timestep - perfect for physics
// Always called with fixedDelta = 1/60 = 0.016666...
velocityY += gravity * fixedDelta
y += velocityY * fixedDelta
},
onRender: (alpha) => {
// Interpolate between physics states for smooth rendering
const renderY = y + velocityY * alpha * fixedDelta
}
})When to use each:
onUpdate: Input handling, game logic, AIonFixedUpdate: Physics, collision detectiononRender: Drawing to canvas
FPS Counter
Monitor performance with the built-in FPS counter:
const loop = createLoop({
onRender: () => {
const fps = loop.getFps()
console.log(`Running at ${fps} FPS`)
}
})Keyboard Input
keyboard() provides reactive keyboard state tracking with support for key press, hold, and release detection.
Basic Usage
import { keyboard, Keys } from 'flexium/interactive'
const keyboard = keyboard()
// Check if key is currently pressed
if (keyboard.isPressed(Keys.Space)) {
player.jump()
}
// Check if key was just pressed this frame
if (keyboard.isJustPressed(Keys.KeyE)) {
player.interact()
}
// Check if key was just released this frame
if (keyboard.isJustReleased(Keys.ShiftLeft)) {
player.stopSprinting()
}
// Get all pressed keys
const pressedKeys = keyboard.getPressedKeys()
console.log(pressedKeys) // ['keyw', 'space', ...]Keys Enum
The Keys enum provides convenient constants for common keys:
import { Keys } from 'flexium/interactive'
// Arrow keys
Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowLeft, Keys.ArrowRight
// WASD
Keys.KeyW, Keys.KeyA, Keys.KeyS, Keys.KeyD
// Common keys
Keys.Space, Keys.Enter, Keys.Escape, Keys.Tab
// Modifiers
Keys.ShiftLeft, Keys.ShiftRight
Keys.ControlLeft, Keys.ControlRight
Keys.AltLeft, Keys.AltRight
// Numbers
Keys.Digit0, Keys.Digit1, Keys.Digit2, ..., Keys.Digit9Custom Target
By default, keyboard events are tracked on window. You can specify a different target:
const canvasElement = document.querySelector('canvas')
const kb = keyboard(canvasElement)Movement Example
const keyboard = keyboard()
const speed = 200
createLoop({
onUpdate: (delta) => {
let vx = 0
let vy = 0
// WASD movement
if (keyboard.isPressed(Keys.KeyW)) vy -= 1
if (keyboard.isPressed(Keys.KeyS)) vy += 1
if (keyboard.isPressed(Keys.KeyA)) vx -= 1
if (keyboard.isPressed(Keys.KeyD)) vx += 1
// Normalize diagonal movement
if (vx !== 0 && vy !== 0) {
const length = Math.sqrt(vx * vx + vy * vy)
vx /= length
vy /= length
}
// Apply movement
player.x += vx * speed * delta
player.y += vy * speed * delta
// Sprint modifier
if (keyboard.isPressed(Keys.ShiftLeft)) {
player.speed = speed * 2
} else {
player.speed = speed
}
}
})Reactive Keyboard State
Access the reactive signal for advanced use cases:
const keyboard = keyboard()
// Watch for any key state changes
effect(() => {
const pressedKeys = keyboard.keys.value
console.log('Pressed keys:', Array.from(pressedKeys))
})Pattern: isPressed vs isJustPressed
isPressed(key): True while key is held down (continuous)- Use for: Movement, aiming, holding actions
isJustPressed(key): True only on the first frame when key is pressed (one-shot)- Use for: Jumping, shooting, interactions, menu navigation
isJustReleased(key): True only when key is released (one-shot)- Use for: Charge-up actions, sprint toggle
onUpdate: (delta) => {
// Continuous movement
if (keyboard.isPressed(Keys.ArrowRight)) {
player.x += speed * delta
}
// One-shot jump
if (keyboard.isJustPressed(Keys.Space)) {
if (player.onGround) {
player.velocityY = -jumpForce
}
}
// Toggle sprint on release
if (keyboard.isJustReleased(Keys.ShiftLeft)) {
player.isSprinting = !player.isSprinting
}
}Cleanup
Call clearFrameState() at the end of each frame to reset just-pressed/just-released states:
const keyboard = keyboard()
createLoop({
onUpdate: (delta) => {
// Handle input
if (keyboard.isJustPressed(Keys.Space)) {
console.log('Jump!')
}
},
onRender: () => {
// Clear frame state after processing
keyboard.clearFrameState()
}
})Don't forget to dispose when done:
onCleanup(() => {
keyboard.dispose()
})Mouse Input
mouse() provides reactive mouse state tracking with position, buttons, and wheel delta.
Basic Usage
import { mouse, MouseButton } from 'flexium/interactive'
const mouse = mouse()
// Get current mouse position
const pos = mouse.position.value
console.log(pos.x, pos.y)
// Check button states
if (mouse.isLeftPressed()) {
player.shoot()
}
if (mouse.isRightPressed()) {
player.aim()
}
if (mouse.isMiddlePressed()) {
camera.reset()
}
// Or use button numbers directly
if (mouse.isPressed(MouseButton.Left)) {
// same as isLeftPressed()
}Mouse Position
The position is relative to the target element (or canvas if specified):
const m = mouse({ canvas: myCanvas })
// Position is in canvas coordinates
effect(() => {
const pos = mouse.position.value
console.log(`Mouse at: ${pos.x}, ${pos.y}`)
})Mouse Delta
Track mouse movement since last frame:
const mouse = mouse()
onUpdate: (delta) => {
const delta = mouse.delta.value
// Camera rotation based on mouse movement
camera.rotateX(delta.y * sensitivity)
camera.rotateY(delta.x * sensitivity)
}Mouse Wheel
Detect scroll wheel input:
const mouse = mouse()
onUpdate: () => {
const wheel = mouse.wheelDelta.value
if (wheel !== 0) {
camera.zoom += wheel * zoomSpeed
}
}Canvas Integration
When using with canvas, provide the canvas element for proper coordinate calculation:
function Game() {
let canvasRef: HTMLCanvasElement | undefined
const m = mouse({
canvas: () => canvasRef // Pass as getter or direct reference
})
return (
<Canvas
ref={(el) => canvasRef = el}
width={800}
height={600}
>
<Circle
x={mouse.position.value.x}
y={mouse.position.value.y}
radius={10}
fill="red"
/>
</Canvas>
)
}MouseButton Enum
import { MouseButton } from 'flexium/interactive'
MouseButton.Left // 0
MouseButton.Middle // 1
MouseButton.Right // 2Cleanup
Clear frame state and dispose when done:
const mouse = mouse()
createLoop({
onRender: () => {
// Clear delta after each frame
mouse.clearFrameState()
}
})
onCleanup(() => {
mouse.dispose()
})Complete Example: Top-Down Shooter
Here's a complete game combining all the systems:
import { createLoop, keyboard, mouse, Keys } from 'flexium/interactive'
import { Canvas, Circle, Rect, CanvasText } from 'flexium/canvas'
import { state } from 'flexium/core'
function TopDownShooter() {
// Game state
const [score, setScore] = state(0)
const player = { x: 400, y: 300, radius: 20, speed: 250 }
const bullets: Array<{ x: number; y: number; vx: number; vy: number }> = []
const enemies: Array<{ x: number; y: number; radius: 15 }> = []
// Input
const keyboard = keyboard()
const mouse = mouse()
// Spawn enemies
let spawnTimer = 0
const spawnInterval = 2 // seconds
// Game loop
const loop = createLoop({
onUpdate: (delta) => {
// Player movement
let vx = 0
let vy = 0
if (keyboard.isPressed(Keys.KeyW)) vy -= 1
if (keyboard.isPressed(Keys.KeyS)) vy += 1
if (keyboard.isPressed(Keys.KeyA)) vx -= 1
if (keyboard.isPressed(Keys.KeyD)) vx += 1
// Normalize diagonal movement
if (vx !== 0 && vy !== 0) {
const len = Math.sqrt(vx * vx + vy * vy)
vx /= len
vy /= len
}
player.x += vx * player.speed * delta
player.y += vy * player.speed * delta
// Keep player in bounds
player.x = Math.max(player.radius, Math.min(800 - player.radius, player.x))
player.y = Math.max(player.radius, Math.min(600 - player.radius, player.y))
// Shooting
if (mouse.isLeftPressed()) {
const mousePos = mouse.position.value
const dx = mousePos.x - player.x
const dy = mousePos.y - player.y
const len = Math.sqrt(dx * dx + dy * dy)
bullets.push({
x: player.x,
y: player.y,
vx: (dx / len) * 500,
vy: (dy / len) * 500
})
}
// Update bullets
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i]
bullet.x += bullet.vx * delta
bullet.y += bullet.vy * delta
// Remove off-screen bullets
if (bullet.x < 0 || bullet.x > 800 || bullet.y < 0 || bullet.y > 600) {
bullets.splice(i, 1)
}
}
// Spawn enemies
spawnTimer += delta
if (spawnTimer >= spawnInterval) {
spawnTimer = 0
const side = Math.floor(Math.random() * 4)
let x, y
switch (side) {
case 0: x = Math.random() * 800; y = -20; break // top
case 1: x = Math.random() * 800; y = 620; break // bottom
case 2: x = -20; y = Math.random() * 600; break // left
case 3: x = 820; y = Math.random() * 600; break // right
}
enemies.push({ x, y, radius: 15 })
}
// Move enemies toward player
for (const enemy of enemies) {
const dx = player.x - enemy.x
const dy = player.y - enemy.y
const len = Math.sqrt(dx * dx + dy * dy)
enemy.x += (dx / len) * 100 * delta
enemy.y += (dy / len) * 100 * delta
}
// Collision: bullets vs enemies
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i]
for (let j = enemies.length - 1; j >= 0; j--) {
const enemy = enemies[j]
const dx = bullet.x - enemy.x
const dy = bullet.y - enemy.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < enemy.radius + 5) {
bullets.splice(i, 1)
enemies.splice(j, 1)
setScore(s => s + 10)
break
}
}
}
// Clear input states
keyboard.clearFrameState()
mouse.clearFrameState()
},
onRender: () => {
// Render will happen here or separately
}
})
loop.start()
// Render function
return () => (
<Canvas width={800} height={600} style={{ border: '2px solid black' }}>
{/* Background */}
<Rect x={0} y={0} width={800} height={600} fill="#111" />
{/* Player */}
<Circle
x={player.x}
y={player.y}
radius={player.radius}
fill="blue"
stroke="white"
strokeWidth={2}
/>
{/* Crosshair at mouse */}
<Circle
x={mouse.position.value.x}
y={mouse.position.value.y}
radius={3}
stroke="white"
strokeWidth={1}
/>
{/* Bullets */}
{bullets.map((bullet, i) => (
<Circle
key={i}
x={bullet.x}
y={bullet.y}
radius={5}
fill="yellow"
/>
))}
{/* Enemies */}
{enemies.map((enemy, i) => (
<Circle
key={i}
x={enemy.x}
y={enemy.y}
radius={enemy.radius}
fill="red"
stroke="darkred"
strokeWidth={2}
/>
))}
{/* Score */}
<CanvasText
x={10}
y={30}
text={`Score: ${score()}`}
fontSize={24}
fontWeight="bold"
fill="white"
/>
{/* FPS */}
<CanvasText
x={700}
y={30}
text={`FPS: ${loop.getFps()}`}
fontSize={16}
fill="white"
/>
</Canvas>
)
}Best Practices
1. Use Delta Time
Always use delta time for movement and time-based calculations:
// Bad - frame rate dependent
x += 5
// Good - frame rate independent
x += speed * delta2. Separate Logic and Rendering
Keep game logic in onUpdate and rendering in onRender:
createLoop({
onUpdate: (delta) => {
// Game logic, physics, input
updatePlayer(delta)
updateEnemies(delta)
checkCollisions()
},
onRender: (alpha) => {
// Only rendering
drawEverything()
}
})3. Use Fixed Timestep for Physics
Physics simulations should use onFixedUpdate:
createLoop({
fixedFps: 60,
onFixedUpdate: (fixedDelta) => {
// Deterministic physics
velocity.y += gravity * fixedDelta
position.y += velocity.y * fixedDelta
}
})4. Clear Input States
Always clear frame-specific input states:
createLoop({
onRender: () => {
keyboard.clearFrameState()
mouse.clearFrameState()
}
})5. Cleanup Resources
Dispose of input handlers when done:
onCleanup(() => {
loop.stop()
keyboard.dispose()
mouse.dispose()
})6. Use Object Pools
For bullets, particles, etc., reuse objects instead of creating new ones:
const bulletPool: Bullet[] = []
function getBullet() {
return bulletPool.pop() || createBullet()
}
function returnBullet(bullet: Bullet) {
bulletPool.push(bullet)
}7. Cap Delta Time
The game loop automatically caps delta at 250ms to prevent spiral of death. If you do manual timing, cap it:
const cappedDelta = Math.min(delta, 0.1) // Cap at 100msIntegration with Canvas
Flexium's game module integrates seamlessly with Canvas primitives:
import { createLoop } from 'flexium/interactive'
import { Canvas, Circle, Rect } from 'flexium/canvas'
import { state } from 'flexium/core'
function GameExample() {
const [entities, setEntities] = state([
{ x: 100, y: 100, color: 'red' },
{ x: 200, y: 150, color: 'blue' }
])
createLoop({
onUpdate: (delta) => {
// Update entity positions
setEntities(prev => prev.map(e => ({
...e,
x: e.x + Math.sin(Date.now() / 1000) * 100 * delta
})))
}
})
return (
<Canvas width={400} height={300}>
{entities().map((e, i) => (
<Circle
key={i}
x={e.x}
y={e.y}
radius={20}
fill={e.color}
/>
))}
</Canvas>
)
}The canvas automatically re-renders when state changes, giving you the best of both worlds: imperative game loop control with declarative rendering.
TypeScript Support
All game APIs are fully typed:
import type { Loop, KeyboardState, MouseState } from 'flexium/interactive'
const game: Loop = createLoop({
onUpdate: (delta: number) => {
// delta is typed as number
}
})
const keyboard: KeyboardState = keyboard()
const mouse: MouseState = mouse()Next Steps
- Learn more about Canvas Primitives
- Explore Animation for easing and tweening
- Check out Performance optimization tips
- See the Showcase for live demos