HOARDE Mascot Studio

A built-in scrubber + exporter for the Three.js mascot defined in hoarde-mascot.js. Lets you freeze a frame, A/B compare options, export single transparent PNGs, and record the whole cycle (or one state) as a numbered PNG sequence that ffmpeg can turn into animated WebP / WebM-alpha / ProRes for re-use anywhere.

How to open it

Link to section: How to open it

The mascot runs on the live /hoarde/ page and exposes the same tuner behind a URL flag. The dedicated studio page bundles only the mascot in a clean box with no surrounding hero content:

URL What you get
/hoarde-mascot-studio/ Mascot + tuner, no site chrome. Use this for tuning.
/hoarde-mascot-studio/?freeze=eureka:1.5 Studio page rendering a frozen frame at t=1.5s of eureka.
/hoarde/?mascot-edit=1 Live /hoarde/ page with the tuner mounted overlay-style.
/hoarde/?freeze=stateId:t Live page with the mascot frozen at one frame, intro/bob disabled (headless-capture friendly).
/hoarde/?mascot-edit=1&freeze=... Both — frozen frame with tuner mounted so you can scrub from there.

The studio page is marked noindex, nofollow and excluded from the site nav / sitemap / RSS.

Tuner controls

Link to section: Tuner controls

A gear icon (⚙) in the top-right of the mascot canvas opens a panel that floats fixed in the viewport corner. Each fieldset:

Action buttons at the bottom:

Double-click any slider to snap it back to its as-designed default (captured the first time the panel opens). The slider's title attribute shows the default value.

Single-frame export (PNG)

Link to section: Single-frame export (PNG)
  1. Scrub to the frame you want, or pass ?freeze=stateId:t in the URL.
  2. Pick size px (e.g. 1024 for a slide asset), leave bg empty for transparent.
  3. Pick wire boost + blending (see "Transparency notes" below).
  4. Click export png.

Filename pattern: hoarde-mascot-{state}-t{t}-{blend}-x{boost}.png so A/B exports don't overwrite each other.

Sequence recording (transparent video)

Link to section: Sequence recording (transparent video)
  1. Open the record fieldset.
  2. Set fps out (5–60). 30 is a sensible default; 15 cuts file size in half with little perceptual loss on most state animations.
  3. Set range:
  4. Tune size, bg, wire boost, blending first; they apply per frame.
  5. Click record sequence.

Full-cycle exports skip the intro fade-in on purpose. On the live page, the fade-in only plays on first load — every loop-wrap goes straight into a fully-revealed booting state. Recording the fade would make the resulting video unloopable (faded frame 0 vs. fully-revealed last frame). The recorder forces introDuration = 0 so frame 0 is already revealed and the last frame of the spin lands you cleanly back at frame 0.

Output destination is chosen automatically:

The status line at the bottom of the fieldset reports recording X / N during the run, then done — N frames plus the ffmpeg recipe for that exact sequence.

While recording, the live animation loop is paused. Anything you change in the tuner during a recording is ignored — the recorder snapshots the options at start.

ffmpeg recipes

Link to section: ffmpeg recipes

Replace BASE_%05d.png with the actual filename pattern (the status line prints it). Replace 30 with whatever fps out you used.

%05d is ffmpeg's printf-style placeholder for "decimal integer, zero-padded to 5 digits" — i.e. it matches the 00000, 00001, … suffix the recorder writes. BASE is the rest of the filename. For frames named hoarde-mascot-full-30fps-add-x1_0_00000.png, hoarde-mascot-full-30fps-add-x1_0_00001.png, …, the input pattern is hoarde-mascot-full-30fps-add-x1_0_%05d.png.

Link to section: Animated WebP — recommended for re-importing into a web page

Universal browser support (Safari 14+ included), small files, alpha kept.

The simplest path is img2webp from the webp package — it's purpose-built for sequence-to-animated-webp and uses shell glob expansion instead of a printf pattern:

brew install webp   # one-time
img2webp -lossless -loop 0 -d 33 BASE_*.png -o out.webp
# -d N = milliseconds per frame; 33 ≈ 30 fps. Use $((1000 / FPS)) for others.
# Add -q 75 -m 6 instead of -lossless for ~10× smaller lossy output.

If you want to stay in ffmpeg, note that Homebrew's default ffmpeg formula is built without libwebp, so libwebp_anim fails with Unknown encoder. Either install webp and use img2webp (above), or swap to a fuller build:

brew uninstall ffmpeg
brew tap homebrew-ffmpeg/ffmpeg
brew install homebrew-ffmpeg/ffmpeg/ffmpeg

That tap ships ffmpeg with libwebp + libwebp_anim enabled, and these work:

ffmpeg -framerate 30 -i BASE_%05d.png \
       -c:v libwebp_anim -lossless 1 -loop 0 \
       out.webp

ffmpeg -framerate 30 -i BASE_%05d.png \
       -c:v libwebp_anim -lossless 0 -q:v 75 -loop 0 \
       out.webp

Other targets (work with the stock Homebrew ffmpeg)

Link to section: Other targets (work with the stock Homebrew ffmpeg)
# WebM-alpha (VP9) — Chrome/Firefox/Edge <video>. Safari can't play it.
ffmpeg -framerate 30 -i BASE_%05d.png \
       -c:v libvpx-vp9 -pix_fmt yuva420p \
       -b:v 0 -crf 30 \
       out.webm

# ProRes 4444 for video editors (Final Cut, Premiere, DaVinci Resolve).
ffmpeg -framerate 30 -i BASE_%05d.png \
       -c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le \
       out.mov

# APNG — bulletproof <img> embed in any browser. Files are large.
ffmpeg -framerate 30 -i BASE_%05d.png \
       -plays 0 \
       out.apng

Re-importing the result

Link to section: Re-importing the result
<!-- Animated WebP (recommended) — drops in anywhere a PNG would. -->
<img src="/assets/img/mascot-eureka.webp"
     alt="HOARDE agent reaching a proof"
     width="360" height="203">

<!-- WebM with PNG fallback (browser picks what it can play). -->
<video autoplay loop muted playsinline width="360" height="203">
  <source src="/assets/img/mascot-eureka.webm" type="video/webm">
  <img src="/assets/img/mascot-eureka.png" alt="HOARDE agent">
</video>

<!-- <picture> with WebP + APNG fallback. -->
<picture>
  <source srcset="/assets/img/mascot.webp" type="image/webp">
  <img src="/assets/img/mascot.apng"
       alt="HOARDE agent" width="360" height="203">
</picture>

Transparency notes (why "wire boost" and "blending" exist)

Link to section: Transparency notes (why "wire boost" and "blending" exist)

The bracket wire layers use THREE.AdditiveBlending at very low opacities (~0.015–0.03 after bracketOpacityFactor). On the live page over the dark --bg-gradient, additive blending writes framebuffer_rgb = dst_rgb + src_rgb — the wire color is added to the dark page background, ignoring its own alpha for RGB purposes.

When exported to PNG with alpha: true, the framebuffer starts at (0,0,0,0). Additive contributes RGB but barely registers in the alpha channel. The PNG ends up with bright-ish RGB and tiny alpha. When you composite that PNG normally (source-over: result = src + dst·(1−srcA)), the destination dominates and the brackets read dim.

Two knobs fix this:

The eye is not always solid — wire-fill, full-recipe, and state-wire build the eye entirely from additive layers. In those variants the eye also gets boosted; that's usually fine because the eye's additive opacities aren't dim-factored (no bracketOpacityFactor), so it reaches readable levels at lower boost values than the brackets.

Files

Link to section: Files