You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, gridSize = "15", style = "Trails", colorTheme = "Original", lengthScale = "2", backgroundColor = "#0b0b1a") {
// Parse parameters
const d = parseInt(gridSize, 10) || 15;
const lenMult = parseFloat(lengthScale) || 2;
const styleMode = (style || 'Trails').toLowerCase();
const theme = (colorTheme || 'Original').toLowerCase();
const bgColor = backgroundColor || '#0b0b1a';
const w = originalImg.width;
const h = originalImg.height;
// Source canvas to extract pixel data
const srcCanvas = document.createElement('canvas');
srcCanvas.width = w;
srcCanvas.height = h;
const srcCtx = srcCanvas.getContext('2d', { willReadFrequently: true });
srcCtx.drawImage(originalImg, 0, 0);
const imgData = srcCtx.getImageData(0, 0, w, h);
const data = imgData.data;
// Output canvas
const outCanvas = document.createElement('canvas');
outCanvas.width = w;
outCanvas.height = h;
const outCtx = outCanvas.getContext('2d');
// Fill background
outCtx.fillStyle = bgColor;
outCtx.fillRect(0, 0, w, h);
// Helper to get brightness at a pixel coordinate out of the linear image data array
const getBrightness = (x, y) => {
// Clamp coordinates to image boundaries
const cx = x < 0 ? 0 : (x >= w ? w - 1 : x);
const cy = y < 0 ? 0 : (y >= h ? h - 1 : y);
const i = (cy * w + cx) * 4;
// Standard relative luminance formula
return 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
};
// 'Span' adds spatial smoothing to the gradient calculation based on the grid spacing
const span = Math.max(1, Math.floor(d / 4));
// Calculate angular direction & magnitude based on pixel gradients
const getAngleAndMag = (x, y) => {
const ix = Math.floor(x);
const iy = Math.floor(y);
const dx = getBrightness(ix + span, iy) - getBrightness(ix - span, iy);
const dy = getBrightness(ix, iy + span) - getBrightness(ix, iy - span);
return {
angle: Math.atan2(dy, dx) + Math.PI / 2, // Add 90 degrees (PI/2) to flow vectors along the contours/edges
mag: Math.sqrt(dx*dx + dy*dy)
};
};
const getColor = (x, y, angle) => {
const cx = Math.floor(x < 0 ? 0 : (x >= w ? w - 1 : x));
const cy = Math.floor(y < 0 ? 0 : (y >= h ? h - 1 : y));
const i = (cy * w + cx) * 4;
if (theme === 'monochrome') {
return '#ffffff';
} else if (theme === 'neon' || theme === 'rainbow') {
const hue = (((angle * 180 / Math.PI) % 360) + 360) % 360; // Normalize the angle to be strictly 0-360
return `hsl(${hue}, 80%, 65%)`;
} else {
// Original colors
return `rgb(${data[i]}, ${data[i+1]}, ${data[i+2]})`;
}
};
const step = Math.max(2, d);
const baseLen = step * lenMult;
// Output stroke settings
outCtx.lineWidth = Math.max(0.5, step * 0.15);
outCtx.lineCap = 'round';
outCtx.lineJoin = 'round';
// Set global alpha to make overlapping sections blend fluidly
outCtx.globalAlpha = styleMode === 'trails' ? 0.6 : 0.85;
for (let y = step / 2; y < h; y += step) {
for (let x = step / 2; x < w; x += step) {
const { angle, mag } = getAngleAndMag(x, y);
// Skip drawing in extremely flat areas to preserve structural contrast of the picture,
// but keep it if rendering pure neon fields.
if (mag < 2 && theme !== 'neon') continue;
const color = getColor(x, y, angle);
const halfLen = baseLen / 2;
outCtx.strokeStyle = color;
outCtx.fillStyle = color;
if (styleMode === 'trails') {
let px = x;
let py = y;
outCtx.beginPath();
outCtx.moveTo(px, py);
// Generate fluid trail (continuous vector curves)
const maxSteps = Math.min(150, Math.floor(baseLen * 2));
for (let s = 0; s < maxSteps; s++) {
const localParams = getAngleAndMag(px, py);
// Trace forward per step
px += Math.cos(localParams.angle) * 1.5;
py += Math.sin(localParams.angle) * 1.5;
outCtx.lineTo(px, py);
if (px < 0 || px >= w || py < 0 || py >= h) break;
}
outCtx.stroke();
} else {
// Render singular geometric elements per cell
outCtx.save();
outCtx.translate(x, y);
outCtx.rotate(angle);
if (styleMode === 'arrows') {
outCtx.beginPath();
outCtx.moveTo(-halfLen, 0);
outCtx.lineTo(halfLen, 0);
// Arrow head calculation
const headSize = Math.max(2, halfLen * 0.35);
outCtx.lineTo(halfLen - headSize, -headSize * 0.6);
outCtx.moveTo(halfLen, 0);
outCtx.lineTo(halfLen - headSize, headSize * 0.6);
outCtx.stroke();
} else if (styleMode === 'particles' || styleMode === 'dots') {
outCtx.beginPath();
// Directional particle streak
outCtx.ellipse(0, 0, halfLen, Math.max(1, step * 0.1), 0, 0, Math.PI * 2);
outCtx.fill();
} else {
// Default behavior (styleMode === 'lines' or undefined)
outCtx.beginPath();
outCtx.moveTo(-halfLen, 0);
outCtx.lineTo(halfLen, 0);
outCtx.stroke();
}
outCtx.restore();
}
}
}
return outCanvas;
}
Apply Changes