<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://0xprathamesh.is-a.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://0xprathamesh.is-a.dev/" rel="alternate" type="text/html" /><updated>2026-07-01T04:23:46+00:00</updated><id>https://0xprathamesh.is-a.dev/feed.xml</id><title type="html">B-logs by Prathamesh</title><subtitle>A developer blog about software engineering, systems, and the craft of building for the web — with interactive, runnable code.</subtitle><author><name>Prathamesh</name><email>prathameshind305@gmail.com</email></author><entry><title type="html">Introducing Raphael: The Sardonic ADHD Advisor Inside Your Desktop HUD</title><link href="https://0xprathamesh.is-a.dev/posts/introducing-raphael-the-sardonic-adhd-hud/" rel="alternate" type="text/html" title="Introducing Raphael: The Sardonic ADHD Advisor Inside Your Desktop HUD" /><published>2026-06-22T04:30:00+00:00</published><updated>2026-06-22T04:30:00+00:00</updated><id>https://0xprathamesh.is-a.dev/posts/introducing-raphael-the-sardonic-adhd-hud</id><content type="html" xml:base="https://0xprathamesh.is-a.dev/posts/introducing-raphael-the-sardonic-adhd-hud/"><![CDATA[<p>We’ve all been there. You sit down to write code or update a doc. You open a browser tab to check a reference, and twenty minutes later you’re deep in a Wikipedia rabbit hole reading about medieval trebuchet engineering.</p>

<p>Standard website blockers don’t work for engineers or power users — they’re too easy to disable, too rigid, and completely context-blind. Block Chrome and you lose everything: the critical docs page <em>and</em> the social media feed you were trying to avoid.</p>

<p>That’s why I built <strong>Raphael</strong>.</p>

<p>Raphael is a sardonically supportive <strong>ADHD cognitive advisor</strong> that integrates directly into your Linux desktop as a cockpit-style HUD. It watches your active workspace context, dynamically categorizes your behavior using an LLM pipeline, calls out focus slippages with ruthless wit, and locks your screen with interactive psychological challenges when you drift too far off track.</p>

<hr />

<h2 id="core-architecture">Core Architecture</h2>

<p>Raphael splits its footprint between a lightweight desktop layer and a background analytics daemon.</p>

<p><strong>QML HUD Frontend</strong> — A pair of frosted-glass overlay panels (<code class="language-plaintext highlighter-rouge">leftQuotePanel</code>, <code class="language-plaintext highlighter-rouge">rightInsightPanel</code>) pinned above your windows using KDE Plasma 6 (Qt Quick/QML). Streams live advisor observations, active telemetry, and your real-time <strong>Focus Efficiency Rating</strong>.</p>

<p><strong>Flask Core Daemon</strong> — A zero-overhead background engine polling system state every few seconds. Parses active window properties, handles tracking triggers, serves a local analytics dashboard, and fires real-time classification requests against a <strong>Groq AI pipeline</strong>.</p>

<hr />

<h2 id="key-features">Key Features</h2>

<h3 id="deep-window--tab-dissection">Deep Window &amp; Tab Dissection</h3>

<p>Most focus trackers read process strings like <code class="language-plaintext highlighter-rouge">google-chrome</code> or <code class="language-plaintext highlighter-rouge">vscodium</code> and stop there. Raphael uses direct active window parsing (optimized for XWayland) to extract the <strong>exact browser tab title</strong> or current file path — stripping the application wrapper entirely. It knows the difference between <code class="language-plaintext highlighter-rouge">github.com/pulls</code> and an entertainment blog.</p>

<h3 id="the-distraction-challenge">The Distraction Challenge</h3>

<p>When the polling module detects you’ve lingered on an unapproved window past your threshold (default: 30 seconds), it overrides your layout with a focus interruption. Not a warning banner — a pop-up that forces you to textually justify your action:</p>

<blockquote>
  <p><em>“Explain exactly how this tab aids your immediate deployment target, or return to work.”</em></p>
</blockquote>

<p>Session metrics don’t resume until you justify the context switch or close the window.</p>

<h3 id="dynamic-mid-session-context-shifting">Dynamic Mid-Session Context Shifting</h3>

<p>You don’t need to edit config files to change Raphael’s behavior mid-session. The integrated <strong>Chat Console</strong> accepts natural language updates on the fly:</p>

<ul>
  <li><em>“Remind me to stretch every 20 minutes.”</em></li>
  <li><em>“From now on, ignore Spotify track changes.”</em></li>
  <li><em>“Escalate the aggression matrix — I’m drifting.”</em></li>
</ul>

<hr />

<h2 id="project-structure">Project Structure</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>raphael/
├── run.sh                  # Environment setup and daemon launcher
├── daemon.log              # Main background log output
└── contents/
    ├── ui/
    │   ├── main.qml        # Core Plasma widget and control surface
    │   ├── ChatPanel.qml   # Chat shell for mid-session rule overrides
    │   └── daemon/
    │       ├── raphael_core_daemon.py   # Parser, Flask app, Groq wrapper
    │       └── dashboard.html           # Local analytics telemetry UI
    └── config/
        └── main.xml        # Preserved state schemas for the desktop layer
</code></pre></div></div>

<hr />

<h2 id="under-the-hood-qml-telemetry-sync">Under the Hood: QML Telemetry Sync</h2>

