015 - Parameter Visualization

"Watch how an artwork thinks"

Das Visualization-Modul ermöglicht es, Parameter eines Artworks als kreative Datenquelle zu nutzen. Jede Parameter-Änderung wird in Echtzeit visualisiert - als radiales Mandala oder als lebendige Kreatur.

Konzept

Die Idee: Parameter sind nicht nur Einstellungen, sondern Daten die man visualisieren kann:

  • ParameterHistory: Sammelt Parameter-Werte über Zeit (Ring-Buffer)
  • ParameterVisualizer: Abstrakte Basis-Klasse für Visualisierungen
  • ParameterMandala: Radiale Darstellung - jeder Parameter = ein Strahl
  • ParameterCreature: Lebendes Wesen das auf Parameter reagiert
  • ParameterMapper: Auto-Mapping von Parametern zu Visualizer-Slots
  • ParameterObserver: Sockets Bridge für Remote-Parameter
Artwork → Sockets → ParameterObserver → ParameterHistory → ParameterMapper → Visualizer → Canvas

Schnellstart

import { 
  ParameterMandala, 
  ParameterCreature,
  ParameterObserver 
} from '@carstennichte/cc-toolbox';

// Option 1: Eigene Parameter visualisieren
const mandala = new ParameterMandala({ animated: true });
mandala.record(parameter);
mandala.update(deltaTime);
mandala.draw(ctx, width, height);

// Option 2: Parameter eines anderen Artworks beobachten
const observer = new ParameterObserver({
  serverUrl: 'http://localhost:4000',
  autoConnect: true,
});
const creature = new ParameterCreature({ personality: 'curious' });
creature.connectHistory(observer.getHistory());

ParameterHistory

Sammelt numerische Parameter-Werte als Zeitreihen:

import { ParameterHistory } from '@carstennichte/cc-toolbox';

const history = new ParameterHistory({
  maxLength: 200,           // Max. Datenpunkte pro Parameter
  sampleInterval: 50,       // Min. ms zwischen Samples
  excludePaths: ['tweakpane', 'debug'],
  numericOnly: true,        // Nur numerische Werte tracken
});

// In der Update-Loop
history.record(parameter);

// Statistiken abrufen
const stats = history.getStats('demo.wave1');
// → { min: 0.1, max: 0.9, avg: 0.5, current: 0.7, trend: 'rising', activity: 0.8 }

// Alle Parameter
const allStats = history.getAllStats();

// Gesamt-Aktivität (0-1)
const activity = history.getTotalActivity();

// Aktivster Parameter
const mostActive = history.getMostActiveParameter();

// 🧠 Smart Filter: Top N aktivste Parameter (nach Aktivität sortiert)
const topActive = history.getTopActiveStats(16, 0.005);
// → Die 16 aktivsten Parameter mit mind. 0.5% Aktivität

// Nur die Pfade der aktivsten Parameter
const activePaths = history.getTopActivePaths(16, 0.005);
// → ['entity.stats.centerX', 'entity.stats.avgSpeed', ...]

Filter-Optionen

const history = new ParameterHistory({
  includePaths: ['physics', 'motion'],  // NUR diese Pfade
  excludePaths: ['tweakpane', 'internal'],
  numericOnly: true,  // Strings, Booleans etc. ignorieren
});

ParameterMandala

Radiale Visualisierung aller Parameter:

import { ParameterMandala } from '@carstennichte/cc-toolbox';

const mandala = new ParameterMandala({
  // Filter
  excludePaths: ['tweakpane', 'artwork.canvas.html'],
  
  // Aussehen
  innerRadius: 30,
  showLabels: true,
  showGrid: true,
  showFill: true,
  showConnections: true,
  showDots: true,
  
  // Animation
  animated: true,
  animationSpeed: 0.1,
  
  // Farben für Trends
  risingColor: '#4ade80',   // Grün - steigend
  fallingColor: '#f87171',  // Rot - fallend
  stableColor: '#94a3b8',   // Grau - stabil
  
  // 🧠 Smart Filter - zeigt nur aktive Parameter
  smartFilter: true,        // Aktiviert intelligente Filterung
  maxParameters: 16,        // Max. angezeigte Parameter
  minActivity: 0.005,       // Min. 0.5% Aktivität erforderlich
  
  // 🫧 Bulge Effect - 3D-Kuppel/Dome-Effekt
  showBulge: true,          // Aktiviert den Bulge-Effekt
  bulgeIntensity: 0.5,      // Stärke (0-2)
  bulgeSource: 'activity',  // 'activity' | 'value' | 'combined'
});

