You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, radiusPercent = 85, bgColor = 'transparent', enableLighting = 1, chromaticAberration = 1) {
// Coerce parameters to expected types
radiusPercent = parseFloat(radiusPercent) || 85;
enableLighting = Number(enableLighting);
chromaticAberration = Number(chromaticAberration);
const width = originalImg.width;
const height = originalImg.height;
// Output canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Draw original image onto an offscreen canvas to extract pixel data
const srcCanvas = document.createElement('canvas');
srcCanvas.width = width;
srcCanvas.height = height;
const srcCtx = srcCanvas.getContext('2d');
srcCtx.drawImage(originalImg, 0, 0);
const srcData = srcCtx.getImageData(0, 0, width, height).data;
const outImgData = ctx.createImageData(width, height);
const outData = outImgData.data;
// Calculate sphere parameters based on canvas dimensions
const R = (Math.min(width, height) / 2) * (radiusPercent / 100);
const cx = width / 2;
const cy = height / 2;
// Parse background color mapping (handles hex, short hex, and transparent keyword)
function parseColor(colorStr) {
if (!colorStr || colorStr.toLowerCase() === 'transparent') return [0, 0, 0, 0];
let c = colorStr.replace(/^#/, '');
if (c.length === 3) c = c.split('').map(x => x + x).join('');
if (c.length === 6) c += 'FF';
let r = parseInt(c.substring(0, 2), 16) || 0;
let g = parseInt(c.substring(2, 4), 16) || 0;
let b = parseInt(c.substring(4, 6), 16) || 0;
let a = parseInt(c.substring(6, 8), 16) || 255;
return [r, g, b, a];
}
const bgRGBA = parseColor(bgColor);
// Setup 3D Lighting source (coming from top-left)
const lx = -0.577;
const ly = -0.577;
const lz = 0.577; // Normalize(-1, -1, 1)
// Calculate Halfway vector for Blinn-Phong Specular Reflection
// View vector is considered directly from front (0, 0, 1)
const Hx_u = lx;
const Hy_u = ly;
const Hz_u = lz + 1;
const H_len = Math.sqrt(Hx_u * Hx_u + Hy_u * Hy_u + Hz_u * Hz_u);
const Hx = Hx_u / H_len;
const Hy = Hy_u / H_len;
const Hz = Hz_u / H_len;
const shininess = 40; // Specular gloss factor
// Bilinear Interpolation helper for smooth coordinate sampling
function getPixel(x, y) {
const x1 = Math.max(0, Math.min(width - 1, Math.floor(x)));
const y1 = Math.max(0, Math.min(height - 1, Math.floor(y)));
const x2 = Math.max(0, Math.min(width - 1, x1 + 1));
const y2 = Math.max(0, Math.min(height - 1, y1 + 1));
const dx = x - x1;
const dy = y - y1;
const i1 = (y1 * width + x1) * 4;
const i2 = (y1 * width + x2) * 4;
const i3 = (y2 * width + x1) * 4;
const i4 = (y2 * width + x2) * 4;
const w1 = (1 - dx) * (1 - dy);
const w2 = dx * (1 - dy);
const w3 = (1 - dx) * dy;
const w4 = dx * dy;
return [
srcData[i1] * w1 + srcData[i2] * w2 + srcData[i3] * w3 + srcData[i4] * w4,
srcData[i1 + 1] * w1 + srcData[i2 + 1] * w2 + srcData[i3 + 1] * w3 + srcData[i4 + 1] * w4,
srcData[i1 + 2] * w1 + srcData[i2 + 2] * w2 + srcData[i3 + 2] * w3 + srcData[i4 + 2] * w4,
srcData[i1 + 3] * w1 + srcData[i2 + 3] * w2 + srcData[i3 + 3] * w3 + srcData[i4 + 3] * w4
];
}
// Process every pixel mapped to the new dimensions
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = x - cx;
const dy = y - cy;
const r2 = dx * dx + dy * dy;
const outIdx = (y * width + x) * 4;
// Check if current mapped pixel is strictly inside the sphere projection area
if (r2 <= R * R) {
const dist = Math.sqrt(r2);
// Normal coordinates mapped from -1 to 1 space
const nx = dx / R;
const ny = dy / R;
const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny)); // Z dimension of hemisphere
// Spherical longitude and latitude mapping onto a flat 2D projection
const phi = Math.asin(ny);
const theta = Math.atan2(nx, nz);
// Convert phi/theta limits to standard UV ranges (0.0 to 1.0)
const u = (theta / (Math.PI / 2)) * 0.5 + 0.5;
const v = (phi / (Math.PI / 2)) * 0.5 + 0.5;
// Absolute image lookup scales
const sx = u * width;
const sy = v * height;
let pr, pg, pb, pa;
// Add a cool "Hype" radial chromatic aberration effect near visual boundaries
if (chromaticAberration) {
const caAmt = (dist / R) * 5.5; // Offset amplifies near outer edges
const dirX = sx - (width / 2);
const dirY = sy - (height / 2);
const dirLen = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
const nxDir = dirX / dirLen;
const nyDir = dirY / dirLen;
const rColor = getPixel(sx - nxDir * caAmt, sy - nyDir * caAmt);
const gColor = getPixel(sx, sy);
const bColor = getPixel(sx + nxDir * caAmt, sy + nyDir * caAmt);
pr = rColor[0];
pg = gColor[1];
pb = bColor[2];
pa = gColor[3];
} else {
const color = getPixel(sx, sy);
pr = color[0]; pg = color[1]; pb = color[2]; pa = color[3];
}
// Simulate realistic glossy 3D glass shading
if (enableLighting) {
// Diffuse
const dotNL = Math.max(0, nx * lx + ny * ly + nz * lz);
const ambient = 0.45;
const diffuse = 0.55 * dotNL;
const lighting = ambient + diffuse;
pr *= lighting;
pg *= lighting;
pb *= lighting;
// Specular reflection (glossy point)
const dotNH = Math.max(0, nx * Hx + ny * Hy + nz * Hz);
const specIntensity = Math.pow(dotNH, shininess) * 255 * 0.9;
pr += specIntensity;
pg += specIntensity;
pb += specIntensity;
}
pr = Math.min(255, pr);
pg = Math.min(255, pg);
pb = Math.min(255, pb);
// Strict smooth 1-pixel anti-aliasing gradient for ball boundary clipping
const alphaMult = Math.max(0, Math.min(1, R - dist));
// Perfect true-alpha image compositing against the destination background
const effAlpha = (pa / 255) * alphaMult;
const bgAlpha = bgRGBA[3] / 255;
const outAlpha = effAlpha + bgAlpha * (1 - effAlpha);
if (outAlpha === 0) {
outData[outIdx] = 0;
outData[outIdx + 1] = 0;
outData[outIdx + 2] = 0;
outData[outIdx + 3] = 0;
} else {
outData[outIdx] = (pr * effAlpha + bgRGBA[0] * bgAlpha * (1 - effAlpha)) / outAlpha;
outData[outIdx + 1] = (pg * effAlpha + bgRGBA[1] * bgAlpha * (1 - effAlpha)) / outAlpha;
outData[outIdx + 2] = (pb * effAlpha + bgRGBA[2] * bgAlpha * (1 - effAlpha)) / outAlpha;
outData[outIdx + 3] = outAlpha * 255;
}
} else {
// Outside boundary layout resolves purely to original background
outData[outIdx] = bgRGBA[0];
outData[outIdx + 1] = bgRGBA[1];
outData[outIdx + 2] = bgRGBA[2];
outData[outIdx + 3] = bgRGBA[3];
}
}
}
ctx.putImageData(outImgData, 0, 0);
return canvas;
}
Apply Changes