LEARNING OBJECTIVES ⌵
- Master the three primitive rectangle methods:
ctx.fillRect(),ctx.strokeRect(), andctx.clearRect(). - Resolve the $0.5\text{px}$ subpixel stroke antialiasing blur on $1\text{px}$ raster outlines.
- Create and position complex
CanvasGradientobjects using linear and radial color interpolation with color stops. - Build a dynamic, data-driven financial bar chart with layered gradient fills.
📖 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:
- Canvas centers the $1\text{px}$ stroke directly on the coordinate line $X=10.0$.
- Half of the line ($0.5\text{px}$) falls into pixel column $9$ (from $9.5$ to $10.0$).
- The other half ($0.5\text{px}$) falls into pixel column $10$ (from $10.0$ to $10.5$).
- 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
+-------------------------------------------------------------+
| [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:
- Create a function
drawBarChart(ctx, data, config)that renders a dynamic horizontal revenue comparison chart. - 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
⚠️ Common Pitfalls
- Global Gradient Coordinate Misalignment: Constructing
createLinearGradient(0, 0, 600, 0)and using it on a small box atx=500will 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. - 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.
- 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
- Cache Gradients When Possible: If gradient endpoints and colors do not change every frame, store the
CanvasGradientinstance in a variable rather than callingctx.createLinearGradient()inside a 60 FPS animation loop. - 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. OnlyclearRect()the previous bounding box of the moving entity to save GPU bandwidth. - 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.
fillRectpaints filled blocks,strokeRectdraws borders, andclearRectcuts 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.
CanvasGradientcoordinates 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)).- --