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