Examples
generic truchet.ts
Truchet tiles
One tile — two quarter-circle arcs — mirrored per cell by a hash bit, and the plane weaves itself into an endless maze. fwidth keeps the strokes screen-constant; energy pulses along the arcs. Tile count is live.
Live
0.0s
One source, compiled every way
The DSL source below, compiled to WebGPU (WGSL), WebGL2 (GLSL ES 3.00 — two stages), and a reflection JSON the host binds from. Nothing but the DSL tab is hand-written.
// ═══ @xgis/shader-dsl example — truchet tiles (hash-flipped arc maze) ═══//// The truchet construction: tile the plane with ONE tile — two quarter-circle// arcs joining edge midpoints — and let a per-cell hash mirror half the tiles.// The arcs connect across every cell boundary by construction, so a single bit// per cell yields an endless woven maze. Anti-aliased with `fwidth`// (screen-constant line width), energy pulsing along the arcs by angle.// WGSL (WebGPU) + GLSL ES 3.00 (WebGL2).
import { fn, module, f32, vec2, vec3, vec4, sin, cos, floor, fract, dot, mix, min, abs, step, length, atan2, smoothstep, fwidth, Let, f32T, vec2fT,} from '../src/index.ts'import { VsOut, vs, fullscreenUniforms } from './_fullscreen.ts'import type { ShaderExample } from './_shared.ts'const U = fullscreenUniforms({ tiles: f32T })
// scalar hash of a lattice point → [0,1)const hash = fn('hash', { p: vec2fT }, ({ p }) => fract(sin(dot(p, vec2(127.1, 311.7))).mul(43758.5453)),)
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 asp = res.x.div(res.y) const q = vec2(vo.uv.x.mul(asp), vo.uv.y).mul(U.field.tiles) const cell = Let(floor(q)) const f = Let(fract(q)) const h = Let(hash({ p: cell })) // one hash bit mirrors the tile — arcs still meet across every edge const fx = mix(f.x, f32(1).sub(f.x), step(0.5, h)) const g = Let(vec2(fx, f.y)) // distance to the two quarter-circle arcs (radius ½, centred on // opposite tile corners) const d1 = abs(length(g).sub(0.5)) const d2 = abs(length(g.sub(vec2(1, 1))).sub(0.5)) const d = Let(min(d1, d2)) // screen-constant stroke via fwidth const aa = Let(fwidth(d).mul(1.4)) const mask = f32(1).sub(smoothstep(f32(0.13).sub(aa), f32(0.13).add(aa), d)) // energy flowing along the arc: parametrise by angle around whichever // corner owns the nearer arc const a1 = atan2(g.y, g.x) const a2 = atan2(g.y.sub(1), g.x.sub(1)) const an = mix(a2, a1, step(d1, d2)) const flow = sin(an.mul(6).add(t.mul(2.5))) .mul(0.35) .add(0.65) // per-cell hue drifting slowly const col = palette({ t: hash({ p: cell.add(vec2(7.7, 3.1)) }) .mul(0.4) .add(t.mul(0.05)), }) const bg = vec3(0.04, 0.05, 0.09) return vec4(mix(bg, col.mul(flow), mask), 1) }, { stage: 'fragment', retAttr: '@location(0)' },)
const truchetModule = module({ structs: [U.struct, VsOut.decl], bindings: [U.binding], funcs: [vs, fs],})
export const truchet: ShaderExample = { id: 'truchet', title: 'Truchet tiles', blurb: 'One tile — two quarter-circle arcs — mirrored per cell by a hash bit, and the plane weaves itself into an endless maze. fwidth keeps the strokes screen-constant; energy pulses along the arcs. Tile count is live.', category: 'generic', file: 'truchet.ts', module: truchetModule, renderable: true, controls: { time: { kind: 'time' }, resolution: { kind: 'resolution' }, tiles: { kind: 'slider', label: 'Tiles', min: 4, max: 18, step: 1, value: 9 }, },}struct Uniforms { time: f32, resolution: vec2<f32>, tiles: f32,}
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn hash(p: vec2<f32>) -> f32 { return fract((sin(dot(p, vec2<f32>(127.1, 311.7))) * 43758.5453));}
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));}
@vertexfn 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)));}
@fragmentfn fs(vo: VsOut) -> @location(0) vec4<f32> { let _cse0 = (vec2<f32>((vo.uv.x * (U.resolution.x / U.resolution.y)), vo.uv.y) * U.tiles); let _cse1 = vec2<f32>(1.0, 1.0); let _v0 = floor(_cse0); let _v1 = fract(_cse0); let _v2 = hash(_v0); let _v3 = vec2<f32>(mix(_v1.x, (1.0 - _v1.x), step(0.5, _v2)), _v1.y); let _v4 = min(abs((length(_v3) - 0.5)), abs((length((_v3 - _cse1)) - 0.5))); let _v5 = (fwidth(_v4) * 1.4); return vec4<f32>(mix(vec3<f32>(0.04, 0.05, 0.09), (palette(((hash((_v0 + vec2<f32>(7.7, 3.1))) * 0.4) + (U.time * 0.05))) * ((sin(((mix(atan2((_v3.y - 1.0), (_v3.x - 1.0)), atan2(_v3.y, _v3.x), step(abs((length(_v3) - 0.5)), abs((length((_v3 - _cse1)) - 0.5)))) * 6.0) + (U.time * 2.5))) * 0.35) + 0.65)), (1.0 - smoothstep((0.13 - _v5), (0.13 + _v5), _v4))), 1.0);}#version 300 esprecision highp float;precision highp int;
struct VsOut { vec4 pos; vec2 uv;};out vec2 uv;
VsOut vs_impl(uint vi) { float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0); float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0); return VsOut(vec4(_cse0, _cse1, 0.0, 1.0), vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));}
void main() { VsOut _out = vs_impl(uint(gl_VertexID)); gl_Position = _out.pos; uv = _out.uv;}#version 300 esprecision highp float;precision highp int;
struct VsOut { vec4 pos; vec2 uv;};layout(std140) uniform Uniforms { float time; vec2 resolution; float tiles;} U;float hash(vec2 p);vec3 palette(float t);float hash(vec2 p) { return fract((sin(dot(p, vec2(127.1, 311.7))) * 43758.5453));}
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;
vec4 fs_impl(VsOut vo) { vec2 _cse0 = (vec2((vo.uv.x * (U.resolution.x / U.resolution.y)), vo.uv.y) * U.tiles); vec2 _cse1 = vec2(1.0, 1.0); vec2 _v0 = floor(_cse0); vec2 _v1 = fract(_cse0); float _v2 = hash(_v0); vec2 _v3 = vec2(mix(_v1.x, (1.0 - _v1.x), step(0.5, _v2)), _v1.y); float _v4 = min(abs((length(_v3) - 0.5)), abs((length((_v3 - _cse1)) - 0.5))); float _v5 = (fwidth(_v4) * 1.4); return vec4(mix(vec3(0.04, 0.05, 0.09), (palette(((hash((_v0 + vec2(7.7, 3.1))) * 0.4) + (U.time * 0.05))) * ((sin(((mix(atan((_v3.y - 1.0), (_v3.x - 1.0)), atan(_v3.y, _v3.x), step(abs((length(_v3) - 0.5)), abs((length((_v3 - _cse1)) - 0.5)))) * 6.0) + (U.time * 2.5))) * 0.35) + 0.65)), (1.0 - smoothstep((0.13 - _v5), (0.13 + _v5), _v4))), 1.0);}
void main() { VsOut vo; vo.pos = gl_FragCoord; vo.uv = uv; _ret = fs_impl(vo);}{ "bindGroups": [ { "group": 0, "entries": [ { "group": 0, "binding": 0, "name": "U", "space": "uniform", "resourceKind": "uniform-buffer", "structName": "Uniforms" } ] } ], "uniforms": [ { "name": "Uniforms", "size": 32, "align": 16, "fields": [ { "name": "time", "type": "f32", "offset": 0, "align": 4, "size": 4 }, { "name": "resolution", "type": "vec2<f32>", "offset": 8, "align": 8, "size": 8 }, { "name": "tiles", "type": "f32", "offset": 16, "align": 4, "size": 4 } ] } ], "storage": [], "entries": [ { "name": "vs", "stage": "vertex", "inputs": [ "u32" ], "output": "struct:VsOut" }, { "name": "fs", "stage": "fragment", "inputs": [ "struct:VsOut" ], "output": "vec4<f32>" } ], "overrides": []}
Run it locally:
npx tsx examples/print.ts truchet
— prints the WGSL, GLSL, reflection. Source:
GitHub.
New to the IR? See Concepts; the full authoring + emit surface is in the API reference.