<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arthur031221</title>
    <description>The latest articles on DEV Community by Arthur031221 (@arthur031221).</description>
    <link>https://dev.to/arthur031221</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4152010%2Fdc59e799-61f4-4c44-aa6a-204b14048265.jpeg</url>
      <title>DEV Community: Arthur031221</title>
      <link>https://dev.to/arthur031221</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arthur031221"/>
    <language>en</language>
    <item>
      <title>Does routing coding agent subtasks to cheaper models pay off? A benchmark of 63 runs per arm</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Thu, 22 Oct 2026 12:07:02 +0000</pubDate>
      <link>https://dev.to/arthur031221/does-routing-coding-agent-subtasks-to-cheaper-models-pay-off-a-benchmark-of-63-runs-per-arm-4cdd</link>
      <guid>https://dev.to/arthur031221/does-routing-coding-agent-subtasks-to-cheaper-models-pay-off-a-benchmark-of-63-runs-per-arm-4cdd</guid>
      <description>&lt;p&gt;I wanted a straight answer to one question: if a coding agent hands its cheap subtasks to a smaller model, how much does a session really cost less, and does the result get worse?&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Token Beggar is a plugin that adds a routing card to the session. The chat model grades each task into one of 7 classes and delegates bulk work (large reads, repeated edits, test loops) to a worker with its own context, at a cheaper model and lower effort. Design and root-cause work stays with the chat model. The chat model and effort are a ceiling, so nothing is routed above what you chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;21 tasks on a small Python shop project, 3 per class, 3 runs each.&lt;/li&gt;
&lt;li&gt;Two arms per task, same prompt, same allowed tools, same chat model (Opus 5.5 at high effort).&lt;/li&gt;
&lt;li&gt;Code tasks: a hidden check plus the project's own tests. Answer tasks: a checklist scored by a blind judge that never sees the arm.&lt;/li&gt;
&lt;li&gt;Cost from the agent's JSON output, at list price.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Without&lt;/th&gt;
&lt;th&gt;With&lt;/th&gt;
&lt;th&gt;Cheaper&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multi-file feature&lt;/td&gt;
&lt;td&gt;$2.47&lt;/td&gt;
&lt;td&gt;$1.58&lt;/td&gt;
&lt;td&gt;1.56x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mechanical edits&lt;/td&gt;
&lt;td&gt;$1.11&lt;/td&gt;
&lt;td&gt;$0.80&lt;/td&gt;
&lt;td&gt;1.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading&lt;/td&gt;
&lt;td&gt;$0.91&lt;/td&gt;
&lt;td&gt;$0.75&lt;/td&gt;
&lt;td&gt;1.21x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;$0.93&lt;/td&gt;
&lt;td&gt;$0.82&lt;/td&gt;
&lt;td&gt;1.14x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routine code&lt;/td&gt;
&lt;td&gt;$1.47&lt;/td&gt;
&lt;td&gt;$1.32&lt;/td&gt;
&lt;td&gt;1.12x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug root cause&lt;/td&gt;
&lt;td&gt;$1.45&lt;/td&gt;
&lt;td&gt;$1.29&lt;/td&gt;
&lt;td&gt;1.12x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;$1.21&lt;/td&gt;
&lt;td&gt;$1.13&lt;/td&gt;
&lt;td&gt;1.07x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All 63 runs per arm&lt;/td&gt;
&lt;td&gt;$9.56&lt;/td&gt;
&lt;td&gt;$7.69&lt;/td&gt;
&lt;td&gt;1.24x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Quality was 0.96 in both arms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would not claim
&lt;/h2&gt;

&lt;p&gt;Design, bug root cause and review never go to a worker, yet they came out 1.07x to 1.14x cheaper. That is run to run spread plus, I suspect, a shorter path the card gives the chat model. So I only quote multipliers of 1.15x or more in the plugin's footer. The routes were tuned on the same 21 tasks, so the table describes this setup and is not a forecast for yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  A scoring bug worth knowing about
&lt;/h2&gt;

&lt;p&gt;One debugging task looked like a regression in 5 of 6 runs with the plugin. The diffs were identical to the passing runs. The cause was a stale .pyc: the fix changed &lt;code&gt;==&lt;/code&gt; to &lt;code&gt;!=&lt;/code&gt;, which keeps the file size, and the bytecode written by the pre-fix test run still passed Python's mtime check. The scorer now clears bytecode before checking, and both arms were rescored. If you score code produced by agents, clear &lt;strong&gt;pycache&lt;/strong&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in the box
&lt;/h2&gt;

&lt;p&gt;A compaction guard that writes a handoff note before an automatic compaction and defers it up to 3 times when files were edited after the last test run. The weekly model list refresh is a GitHub Action.&lt;/p&gt;

