Chapter 35: Canvas Element & 2D Graphics Basics

Drawing Rectangles & Linear Gradients

The primitive rectangle API (`fillRect`, `strokeRect`, `clearRect`), gradient generators, color stops, the 0.5px stroke alignment problem, and dynamic bar charts.

LEARNING OBJECTIVES
  • Master the three primitive rectangle methods: ctx.fillRect(), ctx.strokeRect(), and ctx.clearRect().
  • Resolve the $0.5\text{px}$ subpixel stroke antialiasing blur on $1\text{px}$ raster outlines.
  • Create and position complex CanvasGradient objects using linear and radial color interpolation with color stops.
  • Build a dynamic, data-driven financial bar chart with layered gradient fills.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

The Stencil Cutter, Spray Paint, and the Magic Eraser

In the Canvas 2D specification, rectangles are the only native geometric primitive that can be drawn directly without initializing a path (beginPath()).

+-----------------------------------------------------------------------------+
|                         THE RECTANGLE TOOLKIT                               |
+-----------------------------------------------------------------------------+

 1. fillRect(x, y, w, h)   ---> The Spray Gun:
    - Immediately paints a solid or gradient rectangular block onto the canvas.
    - Does not touch or modify the current path.

 2. strokeRect(x, y, w, h) ---> The Precision Fine-Tip Pen:
    - Draws an outlined rectangular frame with thickness = ctx.lineWidth.
    - Half of the stroke falls inside the rectangle, half falls outside.

 3. clearRect(x, y, w, h)  ---> The Laser Eraser:
    - Instantly resets every pixel in the rectangular region to transparent black:
      rgba(0, 0, 0, 0).
    - Acts like punching a transparent hole clean through the paper!

Technical Deep Dive & Specifications

The Core Rectangle API

// 1. Solid filled box
ctx.fillStyle = '#3b82f6';
ctx.fillRect(x, y, width, height);

// 2. Outlined border box
ctx.strokeStyle = '#60a5fa';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);

// 3. Transparent erasure cutout
ctx.clearRect(x, y, width, height);

Parameter Reference

  • x: The $X$-coordinate of the rectangle's top-left starting corner.
  • y: The $Y$-coordinate of the rectangle's top-left starting corner.
  • width: The horizontal length in pixels. (Negative values draw to the left).
  • height: The vertical length in pixels. (Negative values draw upward).

The $0.5\text{px}$ Subpixel Antialiasing Alignment Trap

Why do $1\text{px}$ rectangle outlines often appear blurry and $2\text{px}$ wide on standard displays?

When you tell Canvas to stroke a line at integer coordinate $X=10$ with lineWidth = 1:

  1. Canvas centers the $1\text{px}$ stroke directly on the coordinate line $X=10.0$.
  2. Half of the line ($0.5\text{px}$) falls into pixel column $9$ (from $9.5$ to $10.0$).
  3. The other half ($0.5\text{px}$) falls into pixel column $10$ (from $10.0$ to $10.5$).
  4. Because the display cannot illuminate half a physical pixel, it antialiases by blending the color across both pixels at $50%$ opacity, resulting in a blurry $2\text{px}$ gray line!
STOKING AT INTEGER X = 10.0 (BLURRY 2px):
Pixel Columns:   [ Pixel 9 ]   |   [ Pixel 10 ]
Line Center:                   | (X = 10.0)
Stroke Coverage:     [ 0.5px ] | [ 0.5px ]
Result: Blended across 2 physical pixels at 50% alpha!

STROKING AT HALF-PIXEL X = 10.5 (CRISP 1px):
Pixel Columns:   [ Pixel 9 ]   |   [ Pixel 10 ]   |   [ Pixel 11 ]
Line Center:                         | (X = 10.5)
Stroke Coverage:               [   1.0px   ]
Result: Fills EXACTLY 1 physical pixel with 100% solid opacity!

The Fix: For razor-sharp $1\text{px}$ strokes on non-scaled displays, offset coordinates by $+0.5\text{px}$:
ctx.strokeRect(10.5, 10.5, 100, 50);


Canvas Gradients (CanvasGradient)

Gradients are created via the context and assigned directly to ctx.fillStyle or ctx.strokeStyle.

1. Linear Gradients (createLinearGradient)

Defines a gradient vector from point $(x_0, y_0)$ to $(x_1, y_1)$:

(x0, y0) ================= Gradient Vector ================> (x1, y1)
   |                                                            |
Offset 0.0 (Start Color)                               Offset 1.0 (End Color)
const linearGrad = ctx.createLinearGradient(0, 0, 0, 400); // Vertical Top-to-Bottom
linearGrad.addColorStop(0.0, '#3b82f6'); // Start at 0%
linearGrad.addColorStop(0.5, '#8b5cf6'); // Midpoint at 50%
linearGrad.addColorStop(1.0, '#ec4899'); // End at 100%

ctx.fillStyle = linearGrad;
ctx.fillRect(0, 0, 200, 400);

2. Radial Gradients (createRadialGradient)

