How We Built the Shimmer Transition

A WebGL iridescent sweep — built from scratch using Gaussian bands, cosine palettes, and Blinn-Phong lighting. No libraries. Zero CSS animation. A GLSL fragment shader runs on the GPU and draws every pixel of the sweep from pure math.

🕵️ First: How did we know the colours?

The website glimm.dev ships a public npm package called glimm. NPM is like an App Store for code that developers share publicly. We downloaded it with two commands:

npm pack glimm@latest    # downloads the package as a .tgz zip
tar xzf glimm-0.1.3.tgz # unzips it

Inside that zip was compiled JavaScript with the colour numbers written right there:

prism: { a:[0.46,0.88,0.33], b:[0.6,0.58,0.74], c:[0.5,0.5,0.5], d:[0.54,0.22,0.84] }

We didn't guess — we read the published package. 100% legal. The "secret sauce" was publicly downloadable all along. 🙂

🎨 The big idea — what IS the shimmer?

Imagine you have a magic highlighter pen. When you drag it across paper, it leaves a glowing rainbow stripe that shimmers and changes colour as it moves — like a holographic sticker. That's the shimmer transition. When you click a button or link, that rainbow stripe slides across the screen, hides the old page, shows the new page underneath, then fades away.

No CSS. No animation library. Just math drawing pixels one by one using WebGL.

🖼️ Block 1 — The Canvas Overlay

Imagine your webpage is a painting on a wall. Now imagine someone tapes a sheet of clear glass in front of it. You can still see the painting through the glass and click links on the page — but someone can draw on the glass without touching the painting. That's exactly what <canvas> is.

┌─────────────────────────────┐  ← Browser window
│   ┌─────────────────────┐   │  ← Your webpage (HTML, text, buttons)
│   │   Hello World       │   │
│   │   [Click me]        │   │
│   └─────────────────────┘   │
│ ┌───────────────────────┐   │  ← <canvas> (glass sheet ON TOP)
│ │  transparent / empty  │   │    position: fixed, inset: 0
│ │  (you see through it) │   │    pointer-events: none
│ └───────────────────────┘   │    z-index: 999
└─────────────────────────────┘
SettingWhy
position: fixed + inset: 0Glass fills the entire window
pointer-events: noneYour clicks fall through to the page behind
z-index: 999Sits above all other page elements
alpha: true + premultipliedAlpha: trueGlass is truly transparent where nothing is drawn

📊 Block 2 — The Gaussian Band (the shape of the stripe)

The shimmer band is NOT a rectangle — it has soft edges that fade out smoothly. The shape comes from the Gaussian bell curve — the same curve from maths/statistics class.

Brightness
   1.0 │           ████
       │         ██████████
   0.5 │       ██████████████
       │     ████████████████████
   0.0 │──██████████████████████████──→ Position across screen
              ↑                  ↑
           left edge          right edge
           (fades out)        (fades out)
                └── bright center ──┘
band = exp( −distance² × tightness )
TermWhat it means
distance (d)How far this pixel is from the centre line of the band. Centre = 0, far away = 0.5, 1.0 etc.
Squaring makes the curve symmetric and the falloff happen faster
tightness (14)Higher = narrower band (laser 🔦). Lower = wider band (floodlight)
exp(−…)exp(0)=1.0 fully bright · exp(-5)=0.007 nearly invisible · exp(-14)=0.000001 black
Real life analogy: Shine a torch at a wall in a dark room. Centre is bright white, edges fade gradually — no sharp border. That gradual fade is a Gaussian. 🔦

➡️ Block 3 — Moving the Band (the animation)

The band has a position — where its centre currently is on screen. Screen goes from 0.0 (left edge) to 1.0 (right edge). The band starts at -0.2 (just off the left, invisible) and ends at 1.2 (just off the right, invisible).

Time:  0ms              550ms             1100ms
       │                  │                  │
  -0.2  0.0       0.5        1.0   1.2
   ├────┼──────────┼──────────┼─────┤
   ●                                    ← band starts here (invisible)
              ●                         ← band at midpoint (covers screen)
                                   ●    ← band ends here (invisible)
pos = −0.2 + progress × 1.4
// progress = 0.0 → pos = −0.2  (off left edge)
// progress = 0.5 → pos =  0.5  (dead centre of screen)
// progress = 1.0 → pos =  1.2  (off right edge)
requestAnimationFrame is the browser saying "hey, time to draw the next frame!" It fires ~60 times per second. Think of it as a movie projector — each frame the band moved a tiny bit right. Played at 60fps your eye sees smooth movement. 🎬

