<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Build Log by Nitin Sijwali]]></title><description><![CDATA[Notes from building small, free, open-source projects in public: Fenceline, a pixel territory game that runs from a single HTML file, and yamlreel, a Python too]]></description><link>https://nitinsijwali.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 11:39:32 GMT</lastBuildDate><atom:link href="https://nitinsijwali.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[yamlreel: turn one YAML file into a finished video]]></title><description><![CDATA[yamlreel is a free, open-source Python tool that turns one YAML file into a finished video: animated diagrams, typed code, original music, word-by-word…]]></description><link>https://nitinsijwali.hashnode.dev/yamlreel-turn-one-yaml-file-into-a-finished-video</link><guid isPermaLink="true">https://nitinsijwali.hashnode.dev/yamlreel-turn-one-yaml-file-into-a-finished-video</guid><category><![CDATA[Python]]></category><category><![CDATA[open source]]></category><category><![CDATA[video]]></category><dc:creator><![CDATA[Nitin Sijwali]]></dc:creator><pubDate>Fri, 06 Nov 2026 15:37:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aedef16d00d08a4784e09e2/535d5768-fda9-4da8-9c81-8c30c7c8890b.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>yamlreel is a free, open-source Python tool that turns one YAML file into a finished video: animated diagrams, typed code, original music, word-by-word subtitles and an optional voiceover. Everything renders on your machine. This post covers what it makes, how to try it in about five minutes, and the design choices behind it.</p>
<p>Last updated 6 Nov 2026, for version 0.3.1. The commands and output layout below come from the project README and release notes.</p>
<blockquote>
<p><strong>Status at a glance</strong></p>
<ul>
<li><strong>Install:</strong> <code>pip install "yamlreel[ffmpeg]"</code> (Python 3.10 or newer)</li>
<li><strong>Source:</strong> <a href="https://github.com/nsijwali/yamlreel">https://github.com/nsijwali/yamlreel</a> (MIT)</li>
<li><strong>Package:</strong> <a href="https://pypi.org/project/yamlreel/">https://pypi.org/project/yamlreel/</a></li>
<li><strong>Output:</strong> vertical 1080x1920, landscape 1920x1080 and square 1080x1080</li>
<li><strong>Built with:</strong> Python. Core dependencies are <code>pillow</code>, <code>numpy</code> and <code>pyyaml</code>, and ffmpeg comes from the <code>[ffmpeg]</code> extra</li>
<li><strong>Agent skill:</strong> a Claude Code skill drives it for you: <a href="https://github.com/nsijwali/yamlreel-video-skills">https://github.com/nsijwali/yamlreel-video-skills</a></li>
</ul>
</blockquote>
<h2>Why I built it</h2>
<p>I make short system-design explainers for YouTube Shorts and LinkedIn, and I wanted to stop opening a video editor every time. Editors make you drag clips around. A video is really a list of scenes, so I made the list the interface. You write the scenes, and the tool does the rest.</p>
<h2>The idea: a video is a list of scenes</h2>
<p>A spec is a short YAML file. Each scene has a type, a length and optional fields.</p>
<pre><code class="language-yaml">meta: {title: "Cache stampede in 45 seconds", tags: [systemdesign, caching]}
theme: midnight
formats: [vertical, landscape]
scenes:
  - type: type
    text: What is a cache stampede?
    seconds: 3
  - type: bullets
    title: The fix
    items: [Let one request rebuild the key, Serve stale while it refreshes, Add jitter to every TTL]
  - type: stat
    value: "10,000 → 1"
    label: database queries per expiry
</code></pre>
<p>Why YAML: the spec is plain text, so it lives in git, diffs cleanly and can be generated or edited by a script. A typo is an error before rendering, not a surprise after ten minutes of encoding.</p>
<h2>Try it in five minutes</h2>
<pre><code class="language-bash">python3 -m venv .venv &amp;&amp; source .venv/bin/activate
pip install "yamlreel[ffmpeg]"

