You have seen the Shorts where a familiar skyline grows from dirt to glass while a year counter ticks.
Channels in that lane do real numbers. CITY 3D TIMELAPSE sits around 87.4K subs with 19 videos. One Paris film, 300 BCE to 2025, has about 9.73M views (YouTube, checked 10 Nov 2026). People stop for the "how did this place become that" beat.
Same grammar. Different tool. Every building here is code: a grid cell, an era, a height table. Your agent can rebuild it. No scraped mesh pack. No copyrighted footage.
I built a working demo for this post. Fictional city called Riverbend. It renders a silent 9:16 MP4 with a pulled-back aerial orbit. The pieces that matter are all in this post, so you can rebuild it from scratch.
(Also rendered as an MP4: 1080×1920, 12s, 30fps, H.264.)
What you are making
- About 10 to 12 seconds, 1080×1920, 30fps
- Mute-safe: year HUD + densifying blocks carry the story
- Fewer, better-placed buildings on a street grid, a river with bridges, parks and trees
- One cathedral landmark stays locked near center so the place is readable
- Not Brand World 3D (floating brand islands). Not a live tilt-shift town. This is historical densify and rise.
Stills from the demo:
Early years. Plaza, parish tower, first houses along the river.
Market centuries. Streets densify. Landmark (green spire) stays pinned.
2025. Glass towers rise behind the cathedral. The river and old town still read.
How the scene is built
The demo is three files:
-
index.html: a full-frame canvas, the year HUD, a one-line caption, and a small label at the bottom -
scene.js: Three.js scene, city layout, growth logic, camera -
render.mjs: opens the page in headless Chrome, steps through time, saves PNG frames, then FFmpeg makes the MP4
The one idea that makes it work: the whole film is a function of one number. setProgress(t) takes t from 0 to 1 and sets the year, every building height, the light and the camera. No timeline, no keyframe editor. Same t, same frame, every time. That is what lets you render frame by frame and get a clean, repeatable video.
1. Footprints on a grid
Buildings sit on a loose lot grid. Some cells stay empty for avenues. Nothing goes in the river. Each lot gets a zone based on where it sits.
const LOT = 1.55; for (let ix = -7; ix <= 7; ix++) { for (let iz = -8; iz <= 6; iz++) { const avenue = (Math.abs(ix) % 3 === 0) || (Math.abs(iz) % 3 === 0); if (avenue && rand() < 0.72) continue; // leave streets open const x = ix * LOT, z = iz * LOT; if (Math.abs(x - riverXAt(z)) < RIVER_HALF + 0.35) continue; // keep the river clear const zone = zoneOf(x, z); // plaza | historic | mid | cbd | near | outer // ... } } Zones are the real design tool. plaza stays empty around the cathedral. historic is the old town ring. near is the strip closest to the camera, and it only gets parks and low cottages. cbd only exists on the far side of the landmark.
rand() is a seeded generator (mulberry32, seed 42), not Math.random(). Same seed, same city on every render.
Too many blocks turn the frame to noise at phone size, so the layout drops a share of lots on purpose. Fewer, better-placed buildings read much better. Riverbend ends with about 75 buildings plus the cathedral.
2. Era and a height table per building
Every building gets an era (the year it appears) and a small height table: [year, height] pairs. Between pairs the height eases with a smoothstep.
if (zone === 'historic') { era = 60 + rand() * 280; kind = 'house'; } if (zone === 'mid') { era = 400 + rand() * 600; kind = 'house'; } if (zone === 'cbd') { era = 1880 + rand() * 130; kind = era > 1960 ? 'tower' : 'office'; } // a tower: appears short, then shoots up within about 25 years heights = [[era, 4], [era + 10, h * 0.55], [era + 25, h * 0.95], [2025, h]]; // a house: grows a floor or two over centuries heights = [[era, h1 * 0.5], [era + 120, h1], [1600, h * 0.85], [1900, h]]; Colour comes from era too, from a small fixed palette: cream and sandstone with red roofs early, brick from 1200, concrete from 1750, glass for towers. A limited palette is what makes it look like an architectural model instead of random boxes.
When a building's year arrives it pops up with a little overshoot (easeOutBack over 50 years), plus a random stagger so a whole district does not appear on the same frame.
Bridges appear in 880. Trees and parks show up early. The caption under the year changes at fixed beats: empty banks, first houses, walls and parish, market streets, brick densifies, mills and docks, rail arrives, towers rise, skyline locks in.
3. Lock the landmark, then move the camera
The cathedral sits at the origin and never moves. The camera stays south of it and orbits slowly, so the river and spire stay near center for the whole film.
function setProgress(t) { const u = clamp(t, 0, 1); const eased = u < 0.9 ? Math.pow(u / 0.9, 0.8) : 1; // last 10% holds on 2025 setYear(START_YEAR + (END_YEAR - START_YEAR) * eased); const angle = 0.48 + u * 0.28; // stay in the southern quadrant const radius = 48 - u * 5; // tiny dolly in const height = 32 - u * 3; camera.position.set(Math.sin(angle) * radius * 0.42, height, Math.cos(angle) * radius); camera.lookAt(0, 1.6 + u * 2.0, -0.8); camera.fov = lerp(32, 28, u); camera.updateProjectionMatrix(); composer.render(); } One rule matters most: towers in front of the landmark kill the shot. Put the CBD behind the cathedral (far side, away from the camera) and keep the near strip low. Then the spire always reads, and the towers rise as a backdrop.
The rest is light work. A sun that moves across the sky, a slow drift to dusk after 1985, warm window lights on the towers as it gets darker, a little bloom and a vignette.
4. Render to MP4
render.mjs serves the folder locally, opens it in headless Chrome at 1080×1920, waits for window.__ready, then loops:
for (let i = 0; i < 360; i++) { // 12s × 30fps await page.evaluate((t) => window.setProgress(t), i / 359); await page.screenshot({ path: `frames/frame-${String(i).padStart(4, '0')}.png` }); } Then FFmpeg stitches the frames: -framerate 30 -c:v libx264 -pix_fmt yuv420p -crf 18 -movflags +faststart. It also saves three stills on the way (at t = 0.10, 0.48 and 1.0), which are the images above.
You need Node.js, Chrome and FFmpeg. No GPU. Headless Chrome renders WebGL in software, so it is slow but steady.
Asking Claude to build it
You do not have to type the code above. In Claude Code, describe it in steps and check a still after each one. Something like:
- "Make a Three.js scene in a 1080×1920 page. Fictional city on a curving river. Seeded random. Expose
window.setProgress(t)that drives everything from t = 0 to 1." - "Place buildings on a loose grid with zones. Keep a plaza around a cathedral at the origin. Towers only behind the cathedral. Near the camera, parks and low houses only. Fewer buildings, not more."
- "Give each building an era and a height table. Colour by era from a small palette. Add a year HUD and a short caption that changes at fixed beats."
- "Camera: south of the landmark, slow orbit, tiny dolly. The spire stays near center for the whole film."
- "Write
render.mjs: headless Chrome, 360 frames, FFmpeg to MP4, plus three stills."
Check stills before you render the full thing. A still takes seconds. A full render does not.
Try these prompts
Once the demo works, these are good next steps:
- Swap the city. "Replace Riverbend with a fictional port city on a curved bay. Keep the same landmark-lock camera. Add 12 more blocks with eras from 200 to 2010. Keep the year HUD and 9:16 frame."
- Stronger eras. "Add a caption table for these beats only: first houses, walls, market, mills, rail, towers. Fade caption text over 8 frames when it changes. Do not add more UI."
- Night finale. "From year 1990 onward, dim the hemisphere light and turn on warm window emissives on modern towers only. Hold the last 1.5 seconds on 2025."
- Render checklist. "Export stills at years 100, 800, 1600, 1900, 2025 into
/stills. Then render a 12-second 1080x1920 MP4 at 24fps silent. Tell me the output paths."
If you want ready-made looks
The free ReelStyles kit has 7 styles you can install and study. Each one is a style guide, scene templates, render steps and a checklist the agent grades its own frames against. Code is on GitHub: github.com/reelstyles/reelstyles-free.
In Claude Code:
/plugin marketplace add reelstyles/reelstyles-free /plugin install reelstyles-free@reelstyles-free Paid tiers are on reelstyles.com: Starter is 31 styles for $19. Full is all 125 styles for $49 for the first 20 buyers, then $79. Both are one payment.
To be clear: city-history as a full kit is a look I am building toward. It is not in the live catalog yet. The demo above is the open recipe, so you can ship your own Short while that catches up.
I am Ahmed (@medevsmaker). I build ReelStyles: Claude Code skills that make videos in code.




