<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="pretty-atom-feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Blog Title</title>
  <subtitle>This is a longer description about your blog.</subtitle>
  <link href="https://example.com/feed/feed.xml" rel="self" />
  <link href="https://example.com/" />
  <updated>2026-03-17T00:00:00Z</updated>
  <id>https://example.com/</id>
  <author>
    <name>Your Name</name>
  </author>
  <entry>
    <title>Rebuilding My Site (Again) With 11ty</title>
    <link href="https://example.com/blog/rebuild/" />
    <updated>2026-03-17T00:00:00Z</updated>
    <id>https://example.com/blog/rebuild/</id>
    <content type="html">&lt;p&gt;I&#39;ve lost count of how many times I&#39;ve rebuilt my personal blog. First it was
plain HTML/CSS. Then React and Tailwind, after a full-stack role turned me down
for not having any React projects on my GitHub. Then my first 11ty site, with
some Three.js demos. Then I tried building my own static site generator, first
with Next.js, then in plain JS because the Next version got too complex. Don&#39;t
ask me why I thought ditching the framework would make things simpler...&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;After all of that, I developed a pretty clear picture of what I actually want.
Content that lives separately from the site plumbing, so I&#39;m not rewriting posts
every time I rework the internals. Something simple enough that I can come back
after six months away and immediately understand how the pieces fit together.
Modular, easy to extend, and able to handle everything from plain blog posts to
live graphics demos without feeling like a hack.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;11ty hits all of those marks. Your filesystem is your sitemap; the directory
structure doubles as site configuration, readable at a glance. The data
cascade lets you attach metadata and settings to entire branches of that
structure, giving you a lot of flexibility without a lot of ceremony. And the
template-centric design keeps content cleanly separated from implementation, so
the two can evolve independently.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Water Effect</title>
    <link href="https://example.com/blog/water/" />
    <updated>2025-11-10T00:00:00Z</updated>
    <id>https://example.com/blog/water/</id>
    <content type="html">&lt;iframe src=&quot;https://example.com/demos/water&quot; title=&quot;Water Shader Demo&quot;&gt; &lt;/iframe&gt;
&lt;p&gt;&lt;a href=&quot;https://example.com/demos/water&quot;&gt;Fullscreen&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;explanation&quot;&gt;Explanation&lt;/h2&gt;
&lt;p&gt;Going into this I had no idea how to write a water shader but once I started looking into it I learned that you can make
pretty decent looking water by just layering on a few relatively simple effects. This shader consists of scrolling two normal
maps at different rates, adding in Blinn-Phong lighting and fresnel shading, and running a grid-based finite-difference Laplacian
simulation for the mouse-based ripples. I also added a skybox as the semi-transparent water against a black background looked
a bit ugly, and then I had the idea to add skybox reflections which turned out to be surprisingly easy and really helped sell
the effect. The geometry itself doesn&#39;t actually change at all except for slight sine-based undulations.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;To start with, I am scrolling two water normal maps at different rates and directions to give the appearance of an uneven
rippled surface. A normal is sampled from each of these and the two are combined. This normal is then combined again with the
normal sampled from the data texture for the ripple map. This is how I am creating the appearance of
distortions on the water&#39;s surface without actually modifying the geometry at all. Then I am doing
Blinn-Phong lighting followed by fresnel shading to make the water shine a bit and get the edges of the ripples to pop. This
amounts to performing some more vector math with the previously computed normal vector, the camera vector and the world vector to
compute color adjustments for the current fragment. After that comes the skybox reflection and refraction which involve
sampling the skybox texture at a point computed from the normal vector, camera vector and world vector to compute more color
adjustments which are blended with those computed previously to determine the final color of the fragment. It amazes me how many
effects boil down to just some simple vector math. You can read more
about fresnel shading as well as environmental reflections and refractions &lt;a href=&quot;https://developer.download.nvidia.com/CgTutorial/cg_tutorial_chapter07.html&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;That covers how the water shader works. The ripple simulation is a grid-based finite-difference Laplacian where the ripples
are produced by adding a gaussian source at the desired location in the grid. You can read more about finite-difference
Laplacian approximation &lt;a href=&quot;https://gpuopen.com/learn/amd-lab-notes/amd-lab-notes-finite-difference-docs-laplacian_part1/&quot;&gt;here&lt;/a&gt;.
If you want to learn more about water simulations check out &lt;a href=&quot;https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models&quot;&gt;this&lt;/a&gt; and &lt;a href=&quot;https://developer.nvidia.com/gpugems/gpugems2/part-ii-shading-lighting-and-shadows/chapter-18-using-vertex-texture-displacement&quot;&gt;this&lt;/a&gt;.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;source&quot;&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import * as THREE from &amp;quot;three&amp;quot;;

const SIZE = 512;
let camera, scene, renderer;
let plane, waterMaterial;
const pendingRipples = [];
const raycaster = new THREE.Raycaster();
const pointerNDC = new THREE.Vector2();
let lastPointerRipple = 0;
const RIPPLE_INTERVAL_MS = 50;

// ---- wave simulation ----

const waveTexParams = {
	type: THREE.FloatType,
	minFilter: THREE.LinearFilter,
	magFilter: THREE.LinearFilter,
	wrapS: THREE.ClampToEdgeWrapping,
	wrapT: THREE.ClampToEdgeWrapping,
	depthBuffer: false,
	stencilBuffer: false,
};
let hPrev = new THREE.WebGLRenderTarget(SIZE, SIZE, waveTexParams);
let hCurr = new THREE.WebGLRenderTarget(SIZE, SIZE, waveTexParams);
let hNext = new THREE.WebGLRenderTarget(SIZE, SIZE, waveTexParams);

const simCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const simScene = new THREE.Scene();
const simMat = new THREE.ShaderMaterial({
	uniforms: {
		uHPrev: { value: hPrev.texture },
		uHCurr: { value: hCurr.texture },
		uTexel: { value: new THREE.Vector2(1 / SIZE, 1 / SIZE) },
		uC2Dt2: { value: 0.04 },
		uDamping: { value: 0.02 },

		uSrcCount: { value: 0 },
		uSrcUV: {
			value: Array(8)
				.fill(0)
				.map(() =&amp;gt; new THREE.Vector2()),
		},
		uSrcAmp: { value: new Float32Array(8) },
		uSrcSigma: { value: new Float32Array(8) },
	},
	vertexShader: `
    varying vec2 vUv;
	void main() {
		vUv=uv;
		gl_Position=vec4(position.xy,0.0,1.0);
	}
    `,
	fragmentShader: `
    precision highp float;
    uniform sampler2D uHPrev, uHCurr;
    uniform vec2  uTexel;
    uniform float uC2Dt2, uDamping;

    const int MAX_SRC = 8;
    uniform int   uSrcCount;
    uniform vec2  uSrcUV[MAX_SRC];
    uniform float uSrcAmp[MAX_SRC];
    uniform float uSrcSigma[MAX_SRC];

    varying vec2 vUv;
    void main(){
      float hC = texture(uHCurr, vUv).r;
      float hL = texture(uHCurr, vUv - vec2(uTexel.x,0.)).r;
      float hR = texture(uHCurr, vUv + vec2(uTexel.x,0.)).r;
      float hD = texture(uHCurr, vUv - vec2(0.,uTexel.y)).r;
      float hU = texture(uHCurr, vUv + vec2(0.,uTexel.y)).r;

      float lap = (hL + hR + hU + hD - 4.0*hC);
      float hP  = texture(uHPrev, vUv).r;
      float next = (2.0 - uDamping)*hC - (1.0 - uDamping)*hP + uC2Dt2 * lap;

      for (int i=0;i&amp;lt;MAX_SRC;i++){
        if (i&amp;gt;=uSrcCount) break;
        vec2 d = vUv - uSrcUV[i];
        float r2 = dot(d,d);
        float s2 = uSrcSigma[i]*uSrcSigma[i];
        float g = exp(-r2/(2.0*s2));
        next += uSrcAmp[i]*g;
      }

      gl_FragColor = vec4(next,0.,0.,1.);
    }
  `,
	depthTest: false,
	depthWrite: false,
});
simScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), simMat));

