Design + Dev · 2023
Vertex Agency Site
A brochure site that burned paid traffic. Rebuilt as an award-style scroll experience: a frame-sequence hero, GSAP section reveals and a CMS so the team publishes case studies without touching code.
Client
Vertex Agency
Industry
Creative Agency
Timeline
2 weeks
My role
Designer & Front-End Developer
The problem
A forgettable brochure site that generated almost no inbound leads despite strong paid traffic.
The solution
An animated, scroll-driven marketing site with a CMS so the team can publish case studies themselves.
+3.1x
Inbound leads
+180%
Time on page
2 wks
Delivery time
What I built
Scroll-driven hero
300-frame sequence mapped to scroll, decoded to ImageBitmap for zero jank.
CMS-driven work
Sanity studio for case studies, team and testimonials with live preview.
Section reveals
GSAP ScrollTrigger timelines that respect prefers-reduced-motion.
Perfect Lighthouse
98+ performance on mobile despite the heavy hero.
Architecture
- TanStack Start with SSR and file-based routing
- GSAP ScrollTrigger for timelines, canvas for the frame sequence
- Sanity CMS with GROQ queries cached at the edge
- Cloudflare Pages deploy, sitemap and per-page OG images
Hard problems
Heavy frame sequence
Bounded-concurrency preloading (6 desktop / 3 mobile) plus a nearest-frame fallback keeps scroll smooth while loading.
SEO vs animation
All copy is server-rendered; animation only enhances what is already in the HTML.
Code from the build
A few real excerpts from the repository.
const draw = (index: number) => {
const bmp = frames[index] ?? nearestLoaded(index);
if (!bmp) return false;
const scale = Math.max(canvas.width / bmp.width, canvas.height / bmp.height);
const w = bmp.width * scale;
const h = bmp.height * scale;
ctx.drawImage(bmp, (canvas.width - w) / 2, (canvas.height - h) / 2, w, h);
return true;
};
const tick = () => {
current += (target - current) * 0.12; // easing toward scroll position
draw(Math.round(current));
raf = requestAnimationFrame(tick);
};const limit = navigator.hardwareConcurrency > 4 ? 6 : 3;
export async function loadFrames(urls: string[], onProgress: (n: number) => void) {
let i = 0, done = 0;
const workers = Array.from({ length: limit }, async () => {
while (i < urls.length) {
const index = i++;
const res = await fetch(urls[index]);
frames[index] = await createImageBitmap(await res.blob());
onProgress(++done / urls.length);
}
});
await Promise.all(workers);
}"The site became our best sales asset — clients bring it up on every intro call."
How it was built
- Discovery call, scope and fixed timeline agreed up front
- Custom design system built in Figma before development
- Typed, reviewed codebase with CI checks on every commit
- Deployed with monitoring, backups and post-launch support