How Fragsplainer Works
A guide for readers exploring these shaders, and for authors who want to write their own.
Reading an explainer
An explainer is a three-panel machine for pulling apart a complex shader and showing how it works from the inside out. The three panels stay in lock-step: an interaction in any one of them is reflected in the others. Drag the dividers to resize them, or click a panel’s corner button to focus it (the others fold to slim tabs); on a narrow screen they stack into a single column with a tab bar.
The render
On the left, the live output — code running on your GPU, not a video. Click and drag directly on it (many shaders read the mouse), or drive it through the widgets embedded in the prose. If the frame-rate sags on a weak GPU, the renderer quietly drops its internal resolution a notch at a time until it’s smooth again; a · NN% badge in the corner tells you when that has happened.
The prose
In the middle, the explanation. Watch for inline sliders and toggles, underlined terms you can hover for a definition, and coloured lenses that highlight the matching code on the right. Expandable disclosures go a level deeper without cluttering the main thread.
The code
On the right, the real source. As you read, it scrolls to the region under discussion and highlights it. Live uniform values are substituted in place, so dragging a slider changes the numbers you see in the code. Hover a function or variable for a glossary popover, and follow the “↗ Shadertoy” link to the original.
The model
Every explainer is built from one folder of content. There is no per-shader JavaScript and no manual DOM wiring — the page assembles itself from data.
The three panels are independent Astro islands (Svelte 5 components). Because each island is its own bundle, they don’t share a normal Svelte store; instead they talk through framework-agnostic nanostores:
| Store | Holds |
|---|---|
config | the active shader’s uniforms + pass graph |
uniforms | live uniform values (persisted per-shader to localStorage) |
passes | which code tab is shown |
focus | the { pass, region } the code panel scrolls to |
tokens | source tokens a Lens is highlighting |
popover | the shared glossary tooltip |
technique | which technique slide-over is open |
The layout seeds these stores twice — once on the server (so each island server-renders with the right values) and once on the client before hydration. After that, each MDX component hydrates itself and reads the stores reactively. Adding an explainer never touches this machinery.
Frontmatter
The YAML header of index.mdx is the shader’s config: its metadata, its uniforms, and its pass graph. This is the “shared brain” that wires the prose to the code.
---
title: "The Triquetra"
blurb: "A three-petalled knot drawn from nothing but circles."
kind: shader
tags: [single-pass]
fit: contain # contain (square, letterboxed) | cover (fill)
source: https://www.shadertoy.com/view/... # optional — shows a ↗ link
passes:
image: { src: ./image.glsl, channels: [] }
uniforms:
zoom: { type: float, default: 0.8, min: 0.1, max: 3.0, step: 0.05, label: zoom }
---Each uniform becomes a live value: it is injected into every pass as a GLSL uniform (referenced by its bare name) and can be driven from the prose with a <Slider> or <Toggle>. The min/max/step bound the slider; label is what shows inline.
For multipass shaders, declare each pass and the input channels it samples (iChannel0..3, given as pass names), plus an optional shared common file:
common: ./common.glsl
passes:
bufferA: { src: ./bufferA.glsl, channels: [bufferA, bufferB] }
bufferB: { src: ./bufferB.glsl, channels: [bufferA, bufferB] }
image: { src: ./image.glsl, channels: [bufferA, bufferB] }Buffer passes (any pass not named image) are ping-pong feedback targets rendered to RGBA16F textures, so they can read their own previous frame. The image pass is always the one shown on screen.
GLSL & regions
Write Shadertoy-style mainImage(out vec4 fragColor, in vec2 fragCoord). The engine injects the #version, the precision line, the standard Shadertoy inputs (iResolution, iTime, iFrame, iMouse, iChannel0..3, plus iTimeDelta, iDate and the rest), a #define R (iResolution.xy) shorthand, and one uniform per declared uniform — so you never write boilerplate.
Mark the spans of code you want the prose to point at with // @region name … // @end. A <Disclosure> or <Lens> can then scroll to and highlight that region by pass#region:
// @region framing vec2 uv = fragCoord / iResolution.xy; uv *= zoom; // @end
Components
Import what you use at the top of the MDX and add a hydration directive (client:visible is the usual choice). Keep components inline with the prose — a component alone on its own line is parsed by MDX as a block and splits the paragraph.
import Slider from '../../../components/mdx/Slider.svelte'; import Lens from '../../../components/mdx/Lens.svelte'; It stamps a single dot <Slider client:visible u="iterations" /> times, walking each one along a path traced by two <Lens client:visible sym="armSpin">arms</Lens>.
| Component | Props | Does |
|---|---|---|
Slider | u | Inline range bound to a uniform. Shows the uniform’s label (or name) and live value; the value also mirrors in the code panel, formatted to match. |
Toggle | u | Switch bound to a bool uniform; shows the label. |
Lens | sym, tech? | Highlights matching source tokens on hover. sym is one token (rotate) or a space-separated list of synonyms for one concept (C.rg myPos target). tech="rotation" opens that technique slide-over on click. |
Term | id | Glossary word; hover shows a definition from src/lib/glossary.ts. |
Disclosure | title, code?, tech? | Collapsible section. code="pass#region" scrolls/highlights the source; tech="sdf" adds a “→ technical explainer” link. |
Pass | name, subtitle? | Section header that switches the code panel’s active tab. |
The glossary — terms, GLSL built-ins, types, and per-uniform value formatting — lives once in src/lib/glossary.ts. Add an entry there and any <Term> or hovered token can reference it.
Techniques
Recurring moves that span several shaders — rotation, SDFs, raymarching, gyroids, feedback buffers, the Laplacian kernel, symmetry folds — are written once in src/lib/techniques.ts. A tech="..." prop on a <Lens> or <Disclosure> opens that entry as a slide-over, and the whole set is browsable at /techniques. To add one: drop an entry in techniques.ts (title, tagline, body, a snippet, and the shaders it appears in) and reference its key from any prose.
Porting a Shadertoy
Bringing an existing Shadertoy in mostly works as-is, but the engine’s auto-injection means a few things have to be removed or adjusted:
- Drop any
#define Rthe original declares — the engine already injects#define R (iResolution.xy), and a second one is a redefinition error. - Delete the local declaration of any constant you promote to a uniform. The engine injects
#define name u_nameper uniform, which would rewrite a localfloat name = …;into a redeclaration. - Feedback buffers wrap with
REPEAT, matching Shadertoy’s default — needed by shaders that sample slightly outside[0,1]. - There is no keyboard input. The
iChannel-as-keyboard trick is unsupported; stub any key reads to a constant. - Always set
source:to the original so the credit link shows.
The /new authoring tool bakes these conventions into its template, so a fresh package starts out compliant.