Applying Colors, Styles, and Gradients

1

Applying Colors, Styles, and Gradients

▶

Color and visual style are at the heart of everything drawn on an HTML5 canvas. Whether you are painting a simple rectangle, outlining a complex path, or compositing layered imagery, the Canvas 2D API provides a rich set of tools for controlling exactly how pixels are colored and how shapes blend together. These tools fall into a few broad categories: solid colors (specified in several formats), gradients (both linear and radial), repeating image patterns, and global compositing controls. Understanding how each category works — and how they interact with one another — is essential for producing anything beyond monochrome drawings.

Before diving into specific color formats, it is important to understand the two fundamental style properties that govern almost all drawing on the canvas: fillStyle and strokeStyle. These two properties live on the rendering context object (conventionally named ctx) and accept any valid color string, gradient object, or pattern object. Setting ctx.fillStyle before calling ctx.fill() determines the interior color of a closed shape. Setting ctx.strokeStyle before calling ctx.stroke() determines the color painted along the outline or path of a shape. Both properties default to solid black (#000000) if you never explicitly assign them, so a freshly created context will draw black fills and black strokes right out of the box.

A critical characteristic of these properties is that they are stateful and persistent. Once you set ctx.fillStyle = 'red', every subsequent fill operation — no matter how many shapes or paths are drawn — will use red until you change fillStyle again. This means you must pay attention to the order of your property assignments. If you forget to reset a style before drawing a new shape, that new shape will inherit whatever style was set last. A common defensive practice is to save and restore context state using ctx.save() and ctx.restore() when working with complex scenes that require many style changes.

// Default styles — both are black
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

ctx.fillRect(10, 10, 100, 50);   // Black fill (default)
ctx.strokeRect(10, 70, 100, 50); // Black stroke (default)

// Now change styles — they persist
ctx.fillStyle = 'steelblue';
ctx.strokeStyle = 'crimson';

ctx.fillRect(120, 10, 100, 50);   // Blue fill
ctx.strokeRect(120, 70, 100, 50); // Crimson stroke
ctx.fillRect(230, 10, 100, 50);   // Still blue — style persists!

The simplest way to specify a color is with a named CSS color or a hex code. The canvas accepts any valid CSS color string, which includes the full set of named colors defined by CSS (such as 'red', 'dodgerblue', 'forestgreen', and more than 140 others) as well as hexadecimal notation like '#ff0000' for pure red or '#3498db' for a specific shade of blue. Three-digit hex shorthand also works: '#f00' is equivalent to '#ff0000'. These formats are concise and easy to read, making them ideal for fixed, well-known colors.

When you need to define a color numerically — for example, when generating colors procedurally in a loop — the rgb() function is extremely useful. It takes three integer arguments, each ranging from 0 to 255, representing the red, green, and blue channels respectively. Pure red is 'rgb(255, 0, 0)', a medium grey is 'rgb(128, 128, 128)', and white is 'rgb(255, 255, 255)'. This format is especially convenient when you are computing channel values mathematically.

To introduce transparency, use rgba() instead. The fourth argument is the alpha channel, a decimal number between 0.0 (fully transparent) and 1.0 (fully opaque). An rgba color with alpha 0.5 will appear semi-transparent, allowing whatever was already drawn on the canvas beneath it to show through. This is a powerful technique for creating overlapping shapes with a sense of depth or for painting soft highlights over imagery.

// Solid red rectangle
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.fillRect(20, 20, 150, 100);

// Semi-transparent blue rectangle overlapping the red one
ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';
ctx.fillRect(80, 60, 150, 100);

An alternative to RGB is the hsl() format, which specifies color using hue, saturation, and lightness. The hue is an angle from 0 to 360 degrees on a color wheel (0/360 is red, 120 is green, 240 is blue). Saturation is a percentage where 0% is grey and 100% is fully saturated. Lightness is also a percentage where 0% is black, 50% is the pure color, and 100% is white. The hsla() variant adds an alpha channel just like rgba(). HSL is often more intuitive than RGB when you want to programmatically cycle through colors — for example, generating a rainbow by iterating the hue from 0 to 360 — or when you want to create color families by varying only the lightness or saturation of a fixed hue.

// Generate 12 evenly-spaced hues in a circle
for (let i = 0; i < 12; i++) {
  const hue = i * 30; // 0, 30, 60, 90, ... 330
  ctx.fillStyle = `hsl(${hue}, 80%, 55%)`;
  ctx.fillRect(10 + i * 40, 20, 35, 80);
}

Beyond solid colors, the canvas API supports linear gradients, which blend smoothly between two or more colors along a straight line. You create a linear gradient by calling ctx.createLinearGradient(x0, y0, x1, y1). The four arguments define the start point (x0, y0) and the end point (x1, y1) of the gradient line in canvas coordinates. The returned gradient object is not a color itself — it is a descriptor that you populate with color stops and then assign to a style property.

Color stops are added with gradient.addColorStop(offset, color). The offset is a number between 0.0 and 1.0 representing a position along the gradient line, where 0.0 is the start point and 1.0 is the end point. The color is any valid CSS color string. You must add at least two color stops. You may add as many as you like to create multi-color transitions. Colors between stops are smoothly interpolated. Once all stops are defined, assign the gradient object to ctx.fillStyle or ctx.strokeStyle and draw as normal.

// Horizontal gradient: red → yellow → green
const linearGrad = ctx.createLinearGradient(0, 0, 400, 0);
linearGrad.addColorStop(0.0, 'red');
linearGrad.addColorStop(0.5, 'yellow');
linearGrad.addColorStop(1.0, 'green');

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

// Diagonal gradient: navy → skyblue
const diagGrad = ctx.createLinearGradient(0, 0, 300, 200);
diagGrad.addColorStop(0, '#001f5b');
diagGrad.addColorStop(1, '#87ceeb');

ctx.fillStyle = diagGrad;
ctx.fillRect(0, 160, 300, 200);

It is important to understand that the gradient's coordinate space is fixed to the canvas, not to the shape being filled. If you create a gradient from x=0 to x=400 and then fill a rectangle that starts at x=200, only the right half of the gradient will be visible within that rectangle. This behavior gives you fine control over how colors span across multiple shapes if you want them all to participate in the same gradient sweep.

Radial gradients work similarly but radiate outward from a point rather than along a line, producing circular or oval color transitions that are ideal for glows, spotlights, and spherical shading. They are created with ctx.createRadialGradient(x0, y0, r0, x1, y1, r1). This function takes two circles: an inner circle defined by center (x0, y0) and radius r0, and an outer circle defined by center (x1, y1) and radius r1. Color stop offset 0.0 maps to the inner circle's edge and 1.0 maps to the outer circle's edge.

When both circles share the same center, the result is a perfectly concentric radial gradient — the classic "glow" effect. When the inner circle's center is offset from the outer circle's center, the gradient shifts to one side, producing effects that resemble a lens flare, a highlight on a sphere, or light coming from an oblique angle. This asymmetric configuration is a powerful technique for adding realism to otherwise flat circular shapes.

// Concentric radial gradient: bright white center fading to deep blue
const radialGrad = ctx.createRadialGradient(200, 200, 10, 200, 200, 150);
radialGrad.addColorStop(0.0, 'white');
radialGrad.addColorStop(0.4, 'rgba(100, 160, 255, 0.9)');
radialGrad.addColorStop(1.0, '#001030');

ctx.fillStyle = radialGrad;
ctx.beginPath();
ctx.arc(200, 200, 150, 0, Math.PI * 2);
ctx.fill();

// Off-center radial gradient: simulates a highlight on a sphere
const sphereGrad = ctx.createRadialGradient(160, 160, 5, 200, 200, 120);
sphereGrad.addColorStop(0, 'rgba(255,255,255,0.9)');
sphereGrad.addColorStop(1, 'rgba(30, 80, 160, 1.0)');

ctx.fillStyle = sphereGrad;
ctx.beginPath();
ctx.arc(200, 200, 120, 0, Math.PI * 2);
ctx.fill();

In addition to colors and gradients, the canvas supports patterns — repeating image tiles that can be used as fills or strokes. Patterns are created with ctx.createPattern(image, repetition). The image argument can be any of several source types: an HTMLImageElement (a loaded <img> tag or a programmatically created Image object), an HTMLCanvasElement (another canvas, enabling you to use procedurally generated graphics as a tile), or an HTMLVideoElement (using a video frame as the tile source). The repetition argument is a string that controls how the image tiles:

Repetition Value Behavior
'repeat' Tiles the image in both the horizontal and vertical directions (default behavior).
'repeat-x' Tiles the image only horizontally; no tiling in the vertical direction.
'repeat-y' Tiles the image only vertically; no tiling in the horizontal direction.
'no-repeat' The image appears exactly once, at the pattern origin. Areas beyond the image are transparent.

Once created, the pattern object is assigned to ctx.fillStyle or ctx.strokeStyle just like any color or gradient, and then shapes are drawn normally. The pattern tiles relative to the canvas origin (0, 0), not relative to the top-left corner of the shape being filled. This means that if your shape starts at x=50 and your tile is 40 pixels wide, the visible portion of the tile at the left edge of your shape will be whatever part of the 40-pixel cycle falls at x=50. To align the pattern's origin with your shape, you can use ctx.translate() to shift the canvas transform before drawing.

const img = new Image();
img.src = 'tile.png';
img.onload = function () {
  const pattern = ctx.createPattern(img, 'repeat');
  ctx.fillStyle = pattern;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
};

// Using another canvas as a pattern source
const offscreen = document.createElement('canvas');
offscreen.width = 20;
offscreen.height = 20;
const offCtx = offscreen.getContext('2d');
offCtx.fillStyle = '#e0e0e0';
offCtx.fillRect(0, 0, 20, 20);
offCtx.fillStyle = '#ffffff';
offCtx.fillRect(0, 0, 10, 10);
offCtx.fillRect(10, 10, 10, 10);

const checkerPattern = ctx.createPattern(offscreen, 'repeat');
ctx.fillStyle = checkerPattern;
ctx.fillRect(0, 0, 400, 300);

The final layer of color control operates at the context level rather than at the individual style level: globalAlpha and globalCompositeOperation. The ctx.globalAlpha property accepts a value between 0.0 and 1.0 and multiplies the opacity of every drawing operation — fills, strokes, images, and text — simultaneously. This is different from using rgba() or hsla() transparency, which only affects the specific fill or stroke color it is embedded in. Setting ctx.globalAlpha = 0.5 and then drawing a complex scene causes all elements of that scene to be rendered at half opacity, which is far more convenient than specifying an alpha value for every individual style.

The ctx.globalCompositeOperation property controls how newly drawn pixels are blended with the pixels already present on the canvas. The default value, 'source-over', is standard painter's algorithm compositing: new pixels are drawn on top, with the new shape's transparency determining how much of the background shows through. Other values produce dramatically different visual effects:

Composite Operation Effect
'source-over' Default. New shape is drawn on top of existing content.
'destination-over' New shape is drawn behind existing content.
'source-in' Only the part of the new shape that overlaps existing opaque pixels is drawn; everything else becomes transparent.
'destination-out' The new shape erases existing pixels where it overlaps — useful for creating cutout effects.
'multiply' Pixel values are multiplied together, darkening the result. Used for shadow and burn effects.
'screen' Inverse of multiply; brightens the result. Used for glow and light effects.
'overlay' Combines multiply and screen depending on the base color; increases contrast.
'lighten' Keeps whichever pixel is lighter at each position.
'darken' Keeps whichever pixel is darker at each position.
'xor' Overlapping areas become transparent; non-overlapping areas are drawn normally.
// Draw a background shape
ctx.fillStyle = 'royalblue';
ctx.fillRect(50, 50, 200, 150);

// Apply a global opacity and a composite operation before drawing more
ctx.globalAlpha = 0.7;
ctx.globalCompositeOperation = 'multiply';

ctx.fillStyle = 'orange';
ctx.fillRect(100, 80, 200, 150); // Blends multiplicatively with the blue rectangle

// Reset to defaults to restore normal drawing behavior
ctx.globalAlpha = 1.0;
ctx.globalCompositeOperation = 'source-over';

// This rectangle is drawn normally again
ctx.fillStyle = 'green';
ctx.fillRect(300, 50, 80, 80);

Always remember to reset globalAlpha to 1.0 and globalCompositeOperation to 'source-over' when you are done with special compositing. Because these are context-wide settings, forgetting to reset them is one of the most common sources of hard-to-diagnose visual bugs in canvas applications. Using ctx.save() before applying special composite settings and ctx.restore() afterward is the safest approach, as it automatically rolls back all context state — including globalAlpha and globalCompositeOperation — to their previous values.

Taken together, these tools — solid colors in multiple formats, linear and radial gradients with arbitrary color stops, image-based repeating patterns, global opacity, and compositing operations — form a comprehensive system for controlling the visual appearance of everything drawn on a canvas. By combining them thoughtfully, it is possible to produce sophisticated visual effects ranging from subtle transparency and shading all the way to complex multi-layer composited imagery, entirely within the browser with no external graphics software required.

NotesInstructors may wish to provide a live demo canvas where students can experiment with changing fillStyle, strokeStyle, globalAlpha, and globalCompositeOperation values in real time to observe the visual effects. A side-by-side comparison of all globalCompositeOperation modes drawn over the same background can be especially illuminating.