You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Creates a video-like effect with color distortion and audio from a static image.
* This simulates a 'glitchy' or 'datamoshed' video aesthetic.
*
* @param {Image} originalImg The input Image object.
* @param {number} distortionAmount A number from 0 to 1 controlling the intensity of visual glitches. Defaults to 0.3.
* @param {number} basePitch The base frequency (in Hz) for the generated audio. Defaults to 200.
* @param {number} pitchModulation The range (in Hz) by which the audio pitch will fluctuate. Defaults to 100.
* @param {string} audioType The type of audio waveform. Can be 'noise', 'sine', 'square', 'sawtooth', or 'triangle'. Defaults to 'noise'.
* @returns {HTMLElement} A div element containing the animated canvas and a play button.
*/
async function processImage(originalImg, distortionAmount = 0.3, basePitch = 200, pitchModulation = 100, audioType = 'noise') {
// Clamp distortionAmount to be between 0 and 1 for predictable results.
const clampedDistortion = Math.max(0, Math.min(1, distortionAmount));
// Create the main container and canvas elements.
const container = document.createElement('div');
const canvas = document.createElement('canvas');
// Use a backing canvas for the original image data to improve performance.
const sourceCanvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', {
willReadFrequently: true
});
const sourceCtx = sourceCanvas.getContext('2d');
// Set canvas dimensions to the original image's natural size.
canvas.width = sourceCanvas.width = originalImg.naturalWidth;
canvas.height = sourceCanvas.height = originalImg.naturalHeight;
// Draw the original image to the backing canvas and store its pixel data.
sourceCtx.drawImage(originalImg, 0, 0);
const originalImageData = sourceCtx.getImageData(0, 0, canvas.width, canvas.height);
// Perform an initial draw on the visible canvas.
ctx.drawImage(originalImg, 0, 0);
// Create a "Play" button to initiate the effects, respecting browser autoplay policies.
const playButton = document.createElement('button');
playButton.textContent = '▶';
container.appendChild(canvas);
container.appendChild(playButton);
let audioContext;
let animationFrameId;
const startEffect = async () => {
playButton.style.display = 'none';
// Initialize the Web Audio API context.
audioContext = new(window.AudioContext || window.webkitAudioContext)();
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
const masterGain = audioContext.createGain();
masterGain.gain.setValueAtTime(0.25, audioContext.currentTime); // Keep volume reasonable.
masterGain.connect(audioContext.destination);
// Audio graph setup based on the 'audioType' parameter.
const validWaveforms = ['sine', 'square', 'sawtooth', 'triangle'];
if (audioType === 'noise') {
const bufferSize = audioContext.sampleRate * 2; // 2 seconds of noise buffer.
const noiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const noiseSource = audioContext.createBufferSource();
noiseSource.buffer = noiseBuffer;
noiseSource.loop = true;
const filter = audioContext.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(basePitch, audioContext.currentTime);
filter.Q.setValueAtTime(10, audioContext.currentTime);
const lfo = audioContext.createOscillator();
lfo.type = 'sine';
lfo.frequency.setValueAtTime(5, audioContext.currentTime);
const lfoGain = audioContext.createGain();
lfoGain.gain.setValueAtTime(pitchModulation, audioContext.currentTime);
lfo.connect(lfoGain);
lfoGain.connect(filter.frequency);
noiseSource.connect(filter);
filter.connect(masterGain);
noiseSource.start();
lfo.start();
} else if (validWaveforms.includes(audioType)) {
const oscillator = audioContext.createOscillator();
oscillator.type = audioType;
oscillator.frequency.setValueAtTime(basePitch, audioContext.currentTime);
const lfo = audioContext.createOscillator();
lfo.type = 'sine';
lfo.frequency.setValueAtTime(8, audioContext.currentTime);
const lfoGain = audioContext.createGain();
lfoGain.gain.setValueAtTime(pitchModulation, audioContext.currentTime);
lfo.connect(lfoGain);
lfoGain.connect(oscillator.frequency);
oscillator.connect(masterGain);
oscillator.start();
lfo.start();
}
// Animation loop for visual effects.
let time = 0;
function animate() {
time += 0.05;
const imageData = ctx.createImageData(originalImageData.width, originalImageData.height);
const data = imageData.data;
const sourceData = originalImageData.data;
const {
width,
height
} = imageData;
// Calculate glitch parameters for the current frame.
const splitOffset = Math.round(Math.sin(time) * 15 * clampedDistortion);
const numBlocks = 3 + Math.floor(clampedDistortion * 25);
const glitchBlocks = [];
for (let i = 0; i < numBlocks; i++) {
glitchBlocks.push({
y: Math.floor(Math.random() * height),
h: Math.floor(Math.random() * height * 0.1),
offset: Math.floor((Math.random() - 0.5) * width * 0.2 * clampedDistortion)
});
}
// Process each pixel to apply combined glitch effects.
for (let y = 0; y < height; y++) {
let horizontalOffset = 0;
for (const block of glitchBlocks) {
if (y >= block.y && y < block.y + block.h) {
horizontalOffset = block.offset;
break;
}
}
for (let x = 0; x < width; x++) {
const destIndex = (y * width + x) * 4;
const sourceX = (x - horizontalOffset + width) % width;
// Apply RGB split to the glitched coordinates.
const rIndex = (y * width + (sourceX - splitOffset + width) % width) * 4;
const gIndex = (y * width + sourceX) * 4;
const bIndex = (y * width + (sourceX + splitOffset + width) % width) * 4;
data[destIndex] = sourceData[rIndex];
data[destIndex + 1] = sourceData[gIndex + 1];
data[destIndex + 2] = sourceData[bIndex + 2];
data[destIndex + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
animationFrameId = requestAnimationFrame(animate);
}
animate();
};
playButton.onclick = startEffect;
// Set up a MutationObserver to clean up resources when the element is removed from the DOM.
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.removedNodes) {
mutation.removedNodes.forEach(node => {
if (node === container) {
if (animationFrameId) cancelAnimationFrame(animationFrameId);
if (audioContext && audioContext.state !== 'closed') audioContext.close();
observer.disconnect();
}
});
}
}
});
playButton.addEventListener('click', () => {
// Once interaction starts, observe the container's parent for removal.
setTimeout(() => {
if (container.parentNode) {
observer.observe(container.parentNode, {
childList: true
});
}
}, 0);
}, {
once: true
});
// Apply styles for proper layout and appearance.
container.style.position = 'relative';
container.style.display = 'inline-block';
container.style.width = `${canvas.width}px`;
container.style.height = `${canvas.height}px`;
canvas.style.display = 'block';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
playButton.style.position = 'absolute';
playButton.style.top = '50%';
playButton.style.left = '50%';
playButton.style.transform = 'translate(-50%, -50%)';
playButton.style.fontSize = '48px';
playButton.style.width = '80px';
playButton.style.height = '80px';
playButton.style.paddingLeft = '5px';
playButton.style.cursor = 'pointer';
playButton.style.border = '3px solid white';
playButton.style.background = 'rgba(0,0,0,0.6)';
playButton.style.color = 'white';
playButton.style.borderRadius = '50%';
playButton.style.textShadow = '0 0 5px black';
playButton.style.transition = 'background 0.2s';
playButton.onmouseover = () => {
playButton.style.background = 'rgba(0,0,0,0.8)';
};
playButton.onmouseout = () => {
playButton.style.background = 'rgba(0,0,0,0.6)';
};
return container;
}
Apply Changes