You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, galaxyCount = 10, minGalaxySize = 50, maxGalaxySize = 150, fieldStarCount = 500) {
// Ensure parameters are numbers
const numGalaxyCount = Number(galaxyCount);
const numMinGalaxySize = Number(minGalaxySize);
const numMaxGalaxySize = Number(maxGalaxySize);
const numFieldStarCount = Number(fieldStarCount);
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// --- Helper function to draw a single galaxy ---
const drawGalaxy = (x, y, size) => {
const coreRadius = size * (0.05 + Math.random() * 0.05);
const starCount = Math.floor(size * 1.5);
const armCount = 2 + Math.floor(Math.random() * 3); // 2 to 4 arms
const spiralTightness = 5 + Math.random() * 5;
// 1. Draw core glow
const coreGradient = ctx.createRadialGradient(x, y, 0, x, y, coreRadius * 2);
coreGradient.addColorStop(0, `rgba(255, 255, 230, 0.95)`);
coreGradient.addColorStop(0.5, `rgba(220, 220, 255, 0.5)`);
coreGradient.addColorStop(1, `rgba(200, 200, 255, 0)`);
ctx.fillStyle = coreGradient;
ctx.beginPath();
ctx.arc(x, y, coreRadius * 2.5, 0, Math.PI * 2);
ctx.fill();
// 2. Draw stars in spiral arms
for (let i = 0; i < starCount; i++) {
// Use a power distribution to cluster stars near the center
const distance = Math.pow(Math.random(), 2) * (size / 2);
const angle = Math.random() * Math.PI * 2;
// Define spiral arms
const arm = Math.floor(angle * armCount / (Math.PI * 2));
const armAngle = (arm / armCount) * Math.PI * 2;
const spiralOffset = distance / spiralTightness;
const fuzziness = (Math.random() - 0.5) * (Math.PI / armCount) * 0.7;
const finalAngle = armAngle + spiralOffset + fuzziness;
const starX = x + Math.cos(finalAngle) * distance;
const starY = y + Math.sin(finalAngle) * distance;
const starSize = Math.random() * 1.5 + 0.5;
const starOpacity = 0.5 + Math.random() * 0.5;
const r = 220 + Math.random() * 35;
const g = 220 + Math.random() * 35;
const b = 240 + Math.random() * 15;
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${starOpacity})`;
ctx.beginPath();
ctx.arc(starX, starY, starSize, 0, Math.PI * 2);
ctx.fill();
}
};
// --- Step 1: Create a dark space background ---
const bgGradient = ctx.createLinearGradient(0, 0, 0, height);
bgGradient.addColorStop(0, '#000010');
bgGradient.addColorStop(1, '#0c0c24');
ctx.fillStyle = bgGradient;
ctx.fillRect(0, 0, width, height);
// --- Step 2: Draw distant field stars ---
for (let i = 0; i < numFieldStarCount; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const size = Math.random() * 1.5 + 0.2;
const alpha = Math.random() * 0.5 + 0.1;
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
// --- Step 3: Analyze original image to find empty space ---
const MASK_GRID_SCALE = 20;
const maskWidth = Math.ceil(width / MASK_GRID_SCALE);
const maskHeight = Math.ceil(height / MASK_GRID_SCALE);
const occupiedMask = new Array(maskWidth * maskHeight).fill(false);
const maskCanvas = document.createElement('canvas');
maskCanvas.width = width;
maskCanvas.height = height;
const maskCtx = maskCanvas.getContext('2d');
maskCtx.drawImage(originalImg, 0, 0);
const imageData = maskCtx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
if (alpha > 50) { // Threshold for considering a pixel "occupied"
const pixelIndex = i / 4;
const x = pixelIndex % width;
const y = Math.floor(pixelIndex / width);
const maskX = Math.floor(x / MASK_GRID_SCALE);
const maskY = Math.floor(y / MASK_GRID_SCALE);
occupiedMask[maskY * maskWidth + maskX] = true;
}
}
const isOccupied = (x, y, radius) => {
const startX = Math.floor((x - radius) / MASK_GRID_SCALE);
const endX = Math.ceil((x + radius) / MASK_GRID_SCALE);
const startY = Math.floor((y - radius) / MASK_GRID_SCALE);
const endY = Math.ceil((y + radius) / MASK_GRID_SCALE);
for (let my = Math.max(0, startY); my < Math.min(maskHeight, endY); my++) {
for (let mx = Math.max(0, startX); mx < Math.min(maskWidth, endX); mx++) {
if (occupiedMask[my * maskWidth + mx]) {
return true;
}
}
}
return false;
};
// --- Step 4: Place galaxies in non-occupied, non-overlapping spaces ---
const galaxies = [];
for (let i = 0; i < numGalaxyCount; i++) {
let tries = 0;
const maxTries = 200;
while (tries < maxTries) {
tries++;
const size = numMinGalaxySize + Math.random() * (numMaxGalaxySize - numMinGalaxySize);
const radius = size / 2;
const x = Math.random() * width;
const y = Math.random() * height;
if (isOccupied(x, y, radius)) {
continue;
}
let overlapsOtherGalaxy = false;
for (const g of galaxies) {
const dx = x - g.x;
const dy = y - g.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < radius + g.radius) {
overlapsOtherGalaxy = true;
break;
}
}
if (overlapsOtherGalaxy) {
continue;
}
// If we've reached here, it's a valid spot
drawGalaxy(x, y, size);
galaxies.push({ x, y, radius });
break;
}
}
// --- Step 5: Draw the original image on top of the generated background ---
ctx.drawImage(originalImg, 0, 0);
return canvas;
}
Apply Changes