〰️ Block 4 — The Wavy Edge

A band with a perfectly straight edge looks like PowerPoint. The real shimmer band has an organic edge — achieved by shifting the band's centre left/right differently at each row using 3 sine waves added together:

wiggle = sin(y ×  6  +  time × 1.3) × 0.020   // wave A: slow + big
       + sin(y × 13  −  time × 0.9) × 0.012   // wave B: medium speed
       + sin(y × 21  +  time × 1.7) × 0.006   // wave C: fast + tiny
Wave A alone:    ∿∿∿∿∿∿∿∿∿∿∿∿   (smooth and regular — boring)
Wave B alone:    ∿∿∿∿∿∿∿∿∿∿∿∿   (regular, faster — still boring)
Wave C alone:    ∿∿∿∿∿∿∿∿∿∿∿∿   (regular, fastest — still boring)
                 ─────────────
All three added: ∿~∿~~∿∿~∿~~∿   (irregular, natural! 🌊)

Each wave also has time in it — so the wiggle changes every frame, making the edge feel animated, not frozen.

Ocean analogy: Small ripples + medium swells + big waves all at once. None of them sync up → the water looks endlessly varied. 🌊

🌈 Block 5 — The Rainbow Colours (the most magical part)

This formula was invented by graphics legend Inigo Quilez. It's pure math, freely shared, and it's the heart of the iridescent shimmer.

colour(t) = a + b × cos( 2π × (c × t + d) )
LetterNameWhat it controlsAnalogy
aOffsetShifts the whole wave up or downAdjusting brightness in a room
bAmplitudeHow wide the oscillation swingsHow saturated / vibrant the colours are
cFrequencyHow many rainbow cycles across t=0→1How fast colours change across the band
dPhaseSlides the wave left/rightWhich colour you start from

Each of R, G, B gets its own a, b, c, d values. 14 presets — 6 original + 8 from Figma:

── Original ────────────────────────────────────────────────────────────────
prism:   a:[0.46,0.88,0.33] b:[0.60,0.58,0.74] c:[0.5,0.5,0.5] d:[0.54,0.22,0.84]
berry:   a:[0.92,0.36,0.56] b:[0.10,0.14,0.37] c:[0.5,0.5,0.5] d:[0.84,0.11,0.50]
lagoon:  a:[0.58,0.64,0.52] b:[0.64,0.30,0.50] c:[0.5,0.5,0.5] d:[0.37,0.66,0.89]
citrus:  a:[0.55,0.61,0.61] b:[0.54,0.41,0.63] c:[0.5,0.5,0.5] d:[0.66,0.92,0.23]
azure:   a:[0.13,0.61,1.00] b:[0.15,0.14,0.00] c:[0.5,0.5,0.5] d:[0.29,0.98,0.74]
ember:   a:[0.83,0.61,0.60] b:[0.20,0.41,0.62] c:[0.5,0.5,0.5] d:[0.73,0.05,0.36]
── From Figma swatches ─────────────────────────────────────────────────────
crimson: a:[0.50,0.05,0.05] b:[0.40,0.05,0.05] c:[0.5,0.5,0.5] d:[0.00,0.50,0.50]
galaxy:  a:[0.50,0.10,0.50] b:[0.45,0.10,0.45] c:[0.5,0.5,0.5] d:[0.20,0.50,0.80]
steel:   a:[0.60,0.64,0.70] b:[0.32,0.33,0.27] c:[0.5,0.5,0.5] d:[0.00,0.00,0.00]
fuchsia: a:[0.58,0.05,0.48] b:[0.35,0.05,0.35] c:[0.5,0.5,0.5] d:[0.00,0.50,0.50]
teal:    a:[0.22,0.42,0.38] b:[0.15,0.35,0.30] c:[0.5,0.5,0.5] d:[0.50,0.00,0.20]
cosmic:  a:[0.47,0.29,0.50] b:[0.47,0.27,0.41] c:[0.5,0.5,0.5] d:[0.50,0.00,0.30]
sunrise: a:[0.57,0.55,0.23] b:[0.41,0.30,0.22] c:[0.5,0.5,0.5] d:[0.00,0.00,0.50]
inferno: a:[0.50,0.41,0.31] b:[0.49,0.38,0.18] c:[0.5,0.5,0.5] d:[0.00,0.00,0.00]

🔑 The Iridescence Trick — WHY the hue shifts: The t that drives the hue is computed from the surface normal tilt. The band is treated as a physical hill:

