Spinning the BootSplash logo effect out into a standalone app¶
This page is a recipe for taking the WebGL logo effect that
BootSplash.tsx runs at startup and lifting it into a brand-new
project — for example, a one-off site, a "now playing" screen, or a
splash screen for a different app you are prototyping. The goal is
to give you a checklist of every file you would copy or re-implement,
plus the gotchas to watch for, so the lift takes a few hours instead
of a day.
What you actually need¶
The effect is split across three files plus a single texture asset. You can lift any of them independently, but for a fully working demo you want all three.
| Role | File in ButtonsCLI | Purpose |
|---|---|---|
| WebGL bootstrap helpers | src/plugins/effects/webglEffect.ts |
Compiles shaders, allocates a full-screen quad, manages uniforms, validates GLSL outside the runtime. |
| React component (live sliders, presets, dismiss) | src/components/BootSplash.tsx |
Hosts the <canvas>, runs the requestAnimationFrame loop, owns the slider state, persists "Don't show again". |
| The fragment shader itself | Inline GLSL string in BootSplash.tsx (HSYNC_LOGO_FRAGMENT_SHADER) |
The HSYNC sine-wave warp + the new hue / glitch / scanline / chromatic-aberration effects. |
| Logo asset | src/assets/buttonscli.png |
The texture. Replace with any PNG/JPG up to ~2048×2048; the shader auto-centers it at 28% of viewport height. |
If you only need the visual (no React, no sliders, no persistence),
you can paste the GLSL into a single HTML file and you are done in
under 200 lines. If you want the full preset library and the "don't
show again" UX, copy BootSplash.tsx as-is into a new React project
and delete the useConfigStore bits.
Minimal standalone HTML version¶
If you just want a hero animation, save the snippet below as
logo.html and open it in a browser. It uses the same fragment
shader as ButtonsCLI but with vanilla JS, no build step, no React.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Logo Effect</title>
<style>
html, body { margin: 0; height: 100%; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
// Paste HSYNC_LOGO_FRAGMENT_SHADER from src/components/BootSplash.tsx here.
// It is the string that starts with `precision mediump float;` and
// ends with the closing brace of main(). The body of main() expects
// the uniforms listed below.
const FRAG = `...`;
const canvas = document.getElementById("c");
const gl = canvas.getContext("webgl", { alpha: false, antialias: false });
// ... compile + link, same pattern as webglEffect.ts.
</script>
</body>
</html>
If you would rather start from a runnable project, the cleanest
incubation environment is a Vite + React + TypeScript scaffold
(pnpm create vite@latest logo-app -- --template react-ts). Copy
webglEffect.ts and BootSplash.tsx into src/, replace the
@/assets/buttonscli.png import with a path to your own logo, and
the dev server (pnpm dev) will render the animation on the
default route.
Decision tree: which approach should you pick?¶
- Marketing splash for a one-page site → plain HTML + the GLSL snippet. No build step, no React, easy to drop into any CMS.
- Standalone desktop splash for a different Tauri / Electron app
→ Vite + React + TypeScript scaffold, copy
BootSplash.tsx. You get the preset library and the slider UI for free. - Reusable npm package for other React apps → create a new
package (
@you/buttonscli-logo-effect) that exports a single<BootSplash logoUrl="..." onComplete={...} />component. Pull the shader out ofBootSplash.tsxinto its own.glslfile and usevite-plugin-glsl(orrollup-plugin-glsl) to inline it. - You want a runtime where the shader is hot-editable → pair
the React version with the existing
src/plugins/effects/shaderLabpattern that already exists in ButtonsCLI for the terminal postprocess effects. The shader lab file watcher is the part to copy.
File-level checklist (full lift into a new project)¶
- Create the new project.
- Copy
src/plugins/effects/webglEffect.ts→ your project'ssrc/effects/webglEffect.ts. The file is self-contained; it only depends on the DOM. There is no ButtonsCLI-specific code in it. - Copy the GLSL string out of
BootSplash.tsxintosrc/effects/bootSplash.glsland load it via a Vite/Rollup glsl plugin, or just keep it as a TypeScript string constant. The inline-string approach is fine for a single shader. - Copy
src/components/BootSplash.tsx→src/BootSplash.tsx, strip theuseConfigStoredependency, and point the asset import at your own logo. The component renders a full-screen canvas + a fixed-position slider panel + a dismiss dialog. - Copy the asset
src/assets/buttonscli.png(or your replacement) into your project'spublic/folder and update the import path. - If you want the in-app slider panel to be invisible (e.g. for a
marketing splash), delete the
<div>block inBootSplash.tsxthat has theposition: fixed; top: 12px;style. The animation still runs. - If you want saveable presets, lift the
BUILTIN_BOOT_PRESETSarray too. Persist custom presets tolocalStorage(or any key-value store your new app already uses).
Things that will trip you up¶
-
Cross-origin assets. If you serve the logo from a CDN and the WebGL context is
alpha: false, the image must be served with CORS headers. ButtonsCLI's logo is bundled with the app, so the effect sidesteps this. For a hosted logo, setgl.texImage2D(... img)afterimg.crossOrigin = "anonymous";and host the asset withAccess-Control-Allow-Origin: *. -
DPR mismatch on retina. The boot loop multiplies the wave period by
devicePixelRatioso a 4K display does not see a slower-looking animation than a 1080p display. If you copy the loop, keep the DPR scaling. The "what this does" comment inBootSplash.tsxis the place to start. -
Frame timing. The boot loop calls
cancelAnimationFramein its cleanup but also clears the GL program. Forgetting thedestroyFullscreenProgramcall leaks GPU memory. The helper inwebglEffect.tsdoes the right thing; just remember to call it on unmount. -
Shader compile errors. If you tweak the GLSL and the screen goes black, open the browser console. WebGL compile errors surface in
gl.getShaderInfoLog. ThevalidateFullscreenFragmentShaderhelper inwebglEffect.tsexists specifically so you can dry-run a shader before you wire it into the live loop — use it from a test or a debug button. -
Color space.
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);inBootSplash.tsxis intentional. Without it, Chrome will silently convert your PNG from sRGB to linear and the colors will look "off" on Windows. Leave the line in unless you are intentionally targeting linear color pipelines. -
Stopping the loop. The
onCompletecallback inBootSplash.tsxis what unmounts the canvas. If your standalone app wants to keep the loop running forever (e.g. as arequestAnimationFramebackground), just delete the OK button and the dismissal logic. The animation will keep going.
Where to extend the effect¶
A few directions the ButtonsCLI team has on the roadmap but is not shipping yet. If you fork the effect, any of these would be a natural next step:
- Per-vertex displacement. Right now the warp is per-scanline (Y direction). A vertex-shader variant could displace individual logo pixels for a "molten" look. The current fragment shader already does this implicitly via the scanline trick, but a real vertex pipeline would be smoother on lower-end GPUs.
- Beat / BPM sync. Add an optional
u_bpmuniform and a beat envelope. The "shift range" already oscillates with a cosine, so multiplying it by a beat pulse is a one-liner. - Color stops. The logo is currently rendered as-is. A
u_colorA → u_colorBmix based on the alpha mask would let the user pick a brand gradient. The existingterminalHsync.tseffect (insrc/plugins/effects/) already does this for the terminal; the technique transfers. - Multi-layer logo. Render the logo twice, once shifted more than the other, with one of them inverted, for a "shadow ghosting" look. Cheap, dramatic, and very '90s.
What is intentionally not in this guide¶
- Tauri-specific window controls. The animation is a pure
browser/WebGL effect; once you have it running in a
<canvas>it will work in any host — Tauri, Electron, a regular browser, a React Native WebView, etc. - The tab-bar / left-dock logos from
TabBar.tsxandPresetBar.tsx. Those are static images with CSS transforms and do not use the WebGL pipeline. They are separate concerns; copy them as plain<img>tags.