Truchet tiles

Truchet tiles

One tile.

Truchet tiles shader, written in TypeScript and compiled by TypeShade, rendered at build time.
// ═══ typeshade 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.js';
import { VsOut, vs, fullscreenUniforms } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
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));
}
@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 * (U.resolution.x / U.resolution.y)), vo.uv.y) * U.tiles);
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 _cse1 = vec2<f32>(1.0, 1.0);
let _gv0 = abs((length(_v3) - 0.5));
let _gv1 = abs((length((_v3 - _cse1)) - 0.5));
let _v4 = min(_gv0, _gv1);
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(_gv0, _gv1)) * 6.0) + (U.time * 2.5))) * 0.35) + 0.65)), (1.0 - smoothstep((0.13 - _v5), (0.13 + _v5), _v4))), 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 tiles;
} U;
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;
void main() {
vec2 _cse0 = (vec2((uv.x * (U.resolution.x / U.resolution.y)), uv.y) * U.tiles);
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);
vec2 _cse1 = vec2(1.0, 1.0);
float _gv0 = abs((length(_v3) - 0.5));
float _gv1 = abs((length((_v3 - _cse1)) - 0.5));
float _v4 = min(_gv0, _gv1);
float _v5 = (fwidth(_v4) * 1.4);
_ret = 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(_gv0, _gv1)) * 6.0) + (U.time * 2.5))) * 0.35) + 0.65)), (1.0 - smoothstep((0.13 - _v5), (0.13 + _v5), _v4))), 1.0);
}

Truchet tiles, 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