<?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: Brian Liu</title>
    <description>The latest articles on DEV Community by Brian Liu (@brian_liu_85cead238ea16be).</description>
    <link>https://dev.to/brian_liu_85cead238ea16be</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%2F4055022%2Fef3fee61-5d4d-4017-b52c-de0af6916c7e.png</url>
      <title>DEV Community: Brian Liu</title>
      <link>https://dev.to/brian_liu_85cead238ea16be</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brian_liu_85cead238ea16be"/>
    <language>en</language>
    <item>
      <title>The 300px Canvas Bug That Shrunk My React Image Editor</title>
      <dc:creator>Brian Liu</dc:creator>
      <pubDate>Thu, 30 Jul 2026 12:14:51 +0000</pubDate>
      <link>https://dev.to/brian_liu_85cead238ea16be/the-300px-canvas-bug-that-shrunk-my-react-image-editor-508k</link>
      <guid>https://dev.to/brian_liu_85cead238ea16be/the-300px-canvas-bug-that-shrunk-my-react-image-editor-508k</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;I am building &lt;a href="https://text-remover.org/tools/text-remover" rel="noopener noreferrer"&gt;a browser-based text removal workspace&lt;/a&gt; where a user uploads an image, paints over unwanted text or objects, and sends the resulting mask to an image-editing pipeline.&lt;/p&gt;

&lt;p&gt;The mask editor uses three stacked &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a base canvas for the uploaded image;&lt;/li&gt;
&lt;li&gt;an overlay canvas for the painted mask;&lt;/li&gt;
&lt;li&gt;a cursor canvas for the brush preview and pointer events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three canvases must have identical dimensions. The pointer coordinates must also map back to the same bitmap coordinate system, or the generated mask will not match the part of the image the user selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix
&lt;/h2&gt;

&lt;p&gt;On desktop, the editor had plenty of horizontal space but the uploaded image appeared inside a narrow strip surrounded by a large empty area. The result preview used the available width correctly, so the two sides of the same workspace looked unrelated.&lt;/p&gt;

&lt;p&gt;The visible symptom was a tiny image editor. The actual failure started before the image was drawn.&lt;/p&gt;

&lt;p&gt;The initialization code measured the width of the canvas wrapper:&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;container&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;parentElement&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;containerWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;containerHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It then calculated the largest canvas size that would preserve the uploaded image's aspect ratio:&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;imgAspectRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&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;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;containerAspectRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;containerHeight&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;canvasWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;canvasHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;imgAspectRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;containerAspectRatio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;canvasWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerWidth&lt;/span&gt;
  &lt;span class="nx"&gt;canvasHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imgAspectRatio&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;canvasHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerHeight&lt;/span&gt;
  &lt;span class="nx"&gt;canvasWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;imgAspectRatio&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The aspect-ratio calculation was correct. The measurement it received was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: The Canvas Measured Itself
&lt;/h2&gt;

&lt;p&gt;The wrapper was a relatively positioned element with no declared width:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"relative transition-all duration-500 ease-out"&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageLoaded&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageLoaded&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&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;0.98&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="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;canvasRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"block"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;maskCanvasRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"pointer-events-none absolute left-0 top-0"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cursorCanvasRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"absolute left-0 top-0 cursor-none"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before initialization, an HTML canvas has an intrinsic default size of &lt;code&gt;300 × 150&lt;/code&gt;. Because the wrapper did not claim the available workspace width, its width collapsed around the in-flow base canvas.&lt;/p&gt;

&lt;p&gt;The sequence became:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The base canvas started at its intrinsic 300px width.&lt;/li&gt;
&lt;li&gt;The wrapper sized itself around that canvas.&lt;/li&gt;
&lt;li&gt;The initializer read the wrapper's &lt;code&gt;clientWidth&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The code received roughly 300px and treated it as the available editor width.&lt;/li&gt;
&lt;li&gt;All three canvases were deliberately resized to fit that incorrect measurement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code was not randomly producing a 300px editor. It was faithfully preserving the browser's default canvas width.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The fix was one utility class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- &amp;lt;div className="relative transition-all duration-500 ease-out"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;div className="relative w-full transition-all duration-500 ease-out"&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;w-full&lt;/code&gt;, the wrapper resolves against the real workspace width before &lt;code&gt;initializeCanvas&lt;/code&gt; reads &lt;code&gt;clientWidth&lt;/code&gt;. The existing aspect-ratio calculation can then do its intended job.&lt;/p&gt;

&lt;p&gt;No hard-coded desktop width was needed. The containing layout remains responsible for the available space, while the canvas code remains responsible for fitting the image inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Did Not Set &lt;code&gt;canvas.style.width = "100%"&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There are two sizes involved in a canvas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the bitmap dimensions represented by &lt;code&gt;canvas.width&lt;/code&gt; and &lt;code&gt;canvas.height&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the displayed CSS dimensions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stretching only the CSS box could make the editor look wider while leaving its internal bitmap and pointer coordinate system unchanged.&lt;/p&gt;

&lt;p&gt;This editor calculates pointer positions using the ratio between the bitmap size and the rendered rectangle:&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;rect&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="nf"&gt;getBoundingClientRect&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;scaleX&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;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scaleY&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;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scaleX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scaleY&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 safer correction was to fix the parent measurement, then let the existing initialization update the bitmap and display dimensions for all three canvas layers together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;I checked the complete interaction rather than stopping at the visual change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the uploaded image now uses the available desktop workspace width;&lt;/li&gt;
&lt;li&gt;the image keeps its original aspect ratio;&lt;/li&gt;
&lt;li&gt;the base, mask, and cursor canvases remain aligned;&lt;/li&gt;
&lt;li&gt;brush strokes stay under the pointer;&lt;/li&gt;
&lt;li&gt;the generated binary mask is still resized to the original image dimensions;&lt;/li&gt;
&lt;li&gt;the editor remains usable at mobile widths;&lt;/li&gt;
&lt;li&gt;TypeScript checks, ESLint, and the production build pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result preview and the editor now have a consistent visual scale, so users can accurately mark small text and objects instead of painting on a compressed version of the image.&lt;/p&gt;

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

&lt;p&gt;A layout measurement is only as reliable as the element being measured.&lt;/p&gt;

&lt;p&gt;When a component reads &lt;code&gt;clientWidth&lt;/code&gt; during initialization, I now check three things first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What establishes the measured element's width?&lt;/li&gt;
&lt;li&gt;Does a replaced element such as &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; contribute an intrinsic fallback size?&lt;/li&gt;
&lt;li&gt;Am I fixing the layout constraint, or merely stretching the rendered output afterward?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This bug looked like a canvas rendering problem, but the drawing code was working correctly. The browser was giving the component exactly the width its layout had asked for.&lt;/p&gt;

&lt;p&gt;Sometimes the most useful fix is one class name backed by a much better understanding of where the number came from.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
