Genuary Day 2: Squash & Stretch in GLSL
Implementing Animation Principles with Audio-Reactive Shaders
Day 2 of Genuary 2026 challenges us to explore the twelve principles of animation—foundational techniques developed by Disney animators Ollie Johnston and Frank Thomas. I focused on two principles that translate beautifully to shader code: squash & stretch and exaggeration.
Click "Load Demo Track" to see the blob react to music, or upload your own audio file.
Traditional animation uses squash and stretch to give objects weight and flexibility. A bouncing ball compresses when it hits the ground, stretches as it launches upward. This deformation conveys mass and energy.
In GLSL, I implemented this using signed distance fields (SDFs) and audio-driven deformation.
The Core SDF
The base shape is a sphere, but its radius is modulated by audio:
1float sdSphere(vec3 p, float r) {2return length(p) - r;3}45float scene(vec3 p) {6vec3 warped = warp(p);7float radius = 1.0 + u_volume * 0.12 + u_bass * 0.08;8float blob = sdSphere(warped, radius);9return blob;10}
u_volumecontrols overall size (average of bass/mid/high)u_bassadds extra expansion when low frequencies hit
This creates squash & stretch—the blob literally grows and shrinks with sound energy. Watch how the radius changes with audio intensity in the demo above.
Warping for Organic Deformation
But uniform scaling isn't enough. Real squash and stretch isn't perfectly symmetrical. Objects deform directionally based on force.
I added multi-layered warping that distorts space itself:
1vec3 warp(vec3 p) {2float warpAmp = 0.3 + u_bass * 0.15;3float warpSpeed = 0.5 + u_high * 0.3;45// Primary noise-based warp (bass-driven amplitude)6p += warpAmp * vec3(7noise(p * 2.0 + u_time * warpSpeed),8noise(p * 2.0 + u_time * warpSpeed + 10.0),9noise(p * 2.0 + u_time * warpSpeed + 20.0)10);1112// Secondary cosine warp (mid-driven)13float midWarp = 0.15 + u_mid * 0.08;14p += midWarp * cos(3.0 * p.yzx + u_time);1516// Tertiary detail warp (high-driven)17float highWarp = 0.08 + u_high * 0.05;18p += highWarp * cos(7.0 * p.zxy + u_time * 1.3);1920return p;21}
This function takes a 3D point and distorts it through three layers:
- Noise-based warp — Creates organic, flowing deformation controlled by bass amplitude and high-frequency speed
- Cosine warp — Adds rolling waves driven by mids
- Detail warp — Surface-level distortion from highs
Each layer responds to different audio frequencies at different scales. Bass creates large, sweeping deformations. Highs add fine detail. Together, they make the blob feel like it's breathing, pulsing, reacting.
This is squash & stretch applied to 3D space. Instead of deforming a mesh, we're deforming the raymarching function itself.
The second principle is exaggeration—pushing reality for dramatic effect. Animation isn't a 1:1 copy of physics. It's an amplified, stylized interpretation.
In my shader, audio data is intentionally exaggerated:
1float warpAmp = 0.3 + u_bass * 0.15; // Bass gets amplified2float radius = 1.0 + u_volume * 0.12 + u_bass * 0.08; // Double influence of bass
Bass doesn't just control the warp—it controls both the warp amplitude AND the radius. This creates compounding visual impact.
The fresnel shine is also exaggerated based on volume:
1float shineIntensity = 0.25 + u_volume * 0.35 + u_high * 0.15;2col += fresnel * shineIntensity * mix(vec3(1.0), palette(u_time * 0.1), u_volume);
At low volumes, the shine is subtle. As audio energy increases, it intensifies dramatically. This isn't realistic lighting—it's expressive lighting.
To further exaggerate the visual response, color shifts based on volume intensity. This creates three distinct emotional states:
Tier 1: White Iridescent (0-0.25 volume)
1vec3 whiteIridescent = vec3(0.92, 0.92, 0.95) + 0.08 * cos(6.28318 * (t + vec3(0.0, 0.33, 0.67)));
During quiet moments, the blob is pearlescent white with subtle rainbow shimmer. Calm. Waiting.
Tier 2: Soft Bloom (0.25-0.5 volume)
1vec3 a2 = vec3(0.7, 0.7, 0.75);2vec3 b2 = vec3(0.3, 0.3, 0.3);3vec3 c2 = vec3(1.0, 1.0, 1.0);4vec3 d2 = vec3(0.0 + u_bass * 0.1, 0.33 + u_mid * 0.08, 0.67 + u_high * 0.1);5vec3 bloomColors = a2 + b2 * cos(6.28318 * (c2 * t + d2));
As sound builds, pastel colors emerge. The palette begins to respond to audio frequencies but remains gentle.
Tier 3: Full Saturation (0.5+ volume)
1vec3 d3 = vec3(0.0 + u_bass * 0.15, 0.33 + u_mid * 0.1, 0.67 + u_high * 0.15);2vec3 fullSaturated = vec3(0.5) + vec3(0.5) * cos(6.28318 * (t + d3));
At high volumes, colors fully saturate and shift dramatically with bass, mids, and highs. The blob becomes vibrant, energetic, alive.
Smooth Transitions
The transitions use smoothstep for smooth blending between tiers:
1float t1 = smoothstep(0.0, 0.3, vol); // white -> bloom2float t2 = smoothstep(0.3, 0.6, vol); // bloom -> saturated34vec3 col = mix(whiteIridescent, bloomColors, t1);5col = mix(col, fullSaturated, t2);
This creates exaggerated emotional progression through color. The visual mood shifts with audio energy, amplifying the feeling beyond what the sound alone conveys. Load the demo track above and watch how the colors evolve through quiet and intense sections.
The piece uses raymarching to render the blob in real-time:
1vec4 raymarch(vec3 ro, vec3 rd) {2float t = 0.0;34for(int i = 0; i < 80; i++) {5vec3 p = ro + rd * t;6float d = scene(p);78if(d < 0.001) {9vec3 normal = getNormal(p);1011float fresnelPow = 2.0 - u_volume * 0.18;12float fresnel = pow(1.0 - max(0.0, dot(normal, -rd)), fresnelPow);1314float colorSpeed = 0.1 + u_mid * 0.1;15vec3 col = palette(fresnel + t * 0.1 + u_time * colorSpeed);1617float shineIntensity = 0.25 + u_volume * 0.35 + u_high * 0.15;18col += fresnel * shineIntensity * mix(vec3(1.0), palette(u_time * 0.1), u_volume);1920return vec4(col, 1.0);21}2223if(t > 10.0) break;24t += d;25}2627float bgPulse = 0.02 + u_bass * 0.012;28return vec4(bgPulse, bgPulse, bgPulse + 0.03, 1.0);29}
This marches a ray through the scene, checking the signed distance at each step. When it hits the surface (distance < 0.001), it calculates:
- Surface normal
- Fresnel effect (edge glow)
- Color based on time and fresnel
- Shine intensity
Even the background subtly pulses with bass. Every element is exaggerated for maximum visual impact.
The Web Audio API's AnalyserNode provides FFT data in real-time:
1// Split spectrum into three bands2for (let i = 0; i < 10; i++) bass += dataArray[i];3for (let i = 10; i < 50; i++) mid += dataArray[i];4for (let i = 50; i < bufferLength; i++) high += dataArray[i];56// Smooth for natural easing7smoothBass = smoothBass * 0.92 + bass * 0.08;8smoothMid = smoothMid * 0.92 + mid * 0.08;9smoothHigh = smoothHigh * 0.92 + high * 0.08;
Smoothing with a factor of 0.92 creates trailing, cascading motion. Bass changes linger longer than highs. This implements another animation principle: follow-through—different parts move at different rates.
Experiment with different music genres using the demo at the top:
- Classical: Watch the blob breathe with dynamics
- Electronic/bass music: See extreme squash & stretch
- Jazz: Notice mid-driven color cycling
- Ambient: Experience the white iridescent tier
The same GLSL interprets different music in completely different ways.
This is day 2 of 31. Follow along with #genuary and #genuary2026.
Find all prompts at genuary.art.