Height
  │      ╭────╮           ← top of hill (normal points UP → base colour)
  │   ╭──╯    ╰──╮
  │───╯            ╰───→  position
       ↑          ↑
    left slope  right slope
    tilts LEFT  tilts RIGHT
    → hue shift one way  → hue shift other way
This is iridescence — the same reason a holographic sticker changes colour as you tilt it. The geometry changes → the reflection changes → different colours reach your eye. 🌈

✨ Block 6 — The Shiny Highlight (Blinn-Phong)

Two physics effects make the band look like real physical foil, not flat colour. Both are additive — they add brightness on top of colour, not replace it.

Fresnel Effect (rim glow): Hold a glass window — looking straight at it, it's mostly transparent. Looking from a sharp angle, it glows bright. That's Fresnel. The band's edges tilt away the most → edges glow brightest → luminous rim.

float NdotV   = dot(Normal, ViewDirection)
float fresnel = pow(1.0 − NdotV, 3.0)
// NdotV=1.0 (facing you): (1-1)³ = 0  → no glow
// NdotV=0.0 (sideways):   (1-0)³ = 1  → full glow

Specular Highlight (catch-light): Look at a shiny metal ball — there's always one tiny bright spot where light bounces directly into your eye. Exponent 80 keeps it razor sharp — only the exact crest of the band lights up.

vec3  H     = normalize(LightDir + ViewDir)  // halfway vector
float NdotH = dot(Normal, H)
float spec  = pow(NdotH, 80.0)
// 0.9^80 = 0.00023 → nearly zero everywhere except the exact crest

⏱️ The Full Animation Timeline

t = 0ms
│  Band appears (alpha = 1, position = off left edge)
│
│  ← Band sweeps LEFT → RIGHT (1100ms, easeOutQuart)
│
t = 550ms  ← MIDPOINT
│  Page content swaps HERE
│  (nobody sees it — the band covers the entire screen!)
│
t = 1100ms
│  Band finishes crossing (position = off right edge)
│  Outro begins: alpha fades 1 → 0 (700ms, easeInOutCubic)
│
t = 1800ms
   Canvas is fully transparent. Done!
Why swap at the midpoint? At 550ms the band covers 100% of the screen — the old page is hidden. The new page can load underneath. Nobody sees the swap — it happens behind the curtain. 🎭

🔢 Easing Functions — Why Movement Feels Natural

Without easing, the band moves at constant speed — like a robot. Easing functions remap time so movement feels physical:

Linear (no easing):
Progress │████████████████  ← constant speed, robotic
    1.0  │
    0.0  └─────────────────→ time

easeOutQuart (band sweep):
Progress │          ██████  ← slows down at end (like a car braking)
    1.0  │      ████
    0.0  └─────────────────→ time

easeInOutCubic (fade out):
Progress │     ████████     ← slow start, fast middle, slow end (like a breath)
    1.0  │   ██        ██
    0.0  └─────────────────→ time
easeOutQuart   = p => 1 − (1 − p)⁴        // fast start, gentle stop
easeInOutCubic = p => p < 0.5             // symmetric S-curve
               ? 4p³
               : 1 − (−2p + 2)³ / 2
Without easing → looks like a PowerPoint slide 😬 · With easing → feels like iOS, feels physical 😍

📦 The 4 Files — What Each Does

shimmer-transition/
├── index.html        ← canvas overlay + page layout + styles
└── src/
    ├── palettes.js   ← 14 colour recipes (just data, no logic)
    ├── sweep.js      ← animation timer: phase 1 sweep + phase 2 fade
    ├── shader.js     ← WebGL setup + the GLSL fragment shader
    └── main.js       ← connects button click → sweep plays
FileDifficultyLinesWhat it does
palettes.js⭐ Easy4014 colour recipes (6 original + 8 from Figma) — just data, no logic
main.js⭐ Easy57Wires button click → palette cycle → playSweep
sweep.js⭐⭐ Medium96Animation timer: phase 1 sweep + phase 2 fade
shader.js⭐⭐⭐ Hard271WebGL setup + the full GLSL fragment shader

🤔 Why WebGL and Not Just CSS?

CSS animations can do: fades, slides, scales, rotations, blurs. CSS animations cannot do:

✗  Run a custom math formula per pixel (millions of calculations per frame)
✗  Synthesize a surface normal from a mathematical function
✗  Compute Fresnel or specular lighting
✗  Make iridescent colour shifts driven by geometry
For any effect that needs per-pixel custom maths, you need a GPU shader (WebGL or WebGPU). This shimmer effect is literally impossible in CSS. That's the whole reason WebGL exists.

