The last time I created an online museum experience, it kinda felt off. You walk from room to room, wait for things to load, and look at static images. It's not bad, but it lacks vibes.
So I decided to try a different approach: build something that felt less like a museum and more like a stream of consciousness. What if you could scrub through art history the way you scrub through a YouTube video?
Enter Gogh With The Flow :)
The Metropolitan Museum of Art has an incredible Open Access API. It’s a treasure trove of human history. But APIs have rate limits, and fetching data for 1450, 1451, 1452... as fast as a user can scroll would instantly hit a wall (or just be incredibly slow).
I wanted the experience to be fluid. No "Loading..." text. No spinners. Just flow.
To solve this, I employed a "hybrid" strategy:
- Pre-indexed "Shards": I scraped the collection to build lightweight JSON files for every year (e.g.,
1889.json,1450.json). These contain just the valid Object IDs for that year. They are tiny and load instantly. - On-Demand Detail: When the dialer settles on a year, the app grabs a random selection of IDs from that shard and fetches the images.
- Lazy High-Res: We load optimized thumbnails first. Only when you click to "enter" a painting do we ping the API for the massive, high-resolution file, allowing you to zoom in and see the individual brushstrokes.
GSAP does the heavy lifting regarding animations. DOM manipulation is heavy but by using GSAP's InertiaPlugin and careful tweening, the transition between years—fading out the old era and drifting in the new—happens without layout trashing or "ghosting" ( as much as I could pull it off :) )
Example here showing how 'paralax' panels are handles with minimal perf impact
// The "Time Travel" Engine
const updatePanels = () => {
// Calculate how fast we are scrubbing
const scrollPos = dialer.scrollLeft;
const delta = scrollPos - lastScrollPos;
panels.forEach(panel => {
// Create parallax depth: background moves slower,
// the "window" moves counter to create focus
let speedMultiplier = panel.className.includes('panel-window') ? -1.5 :
panel.className.includes('panel-further') ? 0.25 : 0.5;
// Apply momentum without layout thrashing
let currentX = gsap.getProperty(panel, 'x') || 0;
currentX += delta * speedMultiplier;
// Infinite Loop Logic:
// If a panel goes off-screen, warp it to the other side
const screenPos = panel.offsetLeft + currentX;
if (screenPos > viewportWidth + 200) {
currentX -= viewportWidth + 400;
} else if (screenPos < -200) {
currentX += viewportWidth + 400;
}
// Use transforms for 60fps performance
gsap.set(panel, { x: currentX });
});
};
Visuals are only half the story. To truly sell the time travel aspect, the soundscape shifts with you.
- 1400s: Pre-Renaissance choral echoes
- 1600s: Baroque strings
- 1800s: Romantic orchestral swells
- 1900s: Early variations of impressionistic sound
The audio player is set up so it cross-fades tracks as you traverse centuries and even handles "radio" logic, automatically queuing up a new random track from the current era when one finishes
If it sounds interesting, go ahead and give it a spin. 1889 is a good year ( but me personally, I'm into baroque heavily, didn't even realize it until debugging this thing :) )
Live Demo - https://notbigmuzzy.github.io/goghwiththeflow/
Source Code - https://github.com/notbigmuzzy/goghwiththeflow
Powered by the Metropolitan Museum of Art API.
Top comments (0)