Project notes

How this was made

Credits, a couple of notes on how AI was used in the build, and a per-section reference any developer can use to rebuild the animations on the home page.

01 Credits

Created by
Will McClung
Illustrations by
Casey Rooney
Rive puppet illustration drawn by
Morgan Zavoral
Get in touch
rivers@riversgoat.com

02 Notes

Built over roughly three months in collaboration with Claude (Anthropic) across three surfaces: Claude Code in the terminal for code generation, edits, and deploys; Claude.ai for planning conversations and design briefs; and Claude in Chrome for live audits on the deployed site. I described what I wanted in plain language, Claude wrote and debugged the code, and the back-and-forth iterated until the page felt right. The art is the part that isn't AI — the candidate portraits and hero composition were drawn by Casey Rooney, Morgan Zavoral drew the Brady puppet layers, and the Rive rig was assembled by Will McClung.

The product idea, the animation moments, and the taste calls are mine. The scroll wiring, the database, the deploy configuration, and most of the CSS are where Claude did the heavy lifting. The references below are what a developer would need to rebuild any individual section — each names the library, the canonical docs, the design pattern in plain words, and the file in this repo where it lives.

03 Animation references

Hero impact scroll-driven cross-fade

  • Library GSAP 3.12 + ScrollTrigger
  • Docs gsap.com/docs/v3/Plugins/ScrollTrigger/
  • Pattern Scrub timeline (timeline progress = scroll progress). Two <img> elements are stacked at the same position — normal Brady on top, shocked Brady underneath. Three tweens share one timeline: (1) the word NOT drops in via y + opacity, (2) the two Brady images cross-fade via opposing opacity tweens, (3) a CSS ::after strike-through bar on "the" scales from 0 to 1. scrub: true on the ScrollTrigger ties timeline progress to scroll progress so the user controls the pace.
  • Why this approach Stacked images cross-fade more cheaply than swapping src (no re-layout, no network round-trip). Sharing one timeline keeps the three tweens in lockstep regardless of scroll speed.
  • In repo index.html_createHeroST() (scroll trigger), _impactTl() (timeline), .line-not CSS, the two <img> elements #hero-brady-img and #hero-brady-img-shocked
  • Your inspiration

Accolades pin stamp-down on scroll

  • Library GSAP + ScrollTrigger (pin + scrub)
  • Docs ScrollTrigger — see the pin and scrub options
  • Pattern The accolades section gets pin: true, scrub: true, end: '+=Npx'. The user feels they're scrolling through the section, but actual page scroll is driving timeline progress while the pinned element holds in place. Inside that span, a chain of fromTo() tweens sequences each accolade (championships, MVPs, Super Bowl wins) onto the page with a discrete progress mark on the timeline. Each sticker uses a scale + filter-blur transform to feel like a physical stamp landing.
  • Why this approach Pinning + scrub is far more reliable than position: sticky for orchestrated multi-element sequences — it gives precise control over when within the scroll each element fires.
  • In repo index.html.section-stage ScrollTrigger and the .word-slot / .cards-col markup it drives
  • Your inspiration

