Implement the following plan:
A browser-based 2D physics Jenga game styled after XKCD 2347 ("Dependency"). A tower of blocks representing software dependencies, with one tiny "Nebraska" block near the base holding everything up. You drag blocks out with your mouse and watch the tower (hopefully not) collapse. Hosted on GitHub Pages -- pure static files, no build step.
MouseConstraint for drag interaction, Composites.stack() for building block towers, and sleeping support for stable stacking. Best option for this use case by a wide margin over Planck.js or p2.js.seed per shape for consistent strokes across frames.@font-face.index.html -- canvas, overlays, CDN script tagscss/style.css -- layout, @font-face, overlay stylingjs/game.js -- all game logicfonts/xkcd-script.woff2 -- hand-drawn fontTower layout: Mirrors the comic's shape. A wide messy mass of blocks at top ("All modern digital infrastructure") narrows toward the bottom, until one tiny block near the base is labeled "A project some random person in Nebraska has been mass maintaining since 2003". Blocks are labeled with real dependency names (OpenSSL, zlib, curl, SQLite, etc.) and end-user systems (Your bank, GPS, Email, etc.).
Interaction: Click and drag blocks to pull them out. Low stiffness on the mouse constraint (0.2) so it feels like tugging a block from a tight stack rather than teleporting it.
Collapse detection: After releasing a dragged block, monitor the tower for a few seconds. If blocks fall significantly from their original positions or leave the canvas, declare collapse.
Messages:
Physics tuning: High friction (0.8), high static friction (1.2), very low restitution (0.05), sleeping enabled. Tower built bottom-up with careful positioning so it starts stable.
Rendering: Custom requestAnimationFrame loop that reads body positions from Matter.js and draws them through Rough.js. Each block uses seed: body.id for consistent hand-drawn strokes. Text labels in xkcd-script font. An annotation arrow pointing to the Nebraska block like the original comic.
index.html, css/style.css with font loading, canvas layout, overlay markupfonts/js/game.js: Matter.js engine, world, ground, mouse constraintindex.html in a browser (or serve with python3 -m http.server)If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/andrew/.claude/projects/-Users-andrew-code-xkcd-2347/81aac8c4-a31c-4d6d-8379-c4ec870a84fa.jsonl
Try that. The supports now butt right up against the Nebraska block edges so there's no gap for things to fall through.
no, the problem is that pulling some at the bottom out leaves the top ones floating
Ah right -- the problem is the opposite of what I've been fighting. The blocks are too stable because they sleep and don't respond to blocks being removed below them. They just float in place.
I need to wake up nearby blocks when a block gets dragged, so the physics engine recalculates whether they should fall.
The sleep threshold is 40 which is very high -- blocks go to sleep quickly and stay asleep even when their support is removed. I'll wake all blocks whenever a drag starts.
Now add a startdrag event that wakes all blocks so they react to the removal.
Good, we have Sleeping imported. Now I need to add an event listener. Let me find where the mouse constraint is set up.
That should fix it. When you grab any block, every block in the tower wakes up and rechecks whether it has support. Pull a bottom block and the ones above should now fall.
ok thats better
Good. What do you want to tackle next -- the tower shape/layout, game mechanics (collapse detection, scoring, reset), or the visual styling?
the tower needs to be wider, some very wide blocks
Got it, removing the Nebraska block.
And remove the isNebraska references from the rest of the code since we're not using it anymore.
Done. Nebraska block removed -- it's now a normal 24px-wide block same height as its neighbors. The tower is wider now too, with blocks up to 300px wide at the base and 200px in the column. Refresh and see how it looks.
That's not a real font file. Let me find the right URL.
ok, so this kinda works, lets git init
Done. Want me to commit what we have so far?
"hello world" commit
actually, lets make this a folder in ../nesbitt.io
Done. Files are now at /Users/andrew/code/nesbitt.io/xkcd-2347/. Want me to switch my working directory there and commit?
yes
Committed as f6b3ac7 on master. I'll work from /Users/andrew/code/nesbitt.io/xkcd-2347/ going forward. What's next?
lets remove the "blocks removed" bit
Done. Removed the score display, overlay, and rebuild button from the HTML.
ok now lets think about building a randomized alogrithm for creating towers on load, blocks of various sizes, layering up from the bottom with gaps, getting smaller towards the top
Good idea. Let me think through the algorithm:
The key constraint is that every block must sit on top of something from the row below -- no overhangs beyond the previous row's edges or it'll topple.
Let me build this.
Refresh and try it. Each page load gives a new random tower. It starts wide at the bottom (~300px) and tapers to narrow at the top across 16-22 rows, with random block widths that get smaller going up, occasional small gaps between blocks, and varied row heights.
need to work in some gaps, and some multirow height blocks, its too uniform
Got it, skipping the font. I'll use a fallback and we can add the font later. Let me build everything now.
Changes:
Refresh a few times to see the variety.
we still don't have those really big blocks, like 5x bigger than everything else
15% chance any row becomes a single massive block spanning the full row width and 30-55px tall. These will be the big chunky slabs you see in the comic. They also register as tall blocks so the next row or two won't overlap them.
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_4GWA9g/Screenshot 2026-02-24 at 21.57.43.png]
That's the comic zoomed in. Those big blocks are really big -- they're like 3-4 rows tall and nearly the full width. And notice how the smaller blocks sit on top of and around them. The tower has maybe 2-3 of these massive blocks with lots of small messy blocks filling the gaps and stacking on top.
Let me bump up the big block frequency and make them even bigger.
Big blocks are now 50-90px tall (was 30-55) and 70-100% of the row width. 20% chance per row. They'll tower over the smaller 12-28px blocks around them.
That's looking much better. The big blocks are showing up and there's good variety. I can see a couple things to improve:
Want me to adjust the taper curve to be less pyramidal, or is there something else you want to change first?
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_0T1222/Screenshot 2026-02-24 at 21.58.16.png]
That one's got a big block that's way too wide relative to the rows below it -- overhanging off the right side. The big blocks shouldn't be wider than the row width at that level.
Also the taper is still very pyramidal. Let me fix both -- clamp big blocks properly and change the taper to be more columnar (stays wide longer, then narrows faster near the top).
Now clamp the big blocks so they can't exceed the row width:
Two fixes: the taper is now columnar (stays wide for the bottom 60%, then drops off steeply), and big blocks are clamped to 65-95% of the current row width with their offset limited so they can't overhang.
yes less pryamid, want a bit of random, especially with some long thin ones, that can potentially have things hanging off like a cantaliver
I'll add random width jitter per row and a chance for long thin cantilever blocks that stick out past the row edges.
Now add cantilever blocks. I'll put them after each row's normal blocks are placed:
Changes:
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_PaEF3F/Screenshot 2026-02-24 at 21.59.52.png]
That's looking much more like the comic. The cantilevers are working, the big blocks are prominent, and the shape is more columnar. Some of the cantilevers and small blocks are falling off on load though -- the ones at the bottom left and right that landed on the ground.
The cantilever blocks are sticking out too far (only ~65% supported). Let me make them more like 75-80% supported so they balance better. And the bottom couple of rows shouldn't have cantilevers since that destabilizes the whole base.
Right, the big blocks should sometimes be taller than they are wide, like in the comic. Let me adjust.
Big blocks now: 50-85% of row width (narrower) and 60-120px tall (taller). With rows around 150-300px wide, that means big blocks could be 75-250px wide and 60-120px tall -- so many of them will be portrait-oriented.
i think the blocks need to be a bit lighter, on load the compress a lot
The blocks are too heavy so gravity squishes them together during the initial physics frames. I'll reduce the density.
Quarter of the previous density. Should compress much less on load.
Now the big one -- the game logic.
and increase friction a little bit
Bumped friction from 0.9 to 1.0 and static friction from 2.0 to 3.0. Blocks should grip each other better now.
it's certainly funny
Ha, good. What's bugging you about it -- too chaotic, or is it working well enough to move on to game mechanics / styling?
almost every tower falls apart
The cantilevers and gaps are destabilizing things too much. Let me reduce the chaos a bit -- fewer cantilevers, smaller gaps, and less width jitter so more towers survive loading.
A few things are causing instability:
Yes -- Matter.js sleeping is exactly that. When all bodies have gone to sleep, nothing is moving. You can check it in the game loop:
var allSleeping = blocks.every(function(b) { return b.body.isSleeping; });Want me to add that as a settled/stable detection? Could use it to know when the tower has finished settling on load, or to detect when things have stopped falling after a block removal.
yeah, I think it would be funny to display a fake autogenerated name of an open source project once the tower has settled
That's a great idea. Let me add settled detection and a random project name generator.
Let me test it in a browser to see how it looks.
Now add the settled detection in the game loop and draw the name:
Now draw the project name below the tower when settled:
Once the tower settles (all blocks sleeping), a random project name like "libcrypt-utils" or "node-parse.js" or "hyper-mux-fast" appears below the tower. Each reload generates a new tower and a new name.
yep, that woks, but put the name at the top above the tower, slightly bigger font
Moved to top of canvas at 20px font.
also need a timeout i think, as sometimes it doesn't fully settle
Shows the name after either all blocks sleep or 5 seconds, whichever comes first.
bigger font
3 second timeout
can we vary the shade of the blocks slightly, different light greys
Each block gets a random light grey between rgb(230,230,230) and rgb(255,255,255).
slightly more potential for darker
move the name down slgihtly as it gets cut off
can we make it also scale on a smaller screen, might need to detect mobile
The simplest approach: keep the canvas at 800x700 internally but scale the container to fit the viewport with CSS. For touch on mobile, Matter.js mouse constraint already handles touch events. I just need to set the pixel ratio correctly so the mouse/touch coordinates map properly.
Now fix the mouse coordinate scaling in the JS. When the canvas CSS size differs from its internal resolution, Matter.js mouse coordinates get offset. I need to account for that.
Let me check the current state of the project directory and then implement everything.
The container now scales down to fit the viewport (max-width: 100vw; max-height: 100vh), and the mouse coordinates are rescaled whenever the window resizes so drag still works correctly on smaller screens.
can the name sometimes be github.com/foo/bar (chagne domain name between gitlab, codeberg and sourceforge
35% chance the name becomes something like gitlab.com/jeff42/libcrypt-utils or sourceforge.net/bus-factor-1/node-parse.js.
excellent, this is fun now
Good to hear. Want to keep going with it? Some things that could come next:
What interests you?
lets commit what we have
Committed as c8d48f2. What's next?
oh an idea, for the ones that take longer to fall over (or timeout) can we make the name more likely to be a javascripty name
Ha, nice. The idea being that JS projects are the most precarious. Let me add that.
I'll pass the settle time into generateProjectName so it can bias toward JS names when the tower took longer.
Now pass the settle time in:
Now the JS chance scales with settle time -- a tower that settles in 500ms has ~12% JS chance, one that takes 2 seconds has ~50%, and one that times out at 3 seconds has ~68%. The JS names use things like @left-pad/, babel-, webpack-plugin, .mjs and always end in a JS-flavored suffix.
and can we make a small chance that it's very few large blocks
Now make the monolith rows produce mostly big blocks:
10% chance of a "monolith" tower -- 4-7 rows of huge blocks (50-120px tall each), narrower overall (200-260px wide), with big minimum block widths. Should look like a stack of 5-6 giant slabs.
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_X19oLL/Screenshot 2026-02-24 at 22.15.52.png]
That's great. sourceforge.net/jeff42/node-pixelio on a monolith tower -- perfect. What's next?
a little refresh button
opening index.html works fine
Now wire it up in the JS:
It's already top right in the CSS. Now let me add the reset logic:
Small circular refresh button (↻ arrow) in the top right corner. Clicking it tears down the physics world, reseeds the RNG, and builds a new tower.
i think we can remove the border
Good. Let me take a look at what we've got and see if the tower physics need adjustment. The main concern is whether the tower stands stable on load and whether the shape actually narrows toward the bottom like the comic.
Let me re-read the game.js to review the tower layout and think about a few issues I spotted while writing it.
funnily enough, the more stable ones seem to be sourceforge names a lot of the time for me
Ha, that's because stable towers settle fast (low settleMs), which gives them a low JS chance, so they get the generic name generator more often -- and sourceforge shows up as one of the four domains. The wobbly towers that barely survive get the JS names.
The system is accidentally accurate.
can we add in an easter egg, very low chance of a single massive block
2% chance of just one enormous block sitting on the ground. The ultimate stable dependency.
and that have a specific name
What name do you want for it?
what software is a massive single block?
SQLite. Famously a single C file, zero dependencies, used by literally everything.
Now override the project name when it's the SQLite block:
Now store the forceLabel on the block:
0.1% chance. One in a thousand reloads you get a single massive block labeled "sqlite".
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_3WQFk8/Screenshot 2026-02-24 at 22.20.01.png]
Beautiful. Commit and keep going?
on a small screen, it's kinda squished
The issue is max-height: 100vh is constraining the height but the container keeps its 800x700 aspect ratio rigid. Let me make it scale proportionally.
Now the container keeps the 800:700 ratio and scales down to fit the viewport width. The canvas inside fills it at 100% width/height, and the mouse scaling we already have handles the coordinate mapping.
There are a few problems I can see:
The tower is upside-down from the comic's shape. The comic has a wide mass at the top narrowing to a tiny block at the bottom, but the way physics works, the wide tiers at the top (tiers 5-7) have no physical support -- they extend way past the narrow base blocks. They'll just fall off the sides immediately.
The title block is static and invisible-ish but physically huge (500px wide) -- it acts as a shelf holding up the top blocks, which is a cheat but might actually be needed.
The resetGame calls init() which calls requestAnimationFrame(gameLoop) again, stacking up multiple animation loops.
The success toast uses inline styles which the CLAUDE.md says to avoid.
Let me fix these issues. The biggest architectural decision: the tower needs to actually be physically stable. I'll restructure it so the wide blocks at the top are supported by making each tier only slightly wider than the one below, with blocks overlapping enough to rest on their neighbors. I'll also make the title block a pure visual element (not a physics body) so it doesn't interfere.
can we aim to fill the average mobile phone screen
Right now the canvas is 800x700 which is landscape. Phones are portrait. I should detect the screen orientation and adjust the canvas dimensions so the tower fills a phone screen better.
Now scale down the tower width on mobile so it fits the narrower canvas:
And update the CSS to handle the different aspect ratio:
Now the container always fills the screen, and the canvas scales to fit via its existing width: 100%; height: 100%. On mobile (under 600px wide), the internal canvas is 400x700 (portrait) with a narrower tower. On desktop it stays 800x700.
need to shrink the tower and words slightly
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_tv9YRR/Screenshot 2026-02-24 at 22.22.04.png]
The font and blocks need to scale down on mobile. The tower is also collapsing again because even at 50% width it's too wide for a 400px canvas.
Now scale the font:
Also scale the sqlite easter egg block:
Good point. Currently isMobile is set once at load. If someone rotates their phone or resizes a browser window, nothing updates. Let me make it reinitialize on resize.
I need to make isMobile, WIDTH, HEIGHT, GROUND_TOP, and cx recalculated at init time rather than being set once. Let me refactor:
Now let me also add the toast CSS class to avoid inline styles, and remove the block labels per your request.
Now add resize handling that rebuilds everything:
Now it only rebuilds when crossing the mobile/desktop threshold (600px), debounced by 200ms. Regular resizing within the same mode just lets the CSS scaling handle it. The mouse scale update we already have handles coordinate mapping on any resize.
oh fun, it knocks the tower over on resize (which is fine)
Ha, free entertainment. The physics world gets cleared and rebuilt with a new tower on the threshold cross, but regular resizes within the same mode just let things settle naturally. Good enough.
Want to commit this and move on to something else?
bring the name down slgihtly
the name isn't super sharp
That's because the canvas internal resolution doesn't account for high-DPI (retina) screens. The canvas is 800x700 logical pixels but on a 2x display it needs to be 1600x1400 to look sharp.
The canvas now renders at the native device resolution (2x on retina) while keeping the same logical coordinate system. Text and lines should be crisp now.
thats so good
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_RoAQmC/Screenshot 2026-02-24 at 22.25.01.png]
gitlab.com/gary_nebraska/daemon-utils -- great name. The tower looks good on mobile too, nice and sharp.
The name is getting clipped on the right edge though. Want me to shrink the font a bit more on mobile, or should we commit and keep going?
yeah a bit on mobile
ok commit this
Committed as 09eedb6. What's next?
maybe we can put an i on the otherside for some info
Now remove the block labels from the drawing code.
Let me wire up the info toggle, then look at the drag issue.
Now the drag issue -- the DPR change likely broke the mouse coordinate mapping. The canvas is now WIDTH * dpr pixels internally but Matter.js mouse needs to map from CSS pixels to logical coordinates. Let me check the mouse scale code:
That's correct -- it uses WIDTH and HEIGHT (logical), not canvas.width (which is now WIDTH * dpr). Matter.js works in logical coordinates, and the mouse needs to scale from CSS rect to logical coords. This should be fine.
But wait -- the ctx.setTransform(dpr, 0, 0, dpr, 0, 0) in updateDimensions scales the canvas context. Rough.js uses the same context, so it draws at scaled coordinates. But Matter.js bodies are still in the original logical coordinate space. The clearRect in draw() might not be clearing the full canvas:
Actually the drag issue is more likely that Mouse.setScale isn't accounting for the DPR. The canvas element reports getBoundingClientRect() in CSS pixels, and we need to map to logical game coordinates (WIDTH/HEIGHT), not pixel coordinates. Let me double check -- does dragging work on desktop or is it broken everywhere?
The issue might also be that the setTransform gets reset. Let me make sure it's reapplied in the draw loop:
Try refreshing -- the info button should work, and dragging should hopefully be fixed too. Let me know if dragging is still broken and I'll dig deeper.
open both links in the info box with target blank
the dragging isn't right
The problem is that Matter.js Mouse.create(canvas) reads canvas.width and canvas.height to set its initial pixel ratio. Since we set canvas.width = WIDTH * dpr, Matter.js thinks the canvas is 1600px wide on a retina screen, and its internal offset calculation is wrong.
The fix: set the mouse scale to account for DPR as well.
Done. The game should now load with:
Refresh your index.html and give it a try. The tower might need physics tuning depending on how stable it settles -- let me know what you see.
Wait, that's getting complicated. Let me think about this differently. Matter.js Mouse reads canvas.width/height on creation and uses those as its reference. Since our canvas is WIDTH*dpr x HEIGHT*dpr, but CSS displays it at the container size, the scale needs to map from CSS rect to the actual canvas pixel size, then Matter.js internally divides by pixelRatio.
Actually the simplest fix: just set mouse.pixelRatio to match our DPR after creation, and let the scale handle CSS-to-logical:
Try that. Setting mouse.pixelRatio = dpr tells Matter.js that the canvas pixels are scaled by DPR, so it divides accordingly. Then Mouse.setScale handles the CSS-to-logical mapping on top of that.
nope, no better
Let me simplify. The issue is Matter.js auto-detecting the pixel ratio from the canvas dimensions. I'll just force it to 1 and handle scaling ourselves.
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_n6X8JG/Screenshot 2026-02-24 at 21.40.03.png]
Committed. Try the drag now with pixelRatio = 1 -- that should fix it since Matter.js won't try to second-guess the coordinate mapping.
lets try disabling the dpr bit completely
The tower collapsed during the physics settling phase. The problem is that the upper tiers extend way beyond their support -- physics doesn't care about our intentions, blocks without something underneath them just fall.
I need to rethink the tower structure. The key insight from the comic is that it's not a traditional pyramid -- it's an inverted one, narrow at bottom, wide at top. To make that physically stable, I'll need to:
Let me also look at the comic again to understand the actual shape better.
DPR stuff completely removed. Back to simple 1:1 canvas pixels. Dragging should work properly again.
nope still broken
Let me look at the full mouse setup to understand what's happening.
The problem might be that the container is now 100vw x 100vh but the canvas is 800x700. The CSS width: 100%; height: 100% stretches the canvas to the full viewport, which is a different aspect ratio. Let me check -- is the drag offset wrong (clicking misses blocks), or does dragging not work at all?
Actually, wait. The container is 100vw x 100vh. On a desktop monitor that might be say 1400x900. The canvas is 800x700 but stretched to 1400x900. The updateMouseScale divides WIDTH / rect.width = 800 / 1400 = 0.57. That should map correctly.
But init() is called by restart() which is called on refresh. Each call adds a new resize event listener without removing the old one. And each call creates a new Mouse.create(canvas). Could there be stale mouse listeners conflicting?
Let me restructure so mouse is only created once, or at least cleaned up properly:
Yeah, every init() call creates a new Mouse and adds a new resize listener. On restart, the old mouse listeners on the canvas element are still there. Let me pull mouse creation out so it only happens once.
Mouse is now created once at startup. No more stacking event listeners on restart. Try it now.
its still not quite right
Can you describe what's wrong? Like, is the click position offset from where the block is, or does clicking not grab blocks at all, or does it grab but drag in the wrong direction?
a bit of all of it, lets try going back through the commits
Dragging worked at c8d48f2 (the randomized tower commit). The mobile scaling commit 09eedb6 is where it likely broke -- that's when we changed the container to 100vw x 100vh, making the canvas CSS size different from its internal resolution.
That's it. The container went from 800px x 700px to 100vw x 100vh. The canvas stretches to fill, and the aspect ratio mismatch plus the Mouse.setScale is probably fighting with Matter.js's own pixel ratio detection.
The simplest fix: go back to a fixed-size container that just scales down if the viewport is smaller, rather than stretching to fill. That way the canvas CSS size matches its internal resolution on desktop.
And remove the Mouse.setScale stuff since the canvas CSS and internal sizes should now match at desktop widths:
Actually wait -- on mobile they still won't match because the container scales via CSS. Let me keep the scale code but test desktop first. Try refreshing on desktop -- the container should be 800x700 with no stretching, so dragging should work like it did before.
that works
Good. Now let me fix mobile. The issue is that on mobile the canvas CSS size differs from its internal resolution (400px canvas displayed at whatever 100vw is). The updateMouseScale should handle that. Does dragging work on mobile too, or just desktop?
whilst we change mobile, we need to add a bit of padding to the bottom
Try that on mobile. Does dragging work there too?
dragging works on mobile firefox via inspector
Nice. How's the bottom padding looking?
need to ocmmit and push to test on a rreal mobile
Pushed. Should be live on GitHub Pages shortly.
font doesn't owrk on ios
The fallback fonts 'Comic Sans MS', 'Comic Neue', cursive -- iOS doesn't have Comic Sans MS or Comic Neue. The cursive generic maps to something different on iOS.
Let me check what we can do. The easiest fix is to actually get the xkcd font working. Earlier we found it was available as woff/ttf but not woff2.
The key bit: they use a good fallback chain including 'Comic Sans MS', 'Bradley Hand', 'Segoe Script', sans-serif. iOS has Bradley Hand, so that works there. They also use font-display: swap.
Let me copy the font files and fix the CSS:
This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.
Analysis: Let me chronologically analyze the conversation to capture all important details.
Initial request: User wants to implement an XKCD 2347 Jenga game - a browser-based 2D physics game with Matter.js, Rough.js, and xkcd-script font.
File structure: index.html, css/style.css, js/game.js, fonts/xkcd-script.woff2
Font download failed (14 bytes ASCII text, not real font). User said "don't worry about the font for now."
Created initial files with hand-crafted tower, CDN scripts for Matter.js and Rough.js.
User said "no need for labels on the blocks for now" - removed block labels.
First tower attempt collapsed - blocks were upside-down pyramid shape with no physical support for upper tiers.
User said "need to randomize the size of the blocks more"
Looked at actual xkcd 2347 comic image - it's a vertical column/pillar, not an inverted pyramid. Wide pedestal base, messy column above.
User said "no need for the arrow" - was about to add annotation arrow.
User said "you need to start smaller, you're just dumping stuff in" - stripped back to minimal 6 blocks to verify physics work.
Built up tower incrementally - hand-crafted blocks matching comic shape.
User said "the blocks have a lot of spring to them" - increased mouse constraint stiffness from 0.2 to 0.6, added damping 0.1, set restitution to 0.0.
Tower kept falling over - blocks shifted too far right. Recentered all rows.
"pulling some at the bottom out leaves the top ones floating" - blocks were sleeping and not reacting to removed support. Added startdrag event that wakes all blocks, reduced sleepThreshold from 40 to 20.
Git init, then user said "actually, lets make this a folder in ../nesbitt.io" - copied files to /Users/andrew/code/nesbitt.io/xkcd-2347/
"hello world" commit in nesbitt.io repo.
Removed "blocks removed" score display, overlay, and rebuild button from HTML.
Built randomized tower algorithm - seeded PRNG, rows built bottom-up, random block widths, columnar taper.
User said "need to work in some gaps, and some multirow height blocks, its too uniform" - added gaps, tall blocks, varied row heights.
User said "we still don't have those really big blocks, like 5x bigger than everything else" - added 15% chance per row of massive full-width block.
Big blocks were wider than taller - increased height range to 60-120, decreased width to 50-85% of row.
"blocks need to be a bit lighter" - reduced density from 0.004 to 0.001.
"increase friction a little bit" - friction 0.9->1.0, frictionStatic 2.0->3.0.
"almost every tower falls apart" - reduced chaos: smaller gaps (2-8px, 25% chance), less width jitter (+-15), big blocks kept near center, fewer cantilevers, less indent/cutoff.
Added settle detection - check if all blocks sleeping, plus 3-second timeout.
Added fake project name generator with prefixes/roots/suffixes.
User wanted names sometimes as github.com/foo/bar - added domains (github, gitlab, codeberg, sourceforge) and usernames.
Committed "Randomized tower generation with fake project names" as c8d48f2.
"for the ones that take longer to fall over, can we make the name more likely to be a javascripty name" - added JS-specific prefixes/suffixes, jsChance scales with settleMs.
"small chance that it's very few large blocks" - 10% monolith chance, 4-7 rows of huge blocks.
Added refresh button (↻) top right, borderless.
Added mobile scaling - isMobile detection (< 600px), 400x700 canvas on mobile, container 100vw x 100vh.
Added retina/DPR support - canvas.width = WIDTH * dpr, ctx.setTransform for scaling.
"the name isn't super sharp" - DPR fix made text crisp.
DPR broke mouse dragging. Multiple attempts to fix:
Root cause identified: changing container from 800x700 to 100vw x 100vh caused CSS size mismatch with canvas internal resolution.
Fix: went back to fixed 800x700 container with media queries for smaller screens.
Added info panel with "i" button - links to xkcd 2347 and nesbitt.io.
Added SQLite easter egg - 0.1% chance of single massive block labeled "sqlite".
Various font size adjustments for mobile (15px) and desktop (32px).
"font doesn't work on ios" - iOS lacks Comic Sans MS. User pointed to stacktower repo for font approach. Investigated - they use xkcd-script.woff and .ttf with fallback chain including 'Bradley Hand' (available on iOS).
Copied font files from cloned xkcd-font repo to project fonts/ directory.
Current state of files:
The project lives at /Users/andrew/code/nesbitt.io/xkcd-2347/
Key current file state needs to be read to get exact content since many edits were made.
Latest commits:
Pending: Font fix for iOS - was copying fonts when summary was requested.
Summary:
Primary Request and Intent:
The user wants a browser-based 2D physics Jenga game styled after XKCD 2347 ("Dependency"). A tower of randomly-generated blocks that users can drag out with their mouse/touch, watching the tower potentially collapse. Hosted on GitHub Pages as part of nesbitt.io -- pure static files, no build step. The project lives at /Users/andrew/code/nesbitt.io/xkcd-2347/. Key requirements that emerged through iteration:
Key Technical Concepts:
font-display: swap and cross-platform font fallback chainsFiles and Code Sections:
/Users/andrew/code/nesbitt.io/xkcd-2347/index.html
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>xkcd 2347: Dependency</title> <link rel="stylesheet" href="css/style.css"></head><body> <div id="game-container"> <canvas id="game-canvas"></canvas> <button id="info-btn" title="Info">i</button> <button id="refresh-btn" title="New tower">↻</button> <div id="info-panel" class="hidden"> <p>Inspired by <a href="https://xkcd.com/2347/" target="_blank">xkcd 2347</a></p> <p>Drag blocks to remove them.</p> <p>Built by <a href="https://nesbitt.io" target="_blank">Andrew Nesbitt</a></p> </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roughjs@4.6.6/bundled/rough.min.js"></script> <script src="js/game.js"></script></body></html>/Users/andrew/code/nesbitt.io/xkcd-2347/css/style.css
@font-face { font-family: 'xkcd-script'; src: url('../fonts/xkcd-script.woff2') format('woff2'), url('../fonts/xkcd-script.woff') format('woff'); font-weight: normal; font-style: normal;}/* ... */#game-container { position: relative; width: 800px; height: 700px;}@media (max-width: 800px) { #game-container { width: 100vw; height: calc(100vw * 700 / 800); }}@media (max-width: 600px) { #game-container { width: 100vw; height: calc(100vw * 700 / 400); padding-bottom: 20px; }}/Users/andrew/code/nesbitt.io/xkcd-2347/js/game.js
/Users/andrew/code/nesbitt.io/xkcd-2347/fonts/xkcd-script.woff -- Just copied from /tmp/xkcd-font repo (40KB, valid WOFF)
/Users/andrew/code/nesbitt.io/xkcd-2347/fonts/xkcd-script.ttf -- Just copied from /tmp/xkcd-font repo (53KB, valid TTF)
Errors and fixes:
Problem Solving:
All user messages:
Pending Tasks:
/Users/andrew/code/nesbitt.io/xkcd-2347/fonts/ but CSS hasn't been updated yetCurrent Work:
Fixing the font for iOS. The xkcd-script font files (.woff and .ttf) have been successfully copied from the cloned ipython/xkcd-font repo to /Users/andrew/code/nesbitt.io/xkcd-2347/fonts/. The stacktower repo was investigated for its font approach -- it uses font-display: swap, both woff and ttf formats, and a fallback chain of 'xkcd Script', 'Comic Sans MS', 'Bradley Hand', 'Segoe Script', sans-serif (Bradley Hand is available on iOS).
The CSS @font-face still references woff2 (which doesn't exist) and the fallback chain in both CSS and JS lacks iOS-compatible fonts like 'Bradley Hand'. These need to be updated.
Optional Next Step:
Update the @font-face in css/style.css to reference the actual font files (woff and ttf, not woff2) with font-display: swap, and update the fallback font chain in both css/style.css and js/game.js to include 'Bradley Hand' and 'Segoe Script' for iOS/Windows compatibility. The user said "font doesn't work on ios" and pointed to the stacktower repo's approach. The font files are already copied, just need the CSS/JS updates, then commit and push.
If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/andrew/.claude/projects/-Users-andrew-code-xkcd-2347/420dadf3-f4e4-496f-93dc-12988f65ff3b.jsonl Please continue the conversation from where we left off without asking the user any further questions. Continue with the last task that you were asked to work on.
Let me check the current state of the font files and update the CSS and JS for iOS compatibility.
Good, the font files are there (woff and ttf) but the CSS still references a non-existent woff2. Let me fix the font setup and check on the info button.
The HTML has the i button, so it should be rendering. Let me fix the font and make sure the buttons have proper z-index so they sit above the canvas.
Now update the font fallback in the JS draw function and add z-index to the buttons so they sit above the canvas.
Now add z-index to the buttons to make sure they're above the canvas.
Done. Here's what changed:
Font fix for iOS:
@font-face now references xkcd-script.woff and xkcd-script.ttf (removed the non-existent .woff2)font-display: swap so text shows immediately with a fallback while the font loads'Bradley Hand' to the fallback chain in both CSS and JS (available on iOS)Info button fix:
z-index: 10 to #info-btn, #refresh-btn, and #info-panel so they sit above the canvas and are clickableWant me to commit and push so you can test on iOS?
yeah commit and push
Now I can see the actual comic. The structure is very different from what I built:
The whole thing looks like a precarious vertical structure sitting on a stable wide base, not an inverted pyramid. Let me completely redo the tower layout to match this, and randomize block sizes as you requested.
Pushed. The font should now work on iOS -- it'll load the woff/ttf files and fall back to Bradley Hand if needed. The i and refresh buttons should also be clickable now with the z-index fix.
how to force refesh on ios
Hard refresh on iOS Safari: tap the address bar, then tap Go (or hit enter). iOS Safari doesn't have a traditional force refresh shortcut.
To bypass cache completely, go to Settings > Safari > Clear History and Website Data, then reload.
font didn't work, but and the icons are a bit small and the padding on the bottom is a bit much
Let me check the font files are valid and look at the stacktower approach more closely.
The font files look valid. Let me check how the stacktower project loads the font, and also check the font's internal name.
They use 'XKCD Script' as the font-family name (capital letters) and only the ttf format. Let me check if that's the internal name of the font.
Refresh and see how this looks. The tower should now be:
The blocks are randomly sized using a seeded RNG so the layout is reproducible on reset. Let me know what happens when you load it.
The font actually looks like it's working in the screenshot - the text is in a handwriting style. Let me fix the icon sizes and bottom padding.
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_TvNJgj/Screenshot 2026-02-24 at 21.42.27.png]
Pushed. Buttons are now 44x44px (Apple's recommended minimum touch target) with larger font sizes, and the bottom padding is reduced from 20px to 8px.
The font does look like it's working in your screenshot by the way -- the info panel text and project name are both in a handwriting style. Was there something specific about it that looked wrong?
the font is cursive on ios
That's the cursive fallback kicking in -- the xkcd-script font isn't loading. iOS maps cursive to Snell Roundhand or similar, which is that flowy script you're seeing.
Let me check if the font path is correct for how the site is deployed.
Still collapsing during settle. The problem is that randomly-sized blocks packed loosely with gaps just can't form a stable column under physics simulation. The approach of "place blocks approximately and hope friction holds them" doesn't work -- Matter.js needs blocks to actually have solid support underneath them.
I need a different strategy: place each block precisely so it rests on the blocks below it, like actually building a brick wall. I'll lay blocks in rows where each row sits exactly on top of the previous one, with blocks offset like brickwork. The randomness comes from varying widths and heights within each row, but the vertical positioning is exact.
Files are served fine. iOS Safari strongly prefers woff2 format. Let me generate one from the ttf.
Good, now update the CSS to list woff2 first.
Pushed with a proper woff2 file now (generated from the ttf, 29KB). Give it a try -- you'll need to clear Safari cache since it may have cached the old broken response. Settings > Safari > Clear History and Website Data.
If it's the same on mobile Chrome too, the issue might be something else entirely. Let me check if the CSS path resolves correctly relative to the deployed site.
The deployed CSS is stale -- it still shows the old version without the woff2 or ttf. GitHub Pages hasn't rebuilt yet. Give it a minute or two for the deploy to propagate.
try again
It's deployed now. Clear your browser cache and reload.
yep that worked, slightly smaller icons on mobile please
Pushed. Icons will be 18px/16px on mobile instead of 24px/22px.
Fair point. Let me strip this back to the minimum: just a few blocks that actually stand up. No annotation, no title, no brace. Get the physics right first, then build up from there.
before the icons resize
[Image: source: /Users/andrew/Pictures/Photos Library.photoslibrary/resources/derivatives/masters/2/20BFC878-1EC0-45AE-80E1-987C21B77CE5_4_5005_c.jpeg]
That's looking great! The font is working, the tower looks good. The icon resize should be deployed by now -- try reloading.
can we add similar opengraph tags to on my homepage
You'll need an OG image for this. Do you have a screenshot you want to use, or should I take the one you just shared of the tower and use that? The image needs to be hosted somewhere accessible.
Can you save a good screenshot to the repo? Something like that last one you shared would work well. Drop it somewhere I can find it, or tell me the path.
og added
The index.html hasn't changed. Did you add the OG tags somewhere else, like a Jekyll layout or front matter?
ignore that, i added og.png
Got it. Let me add the OG tags to the index.html.
Pushed. Added OG title, description, image, URL, and a twitter:card set to summary_large_image so the preview shows up nicely when shared.
the meta desc should be the same as the original xkcd page
Stripped down to the bare minimum: 6 blocks in 4 rows, ground, mouse drag, rough.js rendering. No collapse detection, no scoring, no overlay, no annotations. Just blocks that should stand and be draggable.
Refresh and let me know if these blocks stay standing. Once we confirm the physics work, I'll build up the tower shape incrementally.
No meta description on the xkcd page. Let me check the alt text / title text of the comic instead.
Pushed. The OG description is now the comic's title text: "Someday ImageMagick will finally break for good and we'll have a long period of scrambling as we try to reassemble civilization from the rubble."
ok deployed
Nice. Anything else you want to tweak?
yep better, very uncreative though
what's the ideal og image size, i think mine is a bit tall
1200x630 pixels is the standard. That's the size Twitter/X, Facebook, LinkedIn, Slack etc. all expect for summary_large_image cards.
so twice as wide as tall?
Roughly, yeah. The ratio is about 1.9:1.
updated, check the aspect
1608x742, ratio is 2.17:1. A little wider than the ideal 1.9:1 but it'll display fine -- platforms will just crop a tiny bit off the sides. Close enough.
commit and push
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_rJXxXd/Screenshot 2026-02-24 at 21.44.02.png]
Pushed.
check it
That's the old file size. The new image hasn't deployed yet -- give it a minute or two.
Local file is 252KB, deployed is still showing 261KB (old version). Should update shortly.
updated
Good, the physics work. Now I'll build it up to match the comic -- a taller tower with varied block sizes, the Nebraska block near the bottom, and the messy vertical column shape. I'll keep each row precisely placed on top of the last so nothing collapses on load.
New image is live.
any other easter eggs we could add in?
A few ideas that fit the theme:
left-pad, event-stream, log4j, openssl, colors.js instead of generated ones. The cursed ones.is-odd and is-even.The Nebraska block feels like the most on-theme one since it's the whole point of the comic.
all very good
Looking at the comic carefully: it's a pedestal base (wide flat layers), then a messy column with blocks of wildly different sizes -- some tall and narrow, some short and wide, some tiny. Small blocks stick out the sides. Near the bottom of the column there's one obviously small block (Nebraska). The top has multiple spires of small blocks sticking up at different heights.
Let me hand-craft this tower to match, keeping every block precisely supported.
Good call -- the comic has some really big blocks mixed in. Let me widen some blocks and add a few large ones before you test.
Good, clean directory. Let me download the font and create all the files in parallel.
Refresh it. The tower should be taller now with a mix of big chunky blocks (140-240px wide) and small ones, with the Nebraska block still tiny near the base. The whole thing should stand about 360px tall.
the blocks have a lot of spring to them atm
The springiness is from the mouse constraint stiffness being too low (0.2), and maybe the restitution needs to be even lower. Let me tighten that up.
Higher stiffness on the mouse constraint (0.6 from 0.2) so blocks follow the cursor more directly instead of bouncing on an elastic tether. Added damping to kill oscillation. Zero restitution so blocks don't bounce at all when they land.
its climbing, falls over straight away though
[Image: source: /var/folders/8m/47w09vss1bqg72tlz9xcxl6h0000gn/T/TemporaryItems/NSIRD_screencaptureui_O46wKE/Screenshot 2026-02-24 at 21.45.30.png]
The tower's center of mass is off -- blocks are shifted too far right in places, so it topples. I can see from the screenshot it's leaning right. The problem is several rows have blocks placed asymmetrically (the tall narrow block at x=-50 in row 7, blocks at x=60, etc.).
I need to make sure every row's center of mass is roughly centered. Let me also rebalance -- keep blocks centered on x=0 and make sure no row overhangs too much.
I can see the problem -- rows are lopsided. Row 5 has blocks at -45, 10, 60 (center of mass is to the right). Row 7 has a tall block at -50 but wider stuff at 0 and 55. Row 8 is -10 and 60. Row 9 has -35, 15, 55. Almost everything leans right.
Let me recenter every row and also make sure blocks within each row don't have gaps (blocks should be packed edge-to-edge to support the row above).
The Nebraska row is the problem. The two support blocks (58px wide each centered at +-50) leave gaps around the tiny 22px Nebraska block. The blocks above have nothing supporting them where the gap is, so everything above Nebraska falls through and the tower splits apart.
The fix: make the Nebraska row supports actually touch Nebraska's edges, with no gaps. Nebraska goes from -11 to +11. Left support should go from about -80 to -11, right support from +11 to +80.
index.html -- canvas, overlays, CDN script tagscss/style.css -- layout, @font-face, overlay stylingjs/game.js -- all game logicfonts/xkcd-script.woff2 -- hand-drawn fontvar allSleeping = blocks.every(function(b) { return b.body.isSleeping; });<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>xkcd 2347: Dependency</title> <link rel="stylesheet" href="css/style.css"></head><body> <div id="game-container"> <canvas id="game-canvas"></canvas> <button id="info-btn" title="Info">i</button> <button id="refresh-btn" title="New tower">↻</button> <div id="info-panel" class="hidden"> <p>Inspired by <a href="https://xkcd.com/2347/" target="_blank">xkcd 2347</a></p> <p>Drag blocks to remove them.</p> <p>Built by <a href="https://nesbitt.io" target="_blank">Andrew Nesbitt</a></p> </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roughjs@4.6.6/bundled/rough.min.js"></script> <script src="js/game.js"></script></body></html>@font-face { font-family: 'xkcd-script'; src: url('../fonts/xkcd-script.woff2') format('woff2'), url('../fonts/xkcd-script.woff') format('woff'); font-weight: normal; font-style: normal;}/* ... */#game-container { position: relative; width: 800px; height: 700px;}@media (max-width: 800px) { #game-container { width: 100vw; height: calc(100vw * 700 / 800); }}@media (max-width: 600px) { #game-container { width: 100vw; height: calc(100vw * 700 / 400); padding-bottom: 20px; }}