&lt;p&gt;Repo, raw runs and the scorer: &lt;a href="https://github.com/Arthur031221/Token-Beggar" rel="noopener noreferrer"&gt;https://github.com/Arthur031221/Token-Beggar&lt;/a&gt; (MIT).&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>A seating planner that names the rules that clash, with a SAT solver in TypeScript</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:07:01 +0000</pubDate>
      <link>https://dev.to/arthur031221/a-seating-planner-that-names-the-rules-that-clash-with-a-sat-solver-in-typescript-49n5</link>
      <guid>https://dev.to/arthur031221/a-seating-planner-that-names-the-rules-that-clash-with-a-sat-solver-in-typescript-49n5</guid>
      <description>&lt;p&gt;When a teacher's seating requests can't all hold, most planners say "no solution". That's true and useless. I wanted a tool that names the few requests that clash, so I wrote SeatReason, a static web app that runs entirely in the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8etbfcj56mef7ylyy0q.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8etbfcj56mef7ylyy0q.gif" alt="A conflict in the example class, a repaired plan with changed seats outlined, then a hand-made move that breaks a fixed seat and is undone" width="200" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three requests
&lt;/h2&gt;

&lt;p&gt;The room is a grid with optional blocked seats, up to 40 pupils and 48 seats. A request is one of three kinds: a fixed seat, a place in the first N rows, or two pupils who must not sit next to each other. That is deliberately small, so every request can be explained in one sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the clash is found
&lt;/h2&gt;

&lt;p&gt;Fixed seats and front rows are a matching problem, so a Hall violator gives the clash quickly. Everything else goes to a CDCL SAT solver I wrote in TypeScript. Each request has a selector literal that guards its clauses. If the formula is unsatisfiable under all selectors, the failed assumptions are a conflict. A deletion pass then tries to drop each request from the set and keeps it only if the rest become satisfiable.&lt;/p&gt;

&lt;p&gt;The result is a subset-minimal set: take away any one request and the others can hold. The page only says "minimal" after that pass finished. If the solver runs out of its budget, the answer is "inconclusive", never "impossible".&lt;/p&gt;

&lt;h2&gt;
  
  
  Repairs you can check
&lt;/h2&gt;

&lt;p&gt;Under each request in the clash there is a preview. It plans again with that request off, keeping the current seats where it can, and checks every remaining request together. Changed seats are outlined. The plan is replaced only when you press the button, and Undo brings it back.&lt;/p&gt;

&lt;p&gt;A separate checker that doesn't share the solver's encoding validates every plan. In the tests the solver is compared with an exhaustive search on small rooms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurements
&lt;/h2&gt;

&lt;p&gt;On an AMD Ryzen 5 7500F with Node 24.21, five runs of the bench gave a median of 18 to 32 ms to find a minimal clash that needs the SAT solver, with the slowest case at 0.38 s. Repair previews for those cases took a median of 0.26 to 0.49 s. The machine was busy during those runs, so the numbers moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;Nine pupils who must all be kept apart in a 4 by 4 room run past the 8 second limit, and the page says so. Keep apart means orthogonal neighbours only. There is no gender balance, group seating or rotation.&lt;/p&gt;

&lt;p&gt;The app has nine interface languages, a JSON backup and a printable chart. Try it at &lt;a href="https://arthur031221.github.io/SeatReason/" rel="noopener noreferrer"&gt;https://arthur031221.github.io/SeatReason/&lt;/a&gt; or read the code at &lt;a href="https://github.com/Arthur031221/SeatReason" rel="noopener noreferrer"&gt;https://github.com/Arthur031221/SeatReason&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Matching rescanned pages to a damaged PDF with SIFT, and admitting when it can't</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:07:02 +0000</pubDate>
      <link>https://dev.to/arthur031221/matching-rescanned-pages-to-a-damaged-pdf-with-sift-and-admitting-when-it-cant-db2</link>
      <guid>https://dev.to/arthur031221/matching-rescanned-pages-to-a-damaged-pdf-with-sift-and-admitting-when-it-cant-db2</guid>
      <description>&lt;p&gt;I had a scanned PDF with a few pages ruined by a coffee ring and a crease, plus fresh scans of those pages in no particular order. So I built Darnleaf, a local tool that suggests which page each rescan replaces and lets me check the evidence before it writes anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6e05nub156m4q9pdnanv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6e05nub156m4q9pdnanv.gif" alt="Darnleaf matching seven rescans to a 40-page booklet" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching
&lt;/h2&gt;

&lt;p&gt;Every page is rendered with PDFium and reduced to a working size. A cheap pass uses a spread-out subset of SIFT features to shortlist ten candidate pages per rescan. Each candidate is then verified with RANSAC under a similarity transform, and the transform has to be plausible: scale between 0.7 and 1.4, and a skew of at most 12 degrees from a right angle. That also gives the quarter turn the rescan needs. A one to one assignment resolves conflicts, so two rescans can't claim one page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saying "check"
&lt;/h2&gt;

&lt;p&gt;A match is Clear only if it has enough consistent features, enough ink agreement after alignment and a big margin over the runner-up. Otherwise it's Check. Blank pages carry no evidence, so they're always Check. When OCR is installed, text can raise or lower a score, but it can't create a match on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export
&lt;/h2&gt;

&lt;p&gt;Replaced pages are swapped in place with pikepdf. Untouched pages stay the original objects, and bookmarks and links that point at a replaced page still work. A JSON receipt records SHA-256 hashes of the original, the output and every rescan, plus the evidence for each swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;p&gt;On 90 synthetic rescans over ten held-out seeds, 45 were Clear and none of those was wrong. With 45 trials the real wrong rate could still be about 6.7%. The right page was listed for 89 of 90. The original and the rescans are drawn from the same clean bitmap, so real scans will likely do worse.&lt;/p&gt;