🔑 Key Terms Glossary

TermPlain English
WebGLA way to run GPU code from your browser. Lets you draw anything mathematically.
GLSLThe programming language used to write GPU shaders. Runs on the graphics card.
Fragment shaderA small program that runs once for every single pixel, deciding its colour.
UniformA variable you send FROM JavaScript TO the shader (like uProgress, uTime).
GaussianThe bell curve shape: exp(−x²). Peaks at 1, fades smoothly to 0.
Surface normalAn arrow pointing straight out of a surface. Used for lighting calculations.
IridescenceColour that shifts as you change viewing angle. Like a holographic sticker.
FresnelMore reflection at grazing angles. Why glass looks reflective from the side.
Cosine paletteIQ's formula: a + b·cos(2π(ct+d)). Makes smooth, looping colour gradients.
easeOutQuart1−(1−p)⁴ — starts fast, decelerates. Feels like natural motion.
requestAnimationFrameBrowser calls your draw function ~60×/sec. The animation heartbeat.
premultiplied alphaA way of storing colour+transparency together that blends correctly in WebGL.

🎨 How to Add Your Own Custom Colour Palette

You only need to touch one filepalettes.js. The dots, the cycling, and the shader all pick it up automatically. Here's a worked example building a "mint" palette — soft white → deep teal.

Step 1 — Pick two "pole" colours

With c = [0.5, 0.5, 0.5] (the standard value), the palette oscillates between two poles: one at t = 0 and one at t = 0.5. Pick the two colours you want at those two ends:

Pole A (t = 0)   → soft white-green:  R=0.90  G=1.00  B=0.90
Pole B (t = 0.5) → deep teal:         R=0.10  G=0.50  B=0.50

Step 2 — Compute a and b

Two formulas, one per channel. Do this for R, G, and B separately:

a  =  (Pole A  +  Pole B) / 2     ← the midpoint between the two colours
b  =  (Pole A  −  Pole B) / 2     ← half the distance (the swing size)

── R channel ─────────────────────────────────────────────
a_r  =  (0.90 + 0.10) / 2  =  0.50
b_r  =  (0.90 − 0.10) / 2  =  0.40

── G channel ─────────────────────────────────────────────
a_g  =  (1.00 + 0.50) / 2  =  0.75
b_g  =  (1.00 − 0.50) / 2  =  0.25

── B channel ─────────────────────────────────────────────
a_b  =  (0.90 + 0.50) / 2  =  0.70
b_b  =  (0.90 − 0.50) / 2  =  0.20

Step 3 — Choose c and d

c = [0.5, 0.5, 0.5]   ← always keep this — gives one smooth cycle across the band
d = [0.0, 0.0, 0.0]   ← start from Pole A at t=0. Nudge any channel to shift the hue.

Step 4 — Verify mentally

At t = 0.00 → a + b·cos(0)  = a + b = [0.90, 1.00, 0.90]  ✓ soft white-green
At t = 0.25 → a + b·cos(π/2) = a     = [0.50, 0.75, 0.70]  ✓ mid mint
At t = 0.50 → a + b·cos(π)  = a − b = [0.10, 0.50, 0.50]  ✓ deep teal
At t = 1.00 → a + b·cos(2π) = a + b = [0.90, 1.00, 0.90]  ✓ loops back

Step 5 — Add one line to palettes.js

export const PALETTES = {
  prism:   { a:[0.46,0.88,0.33], b:[0.60,0.58,0.74], c:[0.5,0.5,0.5], d:[0.54,0.22,0.84] },
  // ... other palettes ...

  // 👇 Add your new palette here — pick any name
  mint:    { a:[0.50,0.75,0.70], b:[0.40,0.25,0.20], c:[0.5,0.5,0.5], d:[0.00,0.00,0.00] },
}
That's it. No changes needed anywhere else. The dot gets generated automatically with the correct gradient (computed from the same formula), the name appears in the header, and it joins the cycle.

Tweaking tips:

Want richer, more saturated colours?  → increase b values (try 0.5–0.6)
Want softer, pastel tones?            → decrease b values (try 0.1–0.2)
Want to shift the starting hue?       → nudge d values (0.25 = quarter turn, 0.5 = flip)
Want faster colour cycling?           → increase c values (try 1.0 for two full cycles)
Want a monochrome (single colour)?    → set b_r=b_g=b_b to the same value, same for d
 1<!-- Glass overlay — sits above the page -->
 2<canvas id="shimmer-canvas"></canvas>
 3
 4<div class="page">
 5  <div class="hero">
 6    <h1>Shimmer Transition</h1>
 7    <p>WebGL iridescent sweep — no libraries.</p>
 8  </div>
 9