// In der Update-Loop
mandala.record(parameter);
mandala.update(deltaTime);
mandala.draw(ctx, width, height);

Aufbau

  • Jeder Parameter wird als Strahl vom Zentrum dargestellt
  • Die Länge entspricht dem normalisierten Wert (0-1)
  • Die Farbe zeigt den Trend (steigend/fallend/stabil)
  • Die Verbindungslinien formen ein organisches Polygon
  • Das Zentrum pulsiert mit der Gesamt-Aktivität

🧠 Smart Filter

Ohne Smart Filter werden alle Parameter visualisiert - bei komplexen Artworks können das 100+ Parameter sein. Der Smart Filter zeigt nur die aktivsten Parameter:

const mandala = new ParameterMandala({
  smartFilter: true,     // Aktiviert Smart Filtering
  maxParameters: 16,     // Zeigt max. 16 Parameter
  minActivity: 0.005,    // Min. 0.5% Änderungsrate
});

Wie es funktioniert:

  1. ParameterHistory trackt die Aktivität jedes Parameters
  2. Aktivität = wie oft ändert sich der Wert im Verhältnis zu Samples
  3. getTopActiveStats(maxCount, minActivity) sortiert nach Aktivität
  4. Nur Parameter über dem Schwellwert werden angezeigt

Typische Anwendung: Ein Entities-Artwork sendet 164 Parameter, aber nur Entity-Positionen (e0_x, e0_y, ...) und Statistiken (avgSpeed, centerX) ändern sich. Smart Filter zeigt nur diese ~16 relevanten Parameter.

🫧 Bulge Effect

Der Bulge Effect erzeugt einen 3D-Kuppel/Dome-Effekt, bei dem innere Kreise stärker skaliert werden als äußere:

const mandala = new ParameterMandala({
  showBulge: true,           // Aktiviert den Effekt
  bulgeIntensity: 0.5,       // Stärke (0=aus, 2=extrem)
  bulgeSource: 'activity',   // Was steuert die Skalierung?
});

Bulge Sources:

Source Beschreibung
activity Aktive Parameter = größerer Bulge
value Hohe Werte = größerer Bulge
combined Mix aus Activity und Value

Formel: scale = 1 + intensity * (1 - circleIndex / totalCircles)²

Ergebnis: Innere Kreise wölben sich nach außen wie eine Kuppel oder Blase.

ParameterCreature

Ein lebendes Wesen das auf Parameter reagiert:

import { ParameterCreature } from '@carstennichte/cc-toolbox';

const creature = new ParameterCreature({
  // Persönlichkeit
  personality: 'curious',  // 'calm' | 'energetic' | 'curious' | 'sleepy'
  
  // Aussehen
  baseSize: 60,
  showEyes: true,
  tentacleLength: 80,
  bodyShape: 'organic',    // 'blob' | 'circle' | 'organic'
  
  // Farben
  warmColor: '#f97316',    // Orange (aktiv)
  coldColor: '#3b82f6',    // Blau (passiv)
});

// In der Update-Loop
creature.record(parameter);
creature.update(deltaTime);
creature.draw(ctx, width, height);

Verhalten

  • Körpergröße: Wächst mit Gesamt-Aktivität
  • Herzschlag: Pulsiert mit dem aktivsten Parameter
  • Farbe: Warm (aktiv) ↔ Kalt (passiv)
  • Augen: Folgen der Aktivität, blinzeln bei großen Änderungen
  • Tentakel: Repräsentieren Parameter-Gruppen
  • Mund: Lächelt bei hoher Aktivität

Persönlichkeiten

Persönlichkeit Verhalten
calm Langsame, sanfte Bewegungen
energetic Schnelle, lebhafte Reaktionen
curious Mittlere Geschwindigkeit, folgt Änderungen
sleepy Sehr langsam, schläft bei niedriger Aktivität

ParameterMapper - Auto-Mapping

Der ParameterMapper ordnet automatisch Parameter zu Visualizer-Slots zu. Er analysiert welche Parameter interessant sind (sich ändern) und matcht sie semantisch.

Grundlegende Verwendung

import { ParameterMapper } from '@carstennichte/cc-toolbox';

const mapper = new ParameterMapper();