<p>The HUD polls the Flask daemon on a tight loop and pushes live data directly into the overlay:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">syncNetworkPayload</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">xhr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">XMLHttpRequest</span><span class="p">();</span>
    <span class="nx">xhr</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span>
        <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">http://127.0.0.1:5757/telemetry_v3?caring=</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">caringLevel</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">&amp;ai_track=</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">aiWindowTracking</span>
    <span class="p">);</span>
    <span class="nx">xhr</span><span class="p">.</span><span class="nx">onreadystatechange</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">if </span><span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">DONE</span> <span class="o">&amp;&amp;</span> <span class="nx">xhr</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">var</span> <span class="nx">res</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">responseText</span><span class="p">);</span>

            <span class="nx">sharedState</span><span class="p">.</span><span class="nx">currentQuote</span>       <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">quote</span><span class="p">.</span><span class="nx">text</span><span class="p">;</span>
            <span class="nx">sharedState</span><span class="p">.</span><span class="nx">focusEfficiencyText</span> <span class="o">=</span> <span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">efficiency</span> <span class="o">*</span> <span class="mi">100</span><span class="p">)</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">% FOCUS</span><span class="dl">"</span><span class="p">;</span>

            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">HUD Matrix Sync Complete // Status: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">};</span>
    <span class="nx">xhr</span><span class="p">.</span><span class="nf">send</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="web-telemetry-dashboard">Web Telemetry Dashboard</h2>

<p>The Flask daemon also hosts a full session dashboard at <code class="language-plaintext highlighter-rouge">http://127.0.0.1:5757/dashboard</code>, built with <strong>Chart.js</strong>:</p>

<ul>
  <li><strong>Focus Timeline</strong> — Minute-by-minute view charting active focus against distractions.</li>
  <li><strong>Window Breakdown</strong> — Doughnut graph showing time distribution across file paths, browser tabs, and applications.</li>
  <li><strong>Session Goals</strong> — Checkable goal metrics generated at session start.</li>
</ul>

<hr />

<h2 id="getting-started">Getting Started</h2>

<p>Requirements: <strong>KDE Plasma 6.0+</strong> and a valid Groq API key.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone the repo</span>
git clone https://github.com/prathameshrp/Raphael.git
<span class="nb">cd </span>Raphael

<span class="c"># Set your API key</span>
<span class="nb">export </span><span class="nv">GROQ_API_KEY</span><span class="o">=</span><span class="s2">"your_api_key_here"</span>

<span class="c"># Launch the daemon</span>
<span class="nb">chmod</span> +x run.sh
./run.sh
</code></pre></div></div>

<p>After the daemon starts, add the <strong>Raphael Widget</strong> from your desktop panel’s widget picker.</p>

<p>To reload the Plasma shell after editing QML files (without interrupting the backend):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>killall plasmashell <span class="o">&amp;&amp;</span> kstart plasmashell
</code></pre></div></div>

<hr />

<h2 id="open-source">Open Source</h2>

<p>Raphael was built for those of us who need more than a stopwatch to stay on track. If you want to configure custom modules, tweak the AI persona rules, or extend the Wayland window reader — the repo is open.</p>

<p>👉 <strong><a href="https://github.com/prathameshrp/Raphael">github.com/prathameshrp/Raphael</a></strong></p>