10  <button id="play-btn">Play Transition</button>
11</div>
12
13<!-- ES module — imports shader.js, sweep.js, palettes.js -->
14<script type="module" src="src/main.js"></script>
 1import { createShader } from './shader.js'
 2import { playSweep }    from './sweep.js'
 3import { PALETTES }     from './palettes.js'
 4
 5const canvas = document.getElementById('shimmer-canvas')
 6const btn    = document.getElementById('play-btn')
 7const ctrl   = createShader(canvas, { palette: 'prism' })
 8
 9const paletteNames = Object.keys(PALETTES)
10let   paletteIndex = 0
11let   activeSweep  = null
12
13btn.addEventListener('click', () => {
14  if (!ctrl) return
15
16  // Cancel any in-progress sweep so a double-tap restarts
17  activeSweep?.cancel()
18
19  // Cycle through palettes on each tap
20  const name = paletteNames[paletteIndex++ % paletteNames.length]
21  ctrl.setPalette(name)
22  btn.textContent = `✦ ${name}`
23  btn.classList.add('active')
24
25  activeSweep = playSweep(ctrl, {
26    onMidpoint: () => { /* swap page content here */ },
27    onComplete: () => {
28      btn.textContent = 'Play Transition'
29      btn.classList.remove('active')
30      activeSweep = null
31    },
32  })
33})
 1// Fragment shader — this runs on the GPU for every pixel
 2precision mediump float;
 3
 4uniform vec2  uRes;       // canvas size in pixels
 5uniform float uTime;      // elapsed seconds
 6uniform float uProgress;  // band travel 0 → 1
 7uniform float uAlpha;     // outro fade
 8uniform vec3  uPalA, uPalB, uPalC, uPalD; // palette
 9
10vec3 pal(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
11  return a + b * cos(2.0 * PI * (c * t + d)); // IQ palette
12}
13
14void main() {
15  vec2 uv = gl_FragCoord.xy / uRes;
16
17  // Band position (-0.2 → 1.2 across the screen)
18  float pos = -0.2 + uProgress * 1.4;
19
20  // Wavy edge — 3 sine harmonics superimposed
21  float wave = sin(uv.y * 6.0  + uTime * 1.30) * 0.020
22             + sin(uv.y * 13.0 - uTime * 0.90) * 0.012
23             + sin(uv.y * 21.0 + uTime * 1.70) * 0.006;
24
25  // Gaussian band — smooth bell curve falloff from centre
26  float d    = (uv.x - pos) - wave;
27  float band = exp(-d * d * uBandTight);
28
29  // Surface normal — analytic slope of the Gaussian hill
30  float slope = -2.0 * d * uBandTight * band;
31  vec3  N     = normalize(vec3(-slope * 0.18, 0.0, 1.0));
32
33  // Iridescence — hue driven by normal tilt + position + time
34  float t = N.x * 0.45 + uv.x * 1.40 + uTime * 0.04;
35  vec3  col = pal(t, uPalA, uPalB, uPalC, uPalD);
36
37  // Blinn-Phong — Fresnel rim + specular catch-light (additive)
38  float fresnel = pow(1.0 - dot(N, V), 3.0);
39  float spec    = pow(dot(normalize(L + V), N), 80.0);
40
41  float a = band * uAlpha;
42  gl_FragColor = vec4(col * a + (col * fresnel * 0.55
43                   + vec3(spec) * 1.1) * a,
44                   min(a + (fresnel * 0.4 + spec * 0.9) * a, 1.0));
45}
 1// Animation orchestrator — two phases
 2// Phase 1: sweep band left→right (1100ms, easeOutQuart)
 3// Phase 2: fade alpha 1→0 (700ms, easeInOutCubic)
 4
 5const easeOutQuart   = p => 1 - Math.pow(1 - p, 4)
 6const easeInOutCubic = p => p < 0.5
 7  ? 4 * p * p * p
 8  : 1 - Math.pow(-2 * p + 2, 3) / 2
 9