&lt;p&gt;Demo replay: &lt;a href="https://arthur031221.github.io/Darnleaf/" rel="noopener noreferrer"&gt;https://arthur031221.github.io/Darnleaf/&lt;/a&gt;&lt;br&gt;
Source: &lt;a href="https://github.com/Arthur031221/Darnleaf" rel="noopener noreferrer"&gt;https://github.com/Arthur031221/Darnleaf&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Rendering a 3D terrain flyover of a GPX route in the browser</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:07:01 +0000</pubDate>
      <link>https://dev.to/arthur031221/rendering-a-3d-terrain-flyover-of-a-gpx-route-in-the-browser-2np7</link>
      <guid>https://dev.to/arthur031221/rendering-a-3d-terrain-flyover-of-a-gpx-route-in-the-browser-2np7</guid>
      <description>&lt;p&gt;I wanted a flyover video of a hike, and every tool I found wanted me to upload the track or pay for longer clips. So I built Ridgereel, a browser app that turns a GPX, TCX or FIT file into a 3D terrain flyover MP4.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcffzlmucqsk6xanz1t7o.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcffzlmucqsk6xanz1t7o.gif" alt="Ridgereel flyover of the Yushan Main Peak trail" width="252" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;A Web Worker parses the file into a track. The route is cleaned of GPS jumps and resampled about every 5 m. Elevation tiles in the AWS Terrarium encoding are decoded into one height field, and imagery from Esri, OpenTopoMap or CARTO is stitched into a single texture.&lt;/p&gt;

&lt;p&gt;Three.js draws the terrain with a camera relative sun, contour lines from &lt;code&gt;fwidth&lt;/code&gt; in the shader, fog and an edge fade. The route is three opaque line layers. A 2D overlay adds the stats and the elevation profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frames as a function of time
&lt;/h2&gt;

&lt;p&gt;I prefetch all tiles before rendering, so a frame depends only on the time. Two things follow. Seeking back to the same time redraws identical pixels, which an end to end test checks in one session. And WebCodecs can encode an exact MP4 at 30 or 60 fps instead of recording the screen in real time. Browsers without WebCodecs fall back to a real time recording.&lt;/p&gt;

&lt;p&gt;The camera rig has follow, drone and overview presets, each with an intro, a main orbit and an outro. In 9:16 the outro fits the whole route using the narrower horizontal field of view. That took one fix after the first render cut the route off at the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy
&lt;/h2&gt;

&lt;p&gt;The file is never uploaded. A Playwright test loads a file whose name contains a marker, records every request, and fails if one goes anywhere but the four tile servers, isn't a GET, or carries the marker. Tile servers still see the area you open. By default the first and last 200 m of the route are cut and the map around them is pixelated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers and limits
&lt;/h2&gt;

&lt;p&gt;The 15 s, 1080 x 1920 sample is 450 frames. It rendered in 236 s in headless Chrome 149 on a 3 core Linux machine with software WebGL, and the file is 12.8 MB. I haven't timed a GPU. I tested export in Chrome only. The sample route uses OpenStreetMap geometry with synthetic timestamps, and the file says so.&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://arthur031221.github.io/Ridgereel/" rel="noopener noreferrer"&gt;https://arthur031221.github.io/Ridgereel/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source and docs: &lt;a href="https://github.com/Arthur031221/Ridgereel" rel="noopener noreferrer"&gt;https://github.com/Arthur031221/Ridgereel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a desktop buddy for Claude Code that explains whatever you Ctrl-click</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Mon, 19 Oct 2026 10:15:01 +0000</pubDate>
      <link>https://dev.to/arthur031221/i-built-a-desktop-buddy-for-claude-code-that-explains-whatever-you-ctrl-click-4k85</link>
      <guid>https://dev.to/arthur031221/i-built-a-desktop-buddy-for-claude-code-that-explains-whatever-you-ctrl-click-4k85</guid>
      <description>&lt;p&gt;Say you're reading a dense paragraph in a paper and you want Claude to explain it. The usual route is a screenshot, a switch to the terminal and a paste. Some pages won't even take a link, so there's no shortcut there either.&lt;/p&gt;

&lt;p&gt;Blinkling is my attempt to remove those steps. It's a Claude Code plugin that puts a small green character called Blink in the corner of your screen, above every app. Hold Ctrl (Control on a Mac), click a paragraph, a button or a picture, and the pick flies into Blink. A chat card opens beside Blink and the answer streams in. Drag instead of click to take an area.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdq05yly71sv58uwplhn0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdq05yly71sv58uwplhn0.gif" alt="Ctrl-click a paragraph and the answer streams into the chat card beside Blink"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's MIT licensed and has no API key or account of its own. Here's how the pieces fit together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the current version
&lt;/h2&gt;

&lt;p&gt;Version 3.2.7 is the latest. The model, effort and permission pickers on the chat card all work. Auto explain is now a short muttering bubble beside Blink, one or two sentences in your language, and it no longer opens the card. Guide mode puts marks on the page itself with a line of text under each, and Next and Previous scroll to them like links. Blink shows a thinking animation only while the AI is working. Capture is faster, and the chat card closes and Blink turns half transparent while it happens, so the card is never in the shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it talks to Claude Code
&lt;/h2&gt;

