<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Home on spader.zone</title><link>/</link><description>Recent content in Home on spader.zone</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Wed, 25 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="/index.xml" rel="self" type="application/rss+xml"/><item><title>Nice</title><link>/nice/</link><pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate><guid>/nice/</guid><description>&lt;p&gt;I asked my good friend &lt;code&gt;gpt-5.4-medium&lt;/code&gt; to write a little test for &lt;a href="https://github.com/tspader/spn"&gt;my CMake destroyer of a package manager&lt;/a&gt;. It was one step beyond cut-n-paste, and the machine politely obliged me. Test passed. Story&amp;rsquo;s over.&lt;/p&gt;
&lt;p&gt;Wait, no! We must &lt;em&gt;look&lt;/em&gt; at the code produced by the machines, for it is often fraught with the wringing of many hands. And this one confused me; I added globs, so you can do &lt;code&gt;src/*.c&lt;/code&gt;, for example. The fixture it created had two C files in child directories, and then included them in the target by glob.&lt;/p&gt;</description></item><item><title>Franz Kafka's Lost Treatise on Wayland</title><link>/kafka/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>/kafka/</guid><description>&lt;p&gt;One day, I tried to run a Tauri app. After a long period of reflection, I have categorized this broadly as a &lt;em&gt;mistake&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The app crashed on startup with a Wayland error; I found the same error on a GitHub issue and thought, well, someone&amp;rsquo;s gotta fix it. How hard can it be?&lt;/p&gt;
&lt;p&gt;Well. Somewhere between understanding how a fucking button is rendered in a Tauri app on Linux and my third round of selecting all squares which contain a motorcycle to get into the self-hosted GTK GitLab instance, I thought: &lt;em&gt;pretty fucking hard&lt;/em&gt;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;</description></item><item><title>Claude Code's renderer is more complex than a game engine</title><link>/engine/</link><pubDate>Mon, 02 Feb 2026 00:00:00 +0000</pubDate><guid>/engine/</guid><description>&lt;p&gt;One of the maintainers of Claude Code said something extremely funny on the internet.&lt;/p&gt;
&lt;p&gt;&lt;img src="/images/engine-tweet_hu_5e87abfd5a785fdd.webp" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not here to dunk on this person who is clearly earnest and well-intentioned. I have thought and said far worse, far more confidently.&lt;/p&gt;
&lt;p&gt;But&amp;hellip;how silly is it? Clearly, comparing Claude Code to Grand Theft Auto 6 is not fair. The author did specify a &lt;em&gt;small&lt;/em&gt; game engine, and even the most brittle, sun-bleached of straw men would concede that that&amp;rsquo;s not what they meant.&lt;/p&gt;</description></item><item><title>We don't practice fucked</title><link>/pfm/</link><pubDate>Thu, 22 Jan 2026 00:00:00 +0000</pubDate><guid>/pfm/</guid><description>&lt;p&gt;A story. From Ron Jaworski&amp;rsquo;s excellent &lt;a href="https://www.penguinrandomhouse.com/books/86873/the-games-that-changed-the-game-by-ron-jaworski-with-greg-cosell-and-david-plaut/"&gt;The Games That Changed The Game&lt;/a&gt;. Jaworski&amp;rsquo;s in Indianapolis, watching the inimitable &lt;a href="images/pfm.png"&gt;Peyton F. Manning&lt;/a&gt; taking snaps in practice. A lot of snaps. So many snaps that, after a short while, it becomes clear that the Colts do not plan on anybody whose name is &lt;em&gt;not&lt;/em&gt; Peyton Manning having a football hiked to them in the venerable Lucas Oil Stadium, on that night or any other.&lt;/p&gt;</description></item><item><title>Claude Code changed my life</title><link>/xmas/</link><pubDate>Thu, 25 Dec 2025 00:00:00 +0000</pubDate><guid>/xmas/</guid><description>&lt;p&gt;What brand of AI grifter would write such a thing? With paradigm shifts comes money; and with money come the grifters. But lost among the AI grift, wealth extraction, silicon hoarding which can only be described as Smaug-like, and so forth and so forth is something beautiful, plain, earnest:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LLMs have changed my (and probably your) life, foundationally, permanently, and for the better.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;All of this boils down to the fact that while LLMs are merely pretty good at &lt;em&gt;writing&lt;/em&gt; code, they are dear-god-how-did-we-ever-work-before-this good at &lt;em&gt;reading&lt;/em&gt; code. It turns out that having a little fella on your shoulder that can read code an order of magnitude or two faster than you can literally change your life.&lt;/p&gt;</description></item><item><title>Claude Wrapped</title><link>/wrapped/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>/wrapped/</guid><description>&lt;h1 id="-tldr"&gt;&amp;raquo; &lt;em&gt;tldr&lt;/em&gt;&lt;/h1&gt;
&lt;p&gt;Run to compare your &lt;code&gt;Claude Code&lt;/code&gt; usage against the rest of the world while enjoying a spirited holiday Santa Claude rendered in fully lit 3D in your terminal with the power of WASM.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bun x @spader/claude-wrapped
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="-slightly-less-tldr"&gt;&amp;raquo; &lt;em&gt;slightly less tldr&lt;/em&gt;&lt;/h1&gt;
&lt;style&gt;
.wrapped-demo {
 background: #252540;
 border-radius: 8px;
 margin: 20px 0;
}
.wrapped-demo summary {
 padding: 16px;
 cursor: pointer;
 font-weight: bold;
 user-select: none;
 color: #eee;
}
.wrapped-demo summary:hover {
 background: #303050;
 border-radius: 8px;
}
.wrapped-demo-container {
 padding: 16px;
 display: flex;
 flex-direction: column;
 align-items: center;
}
.wrapped-canvas {
 image-rendering: pixelated;
 background: #0a0a12;
 border-radius: 4px;
}
.wrapped-status {
 color: #888;
 font-size: 12px;
 margin-bottom: 8px;
}
.wrapped-status.ready { color: #2ecc71; }
.wrapped-status.error { color: #e74c3c; }
&lt;/style&gt;

&lt;details class="wrapped-demo" id="wrapped-demo"&gt;
 &lt;summary&gt;cpu-destroying wasm claude&lt;/summary&gt;
 &lt;div class="wrapped-demo-container"&gt;
 &lt;div class="wrapped-status" id="wrapped-status"&gt;Click to load...&lt;/div&gt;
 &lt;canvas class="wrapped-canvas" id="wrapped-canvas"&gt;&lt;/canvas&gt;
 &lt;/div&gt;
&lt;/details&gt;

&lt;script&gt;
(function() {
 const WASM_PATH = '/code/renderer.wasm';

 const ShapeType = { SPHERE: 0, BOX: 1, CYLINDER: 2, CONE: 3, CYLINDER_Y: 4 };
 const BlendMode = { HARD: 0, SMOOTH: 1 };

 const CLAUDE_COLOR = [0.85, 0.45, 0.35];
 const SANTA_RED = [0.8, 0.1, 0.1];
 const SANTA_WHITE = [0.85, 0.85, 0.85];
 const EYE_BLACK = [0.05, 0.05, 0.05];

 function getClaudeBoxes(position, scale, group) {
 scale = scale || 1;
 group = group || 0;
 const [px, py, pz] = position;
 const bodyW = 0.7 * scale, bodyH = 0.28 * scale, bodyD = 0.25 * scale;
 const armW = 1.1 * scale, armH = 0.1 * scale, armD = 0.1 * scale;
 const legW = 0.07 * scale, legH = 0.22 * scale, legD = 0.12 * scale;
 const legY = -bodyH / 2 - legH / 2 + 0.04 * scale;
 const legSpacing = 0.14 * scale;
 const legX = bodyW / 2 - 0.12 * scale;
 const eyeSizeX = 0.10 * scale, eyeSizeY = eyeSizeX / 2, eyeSizeZ = eyeSizeX / 2;
 const eyeX = bodyW / 1.66, eyeY = armH / 2;

 return [
 { shape: { type: ShapeType.BOX, params: [bodyW, bodyH, bodyD], color: CLAUDE_COLOR }, position: [px, py, pz], group: group },
 { shape: { type: ShapeType.BOX, params: [armW, armH, armD], color: CLAUDE_COLOR }, position: [px, py, pz], group: group },
 { shape: { type: ShapeType.BOX, params: [legW, legH, legD], color: CLAUDE_COLOR }, position: [px - legX - legSpacing / 2, py + legY, pz], group: group },
 { shape: { type: ShapeType.BOX, params: [legW, legH, legD], color: CLAUDE_COLOR }, position: [px - legX + legSpacing / 2, py + legY, pz], group: group },
 { shape: { type: ShapeType.BOX, params: [legW, legH, legD], color: CLAUDE_COLOR }, position: [px + legX - legSpacing / 2, py + legY, pz], group: group },
 { shape: { type: ShapeType.BOX, params: [legW, legH, legD], color: CLAUDE_COLOR }, position: [px + legX + legSpacing / 2, py + legY, pz], group: group },
 { shape: { type: ShapeType.CYLINDER_Y, params: [0.24 * scale, 0.06 * scale], color: SANTA_WHITE }, position: [px, py + bodyH / 2 + 0.20 * scale, pz], group: group },
 { shape: { type: ShapeType.CONE, params: [0.20 * scale, 0.38 * scale], color: SANTA_RED }, position: [px, py + bodyH / 2 + 0.19 * scale, pz], group: group },
 { shape: { type: ShapeType.SPHERE, params: [0.08 * scale], color: SANTA_WHITE }, position: [px, py + bodyH / 2 + 0.60 * scale, pz], group: group },
 { shape: { type: ShapeType.BOX, params: [eyeSizeX, eyeSizeY, eyeSizeZ], color: EYE_BLACK }, position: [px - eyeX, py + eyeY, pz - bodyD / 2 - eyeSizeX], group: group },
 { shape: { type: ShapeType.BOX, params: [eyeSizeX, eyeSizeY, eyeSizeZ], color: EYE_BLACK }, position: [px + eyeX, py + eyeY, pz - bodyD / 2 - eyeSizeX], group: group },
 ];
 }

 function compileScene(objects, groupDefs, smoothK) {
 const n = objects.length;
 const types = new Uint8Array(n);
 const params = new Float32Array(n * 4);
 const positions = new Float32Array(n * 3);
 const colors = new Float32Array(n * 3);
 const groups = new Uint8Array(n);

 for (let i = 0; i &lt; n; i++) {
 const obj = objects[i];
 types[i] = obj.shape.type;
 groups[i] = obj.group;
 for (let j = 0; j &lt; obj.shape.params.length &amp;&amp; j &lt; 4; j++) {
 params[i * 4 + j] = obj.shape.params[j];
 }
 positions[i * 3] = obj.position[0];
 positions[i * 3 + 1] = obj.position[1];
 positions[i * 3 + 2] = obj.position[2];
 colors[i * 3] = obj.shape.color[0];
 colors[i * 3 + 1] = obj.shape.color[1];
 colors[i * 3 + 2] = obj.shape.color[2];
 }

 const groupBlendModes = new Uint8Array(groupDefs.length);
 for (let g = 0; g &lt; groupDefs.length; g++) {
 groupBlendModes[g] = groupDefs[g].blendMode;
 }

 return { types: types, params: params, positions: positions, colors: colors, groups: groups, groupBlendModes: groupBlendModes, count: n, groupCount: groupDefs.length, smoothK: smoothK };
 }

 function normalize(v) {
 const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
 return len === 0 ? v : [v[0] / len, v[1] / len, v[2] / len];
 }

 function cross(a, b) {
 return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
 }

 function sub(a, b) {
 return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
 }

 function seededRandom(seed) {
 return function() {
 seed = (seed * 1103515245 + 12345) &amp; 0x7fffffff;
 return seed / 0x7fffffff;
 };
 }

 async function loadWasm() {
 const response = await fetch(WASM_PATH);
 const bytes = await response.arrayBuffer();
 const result = await WebAssembly.instantiate(bytes, {});
 const exports = result.instance.exports;
 const memory = exports.memory;
 const maxRays = exports.get_max_rays();
 const maxShapes = exports.get_max_shapes();
 const maxGroups = exports.get_max_groups();
 const maxPointLights = exports.get_max_point_lights();

 return {
 exports: exports,
 maxRays: maxRays,
 maxShapes: maxShapes,
 maxGroups: maxGroups,
 maxPointLights: maxPointLights,
 bgColor: new Float32Array(memory.buffer, exports.get_bg_ptr(), 3),
 shapeTypes: new Uint8Array(memory.buffer, exports.get_shape_types_ptr(), maxShapes),
 shapeParams: new Float32Array(memory.buffer, exports.get_shape_params_ptr(), maxShapes * 4),
 shapePositions: new Float32Array(memory.buffer, exports.get_shape_positions_ptr(), maxShapes * 3),
 shapeColors: new Float32Array(memory.buffer, exports.get_shape_colors_ptr(), maxShapes * 3),
 shapeGroups: new Uint8Array(memory.buffer, exports.get_shape_groups_ptr(), maxShapes),
 groupBlendModes: new Uint8Array(memory.buffer, exports.get_group_blend_modes_ptr(), maxGroups),
 pointLightX: new Float32Array(memory.buffer, exports.get_point_light_x_ptr(), maxPointLights),
 pointLightY: new Float32Array(memory.buffer, exports.get_point_light_y_ptr(), maxPointLights),
 pointLightZ: new Float32Array(memory.buffer, exports.get_point_light_z_ptr(), maxPointLights),
 pointLightR: new Float32Array(memory.buffer, exports.get_point_light_r_ptr(), maxPointLights),
 pointLightG: new Float32Array(memory.buffer, exports.get_point_light_g_ptr(), maxPointLights),
 pointLightB: new Float32Array(memory.buffer, exports.get_point_light_b_ptr(), maxPointLights),
 pointLightIntensity: new Float32Array(memory.buffer, exports.get_point_light_intensity_ptr(), maxPointLights),
 pointLightRadius: new Float32Array(memory.buffer, exports.get_point_light_radius_ptr(), maxPointLights),
 outChar: new Uint32Array(memory.buffer, exports.get_out_char_ptr(), maxRays),
 outFg: new Float32Array(memory.buffer, exports.get_out_fg_ptr(), maxRays * 4),
 outBg: new Float32Array(memory.buffer, exports.get_out_bg_ptr(), maxRays * 4),
 };
 }

 function setupCamera(wasm, eye, at, up, fov, width, height) {
 const forward = normalize(sub(at, eye));
 const right = normalize(cross(forward, up));
 const upVec = cross(right, forward);
 const aspect = width / height;
 const fovRad = (fov * Math.PI) / 180;
 const halfHeight = Math.tan(fovRad / 2);
 const halfWidth = halfHeight * aspect;
 wasm.exports.set_camera(
 eye[0], eye[1], eye[2],
 forward[0], forward[1], forward[2],
 right[0], right[1], right[2],
 upVec[0], upVec[1], upVec[2],
 halfWidth, halfHeight
 );
 }

 function loadScene(wasm, scene) {
 wasm.shapeTypes.set(scene.types);
 wasm.shapeParams.set(scene.params);
 wasm.shapePositions.set(scene.positions);
 wasm.shapeColors.set(scene.colors);
 wasm.shapeGroups.set(scene.groups);
 wasm.groupBlendModes.set(scene.groupBlendModes);
 wasm.exports.set_scene(scene.count, scene.smoothK);
 wasm.exports.set_groups(scene.groupCount);
 }

 let running = false;
 let animationId = null;

 function init() {
 const status = document.getElementById('wrapped-status');
 const canvas = document.getElementById('wrapped-canvas');
 const demo = document.getElementById('wrapped-demo');

 let wasm = null;

 demo.addEventListener('toggle', async function() {
 if (demo.open) {
 if (!wasm) {
 status.textContent = 'Loading WASM...';
 try {
 wasm = await loadWasm();
 status.textContent = 'running';
 status.className = 'wrapped-status ready';
 } catch (e) {
 status.textContent = 'Error: ' + e.message;
 status.className = 'wrapped-status error';
 return;
 }
 } else {
 status.textContent = 'running';
 status.className = 'wrapped-status ready';
 }
 running = true;
 startRenderLoop(wasm, canvas, status);
 } else {
 running = false;
 if (animationId) {
 cancelAnimationFrame(animationId);
 animationId = null;
 }
 status.textContent = 'Paused';
 status.className = 'wrapped-status';
 }
 });
 }

 function startRenderLoop(wasm, canvas, status) {
 const sceneWidth = 64;
 const sceneHeight = 64;
 const charWidth = 6;
 const charHeight = 10;

 canvas.width = (sceneWidth * charWidth);
 canvas.height = (sceneHeight * charHeight);
 canvas.style.width = (sceneWidth * charWidth) / 2 + 'px';
 canvas.style.height = (sceneHeight * charHeight) / 2 + 'px';

 const ctx = canvas.getContext('2d');
 ctx.font = charHeight + 'px monospace';
 ctx.textBaseline = 'top';

 
 let frameCount = 0;
 let lastFpsTime = performance.now();
 let fps = 0;

 const cameraAt = [0.0, 0.0, 0.0];
 const cameraUp = [0.0, 1.0, 0.0];
 const cameraFov = 25;
 const cameraRadius = 5.0;
 const cameraHeight = 1.5;
 const cameraRotationSpeed = 0.3;

 const groupDefs = [{ blendMode: BlendMode.HARD }, { blendMode: BlendMode.HARD }];
 const CLAUDE_GROUP = 0;
 const SNOW_GROUP = 1;

 const ambientIntensity = 0.1;
 const directionalIntensity = 0.3;
 const lightDirection = [0.5, 0.75, -1.0];

 const dramaticLight = { x: 0.0, y: 0.8, z: -0.3, intensity: 2.0 };
 const dramaticLightColor = [0.8, 0.9, 1.0];
 const dramaticLightRadius = 0.5;

 const snowLightIntensity = 2.0;
 const snowLightColor = [0.8, 0.9, 1.0];
 const snowLightRadius = 0.2;

 const smoothK = 0.0;

 const snowParams = {
 count: 30, radius: 0.025, baseSpeed: 0.2, speedJitter: 0.1, driftStrength: 0.3,
 minX: -1.5, maxX: 1.5, minY: -1.0, maxY: 1.0, minZ: -1.0, maxZ: 1.0,
 };

 const rng = seededRandom(123);
 const snowflakes = [];
 const spawnRadius = 1.5;

 for (let i = 0; i &lt; snowParams.count; i++) {
 const angle = rng() * Math.PI * 2;
 const r = Math.sqrt(rng()) * spawnRadius;
 snowflakes.push({
 x: Math.cos(angle) * r,
 y: snowParams.minY + rng() * (snowParams.maxY - snowParams.minY),
 z: Math.sin(angle) * r,
 speed: snowParams.baseSpeed + (rng() - 0.5) * 2 * snowParams.speedJitter,
 driftX: (rng() - 0.5) * 2 * snowParams.driftStrength,
 driftZ: (rng() - 0.5) * 2 * snowParams.driftStrength,
 });
 }

 let lastTime = performance.now();
 let time = 0;

 function toRGB(r, g, b) {
 return 'rgb(' + Math.round(r * 255) + ',' + Math.round(g * 255) + ',' + Math.round(b * 255) + ')';
 }

 function render() {
 const now = performance.now();
 const dt = (now - lastTime) / 1000;
 lastTime = now;
 time += dt;

 for (let i = 0; i &lt; snowflakes.length; i++) {
 const flake = snowflakes[i];
 flake.y -= flake.speed * dt;
 flake.x += flake.driftX * dt;
 flake.z += flake.driftZ * dt;

 if (flake.x &lt; snowParams.minX) flake.x += (snowParams.maxX - snowParams.minX);
 if (flake.x &gt; snowParams.maxX) flake.x -= (snowParams.maxX - snowParams.minX);
 if (flake.z &lt; snowParams.minZ) flake.z += (snowParams.maxZ - snowParams.minZ);
 if (flake.z &gt; snowParams.maxZ) flake.z -= (snowParams.maxZ - snowParams.minZ);
 if (flake.y &lt; snowParams.minY) flake.y = snowParams.maxY;
 }

 const claudeObjects = getClaudeBoxes([0, 0, 0], 1.0, CLAUDE_GROUP);
 const snowObjects = [];
 for (let i = 0; i &lt; snowflakes.length; i++) {
 const flake = snowflakes[i];
 snowObjects.push({
 shape: { type: ShapeType.SPHERE, params: [snowParams.radius], color: [1.0, 1.0, 1.0] },
 position: [flake.x, flake.y, flake.z],
 group: SNOW_GROUP,
 });
 }

 const allObjects = claudeObjects.concat(snowObjects);
 const flatScene = compileScene(allObjects, groupDefs, smoothK);
 loadScene(wasm, flatScene);

 wasm.exports.compute_background(time);

 const cameraAngle = time * cameraRotationSpeed;
 const cameraEye = [
 Math.sin(cameraAngle) * cameraRadius,
 cameraHeight,
 -Math.cos(cameraAngle) * cameraRadius
 ];
 setupCamera(wasm, cameraEye, cameraAt, cameraUp, cameraFov, sceneWidth, sceneHeight);
 wasm.exports.generate_rays(sceneWidth, sceneHeight);

 wasm.exports.set_lighting(ambientIntensity, lightDirection[0], lightDirection[1], lightDirection[2], directionalIntensity);

 wasm.pointLightX[0] = dramaticLight.x;
 wasm.pointLightY[0] = dramaticLight.y;
 wasm.pointLightZ[0] = dramaticLight.z;
 wasm.pointLightR[0] = dramaticLightColor[0];
 wasm.pointLightG[0] = dramaticLightColor[1];
 wasm.pointLightB[0] = dramaticLightColor[2];
 wasm.pointLightIntensity[0] = dramaticLight.intensity;
 wasm.pointLightRadius[0] = dramaticLightRadius;

 const numSnowLights = Math.min(snowflakes.length, 30);
 for (let i = 0; i &lt; numSnowLights; i++) {
 const flake = snowflakes[i];
 const idx = i + 1;
 wasm.pointLightX[idx] = flake.x;
 wasm.pointLightY[idx] = flake.y;
 wasm.pointLightZ[idx] = flake.z;
 wasm.pointLightR[idx] = snowLightColor[0];
 wasm.pointLightG[idx] = snowLightColor[1];
 wasm.pointLightB[idx] = snowLightColor[2];
 wasm.pointLightIntensity[idx] = snowLightIntensity;
 wasm.pointLightRadius[idx] = snowLightRadius;
 }
 wasm.exports.set_point_lights(1 + numSnowLights);

 wasm.exports.march_rays();
 wasm.exports.composite(sceneWidth, sceneHeight);

 
 const bg = wasm.bgColor;
 ctx.fillStyle = toRGB(bg[0], bg[1], bg[2]);
 ctx.fillRect(0, 0, canvas.width, canvas.height);

 
 for (let y = 0; y &lt; sceneHeight; y++) {
 for (let x = 0; x &lt; sceneWidth; x++) {
 const i = y * sceneWidth + x;
 const char = wasm.outChar[i];

 if (char !== 32 &amp;&amp; char !== 0) {
 const r = wasm.outFg[i * 4];
 const g = wasm.outFg[i * 4 + 1];
 const b = wasm.outFg[i * 4 + 2];
 ctx.fillStyle = toRGB(r, g, b);
 ctx.fillText(String.fromCodePoint(char), x * charWidth, y * charHeight);
 }
 }
 }

 
 frameCount++;
 const now2 = performance.now();
 if (now2 - lastFpsTime &gt;= 1000) {
 fps = frameCount;
 frameCount = 0;
 lastFpsTime = now2;
 status.textContent = 'running at ' + fps + ' FPS';
 }

 if (running) {
 animationId = requestAnimationFrame(render);
 }
 }

 render();
 }

 if (document.readyState === 'loading') {
 document.addEventListener('DOMContentLoaded', init);
 } else {
 init();
 }
})();
&lt;/script&gt;

&lt;p&gt;I wrote a raymarcher that can render scenes of SDF functions and lights in plain C, compiled it with WASM, jammed it all into a Bun executable that grabs your &lt;code&gt;~/.claude/stats-cache.json&lt;/code&gt;, uploads it to a SQLite database on The Cloud so you can see how your Claude Code usage stacks up with the rest of the world.&lt;/p&gt;</description></item><item><title>This Ain't Yer Granddaddy's C</title><link>/tricks/</link><pubDate>Wed, 19 Nov 2025 00:00:00 +0000</pubDate><guid>/tricks/</guid><description>&lt;p&gt;I&amp;rsquo;ve dragged myself out of the rubble of broken glass and half-juiced lemons that is &lt;em&gt;the C programming language&lt;/em&gt; to bring you some fun tricks. These aren&amp;rsquo;t tricks in a clever or brilliant sense. I&amp;rsquo;d consider &lt;a href="https://justine.lol/cosmopolitan/"&gt;APE&lt;/a&gt; binaries a brilliant trick. These are more about ergonomics.&lt;/p&gt;
&lt;p&gt;These go a &lt;em&gt;long&lt;/em&gt; way in making C feel like a modern language. I took care to make sure that you can just cut-n-paste most and put them directly in your code&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;</description></item><item><title>Chicken Soup for the Grug Soul</title><link>/sp-001/</link><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>/sp-001/</guid><description>&lt;h1 id="c-is-my-daily-driver"&gt;c is my daily driver&lt;/h1&gt;
&lt;p&gt;Almost every day, I write C. I am not an embedded developer. I exclusively use regular computers (a Linux desktop and a MacBook Air) and write mostly regular code (small tools, throwaway scripts, GPU experiments, video games).&lt;/p&gt;
&lt;p&gt;Unless I have a good reason otherwise, I do it exclusively in C. I legimately find it to be the best tool for most things I do.&lt;/p&gt;
&lt;p&gt;Out of the box, C is a (mostly) well designed language with perhaps the worst standard library and tooling of any commonly used language. It&amp;rsquo;s truly bad. But those things are not C; they&amp;rsquo;re just the environment. And even though some of C&amp;rsquo;s warts are baked in too deeply, the vast majority of them are not.&lt;/p&gt;</description></item><item><title>A Dead and Dying World</title><link>/a-dead-and-dying-world/</link><pubDate>Wed, 28 Jun 2023 14:13:33 -0400</pubDate><guid>/a-dead-and-dying-world/</guid><description>&lt;p&gt;Found in Unreal Engine 5.1 source, UserWidget.cpp, line 2035&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Look for indications that widgets are being created for a dead and dying world.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Maybe so.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;You need to jump through lots of hoops to view Unreal source on GitHub, but &lt;a href="https://github.com/EpicGames/UnrealEngine/blob/5.1/Engine/Source/Runtime/UMG/Private/UserWidget.cpp#L2035"&gt;here&amp;rsquo;s the file&lt;/a&gt;. It&amp;rsquo;s there as of Unreal 5.1, but seems to have been deleted at some point since.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>A Very Simple Class Implementation in Lua</title><link>/firmament-class/</link><pubDate>Wed, 30 Nov 2022 00:00:00 +0000</pubDate><guid>/firmament-class/</guid><description>&lt;p&gt;Lua is a beautiful language.&lt;/p&gt;
&lt;p&gt;Most scripting languages will make you feel dirty at some point. Packaging a project in Python is a &lt;a href="https://github.com/spaderthomas/tdbuild/commit/20c1a3c20468c7037a6b6abde8cc68490fbd50c6"&gt;hellscape&lt;/a&gt; littered with &lt;a href="https://www.anaconda.com/"&gt;broken glass&lt;/a&gt;, plus it&amp;rsquo;s about three thousand source files between the C stuff and the Python stuff. V8 is roughly two million lines of source code, and the ecosystem around it is &lt;a href="https://nodejs.org/en/"&gt;frac&lt;/a&gt;&lt;a href="https://deno.land/"&gt;tur&lt;/a&gt;&lt;a href="https://bun.sh/"&gt;ed&lt;/a&gt; and generally gorged to exploding with &lt;a href="https://babeljs.io/docs/en/"&gt;transpilers&lt;/a&gt; and &lt;a href="https://javascript.info/polyfills"&gt;polyfills&lt;/a&gt; and &lt;a href="https://webpack.js.org/"&gt;bundlers&lt;/a&gt; other various flavors of &lt;a href="https://drewdevault.com/2021/11/16/Cash-for-leftpad.html"&gt;bullshit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is fine, in the same way that traffic in my city (Atlanta) is fine. It&amp;rsquo;s terrible, and it makes you want to stay away from your car, but nobody&amp;rsquo;s really to &lt;em&gt;blame&lt;/em&gt;. Institutional problems, sure, and the general shit-rolls-downhill quality of the actions of large groups, but it&amp;rsquo;s really just this thing that Sucks and that you can&amp;rsquo;t do anything about.&lt;/p&gt;</description></item><item><title>Firmament Demo Inside!!</title><link>/firmament-1.0.0/</link><pubDate>Tue, 13 Sep 2022 00:00:00 +0000</pubDate><guid>/firmament-1.0.0/</guid><description>&lt;p&gt;The long awaited day is here. Thousands &amp;ndash; &lt;em&gt;no, millions&lt;/em&gt; &amp;ndash; have sat &amp;ndash; &lt;em&gt;no, on third thought it&amp;rsquo;s probably something like billions; sorry&lt;/em&gt; &amp;ndash; HAVE SAT monk-like, forgoing even basic human qualities of living such as eating, sleeping, having sex, or even drinking water. They wander the desert, like the Israelites of the Old Testamant, waiting for someone to deliver them.&lt;/p&gt;
&lt;p&gt;No longer! That&amp;rsquo;s right, &lt;a href="/firmament-000/"&gt;Firmament&lt;/a&gt; is out! It&amp;rsquo;s &amp;ldquo;just&amp;rdquo; a demo, and it spans the first part of the game. The finished product will be perhaps four times larger, or perhaps will never exist. Here are some neat stats about the game!&lt;/p&gt;</description></item><item><title>Half Circles, Quarter Circles, and So On</title><link>/half-circles/</link><pubDate>Wed, 22 Jun 2022 00:00:00 +0000</pubDate><guid>/half-circles/</guid><description>&lt;p&gt;I&amp;rsquo;m making a game. It&amp;rsquo;s an interactive novel mixed with an RPG. It&amp;rsquo;s called &lt;a href="/firmament-1.0.0/"&gt;Firmament&lt;/a&gt;. This is a piece of concept art I wrote while designing the world.&lt;/p&gt;
&lt;p&gt;In this story, a boy learns an important lesson about binary search.&lt;/p&gt;
&lt;h1 id="half-circles-quarter-circles-and-so-on"&gt;HALF CIRCLES, QUARTER CIRCLES, AND SO ON&lt;/h1&gt;
&lt;p&gt;I was looking for Alexander. A few months ago, my assignment had been switched
from changing out capacitors on a little patch in 440A to running integrity
checks on some of the Unchanging Sums. I liked this. Working on capacitors meant
that your hands would always smart after your Maintenance shift. The sums were
easy, though. The worst that would happen with those is that your eyes might
cross after a few hours of comparing the readouts to what was in the Manual.&lt;/p&gt;</description></item><item><title>Static Charge</title><link>/firmament-001/</link><pubDate>Tue, 21 Jun 2022 00:00:00 +0000</pubDate><guid>/firmament-001/</guid><description>&lt;p&gt;This is the first &amp;ldquo;concept art&amp;rdquo; story that I wrote for the game I&amp;rsquo;m making, Firmament. If you haven&amp;rsquo;t yet received the &lt;a href="/firmament-1.0.0/"&gt;Glory&lt;/a&gt;, well, what are you waiting for?&lt;/p&gt;
&lt;p&gt;In this story, two techs who replace panels of the dome&amp;rsquo;s wall notice readings that are inconsistent with the staticism of the dome.&lt;/p&gt;
&lt;h1 id="static-charge"&gt;Static Charge&lt;/h1&gt;
&lt;p&gt;Lao Tzu awoke the same way he had for two decacycles now, in an
unfamiliar tunnel in an unfamiliar segment of the Dome with citizens
that were not from his pod. He could not remember most of their
names. The only man he had bothered to make acquaintance with was
Tiberius Gracchus, a large, quiet man who did the same work Lao Tzu
did.&lt;/p&gt;</description></item><item><title>What's a Firmament?</title><link>/firmament-000/</link><pubDate>Mon, 20 Jun 2022 00:00:00 +0000</pubDate><guid>/firmament-000/</guid><description>&lt;p&gt;&lt;img src="/images/firmament-title_hu_4a6e2037a93ca3b3.webp" alt="" title="yeah"&gt;&lt;/p&gt;
&lt;p&gt;I am making a video game. It&amp;rsquo;s called Firmament, and it&amp;rsquo;s an interative novel + RPG set in a domed society at some point in the future. The dome provides everything that society needs, and was created with a manual detailing instructions for perfectly static homeostasis. Naturally, our player arrives at a time when homeostasis is not perfect, nor static, and is in fact very imperfect, and dynamic.
I am making a video game.&lt;/p&gt;</description></item><item><title>The Fastest Gun</title><link>/the-fastest-gun/</link><pubDate>Wed, 26 Feb 2020 22:21:47 -0500</pubDate><guid>/the-fastest-gun/</guid><description>&lt;p&gt;&lt;img src="/images/watkins-circle_hu_3892e4b8c9e5829d.webp" alt="" title="it&amp;#39;s a metaphor, damnit"&gt;
&amp;ldquo;Killin&amp;rsquo; just don&amp;rsquo;t do it for me anymore, Tex.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Joe leaned into the dusty oak bar. &amp;ldquo;I come home&amp;rsquo;n I take apart my gun, but oil&amp;rsquo;n it just don&amp;rsquo;t feel quite the same.&amp;rdquo; The keep put a cold draught on the table, and Joe took a long swallow and tossed him a few chits.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;How d&amp;rsquo;ya mean? Shootin&amp;rsquo;s shootin&amp;rsquo;. Ain&amp;rsquo;t nothing more to it&amp;rsquo;n that, ain&amp;rsquo;t never will be.&amp;rdquo; Tex said. He drawled how he looked, slow.&lt;/p&gt;</description></item><item><title>A Primer on Competitive Pokemon</title><link>/pokemon-1/</link><pubDate>Wed, 30 Oct 2019 00:00:00 +0000</pubDate><guid>/pokemon-1/</guid><description>&lt;h2 id="an-introduction"&gt;An Introduction&lt;/h2&gt;
&lt;p&gt;Hello! My name is Spader. I have been playing Pokemon at the highest level of singles play for about ten years now (albeit less active recently). You may know me on Smogon as &lt;a href="https://www.smogon.com/forums/members/bad-ass.28700/"&gt;user: Bad Ass&lt;/a&gt; (bonus: an interview I don&amp;rsquo;t remember in the slightest&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;). I play virtually every tier in existence, and love almost all of them! I don&amp;rsquo;t have as much time to invest in the game as I would like, but I&amp;rsquo;ve noticed there is a lack of guides on improving at Pokemon (as opposed to fighting games, where theory and strategy are overflowing on the internet). This will be a series of articles to help you improve at the game.&lt;/p&gt;</description></item><item><title>2D Collisions with Minkowski Differences</title><link>/minkowski/</link><pubDate>Sat, 25 Aug 2018 00:00:00 +0000</pubDate><guid>/minkowski/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This post is going to explain the basic idea behind a &lt;strong&gt;Minkowski difference&lt;/strong&gt; &amp;ndash; what they are, how to visualize them geometrically, and a simple case of leveraging them for collision detection. The intent is to give you a solid intuition for a concept that is very fundamental in many collision detection algorithms, as well as what problems you need to solve to extend my basic presentation of it!&lt;/p&gt;</description></item></channel></rss>