Defines two circles: a starting circle $(x_0, y_0, r_0)$ and an ending circle $(x_1, y_1, r_1)$. The color interpolates outward between the perimeters.

// createRadialGradient(x0, y0, r0, x1, y1, r1)
const radialGrad = ctx.createRadialGradient(150, 150, 10, 150, 150, 100);
radialGrad.addColorStop(0, '#fde047'); // Bright inner core
radialGrad.addColorStop(0.8, '#ea580c'); // Outer orange
radialGrad.addColorStop(1, 'rgba(0, 0, 0, 0)'); // Transparent rim

ctx.fillStyle = radialGrad;
ctx.fillRect(50, 50, 200, 200);

💻 Interactive Code Playground

Starter Code

Line-by-Line Code Breakdown

  • Lines 31–38: Nested loop constructing an alternating checkerboard background using primitive ctx.fillRect().
  • Lines 41–47: Defines a diagonal linear gradient from $(50, 50)$ to $(270, 300)$ with three vibrant color stops.
  • Line 53 (ctx.strokeRect(cardX + 0.5, ...): Applies the $+0.5\text{px}$ offset to guarantee a razor-sharp $1\text{px}$ solid white stroke border without subpixel blur.
  • Line 56 (ctx.clearRect(cardX + 30, cardY + 30, cardW - 60, 80)): Erases an $80\text{px}$ high rectangular window right through the gradient card, revealing the underlying dark checkerboard.
  • Lines 59–67: Creates a two-radius radial gradient simulating a glowing star and paints it into a $240 \times 240$ bounding box.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
+-------------------------------------------------------------+
| [Checkerboard BG]                                           |
|   +-------------------+          \   |   /                  |
|   | [Gradient Card]   |        --- (Sun) ---                |
|   |   +-----------+   |          /   |   \                  |
|   |   | [Cutout]  |   |                                     |
|   |   +-----------+   |      [Radial Gradient Burst]        |
|   +-------------------+                                     |
|   Linear + clearRect()                                      |
+-------------------------------------------------------------+

🏋️ Hands-On Exercise

🎯 The Challenge: Build a High-Tech Financial Revenue Bar Chart

Instructions:

  1. Create a function drawBarChart(ctx, data, config) that renders a dynamic horizontal revenue comparison chart.
  2. For each dataset item { label: 'Q1', value: 450, colorStart: '#3b82f6', colorEnd: '#1d4ed8' }:
    • Calculate the bar width proportional to the canvas max value.
    • Create a horizontal linear gradient across the specific bar width (createLinearGradient(x, y, x + barWidth, y)).
    • Draw the filled bar with fillRect.
    • Stroke a subtle $1\text{px}$ border around each bar.
    • Draw a glowing baseline axis and category labels.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Global Gradient Coordinate Misalignment: Constructing createLinearGradient(0, 0, 600, 0) and using it on a small box at x=500 will only paint the tail-end $15%$ of that gradient. Gradients are defined in the global canvas coordinate space, not in the local coordinates of the rectangle.
  2. Forgetting $0.5\text{px}$ Offset on $1\text{px}$ Strokes: Drawing a $1\text{px}$ stroke on integer coordinates produces fuzzy, double-thick outlines due to subpixel antialiasing splitting.
  3. Using Negative Dimensions without Accounting for Origins: ctx.fillRect(100, 100, -50, -50) draws a $50 \times 50$ box upward and to the left. While completely valid, it can cause bugs in hit-testing math if not handled carefully.

💡 Pro Tips

  1. Cache Gradients When Possible: If gradient endpoints and colors do not change every frame, store the CanvasGradient instance in a variable rather than calling ctx.createLinearGradient() inside a 60 FPS animation loop.
  2. Dirty Rect Clearing: In high-performance games or simulation engines, never call ctx.clearRect(0, 0, canvas.width, canvas.height) to wipe the entire screen if only a small $32 \times 32$ sprite moved. Only clearRect() the previous bounding box of the moving entity to save GPU bandwidth.
  3. Hex Colors with Alpha in Modern Browsers: Modern Canvas engines fully support 8-digit hexadecimal colors (e.g. ctx.fillStyle = '#38bdf880' for $50%$ opacity cyan).

📌 Key Takeaways

  • Rectangles are the only native geometric primitive drawn without initializing paths.
  • fillRect paints filled blocks, strokeRect draws borders, and clearRect cuts transparent holes.
  • To draw crisp $1\text{px}$ strokes on integer coordinate grids, offset by $+0.5\text{px}$ to align with physical pixel boundaries.
  • CanvasGradient coordinates are evaluated in global canvas space; anchor linear gradient vectors to the specific element bounding box.
  • clearRect() sets target pixel values directly to transparent black (rgba(0,0,0,0)).
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why does ctx.strokeRect(50, 50, 100, 100) with ctx.lineWidth = 1 appear blurry on a standard non-Retina display?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What is the resulting pixel state after calling ctx.clearRect(0, 0, 100, 100)?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

If a linear gradient is created with createLinearGradient(0, 0, 0, 200), in which direction does the color transition occur?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP