You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, meteorCount = "60", meteorColor = "#ffffff", angleDegrees = "45", speed = "20", backgroundStars = "150", animated = "1") {
const canvas = document.createElement('canvas');
canvas.width = originalImg.width;
canvas.height = originalImg.height;
const ctx = canvas.getContext('2d');
// Parse arguments
const count = parseInt(meteorCount, 10) || 60;
const angle = parseFloat(angleDegrees) || 45;
const speedVal = parseFloat(speed) || 20;
const numStars = parseInt(backgroundStars, 10) || 0;
const isAnim = parseInt(animated, 10) !== 0;
// Helper to extract exact RGB values for foolproof alpha gradients (avoids the Safari grey-transparent bug)
function getRgba(color, alpha) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = 1;
tempCanvas.height = 1;
const tCtx = tempCanvas.getContext('2d');
tCtx.fillStyle = '#000000'; // Default black fallback if invalid string
tCtx.fillStyle = color;
tCtx.fillRect(0, 0, 1, 1);
const data = tCtx.getImageData(0, 0, 1, 1).data;
return `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${alpha})`;
}
const solidCol = getRgba(meteorColor, 1);
const transparentCol = getRgba(meteorColor, 0);
const angleRads = angle * Math.PI / 180;
const dx = Math.cos(angleRads);
const dy = Math.sin(angleRads);
class Meteor {
constructor(initial = false) {
this.reset(initial);
if (initial) {
// Randomize lifespan stage so frame 1 looks like a meteor shower in progress
this.life = Math.random() * this.maxLife;
}
}
reset(initial = false) {
const maxDim = Math.max(canvas.width, canvas.height);
this.length = Math.random() * (maxDim * 0.1) + 20;
this.thickness = Math.random() * 1.5 + 0.5;
// Adjust speed proportionally to canvas size
this.speed = (Math.random() * (speedVal / 2) + speedVal) * (maxDim / 800);
this.maxLife = Math.random() * 40 + 30; // 30-70 frames
this.life = 0;
// Generate standard starting position across the surface bounds
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
if (!initial) {
// Nudge backwards along path so they smoothly appear entering from edges/sky naturally
const backwardsOffset = Math.random() * maxDim * 0.5;
this.x -= dx * backwardsOffset;
this.y -= dy * backwardsOffset;
}
}
update() {
this.life++;
if (this.life >= this.maxLife) {
this.reset();
} else {
this.x += this.speed * dx;
this.y += this.speed * dy;
}
}
draw() {
const lifeRatio = this.life / this.maxLife;
// Naturally fade in and fade out as it travels
const opacity = Math.sin(lifeRatio * Math.PI);
const currentLength = this.length * Math.sin(lifeRatio * Math.PI);
// Avoid drawing rendering artifacts for invisible meteors
if (currentLength < 0.5) return;
const tailX = this.x - currentLength * dx;
const tailY = this.y - currentLength * dy;
ctx.save();
ctx.globalAlpha = opacity;
// Draw Meteor Trail
ctx.beginPath();
const grad = ctx.createLinearGradient(tailX, tailY, this.x, this.y);
grad.addColorStop(0, transparentCol);
grad.addColorStop(1, solidCol);
ctx.strokeStyle = grad;
ctx.lineWidth = this.thickness;
ctx.lineCap = "round";
ctx.shadowBlur = this.thickness * 4;
ctx.shadowColor = meteorColor;
ctx.moveTo(tailX, tailY);
ctx.lineTo(this.x, this.y);
ctx.stroke();
// Draw Meteor Head (Fireball impact glare)
ctx.beginPath();
ctx.fillStyle = '#ffffff';
ctx.arc(this.x, this.y, this.thickness * 0.8, 0, Math.PI * 2);
ctx.shadowBlur = this.thickness * 6;
ctx.shadowColor = '#ffffff';
ctx.fill();
ctx.restore();
}
}
const meteors = [];
for (let i = 0; i < count; i++) {
meteors.push(new Meteor(true));
}
const stars = [];
for (let i = 0; i < numStars; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 1.5,
baseOpacity: Math.random() * 0.6 + 0.1,
twinkleSpeed: Math.random() * 0.05 + 0.01,
time: Math.random() * Math.PI * 2
});
}
let animationId;
function render() {
// Draw the primary reference image
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Render effects using "screen" composite mode to add brightness (lights mapping overlay)
ctx.globalCompositeOperation = 'screen';
// Render optional atmospheric starry sky
if (numStars > 0) {
ctx.save();
ctx.fillStyle = '#ffffff';
for (let s of stars) {
if (isAnim) {
s.time += s.twinkleSpeed;
}
let op = s.baseOpacity + Math.sin(s.time) * 0.3;
if (op < 0) op = 0;
if (op > 1) op = 1;
ctx.globalAlpha = op;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
// Render falling meteors
for (let m of meteors) {
if (isAnim) m.update();
m.draw();
}
// Setup the loop if configured for animation
if (isAnim) {
animationId = requestAnimationFrame(render);
}
}
// Execute a starting render frame
render();
// Attach a convenient teardown method so apps dynamically rendering this canvas can halt animation and free up resources
canvas.stopAnimation = () => {
if (animationId) cancelAnimationFrame(animationId);
};
return canvas;
}
Apply Changes