// Parameter tracken (jedes Frame aufrufen)
mapper.trackActivity(parameter);

// Nach einigen Frames: Auto-Mapping für Mandala abrufen
const mappedValues = mapper.map(parameter, 'mandala');
// → { 'rings[0]': 0.73, 'rings[1]': 0.45, 'rotation': 127, ... }

Auto-Mapping Strategien

const mapper = new ParameterMapper({
  mappings: {
    mandala: {
      enabled: true,
      autoMap: {
        enabled: true,
        strategy: 'hybrid',  // 'by-activity' | 'by-name' | 'by-type' | 'hybrid'
        maxSlots: 8,
      },
    },
  },
});
Strategie Beschreibung
by-activity Priorisiert Parameter die sich häufig ändern
by-name Matcht Namen semantisch (z.B. entity.counttentacles.count)
by-type Matcht nach Wertebereich (0-1, 0-360, etc.)
hybrid Kombiniert alle Strategien (empfohlen)

Semantische Gruppen

Der Mapper kennt semantische Wortgruppen für intelligentes Matching:

// Eingebaute Gruppen (SEMANTIC_GROUPS):
// - size: size, scale, radius, width, height, count, amount
// - motion: speed, velocity, rate, frequency, acceleration
// - rotation: rotation, angle, direction, heading
// - color: hue, saturation, brightness, color, tint
// - intensity: intensity, strength, force, power, amplitude
// - noise: noise, random, chaos, turbulence
// - entity: entity, particle, agent, item
// - grid: grid, cell, row, column, tile

Beispiel: entity_manager.entityCount wird automatisch zu creature.tentacles.count gemappt, weil beide zur Gruppe size/count gehören.

Kernel-Parameter

Diese Standard-Parameter existieren in jedem Artwork und werden vom Auto-Mapper ignoriert (da sie für Visualisierung uninteressant sind):

// Automatisch gefiltert:
// - artwork.canvas.size.width/height
// - artwork.canvas.html.*
// - artwork.animation.timeStamp/deltaTime
// - artwork.meta.*
// - tweakpane.*

Manuelles Mapping

// Eigenes Mapping hinzufügen
mapper.setMapping('mandala', {
  target: 'rings[0]',
  source: 'myCustom.value',
  transforms: [
    { type: 'normalize', min: 0, max: 100 },
    { type: 'smooth', smoothing: 0.3 },
  ],
});

// Mapping entfernen
mapper.removeMapping('mandala', 'rings[0]');

Transform-Pipeline

Werte können transformiert werden:

const slot = {
  target: 'rotation',
  source: 'physics.angle',
  transforms: [
    { type: 'normalize', min: 0, max: 360 },   // Auf 0-1 normalisieren
    { type: 'scale', factor: 2, offset: 0.5 }, // Skalieren
    { type: 'smooth', smoothing: 0.2 },        // Glätten
    { type: 'clamp', min: 0, max: 1 },         // Begrenzen
  ],
};
Transform Beschreibung
passthrough Keine Änderung
normalize Auf 0-1 normalisieren (min/max angeben)
scale Mit Faktor multiplizieren, Offset addieren
invert 1 - value
clamp Auf Bereich begrenzen
smooth Exponentielles Glätten
threshold Binär: 0 oder 1
modulo Modulo-Operation
abs Absolutwert

Debug-Ausgabe

// Alle getrackten Parameter mit Aktivität
const info = mapper.getActivityInfo();
// → [{ path: 'demo.wave1', activity: 0.87, range: { min: 0, max: 1 }, ... }, ...]

// Debug-Summary
console.log(mapper.getDebugSummary('mandala'));
// === ParameterMapper Debug ===
// Total tracked params: 24
// Sample count: 500
// 
// Top 10 Active Parameters:
//   demo.wave1: activity=87% range=[0.00 - 1.00]
//   demo.pulse: activity=65% range=[0.00 - 1.00]
//   ...
//
// Slot Assignments for 'mandala':
//   rings[0] ← demo.wave1 (auto)
//   rings[1] ← demo.pulse (auto)
//   ...

Config speichern/laden

// Als JSON exportieren
const json = mapper.toJSON();
localStorage.setItem('my-mapping', json);

// Aus JSON laden
const loaded = ParameterMapper.fromJSON(json);

ParameterObserver - Sockets Bridge

Der ParameterObserver empfängt Parameter von anderen Artworks über Sockets:

