You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, starCount = 400, spaceColor = '#050510', lightAngle = 45, atmosphereColor = '#4a90e2') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Parse and validate parameters
starCount = parseInt(starCount);
if (isNaN(starCount) || starCount < 0) starCount = 400;
lightAngle = parseFloat(lightAngle);
if (isNaN(lightAngle)) lightAngle = 45;
// Define dimensions. Cap the maximum radius to prevent memory / performance issues on very large images.
let R = Math.max(originalImg.width, originalImg.height) / 2;
if (R > 800) R = 800; // Limit radius to 800px (Canvas max 2400x2400)
const R_int = Math.floor(R);
// Canvas size provides ample "space" around the ball
canvas.width = R_int * 3;
canvas.height = R_int * 3;
const cx = canvas.width / 2;
const cy = canvas.height / 2;
// --- 1. Draw Deep Space Background ---
ctx.fillStyle = spaceColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Subtle background nebula/cloud
const nebulaGrad = ctx.createRadialGradient(cx, cy, R_int * 1.5, cx, cy, canvas.width * 0.8);
nebulaGrad.addColorStop(0, 'rgba(0, 0, 0, 0)');
nebulaGrad.addColorStop(1, 'rgba(25, 10, 45, 0.4)');
ctx.fillStyle = nebulaGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Light source (Distant Sun/Star) in the background
const radAngle = lightAngle * Math.PI / 180;
const sunX = cx - Math.cos(radAngle) * canvas.width * 0.4;
const sunY = cy - Math.sin(radAngle) * canvas.width * 0.4;
ctx.globalCompositeOperation = 'screen';
const sunGrad = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, canvas.width * 0.3);
sunGrad.addColorStop(0, 'rgba(255, 255, 255, 1)');
sunGrad.addColorStop(0.05, 'rgba(200, 220, 255, 0.6)');
sunGrad.addColorStop(0.3, 'rgba(100, 150, 255, 0.1)');
sunGrad.addColorStop(1, 'rgba(100, 150, 255, 0)');
ctx.fillStyle = sunGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-over';
// Generate Stars
for (let i = 0; i < starCount; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const r = Math.random() * 1.2 + 0.3;
let opacity = Math.random() * 0.7 + 0.1;
// Occasional extra bright star
if (Math.random() < 0.05) opacity = 1;
ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
// --- 2. Spherize Target Image (Pixel Manipulation) ---
const tempCanvas = document.createElement('canvas');
const tCtx = tempCanvas.getContext('2d');
const size2R = R_int * 2;
tempCanvas.width = size2R;
tempCanvas.height = size2R;
// Draw standard image over the square target frame
tCtx.drawImage(originalImg, 0, 0, size2R, size2R);
// Spherize via mapping Cartesian to spherical coordinates
const srcData = tCtx.getImageData(0, 0, size2R, size2R);
const destData = tCtx.createImageData(size2R, size2R);
for (let y = 0; y < size2R; y++) {
for (let x = 0; x < size2R; x++) {
const dy = y - R_int;
const dx = x - R_int;
const distance = Math.sqrt(dx * dx + dy * dy);
const destIdx = (y * size2R + x) * 4;
if (distance <= R_int) {
let sx, sy;
if (distance === 0) {
sx = R_int;
sy = R_int;
} else {
const d_norm = distance / R_int;
// Fisheye mapping gives the illusion of a protruding surface
const theta = Math.asin(Math.max(-1, Math.min(1, d_norm)));
const d_mapped = (2 * theta / Math.PI);
sx = R_int + (dx / distance) * d_mapped * R_int;
sy = R_int + (dy / distance) * d_mapped * R_int;
}
sx = Math.floor(Math.max(0, Math.min(size2R - 1, sx)));
sy = Math.floor(Math.max(0, Math.min(size2R - 1, sy)));
const srcIdx = (sy * size2R + sx) * 4;
destData.data[destIdx] = srcData.data[srcIdx]; // R
destData.data[destIdx + 1] = srcData.data[srcIdx + 1]; // G
destData.data[destIdx + 2] = srcData.data[srcIdx + 2]; // B
// Antialiasing for smooth boundary
if (distance > R_int - 1) {
destData.data[destIdx + 3] = Math.floor(255 * (R_int - distance));
} else {
destData.data[destIdx + 3] = 255;
}
} else {
destData.data[destIdx + 3] = 0; // Transparent outside the ball
}
}
}
tCtx.putImageData(destData, 0, 0);
// Place the spherized image into the main space canvas
ctx.drawImage(tempCanvas, cx - R_int, cy - R_int);
// --- 3. Apply 3D Shading & Lighting Effects ---
ctx.save();
// Create a precise clipping mask shaped like the ball so shading doesn't overflow
ctx.beginPath();
ctx.arc(cx, cy, R_int - 0.5, 0, Math.PI * 2);
ctx.clip();
// Shadow Layer (Ambient Occlusion & Form Shadow)
const hx = cx - Math.cos(radAngle) * R_int * 0.3;
const hy = cy - Math.sin(radAngle) * R_int * 0.3;
const shadowGrad = ctx.createRadialGradient(hx, hy, R_int * 0.1, cx, cy, R_int);
shadowGrad.addColorStop(0, 'rgba(0, 0, 0, 0)');
shadowGrad.addColorStop(0.5, 'rgba(0, 0, 0, 0.4)');
shadowGrad.addColorStop(0.95, 'rgba(0, 0, 0, 0.85)');
shadowGrad.addColorStop(1, 'rgba(0, 0, 0, 1)');
ctx.fillStyle = shadowGrad;
ctx.fillRect(cx - R_int, cy - R_int, R_int * 2, R_int * 2);
// Specular Highlight Layer
const specX = cx - Math.cos(radAngle) * R_int * 0.7;
const specY = cy - Math.sin(radAngle) * R_int * 0.7;
ctx.globalCompositeOperation = 'screen';
const specGrad = ctx.createRadialGradient(specX, specY, 0, specX, specY, R_int * 0.55);
specGrad.addColorStop(0, 'rgba(255, 255, 255, 0.7)');
specGrad.addColorStop(0.3, 'rgba(255, 255, 255, 0.2)');
specGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = specGrad;
ctx.fillRect(cx - R_int, cy - R_int, R_int * 2, R_int * 2);
// Inner Atmosphere Layer (Rim Edge Lighting inside the ball boundary)
let rimR = 74, rimG = 144, rimB = 226; // Default to #4a90e2
if (typeof atmosphereColor === 'string' && atmosphereColor.startsWith('#')) {
let hex = atmosphereColor.replace('#', '');
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
if (hex.length === 6) {
rimR = parseInt(hex.substring(0, 2), 16);
rimG = parseInt(hex.substring(2, 4), 16);
rimB = parseInt(hex.substring(4, 6), 16);
}
}
const innerRimGrad = ctx.createRadialGradient(cx, cy, R_int * 0.75, cx, cy, R_int);
innerRimGrad.addColorStop(0, 'rgba(0,0,0,0)');
innerRimGrad.addColorStop(1, `rgba(${rimR}, ${rimG}, ${rimB}, 0.5)`);
ctx.fillStyle = innerRimGrad;
ctx.fillRect(cx - R_int, cy - R_int, R_int * 2, R_int * 2);
ctx.restore(); // Release clipping mask
// --- 4. Outer Atmospheric Blur ---
// Make sure we only draw on the outside of the ball to leave the edge crisp
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height);
// Draw hole counter-clockwise to subtract from the rect path
ctx.arc(cx, cy, R_int - 1, 0, Math.PI * 2, true);
ctx.clip();
ctx.globalCompositeOperation = 'screen';
const outerGlowGrad = ctx.createRadialGradient(cx, cy, R_int, cx, cy, R_int * 1.15);
outerGlowGrad.addColorStop(0, `rgba(${rimR}, ${rimG}, ${rimB}, 0.7)`);
outerGlowGrad.addColorStop(1, `rgba(${rimR}, ${rimG}, ${rimB}, 0)`);
ctx.fillStyle = outerGlowGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
return canvas;
}
Apply Changes