<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged canvas</title>
<link>https://coderlegion.com/tag/canvas</link>
<description>Powered by Question2Answer</description>
<item>
<title>Canvas cropping: keep source pixels separate from output size</title>
<link>https://coderlegion.com/31492/canvas-cropping-keep-source-pixels-separate-from-output-size</link>
<description>&lt;p&gt;A crop box and an export size describe two different things. The crop box chooses pixels from the decoded source; the export size decides how many pixels to write. Keeping those numbers separate makes an image editor much easier to reason about.&lt;/p&gt;
&lt;h3 id=&quot;start-with-image-pixels-not-the-preview-rectangle&quot;&gt;Start with image pixels, not the preview rectangle&lt;/h3&gt;
&lt;p&gt;Suppose the decoded image is 320 × 240 pixels. You want the region beginning at &lt;code&gt;(40, 24)&lt;/code&gt;, with a width of 200 and a height of 160. That rectangle fits because &lt;code&gt;40 + 200 &amp;lt;= 320&lt;/code&gt; and &lt;code&gt;24 + 160 &amp;lt;= 240&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The preview might be displayed at half size. In that case, a pointer position of 20 CSS pixels from its left edge represents 40 source pixels. Convert pointer coordinates using the actual displayed image rectangle before storing the crop. If the image is letterboxed inside a larger container, use the image rectangle rather than the container, or subtract the padding first.&lt;/p&gt;
&lt;p&gt;For an image drawn without rotation, the conversion is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const sourceX = (pointerX - imageRect.left) * source.width / imageRect.width;
const sourceY = (pointerY - imageRect.top) * source.height / imageRect.height;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clamp the final rectangle to the source bounds and choose an explicit rounding rule. Do not let a partially outside selection silently become an unexpected export.&lt;/p&gt;
&lt;h3 id=&quot;pass-both-rectangles-to-canvas&quot;&gt;Pass both rectangles to Canvas&lt;/h3&gt;
&lt;p&gt;The nine-argument form of &lt;code&gt;drawImage&lt;/code&gt; keeps the two jobs visible. For that 200 × 160 crop, a 500 × 400 export preserves the 5:4 aspect ratio:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;canvas.width = 500;
canvas.height = 400;
ctx.drawImage(source, 40, 24, 200, 160, 0, 0, 500, 400);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first rectangle belongs to the source image. The second belongs to the destination canvas. Setting the canvas bitmap dimensions matters: changing only its CSS width changes the preview, not the exported pixel dimensions.&lt;/p&gt;
&lt;p&gt;Validate that the dimensions are positive and the crop fits the decoded image. Also apply a pixel budget before allocating a large canvas. A very long, narrow image and a square image can have the same pixel count but different side-length constraints, so check both.&lt;/p&gt;
&lt;h3 id=&quot;encode-the-result-then-inspect-it&quot;&gt;Encode the result, then inspect it&lt;/h3&gt;
&lt;p&gt;Export through &lt;code&gt;canvas.toBlob&lt;/code&gt;, handle a null result, and check the returned type. For PNG, the browser can preserve transparency. JPEG is useful when a photographic image needs a smaller raster file, but a quality setting does not change the crop geometry. When decoding an external URL, its cross-origin policy can affect whether the canvas is exportable; a local file selected by the user avoids that particular remote-origin workflow.&lt;/p&gt;
&lt;h3 id=&quot;keep-svg-tracing-as-a-separate-decision&quot;&gt;Keep SVG tracing as a separate decision&lt;/h3&gt;
&lt;p&gt;Resizing a raster image does not turn its pixels into vector paths. Tracing is another step, with choices about color detail and smoothing. For a flat-color motif, crop away irrelevant borders first, trace it, and compare the original and vector preview at more than one zoom level. Look at small gaps, corners and separate color regions before downloading.&lt;/p&gt;
&lt;p&gt;A useful no-setup way to explore that sequence is the &lt;a rel=&quot;nofollow&quot; href=&quot;https://jpgtosvg.io/image-editor/&quot;&gt;browser image workspace in JPG to SVG&lt;/a&gt;: crop or resize a local image, then send it to tracing without selecting the file again. Processing stays in the browser, and the SVG output contains paths. I help maintain the product.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31492/canvas-cropping-keep-source-pixels-separate-from-output-size</guid>
<pubDate>Sat, 17 Oct 2026 06:37:13 +0000</pubDate>
</item>
<item>
<title>Your canvas clicks are in the wrong coordinate system</title>
<link>https://coderlegion.com/30627/your-canvas-clicks-are-in-the-wrong-coordinate-system</link>
<description>&lt;p&gt;A target at &lt;code&gt;(100, 60)&lt;/code&gt; is one quarter across and down a 400 × 240 game. Display the canvas at 800 × 480 CSS pixels and the same target appears at &lt;code&gt;(200, 120)&lt;/code&gt; relative to the canvas corner. A pointer handler that compares those CSS coordinates directly with the target will miss it.&lt;/p&gt;
&lt;p&gt;At the original display size, the two sets of numbers happened to match. Resizing exposed the missing conversion.&lt;/p&gt;
&lt;h3 id=&quot;follow-one-click-through-the-coordinates&quot;&gt;Follow one click through the coordinates&lt;/h3&gt;
&lt;p&gt;Place the larger canvas at viewport position &lt;code&gt;(100, 50)&lt;/code&gt;. A click on the target has client coordinates &lt;code&gt;(300, 170)&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Subtract the canvas origin: &lt;code&gt;300 − 100 = 200&lt;/code&gt; CSS pixels across, and &lt;code&gt;170 − 50 = 120&lt;/code&gt; down.&lt;/li&gt;
&lt;li&gt;Divide by the displayed size: &lt;code&gt;200 / 800&lt;/code&gt; and &lt;code&gt;120 / 480&lt;/code&gt; both give &lt;code&gt;0.25&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Multiply by the logical size: &lt;code&gt;0.25 × 400 = 100&lt;/code&gt; across, and &lt;code&gt;0.25 × 240 = 60&lt;/code&gt; down.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The fraction carries the position from one set of dimensions to another. If the bitmap is 1600 × 960, the same point is &lt;code&gt;(400, 240)&lt;/code&gt; in bitmap pixels. That is useful to a pixel operation, but it is still &lt;code&gt;(100, 60)&lt;/code&gt; to the game.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=3983672196422537203&quot; alt=&quot;A normalized point one quarter across and down maps to CSS coordinates 200,120, logical coordinates 100,60, and bitmap coordinates 400,240 for the listed dimensions.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;One normalized point expressed in CSS pixels, logical game units and bitmap pixels. Original diagram made with AI-assisted code; the dimensions are chosen examples.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The helper below implements that route for a canvas whose full bitmap fills its content box. It assumes no border, padding, CSS transforms on the canvas or its ancestors, letterboxing, clipped viewport or camera transform. The event and rectangle come from the same document. Use an engine's pointer-to-world API when it already accounts for additional transforms.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function toLogical(
  clientX, clientY, rect,
  width, height
) {
  if (![clientX, clientY,
        rect.left, rect.top,
        rect.width, rect.height,
        width, height]
      .every(Number.isFinite)
      || rect.width &amp;lt;= 0
      || rect.height &amp;lt;= 0
      || width &amp;lt;= 0
      || height &amp;lt;= 0)
    return null;
  const u =
    (clientX - rect.left)
    / rect.width;
  const v =
    (clientY - rect.top)
    / rect.height;
  if (u &amp;lt; 0 || u &amp;gt;= 1
      || v &amp;lt; 0 || v &amp;gt;= 1)
    return null;
  return {
    x: u * width,
    y: v * height
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On &lt;code&gt;pointerdown&lt;/code&gt;, pass &lt;code&gt;event.clientX&lt;/code&gt;, &lt;code&gt;event.clientY&lt;/code&gt;, a fresh &lt;code&gt;canvas.getBoundingClientRect()&lt;/code&gt;, and the logical scene dimensions. Ignore &lt;code&gt;null&lt;/code&gt;; otherwise use the returned position for hit testing.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX&quot;&gt;&lt;code&gt;clientX&lt;/code&gt; and &lt;code&gt;clientY&lt;/code&gt;&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect&quot;&gt;&lt;code&gt;getBoundingClientRect()&lt;/code&gt;&lt;/a&gt; share viewport coordinates. Reading the rectangle at the press accounts for scrolling. Adding a scroll offset—or substituting document-relative &lt;code&gt;pageY&lt;/code&gt;—would mix coordinate systems again.&lt;/p&gt;
&lt;p&gt;The helper includes the left and top edges and rejects the right, bottom and outside. It does not clamp a gutter click onto an edge target. Keep fractional results for logical hit testing.&lt;/p&gt;
&lt;h3 id=&quot;density-belongs-on-the-drawing-side&quot;&gt;Density belongs on the drawing side&lt;/h3&gt;
&lt;p&gt;Increasing bitmap density can sharpen the canvas without moving the target in the game. &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio&quot;&gt;&lt;code&gt;devicePixelRatio&lt;/code&gt;&lt;/a&gt; relates physical pixels to CSS pixels and can change with zoom or a different display. Multiplying the helper's output by DPR would apply a second, unwanted conversion.&lt;/p&gt;
&lt;p&gt;For the 400 × 240 logical scene, use &lt;code&gt;ctx.setTransform(canvas.width / 400, 0, 0, canvas.height / 240, 0, 0)&lt;/code&gt; when drawing. The actual integer bitmap dimensions account for resize rounding. &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform&quot;&gt;&lt;code&gt;setTransform&lt;/code&gt;&lt;/a&gt; replaces the scale rather than compounding it on each resize.&lt;/p&gt;
&lt;p&gt;Keep CSS size independent of the bitmap attributes, for example with an explicit width and aspect ratio. Otherwise enlarging &lt;code&gt;canvas.width&lt;/code&gt; can enlarge an intrinsically sized element too. Assigning &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/width&quot;&gt;&lt;code&gt;canvas.width&lt;/code&gt;&lt;/a&gt; resets the context, so restore the transform and drawing styles afterward. Handle density changes even when the element's CSS size stays the same.&lt;/p&gt;
&lt;h3 id=&quot;an-iframe-can-resize-correctly-while-clicks-remain-wrong&quot;&gt;An iframe can resize correctly while clicks remain wrong&lt;/h3&gt;
&lt;p&gt;Rosebud's &lt;a rel=&quot;nofollow&quot; href=&quot;https://rosebud.ai/blog/how-to-embed-your-rosebud-game&quot;&gt;November 2024 embedding guide&lt;/a&gt; describes responsive iframe sizing. That handles the outer display; the pointer conversion still belongs to the document containing the canvas. Host-page coordinates cannot be paired with an inner canvas rectangle, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy&quot;&gt;same-origin restrictions&lt;/a&gt; limit cross-origin access.&lt;/p&gt;
&lt;p&gt;A correctly sized embed is therefore a place to repeat the target test, not evidence that the game's input handler is correct. First verify the arithmetic with the helper above:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const wide = {
  left: 100, top: 50,
  width: 800, height: 480
};
console.log(toLogical(
  300, 170, wide, 400, 240
)); // {x: 100, y: 60}
const small = {
  ...wide,
  width: 400, height: 240
};
console.log(toLogical(
  200, 110, small, 400, 240
)); // {x: 100, y: 60}
console.log(toLogical(
  900, 170, wide, 400, 240
)); // null: right edge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then click that same target at two display sizes in the real delivery page, scroll, and try the edges and gutter. If the arithmetic passes but those presses fail, compare the measured content area with the helper's layout assumptions—especially borders, letterboxing and transforms.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/30627/your-canvas-clicks-are-in-the-wrong-coordinate-system</guid>
<pubDate>Sat, 10 Oct 2026 19:24:16 +0000</pubDate>
</item>
<item>
<title>Freehand drawing with the Canvas API</title>
<link>https://coderlegion.com/30207/freehand-drawing-with-the-canvas-api</link>
<description>&lt;p&gt;When I was trying Canvas, I was completely puzzled. It felt so frustrating; I felt like I was writing in a completely different language instead of my usual JavaScript. But after reading the MDN doc and watching some videos...it still feels difficult and frustrating. But despite everything, let's try to build something. Polishing can come later.&lt;/p&gt;
&lt;p&gt;First, we need to select the ID from HTML&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const canvas = document.querySelector(&quot;#draw&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need &lt;code&gt;canvas.getContext&lt;/code&gt; here to tell the browser what type of drawing we are going to do. For now, the screen is blank, so we need to tell it what we are going to do. The &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element has a method called &lt;code&gt;getContext&lt;/code&gt;. That's the one we are targeting here&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const ctx = canvas.getContext(&quot;2d&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are going to draw on the whole window, so we need to target the whole window. But we can't just let HTML decide the window. We need to overwrite it, and JavaScript can help here with its &lt;code&gt;innerHeight&lt;/code&gt; and &lt;code&gt;innerWidth&lt;/code&gt; properties&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's a lot. I know. So, let's slow down and draw our first rectangle.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function draw() {
  const canvas = document.querySelector(&quot;#draw&quot;);
  const ctx = canvas.getContext(&quot;2d&quot;);

  ctx.fillStyle = &quot;rgb(200 0 0 / 50%)&quot;; // setting the rect color with the fillStyle property
  ctx.fillRect(10, 10, 50, 50); // drawing the rectangle using the fillRect canvas api's method
}
draw();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That was easy, but we are not going to draw shapes. Our goal is to use freehand. Comment out or delete the previous &lt;code&gt;draw&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;For now, we need four things before going forward. Set the color of the pen, how we want our pen to be, how the ends of lines look, and the size of the pen.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;ctx.strokeStyle = &quot;rgb(200 135 90)&quot;;
ctx.lineJoin = &quot;round&quot;;
ctx.lineCap = &quot;round&quot;;
ctx.lineWidth = 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we can set the pen color with &lt;code&gt;strokeStyle&lt;/code&gt;, how corners between connected lines look with &lt;code&gt;lineJoin&lt;/code&gt;, the ending of the lines with &lt;code&gt;lineCap&lt;/code&gt; (default to butt), and the size with &lt;code&gt;lineWidth&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We don't want to draw with every movement our mouse do. Only when we click the mouse and move it. So here it is&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let isDrawing = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let lastX = 0;
let lastY = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;lastX/Y start at 0 as a placeholder. Later, they get replaced by the real mouse X/Y position from e.offsetX/Y.&lt;/p&gt;
&lt;p&gt;Let's draw it now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function draw(e) {
  if (!isDrawing) return;
  console.log(e);
  ctx.beginPath();
  ctx.moveTo(lastX, lastY);
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();
  [lastX, lastY] = [e.offsetX, e.offsetY];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The condition prevents drawing when the mouse button is not being held down&lt;br&gt;
&lt;code&gt;beginPath()&lt;/code&gt; starts a new path every time&lt;br&gt;
&lt;code&gt;moveTo()&lt;/code&gt; sets the starting point of the line&lt;br&gt;
&lt;code&gt;lineTo()&lt;/code&gt; draws a line from the current point to the current mouse position&lt;br&gt;
&lt;code&gt;stroke()&lt;/code&gt; draws the path onto the canvas using the current stroke settings&lt;br&gt;
&lt;code&gt;[lastX, lastY] = [e.offsetX, e.offsetY]&lt;/code&gt; updates to the current mouse position by destructuring&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Mouse button pressed down → start drawing
canvas.addEventListener(&quot;mousedown&quot;, (e) =&amp;gt; {
  isDrawing = true;
  [lastX, lastY] = [e.offsetX, e.offsetY];
});

// Mouse button released → stop drawing
canvas.addEventListener(&quot;mouseup&quot;, () =&amp;gt; {
  isDrawing = false;
});

// Mouse leaves canvas → also stop
canvas.addEventListener(&quot;mouseout&quot;, () =&amp;gt; {
  isDrawing = false;
});

// Mouse moves → only draw if the flag is true
canvas.addEventListener(&quot;mousemove&quot;, draw);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it for today. Tada! Leave a comment if you want to. &lt;/p&gt;
&lt;p&gt;Further reading:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes&quot;&gt;Drawing shapes with canvas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial&quot;&gt;Canvas tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.w3schools.com/html/html5_canvas.asp&quot;&gt;HTML Canvas&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30207/freehand-drawing-with-the-canvas-api</guid>
<pubDate>Thu, 08 Oct 2026 02:55:43 +0000</pubDate>
</item>
</channel>
</rss>