A full-screen WebGL shader that renders an animated heart shape entirely on the GPU. The heart boundary is computed using a signed distance function (SDF) written in GLSL. Colors cycle continuously through a cosine-palette, producing smooth gradient shifts driven byDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/hetari/creative-coding/llms.txt
Use this file to discover all available pages before exploring further.
uTime. Five real-time controls — speed, glow intensity, and three palette color stops — are wired to lil-gui sliders and color pickers, so every parameter change is reflected in the next draw frame without recompiling the shader.
Registry Entry
This sketch is registered insrc/config/sketches.ts as:
Tech Stack
| Layer | Tool |
|---|---|
| Canvas / draw loop | useP5 composable, p5.js WEBGL mode |
| Shaders | GLSL vertex + fragment, loaded with Vite ?raw |
| GUI controls | LilGui component |
| Performance overlay | StatsPanel component |
| External links | ResourcesList component |
GLSL Uniforms
The Vue component sets the following uniforms every frame insidep.draw():
| Uniform | Type | Description |
|---|---|---|
uTime | float | Animation time — p5.millis() / 2000, advancing roughly 0.5 units per second |
uResolution | vec2 | Canvas width and height in physical pixels (matches p.width / p.height) |
PI | float | Math.PI — passed as a uniform so the fragment shader doesn’t need to redeclare it |
uSpeed | float | Animation speed multiplier. GUI range: 0.1 – 5.0, default 1.0 |
uGlow | float | Glow intensity divisor applied to the detail layer. GUI range: 0.01 – 1.0, default 0.25 |
uPaletteA | vec3 | First cosine-palette coefficient. Color picker default: #803333 (≈ 0.5, 0.2, 0.2) |
uPaletteB | vec3 | Second cosine-palette coefficient. Color picker default: #804D4D (≈ 0.5, 0.3, 0.3) |
uPaletteD | vec3 | Fourth (phase-shift) cosine-palette coefficient. Color picker default: #001A33 (≈ 0.0, 0.1, 0.2) |
Fragment Shader Walkthrough
The fragment shader is the core of this sketch. Here is the full source exactly as it lives inshader.frag:
heart(vec2 p) — Heart SDF
The function computes the signed distance from the point p to a heart-shaped curve:
- Vertical offset —
p.y -= 0.25shifts the heart upward so it is centred on screen. - Polar conversion —
atan(p.x, p.y)andlength(p)convert the Cartesian point to polar coordinates(a, r). - Polynomial radial profile — The heart’s radius at angle
ais approximated by a cubic rational:(13h − 22h² + 10h³) / (6 − 5h)whereh = |a / π|. This polynomial closely traces the classic heart curve. - SDF value —
r - d * 0.5returns a positive value outside the heart, negative inside, and zero exactly on the boundary.
palette(float t) — Cosine Color Palette
The palette function maps a scalar t to a colour using Inigo Quilez’s cosine-palette formula:
c = vec3(1, 1, 1) so all three channels oscillate at the same frequency. Changing uPaletteA, uPaletteB, or uPaletteD through the GUI shifts the midpoint, amplitude, or phase of the colour cycle in real time.
main() — Two-Layer Composition
The main function runs in two passes on the same UV space:
- Coordinate normalisation —
(gl_FragCoord.xy * 2.0 - uResolution.xy) / uResolution.ymaps pixel coordinates to the range −0.5 … 0.5, preserving square pixels on any aspect ratio. - Background colour — The palette is sampled at
heart(uv) + uTime * uSpeed. Because the heart SDF varies continuously across the screen, each pixel gets a different phase input, producing a colour gradient that ripples outward from the heart boundary over time. - Detail layer —
fract(uv * 2.0) − 0.5tiles the coordinate space 2×, so a second heart appears at half-size. The glow effect is computed by applying a sine wave to the SDF value, taking its absolute value, then dividinguGlowby that value — creating bright spikes at every zero crossing. - Final output —
color * dmultiplies the background colour by the glow brightness, then writes the result tofragColorwith full opacity.
Vue Component Structure
The Vue<script setup> wires reactive state to shader uniforms every frame:
p.draw(), every uniform is forwarded to the shader before the fullscreen rectangle is drawn:
GUI Controls
The controls array passed to<LilGui>:
Speed
Multiplies
uTime before it reaches the palette function. Higher values cause the colour gradient to sweep across the heart faster.Glow
Sets the numerator of the
uGlow / d expression. Larger values produce a wider, brighter halo around every heart contour in the detail layer.Palette A / B / D
Three
vec3 colour stops fed directly into the cosine-palette formula. Drag the colour pickers to shift the hue, saturation, and phase of the animation cycle.Vertex Shader
The vertex shader simply maps p5.js
aPosition attribute coordinates into clip space with position.xy = position.xy * 2.0 - 1.0, filling the entire viewport.