&lt;p&gt;There are two parts. A desktop app, an Electron shell, draws Blink and does the looking. Claude Code, signed in on the same computer, does the answering.&lt;/p&gt;

&lt;p&gt;Every capture, quick ask, typed message and guide step goes through one session. That session runs the signed-in &lt;code&gt;claude&lt;/code&gt; command without a shell and reads the answer as a stream of events, so Blink can show sentences as they arrive. Send, stop, retry and resume all work on that one conversation.&lt;/p&gt;

&lt;p&gt;A capture is held in memory until it's been handed to Claude Code, then dropped. Nothing about it is written to the app's data folder. Answers can carry marks in screen coordinates, like a highlight on the thing being explained. The overlay checks that the screen hasn't moved before it draws them, and the model returns data, never HTML or script. Markdown in answers is sanitized before display.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plugin side
&lt;/h2&gt;

&lt;p&gt;The plugin's &lt;code&gt;.mcp.json&lt;/code&gt; starts a small local bridge, &lt;code&gt;bin/blinkling.cjs&lt;/code&gt;. It listens on a loopback port (43827 by default, scanning ten) and only lets the optional Chrome and Edge helper in when that helper proves it holds a secret stored locally. The same bridge is an MCP server for Claude Code, so Claude Code can look at the screen when you ask it to. One of its tools samples the screen every 5 to 60 seconds, keeps only the frames that changed and makes no model calls.&lt;/p&gt;

&lt;p&gt;The desktop app itself is downloaded by a launcher the first time you run &lt;code&gt;/blinkling:blinkling&lt;/code&gt;. The launcher checks the build against a SHA-256 digest pinned in the plugin and installs it atomically. A build whose digest doesn't match is never installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the mirroring works
&lt;/h2&gt;

&lt;p&gt;The chat card and the terminal mirror each other, like Remote Control. Half of that is easy. A prompt you type in the terminal session that started Blink, and its finished answer, appear in Blink's chat on their own.&lt;/p&gt;

&lt;p&gt;The other half, Blink's chat showing up in the terminal, uses Claude Code's experimental channels, which only load at startup. So you start Claude Code with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--dangerously-load-development-channels&lt;/span&gt; plugin:blinkling@blinkling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code asks you to confirm once. The flag name is Claude Code's, and it reads scarier than it is. If you skip it, Blink still works and answers from its own session, you just lose the mirror in that direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;Blink only looks and explains. It never clicks, types or controls another app. It refuses to capture while a password field, a password manager or a private window has focus, and the browser helper blanks password and payment fields before anything leaves the page. There's no telemetry and no answer service run by me. What you pick goes to your own Claude Code, so your provider's terms and usage apply.&lt;/p&gt;

&lt;p&gt;Some pages are off limits to any browser extension, like browser settings, extension stores and the built-in PDF viewer. On those, Blink answers from the screen alone. Claude Code on a remote host isn't supported, because Blink and Claude Code must share a computer with a screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add Arthur031221/Blinkling &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;blinkling@blinkling
claude &lt;span class="nt"&gt;--dangerously-load-development-channels&lt;/span&gt; plugin:blinkling@blinkling
/blinkling:blinkling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need Claude Code, signed in. On a Mac, allow Screen Recording once in System Settings, Privacy &amp;amp; Security, and Accessibility once so the Ctrl key can start a pick. The app isn't notarized by Apple and has no paid Windows certificate, so the installer removes the quarantine flag and signs ad hoc, but macOS may still make you choose Open Anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it with Claude Code
&lt;/h2&gt;

&lt;p&gt;The repo's first commit is from 2026-10-12, and it's past 630 commits now. The 3.x releases were built by Claude Code in long builder passes, mostly Sonnet 5.5 with Opus 5.5 asked for the hard calls. I direct it and test by hand on a Mac. The demo clips were recorded on Linux in a virtual display, so the Mac is where I look for what those clips miss.&lt;/p&gt;

&lt;p&gt;The most useful thing I learned is not to trust the builder's own verdict. A green verify report from the build isn't a result for me. Every release waits for a separate Claude Code session that downloads it, runs it under a virtual display and checks each point again before I call anything fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;It works, with limits. I only test by hand on a Mac, so Windows and Linux builds exist but have had less real use.&lt;/p&gt;

&lt;p&gt;If you know a nicer way than the channels flag to get a plugin's chat into a live Claude Code session, I'd like to hear it in the comments.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Arthur031221/Blinkling" rel="noopener noreferrer"&gt;https://github.com/Arthur031221/Blinkling&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Demo page with the install steps animated: &lt;a href="https://arthur031221.github.io/Blinkling/" rel="noopener noreferrer"&gt;https://arthur031221.github.io/Blinkling/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>claudecode</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building a local handwriting font workshop with editable glyphs</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Tue, 13 Oct 2026 23:07:02 +0000</pubDate>
      <link>https://dev.to/arthur031221/building-a-local-handwriting-font-workshop-with-editable-glyphs-20l</link>
      <guid>https://dev.to/arthur031221/building-a-local-handwriting-font-workshop-with-editable-glyphs-20l</guid>
      <description>&lt;p&gt;I built GlyphSprout for projects that need a small set of personal characters. It turns a handwritten page into a font subset that I can inspect and adjust one glyph at a time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F94aj34fhnyvutaj2qljd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F94aj34fhnyvutaj2qljd.gif" alt="GlyphSprout workshop recording" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow begins with an A4 template. Each page has 35 cells, and a project can use up to 512 supported BMP characters across 15 pages. The input is a flat, evenly lit PNG or JPEG scan. I assign the page, confirm its visible ID, and place four registration corners by hand. GlyphSprout shows a rectified preview and asks me to confirm the page before worker processing starts.&lt;/p&gt;