// ---- per-frame sim step ----
function stepRipples() {
	if (!renderer) return;
	simMat.uniforms.uHPrev.value = hPrev.texture;
	simMat.uniforms.uHCurr.value = hCurr.texture;
	renderer.setRenderTarget(hNext);
	renderer.render(simScene, simCam);
	renderer.setRenderTarget(null);

	[hPrev, hCurr, hNext] = [hCurr, hNext, hPrev];

	if (waterMaterial) {
		waterMaterial.uniforms.uRippleMap.value = hCurr.texture;
	}

	simMat.uniforms.uSrcCount.value = 0;
}

function queueRipple(uv, amp = 0.01, sigma = 0.01) {
	pendingRipples.push({
		uv: uv.clone(),
		amp,
		sigma,
	});
}

function uploadPendingRipples() {
	if (!pendingRipples.length) {
		simMat.uniforms.uSrcCount.value = 0;
		return;
	}

	const count = Math.min(
		pendingRipples.length,
		simMat.uniforms.uSrcUV.value.length,
	);

	for (let i = 0; i &amp;lt; count; i++) {
		const src = pendingRipples.shift();
		simMat.uniforms.uSrcUV.value[i].copy(src.uv);
		simMat.uniforms.uSrcAmp.value[i] = src.amp;
		simMat.uniforms.uSrcSigma.value[i] = src.sigma;
	}

	simMat.uniforms.uSrcCount.value = count;
}

// ---- render scene ----

init();

function init() {
	console.log(&amp;quot;test&amp;quot;);
	camera = new THREE.PerspectiveCamera(
		70,
		window.innerWidth / window.innerHeight,
		0.1,
		100,
	);
	camera.position.z = 15;
	camera.position.y += 2;

	scene = new THREE.Scene();

	const loader = new THREE.CubeTextureLoader();
	loader.setPath(&amp;quot;/textures/envmap_miramar/&amp;quot;);
	const textureSkyBox = loader.load([
		&amp;quot;miramar_lf.png&amp;quot;,
		&amp;quot;miramar_rt.png&amp;quot;,
		&amp;quot;miramar_up.png&amp;quot;,
		&amp;quot;miramar_dn.png&amp;quot;,
		&amp;quot;miramar_ft.png&amp;quot;,
		&amp;quot;miramar_bk.png&amp;quot;,
	]);
	scene.background = textureSkyBox;
	const textureLoader = new THREE.TextureLoader();
	const normalMap1 = textureLoader.load(&amp;quot;/textures/waternormals1.jpg&amp;quot;);
	normalMap1.wrapS = normalMap1.wrapT = THREE.RepeatWrapping;
	const normalMap2 = textureLoader.load(&amp;quot;/textures/waternormals2.jpg&amp;quot;);
	normalMap2.wrapS = normalMap2.wrapT = THREE.RepeatWrapping;

	waterMaterial = new THREE.ShaderMaterial({
		uniforms: {
			uTime: { value: 0 },
			uNormalMap1: { value: normalMap1 },
			uNormalMap2: { value: normalMap2 },
			uFlowDir1: { value: new THREE.Vector2(1.0, 0.25).normalize() },
			uFlowDir2: { value: new THREE.Vector2(-0.35, 1.0).normalize() },
			uFlowSpeed1: { value: 0.05 },
			uFlowSpeed2: { value: -0.035 },
			uScale1: { value: 4.0 },
			uScale2: { value: 8.0 },
			uTintDeep: { value: new THREE.Color(0xb8cfe0) },
			uTintShallow: { value: new THREE.Color(0xe3f2ff) },
			uOpacity: { value: 0.6 },
			uLightDir: { value: new THREE.Vector3(0.3, 1.0, 0.2).normalize() },
			uWaveAmp: { value: 0.2 },
			uWaveFreq: { value: new THREE.Vector2(0.25, 0.15) },
			uWaveSpeed: { value: new THREE.Vector2(0.6, 0.45) },
			uCamPos: { value: new THREE.Vector3() },
			uFresnelBias: { value: 0.08 },
			uFresnelPower: { value: 4.0 },
			uSpecColor: { value: new THREE.Color(0xf5f9ff) },
			uSpecStrength: { value: 0.8 },
			uShininess: { value: 32.0 },
			uEnvMap: { value: textureSkyBox },
			uRefractionRatio: { value: 0.75 },
			uEnvBlend: { value: 0.6 },
			uRippleMap: { value: hCurr.texture },
			uRippleTexel: { value: new THREE.Vector2(1 / SIZE, 1 / SIZE) },
			uRippleNormalStrength: { value: 30.0 },
			uRippleNormalMix: { value: 0.55 },
			uRippleTintStrength: { value: 0.12 },
		},
		vertexShader: `
		varying vec2 vUv;
		varying vec3 vWorldPos;
		varying vec3 vT;
		varying vec3 vB;
		varying vec3 vN;
		uniform float uTime;
		uniform float uWaveAmp;
		uniform vec2 uWaveFreq;
		uniform vec2 uWaveSpeed;
		void main(){
			vUv = uv;
			float localX = position.x;
			float localY = position.y;
			vec3 displaced = position;
			float waveA = sin(localX * uWaveFreq.x + uTime * uWaveSpeed.x);
			float waveB = cos(localY * uWaveFreq.y + uTime * uWaveSpeed.y);
			displaced.z += uWaveAmp * (waveA + waveB);
			vec4 worldPos = modelMatrix * vec4(displaced, 1.0);
			vWorldPos = worldPos.xyz;

			float dHdX = uWaveAmp * uWaveFreq.x * cos(localX * uWaveFreq.x + uTime * uWaveSpeed.x);
			float dHdY = -uWaveAmp * uWaveFreq.y * sin(localY * uWaveFreq.y + uTime * uWaveSpeed.y);
			vec3 normalLocal = normalize(vec3(-dHdX, -dHdY, 1.0));
			vT = normalize(mat3(modelMatrix) * vec3(1.0, 0.0, 0.0));
			vB = normalize(mat3(modelMatrix) * vec3(0.0, 1.0, 0.0));
			vN = normalize(normalMatrix * normalLocal);

			gl_Position = projectionMatrix * viewMatrix * worldPos;
		}
		`,
		fragmentShader: `
		precision highp float;
		varying vec2 vUv;
		varying vec3 vWorldPos;
		varying vec3 vT;
		varying vec3 vB;
		varying vec3 vN;

		uniform sampler2D uNormalMap1;
		uniform sampler2D uNormalMap2;
		uniform vec2 uFlowDir1;
		uniform vec2 uFlowDir2;
		uniform float uFlowSpeed1;
		uniform float uFlowSpeed2;
		uniform float uScale1;
		uniform float uScale2;
		uniform float uTime;
		uniform vec3 uTintDeep;
		uniform vec3 uTintShallow;
		uniform float uOpacity;
		uniform vec3 uLightDir;
		uniform vec3 uCamPos;
		uniform float uFresnelBias;
		uniform float uFresnelPower;
		uniform vec3 uSpecColor;
		uniform float uSpecStrength;
		uniform float uShininess;
		uniform samplerCube uEnvMap;
		uniform float uRefractionRatio;
		uniform float uEnvBlend;
		uniform sampler2D uRippleMap;
		uniform vec2 uRippleTexel;
		uniform float uRippleNormalStrength;
		uniform float uRippleNormalMix;
		uniform float uRippleTintStrength;

		vec3 unpackNormal(vec3 n){
			return normalize(n * 2.0 - 1.0);
		}

		void main(){
			vec2 uv1 = vUv * uScale1 + uFlowDir1 * (uFlowSpeed1 * uTime);
			vec2 uv2 = vUv * uScale2 + uFlowDir2 * (uFlowSpeed2 * uTime);
			vec3 n1 = unpackNormal(texture2D(uNormalMap1, uv1).xyz);
			vec3 n2 = unpackNormal(texture2D(uNormalMap2, uv2).xyz);
			vec3 detailNormal = normalize(n1 + n2);

			float rippleH = texture2D(uRippleMap, vUv).r;
			float rippleHx = texture2D(uRippleMap, vUv + vec2(uRippleTexel.x, 0.0)).r - rippleH;
			float rippleHy = texture2D(uRippleMap, vUv + vec2(0.0, uRippleTexel.y)).r - rippleH;
			vec3 rippleNormal = normalize(vec3(-rippleHx * uRippleNormalStrength, 1.0, -rippleHy * uRippleNormalStrength));
			vec3 nT = normalize(mix(detailNormal, rippleNormal, uRippleNormalMix));
			mat3 TBN = mat3(normalize(vT), normalize(vB), normalize(vN));
			vec3 N = normalize(TBN * nT);

			float light = clamp(dot(N, normalize(uLightDir)), 0.0, 1.0);
			float rippleTint = (rippleH - 0.5) * uRippleTintStrength;
			vec3 base = mix(uTintDeep, uTintShallow, pow(light, 1.5));
			base += vec3(rippleTint);

			vec3 V = normalize(uCamPos - vWorldPos);
			float NoV = max(dot(N, V), 0.0);
			float fresnel = clamp(uFresnelBias + pow(1.0 - NoV, uFresnelPower), 0.0, 1.0);

			vec3 I = normalize(vWorldPos - uCamPos);
			vec3 envRefl = textureCube(uEnvMap, reflect(I, N)).rgb;
			vec3 envRefr = textureCube(uEnvMap, refract(I, N, uRefractionRatio)).rgb;
			vec3 envColor = mix(envRefr, envRefl, fresnel);
			vec3 color = mix(base, envColor, uEnvBlend);

			vec3 L = normalize(uLightDir);
			vec3 H = normalize(L + V);
			float spec = pow(max(dot(N, H), 0.0), uShininess) * uSpecStrength;
			color += uSpecColor * spec;

			gl_FragColor = vec4(color, uOpacity);
		}
		`,
		side: THREE.DoubleSide,
		transparent: true,
	});

	const geometry = new THREE.PlaneGeometry(100, 24);
	plane = new THREE.Mesh(geometry, waterMaterial);
	plane.position.y = -2;
	plane.rotation.x = 1.7;
	scene.add(plane);

	renderer = new THREE.WebGLRenderer({ antialias: true });
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(window.innerWidth, window.innerHeight);
	renderer.setAnimationLoop(animate);
	document.body.appendChild(renderer.domElement);

	window.addEventListener(&amp;quot;resize&amp;quot;, onWindowResize);
	window.addEventListener(&amp;quot;pointermove&amp;quot;, handlePointerMove);
}

