Skip to content

Instantly share code, notes, and snippets.

@MagnusThor
Last active December 5, 2025 15:35
Show Gist options
  • Select an option

  • Save MagnusThor/422a8e9427a690290c190068f6f2931a to your computer and use it in GitHub Desktop.

Select an option

Save MagnusThor/422a8e9427a690290c190068f6f2931a to your computer and use it in GitHub Desktop.
CSS Houdini - Fiddling
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
flex-direction: column;
overflow: hidden;
}
.shader-element {
width: 100vw;
height: 100vh;
border: 1px solid #000;
background: paint(shader-worklet);
filter: blur(5px);
--iterations:13;
--pixelSize: 10;
--time:0;
}
</style>
</head>
<body>
<div class="shader-element"></div>
<script>
const shaderElement = document.querySelector('.shader-element');
let lastTime = 1000;
function animate(time) {
if (lastTime) {
const delta = time - lastTime;
shaderElement.style.setProperty('--time', delta);
}
lastTime = time;
requestAnimationFrame(animate);
}
if (CSS.paintWorklet) {
CSS.paintWorklet.addModule('shader-worklet.js').then(() => {
console.log("shader-worklet registred")
requestAnimationFrame(animate);
});
}
</script>
</body>
</html>
class PixelRenderer {
constructor() {
}
run(ctx, size, fn, pixelSize) {
const w = Math.floor(size.width);
const h = Math.floor(size.height);
const PIXEL = pixelSize | 1;
for (let x = 0; x < w; x += PIXEL) {
for (let y = 0; y < h; y += PIXEL) {
const xn = x / (w - 1) * 2 - 1;
const yn = y / (h - 1) * 2 - 1;
const [r, g, b] = fn(xn, yn);
ctx.fillStyle = `rgb(${r | 0},${g | 0},${b | 0})`;
ctx.fillRect(x, y, Math.min(PIXEL, w - x), Math.min(PIXEL, h - y));
}
}
}
}
export class Helpers {
constructor() {}
// -------------------------------------------------------------
// Vector creation
// -------------------------------------------------------------
vec2(x, y) { return [x, y]; }
vec3(x, y, z) { return [x, y, z]; }
vec4(x, y, z, w) { return [x, y, z, w]; }
// -------------------------------------------------------------
// Scalar functions (GLSL-like)
// -------------------------------------------------------------
// fract(x) = x - floor(x)
fract(x) { return x - Math.floor(x); }
// abs(x)
abs(x) { return Array.isArray(x) ? x.map(v => Math.abs(v)) : Math.abs(x); }
// clamp(x, min, max)
clamp(x, a, b) { return Math.max(a, Math.min(b, x)); }
// mix(a,b,t) same as GLSL mix (your lerp)
mix(a, b, t) { return a + t * (b - a); }
// step(edge, x)
// returns 0 if x < edge, else 1
step(edge, x) { return x < edge ? 0 : 1; }
// smoothstep(edge0, edge1, x)
smoothstep(a, b, x) {
x = this.clamp((x - a) / (b - a), 0, 1);
return x * x * (3 - 2 * x);
}
// mod(x, y)
mod(x, y) { return x - y * Math.floor(x / y); }
// sign(x)
sign(x) { return x < 0 ? -1 : (x > 0 ? 1 : 0); }
// -------------------------------------------------------------
// Vector math (GLSL-like)
// -------------------------------------------------------------
// element-wise add
add(a, b) { return a.map((v, i) => v + b[i]); }
// element-wise subtract
sub(a, b) { return a.map((v, i) => v - b[i]); }
// multiply vector * scalar or vec * vec
mul(a, b) {
return Array.isArray(b)
? a.map((v, i) => v * b[i])
: a.map(v => v * b);
}
// dot(a,b)
dot(a, b) {
let s = 0;
for (let i = 0; i < a.length; i++) s += a[i] * b[i];
return s;
}
// length(v)
length(v) { return Math.sqrt(this.dot(v, v)); }
// normalize(v)
normalize(v) {
const l = this.length(v);
return l === 0 ? v : v.map(val => val / l);
}
// distance(a,b)
distance(a, b) {
return this.length(this.sub(a, b));
}
// reflect(I, N) = I - 2 * dot(N,I) * N
reflect(I, N) {
const d = this.dot(N, I) * 2;
return this.sub(I, this.mul(N, d));
}
// faceforward(N, I, Nref)
faceforward(N, I, Nref) {
return this.dot(Nref, I) < 0 ? N : this.mul(N, -1);
}
// -------------------------------------------------------------
// Noise-related & random
// -------------------------------------------------------------
// Classic GLSL hash: returns repeatable pseudo-random value
hash1(x) {
return this.fract(Math.sin(x * 127.1) * 43758.5453123);
}
// 2D hash
hash2(x, y) {
return this.fract(Math.sin(x * 127.1 + y * 311.7) * 43758.5453123);
}
// Permutation table generator (Perlin-style)
seed(n) {
const p = [];
const a = [];
for (let i = 0; i < n; i++) a.push(i);
// shuffle
for (let i = 0; i < n; i++) {
const j = (Math.random() * n) | 0;
const tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
// duplicate to 2n
for (let i = 0; i < n; i++) p[n + i] = p[i] = a[i];
return p;
}
// Apply function to each element of an array
func(a, exp) {
return a.map((v, i) => exp(v, i));
}
}
class ProcedualRenderer {
static get inputProperties() {
return [
'--time',
'--pixelSize',
'--iterations'
];
}
constructor() {
this.renderer = new PixelRenderer();
this.helpers = new Helpers();
}
paint(ctx, size, uniforms) {
const time = parseFloat(uniforms.get('--time').toString()) || 1.0;
const pixelSize = parseInt(uniforms.get('--pixelSize').toString()) || 1;
const iterations = parseFloat(uniforms.get('--iterations').toString()) || 7;
const pixelFunc = (x, y, uniforms) => { // the content of pixelFunc should be self contained, and imported / injected
const t = this.helpers;
const v = t.vec4(x, y,0.,1.);
const a = (a, b) => {
return Math.abs((a * b) * 255);
}
const s = (p) => {
let col = 0, l = col;
for (let i = 0; i < iterations; i++) {
let pl = l;
l = t.length(p);
let dot = t.dot(p, p);
p = t.func(p, function (v) {
return Math.abs(v) / dot - .5;
});
col += Math.exp(-1 / Math.abs(l - pl));
}
return col;
};
let k = s(v) * .18;
let r = a(k, 1.1),
g = a(k * k, 1.3),
b = a(k * k * k, 1.);
return [r, g, b];
};
this.renderer.run(ctx, size, pixelFunc, pixelSize);
}
}
registerPaint('shader-worklet', ProcedualRenderer);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment