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.
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.














