You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
starDensity = 0.002, // Proportion of dark pixels that become stars
starBrightnessMin = 200, // Minimum brightness of a star (0-255)
starBrightnessMax = 255, // Maximum brightness of a star (0-255)
nightEffectAmount = 0.6, // Strength of the night darkening and blue tint (0-1)
darknessThresholdForStars = 70, // Luminance threshold (0-255) for original pixels to be considered for stars
starColorVariation = 0.15 // Amount of color tint (yellowish/bluish) for stars (0 for white, up to 1 for strong color)
) {
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } can potentially optimize getImageData/putImageData calls.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
canvas.width = imgWidth;
canvas.height = imgHeight;
if (imgWidth === 0 || imgHeight === 0) {
// If the image has no dimensions, return an empty canvas.
return canvas;
}
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
const imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
const data = imageData.data;
// Store original luminance for star placement decisions, to avoid night tint affecting it.
const originalLuminanceMap = new Float32Array(imgWidth * imgHeight);
// Pass 1: Apply night effect and calculate original luminance
for (let y = 0; y < imgHeight; y++) {
for (let x = 0; x < imgWidth; x++) {
const R_INDEX = (y * imgWidth + x) * 4;
const G_INDEX = R_INDEX + 1;
const B_INDEX = R_INDEX + 2;
const r = data[R_INDEX];
const g = data[G_INDEX];
const b = data[B_INDEX];
// Calculate original luminance (perceived brightness)
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
originalLuminanceMap[y * imgWidth + x] = lum;
// Apply Night Effect
// 1. Darken based on nightEffectAmount
const darkenFactor = 1.0 - (nightEffectAmount * 0.75); // Max 75% darkening
let newR = r * darkenFactor;
let newG = g * darkenFactor;
let newB = b * darkenFactor;
// 2. Add blue tint, also influenced by nightEffectAmount
const blueBoost = 60 * nightEffectAmount;
const redReduction = 25 * nightEffectAmount;
const greenReduction = 15 * nightEffectAmount;
newR = Math.max(0, newR - redReduction);
newG = Math.max(0, newG - greenReduction);
newB = Math.min(255, newB + blueBoost);
data[R_INDEX] = Math.max(0, Math.min(255, newR));
data[G_INDEX] = Math.max(0, Math.min(255, newG));
data[B_INDEX] = Math.max(0, Math.min(255, newB));
}
}
// Pass 2: Add Stars
const crossGlowIntensity = 0.25; // Intensity of the glow on adjacent pixels for all stars
for (let y = 0; y < imgHeight; y++) {
for (let x = 0; x < imgWidth; x++) {
const originalLum = originalLuminanceMap[y * imgWidth + x];
if (originalLum < darknessThresholdForStars) {
if (Math.random() < starDensity) {
const R_CENTER_INDEX = (y * imgWidth + x) * 4;
const G_CENTER_INDEX = R_CENTER_INDEX + 1;
const B_CENTER_INDEX = R_CENTER_INDEX + 2;
const A_CENTER_INDEX = R_CENTER_INDEX + 3;
// Star brightness (randomized within min/max)
const brightness = starBrightnessMin + Math.random() * (starBrightnessMax - starBrightnessMin);
// Star color initialization (white)
let starR = brightness;
let starG = brightness;
let starB = brightness;
// Apply color variation if specified
if (starColorVariation > 0 && Math.random() < 0.75) { // Apply color to ~75% of stars if variation is on
const colorRoll = Math.random();
// variationMagnitude scales with starColorVariation, from 0.5x to 1.0x of its value
const variationMagnitude = starColorVariation * (0.5 + Math.random() * 0.5);
if (colorRoll < 0.5) { // Yellowish tint
starB *= (1 - variationMagnitude); // Reduce Blue
starG = Math.min(255, starG * (1 + variationMagnitude * 0.2)); // Slightly boost Green for more yellow
} else { // Bluish tint
starR *= (1 - variationMagnitude * 0.8); // Reduce Red significantly
starG *= (1 - variationMagnitude * 0.3); // Reduce Green less for blue
}
}
starR = Math.max(0, Math.min(255, starR));
starG = Math.max(0, Math.min(255, starG));
starB = Math.max(0, Math.min(255, starB));
// Draw star center by overwriting pixel
data[R_CENTER_INDEX] = starR;
data[G_CENTER_INDEX] = starG;
data[B_CENTER_INDEX] = starB;
data[A_CENTER_INDEX] = 255; // Ensure star is opaque
// Add a subtle cross-shaped glow to ALL stars by additively blending with neighbors
const neighbors = [
{ dx: -1, dy: 0 }, { dx: 1, dy: 0 },
{ dx: 0, dy: -1 }, { dx: 0, dy: 1 }
];
for (const n of neighbors) {
const nx = x + n.dx;
const ny = y + n.dy;
if (nx >= 0 && nx < imgWidth && ny >= 0 && ny < imgHeight) {
const R_NEIGHBOR_INDEX = (ny * imgWidth + nx) * 4;
const G_NEIGHBOR_INDEX = R_NEIGHBOR_INDEX + 1;
const B_NEIGHBOR_INDEX = R_NEIGHBOR_INDEX + 2;
data[R_NEIGHBOR_INDEX] = Math.min(255, data[R_NEIGHBOR_INDEX] + starR * crossGlowIntensity);
data[G_NEIGHBOR_INDEX] = Math.min(255, data[G_NEIGHBOR_INDEX] + starG * crossGlowIntensity);
data[B_NEIGHBOR_INDEX] = Math.min(255, data[B_NEIGHBOR_INDEX] + starB * crossGlowIntensity);
}
}
}
}
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes