<?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: David Gao</title>
    <description>The latest articles on DEV Community by David Gao (@ai_builder_ing).</description>
    <link>https://dev.to/ai_builder_ing</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4166366%2Fb30ae007-747a-4533-82e7-a4cfccf3de0a.jpg</url>
      <title>DEV Community: David Gao</title>
      <link>https://dev.to/ai_builder_ing</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ai_builder_ing"/>
    <language>en</language>
    <item>
      <title>Building a Photo to Coloring Page Tool with WebAssembly and AI</title>
      <dc:creator>David Gao</dc:creator>
      <pubDate>Fri, 16 Oct 2026 05:05:46 +0000</pubDate>
      <link>https://dev.to/ai_builder_ing/building-a-photo-to-coloring-page-tool-with-webassembly-and-ai-ee4</link>
      <guid>https://dev.to/ai_builder_ing/building-a-photo-to-coloring-page-tool-with-webassembly-and-ai-ee4</guid>
      <description>&lt;p&gt;A photo of a dog can become two very different coloring pages. One keeps the fur, fabric and small edges close to the source. The other redraws the dog with broader outlines and more room for crayons.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://colorwillow.com/" rel="noopener noreferrer"&gt;ColorWillow&lt;/a&gt;, a printable coloring site for parents and teachers, we kept both approaches: a free conversion running on the user's device, and an AI service that can simplify and restyle a photo.&lt;/p&gt;

&lt;p&gt;The interesting engineering work was deciding what each path should promise, how much work belongs in the browser, and what happens when a remote generation takes longer than the page stays open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same photo reveals the tradeoff
&lt;/h2&gt;

&lt;p&gt;Start with this dog resting in a person's arms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhrnn6zlwts933bwjamz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhrnn6zlwts933bwjamz.webp" alt="A dog resting in a person's arms" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://stocksnap.io/photo/man-dog-ILPCT4ZZ9K" rel="noopener noreferrer"&gt;Kristin Hardwick on StockSnap&lt;/a&gt;, &lt;a href="https://creativecommons.org/publicdomain/zero/1.0/" rel="noopener noreferrer"&gt;CC0 1.0&lt;/a&gt;. Resized for this example.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F50hy0a6rzpa4qd3dvvmq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F50hy0a6rzpa4qd3dvvmq.webp" alt="Free conversion retaining fur and fabric texture" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Free conversion. Fine fur and the checked shirt remain visible as many small lines.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flgvax487tvd8q1mqklh0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flgvax487tvd8q1mqklh0.webp" alt="AI redraw with larger white coloring areas" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI conversion with Classic style, Medium detail, Standard lines and Keep background. It leaves larger white areas, but changes the dog's facial features and coat markings.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That last point matters. A cleaner illustration can be easier to color while being less faithful to the individual animal. We show both methods so people can choose based on their own photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running a line drawing model in the browser
&lt;/h2&gt;

&lt;p&gt;Our &lt;a href="https://colorwillow.com/free-photo-to-coloring-page/" rel="noopener noreferrer"&gt;free photo to coloring page converter&lt;/a&gt; uses an ONNX line drawing model executed through ONNX Runtime Web's WASM backend.&lt;/p&gt;

&lt;p&gt;The local model is derived from &lt;a href="https://github.com/carolineec/informative-drawings" rel="noopener noreferrer"&gt;Informative Drawings by Caroline Chan, Frédo Durand and Phillip Isola&lt;/a&gt;. We retain the model and runtime license notices with the distributed assets.&lt;/p&gt;

&lt;p&gt;Although the interface calls this mode Free and the remote mode AI, the local path also uses machine learning. The distinction is where the processing happens and how much the picture is reinterpreted.&lt;/p&gt;

&lt;p&gt;The pipeline is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the selected image and fit it within a bounded working size.&lt;/li&gt;
&lt;li&gt;Composite transparency onto white.&lt;/li&gt;
&lt;li&gt;Convert RGBA pixels into a normalized RGB tensor.&lt;/li&gt;
&lt;li&gt;Run the model in a dedicated Web Worker.&lt;/li&gt;
&lt;li&gt;Turn its single channel output into a grayscale PNG.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the session setup, shortened from our worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ort&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onnxruntime-web/wasm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This code already runs inside our own Web Worker.&lt;/span&gt;
&lt;span class="nx"&gt;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;numThreads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasmPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;assetPrefix&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InferenceSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;executionProviders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;graphOptimizationLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;enableCpuMemArena&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;enableMemPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;We use one inference thread in this implementation. The &lt;a href="https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html" rel="noopener noreferrer"&gt;ONNX Runtime configuration guide&lt;/a&gt; explains the additional cross origin isolation requirement for WASM multithreading. Running inside our own worker lets us control cancellation and its lifetime directly; we do not enable the runtime's additional proxy worker.&lt;/p&gt;

&lt;p&gt;The model file is about 8.6 MB, excluding the runtime. We fetch it from a versioned asset directory and verify its expected byte length and SHA-256 before creating a session. The JavaScript runtime and WASM binaries must also come from the same build, as the ONNX documentation specifies.&lt;/p&gt;

&lt;p&gt;The first conversion includes downloads and initialization. WASM moves inference onto the device, but loading the page and its assets still needs network access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preprocessing and memory need explicit limits
&lt;/h2&gt;

&lt;p&gt;A successful session initialization does not guarantee a useful image. The model expects RGB channel planes, while canvas gives us interleaved RGBA pixels.&lt;/p&gt;

&lt;p&gt;Our conversion code preserves the aspect ratio, pads the working dimensions to multiples of four with a minimum of 32, and uses reflected pixels at the boundary. After inference, it removes that padding.&lt;/p&gt;

&lt;p&gt;The channel conversion looks like this, with padding omitted for clarity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plane&lt;/span&gt; &lt;span class="o"&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;height&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;plane&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;plane&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="nx"&gt;data&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rgba&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;plane&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rgba&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="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&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;plane&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rgba&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="mi"&gt;4&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;float32&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&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;width&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;The current device policy uses a 512 pixel maximum working edge on phones and tablets, and 1024 on desktop. These are inference dimensions; they do not imply that every source photo gains printable detail at that resolution.&lt;/p&gt;

&lt;p&gt;Memory use includes decoded pixels, tensors, intermediate activations, the model and PNG encoding. We close image bitmaps, dispose tensors and shrink temporary canvases when their work finishes.&lt;/p&gt;

&lt;p&gt;We also separate line thickness adjustments from inference. A small grayscale buffer lets us strengthen neighboring dark pixels without running the model again. On phones and tablets we terminate the worker after a result; on desktop we allow a short idle window before releasing it. The grayscale buffer can survive that worker teardown so an adjustment does not need to reload the model.&lt;/p&gt;

&lt;p&gt;Free conversion does not upload the selected photo. Choosing to save its result to an account is a separate upload action. The remote AI path has a different data flow, so the interface explains it separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI gives more control over the drawing
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://colorwillow.com/ai-photo-to-coloring-page/" rel="noopener noreferrer"&gt;AI photo to coloring page converter&lt;/a&gt; can request simpler details, a simplified background, different line weights and illustration styles.&lt;/p&gt;

&lt;p&gt;These are structured product options. The server validates them and builds the generation instructions; it does not accept an arbitrary prompt from the browser.&lt;/p&gt;

&lt;p&gt;That gives each control a specific job. Simple detail asks for larger coloring areas. Bold lines strengthens contours. Simplifying a background should preserve the main subjects. A style choice should keep the output usable as black outlines on white paper.&lt;/p&gt;

&lt;p&gt;Those instructions express intent, not a guarantee of exact preservation. In the dog example, the AI reduced texture but changed recognizable details. Comparing the result with the original remains part of using the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote generation needs a durable job
&lt;/h2&gt;

&lt;p&gt;The browser path can finish with a local PNG. Remote generation introduces an upload, an external provider, private storage and credits.&lt;/p&gt;

&lt;p&gt;Our backend runs on Cloudflare Workers. A Durable Object with SQLite storage coordinates generation state and credit reservations, while private R2 storage holds originals and results.&lt;/p&gt;

&lt;p&gt;The job progresses through queued, dispatching, uncertain or pending output states, and finally a terminal result. An uncertain provider response stays unresolved while the system checks what happened. A browser timeout alone does not prove that generation failed.&lt;/p&gt;

&lt;p&gt;Three details make this manageable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repeated submissions have an identity.&lt;/strong&gt; Reusing a job ID with the same input and options returns the existing job. Reusing it with different parameters is rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing uses a lease.&lt;/strong&gt; A worker claims the job temporarily, and state changes check its lease token so an older attempt cannot complete over a newer claim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completion follows storage.&lt;/strong&gt; The runner validates and normalizes the returned image, records output metadata, writes it to private storage and confirms the stored object before completing the job and consuming the credit reservation. Confirmed failures release the reservation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output write and the database transaction are separate operations. Keeping an intermediate output state gives recovery code something concrete to reconcile if execution stops between them.&lt;/p&gt;

&lt;p&gt;Private images also need authorization at retrieval time. We keep them separate from the public coloring library and check the requesting owner's access before serving them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the path by the result
&lt;/h2&gt;

&lt;p&gt;For this product, local inference is useful when someone wants a free outline that stays close to the photo, with processing on their device. Remote AI is useful when they want broader coloring areas or a different illustration style and accept that details may change.&lt;/p&gt;

&lt;p&gt;Both paths end with a practical task: inspect the picture, download it or print it, and color it on paper.&lt;/p&gt;

&lt;p&gt;The wider &lt;a href="https://colorwillow.com/" rel="noopener noreferrer"&gt;ColorWillow library of printable coloring pages&lt;/a&gt; is available when a ready made illustration suits the activity better. Photo conversion adds another starting point: a familiar pet, object or moment that someone wants to turn into a page of their own.&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>coloringpage</category>
      <category>ai</category>
      <category>coloringpagemaker</category>
    </item>
    <item>
      <title>Building a Mandala Maker: From a Single Stroke to Printable Art</title>
      <dc:creator>David Gao</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:34:02 +0000</pubDate>
      <link>https://dev.to/ai_builder_ing/building-a-mandala-maker-from-a-single-stroke-to-printable-art-n01</link>
      <guid>https://dev.to/ai_builder_ing/building-a-mandala-maker-from-a-single-stroke-to-printable-art-n01</guid>
      <description>&lt;p&gt;Draw a small curve, repeat it around a center, and it starts to look like a flower. Add another shape, and the whole pattern changes.&lt;/p&gt;

&lt;p&gt;That interaction is the idea behind the mandala maker I recently added to &lt;a href="https://colorwillow.com/" rel="noopener noreferrer"&gt;ColorWillow&lt;/a&gt;, a website with printable coloring pages and tools for creating your own.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvvcubw55wd35pcr6xh7.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvvcubw55wd35pcr6xh7.png" alt="Rainbow mandala generated in ColorWillow on a black background" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An actual Surprise pattern exported from the maker with rainbow ink on a black background.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://colorwillow.com/mandala-maker/" rel="noopener noreferrer"&gt;free online mandala maker&lt;/a&gt; combines freehand drawing, reusable shapes, and generated starting patterns. You can edit the result, download it as SVG, PNG, or PDF, or print it as a coloring page. Drawing and pattern generation run in the browser, without an account or image upload.&lt;/p&gt;

&lt;p&gt;The interesting part of building it was deciding how much the tool should do for you, and how much control it should leave in your hands.&lt;/p&gt;

&lt;h2&gt;
  
  
  One stroke repeated around a center
&lt;/h2&gt;

&lt;p&gt;The basic drawing model is small: record a stroke once, then render rotated copies around the center.&lt;/p&gt;

&lt;p&gt;With eight repeats, each copy is rotated another 45 degrees. Mirror mode adds reflected copies, giving a different rhythm to the same mark. A loose curve can become a ring of petals; a short angular stroke can become a border.&lt;/p&gt;

&lt;p&gt;SVG fits this well: define the original path once in &lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt;, then use &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; to instance it at different angles. This is a simplified version of the rotation-only rendering step:&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;rotatedCopies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;folds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;folds&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&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="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`&amp;lt;use href="#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pathId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" transform="rotate(&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="mi"&gt;360&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;folds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)" /&amp;gt;`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SVG viewBox is centered on &lt;code&gt;(0, 0)&lt;/code&gt;, so each rotation happens around the drawing center. The original path stays shared across its repeats, and the export retains vector geometry.&lt;/p&gt;

&lt;p&gt;I keep the repetition settings with each stroke. Changing the symmetry control affects what you draw next, which lets one design combine different rhythms without unexpectedly rearranging earlier work.&lt;/p&gt;

&lt;p&gt;There are also shapes such as petals, leaves, circles, and decorated fans. You can place one and drag to rotate it, then build outward from there.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foq2ut7qh5mqtc3wmssc4.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foq2ut7qh5mqtc3wmssc4.jpg" alt="ColorWillow mandala maker with Surprise controls, rainbow ink and editable layers" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The live editor combines a drawing surface, a generated starting pattern, and controls for individual layers.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving random patterns some structure
&lt;/h2&gt;

&lt;p&gt;An empty canvas can be a demanding first screen. “Surprise me” gives you a complete pattern to react to.&lt;/p&gt;

&lt;p&gt;The generator combines motif families, repeat counts, silhouettes, and levels of detail. Those choices sit inside layout rules: different elements occupy different distances from the center, and their sizes account for the space available between repeats.&lt;/p&gt;

&lt;p&gt;I use a seeded pseudo-random sequence to make those choices reproducible. The core is small:&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;seededRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &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="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1664525&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1013904223&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;state&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4294967296&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;With the seed, density, and other inputs held fixed, the same generator recipe produces the same strokes. That lets the density control rebuild a pattern from a consistent starting point, rather than drawing a completely unrelated composition on every slider movement.&lt;/p&gt;

&lt;p&gt;That structure matters. Repeating an elaborate petal too many times can turn its details into a knot of lines. Increasing density needs to preserve enough space for the pattern to remain readable.&lt;/p&gt;

&lt;p&gt;Layer overlap needed attention too. Painting a petal white to cover the lines behind it would leave white patches on a dark or transparent export.&lt;/p&gt;

&lt;p&gt;Instead, I wrap the accumulated earlier layers in an SVG mask: white keeps the existing ink, and a black copy of the new petal's boundary hides the covered area. I then draw the new petal's outlines above it. Using &lt;code&gt;maskUnits="userSpaceOnUse"&lt;/code&gt; keeps the mask in the same coordinates as the artwork. The covered region becomes transparent, so the chosen background shows through.&lt;/p&gt;

&lt;p&gt;The density slider gives people a way to explore simpler and more intricate versions. While a Surprise remains unedited, it can reshape that generated pattern live. After you edit a layer, density applies to the next Surprise, preserving the changes you have made.&lt;/p&gt;

&lt;p&gt;These patterns come from geometry and composition rules running locally. They need no image-generation service or per-design inference call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the generated result editable
&lt;/h2&gt;

&lt;p&gt;The most useful property of Surprise is that its output uses the same drawing model as the rest of the editor.&lt;/p&gt;

