[{"data":1,"prerenderedAt":113},["ShallowReactive",2],{"products":3,"post:inside-the-opendraw-engine":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":65,"html":107},"Inside the opendraw Engine","Immutable elements, reference-keyed caches and seeded wobble: the four systems that keep a hand-drawn canvas fast, and what they teach about canvas apps generally.","2026-10-28","taohq","inside-the-opendraw-engine","\u002Fwriting\u002Finside-the-opendraw-engine","\u003Cp>A hand-drawn whiteboard looks like a toy and behaves like a database. Under the sketchy lines there are four systems that have to work together every frame: a data model, an undo history, a renderer and an input layer. This is a tour of how opendraw&#39;s work — and the reasoning is more reusable than the code.\u003C\u002Fp>\n\u003Ch2>The model: elements are immutable\u003C\u002Fh2>\n\u003Cp>Everything in a scene — rectangles, arrows, sticky notes, frames, all ten element types — is a plain, frozen record. \u003Ccode>x\u003C\u002Fcode> and \u003Ccode>y\u003C\u002Fcode> anchor the un-rotated top-left corner, \u003Ccode>angle\u003C\u002Fcode> rotates around the center, and style lives on the element itself. When you drag a shape, opendraw doesn&#39;t mutate it. It produces a new object with new coordinates.\u003C\u002Fp>\n\u003Cp>This one decision is load-bearing for everything downstream:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Undo becomes cheap.\u003C\u002Fstrong> A snapshot of the scene is just references to element objects, not a deep clone. There is nothing to copy, because nothing ever changes in place.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Reactivity becomes honest.\u003C\u002Fstrong> Vue, React and Svelte all have fast paths for &quot;is this the same object?&quot; Copy-on-write means reference equality is a correct answer.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Rendering gets a cache for free.\u003C\u002Fstrong> Hold that thought; it&#39;s the next section.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The trade is allocation: dragging a rectangle creates a new object per pointer event. Modern JavaScript engines eat that cost without noticing, and what it buys is worth far more than what it costs.\u003C\u002Fp>\n\u003Ch2>History: snapshots, not deltas\u003C\u002Fh2>\n\u003Cp>Excalidraw, whose scene model opendraw follows, moved from per-entry snapshots to inverse deltas — because their scenes are huge and shared with collaborators, and storing full snapshots for every keystroke doesn&#39;t scale there.\u003C\u002Fp>\n\u003Cp>opendraw keeps snapshots. The reasoning, from the history module&#39;s own comments: elements are already immutable, so a snapshot is two cheap references, and correctness is trivial to reason about. An undo stack of a hundred entries is a hundred pairs of references. When your data model makes the naive solution cheap, take the naive solution — naive code that&#39;s obviously right beats clever code you have to debug.\u003C\u002Fp>\n\u003Cp>One rule keeps the stack meaningful: \u003Cstrong>only scene changes become history entries\u003C\u002Fstrong>. Changing which tool is armed, what&#39;s selected, what style is pending — those never touch history. So undo always means &quot;my drawing went back a step,&quot; never &quot;the UI went back a step.&quot;\u003C\u002Fp>\n\u003Ch2>Rendering: cache keyed by identity\u003C\u002Fh2>\n\u003Cp>The expensive part of hand-drawn rendering is not drawing — it&#39;s generating the rough.js drawables that decide where every wobbly stroke lands. Do that for every element every frame and you&#39;ll feel it.\u003C\u002Fp>\n\u003Cp>opendraw&#39;s renderer keeps a module-level \u003Ccode>WeakMap\u003C\u002Fcode> keyed by \u003Cstrong>element reference\u003C\u002Fstrong>. First time an element is seen, its drawables are generated and cached; every later frame, unchanged elements are simply replayed. This cache is only correct because elements are immutable — if a rectangle could mutate in place, the cache would silently serve stale geometry. Immutability isn&#39;t a style choice here; it&#39;s what makes the cache sound.\u003C\u002Fp>\n\u003Cp>Two details worth stealing:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>The cache entry records the theme it was colored for, so switching themes regenerates each drawable exactly once and then hits again.\u003C\u002Fli>\n\u003Cli>Freedraw strokes skip rough.js entirely — perfect-freehand produces the outline, filled as one smoothed \u003Ccode>Path2D\u003C\u002Fcode>. Sketchy geometry is for shapes; a hand-drawn stroke is already hand-drawn.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>The wobble is seeded, not random\u003C\u002Fh2>\n\u003Cp>Hand-drawn jitter that changes every frame doesn&#39;t look sketchy; it looks like a broken TV. rough.js takes a numeric seed per shape, and opendraw feeds each element a seed through mulberry32, a tiny deterministic PRNG. Same element, same seed, same wobble — across frames, across reloads, across exports. Determinism is also what makes the SVG you export match the PNG you exported before.\u003C\u002Fp>\n\u003Ch2>Two canvases, split by how often they change\u003C\u002Fh2>\n\u003Cp>The editor mounts two stacked canvases. The \u003Cstrong>static canvas\u003C\u002Fstrong> below renders the scene: background, grid, every element. The \u003Cstrong>overlay canvas\u003C\u002Fstrong> above renders what changes on top of it — selection boxes and handles, arrows being drafted, the laser pointer&#39;s fading trail. Selection changes are the highest-frequency visual event in a whiteboard; keeping them on their own layer means selecting and deselecting never forces a scene re-render.\u003C\u002Fp>\n\u003Cp>It&#39;s the same principle as the WeakMap, one level up: separate what changes often from what doesn&#39;t, and make the expensive thing happen rarely.\u003C\u002Fp>\n\u003Ch2>Input and export\u003C\u002Fh2>\n\u003Cp>Pointer events go through hit testing against element bounds and geometry — which shape you grabbed, which handle, which end of which arrow — with two-finger pinch and pan for touch (ported from Excalidraw, along with the elbow-arrow router and the laser&#39;s trail). Export is pure core: the same element list that renders to screen serializes to \u003Ccode>.opendraw\u003C\u002Fcode> JSON, or renders to an SVG, or rasterizes to a PNG. One model, many outputs, no rendering special cases.\u003C\u002Fp>\n\u003Ch2>What generalizes\u003C\u002Fh2>\n\u003Cp>Three of these ideas travel well beyond whiteboards:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Immutability first.\u003C\u002Fstrong> It&#39;s not React dogma — it makes undo, caching and change detection all trivially correct at once.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Key caches by identity.\u003C\u002Fstrong> If your objects are immutable, a reference is a perfect cache key, and invalidation happens by construction.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Split surfaces by change frequency.\u003C\u002Fstrong> Two cheap canvases beat one clever one.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>That&#39;s the engine. If you&#39;d rather use it than read it: \u003Ca href=\"https:\u002F\u002Fopendraw.taohq.org\">opendraw.taohq.org\u003C\u002Fa> runs it live, \u003Ca href=\"\u002Fwriting\u002Fhow-to-use-opendraw\">the guide\u003C\u002Fa> covers the APIs, and \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FTheAlphaOnes\u002Fopendraw\">the source\u003C\u002Fa> is short on ceremony and long on comments.\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",1793498088133]