Colour ramp

Colour ramp

The data-driven choropleth ramp.

Colour ramp shader, written in TypeScript and compiled by TypeShade, rendered at build time.
// ═══ typeshade example — choropleth colour ramp ═══
//
// A cartographic shader: the data-driven colour ramp a thematic map uses to paint a value
// field. A reusable `ramp()` DSL function maps a normalised value [0,1] through a 5-stop
// sequential palette; the fragment samples an animated value field, colours it, and overlays
// anti-aliased contour isolines whose count is a live uniform.
import {
fn,
module,
vec3,
vec4,
sin,
fract,
smoothstep,
min,
clamp,
fwidth,
mix,
f32,
f32T,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ bands: f32T });
// 5-stop sequential ramp (light yellow → deep red), a classic choropleth legend.
const ramp = fn('ramp', { x: f32T }, ({ x }) => {
const c0 = vec3(0.99, 0.95, 0.74);
const c1 = vec3(0.99, 0.8, 0.45);
const c2 = vec3(0.96, 0.5, 0.24);
const c3 = vec3(0.84, 0.19, 0.15);
const c4 = vec3(0.5, 0.0, 0.05);
const a = mix(c0, c1, smoothstep(0, 0.25, x));
const b = mix(a, c2, smoothstep(0.25, 0.5, x));
const d = mix(b, c3, smoothstep(0.5, 0.75, x));
return mix(d, c4, smoothstep(0.75, 1, x));
});
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const uv = vo.uv;
const t = U.field.time;
const bands = U.field.bands;
// Animated data field in [0,1].
const val = clamp(
sin(uv.x.mul(6.2832).add(t.mul(0.5)))
.mul(0.3)
.add(0.5)
.add(sin(uv.y.mul(6.2832).sub(t.mul(0.35))).mul(0.2)),
0,
1,
);
const col = ramp({ x: val });
// Contour isolines at evenly spaced value levels, screen-constant width via fwidth.
const e = fract(val.mul(bands));
const dd = min(e, f32(1).sub(e));
const lineMask = f32(1).sub(smoothstep(0, fwidth(val.mul(bands)).mul(1.2), dd));
const enabled = smoothstep(0.5, 1.5, bands); // disables the overlay when bands ≈ 0
const contour = clamp(lineMask.mul(enabled), 0, 1);
return vec4(mix(col, col.mul(0.2), contour), 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
// `ramp` is called via its handle in `fs`, so module() collects it transitively — funcs lists only the entry points.
const colorRampModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const colorRamp: ShaderExample = {
id: 'color-ramp',
title: 'Colour ramp',
blurb:
'The data-driven choropleth ramp: a reusable ramp() function maps a value field through a 5-stop palette, with anti-aliased contour isolines. Set how many bands to draw.',
category: 'cartographic',
file: 'color-ramp.ts',
module: colorRampModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
bands: { kind: 'slider', label: 'Contour bands', min: 0, max: 12, step: 1, value: 6 },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
bands: f32,
}
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn ramp(x: f32) -> vec3<f32> {
return mix(mix(mix(mix(vec3<f32>(0.99, 0.95, 0.74), vec3<f32>(0.99, 0.8, 0.45), smoothstep(0.0, 0.25, x)), vec3<f32>(0.96, 0.5, 0.24), smoothstep(0.25, 0.5, x)), vec3<f32>(0.84, 0.19, 0.15), smoothstep(0.5, 0.75, x)), vec3<f32>(0.5, 0.0, 0.05), smoothstep(0.75, 1.0, x));
}
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VsOut {
let _cse0 = ((f32((vi & 1u)) * 4.0) - 1.0);
let _cse1 = ((f32((vi >> 1u)) * 4.0) - 1.0);
return VsOut(vec4<f32>(_cse0, _cse1, 0.0, 1.0), vec2<f32>(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));
}
@fragment
fn fs(vo: VsOut) -> @location(0) vec4<f32> {
let _cse3 = clamp((((sin(((vo.uv.x * 6.2832) + (U.time * 0.5))) * 0.3) + 0.5) + (sin(((vo.uv.y * 6.2832) - (U.time * 0.35))) * 0.2)), 0.0, 1.0);
let _cse0 = ramp(_cse3);
let _cse2 = (_cse3 * U.bands);
let _cse1 = fract(_cse2);
return vec4<f32>(mix(_cse0, (_cse0 * 0.2), clamp(((1.0 - smoothstep(0.0, (fwidth(_cse2) * 1.2), min(_cse1, (1.0 - _cse1)))) * smoothstep(0.5, 1.5, U.bands)), 0.0, 1.0)), 1.0);
}
#version 300 es
precision highp float;
precision highp int;
out vec2 uv;
void main() {
uint vi = uint(gl_VertexID);
float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0);
float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0);
gl_Position = vec4(_cse0, _cse1, 0.0, 1.0);
uv = vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5));
}
#version 300 es
precision highp float;
precision highp int;
layout(std140) uniform Uniforms {
float time;
vec2 resolution;
float bands;
} U;
vec3 ramp(float x) {
return mix(mix(mix(mix(vec3(0.99, 0.95, 0.74), vec3(0.99, 0.8, 0.45), smoothstep(0.0, 0.25, x)), vec3(0.96, 0.5, 0.24), smoothstep(0.25, 0.5, x)), vec3(0.84, 0.19, 0.15), smoothstep(0.5, 0.75, x)), vec3(0.5, 0.0, 0.05), smoothstep(0.75, 1.0, x));
}
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
float _cse3 = clamp((((sin(((uv.x * 6.2832) + (U.time * 0.5))) * 0.3) + 0.5) + (sin(((uv.y * 6.2832) - (U.time * 0.35))) * 0.2)), 0.0, 1.0);
vec3 _cse0 = ramp(_cse3);
float _cse2 = (_cse3 * U.bands);
float _cse1 = fract(_cse2);
_ret = vec4(mix(_cse0, (_cse0 * 0.2), clamp(((1.0 - smoothstep(0.0, (fwidth(_cse2) * 1.2), min(_cse1, (1.0 - _cse1)))) * smoothstep(0.5, 1.5, U.bands)), 0.0, 1.0)), 1.0);
}

Colour ramp, drawn at build time.

The WGSL and GLSL tabs are the compiler's own output at commit 26de7be8, read from the goldens its emit suite bakes (emit-goldens.test.ts).

This example is written against the fn() builder API, which the editor in the Playground does not take.

File on GitHub

Edit this page Report a problem