Run Rob Run is a one-page portfolio constructed round movement, sound, typography, and interplay. The primary scene is a customized goo object that behaves like a digital materials: it idles, reacts to music, responds to hover, and transforms via scroll from an natural blob right into a structured dice.
The mission got here out of a protracted cycle of constructing, scrapping, rebuilding, and by no means fairly feeling just like the work was able to symbolize me correctly. For years, I stored restarting portfolio concepts earlier than that they had an opportunity to develop into completed. This model shouldn’t be about calling the positioning excellent. It’s about lastly reaching one thing I can sit with, share, and preserve constructing from.
That feeling formed loads of the design selections. I needed the positioning to really feel private with out turning into too polished or distant. The movement wanted to have weight. The typography wanted to really feel direct. The goo wanted to really feel alive sufficient to hold the emotion of the web page, however managed sufficient that it nonetheless labored as an interface.
This breakdown focuses on the goo: how it’s structured, how scroll pushes it from blob to dice, and the way the music response is formed so the motion feels musical moderately than chaotic.
The scene makes use of Three.js/WebGPU for the primary expertise, with layered geometry, customized deformation logic, music-reactive behaviour, scroll state, and some floor particulars equivalent to hover readouts and animated mud.
Idea
The purpose was to not make a background animation. I needed the scene to really feel like an object with presence. One thing tactile, imperfect, and responsive. The goo turned the emotional heart of the positioning: a refractive shell round an orange core, consistently shifting between softness and construction.
The design was constructed round distinction:
- Mushy natural movement towards a ultimate structured dice.
- A transparent refractive shell round a dense orange core.
- Refined idle behaviour towards stronger music-driven pushes.
- Playful hover particulars that solely seem when the person investigates the floor.
That meant the technical system wanted to be versatile. The goo couldn’t be a single mesh doing one factor. It wanted layers, separate response channels, damping, scroll management, and efficiency limits.
Implementation
Scene Construction
The goo is constructed from a number of layered meshes that share the identical base deformation logic however use totally different supplies and response settings.
- The outer shell gives the refractive, glass-like look.
- The orange core offers the article weight and identification.
- A skinny outer coat provides floor shimmer and element.
- Scroll progressively reduces the natural deformation and pushes the shape towards a dice.
- Hover interactions add momentary floor particulars, equivalent to grid readouts and animated mud.
At a excessive degree, every body calculates the scroll state, reads the present music-reactive values, eases them, after which passes them into the deformation perform for every layer.
const scrollProgress = scrollPauseState.getSceneScrollProgress();
const {
morphProgress,
splitProgress,
sharedBlobBumpScale,
forceAllNormals,
} = getMorphRenderState(scrollProgress);
const musicReactiveState = musicReactiveInput.getState();
const musicReactive = getMusicReactiveDeformState({
elapsed,
musicReactiveState,
});
Scroll Morph
The scroll transition strikes the article from a smooth natural state right into a extra structured dice. The deformation system blends between two concepts:
- An natural direction-based blob, with lobes and floor noise.
- A dice projection, the place every vertex is pulled towards the closest dice face.
const organicMix = 1 - smoothstep(0.55, 0.98, morphProgress);
const cubeMix = smoothstep(0.68, 0.995, morphProgress);
When the dice combine will increase, every level is progressively pulled towards its dice place.
if (cubeMix > 0) {
const cubeRadius = baseRadius * THREE.MathUtils.lerp(
1.0,
0.9,
morphProgress
);
const cubePoint = getCubePoint(
dx,
dy,
dz,
cubeRadius
);
px = THREE.MathUtils.lerp(px, cubePoint.x, cubeMix);
py = THREE.MathUtils.lerp(py, cubePoint.y, cubeMix);
pz = THREE.MathUtils.lerp(pz, cubePoint.z, cubeMix);
}

This lets the scene preserve its goo-like character early on whereas nonetheless touchdown in a clear ultimate dice state.
Music Response
The music response is deliberately selective. The goo doesn’t react equally to each sound. Early variations responded too evenly to the complete monitor, which made busy songs really feel noisy and brought on small transient sounds, like hi-hats, to create actions that had been too massive.
The ultimate system weighs the response towards stronger rhythmic occasions: kicks, bigger claps, and four-to-the-floor drum hits. Smaller high-frequency transients nonetheless add vitality, however they’re dampened so they don’t drive the primary form.