import { ParameterObserver, ParameterMandala } from '@carstennichte/cc-toolbox';

const observer = new ParameterObserver({
  serverUrl: 'http://localhost:4000',
  targetArtwork: 'item-cc-entities',  // Optional: spezifisches Artwork
  autoConnect: true,
  debug: false,
});

// Visualizer mit Observer-History verbinden
const mandala = new ParameterMandala();
mandala.connectHistory(observer.getHistory());

// Event-Handler
observer.onConnection((connected) => {
  console.log(connected ? 'Connected!' : 'Disconnected');
});

observer.onUpdate((params, artworkId, timestamp) => {
  console.log(`Received ${Object.keys(params).length} params from ${artworkId}`);
});

// Manuelle Kontrolle
await observer.connect();
observer.disconnect();
observer.requestParameterDump();  // Parameter sofort anfordern

Sockets Integration

Visualisiere Parameter eines anderen Artworks:

import { ParameterHistory, ParameterMandala } from '@carstennichte/cc-toolbox';
import { io } from 'socket.io-client';

// Shared History
const sharedHistory = new ParameterHistory({ maxLength: 300 });

// Visualizer mit externer History
const mandala = new ParameterMandala();
mandala.connectHistory(sharedHistory);

// Sockets Connection
const socket = io('http://localhost:4000');
socket.emit('join-room', 'artwork:item-cc-softbodies');

socket.on('parameter-dump', (payload) => {
  sharedHistory.record(payload.data || payload);
});

// Draw Loop
function animate() {
  mandala.update(deltaTime);
  mandala.draw(ctx, width, height);
  requestAnimationFrame(animate);
}

RoomSelector - Dynamische Room/Artwork-Auswahl

Der RoomSelector ist eine wiederverwendbare UI-Komponente für die Auswahl von Sockets Rooms und Artworks:

import { RoomSelector, type RoomInfo, type ArtworkInfo } from '@carstennichte/cc-toolbox';

const roomSelector = new RoomSelector({
  serverUrl: 'http://localhost:4000',
  autoConnect: true,                    // Auto-connect beim Start
  autoRefreshInterval: 5000,            // Auto-refresh alle 5s
  subscribeToChanges: true,             // Updates bei Join/Leave
  roomPrefix: 'artwork:',               // Nur Artwork-Rooms anzeigen
});

Tweakpane Integration

// Fügt komplettes UI zu Tweakpane hinzu
roomSelector.addToPane(pane, {
  onRoomSelected: (room: RoomInfo | null) => {
    console.log('Room selected:', room?.name);
  },
  onArtworkSelected: (artwork: ArtworkInfo | null, room: RoomInfo | null) => {
    if (artwork && room) {
      connectToArtwork(room, artwork);
    }
  },
  onConnectionChange: (connected: boolean) => {
    console.log('Connection:', connected);
  },
  onRoomsRefreshed: (rooms: RoomInfo[]) => {
    console.log(`Found ${rooms.length} rooms`);
  },
});

Manuelle Verwendung

// Connect
await roomSelector.connect();

// Rooms abrufen
const rooms = await roomSelector.refreshRooms();
console.log(rooms);
// → [{ name: 'artwork:item-001', memberCount: 2, artworks: [...] }, ...]

// Room auswählen
roomSelector.selectRoom('artwork:item-001');

// Artwork auswählen
roomSelector.selectArtwork('item-001');

// Status
console.log(roomSelector.getSelectedRoom());
console.log(roomSelector.getSelectedArtwork());

Refresh-Button (Wiederverwendbar)

Der RoomSelector enthält einen wiederverwendbaren Refresh-Button-Pattern:

// Statische Methode für eigene Refresh-Buttons
RoomSelector.createRefreshButton(folder, '🔄 Refresh Data', async () => {
  const data = await fetchMyData();
  updateUI(data);
});

Presets - Laden & Speichern

// Preset speichern
roomSelector.savePreset('Mein Setup');

// Letztes Preset laden
roomSelector.loadPreset();

// Alle Presets abrufen
const presets = roomSelector.loadPresets();

// Presets löschen
roomSelector.clearPresets();

Exhibition Mode

Für Ausstellungen gibt es einen automatischen Setup-Modus über URL-Parameter:

URL-Parameter

