006 - Motion System
The Motion System provides reusable movement patterns based on deltaTime.
Why Motion AND Animation?
At first glance, animation and motion seem to be the same thing – both move things. But they solve different problems:
Animation: "In 3 seconds, the size should go from 80% to 120%."
- Has defined duration
- Interpolates between start and end
- Needs a clock
Motion: "The object moves at 50 pixels per second to the right."
- Runs endlessly (or until boundary)
- Based on velocity, not time
- Only needs deltaTime
Practical Example: A ball animates its pulsing (Breathe Animation). At the same time, it moves across the screen (VelocityMotion). Both together: Animation for values, Motion for position.
Animation vs. Motion
| Aspect | Animation | Motion |
|---|---|---|
| Time Basis | Normalized (0-1) | deltaTime (seconds) |
| Purpose | Interpolate value | Move position |
| Duration | Defined | Endless/continuous |
| Clock | Requires AnimationClock | Not required |
Components
| Class | File | Function |
|---|---|---|
Motion |
Motion.ts |
Base Class |
LinearMotion |
LinearMotion.ts |
A → B |
VelocityMotion |
VelocityMotion.ts |
Constant Velocity |
NoiseMotion |
NoiseMotion.ts |
Organic Drift |
WaveMotion |
WaveMotion.ts |
Sine Oscillation |
PathMotion |
PathMotion.ts |
Along Path |
OrbitMotion |
OrbitMotion.ts |
Orbit |
BoundaryBehavior |
BoundaryBehavior.ts |
Boundary Behavior |
Motion Base Class
Interface:
interface MotionContext {
deltaTime: number; // Time since last frame
time: number; // Total time since start
canvasSize?: Size; // For boundary calculations
}
abstract class Motion<T extends MotionOptions> {
apply(position: Vector, context: MotionContext): Vector;
getOffset(context: MotionContext): Vector;
}LinearMotion
Linear movement between two points.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
from |
Vector |
(0.1, 0.5) | Start Point (normalized) |
to |
Vector |
(0.9, 0.5) | Target Point (normalized) |
mode |
string |
'pingpong' | Movement Mode |
speed |
number |
0.5 | Speed |
Modes:
| Mode | Behavior |
|---|---|
once |
A → B, then stop |
loop |
A → B, jump to A, repeat |
pingpong |
A → B → A → B... |
VelocityMotion
Constant velocity in one direction.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
velocity |
Vector |
(1, 0) | Velocity Vector |
friction |
number |
1.0 | Friction (1 = none) |
NoiseMotion
Organic, random movement (Perlin Noise).
Options:
| Option | Type | Default | Description |
|---|---|---|---|
amplitude |
number |
50 | Movement Strength |
frequency |
number |
0.01 | Noise Frequency |
speed |
number |
1 | Time Factor |
WaveMotion
Sine-based oscillation.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
amplitude |
number |
50 | Deflection |
frequency |
number |
1 | Frequency (Hz) |
axis |
WaveAxis |
'y' | 'x', 'y', or 'both' |
waveform |
WaveformType |
'sine' | Waveform |
Waveforms:
| Waveform | Curve |
|---|---|
sine |
Sine |
triangle |
Triangle |
square |
Square |
sawtooth |
Sawtooth |
PathMotion
Movement along a defined path.
Path Types:
| Class | Description |
|---|---|
CirclePath |
Circular Path |
EllipsePath |
Elliptical Path |
LinePath |
Straight Path |
PolygonPath |
Path along Polygon Vertices |
BezierPath |
Bézier Curve |
Options:
| Option | Type | Description |
|---|---|---|
path |
Path |
The Path |
speed |
number |
Speed |
loop |
boolean |
Repeat |
OrbitMotion
Orbit around a center point.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
center |
Vector |
Canvas Center | Center |
radius |
number |
100 | Orbit Radius |
speed |
number |
1 | Orbit Speed |
startAngle |
number |
0 | Start Angle (rad) |
Boundary Behaviors
Behavior at canvas boundaries.
┌─────────────────────┐
┌───▶│ BounceStrategy │
│ └─────────────────────┘
│ ┌─────────────────────┐
├───▶│ WrapStrategy │
│ └─────────────────────┘
│ ┌─────────────────────┐
┌──────────┐ ┌─────────┐ Yes │ │ ClampStrategy │
│ Position │───▶│ Boundary│────────▶├───▶└─────────────────────┘
└──────────┘ └─────────┘ │ ┌─────────────────────┐
│ ├───▶│ FadeStrategy │
│ No │ └─────────────────────┘
▼ │ ┌─────────────────────┐
┌──────────┐ ├───▶│ ShrinkFadeStrategy│
│ Continue │ │ └─────────────────────┘
└──────────┘ │ ┌─────────────────────┐
├───▶│ DissolveStrategy │
│ └─────────────────────┘
│ ┌─────────────────────┐
├───▶│ SendStrategy │
│ └─────────────────────┘
│ ┌─────────────────────┐
└───▶│ CompositeStrategy │
└─────────────────────┘Basic Strategies
| Strategy | Behavior |
|---|---|
BounceStrategy |
Bounce (reverse velocity) |
WrapStrategy |
Pac-Man (opposite side) |
ClampStrategy |
Stop at edge |
FadeStrategy |
Reduce opacity when leaving |
Advanced Strategies (NEW)
| Strategy | Behavior |
|---|---|
ShrinkFadeStrategy |
Object shrinks AND fades out |
DissolveStrategy |
Object dissolves into particles |
SendStrategy |
Object is sent via Callback/Sockets |
CompositeStrategy |
Combines multiple strategies |
BoundaryResult
| Property | Type | Description |
|---|---|---|
position |
Vector |
New Position |
triggered |
boolean |
Boundary reached? |
edges |
BoundaryEdge[] |
Affected Edges |
reflection |
Vector |
For Velocity Correction |
distanceToEdge |
number |
0-1 Distance to Edge |
scale |
number |
For ShrinkFade (0-1) |
remove |
boolean |
Remove object? |
data |
any |
For Callbacks (Send) |
particleBurst |
object |
For Dissolve Effect |
ShrinkFade Example
const boundary = new ShrinkFadeStrategy({
fadeDistance: 100,
minScale: 0.1,
easing: 'easeOut'
});
const result = boundary.apply(position, canvasSize);
// Apply scale and alpha
ctx.globalAlpha = result.distanceToEdge ?? 1;
ctx.scale(result.scale ?? 1, result.scale ?? 1);
// Remove object when too small
if (result.remove) {
entities.delete(entity.id);
}Dissolve Example
const boundary = new DissolveStrategy({ particleCount: 30 });
const result = boundary.apply(position, canvasSize);
// Process particle burst
if (result.particleBurst) {
particleSystem.emit({
position: result.particleBurst.position,
count: result.particleBurst.count,
velocity: result.particleBurst.velocity,
});
}
if (result.remove) {
entities.delete(entity.id);
}Send/Receive (Multi-Screen)
The SendStrategy integrates with the MultiScreenClient for real multi-screen installations:
import {
SendStrategy,
MultiScreenClient,
MultiScreenDebugPanel,
calculateEntryPosition,
calculateEntryVelocity,
getOppositeEdge
} from '@carstennichte/cc-toolbox';
// 1. Set up MultiScreenClient
const client = new MultiScreenClient(socket,
{ artworkId: 'item-001', instanceId: '0' },
{ room: 'exhibition:demo', autoJoin: true }
);
client.configureTopology({
arrangement: '1x3', // 3 Screens side by side
position: 0, // Left Screen
totalScreens: 3,
wrapAround: true
});
// 2. SendStrategy for boundary handling
const boundary = new SendStrategy({
onExit: (pos, edge, data) => {
// Send via MultiScreenClient to neighbor screen
client.sendEntity(
{
type: 'Entity',
exitPosition: { x: pos.x / canvasWidth, y: pos.y / canvasHeight },
velocity: data.velocity,
appearance: { color: data.color, size: data.size },
},
edge,
'neighbor' // Automatically to correct neighbor
);
},
exitData: () => entity.toData(),
});
// 3. Receive entities
client.onEntityReceive((data) => {
const entryPos = calculateEntryPosition(data.exitEdge, data.entity.exitPosition, { bounds: canvasSize });
const entryVel = calculateEntryVelocity(data.exitEdge, data.entity.velocity);
const newEntity = createEntity({
position: entryPos,
velocity: entryVel,
color: data.entity.appearance.color,
});
entities.add(newEntity);
});
// 4. Debug panel (optional)
const debugPanel = new MultiScreenDebugPanel(client);
debugPanel.show();Send Modes:
| Mode | Description |
|---|---|
broadcast |
To all screens in room |
neighbor |
To neighboring screen (based on topology) |
target |
To specific ArtworkAddress |
random |
To random screen |
See also: Multi-Screen Communication
Composite (Combined Effects)
// Shrink + Send
const boundary = new CompositeStrategy({
strategies: [
new ShrinkFadeStrategy({ fadeDistance: 50 }),
new SendStrategy({ onExit: sendToOtherScreen }),
],
combineMode: 'merge',
});Combining Motion
Motions can be combined:
// Orbit + Noise
const basePos = orbitMotion.apply(center, ctx);
const finalPos = noiseMotion.apply(basePos, ctx);Tweakpane Integration
All motions have Tweakpane support.
Registration:
| Function | Description |
|---|---|
registerLinearMotionTweakpane(module, motion) |
LinearMotion Blades |
registerBoundaryTweakpane(module, boundary) |
Boundary Blades |
Typical Blades:
| Blade | Control |
|---|---|
enabled |
Checkbox |
speed |
Slider |
mode |
Dropdown |
amplitude |
Slider |
boundaryType |
Dropdown |