coderain blog

How to Animate Canvas for Infinite TV Noise Movement: JavaScript Guide

TV noise—also known as “static”—is a nostalgic visual: the grainy, flickering pattern of black, white, and gray pixels that appears when a television loses signal. Beyond nostalgia, this effect is widely used in retro-themed designs, video backgrounds, and artistic projects. In this guide, we’ll explore how to recreate infinite, moving TV noise using HTML5 Canvas and JavaScript. Unlike static flicker, we’ll add subtle movement to the noise, making it feel like it’s scrolling infinitely—perfect for dynamic backgrounds or interactive elements.

By the end of this tutorial, you’ll understand how to:

  • Set up an HTML5 Canvas for fullscreen or custom-sized animations.
  • Generate realistic TV noise using pixel manipulation.
  • Animate the noise to create smooth, infinite movement.
  • Optimize performance for seamless playback.
  • Customize the effect (speed, direction, color, etc.).

No advanced graphics knowledge is required—just basic HTML, CSS, and JavaScript skills. Let’s dive in!

2025-12

Table of Contents#

  1. Prerequisites
  2. Setting Up the HTML Canvas
  3. Understanding TV Noise: What Makes It “Noisy”?
  4. Generating a Single Frame of TV Noise
  5. Animating the Noise: From Flicker to Movement
  6. Making It Infinite: Adding Scroll Motion
  7. Optimizing Performance
  8. Customizations: Speed, Direction, and Color
  9. Troubleshooting Common Issues
  10. Reference

Prerequisites#

Before starting, ensure you have:

  • A basic understanding of HTML, CSS, and JavaScript.
  • A code editor (e.g., VS Code, Sublime Text).
  • A modern web browser (Chrome, Firefox, Safari, or Edge) to test the animation.

Setting Up the HTML Canvas#

The HTML5 Canvas element is a bitmap drawing surface that allows dynamic graphics via JavaScript. We’ll use it to render our TV noise.

Step 1: Basic HTML Structure#

Create a new HTML file (e.g., tv-noise.html) and add the following structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Infinite TV Noise Animation</title>
    <style>
        body { margin: 0; overflow: hidden; } /* Remove default margins/scrollbars */
        canvas { display: block; } /* Ensure canvas fills the window */
    </style>
</head>
<body>
    <canvas id="tvNoiseCanvas"></canvas>
 
    <script>
        // JavaScript code will go here
    </script>
</body>
</html>

Step 2: Configure Canvas Dimensions#

In the <script> tag, we’ll resize the canvas to match the window and handle resizing:

const canvas = document.getElementById('tvNoiseCanvas');
const ctx = canvas.getContext('2d');
 
// Set canvas size to window size
function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
 
// Initial resize and update on window resize
resizeCanvas();
window.addEventListener('resize', resizeCanvas);

This ensures the canvas fills the screen and adjusts when the window is resized.

Understanding TV Noise: What Makes It “Noisy”?#

TV noise (static) is caused by random electromagnetic signals interfering with a TV’s reception. Visually, it appears as a dense grid of pixels with randomly varying brightness (and sometimes color). For our animation, we’ll replicate this with:

  • Randomness: Each pixel’s brightness changes over time.
  • Movement: A subtle scroll effect to simulate the noise “moving” across the screen (instead of just flickering).

Generating a Single Frame of TV Noise#

To draw noise, we’ll use ImageData—a Canvas API object that represents pixel data (RGBA values). Each pixel is defined by 4 values: red (R), green (G), blue (B), and alpha (A, transparency). For grayscale noise, R, G, and B will be identical (0 = black, 255 = white).

Step 1: Create a Pixel Data Array#

The ImageData data array is a Uint8ClampedArray (8-bit unsigned integers, clamped between 0-255). For a canvas of width w and height h, the array length is w * h * 4 (4 values per pixel).

Step 2: Fill the Array with Random Values#

We’ll generate random values (0-255) for R, G, and B, and set alpha to 255 (fully opaque).

Code: Generate a Noise Frame#

Add this function to generate a single frame of noise:

function generateNoiseFrame(width, height) {
    // Create an array to hold pixel data (RGBA)
    const pixelCount = width * height;
    const data = new Uint8ClampedArray(pixelCount * 4);
 
    // Fill with random grayscale values
    for (let i = 0; i < pixelCount; i++) {
        const value = Math.floor(Math.random() * 256); // Random brightness (0-255)
        const index = i * 4;
        data[index] = value;     // R
        data[index + 1] = value; // G
        data[index + 2] = value; // B
        data[index + 3] = 255;   // A (opaque)
    }
 
    // Return ImageData object
    return new ImageData(data, width, height);
}

Step 3: Draw the Frame to the Canvas#

Call generateNoiseFrame with the canvas dimensions and use putImageData to render it:

// Draw initial noise frame
const initialFrame = generateNoiseFrame(canvas.width, canvas.height);
ctx.putImageData(initialFrame, 0, 0);

If you run the code now, you’ll see a single frame of static. To animate it, we need to update the frame continuously.

Animating the Noise: From Flicker to Movement#

To animate, we’ll use requestAnimationFrame—a browser API that syncs updates with the monitor’s refresh rate (~60 times per second). This ensures smooth animation.

Basic Animation Loop#

Add an animate function that generates a new noise frame and redraws it each frame:

function animate() {
    // Generate new noise frame
    const noiseFrame = generateNoiseFrame(canvas.width, canvas.height);
    ctx.putImageData(noiseFrame, 0, 0);
 
    // Request next frame
    requestAnimationFrame(animate);
}
 
// Start animation
animate();

This creates a flickering noise effect, but it’s static—no movement. To make it feel like the noise is moving, we need to add continuity between frames.

Making It Infinite: Adding Scroll Motion#

Instead of generating a completely new frame each time, we’ll scroll the noise upward (or horizontally) and fill the new empty space with fresh noise. This creates the illusion of infinite movement.

How It Works#

  1. Store the current frame’s pixel data.
  2. Shift the data upward by 1 row (removing the top row).
  3. Add a new random row to the bottom.
  4. Repeat, creating a smooth upward scroll.

Code: Implementing Scroll Motion#

First, initialize a buffer to store pixel data. Modify the code to reuse this buffer instead of creating a new array each frame (improves performance):

// Initialize buffer for pixel data (reused to avoid garbage collection)
let pixelBuffer;
let bufferWidth, bufferHeight;
 
function initBuffer() {
    bufferWidth = canvas.width;
    bufferHeight = canvas.height;
    pixelBuffer = new Uint8ClampedArray(bufferWidth * bufferHeight * 4);
    // Fill initial buffer with random noise
    for (let i = 0; i < pixelBuffer.length; i += 4) {
        const value = Math.floor(Math.random() * 256);
        pixelBuffer[i] = value;     // R
        pixelBuffer[i + 1] = value; // G
        pixelBuffer[i + 2] = value; // B
        pixelBuffer[i + 3] = 255;   // A
    }
}
 
// Initialize buffer on startup and resize
initBuffer();
window.addEventListener('resize', () => {
    resizeCanvas();
    initBuffer(); // Reset buffer when canvas resizes
});

Update Animation Loop for Scrolling#

Modify the animate function to shift the buffer upward and add a new row:

function animate() {
    const { width, height } = canvas;
    const rowLength = width * 4; // Pixels per row (4 values per pixel)
 
    // Shift buffer upward by 1 row (remove top row, shift all rows up)
    pixelBuffer.copyWithin(0, rowLength); // Copy data from row 1 to end, starting at index 0
 
    // Generate new bottom row (fill the empty space created by shifting)
    const bottomRowStart = (height - 1) * rowLength;
    for (let i = bottomRowStart; i < pixelBuffer.length; i += 4) {
        const value = Math.floor(Math.random() * 256);
        pixelBuffer[i] = value;     // R
        pixelBuffer[i + 1] = value; // G
        pixelBuffer[i + 2] = value; // B
        pixelBuffer[i + 3] = 255;   // A
    }
 
    // Draw the updated buffer
    ctx.putImageData(new ImageData(pixelBuffer, width, height), 0, 0);
 
    // Repeat
    requestAnimationFrame(animate);
}

How It Works:#

  • copyWithin(0, rowLength) shifts all rows upward by 1: data from index rowLength (start of row 2) is copied to index 0 (start of row 1), overwriting the top row.
  • The bottom row (now empty after shifting) is filled with new random values.

Run the code—you’ll now see TV noise scrolling upward infinitely!

Optimizing Performance#