10export function playSweep(ctrl, opts = {}) {
11  const sweepMs  = opts.sweepMs  ?? 1100
12  const outroMs  = opts.outroMs  ?? 700
13  const midpoint = opts.midpoint ?? 0.5
14  let cancelled = false
15
16  ;(async () => {
17    ctrl.setAlpha(1)
18    ctrl.setProgress(0)
19
20    // Phase 1 — sweep
21    let midFired = false
22    const t0 = performance.now()
23    await new Promise(resolve => {
24      const tick = () => {
25        if (cancelled) { resolve(); return }
26        const raw      = Math.min(1, (performance.now() - t0) / sweepMs)
27        const progress = easeOutQuart(raw)
28        ctrl.setProgress(progress)
29        if (!midFired && progress >= midpoint) {
30          midFired = true
31          opts.onMidpoint?.()   // ← swap page content here
32        }
33        if (raw < 1) requestAnimationFrame(tick)
34        else resolve()
35      }
36      requestAnimationFrame(tick)
37    })
38
39    // Phase 2 — fade out
40    const t1 = performance.now()
41    await new Promise(resolve => {
42      const tick = () => {
43        if (cancelled) { resolve(); return }
44        const raw = Math.min(1, (performance.now() - t1) / outroMs)
45        ctrl.setAlpha(1 - easeInOutCubic(raw))
46        if (raw < 1) requestAnimationFrame(tick)
47        else resolve()
48      }
49      requestAnimationFrame(tick)
50    })
51
52    opts.onComplete?.()
53  })()
54
55  return { cancel: () => { cancelled = true } }
56}
 1// Cosine palette formula by Inigo Quilez — public domain
 2// color(t) = a + b * cos( 2π * (c*t + d) )
 3// Original 6 from glimm npm · New 8 derived from Figma swatches
 4
 5export const PALETTES = {
 6  // ── Original 6 ──────────────────────────────────────────────────────────
 7  prism:   { a:[0.46,0.88,0.33], b:[0.60,0.58,0.74], c:[0.5,0.5,0.5], d:[0.54,0.22,0.84] },
 8  berry:   { a:[0.92,0.36,0.56], b:[0.10,0.14,0.37], c:[0.5,0.5,0.5], d:[0.84,0.11,0.50] },
 9  lagoon:  { a:[0.58,0.64,0.52], b:[0.64,0.30,0.50], c:[0.5,0.5,0.5], d:[0.37,0.66,0.89] },
10  citrus:  { a:[0.55,0.61,0.61], b:[0.54,0.41,0.63], c:[0.5,0.5,0.5], d:[0.66,0.92,0.23] },
11  azure:   { a:[0.13,0.61,1.00], b:[0.15,0.14,0.00], c:[0.5,0.5,0.5], d:[0.29,0.98,0.74] },
12  ember:   { a:[0.83,0.61,0.60], b:[0.20,0.41,0.62], c:[0.5,0.5,0.5], d:[0.73,0.05,0.36] },
13  // ── From Figma swatches ──────────────────────────────────────────────────
14  crimson: { a:[0.50,0.05,0.05], b:[0.40,0.05,0.05], c:[0.5,0.5,0.5], d:[0.00,0.50,0.50] },
15  galaxy:  { a:[0.50,0.10,0.50], b:[0.45,0.10,0.45], c:[0.5,0.5,0.5], d:[0.20,0.50,0.80] },
16  steel:   { a:[0.60,0.64,0.70], b:[0.32,0.33,0.27], c:[0.5,0.5,0.5], d:[0.00,0.00,0.00] },
17  fuchsia: { a:[0.58,0.05,0.48], b:[0.35,0.05,0.35], c:[0.5,0.5,0.5], d:[0.00,0.50,0.50] },
18  teal:    { a:[0.22,0.42,0.38], b:[0.15,0.35,0.30], c:[0.5,0.5,0.5], d:[0.50,0.00,0.20] },
19  cosmic:  { a:[0.47,0.29,0.50], b:[0.47,0.27,0.41], c:[0.5,0.5,0.5], d:[0.50,0.00,0.30] },
20  sunrise: { a:[0.57,0.55,0.23], b:[0.41,0.30,0.22], c:[0.5,0.5,0.5], d:[0.00,0.00,0.50] },
21  inferno: { a:[0.50,0.41,0.31], b:[0.49,0.38,0.18], c:[0.5,0.5,0.5], d:[0.00,0.00,0.00] },
22}
23
14export function resolvePalette(p) {
15  if (!p) return PALETTES.prism
16  if (typeof p === 'string') return PALETTES[p] ?? PALETTES.prism
17  return p
18}
Preview ✦ prism