<?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: tage cc</title>
    <description>The latest articles on DEV Community by tage cc (@tage_cc_b441e6b367279d2fa).</description>
    <link>https://dev.to/tage_cc_b441e6b367279d2fa</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3160328%2F9179c05e-25c3-4df4-a353-ffa7aed42e8b.jpg</url>
      <title>DEV Community: tage cc</title>
      <link>https://dev.to/tage_cc_b441e6b367279d2fa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tage_cc_b441e6b367279d2fa"/>
    <language>en</language>
    <item>
      <title>I open sourced memduck, a self-hosted AI memory workspace</title>
      <dc:creator>tage cc</dc:creator>
      <pubDate>Thu, 21 May 2026 11:19:33 +0000</pubDate>
      <link>https://dev.to/tage_cc_b441e6b367279d2fa/i-open-sourced-memduck-a-self-hosted-ai-memory-workspace-1okd</link>
      <guid>https://dev.to/tage_cc_b441e6b367279d2fa/i-open-sourced-memduck-a-self-hosted-ai-memory-workspace-1okd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrb56zxhaadib5fwsw1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrb56zxhaadib5fwsw1k.png" alt="memduck product preview" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built memduck because useful context keeps disappearing across links, copied text, screenshots, and chat messages.&lt;/p&gt;

&lt;p&gt;Most of my actual working memory is not in one clean notes app. It is spread across browser tabs, saved links, snippets, screenshots, Telegram messages, and random text I copied while researching something. Later, when I want to ask a question about that material, I usually have to reconstruct the context manually.&lt;/p&gt;

&lt;p&gt;memduck is my attempt at a small, self-hosted workspace for that problem.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/tageecc/memduck" rel="noopener noreferrer"&gt;https://github.com/tageecc/memduck&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;memduck lets you save material from browser pages, pasted text, screenshots, and chat channels into a local memory workspace. Then you can ask questions against the saved material and trace answers back to the original sources.&lt;/p&gt;

&lt;p&gt;The current version includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Next.js workspace for asking and managing saved memory&lt;/li&gt;
&lt;li&gt;local SQLite storage and local file assets&lt;/li&gt;
&lt;li&gt;browser extension capture&lt;/li&gt;
&lt;li&gt;channel configuration for browser, Telegram, DingTalk, Slack, Discord, Feishu, WhatsApp, and webhook-style inputs&lt;/li&gt;
&lt;li&gt;model/provider configuration in the UI&lt;/li&gt;
&lt;li&gt;background compilation for summaries, topics, embeddings, and retrieval data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why self-hosted
&lt;/h2&gt;

&lt;p&gt;I wanted this to be closer to a personal memory engine than another hosted knowledge app. The goal is to keep the runtime simple enough to inspect and run locally, while still making AI retrieval useful.&lt;/p&gt;

&lt;p&gt;It is still early, but it is already useful for the workflow I care about: save context first, ask later, and keep the source trail visible.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; memduck@latest
memduck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or from source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/tageecc/memduck.git
&lt;span class="nb"&gt;cd &lt;/span&gt;memduck
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm memduck dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you care about local-first tools, personal knowledge workflows, or AI memory with source-backed answers, I would like feedback.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I open sourced memduck, a self-hosted AI memory workspace</title>
      <dc:creator>tage cc</dc:creator>
      <pubDate>Thu, 21 May 2026 07:35:30 +0000</pubDate>
      <link>https://dev.to/tage_cc_b441e6b367279d2fa/i-open-sourced-memduck-a-self-hosted-ai-memory-workspace-ko5</link>
      <guid>https://dev.to/tage_cc_b441e6b367279d2fa/i-open-sourced-memduck-a-self-hosted-ai-memory-workspace-ko5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrb56zxhaadib5fwsw1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrb56zxhaadib5fwsw1k.png" alt="memduck product preview" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built memduck because useful context keeps disappearing across links, copied text, screenshots, and chat messages.&lt;/p&gt;

&lt;p&gt;Most of my actual working memory is not in one clean notes app. It is spread across browser tabs, saved links, snippets, screenshots, Telegram messages, and random text I copied while researching something. Later, when I want to ask a question about that material, I usually have to reconstruct the context manually.&lt;/p&gt;

&lt;p&gt;memduck is my attempt at a small, self-hosted workspace for that problem.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/tageecc/memduck" rel="noopener noreferrer"&gt;https://github.com/tageecc/memduck&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;memduck lets you save material from browser pages, pasted text, screenshots, and chat channels into a local memory workspace. Then you can ask questions against the saved material and trace answers back to the original sources.&lt;/p&gt;

&lt;p&gt;The current version includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Next.js workspace for asking and managing saved memory&lt;/li&gt;
&lt;li&gt;local SQLite storage and local file assets&lt;/li&gt;
&lt;li&gt;browser extension capture&lt;/li&gt;
&lt;li&gt;channel configuration for browser, Telegram, DingTalk, Slack, Discord, Feishu, WhatsApp, and webhook-style inputs&lt;/li&gt;
&lt;li&gt;model/provider configuration in the UI&lt;/li&gt;
&lt;li&gt;background compilation for summaries, topics, embeddings, and retrieval data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why self-hosted
&lt;/h2&gt;

&lt;p&gt;I wanted this to be closer to a personal memory engine than another hosted knowledge app. The goal is to keep the runtime simple enough to inspect and run locally, while still making AI retrieval useful.&lt;/p&gt;

&lt;p&gt;It is still early, but it is already useful for the workflow I care about: save context first, ask later, and keep the source trail visible.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; memduck@latest
memduck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or from source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/tageecc/memduck.git
&lt;span class="nb"&gt;cd &lt;/span&gt;memduck
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm memduck dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you care about local-first tools, personal knowledge workflows, or AI memory with source-backed answers, I would like feedback.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I open sourced memduck: a self-hosted AI memory workspace</title>
      <dc:creator>tage cc</dc:creator>
      <pubDate>Wed, 20 May 2026 14:25:38 +0000</pubDate>
      <link>https://dev.to/tage_cc_b441e6b367279d2fa/i-open-sourced-memduck-a-self-hosted-ai-memory-workspace-21li</link>
      <guid>https://dev.to/tage_cc_b441e6b367279d2fa/i-open-sourced-memduck-a-self-hosted-ai-memory-workspace-21li</guid>
      <description>&lt;p&gt;I open sourced &lt;strong&gt;memduck&lt;/strong&gt; today.&lt;/p&gt;

&lt;p&gt;It is a self-hosted AI memory workspace for the things that usually disappear across browser tabs, copied notes, screenshots, and chat channels.&lt;/p&gt;

&lt;p&gt;The idea is simple: save useful material once, then ask against your own memory later with citations back to the original source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fij1l9dzsazzfflwut90w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fij1l9dzsazzfflwut90w.png" alt="memduck product preview" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Most note tools make it easy to save more, but retrieval still gets messy.&lt;/p&gt;

&lt;p&gt;I wanted something closer to a personal memory engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the raw source so I can always go back&lt;/li&gt;
&lt;li&gt;turn long links or pasted text into reusable memory cards&lt;/li&gt;
&lt;li&gt;capture from browser and chat channels&lt;/li&gt;
&lt;li&gt;ask questions only against material I actually saved&lt;/li&gt;
&lt;li&gt;keep model providers and channel setup visible in the UI&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;memduck currently includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Next.js web workspace&lt;/li&gt;
&lt;li&gt;local SQLite storage&lt;/li&gt;
&lt;li&gt;browser extension capture&lt;/li&gt;
&lt;li&gt;Telegram runtime&lt;/li&gt;
&lt;li&gt;DingTalk / Slack / Discord / Feishu / WhatsApp-style channel adapters&lt;/li&gt;
&lt;li&gt;configurable model providers such as OpenAI, Anthropic, Gemini, Ollama, and OpenAI-compatible profiles&lt;/li&gt;
&lt;li&gt;grounded Q&amp;amp;A with citations over saved memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is still early, but the core loop is already there: capture, compile, retrieve, ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it is for
&lt;/h2&gt;

&lt;p&gt;This is mainly for developers, builders, and researchers who want a self-hosted place to keep source-backed memory instead of sending every note into another hosted AI app.&lt;/p&gt;

&lt;p&gt;If you care about local control, explicit provider setup, and source traceability, it may be worth trying.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/tageecc/memduck" rel="noopener noreferrer"&gt;https://github.com/tageecc/memduck&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a Desktop Control Center for OpenClaw with Tauri and Rust</title>
      <dc:creator>tage cc</dc:creator>
      <pubDate>Sat, 28 Mar 2026 03:13:29 +0000</pubDate>
      <link>https://dev.to/tage_cc_b441e6b367279d2fa/building-a-desktop-control-center-for-openclaw-with-tauri-and-rust-57o8</link>
      <guid>https://dev.to/tage_cc_b441e6b367279d2fa/building-a-desktop-control-center-for-openclaw-with-tauri-and-rust-57o8</guid>
      <description>&lt;p&gt;I've been working with OpenClaw for managing AI agents, and I kept running into the same problems: juggling multiple instances for different environments, manually editing &lt;code&gt;openclaw.json&lt;/code&gt;, and lacking visibility into token usage and costs.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Pond&lt;/strong&gt; — a desktop app that sits on top of OpenClaw and solves these issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots (from the repo README)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdycx49pkqxwnej8b2ag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdycx49pkqxwnej8b2ag.png" alt="Analytics dashboard" width="800" height="543"&gt;&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%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xfeqcqruppkf5dd2q9q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xfeqcqruppkf5dd2q9q.png" alt="Role management" width="800" height="543"&gt;&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%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7my50mmj8ukciuuo6e6e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7my50mmj8ukciuuo6e6e.png" alt="Team tasks" width="800" height="543"&gt;&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%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0v2uspmog4esb06jtmuh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0v2uspmog4esb06jtmuh.png" alt="Agent chat" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Pond is a control center that lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manage multiple OpenClaw instances&lt;/strong&gt; on one machine (like dev/test/prod environments)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrate team collaboration&lt;/strong&gt; with a built-in task state machine (open → claimed → done/failed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor in real-time&lt;/strong&gt; — tokens, costs, sessions, system resources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit configs visually&lt;/strong&gt; instead of wrestling with JSON files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each instance runs independently with its own Gateway process, port, config, and team data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical stack
&lt;/h2&gt;

&lt;p&gt;I chose &lt;strong&gt;Tauri 2&lt;/strong&gt; instead of Electron because I wanted native performance and smaller binaries. The backend is Rust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tokio::process&lt;/code&gt; for managing Gateway subprocesses&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fs4&lt;/code&gt; for cross-platform file locks (team data concurrency)&lt;/li&gt;
&lt;li&gt;Native WebSocket plugin for bidirectional streaming with OpenClaw&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend is &lt;strong&gt;React 19&lt;/strong&gt; + TypeScript + Zustand for state, with Radix UI components and TailwindCSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The team collaboration layer
&lt;/h2&gt;

