<?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>Forem: maomaoguai</title>
    <description>The latest articles on Forem by maomaoguai (@11suixing11).</description>
    <link>https://forem.com/11suixing11</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%2F3917697%2Ff70312b9-b2f7-427b-8b3a-8b57faa99c00.jpeg</url>
      <title>Forem: maomaoguai</title>
      <link>https://forem.com/11suixing11</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/11suixing11"/>
    <language>en</language>
    <item>
      <title>How I Built a Whiteboard App with 3 Dependencies</title>
      <dc:creator>maomaoguai</dc:creator>
      <pubDate>Thu, 07 May 2026 11:03:26 +0000</pubDate>
      <link>https://forem.com/11suixing11/how-i-built-a-whiteboard-app-with-3-dependencies-bj3</link>
      <guid>https://forem.com/11suixing11/how-i-built-a-whiteboard-app-with-3-dependencies-bj3</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every whiteboard app I tried wanted me to sign up, sync to the cloud, or load 2MB of JavaScript. I just wanted a blank canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/11suixing11/mindnotes-pro" rel="noopener noreferrer"&gt;MindNotes Pro&lt;/a&gt;&lt;/strong&gt; — a drawing app with 3 production dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;react, react-dom, zustand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No Redux. No styled-components. No axios. No lodash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://11suixing11.github.io/mindnotes-pro/" rel="noopener noreferrer"&gt;Try it now →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The drawing engine lives in a single &lt;code&gt;Canvas.tsx&lt;/code&gt; file (~600 lines). It uses the Canvas API directly — no drawing library. State is managed by 3 small Zustand stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useDrawingStore&lt;/code&gt; — strokes, shapes, tools, undo/redo, localStorage persistence&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useViewStore&lt;/code&gt; — zoom and pan&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useThemeStore&lt;/code&gt; — dark/light theme with system detection&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Technical Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Canvas API over SVG
&lt;/h3&gt;

&lt;p&gt;Canvas gives us hardware-accelerated 2D drawing and pixel-perfect control. SVG would have been easier for hit testing, but Canvas handles thousands of strokes without DOM overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Zustand over Redux
&lt;/h3&gt;

&lt;p&gt;Zustand is under 1KB and needs no boilerplate. A single &lt;code&gt;useDrawingStore&lt;/code&gt; holds all drawing state. Subscriptions trigger Canvas redraws.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No external CDN
&lt;/h3&gt;

&lt;p&gt;Everything is bundled — fonts, icons, even the SVG noise texture for the paper background. This makes the app accessible from China without a VPN.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Warm parchment design
&lt;/h3&gt;

&lt;p&gt;Instead of the typical cold blue/gray UI, I used a warm earth tone palette (&lt;code&gt;#f5f0e8&lt;/code&gt; background, &lt;code&gt;#c47a5a&lt;/code&gt; accent) with SVG fractal noise for paper texture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6 brush types&lt;/strong&gt;: pen, highlighter, pencil, calligraphy, dashed, glow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9 drawing tools&lt;/strong&gt;: select, pen, eraser, pan, rectangle, circle, text, line, arrow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 export formats&lt;/strong&gt;: PNG, JPG, PDF, SVG, Word, JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-save&lt;/strong&gt; to localStorage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;50-step undo/redo&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zoom &amp;amp; minimap&lt;/strong&gt; navigation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode&lt;/strong&gt; with system detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch support&lt;/strong&gt; for tablets and phones&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;~160KB gzipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External CDN calls&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build time&lt;/td&gt;
&lt;td&gt;~2 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://11suixing11.github.io/mindnotes-pro/" rel="noopener noreferrer"&gt;Launch App&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;&lt;a href="https://github.com/11suixing11/mindnotes-pro/releases/latest" rel="noopener noreferrer"&gt;Download Offline Zip&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;&lt;a href="https://github.com/11suixing11/mindnotes-pro" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with React 18, TypeScript 5, Vite 5, Zustand, Canvas API, and Tailwind CSS.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
