You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, amplitude = 15, waveLength = 80, phase = 0, flowAngle = 0, tintColorStr = "rgba(0, 40, 80, 0.1)") {
// Nested helper function for color parsing
function _parseColorToObject(colorStr) {
if (!colorStr || typeof colorStr !== 'string' || colorStr.toLowerCase() === 'transparent' || colorStr.trim() === "") {
return null; // No tint if string is empty, null, transparent, or not a string
}
// Use a temporary 1x1 canvas to parse the color string
// This is a robust way to support various color formats (hex, rgb, rgba, named colors)
const tempCanvas = document.createElement('canvas');
tempCanvas.width = 1; tempCanvas.height = 1;
// Add willReadFrequently hint for getContext if using getImageData frequently,
// though for a single parse, it's minor.
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
if (!tempCtx) {
console.warn("Could not create temp context for color parsing.");
return null;
}
tempCtx.fillStyle = colorStr;
tempCtx.fillRect(0, 0, 1, 1);
try {
const imageData = tempCtx.getImageData(0, 0, 1, 1);
if (!imageData) { // Should not happen with fillRect
return null;
}
const [r, g, b, a_byte] = imageData.data;
return { r, g, b, a: a_byte / 255 };
} catch(e) {
console.warn(`Could not parse color string: "${colorStr}"`, e);
return null;
}
}
if (!(originalImg instanceof HTMLImageElement)) {
console.error("Parameter 'originalImg' is not an HTMLImageElement.");
const errCanvas = document.createElement('canvas'); errCanvas.width = 1; errCanvas.height = 1; return errCanvas;
}
try {
if (typeof originalImg.decode === 'function') {
await originalImg.decode();
} else {
if (!originalImg.complete || originalImg.naturalWidth === 0) {
if (!originalImg.src && !originalImg.currentSrc) { // currentSrc for <img> in DOM
throw new Error("Image has no src attribute to load from.");
}
await new Promise((resolve, reject) => {
originalImg.onload = resolve;
originalImg.onerror = (err) => reject(new Error(`Image failed to load from src: ${originalImg.src || originalImg.currentSrc}. Error: ${err}`));
});
}
}
} catch (error) {
console.error("Image could not be loaded or decoded:", error.message || error);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 150; errorCanvas.height = 30; // Small canvas for error message
const ctx = errorCanvas.getContext('2d');
if (ctx) {
ctx.font = '12px Arial';
ctx.fillStyle = 'red';
ctx.fillText('Error loading image.', 5, 20);
}
return errorCanvas;
}
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
if (width === 0 || height === 0) {
console.error("Image has zero dimensions after loading attempt.");
const errorCanvas = document.createElement('canvas'); errorCanvas.width = 1; errorCanvas.height = 1; return errorCanvas;
}
// Output canvas, where the final image will be drawn
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Source canvas to get ImageData from the original image
// This is necessary because we need raw pixel data for manipulation.
const srcCanvas = document.createElement('canvas');
srcCanvas.width = width;
srcCanvas.height = height;
const srcCtx = srcCanvas.getContext('2d', { willReadFrequently: true });
srcCtx.drawImage(originalImg, 0, 0, width, height);
let srcImageData;
try {
srcImageData = srcCtx.getImageData(0, 0, width, height);
} catch (e) {
console.error("Could not get image data from source canvas (cross-origin issue?):", e);
// Fallback: draw the original image onto the output canvas
ctx.drawImage(originalImg, 0, 0, width, height);
// Attempt to apply tint over the original image if pixel manipulation fails
const tint = _parseColorToObject(tintColorStr);
if (tint && tint.a > 0) {
ctx.fillStyle = `rgba(${tint.r},${tint.g},${tint.b},${tint.a})`;
ctx.fillRect(0,0,width,height);
}
return canvas;
}
const srcData = srcImageData.data;
// Create blank ImageData for the destination (output canvas)
const dstImageData = ctx.createImageData(width, height);
const dstData = dstImageData.data;
// Pre-calculate effect parameters
const angleRad = flowAngle * Math.PI / 180;
const cosA = Math.cos(angleRad);
const sinA = Math.sin(angleRad);
let currentWaveLength = waveLength;
if (currentWaveLength <= 0) {
// A non-positive wavelength is problematic.
// Make frequency effectively zero to disable wave, resulting in uniform displacement (if amplitude > 0) or no change.
console.warn("waveLength parameter must be positive. Wave effect will be minimal or uniform.");
currentWaveLength = Math.max(width, height) * 1e6; // Effectively infinite wavelength
}
const freq = (2 * Math.PI) / currentWaveLength;
// Apply the "ocean current" distortion pixel by pixel
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Calculate `yp`, a coordinate perpendicular to the flow direction.
// The wave pattern is based on this `yp` coordinate.
const yp = -x * sinA + y * cosA;
// Calculate displacement magnitude using a sinusoidal wave.
// `amplitude` controls the strength, `freq` controls wave density, `phase` shifts the wave.
const displacement = amplitude * Math.sin(yp * freq + phase);
// Determine the source pixel coordinates by shifting the current pixel (x,y)
// *along* the flow direction (`cosA`, `sinA`) by the `displacement` amount.
const srcX_float = x + displacement * cosA;
const srcY_float = y + displacement * sinA;
// Use nearest neighbor sampling (rounding) to find the source pixel.
const srcX = Math.round(srcX_float);
const srcY = Math.round(srcY_float);
// Clamp source coordinates to ensure they are within the image bounds.
const clampedSrcX = Math.max(0, Math.min(width - 1, srcX));
const clampedSrcY = Math.max(0, Math.min(height - 1, srcY));
// Calculate array indices for source and destination pixel data.
const srcIndex = (clampedSrcY * width + clampedSrcX) * 4; // 4 bytes per pixel (R,G,B,A)
const dstIndex = (y * width + x) * 4;
// Copy pixel data (R,G,B,A) from source to destination.
dstData[dstIndex] = srcData[srcIndex];
dstData[dstIndex + 1] = srcData[srcIndex + 1];
dstData[dstIndex + 2] = srcData[srcIndex + 2];
dstData[dstIndex + 3] = srcData[srcIndex + 3]; // Preserve original alpha
}
}
// Apply tint if specified
const tint = _parseColorToObject(tintColorStr);
if (tint && tint.a > 0) { // tint.a is opacity from 0 (transparent) to 1 (opaque)
const tintR = tint.r; // Tint color components (0-255)
const tintG = tint.g;
const tintB = tint.b;
const tintA = tint.a; // Alpha of the tint layer itself
for (let i = 0; i < dstData.length; i += 4) {
// Alpha blending: FinalColor = PixelColor * (1 - TintAlpha) + TintColor * TintAlpha
// This blends the tint color over the (already distorted) pixel color.
dstData[i] = Math.round(dstData[i] * (1 - tintA) + tintR * tintA);
dstData[i + 1] = Math.round(dstData[i + 1] * (1 - tintA) + tintG * tintA);
dstData[i + 2] = Math.round(dstData[i + 2] * (1 - tintA) + tintB * tintA);
// The pixel's original alpha (dstData[i+3]) is preserved from the distorted image.
// If the tint itself should affect the overall alpha, a different blending model for alpha would be needed.
}
}
// Draw the processed (distorted and tinted) image data onto the output canvas
ctx.putImageData(dstImageData, 0, 0);
return canvas;
}
Apply Changes