006 - Motion System
Das Motion System bietet wiederverwendbare Bewegungsmuster basierend auf deltaTime.
Warum Motion UND Animation?
Auf den ersten Blick scheinen Animation und Motion dasselbe zu sein – beides bewegt Dinge. Aber sie lösen unterschiedliche Probleme:
Animation: "In 3 Sekunden soll die Größe von 80% auf 120% gehen."
- Hat definierte Dauer
- Interpoliert zwischen Start und Ende
- Braucht eine Clock
Motion: "Das Objekt bewegt sich mit 50 Pixeln pro Sekunde nach rechts."
- Läuft endlos (oder bis Grenze)
- Basiert auf Geschwindigkeit, nicht auf Zeit
- Braucht nur deltaTime
Praktisches Beispiel: Eine Kugel animiert ihr Pulsieren (Breathe-Animation). Gleichzeitig bewegt sie sich über den Bildschirm (VelocityMotion). Beides zusammen: Animation für Werte, Motion für Position.
Animation vs. Motion
| Aspekt | Animation | Motion |
|---|---|---|
| Zeitbasis | Normalisiert (0-1) | deltaTime (Sekunden) |
| Zweck | Wert interpolieren | Position bewegen |
| Dauer | Definiert | Endlos/kontinuierlich |
| Clock | Benötigt AnimationClock | Nicht erforderlich |
Komponenten
| Klasse | Datei | Funktion |
|---|---|---|
Motion |
Motion.ts |
Basis-Klasse |
LinearMotion |
LinearMotion.ts |
A → B |
VelocityMotion |
VelocityMotion.ts |
Konstante Geschwindigkeit |
NoiseMotion |
NoiseMotion.ts |
Organischer Drift |
WaveMotion |
WaveMotion.ts |
Sinus-Oszillation |
PathMotion |
PathMotion.ts |
Entlang Pfad |
OrbitMotion |
OrbitMotion.ts |
Umlaufbahn |
BoundaryBehavior |
BoundaryBehavior.ts |
Grenzverhalten |
Motion Basis-Klasse
Interface:
interface MotionContext {
deltaTime: number; // Zeit seit letztem Frame
time: number; // Gesamtzeit seit Start
canvasSize?: Size; // Für Boundary-Berechnungen
}
abstract class Motion<T extends MotionOptions> {
apply(position: Vector, context: MotionContext): Vector;
getOffset(context: MotionContext): Vector;
}LinearMotion
Lineare Bewegung zwischen zwei Punkten.
Optionen:
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
from |
Vector |
(0.1, 0.5) | Startpunkt (normalisiert) |
to |
Vector |
(0.9, 0.5) | Zielpunkt (normalisiert) |
mode |
string |
'pingpong' | Bewegungsmodus |
speed |
number |
0.5 | Geschwindigkeit |
Modi:
| Mode | Verhalten |
|---|---|
once |
A → B, dann stoppen |
loop |
A → B, springe zu A, wiederhole |
pingpong |
A → B → A → B... |
VelocityMotion
Konstante Geschwindigkeit in eine Richtung.
Optionen:
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
velocity |
Vector |
(1, 0) | Geschwindigkeitsvektor |
friction |
number |
1.0 | Reibung (1 = keine) |
NoiseMotion
Organische, zufällige Bewegung (Perlin Noise).
Optionen:
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
amplitude |
number |
50 | Bewegungsstärke |
frequency |
number |
0.01 | Rausch-Frequenz |
speed |
number |
1 | Zeitfaktor |
WaveMotion
Sinus-basierte Oszillation.
Optionen:
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
amplitude |
number |
50 | Auslenkung |
frequency |
number |
1 | Frequenz (Hz) |
axis |
WaveAxis |
'y' | 'x', 'y', oder 'both' |
waveform |
WaveformType |
'sine' | Wellenform |
Wellenformen:
| Waveform | Kurve |
|---|---|
sine |
Sinus |
triangle |
Dreieck |
square |
Rechteck |
sawtooth |
Sägezahn |
PathMotion
Bewegung entlang eines definierten Pfads.
Pfad-Typen:
| Klasse | Beschreibung |
|---|---|
CirclePath |
Kreisförmiger Pfad |
EllipsePath |
Elliptischer Pfad |
LinePath |
Gerader Pfad |
PolygonPath |
Pfad entlang Polygon-Vertices |
BezierPath |
Bézier-Kurve |
Optionen:
| Option | Typ | Beschreibung |
|---|---|---|
path |
Path |
Der Pfad |
speed |
number |
Geschwindigkeit |
loop |
boolean |
Wiederholen |
OrbitMotion
Umlaufbahn um einen Mittelpunkt.
Optionen:
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
center |
Vector |
Canvas-Mitte | Zentrum |
radius |
number |
100 | Umlaufradius |
speed |
number |
1 | Umlaufgeschwindigkeit |
startAngle |
number |
0 | Startwinkel (Rad) |
Boundary Behaviors
Verhalten an Canvas-Grenzen.
┌─────────────────────┐
┌───▶│ BounceStrategy │
│ └─────────────────────┘
│ ┌─────────────────────┐
├───▶│ WrapStrategy │
│ └─────────────────────┘
│ ┌─────────────────────┐
┌──────────┐ ┌─────────┐ Ja │ │ ClampStrategy │
│ Position │───▶│ Grenze? │────────▶├───▶└─────────────────────┘
└──────────┘ └─────────┘ │ ┌─────────────────────┐
│ ├───▶│ FadeStrategy │
│ Nein │ └─────────────────────┘
▼ │ ┌─────────────────────┐
┌──────────┐ ├───▶│ ShrinkFadeStrategy│
│ Weiter │ │ └─────────────────────┘
└──────────┘ │ ┌─────────────────────┐
├───▶│ DissolveStrategy │
│ └─────────────────────┘
│ ┌─────────────────────┐
├───▶│ SendStrategy │
│ └─────────────────────┘
│ ┌─────────────────────┐
└───▶│ CompositeStrategy │
└─────────────────────┘Basis-Strategien
| Strategy | Verhalten |
|---|---|
BounceStrategy |
Abprallen (Velocity umkehren) |
WrapStrategy |
Pac-Man (gegenüberliegende Seite) |
ClampStrategy |
Am Rand stoppen |
FadeStrategy |
Opacity beim Verlassen reduzieren |
Erweiterte Strategien (NEU)
| Strategy | Verhalten |
|---|---|
ShrinkFadeStrategy |
Objekt wird kleiner UND blendet aus |
DissolveStrategy |
Objekt löst sich in Partikel auf |
SendStrategy |
Objekt wird via Callback/Sockets gesendet |
CompositeStrategy |
Kombiniert mehrere Strategien |
BoundaryResult
| Property | Typ | Beschreibung |
|---|---|---|
position |
Vector |
Neue Position |
triggered |
boolean |
Grenze erreicht? |
edges |
BoundaryEdge[] |
Betroffene Kanten |
reflection |
Vector |
Für Velocity-Korrektur |
distanceToEdge |
number |
0-1 Abstand zum Rand |
scale |
number |
Für ShrinkFade (0-1) |
remove |
boolean |
Objekt entfernen? |
data |
any |
Für Callbacks (Send) |
particleBurst |
object |
Für Dissolve-Effekt |
ShrinkFade Beispiel
const boundary = new ShrinkFadeStrategy({
fadeDistance: 100,
minScale: 0.1,
easing: 'easeOut'
});
const result = boundary.apply(position, canvasSize);
// Scale und Alpha anwenden
ctx.globalAlpha = result.distanceToEdge ?? 1;
ctx.scale(result.scale ?? 1, result.scale ?? 1);
// Objekt entfernen wenn zu klein
if (result.remove) {
entities.delete(entity.id);
}Dissolve Beispiel
const boundary = new DissolveStrategy({ particleCount: 30 });
const result = boundary.apply(position, canvasSize);
// Partikel-Burst verarbeiten
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)
Die SendStrategy integriert sich mit dem MultiScreenClient für echte Multi-Screen Installationen:
import {
SendStrategy,
MultiScreenClient,
MultiScreenDebugPanel,
calculateEntryPosition,
calculateEntryVelocity,
getOppositeEdge
} from '@carstennichte/cc-toolbox';
// 1. MultiScreenClient aufsetzen
const client = new MultiScreenClient(socket,
{ artworkId: 'item-001', instanceId: '0' },
{ room: 'exhibition:demo', autoJoin: true }
);
client.configureTopology({
arrangement: '1x3', // 3 Screens nebeneinander
position: 0, // Linker Screen
totalScreens: 3,
wrapAround: true
});
// 2. SendStrategy für Boundary-Handling
const boundary = new SendStrategy({
onExit: (pos, edge, data) => {
// Über MultiScreenClient an Nachbar-Screen senden
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' // Automatisch an richtigen Nachbarn
);
},
exitData: () => entity.toData(),
});
// 3. Entities empfangen
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-Modi:
| Modus | Beschreibung |
|---|---|
broadcast |
An alle Screens im Room |
neighbor |
An benachbarten Screen (basierend auf Topology) |
target |
An spezifische ArtworkAddress |
random |
An zufälligen Screen |
Siehe auch: Multi-Screen Communication
Composite (Kombinierte Effekte)
// Schrumpfen + Senden
const boundary = new CompositeStrategy({
strategies: [
new ShrinkFadeStrategy({ fadeDistance: 50 }),
new SendStrategy({ onExit: sendToOtherScreen }),
],
combineMode: 'merge',
});Kombinieren von Motion
Motions können kombiniert werden:
// Orbit + Noise
const basePos = orbitMotion.apply(center, ctx);
const finalPos = noiseMotion.apply(basePos, ctx);Tweakpane Integration
Alle Motions haben Tweakpane-Support.
Registrierung:
| Funktion | Beschreibung |
|---|---|
registerLinearMotionTweakpane(module, motion) |
LinearMotion Blades |
registerBoundaryTweakpane(module, boundary) |
Boundary Blades |
Typische Blades:
| Blade | Control |
|---|---|
enabled |
Checkbox |
speed |
Slider |
mode |
Dropdown |
amplitude |
Slider |
boundaryType |
Dropdown |