Run Rob Run is a one-page portfolio constructed round movement, sound, typography, and interplay. The principle scene is a customized goo object that behaves like a digital materials: it idles, reacts to music, responds to hover, and transforms by means of 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 characterize me correctly. For years, I stored restarting portfolio concepts earlier than they’d an opportunity to turn into completed. This model just isn’t about calling the location excellent. It’s about lastly reaching one thing I can sit with, share, and maintain constructing from.
That feeling formed a variety of the design selections. I needed the location 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 relatively than chaotic.
The scene makes use of Three.js/WebGPU for the principle expertise, with layered geometry, customized deformation logic, music-reactive behaviour, scroll state, and some floor particulars comparable to hover readouts and animated mud.
Idea
The goal 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 grew to become the emotional heart of the location: a refractive shell round an orange core, always shifting between softness and construction.
The design was constructed round distinction:
- Smooth natural movement in opposition to a closing structured dice.
- A transparent refractive shell round a dense orange core.
- Delicate idle behaviour in opposition to stronger music-driven pushes.
- Playful hover particulars that solely seem when the consumer 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 couple 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, comparable to grid readouts and animated mud.
At a excessive stage, every body calculates the scroll state, reads the present music-reactive values, eases them, after which passes them into the deformation operate 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 comfortable 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 maintain its goo-like character early on whereas nonetheless touchdown in a clear closing 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 your entire observe, which made busy songs really feel noisy and brought about small transient sounds, like hi-hats, to create actions that have 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 principle 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 seems for dominant low-end hits, however bass alone shouldn’t always inflate the core. The kick response is lowered when the observe is crowded or when the high-end is just too lively.
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,
influence,
coreMassImpact,
limbImpact,
blobSpreadMultiplier:
1 -
centerRoundness * 0.46 +
soloKeyboard * 0.025 +
influence * 0.58,
surfaceBoost:
musicMidSoft * 0.045 +
musicHighSoft * 0.035 +
soloKeyboard * 0.025 +
influence * 0.32,
flowSpeed:
1 +
musicMidSoft * 0.015 +
musicHighSoft * 0.055 +
influence * 0.085,
rippleShift:
musicHighSoft * 0.24 +
soloKeyboard * 0.035 +
influence * 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 influence can assault rapidly, however the launch is slower.
const impactEase =
musicReactive.influence > easedMusicImpact
? 0.34
: 0.075;
easedMusicImpact = THREE.MathUtils.lerp(
easedMusicImpact,
musicReactive.influence,
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 inside 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 not 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 principle behaviour was working, more often than not went into tuning. The purpose was to maintain the scene expressive with out letting it turn into noisy or costly.
Hover Floor Particulars
The hover interactions are separate from the music system. They’re designed as momentary floor behaviours relatively than everlasting results.
The grid readout seems on hover, reveals dwell values, and disappears as soon as scrolling begins so it doesn’t intrude with the morph.
The mud works in an analogous approach. It seems underneath the cursor, however the mud texture itself animates by means of 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 principle lesson was that the visible high quality got here from restraint as a lot as complexity.
- Costly noise was precomputed the place potential.
- 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 principle 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 couple of guardrails.
- The scene can nonetheless be explored with out music playback.
- Music response is user-controlled relatively than autoplayed.
- Hover particulars are enhancements, not required for navigation.
- On coarse pointer units, the desktop hover layers are disabled.
- Fallback rendering is accessible for units that can’t run the WebGPU scene.

For manufacturing, I might additionally proceed bettering reduced-motion help. 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 path and restraint. It listens to the music, but it surely 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.
Assets and Instruments
- Canvas-generated textures
- Audio-reactive state mapping
- Three.js
- WebGPU
- GSAP / ScrollTrigger