For large canvases (e.g., 4K screens), generating noise can lag. Use these optimizations:

1. Reuse the ImageData Object#

Avoid creating a new ImageData in every frame. Reuse a single instance:

// Initialize ImageData once
const imageData = ctx.createImageData(canvas.width, canvas.height);
 
function animate() {
    // Update pixelBuffer (as before)...
 
    // Update the existing ImageData's data array
    imageData.data.set(pixelBuffer);
    ctx.putImageData(imageData, 0, 0);
 
    requestAnimationFrame(animate);
}

2. Limit Canvas Size#

If the window is very large (e.g., 3840x2160), scale down the canvas and use CSS to upscale it:

canvas { 
    display: block; 
    width: 100%; 
    height: 100%; 
}
// Set canvas to half resolution (faster rendering)
function resizeCanvas() {
    canvas.width = window.innerWidth / 2;
    canvas.height = window.innerHeight / 2;
}

3. Reduce Randomness Calculations#

Precompute a lookup table of random values to avoid Math.random() in loops (minor gain, but helps):

// Precompute 256 random values (0-255)
const randomValues = Array.from({ length: 256 }, () => Math.floor(Math.random() * 256));
 
// Use the lookup table in the bottom row loop:
const value = randomValues[Math.floor(Math.random() * 256)];

Customizations: Speed, Direction, and Color#

Adjust Speed#

Scroll faster by shifting more rows per frame. For example, shift 2 rows:

const rowsToShift = 2; // Adjust speed here
const shiftAmount = rowsToShift * rowLength;
pixelBuffer.copyWithin(0, shiftAmount);
 
// Fill the new empty rows (bottom `rowsToShift` rows)
const newRowsStart = (height - rowsToShift) * rowLength;
for (let i = newRowsStart; i < pixelBuffer.length; i += 4) {
    // ... (same as before)
}

Change Direction#

Horizontal Scroll#

Shift columns left/right instead of rows. For rightward scroll:

const colLength = 4; // 4 values per pixel (1 column = 1 pixel width)
const colsToShift = 1;
 
// Shift right by 1 column: copy data from column 1 to end, starting at column 1
pixelBuffer.copyWithin(colLength, 0, pixelBuffer.length - colLength);
 
// Fill leftmost column with new noise
for (let i = 0; i < pixelBuffer.length; i += rowLength) {
    const value = Math.floor(Math.random() * 256);
    pixelBuffer[i] = value;     // R
    pixelBuffer[i + 1] = value; // G
    pixelBuffer[i + 2] = value; // B
    pixelBuffer[i + 3] = 255;   // A
}

Add Color#

For colored noise, vary R, G, and B values:

// In the bottom row loop:
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
pixelBuffer[i] = r;     // R
pixelBuffer[i + 1] = g; // G
pixelBuffer[i + 2] = b; // B

Add Scanlines#

Simulate old TV scanlines by darkening every other row:

// After generating the buffer, darken even rows
for (let y = 0; y < height; y++) {
    if (y % 2 === 0) { // Even rows
        const rowStart = y * rowLength;
        for (let i = rowStart; i < rowStart + rowLength; i += 4) {
            pixelBuffer[i] *= 0.7;   // Reduce R by 30%
            pixelBuffer[i + 1] *= 0.7; // Reduce G by 30%
            pixelBuffer[i + 2] *= 0.7; // Reduce B by 30%
        }
    }
}

Troubleshooting Common Issues#

1. Canvas Not Resizing#

Ensure the resizeCanvas function updates imageData and pixelBuffer:

window.addEventListener('resize', () => {
    resizeCanvas();
    initBuffer(); // Rebuild buffer with new size
    imageData = ctx.createImageData(canvas.width, canvas.height); // Update ImageData
});

2. Lag/Stuttering#

  • Reduce canvas resolution (e.g., canvas.width = window.innerWidth / 2).
  • Lower the scroll speed (fewer rows/columns per frame).

3. Noise Isn’t Moving#

Check that copyWithin is correctly shifting the buffer. Verify rowLength is width * 4.

Reference#

Conclusion#

You now know how to create infinite scrolling TV noise using HTML5 Canvas and JavaScript! Experiment with speed, direction, and color to match your project’s needs. Whether for a retro game, video overlay, or artistic installation, this effect adds a nostalgic, dynamic touch.

Happy coding! 🎨📺