&lt;p&gt;The tracing step thresholds each cell and removes small connected specks. It follows the black ink as independent contours, so counters and detached dots remain part of the glyph. I keep the source cell visible beside the outline. That makes it possible to tune threshold, speck filtering, crop, scale, offsets, and spacing separately for each character. I can also draw and erase when a scan is not the right input.&lt;/p&gt;

&lt;p&gt;The live specimen loads the generated font rather than a simulated rendering. Finished projects export as TTF or WOFF2. If some selected characters are incomplete, I must choose an explicitly labeled completed subset export.&lt;/p&gt;

&lt;p&gt;Paper Bloom is the first visit example. It contains six real letter crops from public domain NIST SD19 form f1002_33. The TTF measures 3,284 bytes. I checked its family and character map with Fontconfig, used it in LibreOffice, and confirmed the PDF text and embedded font with Poppler. The size applies only to these six letters. The Han and kana sample is original synthetic geometry, not handwriting.&lt;/p&gt;

&lt;p&gt;Project data stays in browser storage. IndexedDB keeps current and previous recovery snapshots and warns about a write from another tab. I still recommend downloading the project JSON because browser storage can be cleared. Offline use starts only after the complete application cache reports ready.&lt;/p&gt;

&lt;p&gt;I would appreciate feedback on the scan confirmation and per glyph review steps. Try the workshop: &lt;a href="https://Arthur031221.github.io/GlyphSprout/" rel="noopener noreferrer"&gt;https://Arthur031221.github.io/GlyphSprout/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source and setup notes: &lt;a href="https://github.com/Arthur031221/GlyphSprout/" rel="noopener noreferrer"&gt;https://github.com/Arthur031221/GlyphSprout/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Checking a furniture turning route in the browser</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Sun, 11 Oct 2026 10:07:01 +0000</pubDate>
      <link>https://dev.to/arthur031221/checking-a-furniture-turning-route-in-the-browser-257m</link>
      <guid>https://dev.to/arthur031221/checking-a-furniture-turning-route-in-the-browser-257m</guid>
      <description>&lt;p&gt;I built Unwedged because a doorway's width doesn't tell me about the turn behind it. I wanted a small tool to draw the measured floor, place a rectangular piece of furniture and inspect a route instead of guessing from a diagram.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FArthur031221%2FUnwedged%2Fmain%2Fassets%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FArthur031221%2FUnwedged%2Fmain%2Fassets%2Fdemo.gif" alt="A real Unwedged route search and replay"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The model
&lt;/h2&gt;

&lt;p&gt;Unwedged uses centimetres internally. A project contains a rectangular floor, walls or obstacles, furniture dimensions, a safety margin and the start and destination poses. Each pose has a position and an angle. Wall rectangles can have a centred doorway. Carry height determines whether that opening is available in the model.&lt;/p&gt;

&lt;p&gt;I model a rigid rectangle in 2D at a fixed height. I don't model stairs, lifting, bending furniture or the people carrying it. Measurements and those assumptions matter more than a polished animation.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a search to a checked route
&lt;/h2&gt;

&lt;p&gt;The search explores position and angle together. The default lattice uses ten centimetres and ten degrees, with a finer option for harder layouts. Every proposed motion edge goes through the same continuous verifier used for the final path.&lt;/p&gt;

&lt;p&gt;The verifier compares clearance at a segment midpoint with a bound on how far any point of the furniture can move. If that bound is too loose, it subdivides the motion. The returned clearance is a conservative lower bound. It isn't the exact minimum.&lt;/p&gt;

&lt;p&gt;I checked the two successful presets with 3,204 independently evaluated route poses in Shapely, using 800 equal time intervals per segment. Sampling can catch implementation mistakes. It doesn't replace the continuous certificate. The repository includes the reference check and saved route regression cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://arthur031221.github.io/Unwedged/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; needs no account. For a local copy, install Node.js 22.12 or newer and Git, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx github:Arthur031221/Unwedged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can replay or scrub the route, save a JSON project and export an SVG route sheet. After the first visit, wait for the offline indicator before disconnecting. The cached app includes fonts and the search worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want to improve
&lt;/h2&gt;

&lt;p&gt;The search has a finite resolution and budget. Failing to find a path doesn't prove the furniture can't pass. I'd like measured layouts that challenge the model and feedback on making the editor easier to use.&lt;/p&gt;

