Kaleidoscope

Kaleidoscope

The polar mirror fold.

Kaleidoscope shader, written in TypeScript and compiled by TypeShade, rendered at build time.
// ═══ typeshade example — kaleidoscope (polar mirror fold) ═══
//
// The kaleidoscope fold: convert to polar, floor-mod the angle into one
// sector, mirror about the sector's midline, convert back — every sector now
// shows the same wedge of pattern, seamlessly. The pattern inside the wedge is
// swirling fbm + concentric rings through a cosine palette. The fold uses
// `mod` (#839), the portable FLOOR-mod, so the negative angles atan2
// produces wrap identically on both targets. WGSL + GLSL ES 3.00 (WebGL2).
import {
fn,
module,
f32,
vec2,
vec3,
vec4,
sin,
cos,
floor,
fract,
mix,
abs,
mod,
length,
atan2,
smoothstep,
Let,
f32T,
vec2fT,
u32,
i32,
u32T,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ segments: f32T });
// An exact integer hash (lowbias32 with xxHash's primes): every target and the CPU oracle
// agree on every bit of it. `fract(sin(x) * 43758.5453)` did not, since WGSL bounds `sin`
// only to 2^-11 and the multiply puts that error above the fraction (#184).
const hash32 = fn('hash32', { x: u32T }, ({ x }) => {
const a = Let(x.bitXor(x.shr(16)).mul(0x85ebca77));
const b = Let(a.bitXor(a.shr(13)).mul(0xc2b2ae3d));
return b.bitXor(b.shr(16));
});
// scalar hash of a lattice point → [0,1): 24 bits, which f32 holds exactly
const hash = fn('hash', { p: vec2fT }, ({ p }) => {
const h = Let(hash32({ x: u32(i32(p.x)).bitXor(hash32({ x: u32(i32(p.y)) })) }));
return f32(h.shr(8)).mul(5.9604644775390625e-8); // 2^-24, exact: WGSL lets `/` round
});
// bilinear value noise with smootherstep weights
const noise = fn('noise', { p: vec2fT }, ({ p }) => {
const i = Let(floor(p));
const f = Let(fract(p));
const u = f.mul(f).mul(vec2(3).sub(f.mul(2)));
return mix(
mix(hash({ p: i }), hash({ p: i.add(vec2(1, 0)) }), u.x),
mix(hash({ p: i.add(vec2(0, 1)) }), hash({ p: i.add(vec2(1, 1)) }), u.x),
u.y,
);
});
// 4-octave fbm, unrolled so the helper stays a pure value expression
const fbm = fn('fbm', { p: vec2fT }, ({ p }) => {
return noise({ p })
.mul(0.5)
.add(noise({ p: p.mul(2.02) }).mul(0.25))
.add(noise({ p: p.mul(4.08) }).mul(0.125))
.add(noise({ p: p.mul(8.2) }).mul(0.0625));
});
const palette = fn('palette', { t: f32T }, ({ t }) => {
const ph = vec3(0.0, 0.33, 0.67);
return vec3(0.5).add(cos(t.add(ph).mul(6.283)).mul(0.5));
});
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const t = U.field.time;
const res = U.field.resolution;
const p = screenCoords(vo.uv, res);
const r = Let(length(p));
const a0 = Let(atan2(p.y, p.x));
// fold: floor-mod the angle into one sector, mirror about its midline
const sector = Let(f32(6.2831853).div(U.field.segments));
const am = mod(a0, sector);
const af = Let(abs(am.sub(sector.mul(0.5))));
const q = vec2(cos(af), sin(af)).mul(r);
// wedge pattern: swirling fbm + concentric rings
const v = Let(fbm({ p: q.mul(3).add(vec2(t.mul(0.12), t.mul(0.09).neg())) }));
const rings = sin(r.mul(9).sub(t.mul(0.8)))
.mul(0.5)
.add(0.5);
const col = palette({ t: v.mul(0.7).add(rings.mul(0.15)).add(r.mul(0.3)).sub(t.mul(0.03)) });
// the fbm field doubles as a brightness relief so the wedges keep depth
const relief = v.mul(0.9).add(0.35);
// vignette so the fold's outer edge fades instead of clipping
const vig = f32(1).sub(smoothstep(0.55, 1.25, r));
return vec4(col.mul(relief).mul(vig), 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
const kaleidoscopeModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const kaleidoscope: ShaderExample = {
id: 'kaleidoscope',
title: 'Kaleidoscope',
blurb:
'The polar mirror fold — the angle floor-modded into one sector and mirrored about its midline, so every sector repeats the same wedge of swirling fbm and rings. Segment count is live.',
category: 'generic',
file: 'kaleidoscope.ts',
module: kaleidoscopeModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
segments: { kind: 'slider', label: 'Segments', min: 3, max: 12, step: 1, value: 6 },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
segments: f32,
}
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn hash32(x: u32) -> u32 {
let _v0 = ((x ^ (x >> 16u)) * 2246822519u);
let _v1 = ((_v0 ^ (_v0 >> 13u)) * 3266489917u);
return (_v1 ^ (_v1 >> 16u));
}
fn hash(p: vec2<f32>) -> f32 {
let _v0 = hash32((u32(i32(p.x)) ^ hash32(u32(i32(p.y)))));
return (f32((_v0 >> 8u)) * 5.960464477539063e-8);
}
fn noise(p: vec2<f32>) -> f32 {
let _v0 = floor(p);
let _v1 = fract(p);
let _cse0 = vec2<f32>(3.0);
let _gv0 = ((_v1 * _v1) * (_cse0 - (_v1 * 2.0)));
let _lc0 = _gv0.x;
return mix(mix(hash(_v0), hash((_v0 + vec2<f32>(1.0, 0.0))), _lc0), mix(hash((_v0 + vec2<f32>(0.0, 1.0))), hash((_v0 + vec2<f32>(1.0, 1.0))), _lc0), _gv0.y);
}
fn fbm(p: vec2<f32>) -> f32 {
return ((((noise(p) * 0.5) + (noise((p * 2.02)) * 0.25)) + (noise((p * 4.08)) * 0.125)) + (noise((p * 8.2)) * 0.0625));
}
fn palette(t: f32) -> vec3<f32> {
return (vec3<f32>(0.5) + (cos(((t + vec3<f32>(0.0, 0.33, 0.67)) * 6.283)) * 0.5));
}
@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 _cse0 = vec2<f32>((((vo.uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((vo.uv.y * 2.0) - 1.0));
let _v0 = length(_cse0);
let _v1 = atan2(_cse0.y, _cse0.x);
let _v2 = (6.2831853 / U.segments);
let _v3 = abs(((_v1 - _v2 * floor(_v1 / _v2)) - (_v2 * 0.5)));
let _v4 = fbm((((vec2<f32>(cos(_v3), sin(_v3)) * _v0) * 3.0) + vec2<f32>((U.time * 0.12), (-(U.time * 0.09)))));
return vec4<f32>(((palette(((((_v4 * 0.7) + (((sin(((_v0 * 9.0) - (U.time * 0.8))) * 0.5) + 0.5) * 0.15)) + (_v0 * 0.3)) - (U.time * 0.03))) * ((_v4 * 0.9) + 0.35)) * (1.0 - smoothstep(0.55, 1.25, _v0))), 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 segments;
} U;
int _f2i(float x) {
return int(mix(clamp(x, -2147483648.0, 2147483520.0), 0.0, isnan(x)));
}
uint hash32(uint x) {
uint _v0 = ((x ^ (x >> 16u)) * 2246822519u);
uint _v1 = ((_v0 ^ (_v0 >> 13u)) * 3266489917u);
return (_v1 ^ (_v1 >> 16u));
}
float hash(vec2 p) {
uint _v0 = hash32((uint(_f2i(p.x)) ^ hash32(uint(_f2i(p.y)))));
return (float((_v0 >> 8u)) * 5.960464477539063e-8);
}
float noise(vec2 p) {
vec2 _v0 = floor(p);
vec2 _v1 = fract(p);
vec2 _cse0 = vec2(3.0);
vec2 _gv0 = ((_v1 * _v1) * (_cse0 - (_v1 * 2.0)));
float _lc0 = _gv0.x;
return mix(mix(hash(_v0), hash((_v0 + vec2(1.0, 0.0))), _lc0), mix(hash((_v0 + vec2(0.0, 1.0))), hash((_v0 + vec2(1.0, 1.0))), _lc0), _gv0.y);
}
float fbm(vec2 p) {
return ((((noise(p) * 0.5) + (noise((p * 2.02)) * 0.25)) + (noise((p * 4.08)) * 0.125)) + (noise((p * 8.2)) * 0.0625));
}
vec3 palette(float t) {
return (vec3(0.5) + (cos(((t + vec3(0.0, 0.33, 0.67)) * 6.283)) * 0.5));
}
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
vec2 _cse0 = vec2((((uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((uv.y * 2.0) - 1.0));
float _v0 = length(_cse0);
float _v1 = atan(_cse0.y, _cse0.x);
float _v2 = (6.2831853 / U.segments);
float _v3 = abs((mod(_v1, _v2) - (_v2 * 0.5)));
float _v4 = fbm((((vec2(cos(_v3), sin(_v3)) * _v0) * 3.0) + vec2((U.time * 0.12), (-(U.time * 0.09)))));
_ret = vec4(((palette(((((_v4 * 0.7) + (((sin(((_v0 * 9.0) - (U.time * 0.8))) * 0.5) + 0.5) * 0.15)) + (_v0 * 0.3)) - (U.time * 0.03))) * ((_v4 * 0.9) + 0.35)) * (1.0 - smoothstep(0.55, 1.25, _v0))), 1.0);
}

Kaleidoscope, 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