I made an Apple launch page for a bowl of ramen. "Ramen Pro Max". Scroll down and every ingredient gets its own product shot, with its own spec card ("A17 Noodle Engine", "18-hour tonkotsu"). Then they all rain into the bowl and it turns into the finished ramen. Then there's a flavor picker and a Buy button.
The short version: the website is code, the 3D is Tripo. Opus 5.5 can write an amazing Three.js site, but when it has to draw the food with code, you get flat discs and tubes. Tripo turns one line of text into a real textured 3D model. Put the two together and you can do this for anything: sneakers, your product, your college project, your startup.
Here's everything, step by step.
0. Get Tripo (free credits)
🎁 500 bonus credits + 60% off your first month
- Click the link above (or the link in my bio), or
- Go to the official Tripo website, click your profile avatar, find the Redeem Code section and enter J0SG4G
Discount code: TRIPOCREW
⚠️ Complete both registration and purchase within 24 hours to get the offer.
1. The stack
| Piece | What I used |
|---|---|
| 3D models | Tripo Studio, Text to 3D |
| Code | Claude Opus 5.5 |
| 3D in the browser | three.js |
| Smooth scroll | Lenis |
| Scroll animations | GSAP + ScrollTrigger |
| Build tool | Vite |
| Model compression | gltf-transform |
npm create vite@latest ramen-pro-max -- --template vanilla
cd ramen-pro-max
npm i three gsap lenis
npm i -g @gltf-transform/cli
2. Generate every piece as its own model in Tripo
The key idea: don't generate one model of the whole thing. Generate the finished product once (the hero), then every part separately. That's what lets the site pull it apart and put it back together.
In Tripo Studio: Model → Text to 3D, paste a prompt, hit Generate. Each one takes about 2 minutes.
I added this style line to the end of every ingredient prompt so they all match:
The hero (the finished bowl):
The ingredients:
| Part | Prompt |
|---|---|
| Bowl | an empty deep japanese ramen bowl, glossy black ceramic outside with red rim and subtle wave pattern, empty inside |
| Noodles | a nest of wavy yellow ramen noodles lifted and folded, glossy, no bowl |
| Chashu | two round slices of rolled japanese chashu pork belly, spiral fat layers, caramelized torched edges, slices fanned |
| Egg (ajitama) | a soy marinated soft boiled ramen egg cut in half, jammy bright orange yolk, amber brown egg white, two halves side by side |
| Nori | a single rectangular sheet of dark green roasted nori seaweed, slightly curved, glossy texture |
| Scallions | a small pile of thinly sliced fresh green scallion rings |
| Chopsticks | a pair of lacquered dark wooden japanese chopsticks lying parallel, slightly apart |
Export each one as GLB and drop them in public/models/ramen/ as hero.glb, bowl.glb, noodles.glb and so on.
Doing your own product? Same recipe. One prompt for the finished thing, then one prompt per part (a sneaker = sole, upper, laces, insole, box).
3. Shrink the models for the web
Tripo models are big and detailed (mine were around 70 MB each). For a website you want around 1 MB. One command per model:
gltf-transform optimize hero.glb public/models/ramen/hero.glb \
--compress meshopt --texture-compress webp --texture-size 1024 \
--simplify-ratio 0.05 --simplify-error 0.002
Bump --texture-size to 2048 for the hero model if you want it extra crisp.
4. The prompt for Opus 5.5
This is the prompt to get the site built. Paste it with your models already in public/models/ramen/:
Build an Apple-style product launch website for a bowl of ramen called "Ramen Pro Max".
Stack: Vite, three.js, GSAP ScrollTrigger, Lenis smooth scroll. Dark background, Inter font, Apple product page vibe.
3D models (GLB, made in Tripo) are in public/models/ramen/: hero.glb (the finished bowl) plus one file per ingredient: bowl, noodles, chashu, egg, nori, scallions, chopsticks.
Load them with GLTFLoader + MeshoptDecoder. Normalise every model (centre it and scale it to a target size), because they come in at random scales.
Sections, all driven by scroll:
1. Hero: the finished bowl slowly rotating with steam, big title "Ramen Pro Max", tagline "Slurp. Redefined."
2. Statement: "Our most advanced bowl. Ever."
3. "Inside the bowl": a pinned section where each ingredient gets its own product shot, one at a time, centred, same visual size, slowly turning under a soft glow, with a spec card (name, a fake Apple-style spec like "A17 Noodle Engine", one detail line). At the end, every ingredient falls into the empty bowl and it crossfades into the finished hero bowl with a puff of steam.
4. Tech specs grid with count-up numbers (18 hr broth, 120 slurps/s, 98°C, 0.2 mm nori).
5. Flavors: four swatches (Tonkotsu, Shoyu, Miso, Spicy) that tint the glow behind the bowl.
6. Buy: price, monthly price, blue Buy button that triggers a steam burst and a toast "Order up".
Make it work in portrait (phone) too: objects centred, card at the bottom.
Add ?autoplay to scroll the whole page hands-free so I can screen-record it.
Then iterate. My first version showed the whole exploded stack at once and looked messy. What fixed it was telling Opus: one ingredient at a time, same size, centre stage, like a product film.
5. Code snippets that make it work
Normalise every Tripo model
Every GLB comes in at a different scale and offset. This wraps it so it's centred and exactly size units big:
import * as THREE from 'three';
export function normalise(object, size = 1, { anchor = 'center' } = {}) {
object.updateMatrixWorld(true);
const box = new THREE.Box3().setFromObject(object);
const dim = box.getSize(new THREE.Vector3());
const c = box.getCenter(new THREE.Vector3());
const s = size / Math.max(dim.x, dim.y, dim.z, 1e-6);
const pivot = new THREE.Group(), holder = new THREE.Group();
holder.add(object);
holder.scale.setScalar(s);
holder.position.set(-c.x * s, anchor === 'bottom' ? -box.min.y * s : -c.y * s, -c.z * s);
pivot.add(holder);
return pivot;
}
Load with Meshopt (because we compressed them)
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
const loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
const gltf = await loader.loadAsync('/models/ramen/egg.glb');
scene.add(normalise(gltf.scene, 1.8));
Fix "glassy" food
Some exported materials come out see-through or metallic. Food and ceramic should be solid and matte:
function solid(obj) {
obj.traverse((o) => {
if (!o.isMesh) return;
[].concat(o.material).forEach((m) => {
m.transparent = false; m.opacity = 1; m.depthWrite = true;
m.metalness = Math.min(m.metalness ?? 0, 0.15);
m.roughness = Math.max(m.roughness ?? 1, 0.6);
});
});
return obj;
}
The "product film" scroll: one ingredient at a time
p is how far you've scrolled through the pinned section (0 to 1). Each ingredient slides up into the centre, holds, then slides out as the next one comes in:
const n = ingredients.length;
const f = THREE.MathUtils.clamp((p - 0.05) / 0.79, 0, 1) * n; // which ingredient we're on
ingredients.forEach((part, j) => {
const e = f - j; // < 0: not yet, 0..1: on stage, > 1: gone
const enter = THREE.MathUtils.smootherstep(e, -0.15, 0.22);
const exit = j === n - 1 ? 0 : THREE.MathUtils.smootherstep(e, 0.78, 1.15);
part.position.y = (1 - enter) * -2.6 + exit * 2.6; // rise in, rise out
part.scale.setScalar(part.fit * (0.85 + 0.15 * enter)); // same visual size for all
part.rotation.y = time * 0.35; // slow turntable
setOpacity(part, enter * (1 - exit));
});
Everything drops into the bowl
After the last ingredient, they hang in a ring over the empty bowl and fall in one after another. Then the empty bowl crossfades into the finished hero model:
const asm = THREE.MathUtils.smoothstep(p, 0.85, 0.98); // assembly progress
ingredients.forEach((part, j) => {
const a = (j / n) * Math.PI * 2;
const k = THREE.MathUtils.clamp(asm * 1.6 - (j / n) * 0.6, 0, 1); // staggered
part.position.set(
Math.cos(a) * 1.3 * (1 - k),
THREE.MathUtils.lerp(1.05, 0.1, k * k), // gravity-ish ease
Math.sin(a) * 0.6 * (1 - k)
);
part.scale.setScalar(part.fit * 0.45 * (1 - k * 0.7));
});
heroBowl.visible = asm > 0.7; // the finished Tripo bowl takes over
Steam (cheap and convincing)
A few hundred points with a soft round texture, each with a "life" from 0 to 1. Rise, drift, fade, respawn:
for (let i = 0; i < N; i++) {
life[i] += dt * 0.25;
if (life[i] > 1) life[i] = 0;
const a = seed[i] * 6.283, r = 0.6 * (1 - life[i] * 0.3);
pos[i * 3] = Math.cos(a) * r + Math.sin(t * 0.8 + seed[i]) * 0.25 * life[i];
pos[i * 3 + 1] = 0.4 + life[i] * 1.3;
pos[i * 3 + 2] = Math.sin(a) * r;
}
steamGeometry.attributes.position.needsUpdate = true;
Fade each puff with sin(life * PI) in the shader so it appears and disappears smoothly.
6. Code vs Tripo (why you need both)
I also asked Opus to build the ramen out of pure code: cylinders, discs, torus rings. It works, it reads as ramen, but it looks like a low-poly game asset. Swap in the Tripo model and it suddenly looks like a product shot. That's the whole point:
- Opus 5.5 is great at the experience: the scroll story, the animation timing, the layout, the copy.
- Tripo is great at the object: real shape, real textures, in one line of text.
7. Do it with your own thing
- Pick an object (sneaker, headphones, your product, your mascot).
- Generate the finished object + each part in Tripo.
- Compress them with gltf-transform.
- Paste the prompt from step 4 into Opus 5.5, swapping "ramen" for your thing and the spec card jokes for yours.
- Iterate on the scroll story until every section has one clear shot.
Post it and tag me @avi_vashishta29, I want to see what you make.
Free Tripo credits: my invite link · code J0SG4G · discount TRIPOCREW · 500 bonus credits + 60% off your first month (register and purchase within 24 hours).