<!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>