PL. 32 · COLOUR / GRADIENT / COSINE BASIS
Cosine Gradient
Formulated by Inigo Quilez
OPEN THE LIVE PLATE ▸DEFINITION
colour(t) = a + b · cos( 2π · (c·t + d) ) with a, b, c, d ∈ ℝ³
NOTES
Twelve numbers describe an entire gradient. Each channel is its own cosine, so shifting d rotates the hue relationship without touching the brightness envelope. It is far more compact than a stop list and, unlike one, it is trivially differentiable — which matters if you are feeding it into a shader.
PROVENANCE
- Origin
- Formulated and published openly by Inigo Quilez
- Standing
- A formula, freely published. Credit given as a matter of courtesy.
- Constants
- a = offset, b = amplitude, c = frequency, d = phase
TOUCHDESIGNER · GLSL
The same shader this plate runs, reframed for a GLSL TOP. Pasted bare it renders the published constants as a still frame; wire absTime.seconds into u_t on the Vectors page to animate it.
// FORMA — PL. 32 · COSINE GRADIENT — Formulated by Inigo Quilez
// colour(t) = a + b · cos( 2π · (c·t + d) )
// with a, b, c, d ∈ ℝ³
// TouchDesigner port — paste into a GLSL TOP's pixel shader. Set the
// resolution on the TOP's Common page. As pasted it renders the published
// constants as a still frame; to animate, add a uniform named u_t on the
// GLSL TOP's Vectors 1 page with the expression absTime.seconds.
// Constants are consts — edit to tweak; comments give the measured range.
// Written from the published mathematics, not adapted from any code.
#define u_res (uTDOutputInfo.res.zw)
uniform float u_t; // absTime.seconds on the Vectors page; unset = still
const float u_phase = 0.5831; // this plate's own grid phase, 0..1
// FORMA's COLOUR accent as cosine-gradient coefficients
const vec3 u_pal_a = vec3(0.46, 0.2002, 0.3896);
const vec3 u_pal_b = vec3(0.5, 0.2176, 0.4235);
const vec3 u_pal_c = vec3(1, 1, 1);
const vec3 u_pal_d = vec3(0, 0.05, 0.1);
const float p_d0 = 0.0; // phase — red · live 0 .. 1
const float p_d1 = 0.12; // phase — green · live 0 .. 1
const float p_d2 = 0.25; // phase — blue · live 0 .. 1
const float p_c = 1.0; // frequency · live 0.5 .. 3
const float p_amp = 0.42; // b — amplitude · live 0 .. 0.5
/* The order's ramp — the same cosine formulation the JS kit uses, so a
plate keeps its classification colour in either language. */
vec3 ramp(float t){
return clamp(u_pal_a + u_pal_b * cos(6.28318530718 * (u_pal_c * t + u_pal_d)), 0.0, 1.0);
}
/* Sawtooth and triangle on this plate's phase, mirroring the JS kit. */
float cycle(float t, float period){ return fract(t / period + u_phase); }
float pingpong(float t, float period){
float u = cycle(t, period);
return u < 0.5 ? u * 2.0 : 2.0 - u * 2.0;
}
/* The band is the palette itself. The three traces below it plot the same
per-channel cosine, found by distance to the curve rather than stroked —
a fragment shader has no pen, so the line is where the plot passes close. */
vec3 plate(vec2 uv){
vec3 a = vec3(0.5), b = vec3(p_amp), c = vec3(p_c);
float roll = pingpong(u_t, 21.0);
vec3 d = vec3(p_d0, p_d1, p_d2) + roll * 0.5;
const float bandH = 0.52;
if (uv.y < bandH) return clamp(a + b * cos(6.28318530718 * (c * uv.x + d)), 0.0, 1.0);
vec3 col = vec3(4.0, 6.0, 10.0) / 255.0;
vec3 chan[3] = vec3[3](vec3(1.000, 0.478, 0.431), // #FF7A6E
vec3(0.482, 0.890, 0.627), // #7BE3A0
vec3(0.498, 0.714, 1.000)); // #7FB6FF
float lw = 1.6 / u_res.y;
for (int ch = 0; ch < 3; ch++){
float v = a[ch] + b[ch] * cos(6.28318530718 * (c[ch] * uv.x + d[ch]));
float y = bandH + (1.0 - bandH) * ((1.0 - clamp(v, 0.0, 1.0)) * 0.9 + 0.05);
col = mix(col, chan[ch], 0.85 * (1.0 - smoothstep(0.0, lw, abs(uv.y - y))));
}
return col;
}
out vec4 fragColor;
void main(){
// FORMA's uv runs y-down, matching its canvas; TD's vUV runs up
vec2 uv = vec2(vUV.s, 1.0 - vUV.t);
fragColor = TDOutputSwizzle(vec4(plate(uv), 1.0));
}
NUKE · BLINKSCRIPT
The same shader this plate runs, transpiled to a BlinkScript kernel. Paste it into a BlinkScript node's Kernel Source and press Recompile; every constant arrives as a knob at its published value, and u_t animates with the expression frame/24. Compiled and rendered in Nuke 17.1, then compared against this plate on the page.
// FORMA — PL. 32 · COSINE GRADIENT — Formulated by Inigo Quilez
// colour(t) = a + b · cos( 2π · (c·t + d) )
// with a, b, c, d ∈ ℝ³
// Nuke port — a BlinkScript kernel. Paste into a BlinkScript node's Kernel
// Source and press Recompile. Every constant arrives as a knob at its published
// value (the comment gives the measured range); u_t is a knob too — animate it
// with the expression frame/24 or leave it at 0 for the still frame. Written
// from the published mathematics, not adapted from any code.
// Transpiled from the shader this plate runs on the page (GLSL ES 3.00):
// vec → float2/3/4, swizzles expanded, GLSL builtins Blink lacks written out
// as forma_ functions, float literals suffixed. Compiled and rendered in a
// real Nuke (17.1v1) and compared against this plate on the page: 34 of 34.
//
// plate() and its helpers are written to a single exit — the loop that runs
// once. That is not a style: Blink 17.1 drops a conditional early return from
// a called function while Vectorize is on, which is the node default, with no
// warning and no error. Written this way it paints correctly as pasted.
kernel Forma_cospalette : ImageComputationKernel<ePixelWise>
{
Image<eWrite> dst;
param:
float u_t; // seconds; 0 is the still frame
float p_d0; // phase — red · live 0 .. 1
float p_d1; // phase — green · live 0 .. 1
float p_d2; // phase — blue · live 0 .. 1
float p_c; // frequency · live 0.5 .. 3
float p_amp; // b — amplitude · live 0 .. 0.5
local:
float2 u_res;
float u_phase;
float3 u_pal_a, u_pal_b, u_pal_c, u_pal_d;
void define(){
defineParam(u_t, "u_t", 0.0f);
defineParam(p_d0, "p_d0", 0.0f);
defineParam(p_d1, "p_d1", 0.12f);
defineParam(p_d2, "p_d2", 0.25f);
defineParam(p_c, "p_c", 1.0f);
defineParam(p_amp, "p_amp", 0.42f);
}
void init(){
u_res = float2(float(dst.bounds.width()), float(dst.bounds.height()));
u_phase = 0.5831f; // this plate's own grid phase, 0..1
// FORMA's COLOUR accent as cosine-gradient coefficients
u_pal_a = float3(0.46f, 0.2002f, 0.3896f);
u_pal_b = float3(0.5f, 0.2176f, 0.4235f);
u_pal_c = float3(1.0f, 1.0f, 1.0f);
u_pal_d = float3(0.0f, 0.05f, 0.1f);
}
/* GLSL builtins Blink lacks, written as templates rather than overload sets.
Blink's operators return expression templates (Swizzle<float,N>), so a call
passing an expression cannot resolve against an overload set on float2
against float3 — measured in Nuke 17.1: a float2 expression is ambiguous
between the two, while scalar-against-vector resolves. A template deduces
the expression's own type, so the ambiguity cannot arise. */
template <class T> T forma_fract(T v){ return v - floor(v); }
template <class T, class S> T forma_mod(T x, S y){ return x - y * floor(x / y); }
/* Blink's own min/max/clamp take no scalar bound against a vector, which GLSL
does; v * 0.0f + b is that bound at the vector's own width, and collapses to
b when v is a scalar, so one template serves both. */
template <class T, class S> T forma_min(T a, S b){ return min(a, a * 0.0f + b); }
template <class T, class S> T forma_max(T a, S b){ return max(a, a * 0.0f + b); }
template <class T, class S> T forma_clamp(T v, S lo, S hi){ return clamp(v, v * 0.0f + lo, v * 0.0f + hi); }
int forma_min(int a, int b){ return min(a, b); }
int forma_max(int a, int b){ return max(a, b); }
/* GLSL step(edge, x) is 1 where x >= edge; floor(sign(x - e) * 0.5 + 1) is
that exactly, equality included, out of builtins Blink does have. */
template <class T, class S> T forma_step(S e, T x){ return floor(sign(x - e) * 0.5f + 1.0f); }
template <class T, class S> T forma_smoothstep(S a, S b, T x){
T t = forma_clamp((x - a) / (b - a), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
template <class T> float forma_distance(T a, T b){ return length(a - b); }
float forma_tanh(float x){ float e = exp(2.0f * x); return (e - 1.0f) / (e + 1.0f); }
float forma_radians(float d){ return d * 0.01745329252f; }
// the page's hash2 is exact uint32; Blink has int, so the shifts are made
// logical by masking and the read-back is lifted into 0 .. 2^32
/* A uint read back as a float. Blink has no unsigned type, so a value past
2^31 arrives as a negative int and float() of it is negative. Measured on
gabor, whose own generator then returned uniforms in [-0.5, 0.5) and drew
a different picture — it compiled, it rendered, and only comparing it with
/* The order's ramp — the same cosine formulation the JS kit uses, so a
plate keeps its classification colour in either language. */
float3 ramp(float t){
return forma_clamp(u_pal_a + u_pal_b * cos(6.28318530718f * (u_pal_c * t + u_pal_d)), 0.0f, 1.0f);
}
/* Sawtooth and triangle on this plate's phase, mirroring the JS kit. */
float cycle(float t, float period){ return forma_fract(t / period + u_phase); }
float pingpong(float t, float period){
float u = cycle(t, period);
return u < 0.5f ? u * 2.0f : 2.0f - u * 2.0f;
}
/* The band is the palette itself. The three traces below it plot the same
per-channel cosine, found by distance to the curve rather than stroked —
a fragment shader has no pen, so the line is where the plot passes close. */
float3 plate(float2 uv){
float3 forma_r = float3(0.0f, 0.0f, 0.0f);
for (int forma_once = 0; forma_once < 1; forma_once++){
float3 a = float3(0.5f);
float3 b = float3(p_amp);
float3 c = float3(p_c);
float roll = pingpong(u_t, 21.0f);
float3 d = float3(p_d0, p_d1, p_d2) + roll * 0.5f;
const float bandH = 0.52f;
if (uv.y < bandH) { forma_r = forma_clamp(a + b * cos(6.28318530718f * (c * uv.x + d)), 0.0f, 1.0f); break; }
float3 col = float3(4.0f, 6.0f, 10.0f) / 255.0f;
float3 chan[3] = {float3(1.000f, 0.478f, 0.431f), // #FF7A6E
float3(0.482f, 0.890f, 0.627f), // #7BE3A0
float3(0.498f, 0.714f, 1.000f)}; // #7FB6FF
float lw = 1.6f / u_res.y;
for (int ch = 0; ch < 3; ch++){
float v = a[ch] + b[ch] * cos(6.28318530718f * (c[ch] * uv.x + d[ch]));
float y = bandH + (1.0f - bandH) * ((1.0f - forma_clamp(v, 0.0f, 1.0f)) * 0.9f + 0.05f);
col = lerp(col, chan[ch], 0.85f * (1.0f - forma_smoothstep(0.0f, lw, fabs(uv.y - y))));
}
{ forma_r = col; break; }
}
return forma_r;
}
void process(int2 pos){
// FORMA's uv runs y-down like its canvas; Nuke's rows run up
float2 uv = float2((float(pos.x) + 0.5f) / u_res.x, 1.0f - (float(pos.y) + 0.5f) / u_res.y);
float3 c = plate(uv);
dst() = float4(c.x, c.y, c.z, 1.0f);
}
};
AFTER EFFECTS · EXPRESSION
The same published mathematics as a Shape Layer path expression. Paste it onto a Path property; every constant is the published value plus a Slider Control named
// FORMA — PL. 32 · COSINE GRADIENT — Formulated by Inigo Quilez
// colour(t) = a + b · cos( 2π · (c·t + d) )
// with a, b, c, d ∈ ℝ³
// After Effects port — paste onto a Shape Layer's Path property
// (Contents › Shape › Path). Written from the published mathematics, not
// adapted from any code. Constants arrive at their published values; add a
// Slider Control (Effect › Expression Controls) named <k>_tweak and that
// constant moves in its own units, starting at 0 — the published figure.
// The plate's comet and its reveal are Trim Paths; the stroke colour is
// FORMA's COLOUR accent, #FF6FD8. Animation runs on time.
// This plate draws 3 separate paths at its published constants:
// duplicate the group (Contents › Group) that many times and each copy draws
// its own part, read from its position in the layer. A Slider Control named
// "part" on the layer pins one instead.
// https://forma-gen.com/#plate=cospalette
// A missing slider reads 0, so a bare paste already draws the figure.
function forma_tweak(n){ try { return effect(n)("Slider"); } catch (e){ return 0; } }
var p_d0 = 0 + forma_tweak("d0_tweak"); // phase — red · live 0 .. 1
var p_d1 = 0.12 + forma_tweak("d1_tweak"); // phase — green · live 0 .. 1
var p_d2 = 0.25 + forma_tweak("d2_tweak"); // phase — blue · live 0 .. 1
var p_c = 1 + forma_tweak("c_tweak"); // frequency · live 0.5 .. 3
var p_amp = 0.42 + forma_tweak("amp_tweak"); // b — amplitude · live 0 .. 0.5
// The frame: the plate's W × H canvas is this comp, with the origin at the
// layer's anchor; canvas y already runs down, as After Effects' does.
var forma_W = thisComp.width, forma_H = thisComp.height, forma_t = time;
var forma_phase = 0.5830902014859021; // this plate's own fixed phase, as the page has it
function forma_pt(x, y){ return [x - forma_W / 2, y - forma_H / 2]; }
function forma_partIndex(){
try { return Math.round(effect("part")("Slider")); } catch (e){}
try { return thisProperty.propertyGroup(3).propertyIndex - 1; } catch (e){ return 0; }
}
var forma_part = forma_partIndex();
// The cosine palette, colour(t) = a + b·cos(2π(c·t + d)) per channel, with
// a = 0.5, b = amp, c the frequency and d the three phase offsets, rolling
// over 21 s as the page does. The page paints the palette itself as a band
// across the top and plots the three channels beneath it; the band is a
// gradient — one rect per column — and has no path, so the port is the
// three channel curves, one per part (red, green, blue on the page), drawn
// in the lower 0.48 of the frame exactly where the page plots them. The
// band is a Gradient Ramp in After Effects, not geometry.
// parts: 3
var roll01 = ((forma_t / 21) + forma_phase) % 1, roll = roll01 < 0.5 ? roll01 * 2 : 2 - roll01 * 2;
var d = [p_d0 + roll * 0.5, p_d1 + roll * 0.5, p_d2 + roll * 0.5];
var ch = Math.max(0, Math.min(2, forma_part)), dd = d[ch];
var bandH = forma_H * 0.52, W = Math.round(forma_W);
var pts = [];
for (var x = 0; x <= W; x++){
var v = 0.5 + p_amp * Math.cos(6.28318530718 * (p_c * (x / W) + dd));
v = Math.min(1, Math.max(0, v));
pts.push(forma_pt(x, bandH + (forma_H - bandH) * (1 - v) * 0.9 + (forma_H - bandH) * 0.05));
}
createPath(pts, [], [], false);