TECH DETAILS

Kinetic Typography - Day 28

Advanced Level

Table of Contents

01 / Letter Particle System

Every letter in the kinetic typography system is treated as an independent particle. Each letter-particle carries its own position, velocity, orbital properties, animation state, and visual styling. This allows each character to move, orbit, and transition independently while still being part of a coherent word formation.

class LetterParticle { constructor(char, x, y, color) { // Identity this.char = char; // Current position and target this.x = x; this.y = y; this.targetX = x; this.targetY = y; // Velocity for physics-based movement this.vx = (Math.random() - 0.5) * 8; this.vy = (Math.random() - 0.5) * 8; // Orbital properties this.angle = Math.random() * Math.PI * 2; this.orbitRadius = Math.random() * 120 + 60; this.orbitSpeed = Math.random() * 0.04 + 0.01; // Animation state this.state = 'idle'; this.color = color || '#00ffcc'; this.fontSize = 24; this.alpha = 1; this.scale = 1; } }

Property Randomization for Natural Motion

Each letter receives randomized angle, orbitRadius, and orbitSpeed values. This prevents letters from clumping together during orbital animations. The initial velocity vx and vy values give each letter a unique explosion trajectory when transitioning out of the idle state.

// Create particles from a word string createWordParticles(word, centerX, centerY) { const particles = []; const spacing = 30; const startX = centerX - (word.length * spacing) / 2; for (let i = 0; i < word.length; i++) { const hue = (i / word.length) * 60 + 160; const color = `hsl(${hue}, 100%, 70%)`; const p = new LetterParticle( word[i], startX + i * spacing, centerY, color ); particles.push(p); } return particles; }
Color Gradient Trick

Each letter's hue is derived from its index within the word. This automatically creates a rainbow gradient across the word without manually assigning colors. The hue range 160-220 maps from cyan to blue-violet, fitting the project's color palette.

02 / Orbital Physics Engine

The orbital physics engine governs how letter-particles move through space when they are not forming words. Each particle orbits around a center point using angular velocity, with optional wave motion and spiral decay patterns layered on top for visual complexity.

updateOrbital(centerX, centerY, dt) { // Advance angle based on orbit speed this.angle += this.orbitSpeed * dt; // Circular orbit position const orbitX = centerX + Math.cos(this.angle) * this.orbitRadius; const orbitY = centerY + Math.sin(this.angle) * this.orbitRadius; // Wave motion overlay (vertical wobble) const wave = Math.sin(this.angle * 3) * 15; orbitY += wave; // Smooth interpolation toward orbit position this.x += (orbitX - this.x) * 0.08; this.y += (orbitY - this.y) * 0.08; } // Spiral pattern - radius shrinks over time updateSpiral(centerX, centerY, dt) { this.angle += this.orbitSpeed * dt * 1.5; this.orbitRadius *= 0.998; // Gradual decay const x = centerX + Math.cos(this.angle) * this.orbitRadius; const y = centerY + Math.sin(this.angle) * this.orbitRadius; this.x += (x - this.x) * 0.1; this.y += (y - this.y) * 0.1; }

Gravity Influence

A configurable gravity parameter pulls particles downward during the explode phase, giving the explosion a natural arc. During orbit mode, gravity is disabled so letters float freely in their circular paths.

// Apply gravity during explosion phase if (this.state === 'exploding') { this.vy += gravity * dt; this.x += this.vx * dt; this.y += this.vy * dt; // Apply drag to slow particles over time this.vx *= 0.99; this.vy *= 0.99; }
Smooth Interpolation

Instead of snapping particles directly to their orbit positions, we use linear interpolation (lerp) with a factor of 0.08. This creates a smooth, elastic feel as letters glide into orbit rather than teleporting.

03 / Web Audio Synthesis

The Web Audio API provides real-time sound synthesis without any audio files. Each letter triggers a note when activated, using an OscillatorNode for tone generation and a GainNode for volume envelope shaping. The result is a musical typewriter effect where words become melodies.

const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); function playNote(frequency, duration) { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); // Connect: oscillator -> gain -> output osc.connect(gain); gain.connect(audioCtx.destination); // Sine wave for soft, clean tone osc.type = 'sine'; osc.frequency.value = frequency; // ADSR-like envelope const now = audioCtx.currentTime; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.3, now + 0.02); // Attack gain.gain.linearRampToValueAtTime(0.2, now + 0.08); // Decay gain.gain.linearRampToValueAtTime(0, now + duration); // Release osc.start(now); osc.stop(now + duration); }

Oscillator Types and Timbre

The Web Audio API offers four built-in waveforms: sine (pure, mellow), square (retro, buzzy), sawtooth (bright, rich), and triangle (soft, warm). The kinetic typography project uses sine waves for a clean, musical box quality that complements the visual motion without being harsh.

// Frequency from MIDI note number function midiToFreq(note) { return 440 * Math.pow(2, (note - 69) / 12); } // Example: Middle C (C4) = MIDI 60 const middleC = midiToFreq(60); // 261.63 Hz
AudioContext Resumption

Modern browsers require user interaction before allowing audio playback. Always call audioCtx.resume() inside a click or touch event handler. Without this, the oscillator will be created but produce no sound, a common source of bugs in Web Audio projects.

04 / Pentatonic Scale & Melody

The pentatonic scale contains five notes per octave instead of the usual seven. Its special property is that any combination of these notes sounds harmonious together. This makes it perfect for generative music where random note selection must always sound pleasant.

// C major pentatonic: C4, D4, E4, G4, A4 const pentatonic = [ 261.63, // C4 293.66, // D4 329.63, // E4 392.00, // G4 440.00 // A4 ]; // Extended with octave above for range const extendedScale = [ ...pentatonic, 523.25, // C5 587.33, // D5 659.25 // E5 ]; // Map letter index to scale note function letterToNote(index, totalLetters) { const scaleIndex = index % extendedScale.length; return extendedScale[scaleIndex]; }

Words Become Melodies

Each letter in a word maps to a specific note based on its index. The first letter plays C4, the second D4, and so on. When a word forms, its letters play in sequence, turning the word into a unique melody. Longer words create more complex musical phrases that ascend through the scale and wrap back around.

// Play word as ascending melody function playWordMelody(word, tempo) { const noteDelay = 60 / tempo; // seconds per beat for (let i = 0; i < word.length; i++) { const freq = letterToNote(i, word.length); const delay = i * noteDelay * 1000; setTimeout(() => { playNote(freq, noteDelay * 0.8); particles[i].scale = 1.3; // Visual pulse }, delay); } }
Why Pentatonic?

The pentatonic scale avoids semitone intervals (half steps), which are the source of dissonance in Western music. Without semitones, any two notes played simultaneously or sequentially sound consonant. This is why pentatonic scales appear in music traditions worldwide, from Chinese folk to blues.

05 / Animation State Machine

The animation system uses a finite state machine with four states: idle, exploding, orbiting, and forming. Each state defines how particles behave, and transitions between states are triggered by timers or user interaction. This keeps the animation logic clean and predictable.

// State machine: idle -> exploding -> orbiting -> forming -> idle const states = { IDLE: 'idle', EXPLODING: 'exploding', ORBITING: 'orbiting', FORMING: 'forming' }; let currentState = states.IDLE; let stateTimer = 0; function updateStateMachine(dt) { stateTimer += dt; switch (currentState) { case states.IDLE: // Letters sit in word formation if (stateTimer > 2.0) { transitionTo(states.EXPLODING); } break; case states.EXPLODING: // Letters fly apart with velocity applyExplosionForce(); if (stateTimer > 0.8) { transitionTo(states.ORBITING); } break; case states.ORBITING: // Letters orbit center point updateAllOrbits(); if (stateTimer > 3.0) { transitionTo(states.FORMING); } break; case states.FORMING: // Letters return to word positions moveToTargets(); if (allParticlesSettled()) { transitionTo(states.IDLE); selectNextWord(); } break; } }

State Transitions

The transitionTo function resets the state timer and prepares particles for their new behavior. During the explosion transition, each particle gets a random velocity. During the forming transition, target positions are calculated for the new word layout.

function transitionTo(newState) { currentState = newState; stateTimer = 0; if (newState === states.EXPLODING) { // Give each particle random outward velocity particles.forEach(p => { const angle = Math.random() * Math.PI * 2; const force = Math.random() * 6 + 4; p.vx = Math.cos(angle) * force; p.vy = Math.sin(angle) * force; p.state = 'exploding'; }); } if (newState === states.FORMING) { // Calculate target positions for new word particles.forEach((p, i) => { p.targetX = startX + i * spacing; p.targetY = centerY; p.state = 'forming'; }); } }
State Timer Pattern

Using a state timer that resets on each transition is simpler than tracking global time. Each state only needs to know how long it has been active. This pattern is widely used in game development for animation sequences, AI behaviors, and UI transitions.

06 / Interactive Controls & Touch

The project supports both desktop and mobile interaction through unified input handling. Users can tap words to trigger animations, adjust tempo and gravity with sliders, and switch between animation modes. All control values feed directly into the physics engine parameters.

// Unified pointer handling (mouse + touch) const canvas = document.getElementById('canvas'); canvas.addEventListener('pointerdown', (e) => { e.preventDefault(); const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Check if tap hit a letter particle const hit = particles.find(p => { const dx = p.x - x; const dy = p.y - y; return Math.sqrt(dx * dx + dy * dy) < 25; }); if (hit) { // Trigger single letter animation + sound activateLetter(hit); } else { // Trigger full word cycle transitionTo(states.EXPLODING); } }); // Touch-friendly: prevent scroll on canvas canvas.addEventListener('touchmove', (e) => { e.preventDefault(); }, { passive: false });

Slider Controls for Physics

Range inputs allow real-time adjustment of the physics simulation. The tempo slider controls how fast the word melody plays, while the gravity slider adjusts how quickly particles fall during the explosion phase. Mode switching changes the orbital pattern between circular, wave, and spiral.

// Tempo control: affects melody speed and animation timing const tempoSlider = document.getElementById('tempo'); tempoSlider.addEventListener('input', (e) => { settings.tempo = parseFloat(e.target.value); // Update state durations based on tempo settings.idleDuration = 120 / settings.tempo; settings.orbitDuration = 180 / settings.tempo; }); // Gravity control: affects explosion arcs const gravSlider = document.getElementById('gravity'); gravSlider.addEventListener('input', (e) => { settings.gravity = parseFloat(e.target.value); }); // Mode switching (circular, wave, spiral) const modeButtons = document.querySelectorAll('.mode-btn'); modeButtons.forEach(btn => { btn.addEventListener('click', () => { settings.orbitMode = btn.dataset.mode; modeButtons.forEach(b => b.classList.remove('active') ); btn.classList.add('active'); }); });
PointerEvents over Touch + Mouse

Using the Pointer Events API (pointerdown, pointermove, pointerup) handles both mouse and touch input with a single set of event listeners. This eliminates the need for separate mouse and touch handlers and avoids the 300ms touch delay on mobile browsers.