function onWindowResize() {
	camera.aspect = window.innerWidth / window.innerHeight;
	camera.updateProjectionMatrix();

	renderer.setSize(window.innerWidth, window.innerHeight);
}

function handlePointerMove(event) {
	if (!plane || !camera) return;
	const now = performance.now();
	if (now - lastPointerRipple &amp;lt; RIPPLE_INTERVAL_MS) {
		return;
	}
	pointerNDC.x = (event.clientX / window.innerWidth) * 2 - 1;
	pointerNDC.y = -(event.clientY / window.innerHeight) * 2 + 1;
	raycaster.setFromCamera(pointerNDC, camera);
	const hits = raycaster.intersectObject(plane);
	if (!hits.length) return;
	lastPointerRipple = now;
	const uv = hits[0].uv.clone();
	uv.x = THREE.MathUtils.clamp(uv.x, 0, 1);
	uv.y = THREE.MathUtils.clamp(uv.y, 0, 1);
	queueRipple(uv, 0.012, 0.02);
}

function animate() {
	if (waterMaterial) {
		const t = performance.now() * 0.001;
		waterMaterial.uniforms.uTime.value = t;
		waterMaterial.uniforms.uCamPos.value.copy(camera.position);
	}

	uploadPendingRipples();
	stepRipples();
	renderer.render(scene, camera);
}

function getIndex(x, y) {
	return y * (SIZE + 1) + x;
}

&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Boids</title>
    <link href="https://example.com/blog/boids/" />
    <updated>2025-02-10T00:00:00Z</updated>
    <id>https://example.com/blog/boids/</id>
    <content type="html">&lt;iframe src=&quot;https://example.com/demos/boids&quot; title=&quot;Boids Demo&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;a href=&quot;https://example.com/demos/boids&quot;&gt;Fullscreen&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;explanation&quot;&gt;Explanation&lt;/h2&gt;
&lt;p&gt;This demo was a little different from the others in that it does not use any custom shaders, the whole thing is made with
ThreeJS primitives. In particular, a buffer geometry and a points material. Boids would be a good candidate problem for
GPU-acceleration via compute shaders, and in fact the first version of this demo I wrote used a ThreeJS plugin to provide the
traditional shoehorned OpenGL compute shader stuff, but I decided to go without it to get rid of the additional dependency.
WebGPU offers built in support for compute shaders, but browser support for WebGPU is still lacking, so I decided to stick
with WebGL/ThreeJS. Hence all the velocity calculations and position updates are being done on the CPU.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;source&quot;&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import * as THREE from &amp;quot;three&amp;quot;;

const WIDTH = 16;
const NUM_BOIDS = WIDTH * WIDTH;
const MAX_SPEED = 0.005;
const EDGE_MIN = 0.01;
const EDGE_MAX = 0.99;
const EDGE_REPULSION = 0.03;
const COHESION = 0.00015;
const ALIGNMENT = 0.0125;
const SEPARATION = 0.05;
const MOUSE_PULL = 0.0001;
const VELOCITY_DAMPING = 0.995;

const clamp01 = (v) =&amp;gt; (v &amp;lt; 0 ? 0 : v &amp;gt; 1 ? 1 : v);

let width = window.innerWidth;
let height = window.innerHeight;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setSize(width, height);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1410);

const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

const geometry = new THREE.BufferGeometry();

// this seems redundant but the ThreeJS points class requires 3D positions in our buffer geometry
// so I have opted to maintain two separate data structures, one for the points in two dimensions
// and one for the points in three dimensions even though the boids are technically only moving in
// two dimensions
const positions = new Float32Array(NUM_BOIDS * 3);
const positionBuffer = new Float32Array(NUM_BOIDS * 2);
const velocities = new Float32Array(NUM_BOIDS * 2);

// randomize positions and velocities
for (let i = 0; i &amp;lt; NUM_BOIDS; i++) {
	const x = Math.random();
	const y = Math.random();
	positionBuffer[i * 2] = x;
	positionBuffer[i * 2 + 1] = y;
	positions[i * 3] = x * 2 - 1;
	positions[i * 3 + 1] = y * 2 - 1;
	positions[i * 3 + 2] = 0;
	velocities[i * 2] = (Math.random() - 0.5) * 2.0 * MAX_SPEED;
	velocities[i * 2 + 1] = (Math.random() - 0.5) * 2.0 * MAX_SPEED;
}

geometry.setAttribute(
	&amp;quot;position&amp;quot;,
	new THREE.BufferAttribute(positions, 3).setUsage(THREE.DynamicDrawUsage),
);

const material = new THREE.PointsMaterial({ color: 0xffffff, size: 2.0 });
const boids = new THREE.Points(geometry, material);
scene.add(boids);

const mouse = { x: 0.5, y: 0.5, active: false };
let lastMouseMove = 0;

const handleResize = () =&amp;gt; {
	width = window.innerWidth;
	height = window.innerHeight;
	renderer.setPixelRatio(window.devicePixelRatio || 1);
	renderer.setSize(width, height);
	camera.updateProjectionMatrix();
};

const handleMouseMove = (event) =&amp;gt; {
	const rect = renderer.domElement.getBoundingClientRect();
	mouse.x = clamp01((event.clientX - rect.left) / rect.width);
	mouse.y = clamp01(1 - (event.clientY - rect.top) / rect.height);
	mouse.active = true;
	lastMouseMove = performance.now();
};

const handleMouseLeave = () =&amp;gt; {
	mouse.active = false;
	mouse.x = 0.5;
	mouse.y = 0.5;
	lastMouseMove = 0;
};

window.addEventListener(&amp;quot;resize&amp;quot;, handleResize);
window.addEventListener(&amp;quot;mousemove&amp;quot;, handleMouseMove);
renderer.domElement.addEventListener(&amp;quot;mouseleave&amp;quot;, handleMouseLeave);

document.body.appendChild(renderer.domElement);

const animate = () =&amp;gt; {
	requestAnimationFrame(animate);

	// iterate over boids...
	for (let i = 0; i &amp;lt; NUM_BOIDS; i++) {
		const idxPos = i * 2;
		const idxVel = i * 2;

		const boidPosX = positionBuffer[idxPos];
		const boidPosY = positionBuffer[idxPos + 1];
		let boidVelX = velocities[idxVel];
		let boidVelY = velocities[idxVel + 1];

		let sumPosX = 0;
		let sumPosY = 0;
		let sumVelX = 0;
		let sumVelY = 0;
		let sepX = 0;
		let sepY = 0;

		// compute the average position and velocity of the flock as well as the deviation for
		// the current boid
		for (let j = 0; j &amp;lt; NUM_BOIDS; j++) {
			if (i === j) continue;
			const nPosX = positionBuffer[j * 2];
			const nPosY = positionBuffer[j * 2 + 1];
			const nVelX = velocities[j * 2];
			const nVelY = velocities[j * 2 + 1];

			sumPosX += nPosX;
			sumPosY += nPosY;
			sumVelX += nVelX;
			sumVelY += nVelY;

			const diffX = nPosX - boidPosX;
			const diffY = nPosY - boidPosY;
			const distanceSq = diffX * diffX + diffY * diffY;
			if (distanceSq &amp;lt; 0.00005 &amp;amp;&amp;amp; distanceSq &amp;gt; 0) {
				sepX -= diffX;
				sepY -= diffY;
			}
		}

		const avgPosX = sumPosX / (NUM_BOIDS - 1);
		const avgPosY = sumPosY / (NUM_BOIDS - 1);
		const avgVelX = sumVelX / (NUM_BOIDS - 1);
		const avgVelY = sumVelY / (NUM_BOIDS - 1);

		// apply the boid rules
		const v1x = (avgPosX - boidPosX) * COHESION;
		const v1y = (avgPosY - boidPosY) * COHESION;
		const v2x = (avgVelX - boidVelX) * ALIGNMENT;
		const v2y = (avgVelY - boidVelY) * ALIGNMENT;
		const v3x = sepX * SEPARATION;
		const v3y = sepY * SEPARATION;

		// add in an adjustment for edge repulsion
		let v4x = 0;
		let v4y = 0;
		if (boidPosX &amp;lt; EDGE_MIN) v4x += (EDGE_MIN - boidPosX) * EDGE_REPULSION;
		if (boidPosX &amp;gt; EDGE_MAX) v4x += (EDGE_MAX - boidPosX) * EDGE_REPULSION;
		if (boidPosY &amp;lt; EDGE_MIN) v4y += (EDGE_MIN - boidPosY) * EDGE_REPULSION;
		if (boidPosY &amp;gt; EDGE_MAX) v4y += (EDGE_MAX - boidPosY) * EDGE_REPULSION;

		// add in an adjustment to follow the mouse
		let v5x = 0;
		let v5y = 0;
		if (mouse.active) {
			// ignore the mouse if it is not present or standing still for &amp;gt;3 secs
			if (lastMouseMove &amp;amp;&amp;amp; performance.now() - lastMouseMove &amp;gt; 3000) {
				mouse.active = false;
				mouse.x = 0.5;
				mouse.y = 0.5;
				lastMouseMove = 0;
			} else {
				const mouseDirX = mouse.x - boidPosX;
				const mouseDirY = mouse.y - boidPosY;
				const mouseMag =
					Math.sqrt(mouseDirX * mouseDirX + mouseDirY * mouseDirY) || 1;
				v5x = (mouseDirX / mouseMag) * MOUSE_PULL;
				v5y = (mouseDirY / mouseMag) * MOUSE_PULL;
			}
		}

		boidVelX = boidVelX * VELOCITY_DAMPING + v1x + v2x + v3x + v4x + v5x;
		boidVelY = boidVelY * VELOCITY_DAMPING + v1y + v2y + v3y + v4y + v5y;

		// clamp max speed to keep the simulation well behaved
		const speed = Math.sqrt(boidVelX * boidVelX + boidVelY * boidVelY);
		if (speed &amp;gt; MAX_SPEED) {
			boidVelX = (boidVelX / speed) * MAX_SPEED;
			boidVelY = (boidVelY / speed) * MAX_SPEED;
		}

		let newPosX = boidPosX + boidVelX;
		let newPosY = boidPosY + boidVelY;

		if (newPosX &amp;lt; 0) newPosX = 0;
		if (newPosX &amp;gt; 1) newPosX = 1;
		if (newPosY &amp;lt; 0) newPosY = 0;
		if (newPosY &amp;gt; 1) newPosY = 1;

		positionBuffer[idxPos] = newPosX;
		positionBuffer[idxPos + 1] = newPosY;
		velocities[idxVel] = boidVelX;
		velocities[idxVel + 1] = boidVelY;
		positions[i * 3] = newPosX * 2 - 1;
		positions[i * 3 + 1] = newPosY * 2 - 1;
	}

	geometry.attributes.position.needsUpdate = true;
	renderer.render(scene, camera);
};

animate();

&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Matrix (-like) Effect</title>
    <link href="https://example.com/blog/matrix/" />
    <updated>2025-01-27T00:00:00Z</updated>
    <id>https://example.com/blog/matrix/</id>
    <content type="html">&lt;iframe src=&quot;https://example.com/demos/matrix&quot; title=&quot;Matrix Effect Demo&quot;&gt; &lt;/iframe&gt;
&lt;p&gt;&lt;a href=&quot;https://example.com/demos/matrix&quot;&gt;Fullscreen&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;explanation&quot;&gt;Explanation&lt;/h2&gt;
&lt;p&gt;During the winter of 2024 I was applying to jobs, checking out the websites of various companies and I came across one that
had this cool animation on the homepage of these walls of binary data stretching into the distance. I don&#39;t even remember the
company, I think it was some local software house, but I still remember the effect. It was pretty slick. I guess that&#39;s the
kind of stuff you come up with when you bill by the hour. Seeing that made me want to try my hand at doing some kind of
matrix-inspired shader effect, so this is what I came up with.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;I found a bitmap of some 8 by 8 font from a Hitachi LCD display and converted it to a base64 string that ThreeJS could load as
a data texture that way I could bundle it in with the script itself. Used some algebra to come up with grid coordinates for
each fragment based on the UV coordinate and assign each one a random (ish) character from the bitmap which is sampled
according to the fragment&#39;s position relative to its grid cell. Lots of modulus operations. The random character
selection is also made a function of some truncated portion of the current time to get the characters to update with time. I
also gave each grid character a random amount of variability which is why some flicker between many different characters and
others stay more or less the same. Then I layered on some undulating per-character brightnesses and some vertically scrolling
scanlines with sine and cosine operations. The result is something kind of lo-fi, not quite as nice as the thing that inspired
it but I kind of like it. I made it amber in color because I love those old amber displays like in the Compaq Portable 3 and
the Grid Compass.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;source&quot;&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import * as THREE from &amp;quot;three&amp;quot;;

