Run Rob Run presents a single-page portfolio centered on motion, sound, typography, and user interaction. At its core sits a custom goo object—a digital material that idles quietly, pulses to music, responds when users hover over it, and gradually transforms from a soft, organic blob into a rigid cube as visitors scroll down the page.

The project emerged from years of starting over. The creator repeatedly scrapped and rebuilt portfolio concepts, never quite satisfied that they captured their work fairly. This version abandons the pursuit of perfection in favor of something livable, shareable, and open to future iteration.

That philosophy shaped every design choice. The site needed to feel personal without becoming overly polished. Motion had to carry weight. Typography had to speak directly. The goo itself needed enough life to convey emotion while remaining controlled enough to function as an interface.

The technical foundation rests on Three.js and WebGPU, layering geometry, custom deformation logic, music-reactive behavior, scroll state management, and surface details like hover readouts and animated dust particles.

Concept

Rather than treating the goo as a background animation, the creator wanted it to feel like an object with tangible presence—something tactile, imperfect, and alive. The goo became the emotional anchor of the entire site: a refractive shell surrounding an orange core, perpetually shifting between softness and structure.

  • Soft organic motion contrasted against a final structured cube
  • A clear refractive shell wrapping a dense orange core
  • Subtle idle behavior punctuated by stronger music-driven movements
  • Playful hover details that emerge only when users engage with the surface

Achieving this required a flexible technical system. The goo could not be a single mesh performing one function. It needed multiple layers, separate reaction channels, damping mechanisms, scroll control, and performance safeguards.

Implementation

Scene Structure

The goo comprises several layered meshes that share the same underlying deformation logic but employ different materials and response settings.

  • The outer shell delivers the refractive, glass-like appearance
  • The orange core provides weight and visual identity
  • A thin outer coat adds surface shimmer and fine detail
  • Scroll gradually suppresses organic deformation and nudges the form toward a cube
  • Hover interactions introduce temporary surface details, including grid readouts and animated dust

Each frame follows the same sequence: calculate scroll state, read current music-reactive values, ease them smoothly, and feed them into the deformation function for each 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 guides the object from a soft organic state into a more structured cube by blending between two distinct deformation approaches: an organic direction-based blob with lobes and surface noise, and a cube projection where each vertex is pulled toward the nearest cube face.

const organicMix = 1 - smoothstep(0.55, 0.98, morphProgress);
const cubeMix = smoothstep(0.68, 0.995, morphProgress);

As the cube mix increases, each point gradually moves toward its cube position.

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 approach preserves the goo's character early in the scroll while still arriving at a clean, final cube state.

Music Reaction

The music reaction is deliberately selective. Early iterations responded equally to every sound, causing busy tracks to feel noisy and small transient sounds like hi-hats to create disproportionately large movements.

The final system prioritizes stronger rhythmic events: kicks, larger claps, and four-to-the-floor drum hits. High-frequency transients still contribute energy, but they are dampened so they do not dominate the overall shape.

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.high : 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 identifies dominant low-end hits, but bass alone should not constantly inflate the core. Kick response diminishes when the track is crowded or when high-end activity is too intense.

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 larger mid-range hits receive separate handling from high-frequency detail. A clap can still move the core, while fast hats translate into lighter surface motion.

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 converted into separate motion channels.

return {
  centerRoundness,
  impact,
  coreMassImpact,
  limbImpact,
  blobSpreadMultiplier: 1 - centerRoundness * 0.46 + soloKeyboard * 0.025 + impact * 0.58,
  surfaceBoost: musicMidSoft * 0.045 + musicHighSoft * 0.035 + soloKeyboard * 0.025 + impact * 0.32,
  flowSpeed: 1 + musicMidSoft * 0.015 + musicHighSoft * 0.055 + impact * 0.085,
  rippleShift: musicHighSoft * 0.24 + soloKeyboard * 0.035 + impact * 0.58,
};

Damping and Return

Movement needed to feel weighty. If the goo returned too quickly after being hit, it felt jittery and artificial. The impact can attack rapidly, but the release unfolds more slowly.

const impactEase = musicReactive.impact > easedMusicImpact ? 0.34 : 0.075;
easedMusicImpact = THREE.MathUtils.lerp(
  easedMusicImpact,
  musicReactive.impact,
  1 - Math.pow(1 - impactEase, dt * 60),
);

The limb response employs its own release damping. Larger movements settle more gradually, giving the core a heavier, more physical presence.

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),
);

Applying the Reaction

The outer shell and inner core do not receive identical reactions. The shell gets a softer response to maintain clarity and refraction. The orange core receives more of the physical movement.

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 receives larger movement, but it is clamped to prevent it from pushing beyond 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

After the core behavior was functioning, most effort went into tuning. The objective was maintaining expressiveness without introducing noise or excessive computational cost.

Hover Surface Details

Hover interactions operate independently from the music system. They function as temporary surface behaviors rather than permanent effects.

The grid readout appears on hover, displays live values, and vanishes when scrolling begins so it does not interfere with the morph.

The dust behaves similarly. It appears beneath the cursor, but the dust texture itself animates through precomputed noise frames, much like the site-wide noise layer. This approach makes the dust feel dynamic without recalculating expensive noise calculations every frame.

if (now - lastDustUpdate > 1000 / DUST_NOISE_SPEED) {
  lastDustUpdate = now;
  dustFrameIndex += 1;
  drawDustFrame({
    context: dustA.context,
    texture: dustA.texture,
    frames: dustFramesA,
    frameIndex: dustFrameIndex,
  });
}

Performance Notes

The primary insight was that visual quality came from restraint as much as from complexity.

  • Expensive noise was precomputed wherever feasible
  • Normal updates are distributed across layers
  • The music response is smoothed before modifying geometry
  • Hover effects are disabled during scroll to keep the morph clean
  • The WebGPU version includes fallback paths for devices unable to run the main scene

The scene also avoids treating every layer identically. The outer shell, core, and coat update with varying levels of intensity, which keeps the composition readable and helps the transparent material remain clear.

Accessibility

Since the project depends heavily on motion and sound, the experience requires certain safeguards.

  • The scene can be explored without music playback
  • Music reaction is user-controlled rather than autoplayed
  • Hover details are enhancements, not navigation requirements
  • On coarse pointer devices, desktop hover layers are disabled
  • Fallback rendering is available for devices unable to run the WebGPU scene

For production use, the creator would continue strengthening reduced-motion support. The scroll narrative can function with calmer deformation, fewer automatic pulses, and less reactive movement for users who prefer reduced motion.

Wrap-up

The finished goo system balances art direction with restraint. It listens to the music, but it does not obey every sound. Kicks and larger claps create stronger pushes, while high-frequency detail becomes smaller surface energy. Scroll pulls the form from organic to structured, and hover interactions reveal small traces of the underlying system.

The central lesson from building it was that quality did not come from adding more movement. It came from deciding which movement mattered, then damping, clamping, and separating the reactions until the object felt like it had mass, memory, and resistance.

Resources and Tools

  • Canvas-generated textures
  • Audio-reactive state mapping
  • Three.js
  • WebGPU
  • GSAP / ScrollTrigger

Source: Codrops