Puppet dance vector character animation

  • Library Rive runtime (@rive-app/canvas) loaded via unpkg CDN
  • Docs rive.app/docs
  • Editor rive.app — the designer rigs the layered art into a state machine inside the editor; the runtime plays the resulting .riv file
  • Pattern The character is rigged from twelve named bones (head, head_hype, torso, upper_arm L/R, forearm L/R, hand_L/R open/closed, legs). On the page, scroll position drives the dance timeline's progress; a separate JS loop tracks scroll velocity and triggers a "celebration" state-machine branch above a threshold — the easter egg you see on rapid-scroll.
  • Why this approach A full Rive character is ~50 KB versus 5+ MB for a comparable GIF, stays sharp at any size because it's vector, and can respond to multiple inputs (scroll, click, hover) without re-rigging.
  • In repo index.html Rive init + scrub loop · Assets/puppet/*.riv (rigged source files)
  • Your inspiration

Mobile viewport URL-bar jitter fix

  • APIs window.visualViewport, dynamic viewport units (dvh / svh / lvh)
  • Docs MDN: Visual Viewport API
  • Reference web.dev: large, small, and dynamic viewport units
  • Pattern A <script> at the very top of <head> reads window.visualViewport.height (falling back to window.innerHeight) and publishes it as a CSS custom property, --vh-px. The value is grow-only: once the URL bar retracts and the viewport gets taller, that height becomes the new high-water mark and never shrinks back. CSS uses var(--vh-px, 100svh) for any pinned-section height. orientationchange resets the high-water mark.
  • Why this approach Without the shim, the URL bar's transition between expanded and collapsed states makes svh/dvh resolve to wildly different values on the same load — producing the "everything in the corner of the screen" mobile bug. visualViewport is the only API that reports the actually-visible region rather than the layout viewport.
  • In repo index.html — first <script> in <head>
  • Your inspiration CSS-Tricks: The trick to viewport units on mobile — using a CSS custom property synced to visualViewport.height to prevent URL-bar jitter

riversgoat.com canvas 2d lightning + character reveal

  • What it is Vote for Brady on tb12notgoat.com and you're ejected here — no warning, no modal. Full-bleed Philip Rivers silhouette, procedural lightning, drifting spark particles, and a "vote again" link back. Live at riversgoat.com.
  • Library None. Canvas 2D, vanilla JS, Google Fonts. Zero dependencies.
  • Pattern Bolts are drawn in two passes per segment: a wide low-alpha glow stroke, then a bright core stroke. Each bolt carries an array of branch sub-paths drawn the same way. Lifetime is an alpha curve — fast flash in, slow afterglow out. 70 spark particles drift upward continuously and get velocity-kicked when a bolt strikes near them. A 25% flurry chance fires a second bolt 120ms after the first. Clicking/tapping anywhere spawns a bolt at the cursor — the user is the weather. Rivers sits on top as a PNG with a data-mode attribute on the stage toggling four reveal states: silhouette, flash, outline, lit.
  • Why this approach Zero-dependency Canvas 2D means no load overhead. The branching bolt algorithm and the particle kick system came back correct on the first pass with a detailed brief — no debugging loop. This was the clearest example in the project of a thorough brief producing accurate output in few exchanges.
  • In repo rivers-goat.html — self-contained, no imports, everything inline

04 Responsible AI — cite the build

Project
Tom Brady Is Not The GOAT (tb12notgoat.com)
Human author
Will McClung — rivers@riversgoat.com
Level of effort
~8 days of active git history (Apr 19–26, 2026), ~50 commits, ~300+ Claude exchanges across three surfaces. Earlier planning and design conversations in claude.ai are not measurable from the codebase but added several weeks to the wall-clock total
Level of dev
Northwestern full stack bootcamp certificate. No CS degree, and hadn't used GSAP, Rive, Express, or Postgres before this project. Can read code, write briefs, and push back when the output is wrong. Daily AI tool user. The build required an AI partner to ship.
AI models
Claude (Anthropic) — claude-sonnet-4-6, claude-opus-4-6, claude-opus-4-7
AI surfaces
Claude Code (CLI) — in-repo edits, debugging, deploys
Claude on the web (claude.ai) — planning, briefs, copy
Claude in Chrome (extension) — live audits on the deployed site
Human role
All product decisions, art direction, design briefs, taste calls, review of every code change before commit, mobile testing, copy edits
AI role
Majority of code generation, debugging, refactoring, deployment configuration, scroll-animation wiring, server + database, draft of this page
Illustrators
Casey Rooney (illustrations), Morgan Zavoral (puppet layers)
Third-party
GSAP, Rive runtime, Express, node-postgres, Railway, Postgres
Data policy
No analytics. No third-party tracking. Vote counts stored as anonymous integers. No user account. localStorage records only the user's own vote choice for the current device

05 Prompts that pushed the needle

Verbatim from the build. The seven that did the most work, with a note on why each one was effective. Almost no technical jargon — Claude diagnosed the cause, I held the taste line.

  1. "Desktop sees 2 votes. Mobile sees 10. Same database." Symptom + contrast + same source. Three short clauses told Claude where the bug had to be (between DB and client) without me knowing what a CDN cache was. Diagnosis came back inside one turn.
  2. "On mobile the first scroll has resistance. The second one feels normal. Desktop is fine. Don't change desktop." Symptom, contrast, and an explicit constraint on what NOT to break. The constraint is the part most bug reports skip and it's what kept Claude from "fixing" the working desktop hero in the process.
  3. "I voted for Alexander on the live site and it didn't add one but it knew it was my vote." Two paired observations: behavior A happened, behavior B didn't, both witnessed. The pairing localized the bug to the gap between local state and the server write — exactly where it was.
  4. "We have about perfected the experience. There is one issue left and we should save state now because every time we attempt this we come up short. Let's think differently." A meta-prompt that broke a loop. We'd been iterating on the same hero bug across six commits with no progress. This paused the cadence and forced a different framing — which led to actually finding the cause. The most valuable shape of prompt in the whole build.
  5. "Wait is the only issue SVG is a silhouette? If that works that's a cleaner solve." A course-correction with a hypothesis embedded. Not "you're wrong, try again" — a specific alternative theory that turned out to be the actual fix (PNG re-rasterization on transform). Hypothesis-bearing prompts are far stronger than open-ended rejections.
  6. "Make the comments dev-friendly. The point of everything is that this is animation design inspo for devs and non-technical people." Defined the project's audience and intent in one sentence. Every code comment, the About page tone, and the README structure flowed from this prompt afterward. A single line that set the standard for everything downstream.
  7. "I hate the voice and attitude of the document." Direct rejection without softening. No "this is good but..." preamble. That bluntness saved several rounds of incremental polishing on a draft that needed to be rewritten from scratch — and signaled a different draft was wanted, not edits to this one.