const musicLow = musicReactiveState.isActive
? musicReactiveState.low
: 0;
const musicLowPulse = musicReactiveState.isActive
? musicReactiveState.lowPulse
: 0;
const musicMid = musicReactiveState.isActive
? musicReactiveState.mid
: 0;
const musicMidPulse = musicReactiveState.isActive
? musicReactiveState.midPulse
: 0;
const musicHigh = musicReactiveState.isActive
? musicReactiveState.excessive
: 0;
const musicHighSoft = Math.min(musicHigh, 0.38);
const musicMidSoft = Math.min(musicMid, 0.72);
const crowdDensity = Math.min(
1,
musicMidSoft * 0.58 + musicHighSoft * 0.86
);
The system appears for dominant low-end hits, however bass alone shouldn’t consistently inflate the core. The kick response is lowered when the monitor is crowded or when the high-end is just too energetic.
const lowDominance = Math.min(
1,
Math.max(
0,
musicLow - (musicMidSoft * 0.22 + musicHighSoft * 0.32)
) * 4.4
);
const kickLift = Math.max(
0,
musicLowPulseSoft - (musicMidSoft * 0.12 + musicHighSoft * 0.09)
);
const kickHit = mode === "thump"
? Math.min(1, kickLift * 3.5)
: Math.min(1, kickLift * 2.1);
const dominantKickHit =
kickHit * Math.max(
0.45,
lowDominance,
1 - crowdDensity * 0.26
);
Claps and bigger mid-range hits are dealt with individually from high-frequency element. This implies a clap can nonetheless transfer the core, whereas quick hats translate into lighter floor movement.
const highOnlyMask = Math.min(
1,
Math.max(
0,
musicHighSoft - musicMidSoft * 0.72
) * 3.4
);
const clapCoreImpact =
musicMidPulse *
Math.max(0, midBody - highOnlyMask * 0.28) *
(1 - crowdDensity * 0.34) *
0.54;
These audio readings are then transformed into separate movement channels.
return {
centerRoundness,
affect,
coreMassImpact,
limbImpact,
blobSpreadMultiplier:
1 -
centerRoundness * 0.46 +
soloKeyboard * 0.025 +
affect * 0.58,
surfaceBoost:
musicMidSoft * 0.045 +
musicHighSoft * 0.035 +
soloKeyboard * 0.025 +
affect * 0.32,
flowSpeed:
1 +
musicMidSoft * 0.015 +
musicHighSoft * 0.055 +
affect * 0.085,
rippleShift:
musicHighSoft * 0.24 +
soloKeyboard * 0.035 +
affect * 0.58,
};
Damping and Return
The motion additionally wanted weight. If the goo returned too rapidly after a success, it felt nervous and digital. The affect can assault rapidly, however the launch is slower.
const impactEase =
musicReactive.affect > easedMusicImpact
? 0.34
: 0.075;
easedMusicImpact = THREE.MathUtils.lerp(
easedMusicImpact,
musicReactive.affect,
1 - Math.pow(1 - impactEase, dt * 60),
);
The limb response has its personal launch damping. Greater actions settle extra slowly, which provides the core a heavier bodily really feel.
const limbReleaseEase = THREE.MathUtils.lerp(
0.045,
0.022,
easedMusicLimbImpact
);
const limbImpactEase =
limbImpact > easedMusicLimbImpact
? 0.22
: limbReleaseEase;
easedMusicLimbImpact = THREE.MathUtils.lerp(
easedMusicLimbImpact,
limbImpact,
1 - Math.pow(1 - limbImpactEase, dt * 60),
);
Making use of the Response
The outer shell and interior core don’t obtain the identical response. The shell will get a softer response so it stays clear and refractive. The orange core receives extra of the bodily motion.
deformBlob(blobGeometry, elapsed, {
baseRadius: 1.64,
bumpScale: sharedBlobBumpScale,
blobSpreadMultiplier: THREE.MathUtils.lerp(
1,
easedMusicReactive.blobSpreadMultiplier,
0.28,
),
morphProgress,
reactiveSurfaceBoost:
easedMusicReactive.surfaceBoost * 0.32,
reactivePointerBoost:
easedMusicReactive.pointerBoost * 0.24,
reactiveRippleShift:
easedMusicReactive.rippleShift * 0.36,
reactiveCenterRoundness:
shellCenterRoundness,
});
The core will get the bigger motion, however it’s clamped so it can’t push outdoors the clear outer shell.
deformBlob(innerGeometry, elapsed, {
baseRadius: 1.64 * 0.6 * innerCoreScale,
bumpScale:
sharedBlobBumpScale *
coreSurfaceReaction *
THREE.MathUtils.lerp(1, 0.54, limbReturnDamping),
blobSpreadMultiplier:
easedMusicReactive.blobSpreadMultiplier,
lobeStrengthMultiplier:
coreLobeReaction,
reactiveSurfaceBoost:
easedMusicReactive.surfaceBoost *
coreSurfaceReaction *
coreReturnCalm,
reactivePointerBoost:
easedMusicReactive.pointerBoost *
0.8 *
coreReturnCalm,
sphericalDirectionBlend:
musicReactiveState.isPlaying ? 0.68 : 0,
maxRadius: 1.58,
});
Refinement
As soon as the primary behaviour was working, more often than not went into tuning. The aim was to maintain the scene expressive with out letting it develop into noisy or costly.
Hover Floor Particulars
The hover interactions are separate from the music system. They’re designed as momentary floor behaviours moderately than everlasting results.
The grid readout seems on hover, exhibits dwell values, and disappears as soon as scrolling begins so it doesn’t intrude with the morph.
The mud works in an identical method. It seems beneath the cursor, however the mud texture itself animates via precomputed noise frames, just like the site-wide noise layer. This makes the mud really feel alive with out recalculating costly noise each body.
if (now - lastDustUpdate > 1000 / DUST_NOISE_SPEED) {
lastDustUpdate = now;
dustFrameIndex += 1;
drawDustFrame({
context: dustA.context,
texture: dustA.texture,
frames: dustFramesA,
frameIndex: dustFrameIndex,
});
}
Efficiency Notes
The primary lesson was that the visible high quality got here from restraint as a lot as complexity.
- Costly noise was precomputed the place doable.
- Regular updates are staggered throughout layers.
- The music response is smoothed earlier than touching the geometry.
- Hover results are disabled throughout scroll so the morph stays clear.
- The WebGPU model has fallback paths for units that can’t run the primary scene.
The scene additionally avoids treating each layer the identical. The outer shell, core, and coat replace with totally different ranges of depth, which retains the composition readable and helps the clear materials keep clear.
Accessibility
As a result of the mission depends closely on movement and sound, the expertise wants a number of guardrails.
- The scene can nonetheless be explored with out music playback.
- Music response is user-controlled moderately than autoplayed.
- Hover particulars are enhancements, not required for navigation.
- On coarse pointer units, the desktop hover layers are disabled.
- Fallback rendering is obtainable for units that can’t run the WebGPU scene.

For manufacturing, I might additionally proceed bettering reduced-motion assist. The scroll story can nonetheless work with calmer deformation, fewer computerized pulses, and fewer reactive motion for customers preferring lowered movement.
Wrap-up
The ultimate goo system is a stability between artwork course and restraint. It listens to the music, nevertheless it doesn’t obey each sound. Kicks and bigger claps create the stronger pushes, whereas high-frequency element turns into smaller floor vitality. Scroll pulls the shape from natural to structured, and hover interactions reveal small traces of the system beneath.
What I discovered most from constructing it was that the standard didn’t come from including extra motion. It got here from deciding which motion mattered, then damping, clamping, and separating the reactions till the article felt prefer it had mass, reminiscence, and resistance.
Sources and Instruments
- Canvas-generated textures
- Audio-reactive state mapping
- Three.js
- WebGPU
- GSAP / ScrollTrigger