&lt;p&gt;One unique feature is the collaboration workflow. You define roles in &lt;code&gt;agents.list&lt;/code&gt; (a Leader and executors), and Pond manages a task lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leader creates tasks (state: &lt;code&gt;open&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Executors claim tasks (state: &lt;code&gt;claimed&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Executors mark them &lt;code&gt;done&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt; with reasons&lt;/li&gt;
&lt;li&gt;Real-time notifications pushed via WebSocket to relevant roles&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's a built-in &lt;code&gt;pond-team&lt;/code&gt; skill that orchestrates this entire workflow without manual coordination.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's missing
&lt;/h2&gt;

&lt;p&gt;It's still early (v1.0.4), and there are gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No cloud sync for configs yet&lt;/li&gt;
&lt;li&gt;No mobile clients (only desktop)&lt;/li&gt;
&lt;li&gt;Skill marketplace is planned but not built&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the core features work, and it's fully open source.&lt;/p&gt;

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

&lt;p&gt;Installers for macOS (Intel + Apple Silicon), Windows, and Linux are on the GitHub releases page:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/tageecc/pond" rel="noopener noreferrer"&gt;https://github.com/tageecc/pond&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building with OpenClaw, I'd appreciate feedback — especially around what metrics or workflows you need most.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>openclaw</category>
      <category>rust</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Built a Browser Tool to Stop Using Photoshop for Theme Comparisons</title>
      <dc:creator>tage cc</dc:creator>
      <pubDate>Fri, 27 Mar 2026 15:24:34 +0000</pubDate>
      <link>https://dev.to/tage_cc_b441e6b367279d2fa/built-a-browser-tool-to-stop-using-photoshop-for-theme-comparisons-3mmn</link>
      <guid>https://dev.to/tage_cc_b441e6b367279d2fa/built-a-browser-tool-to-stop-using-photoshop-for-theme-comparisons-3mmn</guid>
      <description>&lt;p&gt;How I used Canvas to create diagonal split screenshots in one hour&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;While working on my project docs, I wanted to show light/dark theme comparisons. Not just two screenshots side by side—I wanted that nice diagonal split effect.&lt;/p&gt;

&lt;p&gt;My options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Photoshop&lt;/strong&gt; - tedious to repeat every time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online tools&lt;/strong&gt; - mostly paid, and you have to upload images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual stitching&lt;/strong&gt; - doesn't look professional&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Couldn't find a good free tool, so I built one.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnesc3wdll124qc4oi40a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnesc3wdll124qc4oi40a.png" alt="demo" width="800" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it&lt;/strong&gt;: &lt;a href="https://tageecc.github.io/theme-merge/" rel="noopener noreferrer"&gt;https://tageecc.github.io/theme-merge/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload two images (light/dark theme)&lt;/li&gt;
&lt;li&gt;Diagonal split with adjustable angle&lt;/li&gt;
&lt;li&gt;Paste from clipboard (Ctrl+V)&lt;/li&gt;
&lt;li&gt;Everything runs locally in your browser&lt;/li&gt;
&lt;li&gt;Free and open source&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;Kept it simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML5 Canvas for image manipulation&lt;/li&gt;
&lt;li&gt;Vanilla JavaScript (no frameworks)&lt;/li&gt;
&lt;li&gt;GitHub Pages for hosting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Goal was to make something you can just open and use. No install, no signup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Core Algorithm
&lt;/h3&gt;

&lt;p&gt;The diagonal split uses Canvas's &lt;code&gt;clip()&lt;/code&gt; method:&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;function&lt;/span&gt; &lt;span class="nf"&gt;drawMergedImage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Draw dark theme as base&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;darkImg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Save state&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Create diagonal clipping path&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;moveTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// rotated endpoints&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closePath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Apply clip and draw light theme&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lightImg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Angle Calculation
&lt;/h3&gt;

&lt;p&gt;My math isn't great, so this part took some trial and error:&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;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angleSlider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;angleRad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseAngle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Calculate rotated diagonal endpoints&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseAngle&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;angleRad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseAngle&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;angleRad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseAngle&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;angleRad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;diagonal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseAngle&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;angleRad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Formula looks complicated, but the logic is: base diagonal + angle offset = new diagonal position.&lt;/p&gt;

&lt;h3&gt;
  
  
  UX Improvements
&lt;/h3&gt;

&lt;p&gt;Started with basic file upload, then added enhancements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Click left/right to upload&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Detect which side of the diagonal was clicked:&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;function&lt;/span&gt; &lt;span class="nf"&gt;isPointOnLightSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;centerX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;centerY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;centerX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;centerY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Rotate coordinate system&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rotatedX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;angleRad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;angleRad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rotatedY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;angleRad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;angleRad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rotatedY&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rotatedX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;baseSlope&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Clipboard paste&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Added this later because saving screenshots then uploading was annoying:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paste&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboardData&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;getAsFile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;loadImageToSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;determineSide&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can screenshot and Ctrl+V directly. Much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue 1: Canvas Size
&lt;/h3&gt;

&lt;p&gt;Initially set canvas size via CSS. Images came out blurry. Turns out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSS controls display size&lt;/li&gt;
&lt;li&gt;width/height attributes control actual resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Issue 2: Large Image Performance
&lt;/h3&gt;

&lt;p&gt;4K images made pixel-by-pixel calculation slow. Solution: use Canvas's native &lt;code&gt;clip()&lt;/code&gt; and let the browser handle it. Much faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue 3: Mobile Responsiveness
&lt;/h3&gt;

&lt;p&gt;Canvas would overflow on mobile. Added max-width/height CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90vw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scales display while keeping original resolution for download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;p&gt;Perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; - showing feature differences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolios&lt;/strong&gt; - displaying design work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blog posts&lt;/strong&gt; - creating comparison images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;READMEs&lt;/strong&gt; - showcasing project themes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Current version works for my needs, but could add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vertical split mode&lt;/li&gt;
&lt;li&gt;More export formats (JPG, WebP)&lt;/li&gt;
&lt;li&gt;Batch processing&lt;/li&gt;
&lt;li&gt;Preset angle templates&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://tageecc.github.io/theme-merge/" rel="noopener noreferrer"&gt;https://tageecc.github.io/theme-merge/&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Source code&lt;/strong&gt;: &lt;a href="https://github.com/tageecc/theme-merge" rel="noopener noreferrer"&gt;https://github.com/tageecc/theme-merge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Took about an hour to build, less than 500 lines of code. Canvas clip() works really well for this.&lt;/p&gt;

&lt;p&gt;If you need something similar, feel free to fork and modify. Issues and PRs welcome!&lt;/p&gt;




&lt;h3&gt;
  
  
  What would you add?
&lt;/h3&gt;

&lt;p&gt;Have ideas for features? Drop a comment below or open an issue on GitHub!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog. Follow me for more web dev tips and tools.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: #webdev #javascript #canvas #opensource #tools&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