&lt;p&gt;The source, architecture notes and reproducible checks are in &lt;a href="https://github.com/Arthur031221/Unwedged" rel="noopener noreferrer"&gt;Unwedged&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why agents draw bad maps, and a checklist that catches them</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:07:03 +0000</pubDate>
      <link>https://dev.to/arthur031221/why-agents-draw-bad-maps-and-a-checklist-that-catches-them-3g92</link>
      <guid>https://dev.to/arthur031221/why-agents-draw-bad-maps-and-a-checklist-that-catches-them-3g92</guid>
      <description>&lt;p&gt;Ask a coding agent to map a spreadsheet and it'll give you something that looks fine. The trouble is what the picture says. I kept seeing the same four mistakes, so I wrote &lt;a href="https://github.com/Arthur031221/Graticule" rel="noopener noreferrer"&gt;Graticule&lt;/a&gt; to stop them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5n8xyby1yv616n7s8e6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5n8xyby1yv616n7s8e6.gif" alt="A map with four mistakes is blocked, one click fixes it, then the labels change language" width="560" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Raw counts.&lt;/strong&gt; Coloring countries by COVID deaths mostly shows where people live. The fix is a rate. Graticule asks you to declare what the numbers are. If you don't, it checks whether the values rank like population. On 33 World Bank indicators, that check flagged 0 of 25 rates and caught 8 of 8 counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mercator for the world.&lt;/strong&gt; Greenland looks as big as Africa. Shaded maps are compared by area, so Graticule defaults to an equal-area projection and refuses Mercator above 20 degrees of latitude.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rows that vanish.&lt;/strong&gt; "Korea, Rep." doesn't match "South Korea" and nothing tells you. Every row that fails to join is listed with its closest names. If a name fits two places, Graticule won't guess. On 235 ISO country names, the join found 232 with no wrong match. Exact matching against Natural Earth found 202.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rainbow palettes.&lt;/strong&gt; Lightness that goes up and down draws borders that aren't in the data. The palette has to get steadily lighter or darker, and its colors have to stay apart under simulated color vision deficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;A map is a small JSON spec. &lt;code&gt;graticule check&lt;/code&gt; runs 25 rules and prints every finding with a one-sentence reason. &lt;code&gt;--fix&lt;/code&gt; applies the smallest repair for each error and records it. Only then does it draw SVG, PNG, PDF or an interactive page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graticule render map.json --fix -f svg,png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are no native dependencies. PNG goes through resvg compiled to WebAssembly. Fonts are subset per map with the HarfBuzz subsetter, so Chinese, Japanese and Korean labels survive on any machine. Geography is bundled, so it works offline. The same modules run in the &lt;a href="https://arthur031221.github.io/Graticule/" rel="noopener noreferrer"&gt;browser playground&lt;/a&gt;, which has nine interface languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I checked
&lt;/h2&gt;

&lt;p&gt;Eleven of twelve projections match PROJ to 0.00001 m on 3,000 points each. Robinson agrees to 10.2 km after fitting one scale. Class breaks match mapclassify on 45 cases, before the legend rounds the edges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rough edges
&lt;/h2&gt;

&lt;p&gt;Boundaries are drawn for maps rather than surveying. Small countries become dots. Labels use a greedy placer, so dense maps drop some and the report counts them. The counts detector is a heuristic with a limit of 0.6. If you have a table that joins wrongly or a map that gets past the checker, I want to see it.&lt;/p&gt;

&lt;p&gt;Install it as an agent skill with &lt;code&gt;npx skills add Arthur031221/Graticule&lt;/code&gt;, or open the playground and drop a CSV.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>dataviz</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Replaying earthquakes in the browser, and checking the waves against 3,933 real picks</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:07:01 +0000</pubDate>
      <link>https://dev.to/arthur031221/replaying-earthquakes-in-the-browser-and-checking-the-waves-against-3933-real-picks-2h94</link>
      <guid>https://dev.to/arthur031221/replaying-earthquakes-in-the-browser-and-checking-the-waves-against-3933-real-picks-2h94</guid>
      <description>&lt;p&gt;When an earthquake is in the news, you get a magnitude and a map. I wanted to see what the waves did and how many seconds I would've had. I built &lt;a href="https://arthur031221.github.io/Rumbleback/" rel="noopener noreferrer"&gt;Rumbleback&lt;/a&gt; to replay a real earthquake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4zk6cym9qc3vae7r7sp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4zk6cym9qc3vae7r7sp.gif" alt="The 2024 Hualien earthquake replayed on a map of Taiwan" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you see
&lt;/h2&gt;

&lt;p&gt;The blue ring is the first P wave at the surface. The orange ring is the first S wave. Small dots light up at real stations when analysts picked a P arrival. Strong motion instruments appear when the S wave reaches them, colored by the intensity USGS computed from their recording. Below the timeline is a nearby station's real seismogram, with the model's P and S arrivals drawn over it.&lt;/p&gt;

&lt;p&gt;Drop a pin on your home to see the P and S arrival there. A slider asks "if an alert went out at this second" and shows how long before the S wave you'd have heard it, or whether you were inside the blind zone.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the waves are computed
&lt;/h2&gt;

&lt;p&gt;Travel times come from the iasp91 Earth model. A small Python script asks ObsPy TauP for the first P and S arrival on a grid of 34 source depths and about 900 distances, then stores a 166 KB table. The browser interpolates it for the quake's depth. A test compares that with 600 random TauP results off the grid. The median error is 0.01 s.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked it
&lt;/h2&gt;

