Last active
December 5, 2025 15:35
-
-
Save MagnusThor/422a8e9427a690290c190068f6f2931a to your computer and use it in GitHub Desktop.
CSS Houdini - Fiddling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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