index.html?sketch=observer
  &exhibitionId=mosaik-2027
  &serverUrl=http://192.168.1.100:4000
  &room=artwork:item-cc-softbodies
  &artwork=item-cc-softbodies
  &autoConnect=true
  &hideControls=true

Im Sketch verwenden

const roomSelector = new RoomSelector({
  serverUrl: 'http://localhost:4000',
});

// Automatisch URL-Parameter anwenden
if (roomSelector.initFromURL()) {
  console.log('Exhibition mode: Auto-connecting...');
}

Exhibition Config Datei

Eine exhibition.config.json im Projekt-Root definiert die Exhibition-Einstellungen:

{
  "name": "Parameter Visualization Exhibition",
  "server": {
    "url": "http://192.168.1.100:4000",
    "fallbackUrls": ["http://localhost:4000"]
  },
  "observers": [
    {
      "id": "mandala-observer",
      "visualizer": "mandala",
      "autoConnect": true,
      "targetRoom": "artwork:item-cc-softbodies",
      "url": "index.html?sketch=observer&exhibitionId=param-viz&..."
    }
  ],
  "display": {
    "fullscreen": true,
    "hideControls": true
  },
  "startup": {
    "autoStart": true,
    "delay": 2000
  }
}

Programmatisch konfigurieren

import { type RoomDiscoveryConfig, defaultRoomDiscoveryConfig } from '@carstennichte/cc-toolbox';

const config: RoomDiscoveryConfig = {
  exhibitionId: 'mosaik-2027',
  serverUrl: 'http://192.168.1.100:4000',
  defaultRoom: 'artwork:item-cc-softbodies',
  defaultArtwork: 'item-cc-softbodies',
  autoConnect: true,
  hideControls: true,
};

roomSelector.applyExhibitionConfig(config);

ObjectUtils

Das Visualization-Modul nutzt erweiterte Object-Utilities:

import { ObjectUtils } from '@carstennichte/cc-toolbox';

// Nested Object → Flat Map
const flat = ObjectUtils.flatten({
  physics: { gravity: 9.8, friction: 0.5 },
  motion: { speed: 10 }
});
// → { 'physics.gravity': 9.8, 'physics.friction': 0.5, 'motion.speed': 10 }

// Flat Map → Nested Object
const nested = ObjectUtils.unflatten(flat);

// Filtern
const filtered = ObjectUtils.filter(flat, {
  exclude: ['physics'],
  numericOnly: true,
});

// Kombiniert
const result = ObjectUtils.flattenAndFilter(myObject, {
  exclude: ['tweakpane', 'internal'],
  numericOnly: true,
});

Demo-Projekt

Das Demo-Projekt item-cc-parameter-viz zeigt alle Features:

cd workspaces/default/catalog/items/item-cc-parameter-viz
npm install
npm run dev

URL-Parameter für Sketch-Auswahl:

  • ?sketch=mandala - Radiales Mandala (default)
  • ?sketch=creature - Lebendige Kreatur
  • ?sketch=observer - Sockets Observer mit RoomSelector

Observer Exhibition-Mode:

?sketch=observer&serverUrl=http://localhost:4000&room=artwork:item-cc-softbodies&autoConnect=true

Best Practices

1. Filter sinnvoll setzen

// Nur relevante Parameter tracken
const history = new ParameterHistory({
  includePaths: ['physics', 'motion', 'demo'],
  excludePaths: ['tweakpane', 'artwork.canvas.html', 'artwork.meta'],
});

2. Sample-Intervall anpassen

// Für schnelle Änderungen: kürzeres Intervall
const history = new ParameterHistory({
  sampleInterval: 16,  // ~60fps
  maxLength: 500,      // Mehr History
});

// Für langsame Trends: längeres Intervall
const history = new ParameterHistory({
  sampleInterval: 100,
  maxLength: 200,
});

3. Animation glätten

const mandala = new ParameterMandala({
  animated: true,
  animationSpeed: 0.05,  // Langsamer = glatter
});

Erweiterbarkeit

Eigene Visualizer erstellen:

import { ParameterVisualizer, ParameterVisualizerConfig } from '@carstennichte/cc-toolbox';

export class MyVisualizer extends ParameterVisualizer {
  draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
    const allStats = this.history.getAllStats();
    
    // Eigene Visualisierung hier
    for (const [path, stats] of allStats) {
      const color = this.getTrendColor(stats);
      const value = this.getAnimatedValue(path);
      // ...
    }
  }
}