<p>If Raphael has successfully guilted you back into finishing your codebase today, consider dropping a ⭐️.</p>]]></content><author><name>Prathamesh</name><email>prathameshind305@gmail.com</email></author><category term="KDE" /><category term="Plasma" /><category term="Python" /><category term="AI" /><category term="Productivity" /><summary type="html"><![CDATA[A cockpit-style HUD widget for KDE Plasma that acts as an aggressive, sardonically supportive ADHD cognitive advisor.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xprathamesh.is-a.dev/assets/og/introducing-raphael-the-sardonic-adhd-hud.png" /><media:content medium="image" url="https://0xprathamesh.is-a.dev/assets/og/introducing-raphael-the-sardonic-adhd-hud.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Set Cover Meets Ant Colony: How We Optimized Multi-Store Delivery Routes</title><link href="https://0xprathamesh.is-a.dev/posts/logistic-optimization/" rel="alternate" type="text/html" title="Set Cover Meets Ant Colony: How We Optimized Multi-Store Delivery Routes" /><published>2025-10-10T04:30:00+00:00</published><updated>2025-10-10T04:30:00+00:00</updated><id>https://0xprathamesh.is-a.dev/posts/logistic-optimization</id><content type="html" xml:base="https://0xprathamesh.is-a.dev/posts/logistic-optimization/"><![CDATA[<p>Last semester I co-authored a paper on delivery route optimization that got
published in the Journal of Robotics and Control. The core question: if a user’s
cart has items spread across five different nearby stores, what’s the cheapest,
fastest way to collect everything and deliver it?</p>

<p>This post is a deeper walkthrough of what we actually built and why we made the
choices we did — more than what fits in a paper’s page limit.</p>

<hr />

<h2 id="the-problem-nobody-talks-about">The problem nobody talks about</h2>

<p>Most delivery optimization research assumes a single warehouse. Real
quick-commerce is messier: a user orders pasta, medicine, and phone cable. Those
three items might be in three different stores within 2km. The system has to
decide <em>which stores to visit</em> and <em>in what order</em> — and those are two
fundamentally different problems stacked on top of each other.</p>

<p>We formalized this as two stages:</p>

<ol>
  <li><strong>Store selection</strong> — find the minimal subset of stores that collectively
stock every item in the cart</li>
  <li><strong>Route optimization</strong> — find the cheapest path through those stores to the
delivery address</li>
</ol>

<p>Stage 1 is a <strong>Set Cover Problem</strong>. Stage 2 is a <strong>Traveling Salesman Problem</strong>.
Both are NP-hard. Great.</p>

<hr />

<h2 id="stage-1-modeling-store-selection-as-set-cover">Stage 1: Modeling store selection as Set Cover</h2>

<p>Given a set of required items <code class="language-plaintext highlighter-rouge">I = {i₁, i₂, ..., iₘ}</code> and nearby stores
<code class="language-plaintext highlighter-rouge">S = {s₁, s₂, ..., sₙ}</code>, we build an availability matrix <code class="language-plaintext highlighter-rouge">A</code> where <code class="language-plaintext highlighter-rouge">A[s][i] = 1</code>
if store <code class="language-plaintext highlighter-rouge">s</code> stocks item <code class="language-plaintext highlighter-rouge">i</code>.</p>

<p>The goal is to find the smallest subset <code class="language-plaintext highlighter-rouge">S* ⊆ S</code> such that every item in <code class="language-plaintext highlighter-rouge">I</code> is
covered by at least one store in <code class="language-plaintext highlighter-rouge">S*</code>, while also minimizing total travel cost.</p>

<p>We use a greedy set cover approach: start with an empty <code class="language-plaintext highlighter-rouge">S*</code>, iteratively add
whichever store covers the most currently uncovered items, break ties by
proximity to the delivery address.</p>

<p>Here’s the core of that logic:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; Greedy Set Cover — store selection</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
// Greedy Set Cover
// items: array of item names
// stores: array of {name, stocks: Set of items, cost}

function greedySetCover(items, stores) {
  const required = new Set(items);
  const selected = [];
  let uncovered = new Set(items);

  while (uncovered.size &gt; 0) {
    // Pick store with best coverage/cost ratio
    let best = null, bestScore = -1;

    for (const store of stores) {
      if (selected.includes(store)) continue;
      const covers = [...store.stocks].filter(i =&gt; uncovered.has(i)).length;
      if (covers === 0) continue;
      // Score: items covered per unit cost
      const score = covers / store.cost;
      if (score &gt; bestScore) { bestScore = score; best = store; }
    }

    if (!best) break; // shouldn't happen if stores cover all items
    selected.push(best);
    best.stocks.forEach(i =&gt; uncovered.delete(i));
    console.log(`Selected: ${best.name} → covers [${[...best.stocks].join(', ')}]`);
    console.log(`  Remaining uncovered: [${[...uncovered].join(', ')}]`);
  }

  return selected;
}

const items = ['pasta', 'medicine', 'cable'];
const stores = [
  { name: 'QuickMart',   stocks: new Set(['pasta', 'cable']),    cost: 2 },
  { name: 'HealthPlus',  stocks: new Set(['medicine']),          cost: 3 },
  { name: 'MegaStore',   stocks: new Set(['pasta', 'medicine', 'cable']), cost: 8 },
  { name: 'LocalShop',   stocks: new Set(['pasta']),             cost: 1 },
];

const result = greedySetCover(items, stores);
console.log(`\nFinal stores to visit: ${result.map(s =&gt; s.name).join(' → ')}`);
console.log(`MegaStore covers everything but costs 8. Greedy picks the cheaper combination.`);
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<p>Notice how the greedy approach skips MegaStore (which covers everything, cost 8)
in favor of QuickMart + HealthPlus (covers everything, cost 5). This is the
classical approximation — it’s guaranteed to be within <code class="language-plaintext highlighter-rouge">O(log n)</code> of optimal,
which is the best we can do for NP-hard problems with a polynomial algorithm.</p>

<hr />

<h2 id="stage-2-route-optimization-as-tsp">Stage 2: Route optimization as TSP</h2>

<p>Once we have <code class="language-plaintext highlighter-rouge">S*</code>, we need the shortest path through those stores to the delivery
address. This is a Traveling Salesman Problem on a small graph (usually 2-5
nodes), so even brute force is fast.</p>

<p>The cost matrix captures inter-store distances plus the distance from each store
to the delivery location <code class="language-plaintext highlighter-rouge">d</code>. We want to minimize:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>minimize Σ Cost(sᵢ, sⱼ) for (sᵢ,sⱼ) ∈ route
</code></pre></div></div>

<p>For small <code class="language-plaintext highlighter-rouge">|S*|</code>, nearest-neighbor heuristic gets us close to optimal:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; Nearest-Neighbor TSP — route optimization</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
// Nearest-Neighbor TSP heuristic
// nodes: array of {name, x, y}
// Returns visit order

function dist(a, b) {
  return Math.sqrt((a.x-b.x)**2 + (a.y-b.y)**2);
}

function nearestNeighborTSP(nodes, startName) {
  const unvisited = new Set(nodes.map(n =&gt; n.name));
  let current = nodes.find(n =&gt; n.name === startName);
  unvisited.delete(current.name);
  
  const route = [current];
  let totalCost = 0;

  while (unvisited.size &gt; 0) {
    let nearest = null, nearestDist = Infinity;
    for (const name of unvisited) {
      const node = nodes.find(n =&gt; n.name === name);
      const d = dist(current, node);
      if (d &lt; nearestDist) { nearestDist = d; nearest = node; }
    }
    totalCost += nearestDist;
    route.push(nearest);
    unvisited.delete(nearest.name);
    current = nearest;
  }

  // Return to depot
  totalCost += dist(current, nodes.find(n =&gt; n.name === startName));

  console.log("Route: " + route.map(n =&gt; n.name).join(" → ") + " → depot");
  console.log(`Total cost: ${totalCost.toFixed(2)} units`);
  return { route, totalCost };
}

// Depot at origin, stores at various coordinates
const nodes = [
  { name: 'depot',      x: 0,  y: 0  },
  { name: 'QuickMart',  x: 3,  y: 4  },
  { name: 'HealthPlus', x: 7,  y: 2  },
  { name: 'Customer',   x: 5,  y: 7  },
];

nearestNeighborTSP(nodes, 'depot');
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<hr />

<h2 id="the-ant-colony-optimization-approach">The Ant Colony Optimization approach</h2>

<p>The classical two-stage method gives us a deterministic, predictable answer. But
as the number of deliveries grows, it struggles — the greedy set cover can
miss better combinations, and the TSP heuristic doesn’t adapt to constraints
like traffic congestion or time windows.</p>

<p>ACO takes a completely different approach. It simulates a colony of ants, each
building a route probabilistically. Ants that find better routes deposit more
pheromone, biasing future ants toward those paths. Over hundreds of iterations,
the colony converges on a near-optimal solution.</p>

<p>The key formula is the transition probability — how likely ant <code class="language-plaintext highlighter-rouge">k</code> is to move
from city <code class="language-plaintext highlighter-rouge">i</code> to city <code class="language-plaintext highlighter-rouge">j</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>p_ij^k(t) = [τ_ij(t)]^α · [η_ij]^β  /  Σ [τ_ij(t)]^α · [η_ij]^β
</code></pre></div></div>

<p>Where <code class="language-plaintext highlighter-rouge">τ_ij</code> is pheromone on edge (i,j), <code class="language-plaintext highlighter-rouge">η_ij = 1/distance</code> is visibility,
and <code class="language-plaintext highlighter-rouge">α</code>, <code class="language-plaintext highlighter-rouge">β</code> control the tradeoff between following pheromone vs. picking
nearby nodes.</p>

<p>Pheromone evaporates over time (controlled by <code class="language-plaintext highlighter-rouge">ρ</code>) so bad paths don’t get
permanently reinforced:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>τ_ij(t+n) = ρ · τ_ij(t) + Δτ_ij(t)
</code></pre></div></div>

<p>Here’s a stripped-down simulation to show how pheromone reinforcement works:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; ACO pheromone reinforcement — watch short paths win</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
// Pheromone update simulation — watch good paths get reinforced

function simulateACO(edges, iterations = 5) {
  // Initialize pheromone equally on all edges
  const pheromone = {};
  edges.forEach(e =&gt; { pheromone[e.id] = 1.0; });

  const rho = 0.5;   // evaporation rate
  const Q   = 10;    // pheromone deposit constant

  console.log("Initial pheromone:", JSON.stringify(pheromone));
  console.log("---");

  for (let t = 1; t &lt;= iterations; t++) {
    // Simulate: ants prefer shorter edges, deposit more pheromone there
    edges.forEach(e =&gt; {
      // Evaporate
      pheromone[e.id] *= (1 - rho);
      // Deposit: shorter distance → more ants use it → more pheromone
      const deposit = Q / e.distance;
      pheromone[e.id] += deposit;
    });

    console.log(`Iteration ${t}:`);
    edges.forEach(e =&gt; {
      const bar = "█".repeat(Math.round(pheromone[e.id]));
      console.log(`  ${e.id.padEnd(20)} τ=${pheromone[e.id].toFixed(2)}  ${bar}`);
    });
    console.log("---");
  }

  const best = edges.reduce((a,b) =&gt; pheromone[a.id] &gt; pheromone[b.id] ? a : b);
  console.log(`Strongest pheromone trail: ${best.id} (distance ${best.distance})`);
}

const edges = [
  { id: "depot→QuickMart",  distance: 5  },
  { id: "depot→HealthPlus", distance: 10 },
  { id: "depot→Customer",   distance: 8  },
];

simulateACO(edges, 4);
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<p>Run this and watch: the shorter <code class="language-plaintext highlighter-rouge">depot→QuickMart</code> edge accumulates pheromone
faster than the longer routes, even though all edges start equal. This emergent
bias is what guides the colony toward good solutions without anyone explicitly
programming “prefer short edges.”</p>

<hr />

<h2 id="adding-congestion-as-an-independent-parameter">Adding congestion as an independent parameter</h2>

<p>One of the more interesting things we did was introduce traffic congestion as
a weighted parameter on each node. We generated a synthetic heatmap where
certain areas have high congestion levels, then ran both algorithms on it.</p>

<p>The result was surprising: <strong>in congested scenarios, the classical model
actually beat ACO on cost</strong>. The classical model follows a fixed, mathematically
computed path that sidesteps congested nodes by construction. ACO’s pheromone
trails, built before congestion data is fully incorporated, can get “stuck”
reinforcing paths that happen to run through congested zones.</p>

<p>This gave us the main practical finding of the paper:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; Congestion-aware cost — why environment matters</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
// Simplified congestion-aware cost model
// Shows why algorithm choice depends on environment

function routeCost(route, congestionMap, baseDistances) {
  let total = 0;
  for (let i = 0; i &lt; route.length - 1; i++) {
    const from = route[i], to = route[i+1];
    const baseDist = baseDistances[`${from}-${to}`] || baseDistances[`${to}-${from}`] || 0;
    const congestion = congestionMap[to] || 0;
    // Congestion multiplies effective travel time
    const effectiveCost = baseDist * (1 + congestion);
    total += effectiveCost;
    console.log(`  ${from} → ${to}: base=${baseDist}, congestion=${congestion}, cost=${effectiveCost.toFixed(1)}`);
  }
  return total;
}

const baseDistances = {
  'depot-A': 3, 'depot-B': 5, 'A-C': 4, 'B-C': 2, 'C-customer': 3
};

// High-congestion scenario: node B is in a traffic hotspot
const congestionMap = { A: 0.1, B: 0.9, C: 0.2, customer: 0.0 };

console.log("=== Classical route (avoids B): depot → A → C → customer ===");
const classicalCost = routeCost(
  ['depot','A','C','customer'], congestionMap, baseDistances
);

console.log(`\n=== ACO route (reinforced B as 'short'): depot → B → C → customer ===`);
const acoCost = routeCost(
  ['depot','B','C','customer'], congestionMap, baseDistances
);

console.log(`\nClassical cost: ${classicalCost.toFixed(1)}`);
console.log(`ACO cost:       ${acoCost.toFixed(1)}`);
console.log(classicalCost &lt; acoCost
  ? "→ Classical wins here (congestion punishes ACO's pheromone trail)"
  : "→ ACO still wins despite congestion");
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<hr />

<h2 id="what-the-simulations-showed">What the simulations showed</h2>

<p>We ran both approaches on 100 simulated deliveries in Python and measured
average delivery time and total cost at different scales.</p>

<p>The findings:</p>

<p><strong>Classical (Set Cover + TSP)</strong></p>
<ul>
  <li>Fast and exact for small delivery counts</li>
  <li>Average delivery time increases steeply as volume grows</li>
  <li>Wins on cost in high-congestion environments</li>
  <li>No real-time adaptability</li>
</ul>

<p><strong>ACO</strong></p>
<ul>
  <li>Consistently better average delivery time at high volumes</li>
  <li>Cost scales more gracefully</li>
  <li>Adapts dynamically via pheromone updates</li>
  <li>Struggles above ~80 nodes when time window constraints are strict (ACO2 variant)</li>
</ul>

<p>Neither approach dominates across all conditions. The paper concludes with a
proposal for a <strong>hybrid model</strong> controlled by a parameter <code class="language-plaintext highlighter-rouge">θ</code> that selects the
algorithm based on detected delivery environment — use classical for
low-volume or high-congestion scenarios, ACO for large-scale or dynamic ones.</p>

<hr />

<h2 id="what-id-do-differently">What I’d do differently</h2>

<p>The congestion parameter was static — we baked it into the cost matrix before
running either algorithm. A real system would need live traffic data updating
the weights mid-route. That’s the natural next step: integrating a real-time
traffic API and testing how ACO’s pheromone evaporation rate responds to sudden
congestion spikes vs. how quickly a classical re-plan can compute a new optimal.</p>

<p>The paper’s code and simulation data are available if you want to dig into the
Python implementation.</p>

<!-- 👉 **[Published paper — Journal of Robotics and Control](https://journal.umy.ac.id/index.php/jrc)** -->]]></content><author><name>Prathamesh</name><email>prathameshind305@gmail.com</email></author><category term="Algorithms" /><category term="Optimization" /><category term="Research" /><category term="Python" /><summary type="html"><![CDATA[A breakdown of our published paper comparing a classical set cover + TSP formulation against Ant Colony Optimization for last-mile delivery — what we built, what we found, and why the answer isn't one or the other.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xprathamesh.is-a.dev/assets/og/logistic-optimization.png" /><media:content medium="image" url="https://0xprathamesh.is-a.dev/assets/og/logistic-optimization.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building a Real-Time Virtual Try-On with MediaPipe and a Webcam</title><link href="https://0xprathamesh.is-a.dev/posts/virtual-try-on/" rel="alternate" type="text/html" title="Building a Real-Time Virtual Try-On with MediaPipe and a Webcam" /><published>2025-04-08T04:30:00+00:00</published><updated>2025-04-08T04:30:00+00:00</updated><id>https://0xprathamesh.is-a.dev/posts/virtual-try-on</id><content type="html" xml:base="https://0xprathamesh.is-a.dev/posts/virtual-try-on/"><![CDATA[<p>Most virtual try-on demos you see online are either pre-recorded, server-heavy,
or involve a 30-second upload cycle. I wanted something that felt instant —
point a webcam at yourself, and a shirt just appears on you, live.</p>

<p>Here’s how it works and what I learned building it.</p>

<h2 id="the-architecture">The architecture</h2>

<p>The pipeline is split across two servers deliberately:</p>

<ul>
  <li>A <strong>Node.js/Express</strong> frontend server handles the webcam stream and serves
the UI. Every 500ms it captures a frame from the browser, POSTs it as a JPEG,
and replaces the output image with whatever comes back.</li>
  <li>A <strong>Flask/Python</strong> backend does the actual computer vision — pose detection,
clothing resize and placement, alpha compositing — and returns a processed JPEG.</li>
</ul>

<p>Keeping them separate meant I could iterate on the CV logic without touching the
frontend, and swap out the pose model without breaking the web layer.</p>

<h2 id="pose-detection-with-mediapipe">Pose detection with MediaPipe</h2>

<p>MediaPipe’s Pose solution gives you 33 body landmarks per frame. For draping a
shirt, you only really need four of them: left shoulder, right shoulder, left hip,
and right hip.</p>

<p>From those four points you can derive everything you need:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">shoulder_distance</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="nf">norm</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">left_shoulder</span><span class="p">)</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">right_shoulder</span><span class="p">))</span>
<span class="n">shirt_width</span>  <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">shoulder_distance</span> <span class="o">*</span> <span class="mf">1.5</span><span class="p">)</span>
<span class="n">shirt_height</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">shoulder_hip_distance</span> <span class="o">*</span> <span class="mf">1.2</span><span class="p">)</span>
</code></pre></div></div>

<p>The multipliers (<code class="language-plaintext highlighter-rouge">1.5</code>, <code class="language-plaintext highlighter-rouge">1.2</code>) were tuned by feel — the shirt needs to be a bit
wider than the shoulder span and slightly taller than the torso to look natural.</p>

<h2 id="alpha-compositing">Alpha compositing</h2>

<p>The clothing PNG has a transparency channel, so the overlay is a standard
alpha blend per pixel:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alpha</span> <span class="o">=</span> <span class="n">resized_clothing</span><span class="p">[:,</span> <span class="p">:,</span> <span class="mi">3</span><span class="p">]</span> <span class="o">/</span> <span class="mf">255.0</span>
<span class="n">frame</span><span class="p">[</span><span class="n">y1</span><span class="p">:</span><span class="n">y2</span><span class="p">,</span> <span class="n">x1</span><span class="p">:</span><span class="n">x2</span><span class="p">,</span> <span class="n">c</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
    <span class="n">clothing</span><span class="p">[:,</span> <span class="p">:,</span> <span class="n">c</span><span class="p">]</span> <span class="o">*</span> <span class="n">alpha</span> <span class="o">+</span> <span class="n">frame</span><span class="p">[</span><span class="n">y1</span><span class="p">:</span><span class="n">y2</span><span class="p">,</span> <span class="n">x1</span><span class="p">:</span><span class="n">x2</span><span class="p">,</span> <span class="n">c</span><span class="p">]</span> <span class="o">*</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">-</span> <span class="n">alpha</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The trickier part is clamping the bounding box so the shirt doesn’t bleed outside
the frame when you’re close to the camera edge — that’s what the <code class="language-plaintext highlighter-rouge">crop_x1/y1</code>
offset math handles.</p>

<h2 id="what-surprised-me">What surprised me</h2>

<p><strong>MediaPipe is fast.</strong> Running pose detection on every frame at 500ms intervals
felt seamless on a mid-range laptop. The bottleneck turned out to be the Node →
Python round-trip over localhost HTTP, not the CV itself.</p>

<p><strong>PNG alpha quality matters a lot.</strong> A clothing image with a clean, well-cut
alpha channel looks convincing. A sloppy one with fringing makes the whole thing
look broken regardless of how accurate your landmark math is.</p>

<h2 id="whats-next">What’s next</h2>

<p>Right now it only supports one hardcoded shirt. The obvious next step is a
clothing selector — a small gallery where you pick a PNG and the backend swaps
the overlay. The pose pipeline doesn’t need to change at all for that.</p>

<p>The repo is open if you want to drop in your own clothing images and play with
the multipliers.</p>

<p>👉 <strong><a href="https://github.com/prathameshrp/virtual-tryon">github.com/prathameshrp</a></strong></p>]]></content><author><name>Prathamesh</name><email>prathameshind305@gmail.com</email></author><category term="Python" /><category term="Computer Vision" /><category term="MediaPipe" /><category term="Flask" /><category term="Node.js" /><summary type="html"><![CDATA[How I built a real-time clothing overlay that detects your pose landmarks and drapes a shirt over you, live from a webcam feed.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xprathamesh.is-a.dev/assets/og/virtual-try-on.png" /><media:content medium="image" url="https://0xprathamesh.is-a.dev/assets/og/virtual-try-on.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">cgPaint: Classic CG Algorithms, Pixel by Pixel</title><link href="https://0xprathamesh.is-a.dev/posts/cg-paint/" rel="alternate" type="text/html" title="cgPaint: Classic CG Algorithms, Pixel by Pixel" /><published>2024-12-02T04:30:00+00:00</published><updated>2024-12-02T04:30:00+00:00</updated><id>https://0xprathamesh.is-a.dev/posts/cg-paint</id><content type="html" xml:base="https://0xprathamesh.is-a.dev/posts/cg-paint/"><![CDATA[<p>For my computer graphics mini project I built a paint app where every primitive
is implemented from scratch. No <code class="language-plaintext highlighter-rouge">ctx.lineTo</code> for lines. No <code class="language-plaintext highlighter-rouge">ctx.arc</code> for circles.
Just the raw algorithms, plotting one pixel at a time.</p>

<p>The most useful part turned out to be the speed slider: slow an algorithm down
to 100ms per step and you can <em>watch</em> Bresenham decide whether to step right or
diagonally. That made the theory click in a way lecture notes didn’t.</p>

<h2 id="dda-vs-bresenham-two-ways-to-draw-a-line">DDA vs Bresenham: two ways to draw a line</h2>

<p>DDA (Digital Differential Analyzer) is the intuitive approach — divide the
total change in x and y by the number of steps, then increment:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; DDA Line — try changing the endpoints</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
// DDA: straightforward but uses floating-point division
function ddaLine(x1, y1, x2, y2) {
  const dx = x2 - x1, dy = y2 - y1;
  const steps = Math.max(Math.abs(dx), Math.abs(dy));
  const xInc = dx / steps, yInc = dy / steps;

  const pixels = [];
  let x = x1, y = y1;
  for (let i = 0; i &lt;= steps; i++) {
    pixels.push(`(${Math.round(x)}, ${Math.round(y)})`);
    x += xInc;
    y += yInc;
  }
  return pixels;
}

// Draw from (0,0) to (7,3)
const pixels = ddaLine(0, 0, 7, 3);
console.log("Pixels plotted:");
console.log(pixels.join(" → "));
console.log(`\nTotal: ${pixels.length} pixels for a 7-unit line`);
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<p>Bresenham eliminates the floating point entirely using an integer error term.
At each step, the error tells you whether the next pixel should be directly
right or diagonally up-right:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; Bresenham Line — inspect the error term</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
// Bresenham: pure integer arithmetic, tracks an "error" budget
function bresenhamLine(x1, y1, x2, y2) {
  const dx = Math.abs(x2 - x1), dy = Math.abs(y2 - y1);
  const sx = x1 &lt; x2 ? 1 : -1, sy = y1 &lt; y2 ? 1 : -1;
  let err = dx - dy;

  const steps = [];
  while (true) {
    steps.push({ x: x1, y: y1, err });
    if (x1 === x2 &amp;&amp; y1 === y2) break;
    const e2 = 2 * err;
    if (e2 &gt; -dy) { err -= dy; x1 += sx; }
    if (e2 &lt;  dx) { err += dx; y1 += sy; }
  }
  return steps;
}

const steps = bresenhamLine(0, 0, 7, 3);
console.log("pixel        | error after step");
console.log("-------------|------------------");
steps.forEach(s =&gt; {
  const pixel = `(${s.x}, ${s.y})`.padEnd(13);
  console.log(`${pixel}| err = ${s.err}`);
});
// Watch how the error accumulates and resets — that's what controls diagonal steps
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<p>Run both on the same line: they produce identical pixels. The difference shows
up at scale — Bresenham avoids floating-point rounding that accumulates across
millions of lines in a real renderer.</p>

<h2 id="bresenhams-circle-computing-one-octant-mirroring-eight">Bresenham’s circle: computing one octant, mirroring eight</h2>

<p>A circle has 8-fold symmetry. Bresenham’s algorithm exploits this: compute
one octant (45°), then mirror it to all eight positions simultaneously.
For a radius of <code class="language-plaintext highlighter-rouge">r</code>, you only do <code class="language-plaintext highlighter-rouge">~r</code> iterations to draw the full circle:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; Bresenham Circle — 8-way symmetry</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
function bresenhamCircle(xc, yc, r) {
  let x = 0, y = r, d = 3 - 2 * r;
  const byOctant = Array.from({length: 8}, () =&gt; []);

  while (y &gt;= x) {
    // The 8 mirror points from one (x,y) computation
    byOctant[0].push([xc + x, yc + y]);
    byOctant[1].push([xc - x, yc + y]);
    byOctant[2].push([xc + x, yc - y]);
    byOctant[3].push([xc - x, yc - y]);
    byOctant[4].push([xc + y, yc + x]);
    byOctant[5].push([xc - y, yc + x]);
    byOctant[6].push([xc + y, yc - x]);
    byOctant[7].push([xc - y, yc - x]);

    x++;
    d = d &gt; 0 ? d + 4 * (x - y--) + 10 : d + 4 * x + 6;
  }

  const totalPixels = byOctant.reduce((s, o) =&gt; s + o.length, 0);
  console.log(`Circle r=${r}: ${x} iterations → ${totalPixels} pixels`);
  console.log(`\nFirst octant (the only one computed from scratch):`);
  byOctant[0].forEach(([px, py]) =&gt; console.log(`  (${px}, ${py})`));
  console.log(`\n...then mirrored to 7 more octants automatically.`);
}

bresenhamCircle(10, 10, 6);
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<h2 id="scan-line-polygon-fill">Scan-line polygon fill</h2>

<p>Given an arbitrary polygon, the scan-line algorithm sweeps horizontal lines
top to bottom. For each scan line it finds every edge intersection, sorts them,
and fills between pairs. The tricky part is the edge table — you only activate
edges whose <code class="language-plaintext highlighter-rouge">yMin</code> the current scan line has reached:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; Scan-line fill — watch the sweep</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
function scanLineFill(polygon) {
  // Build edge table (skip horizontal edges)
  const edges = [];
  const n = polygon.length;
  for (let i = 0; i &lt; n; i++) {
    const p1 = polygon[i], p2 = polygon[(i + 1) % n];
    if (p1.y === p2.y) continue;
    edges.push({
      yMin: Math.min(p1.y, p2.y),
      yMax: Math.max(p1.y, p2.y),
      xAtYMin: p1.y &lt; p2.y ? p1.x : p2.x,
      slopeInv: (p2.x - p1.x) / (p2.y - p1.y),
    });
  }

  const yMin = Math.min(...polygon.map(p =&gt; p.y));
  const yMax = Math.max(...polygon.map(p =&gt; p.y));

  console.log("y  | active edges  | fill range");
  console.log("---|---------------|------------");

  for (let y = yMin; y &lt;= yMax; y++) {
    const xs = edges
      .filter(e =&gt; y &gt;= e.yMin &amp;&amp; y &lt; e.yMax)
      .map(e =&gt; Math.round(e.xAtYMin + e.slopeInv * (y - e.yMin)))
      .sort((a, b) =&gt; a - b);

    for (let i = 0; i &lt; xs.length; i += 2) {
      const row = `y=${y}`.padEnd(3);
      const xPairs = `x=${xs[i]}..${xs[i+1]}`;
      const bar = "█".repeat(xs[i + 1] - xs[i] + 1);
      console.log(`${row}| ${xs.length} intersections | ${xPairs}  ${bar}`);
    }
  }
}

// A simple triangle
const triangle = [
  { x: 5, y: 0 },
  { x: 0, y: 8 },
  { x: 10, y: 8 },
];
scanLineFill(triangle);
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<p>The <code class="language-plaintext highlighter-rouge">█</code> blocks in the output show the actual fill span per scan line. You can
see the triangle widen as y increases.</p>

<h2 id="2d-transformations-and-why-order-matters">2D transformations and why order matters</h2>

<p>Translate, rotate, and scale are applied via the canvas transform stack.
The important thing: the order you apply them changes the result.
Rotate-then-translate is not the same as translate-then-rotate:</p>

<div class="playground" data-playground="">
  <div class="playground__bar">
    <span class="playground__title"><strong>JS</strong>&nbsp; Transform order — rotate vs translate first</span>
    <button class="playground__run" type="button" data-run="" aria-label="Run code">
      <span class="icon icon--play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3" /></svg></span>
 Run
    </button>
  </div>
  <textarea class="playground__editor" spellcheck="false" data-editor="">
function applyTransform(point, ops) {
  let {x, y} = point;
  const log = [`start: (${x}, ${y})`];

  for (const op of ops) {
    if (op.type === 'translate') {
      x += op.x; y += op.y;
      log.push(`translate(${op.x}, ${op.y}) → (${x.toFixed(1)}, ${y.toFixed(1)})`);
    } else if (op.type === 'rotate') {
      const cos = Math.cos(op.angle), sin = Math.sin(op.angle);
      [x, y] = [x*cos - y*sin, x*sin + y*cos];
      log.push(`rotate(${(op.angle * 180/Math.PI).toFixed(0)}°) → (${x.toFixed(1)}, ${y.toFixed(1)})`);
    } else if (op.type === 'scale') {
      x *= op.factor; y *= op.factor;
      log.push(`scale(${op.factor}) → (${x.toFixed(1)}, ${y.toFixed(1)})`);
    }
  }
  return log;
}

const point = { x: 10, y: 0 };
const angle = Math.PI / 2; // 90°

console.log("=== Rotate THEN Translate ===");
applyTransform(point, [
  { type: 'rotate', angle },
  { type: 'translate', x: 5, y: 0 },
]).forEach(l =&gt; console.log(l));

console.log("\n=== Translate THEN Rotate ===");
applyTransform(point, [
  { type: 'translate', x: 5, y: 0 },
  { type: 'rotate', angle },
]).forEach(l =&gt; console.log(l));

console.log("\nDifferent end points — order matters.");
</textarea>
  <div class="playground__console" data-console=""></div>
</div>

<p>In the actual app, each shape stores its own transform state and
<code class="language-plaintext highlighter-rouge">ctx.save()</code>/<code class="language-plaintext highlighter-rouge">ctx.restore()</code> isolates it so one object’s rotation doesn’t
bleed into the next.</p>

<p>👉 <strong><a href="https://github.com/prathameshrp/CG_MINI_PROJECT_PAINT_APP">github.com/prathameshrp</a></strong></p>]]></content><author><name>Prathamesh</name><email>prathameshind305@gmail.com</email></author><category term="JavaScript" /><category term="Canvas" /><category term="Computer Graphics" /><category term="Algorithms" /><summary type="html"><![CDATA[Building DDA, Bresenham lines and circles, scan-line fill, and polygon clipping from scratch on a raw HTML canvas — with a step visualizer to watch each algorithm think.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xprathamesh.is-a.dev/assets/og/cg-paint.png" /><media:content medium="image" url="https://0xprathamesh.is-a.dev/assets/og/cg-paint.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>