2024 — 2025 · Frontend developer

ASIS Badges

Three bilingual Next.js sites for the ASIS hardware-CTF badge programme — Iran Badge, YUZ Badge, and the umbrella landing at badge.asis.sh — one design family across scroll-scrubbed 3D assembly, interactive hardware specs, challenges, and scoreboards.

Next.jsTypeScriptTailwind CSSGSAPMotionmodel-viewershadcn/ui

ASIS Badges screenshots

1 / 15

Overview

ASIS Badges is one programme with three public frontends. Each edition of the hardware CTF ships a custom PCB competitors wear and attack; each edition also ships a site that tells the board's story, walks through assembly in 3D, lists challenges, and shows the live scoreboard. The umbrella page at badge.asis.sh ties the editions into one family.

EditionYearBadgeSite
1st2024Iran Badgeiran.badge.asis.sh
2nd2025YUZ Badgeyuz.badge.asis.sh
——Collection landingbadge.asis.sh

A third board (Damavand) is in design; the landing already reserves its slot.

Scroll-scrubbed 3D assembly

The centrepiece of each edition site is not a play button — it is a scroll runway. A tall section pins a sticky stage under the fixed header while window.scrollY maps continuously onto a GLB explode→assemble clip inside Google's model-viewer. Progress is 0 when the stage locks and 1 when the section ends, so the visitor assembles the badge by scrolling.

The scrub path never goes through React state. Driving setState from scroll would re-render the viewer tree every frame and force layout reads on top of WebGL work — enough to stall a mid-range phone past its frame budget. Instead an imperative BoardViewer handle exposes setProgress, and a requestAnimationFrame loop eases toward the target with frame-rate-independent smoothing. Geometry is measured only on mount and resize; the frame loop is pure arithmetic over scroll position. An IntersectionObserver starts and stops the loop so idle sections do not burn GPU. prefers-reduced-motion snaps straight to the assembled pose.

Camera and runway live in a single site-config scene object — orbit, FOV, scroll runway (taller on desktop, shorter on phones), animation clip name — so framing can be tuned without touching component code. Because model-viewer's field of view is vertical, a landscape board like YUZ would clip left and right on a portrait phone even when it fits top-to-bottom. The viewer tracks stage aspect and pulls the orbit radius back proportionally (capped) so the silhouette stays inside the frame.

Scroll is not only for the board. Section titles use GSAP SplitText + ScrollTrigger, loaded lazily so a cold 3G visit still sees readable headings before the animation bundle arrives. Velocity-linked marquees on Motion sit between sections as lighter scroll texture.

Interactive specs in 3D

/specs is a second scene, not a screenshot of the first. A dedicated static GLB ships already assembled with no animation clips, so nothing has to be parked or seeked. Camera controls are on — orbit and zoom — and bilingual hotspots sit on the mesh (data-position / data-normal) for the MCU, OLED, GNSS, cellular SIM path, SPI flash, shift registers, buttons, and USB-C. Tapping a hotspot opens a dialog with what the part is and why it matters as an attack surface — the same language competitors use on the floor.

Below the model, tabbed tables cover overview, modules, interfaces, and mechanical data so the 3D layer and the datasheet stay one page instead of two disconnected artefacts.

Performance as a design constraint

A scroll-scrubbed clip pays its render bill on every frame. These models carry hundreds of materials; any visible KHR_materials_transmission forces three.js to re-render the whole scene into a multisampled, mipmapped target, then draw transmissive objects again backface-first. That cost is flattened after load — on desktop too, not only mobile — because the affected parts read as opaque plastic anyway. The mobile render profile also zeros contact-shadow intensity so model-viewer never allocates the per-frame depth and blur passes a phone barely resolves. Touch gesture ownership flips while scrubbing (touch-action: none, camera controls off) so the page scroll owns the runway and the board does not fight the finger.

One chassis, three accents

Iran Badge leads with cobalt. YUZ Badge leads with brass. The umbrella landing owns neither — its theme tokens sit near zero chroma so colour on the page comes only from the badge accents it presents. That keeps the shell from competing with the boards it introduces, and keeps the three sites readable as one family rather than three unrelated marketing pages.

Under the paint, the stack is the same: Next.js 16, React 19, Tailwind CSS 4, shadcn/ui, bilingual Persian/English with proper RTL, dark-first theming, and Docker images pushed to GHCR then deployed over SSH. Challenges, galleries, and scoreboards round out each edition site; locale and theme switchers live in the header so one build serves both audiences.