You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, particleCount = "2500", speed = "2", strokeSize = "1.5", fadeSpeed = "0.05", recordDurationSeconds = "5") {
// Parse parameters
const pCount = parseInt(particleCount);
const spd = parseFloat(speed);
const sSize = parseFloat(strokeSize);
const fSpeed = parseFloat(fadeSpeed);
// Setup main container UI
const container = document.createElement('div');
container.style.position = 'relative';
container.style.display = 'inline-flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
container.style.gap = '15px';
container.style.fontFamily = 'sans-serif';
container.style.background = '#1a1a1a';
container.style.padding = '20px';
container.style.borderRadius = '12px';
container.style.maxWidth = '100%';
container.style.boxSizing = 'border-box';
// Responsively scale original image for optimal performance
let w = originalImg.width;
let h = originalImg.height;
const MAX_DIM = 1200;
if (w > MAX_DIM || h > MAX_DIM) {
if (w > h) {
h = Math.floor(h * (MAX_DIM / w));
w = MAX_DIM;
} else {
w = Math.floor(w * (MAX_DIM / h));
h = MAX_DIM;
}
}
const wrapper = document.createElement('div');
wrapper.style.position = 'relative';
wrapper.style.display = 'inline-block';
wrapper.style.maxWidth = '100%';
wrapper.style.boxShadow = '0 6px 18px rgba(0,0,0,0.6)';
wrapper.style.borderRadius = '8px';
wrapper.style.overflow = 'hidden';
// Setup rendering canvas
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
canvas.style.maxWidth = '100%';
canvas.style.display = 'block';
const ctx = canvas.getContext('2d', { alpha: false }); // performance optimization
wrapper.appendChild(canvas);
container.appendChild(wrapper);
// Fill screen purely black initially
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, w, h);
// Extract image pixel data using an offscreen canvas
const offCanvas = document.createElement('canvas');
offCanvas.width = w;
offCanvas.height = h;
const offCtx = offCanvas.getContext('2d', { willReadFrequently: true });
offCtx.drawImage(originalImg, 0, 0, w, h);
let imgData;
try {
imgData = offCtx.getImageData(0, 0, w, h).data;
} catch(e) {
container.innerHTML = `<span style="color:#ff6b6b; font-family:sans-serif; text-align:center;">Error: Could not retrieve image data due to CORS policy. Please process a local or cleanly-proxied image.</span>`;
return container;
}
// Precalculate angle variations based on image gradients (generative flow field calculation)
// This allows particles to aesthetically follow the contours and strokes of the image subject
const angles = new Float32Array(w * h);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const px0 = x - 1 < 0 ? 0 : x - 1;
const px1 = x + 1 >= w ? w - 1 : x + 1;
const py0 = y - 1 < 0 ? 0 : y - 1;
const py1 = y + 1 >= h ? h - 1 : y + 1;
const iU = (py0 * w + x) * 4;
const iD = (py1 * w + x) * 4;
const iL = (y * w + px0) * 4;
const iR = (y * w + px1) * 4;
const bU = imgData[iU] + imgData[iU+1] + imgData[iU+2];
const bD = imgData[iD] + imgData[iD+1] + imgData[iD+2];
const bL = imgData[iL] + imgData[iL+1] + imgData[iL+2];
const bR = imgData[iR] + imgData[iR+1] + imgData[iR+2];
const dx = bR - bL;
const dy = bD - bU;
let angle;
if (dx === 0 && dy === 0) {
// Introduce subtle structured noise in uniform flat areas
angle = Math.sin(x * 0.05) + Math.cos(y * 0.05);
} else {
angle = Math.atan2(dy, dx) + Math.PI / 2;
}
angles[y * w + x] = angle;
}
}
// Define generative particle structure modeling stroke brushes
class Particle {
constructor() {
this.reset();
this.life = Math.random() * 200;
}
reset() {
this.x = Math.random() * w;
this.y = Math.random() * h;
this.prevX = this.x;
this.prevY = this.y;
this.vx = 0;
this.vy = 0;
this.life = Math.random() * 150 + 50;
}
update() {
this.prevX = this.x;
this.prevY = this.y;
const ix = Math.floor(this.x);
const iy = Math.floor(this.y);
const i = iy * w + ix;
const angle = angles[i];
const targetVx = Math.cos(angle) * spd;
const targetVy = Math.sin(angle) * spd;
this.vx += (targetVx - this.vx) * 0.1;
this.vy += (targetVy - this.vy) * 0.1;
this.x += this.vx;
this.y += this.vy;
this.life--;
if (this.life <= 0 || this.x < 1 || this.x >= w - 1 || this.y < 1 || this.y >= h - 1) {
this.reset();
return null;
}
const idx = i * 4;
return {
r: imgData[idx],
g: imgData[idx + 1],
b: imgData[idx + 2]
};
}
draw(ctx, color) {
ctx.beginPath();
ctx.moveTo(this.prevX, this.prevY);
ctx.lineTo(this.x, this.y);
// Draw with transparency for rich blending overlap
ctx.strokeStyle = `rgba(${color.r}, ${color.g}, ${color.b}, 0.7)`;
ctx.lineWidth = sSize;
ctx.lineCap = 'round';
ctx.stroke();
}
}
const particles = [];
for (let i = 0; i < pCount; i++) {
particles.push(new Particle());
}
let hasBeenConnected = false;
let animId;
function animate() {
// Prevent memory leak by cleaning up rendering if tool is unmounted
if (!hasBeenConnected) {
if (container.isConnected) hasBeenConnected = true;
} else {
if (!container.isConnected) return;
}
// Add a slight fading trail representing motion trace
ctx.fillStyle = `rgba(0, 0, 0, ${fSpeed})`;
ctx.fillRect(0, 0, w, h);
// Update and draw flowing colored painter strokes
for (let p of particles) {
const color = p.update();
if (color) {
p.draw(ctx, color);
}
}
animId = requestAnimationFrame(animate);
}
// Quick-seed positions sequentially for an aesthetically faster start
for (let i = 0; i < 30; i++) { for (let p of particles) p.update(); }
animate(); // Run artistic animation
// Set up Native Web Video Recording Utility
const btnContainer = document.createElement('div');
btnContainer.style.display = 'flex';
btnContainer.style.gap = '15px';
const btnRecord = document.createElement('button');
btnRecord.innerText = `Record & Download Video (${recordDurationSeconds}s)`;
btnRecord.style.padding = '12px 24px';
btnRecord.style.background = '#007BFF';
btnRecord.style.color = '#fff';
btnRecord.style.border = 'none';
btnRecord.style.borderRadius = '24px';
btnRecord.style.fontSize = '15px';
btnRecord.style.cursor = 'pointer';
btnRecord.style.fontWeight = 'bold';
btnRecord.style.transition = 'all 0.2s ease';
btnRecord.style.boxShadow = '0 4px 10px rgba(0, 123, 255, 0.4)';
btnRecord.onmouseenter = () => { if (!btnRecord.disabled) btnRecord.style.background = '#0056b3'; };
btnRecord.onmouseleave = () => { if (!btnRecord.disabled) btnRecord.style.background = '#007BFF'; };
// Start video recording action
btnRecord.onclick = () => {
if (!canvas.captureStream) {
alert('Video recording is not supported natively in this browser version.');
return;
}
let options;
if (MediaRecorder.isTypeSupported('video/webm; codecs=vp9')) {
options = { mimeType: 'video/webm; codecs=vp9' };
} else if (MediaRecorder.isTypeSupported('video/webm')) {
options = { mimeType: 'video/webm' };
} else if (MediaRecorder.isTypeSupported('video/mp4')) {
options = { mimeType: 'video/mp4' };
} else {
options = {};
}
try {
const stream = canvas.captureStream(30); // Capture canvas at 30fps
const recorder = new MediaRecorder(stream, options);
const chunks = [];
recorder.ondataavailable = e => {
if (e.data && e.data.size > 0) chunks.push(e.data);
};
recorder.onstop = () => {
const mimeType = options.mimeType || 'video/webm';
const blob = new Blob(chunks, { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const ext = mimeType.includes('mp4') ? 'mp4' : 'webm';
a.download = `art_painted_video.${ext}`;
a.click();
URL.revokeObjectURL(url);
// Revert button styling
btnRecord.innerText = `Record & Download Video (${recordDurationSeconds}s)`;
btnRecord.disabled = false;
btnRecord.style.background = '#007BFF';
btnRecord.style.cursor = 'pointer';
btnRecord.style.boxShadow = '0 4px 10px rgba(0, 123, 255, 0.4)';
};
recorder.start();
// Set Recording UI
btnRecord.innerText = 'Recording Video... Please wait';
btnRecord.disabled = true;
btnRecord.style.background = '#555';
btnRecord.style.boxShadow = 'none';
btnRecord.style.cursor = 'not-allowed';
// Wait standard duration to halt WebM clip capture
setTimeout(() => {
if (recorder.state === 'recording') {
recorder.stop();
}
}, parseFloat(recordDurationSeconds) * 1000);
} catch (err) {
console.error('Recording context initialization error:', err);
alert('An error occurred while generating the web video output setup.');
btnRecord.innerText = `Record & Download Video (${recordDurationSeconds}s)`;
btnRecord.disabled = false;
btnRecord.style.background = '#007BFF';
btnRecord.style.cursor = 'pointer';
}
};
btnContainer.appendChild(btnRecord);
container.appendChild(btnContainer);
return container;
}
Apply Changes