[{"data":1,"prerenderedAt":113},["ShallowReactive",2],{"products":3,"post:how-we-built-opendraw":100,"product-by-slug:opendraw":108},[4,23,38,60,73,85],{"title":5,"tagline":6,"description":7,"status":8,"kind":9,"order":10,"art":11,"artAlt":12,"platforms":13,"facts":16,"links":20,"slug":11,"path":22},"Fold","Journaling, but cool.","The journaling app for Gen Z. Photos, voice notes and quick thoughts, captured with a tap, a hold, or a swipe.","waitlist","app",1,"fold","A skeleton reading a book in a field of roses",[14,15],"iOS","Android",[17,18,19],"Free","Coming 2026","No account, no cloud",{"waitlist":21},"https:\u002F\u002Ffold.taohq.org","\u002Fproducts\u002Ffold",{"title":24,"tagline":25,"description":26,"status":27,"kind":9,"order":28,"art":29,"artAlt":30,"platforms":31,"facts":33,"links":35,"slug":29,"path":37},"Bottles","Bookmarks and docs, filed properly.","A quiet place for your links, notes and files. No feed, no noise. One space for what you keep, one for what you write.","live",2,"bottles","A skull resting in still water, surrounded by rings of ripples",[32],"Web",[34],"Live now",{"app":36},"https:\u002F\u002Fbottles.taohq.org","\u002Fproducts\u002Fbottles",{"title":39,"tagline":40,"description":41,"status":42,"kind":43,"order":44,"art":39,"artAlt":45,"platforms":46,"facts":51,"links":54,"install":58,"slug":39,"path":59},"opendraw","Hand-drawn diagrams, as one component.","A free, open-source hand-drawn whiteboard for your app. Draw hand-drawn diagrams with shapes, arrows, text and sticky notes, one component for Vue, Nuxt, React and Svelte.","open-source","library",3,"A hand-drawn diagram: a rectangle, an ellipse, an arrow, a sticky note, a diamond and a spark",[47,48,49,50],"Vue","Nuxt","React","Svelte",[52,53],"MIT license","Autosaves to localStorage",{"app":55,"github":56,"npm":57},"https:\u002F\u002Fopendraw.taohq.org","https:\u002F\u002Fgithub.com\u002FTheAlphaOnes\u002Fopendraw","https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@opendrawjs\u002Fnuxt","npm i @opendrawjs\u002Fnuxt","\u002Fproducts\u002Fopendraw",{"title":61,"tagline":62,"description":63,"status":64,"kind":9,"order":65,"art":66,"artAlt":67,"platforms":68,"facts":69,"slug":66,"path":72},"Bubbles.space","Your AI assistant's home.","A personal AI workspace OS built for the way you actually think. Bubbles, your assistant, lives and works alongside you.","in-development",4,"bubbles-space","A small figure sitting under a blossoming tree beneath a full moon",[],[70,71],"Part of the Bubbles family","AI-agent first","\u002Fproducts\u002Fbubbles-space",{"title":74,"tagline":75,"description":76,"status":64,"kind":9,"order":77,"art":78,"artAlt":79,"platforms":80,"facts":82,"slug":78,"path":84},"Bubbles.mail","It reads your inbox so you don't have to.","An intelligent mail client that turns the raw chaos of email into a short report of what matters, what can wait, and what to hand off.",5,"bubbles-mail","A skull crowned with blossoms, pierced by an arrow",[81],"macOS",[70,83],"Native macOS","\u002Fproducts\u002Fbubbles-mail",{"title":86,"tagline":87,"description":88,"status":42,"kind":43,"order":89,"art":86,"artAlt":90,"platforms":91,"facts":93,"links":95,"install":98,"slug":86,"path":99},"web-have-sounds","UI sounds, generated with math.","A zero-dependency JavaScript library for UI sound effects. No audio files, no network requests. Just the Web Audio API.",6,"A crow perched on a skull inside an arched frame",[32,92],"npm",[94],"Zero dependencies",{"github":96,"npm":97},"https:\u002F\u002Fgithub.com\u002FNormVg\u002Fweb-have-sound","https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@thenormvg\u002Fweb-have-sounds","npm i @thenormvg\u002Fweb-have-sounds","\u002Fproducts\u002Fweb-have-sounds",{"title":101,"description":102,"date":103,"author":104,"product":39,"slug":105,"path":106,"readingMinutes":44,"html":107},"How We Built opendraw","One engine, five packages, four frameworks: how opendraw keeps its canvas core framework-free so every adapter stays a few hundred lines.","2026-10-30","taohq","how-we-built-opendraw","\u002Fwriting\u002Fhow-we-built-opendraw","\u003Cp>Every architecture decision in opendraw comes from one constraint: the same editor has to run in Vue, Nuxt, React and Svelte, and we refuse to maintain four editors.\u003C\u002Fp>\n\u003Cp>There&#39;s an obvious way to do this badly. You build the editor in your favourite framework, then &quot;port&quot; it, and now every bug is four bugs and every feature ships four times late. We&#39;ve seen that movie. So we set a rule before writing a line of code: \u003Cstrong>everything below the framework line\u003C\u002Fstrong>. All state, all geometry, all gestures, all rendering logic lives in code that doesn&#39;t know what a component is.\u003C\u002Fp>\n\u003Ch2>Three layers\u003C\u002Fh2>\n\u003Cp>The result is a pnpm monorepo in three layers.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>\u003Ccode>@opendrawjs\u002Fcore\u003C\u002Fcode> is the engine.\u003C\u002Fstrong> The element model — ten element types, from rectangles to frames — a document and its undo history, geometry, hit testing, rough.js rendering, and PNG\u002FSVG\u002FJSON export. It has no DOM requirement and no framework imports. You can use it to generate a diagram server-side and never draw an interactive pixel.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>\u003Ccode>@opendrawjs\u002Feditor\u003C\u002Fcode> is the interactive editor.\u003C\u002Fstrong> A framework-free controller you create with \u003Ccode>createEditor()\u003C\u002Fcode>, and a \u003Ccode>mountCanvas()\u003C\u002Fcode> that wires every gesture, shortcut and the in-place text editor onto two stacked canvases you hand it. Tools, the action registry and persistence live here too. It knows about the DOM, but it still doesn&#39;t know about Vue or React.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>The adapters are the thin part.\u003C\u002Fstrong> \u003Ccode>@opendrawjs\u002Fnuxt\u003C\u002Fcode>, \u003Ccode>@opendrawjs\u002Freact\u003C\u002Fcode> and \u003Ccode>@opendrawjs\u002Fsvelte\u003C\u002Fcode> each mount the canvas, mirror the controller&#39;s state into their framework&#39;s reactive primitives, and render the toolbar and panels around it. The React and Svelte adapters are a few hundred lines each — complete, readable references if you want to build one for Solid, Angular or plain JavaScript.\u003C\u002Fp>\n\u003Ch2>The choice that made everything cheap\u003C\u002Fh2>\n\u003Cp>The single most load-bearing decision is that \u003Cstrong>elements are immutable\u003C\u002Fstrong>. Every mutation produces a new object — copy-on-write — instead of mutating a rectangle in place.\u003C\u002Fp>\n\u003Cp>This sounds wasteful until you see what it buys. Undo history becomes trivially cheap: a snapshot is just references to the element objects, not a deep clone of the scene, so we store before\u002Fafter snapshot pairs and cap the stack at a hundred. Reactivity gets honest: a framework can compare references instead of diffing trees. And rendering gets a cache: rough.js drawable generation is the expensive part of drawing a hand-drawn scene, so a \u003Ccode>WeakMap\u003C\u002Fcode> keyed by \u003Cem>element reference\u003C\u002Fem> caches the generated drawables — unchanged elements are simply replayed each frame, because an unchanged element is guaranteed to be the same object.\u003C\u002Fp>\n\u003Cp>There&#39;s a second, quieter payoff. The hand-drawn look depends on randomness — but jitter that changes on every frame looks like static, not sketching. So every element carries a numeric seed, and rough.js generates its wobble from that seed through a small deterministic PRNG. Same element, same sketch, forever, including in exports.\u003C\u002Fp>\n\u003Ch2>The look, honestly sourced\u003C\u002Fh2>\n\u003Cp>The sketch style is three libraries doing one job each: \u003Ca href=\"https:\u002F\u002Froughjs.com\">rough.js\u003C\u002Fa> draws geometry that looks drawn, \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fsteveruizok\u002Fperfect-freehand\">perfect-freehand\u003C\u002Fa> produces pressure-aware freedraw outlines (those strokes bypass rough.js entirely and fill as a single smoothed path), and \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fexcalidraw\u002Fexcalifont\">Excalifont\u003C\u002Fa> is the hand-drawn text face. The elbow-arrow router, the laser pointer trail and the two-finger gestures were ported from Excalidraw&#39;s MIT source with their notices kept — we documented exactly what we took, because that&#39;s how open source should work.\u003C\u002Fp>\n\u003Ch2>Testing it without losing our minds\u003C\u002Fh2>\n\u003Cp>One test suite, run everywhere it matters. Unit tests cover the core, editor and Nuxt package; typecheck covers every package, the playground and the e2e specs; and a single Playwright suite runs against the Nuxt demo, then against plain Vue, React and Svelte test apps. When the framework-free rule is followed, &quot;does it work in React?&quot; is mostly &quot;does the adapter mount correctly?&quot; — and that&#39;s the kind of question a browser test answers once, for everyone.\u003C\u002Fp>\n\u003Cp>The lesson generalizes past whiteboards. When you&#39;re asked to support N frameworks, don&#39;t write N implementations — push your state and your logic below the framework line until the framework is a detail. Then the adapter is a few hundred lines, the test suite is one suite, and the next framework on the list is an afternoon.\u003C\u002Fp>\n\u003Cp>The code is all here: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FTheAlphaOnes\u002Fopendraw\">github.com\u002FTheAlphaOnes\u002Fopendraw\u003C\u002Fa>. If you want the engine internals — the history, the cache, the two canvases — \u003Ca href=\"\u002Fwriting\u002Finside-the-opendraw-engine\">we wrote that up too\u003C\u002Fa>.\u003C\u002Fp>\n",{"title":39,"tagline":40,"description":41,"status":42,"kind":43,"order":44,"art":39,"artAlt":45,"platforms":109,"facts":110,"links":111,"install":58,"slug":39,"path":59,"html":112},[47,48,49,50],[52,53],{"app":55,"github":56,"npm":57},"\u003Cp>\u003Cstrong>opendraw is a free, open-source hand-drawn whiteboard for your app: shapes, arrows, text and sticky notes, rendered sketchy, as one component for Vue, Nuxt, React and Svelte.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2>What you can draw\u003C\u002Fh2>\n\u003Cp>Rectangles, diamonds and ellipses, arrows that connect, lines, freehand drawing, text, sticky notes and frames. Everything renders sketchy through rough.js, so it looks drawn by hand, because it was. There are tools and shortcuts for everything: every tool has a key, from V for select to K for the laser pointer. Press \u003Ccode>?\u003C\u002Fcode> inside the editor for the full list.\u003C\u002Fp>\n\u003Ch2>One engine, five packages\u003C\u002Fh2>\n\u003Cp>opendraw ships as five packages:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>@opendrawjs\u002Fcore\u003C\u002Fstrong>, the engine: element model, document &amp; history, geometry, hit testing, rendering and export. No framework code.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>@opendrawjs\u002Feditor\u003C\u002Fstrong>, the framework-free editor: tools, gestures, keyboard, the action registry, persistence. Mounts onto any DOM.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>@opendrawjs\u002Fnuxt\u003C\u002Fstrong>, the Vue\u002FNuxt adapter: \u003Ccode>&lt;OpenDraw \u002F&gt;\u003C\u002Fcode>, \u003Ccode>useOpenDraw()\u003C\u002Fcode> and the full editor UI.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>@opendrawjs\u002Freact\u003C\u002Fstrong> and \u003Cstrong>@opendrawjs\u002Fsvelte\u003C\u002Fstrong>, the React and Svelte 5 adapters.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Getting started\u003C\u002Fh2>\n\u003Cp>Install, register the module, and drop the component anywhere. It fills its container, so give the container a height:\u003C\u002Fp>\n\u003Cpre class=\"shiki taohq\" style=\"background-color:#201e25;color:#ece8f3\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#ECE8F3\">npm \u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\">install\u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\"> @opendrawjs\u002Fnuxt\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003Cpre class=\"shiki taohq\" style=\"background-color:#201e25;color:#ece8f3\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#8A8494;font-style:italic\">\u002F\u002F nuxt.config.ts\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B2A0D8\">export\u003C\u002Fspan>\u003Cspan style=\"color:#B2A0D8\"> default\u003C\u002Fspan>\u003Cspan style=\"color:#ECE8F3\"> defineNuxtConfig\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">({\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#ECE8F3\">  modules\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">:\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\"> [\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">'\u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\">@opendrawjs\u002Fnuxt\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">'\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">],\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#A39DAD\">})\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003Cpre class=\"shiki taohq\" style=\"background-color:#201e25;color:#ece8f3\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#A39DAD\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\">template\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#A39DAD\">  &#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\">div\u003C\u002Fspan>\u003Cspan style=\"color:#B2A0D8\"> style\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">=\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\">height: 100vh\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#A39DAD\">    &#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\">OpenDraw\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\"> \u002F>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#A39DAD\">  &#x3C;\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\">div\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#A39DAD\">&#x3C;\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#C4B5E3\">template\u003C\u002Fspan>\u003Cspan style=\"color:#A39DAD\">>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>The module auto-imports the components and composables. Plain Vue 3 without Nuxt works too: \u003Ccode>import { OpenDraw } from &#39;@opendrawjs\u002Fnuxt\u002Fvue&#39;\u003C\u002Fcode>. The scene saves itself to \u003Ccode>localStorage\u003C\u002Fcode> and restores on reload, and a drawings sidebar keeps many drawings, each saved on its own.\u003C\u002Fp>\n\u003Ch2>Where to Find It\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Docs and demo\u003C\u002Fstrong>: \u003Ca href=\"https:\u002F\u002Fopendraw.taohq.org\">opendraw.taohq.org\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Cstrong>GitHub\u003C\u002Fstrong>: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FTheAlphaOnes\u002Fopendraw\">github.com\u002FTheAlphaOnes\u002Fopendraw\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Cstrong>npm\u003C\u002Fstrong>: \u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@opendrawjs\u002Fnuxt\">npmjs.com\u002Fpackage\u002F@opendrawjs\u002Fnuxt\u003C\u002Fa>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>MIT licensed. If you draw something with it, we&#39;d genuinely love to see it.\u003C\u002Fp>\n",1793498088120]