&lt;p&gt;Each featured quake includes the picks USGS used to locate it. I compare the model with 3,933 first P arrivals from 14 quakes, up to 95 degrees away. The median error is 0.66 s, and 90 percent are within 1.75 s. I recomputed that with ObsPy directly to check that my interpolation wasn't hiding anything.&lt;/p&gt;

&lt;p&gt;The model reproduces what instruments recorded for quakes 8 to 48 km deep. It doesn't forecast a quake it hasn't seen, because picks and origin times come from the same data. S waves have almost no picks in USGS data, so the S ring is the model alone. Surface waves and local ground aren't modeled, and the docs say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;It's a static page with MapLibre and a canvas overlay for the waves. The pin lives in the browser. Shared links keep it after the &lt;code&gt;#&lt;/code&gt;, so no server sees it. The end-to-end tests record every request the page makes and look for the pin's coordinates.&lt;/p&gt;

&lt;p&gt;The code is MIT licensed: &lt;a href="https://github.com/Arthur031221/Rumbleback" rel="noopener noreferrer"&gt;https://github.com/Arthur031221/Rumbleback&lt;/a&gt;. I'd like to hear about any quake where the timing looks wrong.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>A passport photo guide that runs in your browser</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Sat, 10 Oct 2026 02:07:02 +0000</pubDate>
      <link>https://dev.to/arthur031221/a-passport-photo-guide-that-runs-in-your-browser-1i56</link>
      <guid>https://dev.to/arthur031221/a-passport-photo-guide-that-runs-in-your-browser-1i56</guid>
      <description>&lt;p&gt;I needed a passport photo and the photo counter wanted $17 for one picture. Printing it yourself on a 4 x 6 sheet is a familiar workaround, but you only find out the head is the wrong size when someone refuses the photo.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://arthur031221.github.io/Deadpan/" rel="noopener noreferrer"&gt;Deadpan&lt;/a&gt; to check that while you stand in front of your camera. Each rule turns green as you meet it. When everything holds for a second, it takes the picture and lays out six copies on one 4 x 6 print.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdd18gv0ls4vbcisikk1u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdd18gv0ls4vbcisikk1u.gif" alt="The head size rule turns green as the person leans in, then six copies appear on a 4 x 6 sheet" width="599" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it measures
&lt;/h2&gt;

&lt;p&gt;MediaPipe's face landmarker finds 478 points on the face. One is the chin. The pupils give the tilt of the head, and a person segmenter run on a square around the face finds the top of the head. Hair counts because the issuing authorities measure to the top of the head.&lt;/p&gt;

&lt;p&gt;Once the crop is known, millimetres per pixel is the photo height divided by the crop height. Every rule is arithmetic on that scale. Each rule says whether its number comes from the authority's page or is my own tolerance for a rule that has no number.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked it
&lt;/h2&gt;

&lt;p&gt;I ran the pipeline on 30 public domain portraits and compared its head height measurement with chin and crown points I marked by hand on a ruler overlay. The pass or fail verdict matched on 29 of 30 photos. All 20 photos it passed are inside the US window of 25 to 35 mm by those points. Ten portraits are cropped too tightly for a 2 inch photo, and Deadpan refuses them instead of padding the picture. The median error is 1.0 mm and the worst is 2.5 mm.&lt;/p&gt;

&lt;p&gt;That's one person marking points on formal adult portraits. It says little about children or hats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing leaves the tab
&lt;/h2&gt;

&lt;p&gt;The models, fonts and WebAssembly runtime are in the repository, so the page calls no CDN. It ships a Content Security Policy that blocks every other origin. An end to end test plays a recorded clip as the camera, takes the picture and builds the sheet while recording every request. Then it tries a fetch to another origin and expects the policy to stop it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is rough
&lt;/h2&gt;

&lt;p&gt;There are four presets: US passport, US visa, Taiwan and Schengen. I only added numbers I could read on the authority's page. Canada needs a commercial photographer, so a home print would be refused. Only Chromium is tested, and I haven't tried a real phone camera.&lt;/p&gt;

&lt;p&gt;If you know a rule I should add, or a head the measurement gets wrong, the source is on &lt;a href="https://github.com/Arthur031221/Deadpan" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>mediapipe</category>
    </item>
    <item>
      <title>Where does the sun land in my room? A browser tool with a checked sun model</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Fri, 09 Oct 2026 18:07:01 +0000</pubDate>
      <link>https://dev.to/arthur031221/where-does-the-sun-land-in-my-room-a-browser-tool-with-a-checked-sun-model-260o</link>
      <guid>https://dev.to/arthur031221/where-does-the-sun-land-in-my-room-a-browser-tool-with-a-checked-sun-model-260o</guid>
      <description>&lt;p&gt;West sun is a constant worry for people who rent in Taiwan, and a listing photo can't show what it'll do in your room. I wanted to see the patch of light on the floor, so I built &lt;a href="https://arthur031221.github.io/Sunspill/" rel="noopener noreferrer"&gt;Sunspill&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi4m3tf58rbavt2lb0et.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi4m3tf58rbavt2lb0et.gif" alt="The sun patch from a west window crossing a bedroom floor" width="720" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You draw the room, up to four windows, a shade above each window and a building across the street. Pick a city from the built-in list and a date, then drag the clock. The patch moves across the floor and the walls. A second view colours the floor by the hours of direct sun per day, and a printed rule counts the afternoon sun after a time you choose.&lt;/p&gt;

&lt;p&gt;Everything runs in the page. The app is one HTML file with a Content Security Policy that allows no network requests, so nothing leaves your browser. After the first visit it works offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the light is found
&lt;/h2&gt;

&lt;p&gt;The sun position comes from the NOAA equations. For each window I take the opening, trim it by the wall thickness (a beam has to clear both faces of the wall), and subtract the shadow that the eave and the building throw on the window plane. Each shadow is convex, so what's left is a few convex pieces.&lt;/p&gt;

&lt;p&gt;Each piece is carried along the sun rays onto the floor and the walls and clipped to them. The room is convex, so a ray that gets in meets the boundary exactly once and nothing inside can block it. So there's no ray casting at runtime, and the polygons are exact instead of pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked it
&lt;/h2&gt;

&lt;p&gt;Sun position: 403 samples in 15 cities against the NREL Solar Position Algorithm through pvlib. Elevation is within 0.007 degrees and azimuth within 0.06 degrees.&lt;/p&gt;

&lt;p&gt;Window light: I wrote a second program that shares no code with the first. From a point in the room it follows the ray toward the sun through the wall, the shade and the building and asks whether it gets out of a window. On 3,767 random rooms and 3.77 million probe points the two disagree 0 times. Writing the comparison turned up a real bug: light below a raised plane was dropped from the wall patches. It's fixed and tested now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;Clear sky and direct sun only. No reflections, no furniture shadows, one box shaped room. The model has never been compared with a photograph of a real room, so treat the patch as a geometric prediction. If you can measure a room, an issue with a photo would help more than any feature.&lt;/p&gt;

&lt;p&gt;The source is MIT licensed: &lt;a href="https://github.com/Arthur031221/Sunspill" rel="noopener noreferrer"&gt;https://github.com/Arthur031221/Sunspill&lt;/a&gt;. The sun and light code is also a small library with no dependencies.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>A day planner where one drag re-flows the whole day</title>
      <dc:creator>Arthur031221</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:07:02 +0000</pubDate>
      <link>https://dev.to/arthur031221/a-day-planner-where-one-drag-re-flows-the-whole-day-4a7h</link>
      <guid>https://dev.to/arthur031221/a-day-planner-where-one-drag-re-flows-the-whole-day-4a7h</guid>
      <description>&lt;p&gt;Whenever a meeting moved, I ended up dragging five blocks around by hand. Calendar apps let you move one block and leave the overlaps for you to fix. I wanted the other half of the move done for me, and visible before I commit to it.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://arthur031221.github.io/Jostle/" rel="noopener noreferrer"&gt;Jostle&lt;/a&gt;. You drag one card, and while you still hold it, the cards after it slide around your fixed appointments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8frir68xr879qvll2bq8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8frir68xr879qvll2bq8.gif" alt="A card is dragged from 8:30 to 10:00, the cards after it slide around the locked appointments, undo puts them back, then a card from the tray is dropped in" width="719" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules
&lt;/h2&gt;

&lt;p&gt;The planner is six rules, and a test checks each one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The card you hold lands where you drop it. If that spot overlaps a fixed appointment, it lands in the nearest gap it fits.&lt;/li&gt;
&lt;li&gt;Fixed items never move, and nothing lands on top of them.&lt;/li&gt;
&lt;li&gt;Flexible tasks keep their order and only move later, and only as far as they must.&lt;/li&gt;
&lt;li&gt;Gaps stay gaps. Nothing slides up to fill the hole you left. A Pack button does that when you ask.&lt;/li&gt;
&lt;li&gt;A task that no longer fits before 23:00 goes to a Not scheduled list. It's never deleted and never silently moved to tomorrow.&lt;/li&gt;
&lt;li&gt;Dragging back to where you started restores the day exactly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rule 4 surprised me. Closing gaps automatically felt like the planner was guessing. Leaving holes alone makes every move predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is built
&lt;/h2&gt;

&lt;p&gt;The planner is one pure function. It takes the day's items and a change, and returns the new layout. The page calls it on every pointer move, animates the cards to the previewed positions with CSS transforms, and commits the same result when you let go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brief&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;    &lt;span class="c1"&gt;// the whole day, re-laid out&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shifted&lt;/span&gt;  &lt;span class="c1"&gt;// the tasks that were pushed later&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tray&lt;/span&gt;     &lt;span class="c1"&gt;// the tasks that no longer fit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I checked it
&lt;/h2&gt;

&lt;p&gt;A fuzz test plays 50,000 random drags on random days and checks the rules after each one. It also compares every answer with a brute force version that scans the day in five minute steps. If a task is allowed to move earlier, or the tasks that follow get five extra minutes, the test fails.&lt;/p&gt;

&lt;p&gt;The page is tested in Chromium and Firefox with a real mouse and keyboard, and the cards on screen are compared with the planner's answer. A Content-Security-Policy blocks requests to other sites. I haven't tested Safari or a real phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It's one 178 KB HTML file, MIT licensed, no account needed. The live page has a sample day, so drag "Write the brief" down to 10:00 and watch the rest move. The code is on &lt;a href="https://github.com/Arthur031221/Jostle" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What I'd like to hear: does "later only, no backfill" match how you expect a day to re-flow?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