&lt;p&gt;Each repeated motif remains a layer. Select a petal layer and you can move, resize, recolor, or hide all of its repeats together. You can remove a busy border, open up the center, or add your own strokes.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqxgcgc0gkimen7fmpn5.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqxgcgc0gkimen7fmpn5.jpg" alt="Selecting a mandala layer highlights all its repeated petals" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Selecting one layer highlights its repeated petals together. The highlight is an editing guide and does not appear in the export.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Keeping structured drawing data makes that possible. A flattened image would lose the relationships between the original mark, its repeats, and its settings.&lt;/p&gt;

&lt;p&gt;The SVG scene is derived from that data. The editor and exports share the rendering code, which helps keep the downloaded artwork consistent with what you were editing.&lt;/p&gt;

&lt;p&gt;It also means a generated pattern can become something quite different through a few deliberate changes. Surprise supplies the initial composition; the layers give you a way to take it somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing for both the screen and the printer
&lt;/h2&gt;

&lt;p&gt;A colorful mandala on black makes a striking screen image. A coloring page needs a different presentation: clear black outlines, white paper, and space for pencils or markers.&lt;/p&gt;

&lt;p&gt;Ink and output background are separate controls. Choosing a white export background keeps your ink colors; turning on Black &amp;amp; white produces black outlines. That distinction lets you explore color on screen and then prepare a paper version of the same design.&lt;/p&gt;

&lt;p&gt;Rainbow rings use a shared radial gradient centered on the artwork. Colors change with distance from the center, so motifs around the same ring stay visually connected. Setting &lt;code&gt;gradientUnits="userSpaceOnUse"&lt;/code&gt; makes the gradient use the drawing's coordinate system, rather than fitting a separate gradient into each shape's bounding box.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dei6dgz796r9h76c7o2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dei6dgz796r9h76c7o2.png" alt="The same mandala exported as black outlines on white paper" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same pattern exported with Black &amp;amp; white enabled and a white background, ready to use as a coloring page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The formats serve different purposes. SVG keeps vector paths for scaling and further use. PNG provides a raster image. PDF places the artwork on A4 or US Letter, with an adjustable print size and a small paper preview.&lt;/p&gt;

&lt;p&gt;PNG is rasterized from the SVG scene. The PDF embeds that rendered image on the selected paper size.&lt;/p&gt;

&lt;p&gt;For a quick printable result, choose Black &amp;amp; white, keep the output background white, and select your paper size. If you would rather start with a finished illustration, ColorWillow also has a collection of &lt;a href="https://colorwillow.com/coloring-pages/mandalas/" rel="noopener noreferrer"&gt;printable mandala coloring pages&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it easy to return to a drawing
&lt;/h2&gt;

&lt;p&gt;The maker saves up to ten editable drawings in the current browser. A saved drawing keeps its layers and settings, so you can continue adjusting it later.&lt;/p&gt;

&lt;p&gt;There is also an editable-file download and a restore option. Browser storage can be cleared or become unavailable, so a downloaded file gives people a portable copy of work they want to keep.&lt;/p&gt;

&lt;p&gt;This keeps the first session simple: open the page, try a pattern, and start changing it. Signing in is unnecessary for this tool.&lt;/p&gt;

&lt;p&gt;Building it reinforced three choices I would make again: let a small gesture produce an immediate result, give randomness a composition to work within, and preserve enough structure for people to keep editing.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://colorwillow.com/mandala-maker/" rel="noopener noreferrer"&gt;try the mandala maker&lt;/a&gt; yourself, or explore &lt;a href="https://colorwillow.com/" rel="noopener noreferrer"&gt;ColorWillow&lt;/a&gt; for more free printable coloring pages.&lt;/p&gt;

&lt;p&gt;I would love to see what you make with it—and hear which parts of drawing, editing, or printing could feel better.&lt;/p&gt;

</description>
      <category>svg</category>
      <category>mandala</category>
      <category>coloringpage</category>
      <category>mandalamaker</category>
    </item>
  </channel>
</rss>
