


Table of Contents
Because the canvas operates at such a low level, it gives us the ability to directly manipulate individual pixels on images. This allows us to create some really interesting effects, like the following glitch effect that also plays a static-sounding hissing noise:
By default, you won't hear the sound when the page loads. That is due to a browser feature to prevent our pages from automatically blaring random stuff into our speakers. To hear the sound, click or tap on the above image. If the sound gets too annoying after a while (like, let's say in 4 seconds), just click or tap the image again to toggle the sound on or off.
Your coding exercise here is to recreate the above effect. It's definitely a bit involved, so a helpful tip would be to break the functionalilty up into smaller sections.
Onwards!
The following tutorials may provide some helpful tips and techniques to help you with this exercise: Playing Sounds Using the Audio Element, Playing a Randomly Generated Sound, and Drawing Images on the Canvas.
A big part of this effect revolves around manipulating pixels. Take a look at the following example:
Click on the various buttons to change the appearance of the images. The full source code for this example can be seen here, so take a look at that for some inspiration that can help you create the glitch effect.
We want to make this a fun learning activity. If you are stuck and need help, please ask on the forums. Please explain your problem in detail and one of the many helpful forum members will help you out.
Once you have completed this exercise, you have earned the tremendous bragworthy privilege of adding the following badge to your collection:
To claim it, head to the forums and respond in the Advanced Glitch Effect topic. Be sure to include a link to your solution or insert a copy of your HTML/CSS/JS in the body of the message. Once you have created your topic, Kirupa will give you a virtual high-five and ensure this badge is added to your list of assigned badges.
As with all coding exercises, there are a billion ways to solve them. Below is the code behind the example we saw at the beginning:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Glitch Image Animation with Sound</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #000;
}
canvas {
max-width: 500px;
max-height: 500px;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
cursor: pointer;
}
</style>
</head>
<body>
<canvas id="glitchCanvas"></canvas>
<script>
// Class definition for creating the advanced glitch animation
class AdvancedGlitchAnimation {
/**
* Constructor for AdvancedGlitchAnimation.
* @param {string} imageUrl - The URL of the image to glitch.
* @param {string} canvasId - The ID of the canvas element.
*/
constructor(imageUrl, canvasId) {
// Get the canvas element from the DOM
this.canvas = document.getElementById(canvasId);
// Get the 2D rendering context for the canvas
this.ctx = this.canvas.getContext('2d');
// Create a new Image object
this.image = new Image();
// Set the source of the image to the provided URL
this.image.src = imageUrl;
// Set up the AudioContext for creating and controlling sound
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Define the channels (colors) to be affected by glitching
this.glitchChannels = ['red', 'green', 'blue'];
// Define the types of glitch effects that can occur
this.glitchEffects = ['shift', 'slice', 'distort'];
/**
* @type {number}
* @description Glitch intensity, a value between 0 and 1.
*/
this.glitchIntensity = 0.5;
/**
* @type {number}
* @description Glitch frequency, a value between 0 and 1, where higher is more frequent.
*/
this.glitchFrequency = 0.1;
// When the image has loaded, set the canvas dimensions and start the animation
this.image.onload = () => {
this.canvas.width = this.image.width;
this.canvas.height = this.image.height;
this.startAnimation();
};
}
/**
* Creates a static glitch sound effect.
* @param {number} intensity - The intensity of the glitch effect.
*/
createStaticGlitch(intensity) {
// Calculate the buffer size needed for 100 milliseconds of static sound
const bufferSize = this.audioContext.sampleRate * 0.1; // 0.1 seconds (100 milliseconds) of static
// Create an audio buffer with one channel, calculated buffer size, and audio context's sample rate
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
// Get the raw audio data for the buffer to manipulate
const data = buffer.getChannelData(0);
// Loop through each sample in the buffer and fill it with random noise values
for (let i = 0; i < bufferSize; i++) {
// Generate a random number between -1 and 1
// This creates white noise, a random signal with equal intensity at different frequencies
data[i] = Math.random() * 2 - 1;
}
// Create a buffer source node to play the audio buffer
const noiseSource = this.audioContext.createBufferSource();
// Assign the buffer to the noise source
noiseSource.buffer = buffer;
// Create a gain node to control the volume
const gainNode = this.audioContext.createGain();
// Immediately set the gain to 0 to start silently
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime);
// Generate a random volume variation between 0.5 and 1 for dynamic volume
const randomMultiplier = (Math.random() * 0.5) + 0.5;
// Calculate the gain value based on intensity and the random multiplier
// Reduced intensity for static sound for a subtler effect
const gainValue = intensity * 0.05 * randomMultiplier;
// Immediately set the gain to the calculated gainValue to control the initial volume
gainNode.gain.setValueAtTime(gainValue, this.audioContext.currentTime);
// Fade out the static sound by setting the gain back to 0 after 100 milliseconds
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime + 0.1); // Fade out after 100 milliseconds
// Connect the noise source to the gain node and then to the audio destination
noiseSource.connect(gainNode);
// Connect the gain node to the audio destination, which is the device's speakers
gainNode.connect(this.audioContext.destination);
// Start playing the noise
noiseSource.start();
// Stop playing after 100ms, matching the duration of the buffer
noiseSource.stop(this.audioContext.currentTime + 0.1);
}
/**
* Applies a random glitch effect to the image.
*/
randomGlitch() {
// Get local references to the canvas and image objects
const canvas = this.canvas;
const img = this.image;
// Clear the canvas before drawing the next frame
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw the original image onto the canvas as a base
this.ctx.drawImage(img, 0, 0);
// Determine whether to apply a glitch effect based on the glitch frequency
if (Math.random() < this.glitchFrequency) {
// Store the current glitch intensity for use in effects
const intensity = this.glitchIntensity;
// Select a random effect from the list of available glitch effects
const effect = this.glitchEffects[Math.floor(Math.random() * this.glitchEffects.length)];
// Create a static glitch sound effect based on the current intensity
this.createStaticGlitch(intensity);
// Apply the selected glitch effect based on the 'effect' variable
switch (effect) {
case 'shift':
this.colorShiftGlitch(intensity);
break;
case 'slice':
this.imageSliceGlitch(intensity);
break;
case 'distort':
this.pixelDistortGlitch(intensity);
break;
}
} else {
// If no glitch is applied, redraw the original image without changes
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(this.image, 0, 0);
}
}
/**
* Applies a color shift glitch effect by randomly shifting color channels.
* @param {number} intensity - The intensity of the color shift.
*/
colorShiftGlitch(intensity) {
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const data = imageData.data;
const maxOffset = Math.floor(100 * intensity);
for (let i = 0; i < data.length; i += 4) {
if (Math.random() < intensity) {
const channel = this.glitchChannels[Math.floor(Math.random() * this.glitchChannels.length)];
const offset = Math.floor(Math.random() * maxOffset * 2 - maxOffset);
switch (channel) {
case 'red':
// Adjust the red color channel value within the valid range
data[i] = Math.max(0, Math.min(255, data[i] + offset));
break;
case 'green':
// Adjust the green color channel value within the valid range
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + offset));
break;
case 'blue':
// Adjust the blue color channel value within the valid range
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + offset));
break;
}
}
}
// Put the modified image data back onto the canvas
this.ctx.putImageData(imageData, 0, 0);
}
// Method to apply an image slice glitch effect
// This method randomly slices and repositions portions of the image
imageSliceGlitch(intensity) {
const sliceCount = Math.floor(Math.random() * (10 * intensity) + 3);
const maxOffset = Math.floor(50 * intensity);
for (let i = 0; i < sliceCount; i++) {
const isVertical = Math.random() < 0.1;
if (isVertical) {
const sliceWidth = Math.floor(Math.random() * this.canvas.width * intensity / 4);
const sourceX = Math.floor(Math.random() * this.canvas.width);
const destX = Math.floor(Math.random() * this.canvas.width);
const offsetY = Math.floor(Math.random() * maxOffset * 2 - maxOffset);
this.ctx.drawImage(
this.canvas,
sourceX, 0, sliceWidth, this.canvas.height,
destX, offsetY, sliceWidth, this.canvas.height
);
} else {
const sliceHeight = Math.floor(Math.random() * this.canvas.height * intensity / 4);
const sourceY = Math.floor(Math.random() * this.canvas.height);
const destY = Math.floor(Math.random() * this.canvas.height);
const offsetX = Math.floor(Math.random() * maxOffset * 2 - maxOffset);
this.ctx.drawImage(
this.canvas,
0, sourceY, this.canvas.width, sliceHeight,
offsetX, destY, this.canvas.width, sliceHeight
);
}
}
}
// Method to apply a pixel distort glitch effect
// This method shifts individual pixels to create a distortion effect
pixelDistortGlitch(intensity) {
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const data = imageData.data;
const maxDistort = Math.floor(20 * intensity);
for (let i = 0; i < data.length; i += 4) {
if (Math.random() < intensity) {
const distortX = Math.floor(Math.random() * maxDistort * 2 - maxDistort);
const distortY = Math.floor(Math.random() * maxDistort * 2 - maxDistort);
const targetIndex = i + (distortY * this.canvas.width * 4) + (distortX * 4);
if (targetIndex >= 0 && targetIndex < data.length) {
data[i] = data[targetIndex];
data[i + 1] = data[targetIndex + 1];
data[i + 2] = data[targetIndex + 2];
}
}
}
this.ctx.putImageData(imageData, 0, 0);
}
/**
* Starts the animation loop.
*/
startAnimation() {
// Begin the animation process
this.animate();
}
/**
* Handles the animation, drawing the glitch on the canvas.
*/
animate() {
// Apply a random glitch effect in each animation frame
this.randomGlitch();
// Request the next animation frame, recursively calling this method
requestAnimationFrame(() => this.animate());
}
}
// Initialize the glitch animation after the DOM content is fully loaded
document.addEventListener('DOMContentLoaded', () => {
const glitchAnimation = new AdvancedGlitchAnimation('/codingexercises/images/kirupa.png', 'glitchCanvas'); // Instantiate the AdvancedGlitchAnimation class
let isSoundEnabled = false;
// Modify the createStaticGlitch method to respect sound toggle
glitchAnimation.createStaticGlitch = function(intensity) {
if (!isSoundEnabled) return;
const bufferSize = this.audioContext.sampleRate * 0.1;
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noiseSource = this.audioContext.createBufferSource();
noiseSource.buffer = buffer;
const gainNode = this.audioContext.createGain();
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime);
const randomMultiplier = (Math.random() * 0.5) + 0.5;
const gainValue = intensity * 0.05 * randomMultiplier;
gainNode.gain.setValueAtTime(gainValue, this.audioContext.currentTime);
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime + 0.1);
noiseSource.connect(gainNode);
gainNode.connect(this.audioContext.destination);
noiseSource.start();
noiseSource.stop(this.audioContext.currentTime + 0.1);
};
// Add click event to toggle sound and resume audio context
document.getElementById('glitchCanvas').addEventListener('click', () => {
if (glitchAnimation.audioContext.state === 'suspended') {
glitchAnimation.audioContext.resume();
isSoundEnabled = true;
} else {
// Toggle sound
isSoundEnabled = !isSoundEnabled;
// Optional: Add visual feedback for sound state
const canvas = document.getElementById('glitchCanvas');
canvas.style.opacity = isSoundEnabled ? '1' : '0.7';
canvas.style.filter = isSoundEnabled ? 'none' : 'grayscale(50%)';
}
});
});
</script>
</body>
</html>
If you have some suggestions, alternate ways of solving this, or just have some questions, please do share this in the How to Print this Pyramid Pattern thread!
Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!
:: Copyright KIRUPA 2025 //--