yamlreel init system-design my-video.yaml
yamlreel render my-video.yaml --preview
yamlreel render my-video.yaml
</code></pre>
<p><code>--preview</code> renders at half size and 12 fps, so you can check layout and timing in seconds. The full render writes one folder per format.</p>
<pre><code class="language-plaintext">yamlreel-output/
├── vertical/
│   ├── video.mp4
│   ├── poster.jpg
│   └── contact.png
└── share-copy.txt
</code></pre>
<p>Open <code>contact.png</code> first. It shows two labelled stills per scene, so you can spot overlap or a missing reveal before you upload. <code>share-copy.txt</code> holds the title, description and hashtags, ready to paste.</p>
<h2>What it can draw</h2>
<table>
<thead>
<tr>
<th>Scene</th>
<th>What it shows</th>
</tr>
</thead>
<tbody><tr>
<td><code>type</code></td>
<td>one line typed out with a cursor, good for a hook</td>
</tr>
<tr>
<td><code>bullets</code></td>
<td>points that appear one at a time</td>
</tr>
<tr>
<td><code>code</code></td>
<td>code typed on screen with highlighted lines</td>
</tr>
<tr>
<td><code>stat</code></td>
<td>one big number, optionally counting up</td>
</tr>
<tr>
<td><code>diagram</code></td>
<td>boxes and arrows that appear in order, with packets flowing</td>
</tr>
<tr>
<td><code>table</code></td>
<td>rows that appear one by one with a scan bar walking down them</td>
</tr>
<tr>
<td><code>array</code></td>
<td>cells that take sorted inserts, pushes and pops</td>
</tr>
<tr>
<td><code>cta</code></td>
<td>an end card with a question and lettered answer chips</td>
</tr>
<tr>
<td><code>shot</code></td>
<td>a screenshot of a URL or image with a slow camera move</td>
</tr>
</tbody></table>
<h2>Let an AI agent drive it</h2>
<p>There is also an Agent Skill, <code>yamlreel-video-skills</code>, for Claude Code and compatible clients. You ask for a video, for example "make a 15 second Short about Python f-string debugging", and the agent writes the spec with <code>yamlreel story</code>, previews it, checks the contact sheet and renders the final video. The skill supports macOS and Linux. Windows is untested and unsupported for now. Repo: <a href="https://github.com/nsijwali/yamlreel-video-skills">https://github.com/nsijwali/yamlreel-video-skills</a></p>
<h2>Three decisions worth explaining</h2>
<p><strong>Music is generated, not licensed.</strong> There are five styles, from plucked strings in a Japanese scale to lo-fi beats. Because the notes are produced on your machine, there is nothing to license or credit.</p>
<p><strong>Subtitles are estimated, and the tool says so.</strong> With <code>subtitles: true</code>, narrated scenes get captions that show three to five words at a time with the spoken word highlighted. Without a word-timing file, yamlreel works out timing from the audio by finding loud parts and silent gaps. On a fast 40-second recording the average error was about 0.2 seconds per word. That is an estimate, so the tool prints a warning, and you can supply exact times with <code>narration_words</code>.</p>
<p><strong>Unknown keys are errors.</strong> A misspelled field such as <code>titel</code> stops the render with a message that lists the allowed fields. Silent typos are the most annoying bug in a config-driven tool.</p>
<h2>Known limits</h2>
<ul>
<li>Fonts: yamlreel draws text with the free DejaVu fonts. Linux usually has them. On macOS run <code>brew install --cask font-dejavu</code>.</li>
<li>Voiceover is optional and needs a separate speech server such as VoiceStudio. It is not bundled, and the agent skill renders music only by default.</li>
<li>Subtitle timing is an estimate unless you provide a word-timing file.</li>
</ul>
<h2>FAQ</h2>
<p><strong>Do I need a video editor?</strong> No. The output is a finished MP4 with audio.</p>
<p><strong>Does it need the internet?</strong> Rendering is local. Only <code>shot</code> scenes that capture a URL need a network.</p>
<p><strong>Can it write the script for me?</strong> <code>yamlreel story outline.yaml</code> turns a short outline into a starter spec with a hook, a problem, one scene per step and a question end card. It works offline and gives the same spec for the same outline.</p>
<p><strong>Can I use it commercially?</strong> Yes, it is MIT licensed.</p>
<h2>Try it and tell me what breaks</h2>
<p>Start with the example: <code>examples/cache-stampede-45s.yaml</code> in the repo, rendered with <code>--no-voice</code>. If something looks wrong, open an issue at <a href="https://github.com/nsijwali/yamlreel/issues">https://github.com/nsijwali/yamlreel/issues</a> with your system and the spec. Contributors are welcome too.</p>
<h2>What else I'm building</h2>
<p>Fenceline is a pixel territory game that runs from a single HTML file: <a href="https://fenceline.nitinsijwali.com">https://fenceline.nitinsijwali.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Building Fenceline: a Qix-style game in a single HTML file]]></title><description><![CDATA[Fenceline is a free Qix-style territory game that runs from one HTML file of about 58 KB. No login, no install, no build step. This post covers the three…]]></description><link>https://nitinsijwali.hashnode.dev/building-fenceline-a-qix-style-game-in-a-single-html-file</link><guid isPermaLink="true">https://nitinsijwali.hashnode.dev/building-fenceline-a-qix-style-game-in-a-single-html-file</guid><category><![CDATA[Game Development]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[HTML5]]></category><dc:creator><![CDATA[Nitin Sijwali]]></dc:creator><pubDate>Fri, 06 Nov 2026 15:24:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aedef16d00d08a4784e09e2/77dde23f-7eaa-46db-be2c-537edf2c2869.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Fenceline is a free Qix-style territory game that runs from one HTML file of about 58 KB. No login, no install, no build step. This post covers the three decisions that shaped it and the bugs that taught me the most, with the real code.</p>
<p>Last updated 6 Nov 2026. Every snippet below is copied from the shipped file, and the layout and flood-fill bugs were reproduced in headless Chromium tests before I fixed them.</p>
<blockquote>
<p><strong>Status at a glance</strong></p>
<ul>
<li><strong>Play:</strong> <a href="https://fenceline.nitinsijwali.com">https://fenceline.nitinsijwali.com</a></li>
<li><strong>Source:</strong> <a href="https://github.com/nsijwali/fenceline">https://github.com/nsijwali/fenceline</a> (MIT)</li>
<li><strong>Board:</strong> 96x64 cells in landscape, 64x96 in portrait</li>
<li><strong>Goal:</strong> claim 70% of the board to clear a level</li>
<li><strong>Tested:</strong> desktop Chromium, small portrait and short landscape viewports. Not yet verified on a real iPhone.</li>
</ul>
</blockquote>
<h2>The rules, in one paragraph</h2>
<p>You walk along the walls. Step into open ground and you draw a fence. Close the loop and everything on the side without enemies becomes yours. Enemies and a pterodactyl try to cut your fence, and you have three lives.</p>
<h2>Decision 1: flood fill from the enemies, not from the player</h2>
<p>The classic problem is deciding which side of a new fence is "inside". Geometry tricks break on odd shapes. Instead, I flood outward from every enemy through open cells. Whatever the flood cannot reach gets claimed.</p>
<pre><code class="language-js">while (h &lt; t) {
  const i = q[h++], x = i % W, y = (i / W) | 0;
  if (x &gt; 0     &amp;&amp; grid[i - 1] === 0 &amp;&amp; !vis[i - 1]) { vis[i - 1] = 1; q[t++] = i - 1; }
  if (x &lt; W - 1 &amp;&amp; grid[i + 1] === 0 &amp;&amp; !vis[i + 1]) { vis[i + 1] = 1; q[t++] = i + 1; }
  // same for up and down
}
for (let i = 0; i &lt; N; i++) if (grid[i] === 0 &amp;&amp; !vis[i]) { grid[i] = 1; filled++; }
</code></pre>
<p>Why this works: it handles any number of enemies and any fence shape with one pass over the grid, and there is no polygon maths to get wrong.</p>
<h2>Decision 2: pixel look from a handful of tokens</h2>
<p>The board is drawn on a canvas as chunky cells, and each level look is a small set of colour tokens per theme. A new look is a few lines of data, not new drawing code. The pixel font is built in multiples of 9px so text stays crisp.</p>
<h2>Decision 3: music with WebAudio, scheduled ahead</h2>
<p>A <code>setInterval</code> timer is not accurate enough to play notes on time. So a 25 ms timer only schedules notes up to 0.18 s ahead on the audio clock, and the audio clock plays them precisely.</p>
<pre><code class="language-js">function musicTick() {
  if (!M.on || !AC) return;
  const T = TRACKS[M.track], sp = 60 / T.bpm / 4;
  if (M.nextT &lt; AC.currentTime - .5) M.nextT = AC.currentTime + .05;
  while (M.nextT &lt; AC.currentTime + .18) {
    musicStep(T, M.step, M.nextT);
    M.nextT += sp; M.step = (M.step + 1) % (T.bars.length * 16);
  }
}
</code></pre>
<p>The <code>- .5</code> line is the guard. Browsers throttle timers in background tabs. Without it, the scheduler wakes up far behind the clock and plays a burst of catch-up notes. With it, a stalled scheduler skips ahead and resumes in time.</p>
<h2>What broke</h2>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td>HUD overflowed and sat under the notch on phones</td>
<td>Fixed widths and no safe-area padding</td>
<td><code>env(safe-area-inset-*)</code> padding and compact HUD numbers</td>
</tr>
<tr>
<td>Intro was clipped on wide phones in landscape</td>
<td>Overlay was sized to the board, not the screen</td>
<td>Full-viewport overlay and a three-column landscape layout</td>
</tr>
<tr>
<td>About 2px of overflow on small portrait screens</td>
<td>A focus <code>outline</code> counts toward scroll width</td>
<td>Use <code>box-shadow</code> for the halo instead</td>
</tr>
<tr>
<td>Players died and did not know why</td>
<td>No feedback on the cause of death</td>
<td>Popup and game-over text now name the cause</td>
</tr>
<tr>
<td>An enemy's whole region was claimed</td>
<td>The flood was seeded from a cell that had just become fence</td>
<td>Seed from the cell, else its open neighbours</td>
</tr>
<tr>
<td>A burst of notes after switching tabs</td>
<td>Throttled timer fell behind the audio clock</td>
<td>Skip-ahead guard shown above</td>
</tr>
</tbody></table>
<p>The last two came from a reader's comment on the first version of this project. Review from strangers found things my own testing missed.</p>
<h2>The bug that was hardest to see</h2>
<p>If an enemy overlapped your fence while you were invincible, the cell it stood on became fence. The flood started on a wall, found no open cells, and the enemy's region was treated as unreachable. The game then filled it in and took the enemy's territory for you.</p>
<pre><code class="language-js">for (const [dx, dy] of [[0,0],[-1,0],[1,0],[0,-1],[0,1]]) {
  const s = idx(cx + dx, cy + dy);
  if (grid[s] === 0) { /* seed here */ }
}
</code></pre>
<p>Seeding from the cell and its four neighbours fixes it. The lesson: a flood fill is only as correct as its starting cells.</p>
<h2>FAQ</h2>
<p><strong>Does it work offline?</strong> Yes. It is one file with no network calls.</p>
<p><strong>Does it work on phones?</strong> Swipe to move. Portrait and landscape layouts are both handled, but I have only tested them in emulation so far.</p>
<p><strong>Can I fork it?</strong> Yes, it is MIT licensed.</p>
<p><strong>How do I win a level?</strong> Claim 70% of the board without losing all three lives.</p>
<h2>Try it and break it</h2>
<p>Play it at <a href="https://fenceline.nitinsijwali.com">https://fenceline.nitinsijwali.com</a>. If something looks wrong on your phone or browser, open an issue at <a href="https://github.com/nsijwali/fenceline/issues">https://github.com/nsijwali/fenceline/issues</a> with your device and what you saw. Those reports are the most useful thing you can send.</p>
<h2>What else I'm building</h2>
<p>yamlreel is a Python tool that turns one YAML file into a finished video: <a href="https://github.com/nsijwali/yamlreel">https://github.com/nsijwali/yamlreel</a></p>
]]></content:encoded></item></channel></rss>