<!DOCTYPE html> <html lang="en-us"> <head> <meta charset="utf-8"> <meta name="robots" content="noindex"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <title>Sierpiński Carpet Threads</title> <style> html, body, canvas { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; } </style> </head> <body> <canvas id="canvas"></canvas> <!-- © Adam Murray 2024 https://adammurray.link/ Creative Commons License Attribution-NonCommercial-ShareAlike 4.0 International https://creativecommons.org/licenses/by-nc-sa/4.0/ --> <script id="vertexShader" type="x-shader/x-vertex"> #version 300 es in vec4 vertexPosition; void main() { // no-op vertex shader gl_Position = vertexPosition; } </script> <script id="fragmentShader" type="x-shader/x-fragment"> #version 300 es precision highp float; uniform vec2 canvasSize; uniform float time; // time in seconds out vec4 fragColor; const float QUALITY = 2.; // anti-aliasing amount vec3 palette(float idx) { float i = mod(idx,4.); if (i < 1.) return vec3(0.80,0.78,0.99); else if (i < 2.) return vec3(0.71,0.93,0.80); else if (i < 3.) return vec3(0.99,0.93,0.65); else return vec3(1.00,0.70,0.65); } vec3 draw(vec2 p, float start, float end, float iterations) { float iter = min(20., iterations); float len = end - start; float x = p.x - start; float y = p.y - start; float thresh = 0.005; // for added fun: // thresh = 0.3 + 0.25*-cos(time/5.); float pct = 0.; float i; for(i=0.; i<iter+1.; i++) { len /= 3.; float xd = mod(x/len-1.,3.); float yd = mod(y/len-1.,3.); if (xd < 1. && yd < 1.) { pct = (smoothstep(0.,thresh,xd) - smoothstep(1.-thresh,1.,xd)) * (smoothstep(0.,thresh,yd) - smoothstep(1.-thresh,1.,yd)); if (i>iter) { pct *= (1.-(i-iter)); // fade in partial iterations } break; } } vec3 color = palette(i); // alternately, an algorithmic palette (needs some work): // color = vec3(-cos(i)/3. + 0.34, -sin(i*3.)/2.+0.5, sin(i)/2.+0.5); return mix(vec3(0,0,0), color, pct); } void main() { float speed = 0.25; float zRange = 0.92; float zMin = 0.025; float z = (1.+cos(time*speed))/2. * zRange + zMin; // for testing formulas at the edges of the zoom range: // z = zRange + zMin; // z = zMin; float zoom = 5.*(pow(z,3.)); vec2 offset = vec2(0.5,0.3); vec3 color = vec3(0,0,0); float samples = 0.; float subpixel = 1./float(QUALITY); for (float x=0.; x<1.; x+=subpixel) { for (float y=0.; y<1.; y+=subpixel) { vec2 fragCoord = gl_FragCoord.xy + vec2(x,y); vec2 coord = (2.*fragCoord - canvasSize)/min(canvasSize.x, canvasSize.y); vec2 p = coord * zoom + offset; // Aliasing is horrible when hard-coding iteration values > ~10 (depending on resolution) // We need to adjust the iterations depending on the resolution, zoom factor, and antialias setting: // TODO: This should be based on zoom, not z-zMin float iterations = 14. - (12. + canvasSize.x/2000.)*pow(z-zMin,0.25) + QUALITY/2.; // old formula (aa == true|false) // float iterations = 16.-(12.25+1000./(canvasSize.x))*pow(z-zMin,0.25) - (aa ? 0. : 0.5); color += draw(p, -1., 1., iterations); samples++; } } fragColor = vec4(color/samples, 1); } </script> <script> const gl = canvas.getContext("webgl2"); if (!gl) { document.body.innerHTML = '<h2>Error: WebGL2 is <a href="https://get.webgl.org/webgl2/">not supported by your browser</a></h2>'; throw "WebGL2 not supported"; } function createShader(shaderType, sourceCode) { const shader = gl.createShader(shaderType); gl.shaderSource(shader, sourceCode); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw gl.getShaderInfoLog(shader); return shader; } const program = gl.createProgram(); gl.attachShader(program, createShader(gl.VERTEX_SHADER, vertexShader.textContent.trim())); gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, fragmentShader.textContent.trim())); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw gl.getProgramInfoLog(program); gl.useProgram(program); const vertices = [[-1, -1], [1, -1], [-1, 1], [1, 1]]; gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices.flat()), gl.STATIC_DRAW); const vertexPosition = gl.getAttribLocation(program, "vertexPosition"); gl.enableVertexAttribArray(vertexPosition); gl.vertexAttribPointer(vertexPosition, 2, gl.FLOAT, false, 0, 0); const canvasSizeUniform = gl.getUniformLocation(program, 'canvasSize'); const timeUniform = gl.getUniformLocation(program, 'time'); function draw() { const width = canvas.clientWidth; const height = canvas.clientHeight; canvas.width = width; canvas.height = height; gl.viewport(0, 0, width, height); gl.uniform2f(canvasSizeUniform, width, height); gl.uniform1f(timeUniform, performance.now() / 1000); gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length); requestAnimationFrame(draw); } draw(); </script> </body> </html>