You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
centerXParam = null,
centerYParam = null,
eventHorizonRadiusParam = null,
strength = 0.1,
glowColorString = "rgba(255,200,0,0.7)",
glowWidthParam = null
) {
// Helper function to parse color strings (rgba, rgb, hex)
// Returns [r, g, b, a] where r,g,b are 0-255 and a is 0-1.
function _parseColor(colorStrIn) {
let r = 0, g = 0, b = 0, a = 1.0; // Default to opaque black
const colorStr = String(colorStrIn).trim();
if (colorStr.startsWith("rgba(")) {
try {
const parts = colorStr.substring(5, colorStr.length - 1).split(',');
r = parseInt(parts[0].trim(), 10);
g = parseInt(parts[1].trim(), 10);
b = parseInt(parts[2].trim(), 10);
a = parseFloat(parts[3].trim());
} catch (e) { console.warn("Image Black Hole: Could not parse rgba color:", colorStr, e); return [0,0,0,0]; }
} else if (colorStr.startsWith("rgb(")) {
try {
const parts = colorStr.substring(4, colorStr.length - 1).split(',');
r = parseInt(parts[0].trim(), 10);
g = parseInt(parts[1].trim(), 10);
b = parseInt(parts[2].trim(), 10);
a = 1.0;
} catch (e) { console.warn("Image Black Hole: Could not parse rgb color:", colorStr, e); return [0,0,0,0]; }
} else if (colorStr.startsWith("#")) {
let hex = colorStr.substring(1);
try {
if (hex.length === 3) { // #RGB
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
} else if (hex.length === 4) { // #RGBA
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
}
// Now hex should be 6 or 8 characters long
if (hex.length === 8) { // #RRGGBBAA
a = parseInt(hex.substring(6, 8), 16) / 255.0;
hex = hex.substring(0, 6); // For RGB parsing
} else if (hex.length === 6) { // #RRGGBB
a = 1.0;
} else {
console.warn("Image Black Hole: Invalid hex color string length (input: "+colorStrIn+"): resolved hex '"+hex+"' not 6 or 8 chars");
return [0,0,0,0];
}
r = parseInt(hex.substring(0, 2), 16);
g = parseInt(hex.substring(2, 4), 16);
b = parseInt(hex.substring(4, 6), 16);
} catch (e) { console.warn("Image Black Hole: Could not parse hex color:", colorStr, e); return [0,0,0,0]; }
} else {
console.warn("Image Black Hole: Unknown color string format:", colorStr, ". Defaulting to transparent black.");
return [0,0,0,0];
}
r = Math.max(0, Math.min(255, isNaN(r) ? 0 : r));
g = Math.max(0, Math.min(255, isNaN(g) ? 0 : g));
b = Math.max(0, Math.min(255, isNaN(b) ? 0 : b));
a = Math.max(0, Math.min(1.0, isNaN(a) ? 0 : a));
return [r, g, b, a];
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const W = originalImg.naturalWidth || originalImg.width;
const H = originalImg.naturalHeight || originalImg.height;
canvas.width = W;
canvas.height = H;
let centerX, centerY, eventHorizonRadius, glowWidth;
if (typeof centerXParam === 'string' && centerXParam.endsWith('%')) {
centerX = W * (parseFloat(centerXParam) / 100);
} else if (typeof centerXParam === 'number') {
centerX = centerXParam;
} else {
centerX = W / 2;
}
if (typeof centerYParam === 'string' && centerYParam.endsWith('%')) {
centerY = H * (parseFloat(centerYParam) / 100);
} else if (typeof centerYParam === 'number') {
centerY = centerYParam;
} else {
centerY = H / 2;
}
if (typeof eventHorizonRadiusParam === 'string' && eventHorizonRadiusParam.endsWith('%')) {
eventHorizonRadius = Math.min(W, H) * (parseFloat(eventHorizonRadiusParam) / 100);
} else if (typeof eventHorizonRadiusParam === 'number') {
eventHorizonRadius = eventHorizonRadiusParam;
} else {
eventHorizonRadius = Math.min(W, H) / 8;
}
if (eventHorizonRadius <= 0) eventHorizonRadius = 1;
if (typeof glowWidthParam === 'string' && glowWidthParam.endsWith('%')) {
glowWidth = eventHorizonRadius * (parseFloat(glowWidthParam) / 100);
} else if (typeof glowWidthParam === 'number') {
glowWidth = glowWidthParam;
} else {
glowWidth = eventHorizonRadius * 0.3;
}
if (glowWidth < 0) glowWidth = 0;
ctx.drawImage(originalImg, 0, 0, W, H);
const originalImageData = ctx.getImageData(0, 0, W, H);
const outputImageData = ctx.createImageData(W, H);
const [glowR, glowG, glowB, glowA_color] = _parseColor(glowColorString);
function _getPixelClamped(imgData, x, y) {
const ix = Math.max(0, Math.min(imgData.width - 1, Math.floor(x)));
const iy = Math.max(0, Math.min(imgData.height - 1, Math.floor(y)));
const i = (iy * imgData.width + ix) * 4;
return [imgData.data[i], imgData.data[i+1], imgData.data[i+2], imgData.data[i+3]];
}
function _getBilinearPixel(imgData, x, y) {
const x0 = Math.floor(x);
const y0 = Math.floor(y);
// Speed up if way out of bounds for the first corner (x0,y0)
if (x0 < -1 || x0 > imgData.width || y0 < -1 || y0 > imgData.height) {
return [0, 0, 0, 0];
}
const p00 = _getPixelClamped(imgData, x0, y0);
const p10 = _getPixelClamped(imgData, x0 + 1, y0);
const p01 = _getPixelClamped(imgData, x0, y0 + 1);
const p11 = _getPixelClamped(imgData, x0 + 1, y0 + 1);
const tx = x - x0;
const ty = y - y0;
const res = [0,0,0,0];
for (let i=0; i<4; ++i) {
res[i] = Math.round(
p00[i]*(1-tx)*(1-ty) +
p10[i]*tx*(1-ty) +
p01[i]*(1-tx)*ty +
p11[i]*tx*ty
);
}
return res;
}
for (let y_coord = 0; y_coord < H; y_coord++) {
for (let x_coord = 0; x_coord < W; x_coord++) {
const dx = x_coord - centerX;
const dy = y_coord - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
let r_out=0, g_out=0, b_out=0, a_out=255;
if (distance <= eventHorizonRadius) {
// Pixel is inside the event horizon, remains black (or values set above)
} else {
// Pixel is outside the event horizon
const r_eff = distance;
const source_r = r_eff + strength * eventHorizonRadius * (eventHorizonRadius / r_eff);
const angle = Math.atan2(dy, dx);
const source_x = centerX + source_r * Math.cos(angle);
const source_y = centerY + source_r * Math.sin(angle);
const distortedPixelColor = _getBilinearPixel(originalImageData, source_x, source_y);
let current_r = distortedPixelColor[0];
let current_g = distortedPixelColor[1];
let current_b = distortedPixelColor[2];
let current_a = distortedPixelColor[3]; // 0-255
// Apply glow if applicable
if (glowWidth > 0 && distance <= eventHorizonRadius + glowWidth) {
let glowIntensityFactor = 1.0 - (distance - eventHorizonRadius) / glowWidth;
glowIntensityFactor = Math.max(0, Math.min(1, glowIntensityFactor));
// glowIntensityFactor = glowIntensityFactor * glowIntensityFactor; // Optional: quadratic falloff for glow
const alphaGlowEffective = glowIntensityFactor * glowA_color; // Overall opacity of glow layer at this pixel
// Blend RGB: C_out = C_glow * A_glow_eff + C_current * (1 - A_glow_eff)
r_out = Math.round(glowR * alphaGlowEffective + current_r * (1 - alphaGlowEffective));
g_out = Math.round(glowG * alphaGlowEffective + current_g * (1 - alphaGlowEffective));
b_out = Math.round(glowB * alphaGlowEffective + current_b * (1 - alphaGlowEffective));
// Blend Alpha: A_out = A_glow_eff + A_current_norm * (1 - A_glow_eff)
const currentAlphaNormalized = current_a / 255.0;
const finalAlphaNormalized = alphaGlowEffective + currentAlphaNormalized * (1 - alphaGlowEffective);
a_out = Math.round(finalAlphaNormalized * 255);
} else {
// No glow, use distorted pixel color directly
r_out = current_r;
g_out = current_g;
b_out = current_b;
a_out = current_a;
}
}
const i = (y_coord * W + x_coord) * 4;
outputImageData.data[i] = r_out;
outputImageData.data[i+1] = g_out;
outputImageData.data[i+2] = b_out;
outputImageData.data[i+3] = a_out;
}
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes