How to create a light point with JavaScript and Canvas? | Tutorial

- Andrés Cruz

ES En español

How to create a light point with JavaScript and Canvas? | Tutorial

View live example Download source code

Continuing with our visual experiments using HTML5, Canvas, and JavaScript, today we are going to explore how to generate lighting effects from scratch. In this article, you will learn how to create a luminous point by directly manipulating the canvas pixels, a fundamental technique for understanding how graphics engines and image processing work.

How to create a luminous point with JavaScript and Canvas?

Although there are automatic methods in Canvas to create shadows, the approach of this experiment is mathematical. We are going to calculate the lighting of each pixel based on its distance from a central point.

Here is the modernized code using ES6+ standards:

const canvas = document.getElementById('canv');
const ctx = canvas.getContext('2d');
const width = 800;
const height = 400;
// We define our "sun" or light point
const lightSource = {
    x: 100,
    y: 100,
    intensity: 15
};
const drawLightPoint = () => {
    // We get the space for the pixels
    const imageData = ctx.createImageData(width, height);
    const pixels = imageData.data;
    for (let x = 0; x < width; x++) {
        for (let y = 0; y < height; y++) {
            // We calculate the distance between the current pixel and the light source
            const d = Math.sqrt(Math.pow(x - lightSource.x, 2) + Math.pow(y - lightSource.y, 2));
            
            // We calculate the light density (shorter distance, higher intensity)
            const density = lightSource.intensity / d;
            
            // Index in the one-dimensional array (RGBA)
            const index = (x + y * width) * 4;
            // We manipulate the color channels
            pixels[index]     = density * 70;       // Red
            pixels[index + 1] = density * 70;       // Green
            pixels[index + 2] = density * 38;       // Blue (70 * 0.55)
            pixels[index + 3] = 255;                // Alpha (Opaque)
        }
    }
    // We dump the data back to the canvas
    ctx.putImageData(imageData, 0, 0);
};
drawLightPoint();

Mathematical analysis of the code and logic

For this effect to work, the secret lies in the formula for the distance between two points.

The importance of distance

The magic happens in the line const density = lightSource.intensity / d;. By dividing the intensity by the distance (d), we ensure that the pixels closest to the center have very high color values (white/yellow), while those far away tend toward zero (black).

Representation of light intensity according to distance in Canvas
Pixels at the same distance from the center form circles of equal intensity.

The pixel array (ImageData)

A common mistake when starting with Canvas is thinking that pixels are handled as a two-dimensional matrix. In reality, getImageData returns a one-dimensional TypedArray where each pixel occupies 4 consecutive positions (R, G, B, and A).

If our canvas is 800x400, the total data is: 
800 (width) * 400 (height) * 4 (channels) = 1,280,000 elements.

Varying the color of the luminous point

Multiplications by constant values serve to vary the color; try different values and you will see color changes.

Calculating the position to paint

The calculation of the idx variable is to traverse a one-dimensional array using the counters (x and y) from a matrix; this is because the getImageData function returns a one-dimensional array rather than a matrix as we might expect.

It is multiplied by four because the RGBA channels (which are 4) must be added to the matrix total; if you run the following command:

id.data.length

You will see that it returns—for our experiment—1280000; that is, the data length is calculated as:

CW * CH * TRGBA

Where:

  • CW = Canvas Width.
  • CH = Canvas Height.
  • TRGBA = RGBA channel size (4).

Which in our experiment is:

800 * 400 * 4 = 1280000

Painting the luminous point on the Canvas

Finally, we paint the color calculated through the distance between two points for each RGBA channel:

    pxl[idx] = dens * 70;
    pxl[idx + 1] = (dens * 70);
    pxl[idx + 2] = (dens * 70) * 0.55;
    pxl[idx + 3] = 255;

Optimization: The Radial Gradient method

The previous method is excellent for learning, but processing a million operations on the CPU every time you want to move the light is inefficient. For real-time applications or games, the Canvas API offers createRadialGradient, which utilizes hardware acceleration (GPU).

// Efficient alternative for production
const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 50);
gradient.addColorStop(0, 'rgba(255, 255, 200, 1)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);

Conclusion and next steps

Understanding how to manipulate individual pixels opens the doors to creating image filters, particle effects, and custom shaders in the browser. While gradients are better for simple effects, direct manipulation of ImageData is the foundation of advanced computer graphics.

If you enjoyed this experiment, don't miss our next article on how to create a sine wave effect with Canvas and JavaScript, where we will apply trigonometric functions to bring motion to our graphics.

Learn how to create lighting effects in HTML5 Canvas by manipulating pixels with JavaScript (ImageData). Step-by-step tutorial with code updated for ES6.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español