const vertexShader = `
varying vec2 vUv;
varying vec3 vPosition;

void main()	{
    vUv = uv;
    vPosition = position;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragmentShader = `
varying vec2 vUv;
varying vec3 vPosition;

uniform float time;
uniform sampler2D font;

float random(vec2 st) {
  return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}

void main()	{

  vec2 uv = vUv;

  // dilate the texture coordinates a bit
  uv.xy *= 6.7;

  // scroll up with time and be sure to handle the wrap around case
  uv.y += 0.1 * time;

  // Create a perspective effect using vPosition
  float depth = abs(vPosition.z);
  uv /= depth * 0.1 + 1.0; // Compress UVs with distance for a tunnel effect

  float truncated_time = time - fract(time);
  float fps = 0.1;
  truncated_time = time - fract(time * fps)/fps;

  // compute the block the current uv value lies in
  vec2 block = floor(uv.xy * 16.0);

  // select random character
  float rindex = floor(random(block + fps) * 224.0 + 15.0); // Character index (0 to 255)

  // random choice between the selected character and a completely random character
  float threshold = random(block + truncated_time * 0.02) * 0.6;
  float outcome = random(block + time * 0.02);
  float choice = floor(outcome * 224.0 + 15.0);

  if (outcome &amp;gt; threshold + 0.4) {
    rindex = choice;
  }

  // convert selection to indices
  float col = mod(rindex, 16.0); // Column (0 to 15)
  float row = floor(rindex * 0.0625); // Row (0 to 15)

  // compute base uv offset for selected character
  vec2 rindex_uv = vec2(col, row) * 0.0625;

  // compute the uv offset of the current fragment
  vec2 frag_uv = fract(uv * 16.0) * 0.0625;

  // compute translated uv
  vec2 trans_uv = rindex_uv + frag_uv;

  // text color
  vec4 text_color = vec4(0.9, 0.6, 0.0, 1.0);

  // sample the bitmap
  vec4 color = 0.95 * texture(font, fract(trans_uv)) * text_color;
  vec4 scanlines = vec4(vec3(0.1*sin(uv.y * 3.14 * 50.0 + time * 10.2)), 0.0);
  color = color * vec4(vec3(abs(sin(random(block) * 6.28 + 3.14 * time))), 1.0) + scanlines;
  gl_FragColor = color;
}
`;

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
	75,
	window.innerWidth / window.innerHeight,
	0.1,
	1000,
);
camera.position.z = -5;
camera.fov = 75;

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

renderer.debug.onShaderError = (gl, program, vertexShader, fragmentShader) =&amp;gt; {
	const vertexShaderSource = gl.getShaderSource(vertexShader);
	const fragmentShaderSource = gl.getShaderSource(fragmentShader);

	console.groupCollapsed(&amp;quot;vertexShader&amp;quot;);
	console.log(vertexShaderSource);
	console.groupEnd();

	console.groupCollapsed(&amp;quot;fragmentShader&amp;quot;);
	console.log(fragmentShaderSource);
	console.groupEnd();
};

// Uniforms
const uniforms = {
	time: { value: 0 },
	font: {
		value: new THREE.TextureLoader().load(
			&amp;quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAQAAAADrRVxmAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAACYktHRAAAqo0jMgAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+gMBgcwBAs8Zf0AAASwSURBVEjHnZU9aBxHFIDfTQYyhCN6HFcsRsjDMcVgTFiCMEMYjmEYxHGIIMQKhElhjAoVKVSEcMUwnI4QVIYrUwYX4epUKoJIgjEhlQsXqdKlM0cIKUKYzZvT2YolkcKPY5f97s17b9/fArRL2FguNzaW7Qa0LcDbgOXGcqOF9vX9bUAXoEKyxTkkQ3foAShxCWQAhDv3j+4r0RsO7fEAZ9v076Daj1WvGY3vjcVMZBh9iL2RVn0cj8cYyNhot1Yj/ZHqW/tQ6hph20leB7cjEFV6uQu3CH76nvB1TRolGkZADYdPRoiiWYVHYNQfjfak5KYAChGhb1GB5Jgh01MBPdVA0agBZDGb4vgMig2E2hUNjV98DsWLhHp6MwyL1vJkk7XFp7Tga++7zjPvq0oAethBxvrR8QIwCQYPsbupfBR2WHzyLjzGmgXmhWcrDQSPzifvkY4IUQK1Jlluiy9A8oI34zAYNycpbXrDvJv5CoL2g+DcwCML7uljBV9r77xzjsDILfY9LEJ0PsaPE7I9txh7+C6uNMgxO3bzHQ9kaG3DBzf3CsyseHHkxfpUvNyQWEWb/YwkmVAS71V5kdk8BCaD0AS8d2RvrjWXujRO9NESWFDpZCzAjUcMvV5oSjqugApkQ4eAcq1xx1MyMMwwGTRUzBvSgqN6dMGR0foKIF3X+WvNodk2xh6iUHh8aKDlR2E3YDhCobE5CtDa4EJEG1DU2AQCgcQhDzjSl8BOjKUIAtaKgCkD8j+C68LJ9U1Q7DCxh3ZSDFM5ODYAIRw5GUKjtci5HJlYZ2TQDWp0vHS3446rXa015UOYSzCptlGHR/HKyzXXuL5SmvEa4PIKmAKYKwm6u1zebbFdtnfbTml9XNkmjZXmtPMK4CVw7BqQ/BqgYkoQlTGGxp7IAqelKipEY16apoID42AKzM/yi5hj7OdFLKF1Tme5SNPPB5kMnXagaMQ8r/9aZGoIz4AKZcyLfPLgINPsKw7FC5r5oweLc9oOlbjKxcHqulWm/Xp12HUwjBxBSJisQc36IppojWguU1bzqgpYHQlDoHQsLYtj+qEQq5WzJBUp6lhhnNS3FCYxXK0XB8qB9BQCT5BT3EopyygHMO0LWhY9HIyY8jwPIFUVw6qH/oypgRiU1XfMUfcSF4xzocrqEwKfDejChNiWorRanasdUctUn/ywdUsc2IVIcxFpvazWHmydfEODEnnk2iVJG3OA+1lm2BZc7bC9THss75duETQ/E3ZGBzlyVAjUiN5x3ujyF3VoVs+ld4LNSyZqKR1c4MVsWEMtbsZxgtxSyW2gnIPbQ+iS0Q8mifaZTElqDVJ+hn8Ilgh4X1qukiIpMWRBDzxbFXDUpyanXNyjhPFiNCjETLnY1b5UBbp/X7yPkOr6rKaE3DaqQj9zw4SXktHCP/qTRIuwl6nGm7Os4Nvw61dD3/yJOSKe60PI3x/HQTosbacjLS940v95b+ixaTKBcxxCVj9l7/V83mCAmX4O/V8O8cv0tMacZ4v8dOtmHNOL3zrvvNuuPt+d6Wm7Bpffc+Z+TK/AavVw+bt9DVavgW31BkDxJmglf+NI2zr2H6P03E47p+2/NC6r6gHyRisAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjQtMTItMDZUMDU6NDQ6MDMrMDA6MDCHitvXAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI0LTEyLTA0VDIwOjA1OjAyKzAwOjAwdFoivgAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNC0xMi0wNlQwNzo0ODowNCswMDowMB4FKf8AAAAASUVORK5CYII=&amp;quot;,
		),
	},
};

// Geometry
const planeGeometry = new THREE.PlaneGeometry(100, 100);
const shaderMaterial = new THREE.ShaderMaterial({
	vertexShader,
	fragmentShader,
	uniforms,
});

const leftPlane = new THREE.Mesh(planeGeometry, shaderMaterial);
leftPlane.position.set(-20, 0, -20);
leftPlane.rotation.y = Math.PI / 2.5;

const rightPlane = new THREE.Mesh(planeGeometry, shaderMaterial);
rightPlane.position.set(20, 0, -20);
rightPlane.rotation.y = -Math.PI / 2.5;

scene.add(leftPlane);
scene.add(rightPlane);

// Animation loop
const clock = new THREE.Clock();
const animate = () =&amp;gt; {
	requestAnimationFrame(animate);
	uniforms.time.value = clock.getElapsedTime();
	renderer.render(scene, camera);
};

animate();

// Resize handling
const onResize = () =&amp;gt; {
	const { innerWidth, innerHeight } = window;
	camera.aspect = innerWidth / innerHeight;
	camera.updateProjectionMatrix();
	renderer.setSize(innerWidth, innerHeight);
};

window.addEventListener(&amp;quot;resize&amp;quot;, onResize);
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Conway&#39;s Game of Life</title>
    <link href="https://example.com/blog/life/" />
    <updated>2024-11-25T00:00:00Z</updated>
    <id>https://example.com/blog/life/</id>
    <content type="html">&lt;iframe src=&quot;https://example.com/demos/life&quot; title=&quot;Conway&#39;s Game of Life Demo&quot;&gt; &lt;/iframe&gt;
&lt;p&gt;Hold down the mouse and drag it across to paint randomness on the game world.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://example.com/demos/life&quot;&gt;Fullscreen&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;explanation&quot;&gt;Explanation&lt;/h2&gt;
&lt;p&gt;Whenever I learn a new graphics API I always end up implementing two programs first: the Mandelbrot
set and Conway&#39;s Game of Life. &lt;a href=&quot;https://example.com/blog/mandelbrot&quot;&gt;I already did the Mandelbrot set&lt;/a&gt; so the
game of life was next. I am just using a ThreeJS data texture to represent the game world which I am
periodically updating. Everything is done on the CPU. One issue I ran into was the pixels were not perfectly
square and uniform, that stumped me for a bit but it turned out I just needed to turn off mipmaps.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;source&quot;&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import * as THREE from &amp;quot;three&amp;quot;;

const CELL_PIXELS = 4;
const BRUSH_RADIUS = 10;
const computeSpawnRadius = (rows, cols) =&amp;gt;
	Math.max(4, Math.min(150, Math.floor(Math.min(rows, cols) / 3)));

const populateGrid = (grid, radius, rowNum, colNum) =&amp;gt; {
	const gridcp = grid.slice();
	for (let i = -radius; i &amp;lt;= radius; i++) {
		for (let j = -radius; j &amp;lt;= radius; j++) {
			if (Math.sqrt(i * i + j * j) &amp;lt;= radius) {
				const indx = (Math.floor(colNum / 2) + i + colNum) % colNum;
				const indy = (Math.floor(rowNum / 2) + j + rowNum) % rowNum;
				gridcp[indx + indy * colNum] = Math.random() &amp;gt; 0.5 ? 255 : 0;
			}
		}
	}
	return gridcp;
};

const nextGen = (rows, cols, currentGrid) =&amp;gt; {
	return currentGrid.map((cell, idx) =&amp;gt; {
		const row = Math.floor(idx / cols);
		const col = idx % cols;

		const neighbors = [
			currentGrid[((row - 1 + rows) % rows) * cols + ((col - 1 + cols) % cols)],
			currentGrid[((row - 1 + rows) % rows) * cols + col],
			currentGrid[((row - 1 + rows) % rows) * cols + ((col + 1) % cols)],
			currentGrid[row * cols + ((col - 1 + cols) % cols)],
			currentGrid[row * cols + ((col + 1) % cols)],
			currentGrid[((row + 1) % rows) * cols + ((col - 1 + cols) % cols)],
			currentGrid[((row + 1) % rows) * cols + col],
			currentGrid[((row + 1) % rows) * cols + ((col + 1) % cols)],
		];

		const aliveNeighbors = neighbors.filter((n) =&amp;gt; n === 255).length;

		if (cell === 255 &amp;amp;&amp;amp; (aliveNeighbors &amp;lt; 2 || aliveNeighbors &amp;gt; 3)) {
			return 0;
		}
		if (cell === 0 &amp;amp;&amp;amp; aliveNeighbors === 3) {
			return 255;
		}
		return cell;
	});
};

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const renderer = new THREE.WebGLRenderer({ antialias: false });
let texture;
let material;
let grid = new Uint8Array(0);
let width = 0;
let height = 0;
let aspect = 1;
let rowNum = 0;
let colNum = 0;

const interval = 0.033;
let mousePressed = false;
let mousePosX = null;
let mousePosY = null;

const syncDimensions = (seed = false) =&amp;gt; {
	const pixelRatio = window.devicePixelRatio || 1;
	renderer.setPixelRatio(pixelRatio);
	renderer.setSize(window.innerWidth, window.innerHeight);

	let bufferWidth = renderer.domElement.width;
	let bufferHeight = renderer.domElement.height;

	const newCol = Math.max(1, Math.floor(bufferWidth / CELL_PIXELS));
	const newRow = Math.max(1, Math.floor(bufferHeight / CELL_PIXELS));

	bufferWidth = newCol * CELL_PIXELS;
	bufferHeight = newRow * CELL_PIXELS;

	renderer.setSize(bufferWidth / pixelRatio, bufferHeight / pixelRatio, false);

	width = renderer.domElement.clientWidth;
	height = renderer.domElement.clientHeight;
	aspect = width / height;

	const needsSeed =
		seed ||
		newCol !== colNum ||
		newRow !== rowNum ||
		grid.length !== newRow * newCol;

	colNum = newCol;
	rowNum = newRow;

	if (needsSeed) {
		grid = new Uint8Array(rowNum * colNum).fill(0);
		grid.set(
			populateGrid(grid, computeSpawnRadius(rowNum, colNum), rowNum, colNum),
		);
		if (texture) {
			texture.image.data = grid;
			texture.image.width = colNum;
			texture.image.height = rowNum;
			texture.needsUpdate = true;
			if (material) {
				material.needsUpdate = true;
			}
		}
	}
};

syncDimensions(true);

document.body.appendChild(renderer.domElement);

texture = new THREE.DataTexture(
	grid,
	colNum,
	rowNum,
	THREE.RedFormat,
	THREE.UnsignedByteType,
);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.generateMipmaps = false;
texture.needsUpdate = true;

// Geometry
const planeGeometry = new THREE.PlaneGeometry(2, 2);
material = new THREE.MeshBasicMaterial({ map: texture });
const plane = new THREE.Mesh(planeGeometry, material);
scene.add(plane);

// Animation loop
const clock = new THREE.Clock();
let time_passed = 0;

const animate = () =&amp;gt; {
	if (mousePressed &amp;amp;&amp;amp; mousePosX !== null &amp;amp;&amp;amp; mousePosY !== null) {
		const posX = mousePosX;
		const posY = mousePosY;
		const radius = BRUSH_RADIUS;

		const gridcp = grid.slice();
		for (let i = -radius; i &amp;lt;= radius; i++) {
			for (let j = -radius; j &amp;lt;= radius; j++) {
				if (Math.sqrt(i * i + j * j) &amp;lt;= radius) {
					const indx = (posX + i + colNum) % colNum;
					const indy = (posY + j + rowNum) % rowNum;
					gridcp[indx + indy * colNum] = Math.random() &amp;gt; 0.5 ? 255 : 0;
				}
			}
		}

		grid.set(gridcp);
		texture.needsUpdate = true;
		material.needsUpdate = true;
	}

	time_passed += clock.getDelta();
	if (time_passed &amp;gt; interval) {
		grid.set(nextGen(rowNum, colNum, grid));
		texture.needsUpdate = true;
		material.needsUpdate = true;
		time_passed = 0;
	}
	requestAnimationFrame(animate);
	renderer.render(scene, camera);
};

animate();

// Event handlers
const onResize = () =&amp;gt; {
	syncDimensions(true);
	camera.updateProjectionMatrix();
};

const handleMouseDown = () =&amp;gt; {
	mousePressed = true;
};

const handleMouseUp = () =&amp;gt; {
	mousePressed = false;
};

const handleMouseMove = (event) =&amp;gt; {
	const rect = renderer.domElement.getBoundingClientRect();
	const normX = (event.clientX - rect.left) / rect.width;
	const normY = 1 - (event.clientY - rect.top) / rect.height;

	if (normX &amp;lt; 0 || normX &amp;gt; 1 || normY &amp;lt; 0 || normY &amp;gt; 1) {
		mousePosX = null;
		mousePosY = null;
		return;
	}

	mousePosX = Math.floor(normX * colNum);
	mousePosY = Math.floor(normY * rowNum);
};

const handleMouseLeave = () =&amp;gt; {
	mousePressed = false;
	mousePosX = null;
	mousePosY = null;
};

window.addEventListener(&amp;quot;resize&amp;quot;, onResize);
renderer.domElement.addEventListener(&amp;quot;mouseleave&amp;quot;, handleMouseLeave);
renderer.domElement.addEventListener(&amp;quot;mousedown&amp;quot;, handleMouseDown);
window.addEventListener(&amp;quot;mouseup&amp;quot;, handleMouseUp);
renderer.domElement.addEventListener(&amp;quot;mousemove&amp;quot;, handleMouseMove);
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Mandelbrot Viewer</title>
    <link href="https://example.com/blog/mandelbrot/" />
    <updated>2024-11-17T00:00:00Z</updated>
    <id>https://example.com/blog/mandelbrot/</id>
    <content type="html">&lt;iframe src=&quot;https://example.com/demos/mandelbrot&quot; title=&quot;Mandelbrot Viewer Demo&quot;&gt; &lt;/iframe&gt;
&lt;p&gt;Click and drag to select a region to zoom in on. Right click to zoom back out.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://example.com/demos/mandelbrot&quot;&gt;Fullscreen&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;explanation&quot;&gt;Explanation&lt;/h2&gt;
&lt;p&gt;This was my first time implementing the continuous coloration algorithm for the Mandelbrot
set. I had always been intimidated by this algorithm as the mathematics looked quite
strange at first glance, and complex analysis was not my strongest area. I still do not
fully understand the mathematics involved in deriving the potential function of the
Mandelbrot set itself, but if one simply accepts that the potential function is valid then
the broad strokes of the algorithm are quite simple. In the end, figuring out how to
display a border over the selection when you click and drag the mouse proved to be the more
challenging problem in putting together this demo. Maybe next time I write a Mandelbrot
renderer I will finally get around to learning how to simulate double precision floating
point values with a float vector in order to maximize zoom depth.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Plotting_algorithms_for_the_Mandelbrot_set#Continuous_(smooth)_coloring&quot;&gt;The Wikipedia
page&lt;/a&gt; on plotting algorithms for the Mandelbrot set has a pretty decent explanation of
how the algorithm works.
The potential function gives us a continuous analog to the escape time of a given
point, and by taking the limit as the bailout radius gets very large and rearranging some
terms we can derive an expression for a continuous value that is a function of the selected
point and is within an error of at most 1 of the escape time of that point. This gives us a
way to assign colorings to individual points that behaves much the same as the naive escape
time algorithm with the benefit of being continuous and hence yielding smooth colorations.
The code itself actually does the usual iterative escape time approach with an added step
of computing the continuous error term and adding it to the iteration number, so in
practice it is really just like adding an additional smoothing step to the naive escape
time algorithm.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;source&quot;&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import * as THREE from &amp;quot;three&amp;quot;;

const vertexShader = `
varying vec2 vUv;
varying vec3 vPosition;

void main()	{
    vUv = uv;
    vPosition = position;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragmentShader = `
varying vec2 vUv;
varying vec3 vPosition;

uniform vec2 bot_left;
uniform vec2 top_right;

void main() {
    vec2 dims = top_right.xy - bot_left.xy;
    vec2 coord = bot_left.xy + vUv.xy * dims.xy;

    int iteration = 0;
    int max = 1000;

    float x = 0.0;
    float y = 0.0;

    float val = 0.0;

    vec3 black = vec3(0.0, 0.0, 0.0);
    vec3 blue = vec3(0.0, 0.1, 1.0);
    vec3 orange = vec3(0.9, 0.5, 0.1);
    vec3 white = vec3(1.0, 1.0, 1.0);

    while ((x * x + y * y &amp;lt; float(1 &amp;lt;&amp;lt; 16)) &amp;amp;&amp;amp; (iteration &amp;lt; max)) {
        float xtemp = x * x - y * y + coord.x;
        y = 2.0 * x * y + coord.y;
        x = xtemp;
        iteration = iteration + 1;
    }

    if (iteration &amp;lt; max) {
        float logz_n = log(x * x + y * y) / 2.0;
        float nu = log(logz_n / log(2.0)) / log(2.0);
        val = float(iteration) + 1.0 - nu;
    }

    val = val / 10.0;

    float t1 = mod(floor(val), 4.0);

    float l2 = fract(val);
    float l1 = 1.0 - fract(val);

    vec3 color = vec3(1.0, 1.0, 1.0);

    if (t1 &amp;lt; 1.0) {
        color =  l1 * black + l2 * blue;
    }
    else if (t1 &amp;lt; 2.0) {
        color =  l1 * blue + l2 * white;
    }
    else if (t1 &amp;lt; 3.0) {
        color =  l1 * white + l2 * orange;
    }
    else {
        color = l1 * orange + l2 * black;
    }

    gl_FragColor = vec4(color, 1.0);
}
`;

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

renderer.debug.onShaderError = (gl, program, vertexShader, fragmentShader) =&amp;gt; {
	const vertexShaderSource = gl.getShaderSource(vertexShader);
	const fragmentShaderSource = gl.getShaderSource(fragmentShader);

	console.groupCollapsed(&amp;quot;vertexShader&amp;quot;);
	console.log(vertexShaderSource);
	console.groupEnd();

	console.groupCollapsed(&amp;quot;fragmentShader&amp;quot;);
	console.log(fragmentShaderSource);
	console.groupEnd();
};

// Selection canvas overlay
const selectionCanvas = document.createElement(&amp;quot;canvas&amp;quot;);
selectionCanvas.width = window.innerWidth;
selectionCanvas.height = window.innerHeight;
selectionCanvas.style.position = &amp;quot;absolute&amp;quot;;
selectionCanvas.style.left = &amp;quot;0&amp;quot;;
selectionCanvas.style.top = &amp;quot;0&amp;quot;;
selectionCanvas.style.pointerEvents = &amp;quot;none&amp;quot;;
selectionCanvas.style.zIndex = &amp;quot;10&amp;quot;;
const selectionCtx = selectionCanvas.getContext(&amp;quot;2d&amp;quot;);
document.body.appendChild(selectionCanvas);

let width = window.innerWidth;
let height = window.innerHeight;

// Calculate initial framing based on screen aspect ratio
const calculateInitialView = (screenWidth, screenHeight) =&amp;gt; {
	const screenAspect = screenHeight / screenWidth;

	// Mandelbrot classic view parameters
	const centerX = -0.5;
	const centerY = 0.0;
	const canonicalWidth = 3.5; // Shows from about -2.5 to 1.0
	const canonicalHeight = 2.5; // Shows from about -1.25 to 1.25
	const canonicalAspect = canonicalHeight / canonicalWidth;

	let viewWidth, viewHeight;

	if (screenAspect &amp;gt; canonicalAspect) {
		// Screen is taller than canonical view - fit to width
		viewWidth = canonicalWidth;
		viewHeight = canonicalWidth * screenAspect;
	} else {
		// Screen is wider than canonical view - fit to height
		viewHeight = canonicalHeight;
		viewWidth = canonicalHeight / screenAspect;
	}

	return {
		bot_left: [centerX - viewWidth / 2, centerY - viewHeight / 2],
		top_right: [centerX + viewWidth / 2, centerY + viewHeight / 2],
	};
};

const initialView = calculateInitialView(width, height);

const uniforms = {
	bot_left: { value: initialView.bot_left },
	top_right: { value: initialView.top_right },
};

let state = {
	bot_left: initialView.bot_left,
	top_right: initialView.top_right,
	prev: null,
};

// Geometry
const planeGeometry = new THREE.PlaneGeometry(2, 2);
const shaderMaterial = new THREE.ShaderMaterial({
	vertexShader,
	fragmentShader,
	uniforms,
});

const plane = new THREE.Mesh(planeGeometry, shaderMaterial);
scene.add(plane);

// Mouse state
let mousePos = null;
let mouseDownPos = null;
let mouseUpPos = null;

// Resize handling
const onResize = () =&amp;gt; {
	width = window.innerWidth;
	height = window.innerHeight;
	const newAspect = height / width;

	camera.aspect = newAspect;
	camera.updateProjectionMatrix();
	renderer.setSize(width, height);
	selectionCanvas.width = window.innerWidth;
	selectionCanvas.height = window.innerHeight;

	// Maintain the current center and zoom level but adjust for new aspect
	const currentWidth = uniforms.top_right.value[0] - uniforms.bot_left.value[0];
	const currentHeight =
		uniforms.top_right.value[1] - uniforms.bot_left.value[1];
	const centerX =
		(uniforms.bot_left.value[0] + uniforms.top_right.value[0]) / 2;
	const centerY =
		(uniforms.bot_left.value[1] + uniforms.top_right.value[1]) / 2;

	// Keep width the same, adjust height based on new aspect ratio
	const newHeight = currentWidth * newAspect;

	uniforms.bot_left.value = [
		centerX - currentWidth / 2,
		centerY - newHeight / 2,
	];
	uniforms.top_right.value = [
		centerX + currentWidth / 2,
		centerY + newHeight / 2,
	];

	state.bot_left = uniforms.bot_left.value;
	state.top_right = uniforms.top_right.value;

	renderer.render(scene, camera);
};

window.addEventListener(&amp;quot;resize&amp;quot;, onResize);

const getLocalCoords = (event) =&amp;gt; {
	const rect = renderer.domElement.getBoundingClientRect();
	return {
		x: event.clientX - rect.left,
		y: event.clientY - rect.top,
		width: rect.width,
		height: rect.height,
	};
};

const computeSelection = (start, current, dims) =&amp;gt; {
	const ratio = dims.height / dims.width;
	const rawWidth = current.x - start.x;
	const rawHeight = current.y - start.y;
	if (Math.abs(rawWidth) &amp;lt; 1) {
		return null;
	}

	const signX = rawWidth === 0 ? 1 : Math.sign(rawWidth);
	const signY = rawHeight === 0 ? 1 : Math.sign(rawHeight);
	const widthMag = Math.abs(rawWidth);
	const heightMag = Math.abs(rawHeight);
	const adjustedHeight = widthMag * ratio;

	let rectWidth;
	let rectHeight;
	if (heightMag &amp;gt; adjustedHeight) {
		rectWidth = signX * widthMag;
		rectHeight = signY * adjustedHeight;
	} else {
		const adjustedWidth = heightMag / ratio;
		rectWidth = signX * adjustedWidth;
		rectHeight = signY * heightMag;
	}

	const endX = start.x + rectWidth;
	const endY = start.y + rectHeight;

	return {
		rectWidth,
		rectHeight,
		minX: Math.min(start.x, endX),
		maxX: Math.max(start.x, endX),
		minY: Math.min(start.y, endY),
		maxY: Math.max(start.y, endY),
		width: Math.abs(endX - start.x),
		height: Math.abs(endY - start.y),
	};
};

const handleMouseDown = (event) =&amp;gt; {
	if (event.button !== 0) {
		return;
	}
	const local = getLocalCoords(event);
	mouseDownPos = { x: local.x, y: local.y };
	selectionCtx.clearRect(0, 0, selectionCanvas.width, selectionCanvas.height);
};

const handleMouseUp = (event) =&amp;gt; {
	if (event.button !== 0) {
		return;
	}

	const local = getLocalCoords(event);
	mouseUpPos = { x: local.x, y: local.y };

	selectionCtx.clearRect(0, 0, selectionCanvas.width, selectionCanvas.height);

	if (mouseDownPos &amp;amp;&amp;amp; mouseUpPos) {
		const rect = computeSelection(mouseDownPos, mouseUpPos, { width, height });
		if (!rect) {
			mouseDownPos = null;
			mouseUpPos = null;
			return;
		}

		if (rect.width &amp;lt; 5) {
			mouseDownPos = null;
			mouseUpPos = null;
			return;
		}

		const currentWidth =
			uniforms.top_right.value[0] - uniforms.bot_left.value[0];
		const currentHeight =
			uniforms.top_right.value[1] - uniforms.bot_left.value[1];
		const scaleX = currentWidth / width;
		const scaleY = currentHeight / height;

		const new_bot_left = [
			uniforms.bot_left.value[0] + rect.minX * scaleX,
			uniforms.bot_left.value[1] + (height - rect.maxY) * scaleY,
		];
		const new_top_right = [
			uniforms.bot_left.value[0] + rect.maxX * scaleX,
			uniforms.bot_left.value[1] + (height - rect.minY) * scaleY,
		];

		uniforms.bot_left.value = new_bot_left;
		uniforms.top_right.value = new_top_right;

		state = {
			bot_left: new_bot_left,
			top_right: new_top_right,
			prev: state,
		};

		renderer.render(scene, camera);
	}
	mouseDownPos = null;
	mouseUpPos = null;
};

const handleMouseMove = (event) =&amp;gt; {
	const local = getLocalCoords(event);
	mousePos = {
		x: local.x,
		y: local.y,
	};

	if (mouseDownPos) {
		selectionCtx.clearRect(0, 0, selectionCanvas.width, selectionCanvas.height);

		const rect = computeSelection(
			mouseDownPos,
			{ x: local.x, y: local.y },
			{ width, height },
		);
		if (rect) {
			selectionCtx.strokeStyle = &amp;quot;rgba(255, 255, 255, 0.8)&amp;quot;;
			selectionCtx.lineWidth = 1.5;
			selectionCtx.setLineDash([5, 5]);
			selectionCtx.strokeRect(rect.minX, rect.minY, rect.width, rect.height);
			selectionCtx.setLineDash([]);
		}
	}
};

const handleRightClick = (event) =&amp;gt; {
	event.preventDefault();
	if (state.prev) {
		const prevState = state.prev;
		const prev_bot_left = prevState.bot_left;
		const prev_top_right = prevState.top_right;

		console.log(&amp;quot;(&amp;quot;, prev_bot_left, &amp;quot;, &amp;quot;, prev_top_right, &amp;quot;)&amp;quot;);

		uniforms.bot_left.value = prev_bot_left;
		uniforms.top_right.value = prev_top_right;

		Object.assign(state, prevState);

		renderer.render(scene, camera);
	}
	return false;
};

window.addEventListener(&amp;quot;mousedown&amp;quot;, handleMouseDown);
window.addEventListener(&amp;quot;mouseup&amp;quot;, handleMouseUp);
window.addEventListener(&amp;quot;mousemove&amp;quot;, handleMouseMove);
window.addEventListener(&amp;quot;contextmenu&amp;quot;, handleRightClick);

renderer.render(scene, camera);
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
</feed>