You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, pinkAmount = 0.3, cyanAmount = 0.3, chromaticOffset = 3, glitchProbability = 0.05, glitchOffset = 15, glitchBlockSize = 8, scanlineOpacity = 0.1, scanlineHeight = 1) {
// Ensure numerical parameters and provide safe fallbacks for critical ones
pinkAmount = Number(pinkAmount);
cyanAmount = Number(cyanAmount);
chromaticOffset = Number(chromaticOffset);
glitchProbability = Number(glitchProbability);
glitchOffset = Number(glitchOffset);
glitchBlockSize = Math.max(1, Number(glitchBlockSize)); // Must be at least 1 to prevent infinite loops
scanlineOpacity = Number(scanlineOpacity);
scanlineHeight = Math.max(1, Number(scanlineHeight)); // Must be at least 1
const canvas = document.createElement('canvas');
// Opt-in for performance hint for frequent getImageData/putImageData calls
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Basic check for image validity
if (!originalImg || typeof originalImg.width === 'undefined' || originalImg.width === 0 || originalImg.height === 0) {
console.error("Vaporwave Filter: Invalid image input or image not loaded.");
// Return a canvas with an error message
canvas.width = 200;
canvas.height = 50;
ctx.fillStyle = "black";
ctx.fillRect(0,0,200,50);
ctx.fillStyle = "red";
ctx.font = "12px Arial";
ctx.fillText("Error: Invalid image input.", 10, 30);
return canvas;
}
const width = originalImg.width;
const height = originalImg.height;
canvas.width = width;
canvas.height = height;
// Draw original image to canvas as the starting point
ctx.drawImage(originalImg, 0, 0, width, height);
// --- Step 1: Chromatic Aberration ---
if (chromaticOffset > 0) {
const originalImageData = ctx.getImageData(0, 0, width, height);
// Create new ImageData object. Using ctx.createImageData is preferred.
const aberratedImageData = ctx.createImageData(width, height);
const origData = originalImageData.data;
const newData = aberratedImageData.data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// Calculate source pixel x-coordinates for R and B channels with clamping
const rX = Math.max(0, Math.min(width - 1, x - chromaticOffset));
const bX = Math.max(0, Math.min(width - 1, x + chromaticOffset));
// Calculate source indices in the flat pixel array
const rIdx = (y * width + rX) * 4;
const gIdx = i; // Green channel from current pixel (no shift)
const bIdx = (y * width + bX) * 4;
newData[i] = origData[rIdx]; // Red channel
newData[i + 1] = origData[gIdx + 1]; // Green channel
newData[i + 2] = origData[bIdx + 2]; // Blue channel
newData[i + 3] = origData[gIdx + 3]; // Alpha channel
}
}
ctx.putImageData(aberratedImageData, 0, 0);
}
// --- Step 2: Color Tinting (Pinks/Cyans) ---
if (pinkAmount > 0 || cyanAmount > 0) {
const currentImageData = ctx.getImageData(0, 0, width, height);
const data = currentImageData.data;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Additive tinting, scaled by amounts. These constants (40, 20) can be tuned.
let newR = r + 40 * pinkAmount;
let newG = g + 40 * cyanAmount;
let newB = b + 20 * (pinkAmount + cyanAmount); // Blue is part of both magenta (pink) and cyan
// Slightly reduce the influence of the "opposite" tint to make pinks more pinkish and cyans more cyanish.
// Normalize amounts to be between 0 and 1 for this factor to avoid overly strong reduction.
const normCyan = Math.min(1, Math.max(0, cyanAmount));
const normPink = Math.min(1, Math.max(0, pinkAmount));
newR = newR * (1 - 0.3 * normCyan);
newG = newG * (1 - 0.3 * normPink);
data[i] = Math.min(255, Math.max(0, newR));
data[i + 1] = Math.min(255, Math.max(0, newG));
data[i + 2] = Math.min(255, Math.max(0, newB));
}
ctx.putImageData(currentImageData, 0, 0);
}
// --- Step 3: Glitch Effect (Horizontal Block Shift) ---
if (glitchProbability > 0 && glitchOffset > 0) {
const imageDataToGlitch = ctx.getImageData(0, 0, width, height);
const dataToGlitch = imageDataToGlitch.data;
for (let blockY = 0; blockY < height; blockY += glitchBlockSize) {
if (Math.random() < glitchProbability) {
// Random shift amount (-glitchOffset to +glitchOffset)
const shift = Math.floor((Math.random() * 2 - 1) * glitchOffset);
if (shift === 0) continue; // No shift, skip this block
// Actual height of the current block (can be less than glitchBlockSize at the image bottom)
const currentBlockActualHeight = Math.min(glitchBlockSize, height - blockY);
for (let lineInBlock = 0; lineInBlock < currentBlockActualHeight; lineInBlock++) {
const currentLineY = blockY + lineInBlock;
// Buffer for the original row data of the current line before shifting
const rowPixelData = new Uint8ClampedArray(width * 4);
const rowStartIndexInFullData = currentLineY * width * 4;
// Efficiently copy the row data
rowPixelData.set(dataToGlitch.subarray(rowStartIndexInFullData, rowStartIndexInFullData + width * 4));
// Write the shifted row back into the main data array (dataToGlitch)
for (let x = 0; x < width; x++) {
let sourceX = x - shift;
// Modulo arithmetic for wrap-around behavior
sourceX = (sourceX % width + width) % width;
const targetIdxInFullData = (currentLineY * width + x) * 4;
const sourcePixelInRowArrayIdx = sourceX * 4;
dataToGlitch[targetIdxInFullData] = rowPixelData[sourcePixelInRowArrayIdx];
dataToGlitch[targetIdxInFullData + 1] = rowPixelData[sourcePixelInRowArrayIdx + 1];
dataToGlitch[targetIdxInFullData + 2] = rowPixelData[sourcePixelInRowArrayIdx + 2];
dataToGlitch[targetIdxInFullData + 3] = rowPixelData[sourcePixelInRowArrayIdx + 3];
}
}
}
}
ctx.putImageData(imageDataToGlitch, 0, 0);
}
// --- Step 4: Scanlines ---
if (scanlineOpacity > 0) {
// Using a dark, slightly purple color for scanlines, fitting the vaporwave theme
ctx.fillStyle = `rgba(30, 0, 50, ${scanlineOpacity})`;
// Draw lines with gaps of the same height
const scanlineStep = scanlineHeight * 2;
for (let slY = 0; slY < height; slY += scanlineStep) {
ctx.fillRect(0, slY, width, scanlineHeight);
}
}
return canvas;
}
Apply Changes