fragsplainer
Documentation

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.

Tip. The widgets are the point. Almost everything you can read about, you can also move — grab a slider mid-sentence and watch both the render and the code respond.

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:

StoreHolds
configthe active shader’s uniforms + pass graph
uniformslive uniform values (persisted per-shader to localStorage)
passeswhich code tab is shown
focusthe { pass, region } the code panel scrolls to
tokenssource tokens a Lens is highlighting
popoverthe shared glossary tooltip
techniquewhich 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.

Authoring

A new explainer is a single folder dropped under src/content/shaders/. The build picks it up automatically and a gallery card appears.

src/content/shaders/my-shader/
  index.mdx        # frontmatter (config) + prose
  image.glsl       # the main render pass (mainImage)
  common.glsl      # optional — shared helpers, prepended to every pass
  bufferA.glsl     # optional — multipass feedback buffers
  bufferB.glsl

Prefer to scaffold it in the browser? The /new authoring tool gives you live-updating fields and exports these exact files for you to drop into the repo.

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>.
ComponentPropsDoes
SlideruInline 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.
ToggleuSwitch bound to a bool uniform; shows the label.
Lenssym, 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.
TermidGlossary word; hover shows a definition from src/lib/glossary.ts.
Disclosuretitle, code?, tech?Collapsible section. code="pass#region" scrolls/highlights the source; tech="sdf" adds a “→ technical explainer” link.
Passname, 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 R the 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_name per uniform, which would rewrite a local float 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.