Angelic grunge

Loved this frosty, corroded look where the letters are eaten by texture and the edges melt off into a soft mist. So I built it from the ground up.

Real grunge scans (a cracked frost and a spatter of blooms) carve holes and cracks into the word, and a soft frosty halo blooms off the edges into the paper. The whole thing sits in a cold duotone. The frost slowly creeps on its own, and moving over it clears the frost so a cleaner letter shows through. Type your own word and pick the two colors below.

Playground

Word
Grunge
Duotone

Code

// Angelic Grunge presets + Remix. Each preset is a duotone ink on a suited paper with its
// own grunge amount / frost bloom. Remix keeps the frost-erosion mechanic but rolls a
// fresh cold-or-warm duotone on a matching sheet.

import { GRUNGE_DEFAULTS, type GrungeParams } from "./engine";

export type GrungePreset = { id: string; name: string; params: Partial<GrungeParams> };

export const GRUNGE_PRESETS: GrungePreset[] = [
  {
    id: "angelic",
    name: "Angelic",
    // the source look: cold blue-teal frost on cool white (the default)
    params: { ink: [0.09, 0.16, 0.22], paper: [0.99, 0.99, 1.0], grunge: 0.55, bloom: 0.5, grain: 0.06 },
  },
  {
    id: "rust",
    name: "Rust",
    // corroded warm rust-brown on aged paper
    params: { ink: [0.28, 0.13, 0.06], paper: [0.96, 0.93, 0.86], grunge: 0.62, bloom: 0.45, grain: 0.08 },
  },
  {
    id: "mono",
    name: "Mono",
    // pure black corrosion on white
    params: { ink: [0.06, 0.06, 0.07], paper: [1.0, 1.0, 1.0], grunge: 0.6, bloom: 0.4, grain: 0.05 },
  },
  {
    id: "toxic",
    name: "Toxic",
    // acid green corrosion on a dark charcoal sheet
    params: { ink: [0.5, 0.85, 0.2], paper: [0.08, 0.09, 0.08], grunge: 0.58, bloom: 0.55, grain: 0.07 },
  },
  {
    id: "ember",
    name: "Ember",
    // hot orange-red burn on near-black
    params: { ink: [0.95, 0.42, 0.14], paper: [0.07, 0.06, 0.07], grunge: 0.64, bloom: 0.6, grain: 0.08 },
  },
  {
    id: "violet",
    name: "Violet",
    // deep violet frost on pale lilac
    params: { ink: [0.24, 0.12, 0.36], paper: [0.96, 0.95, 0.98], grunge: 0.52, bloom: 0.5, grain: 0.06 },
  },
];

export function defaultGrungeParams(): GrungeParams {
  return { ...GRUNGE_DEFAULTS, ...GRUNGE_PRESETS[0].params };
}

// rgb 0..1 <-> #hex for the ColorControl (which speaks hex)
export const toHex = (c: [number, number, number]) =>
  "#" + c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("");
export const fromHex = (h: string): [number, number, number] => {
  const n = h.replace("#", "");
  return [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2) || "0", 16) / 255) as [number, number, number];
};

// a saturated-but-dark ink hue (reads as corroded ink, not a pastel)
function inkHue(hue: number): [number, number, number] {
  const c = 1;
  const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
  const seg = [
    [c, x, 0], [x, c, 0], [0, c, x], [0, x, c], [x, 0, c], [c, 0, x],
  ][Math.floor(hue / 60) % 6];
  return [seg[0] * 0.4 + 0.06, seg[1] * 0.4 + 0.06, seg[2] * 0.4 + 0.06];
}

// Remix: a fresh duotone. ~1 in 3 rolls a DARK sheet (light ink on dark), else a light
// paper (dark ink on light), with varied grunge + bloom.
export function remixGrungeParams(): Partial<GrungeParams> {
  const h = Math.random() * 360;
  const darkSheet = Math.random() < 0.34;
  let ink: [number, number, number];
  let paper: [number, number, number];
  if (darkSheet) {
    // a vivid ink on a near-black sheet (glows out of the dark)
    const c = 1;
    const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
    const seg = [[c, x, 0], [x, c, 0], [0, c, x], [0, x, c], [x, 0, c], [c, 0, x]][Math.floor(h / 60) % 6];
    ink = [seg[0] * 0.8 + 0.15, seg[1] * 0.8 + 0.15, seg[2] * 0.8 + 0.15];
    paper = [0.07, 0.07, 0.08];
  } else {
    ink = inkHue(h);
    // a light sheet faintly tinted toward the ink hue so they feel paired
    const m = Math.max(ink[0], ink[1], ink[2], 0.001);
    paper = [0.96 + (ink[0] / m) * 0.03, 0.96 + (ink[1] / m) * 0.03, 0.96 + (ink[2] / m) * 0.03];
  }
  return {
    ink,
    paper,
    grunge: 0.45 + Math.random() * 0.3,
    bloom: 0.35 + Math.random() * 0.35,
    grain: 0.04 + Math.random() * 0.06,
  };
}

Credits

CompanyStudy
DateJul 14, 2026
TagsWebGL, Grunge, Texture

MIT → free to copy