You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, numPoints = 1500, pointSelectionMode = "edge_priority", edgeThreshold = 50, blurRadius = 1.0) {
// Helper function for Sobel edge detection
function _getEdgeMagnitudeMap(imageData, W, H, threshold) {
const grayData = new Uint8ClampedArray(W * H);
const magnitudeData = new Float32Array(W * H); // Store float magnitudes
let maxMagnitude = 0;
// Convert to grayscale (luminosity method)
for (let i = 0; i < imageData.data.length; i += 4) {
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
grayData[i / 4] = 0.299 * r + 0.587 * g + 0.114 * b;
}
// Sobel kernels
const Gx = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const Gy = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
let sumX = 0;
let sumY = 0;
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
// Clamp coordinates to handle image borders
const currentY = Math.max(0, Math.min(H - 1, y + i));
const currentX = Math.max(0, Math.min(W - 1, x + j));
const pixelVal = grayData[currentY * W + currentX];
sumX += pixelVal * Gx[i + 1][j + 1];
sumY += pixelVal * Gy[i + 1][j + 1];
}
}
const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
magnitudeData[y * W + x] = magnitude;
if (magnitude > maxMagnitude) {
maxMagnitude = magnitude;
}
}
}
const edgeMap = new Uint8ClampedArray(W * H); // 0 (not edge) or 255 (edge)
if (maxMagnitude > 0) { // Avoid division by zero for blank images
for (let i = 0; i < magnitudeData.length; i++) {
const normalizedMagnitude = (magnitudeData[i] / maxMagnitude) * 255;
if (normalizedMagnitude > threshold) {
edgeMap[i] = 255;
} else {
edgeMap[i] = 0;
}
}
}
return { edgeMap, magnitudeData, maxMagnitude }; // Return all parts, edgeMap is main output
}
// Parameter validation and type coercion
numPoints = Math.max(5, Number(numPoints)); // Need at least a few points for triangulation
blurRadius = Math.max(0, Number(blurRadius));
edgeThreshold = Math.max(0, Math.min(255, Number(edgeThreshold)));
if (!["random", "grid", "edge_priority"].includes(String(pointSelectionMode))) {
pointSelectionMode = "edge_priority"; // Default if invalid mode provided
}
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
if (width === 0 || height === 0) {
console.error("Image has zero width or height. Ensure it's loaded and valid.");
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 1;
emptyCanvas.height = 1; // Return a 1x1 empty canvas for error cases
return emptyCanvas;
}
// 0. Dynamically load d3-delaunay library if not already available
if (typeof d3 === 'undefined' || typeof d3.Delaunay === 'undefined') {
try {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/d3-delaunay@6'; // UMD bundle
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error("Failed to load d3-delaunay library from CDN."));
document.head.appendChild(script);
});
} catch (error) {
console.error(error.message);
throw error; // Re-throw if library loading is critical
}
// Check again after script loading attempt
if (typeof d3 === 'undefined' || typeof d3.Delaunay === 'undefined') {
const err = new Error("d3.Delaunay is not available even after attempting to load from CDN.");
console.error(err.message);
throw err;
}
}
// 1. Prepare canvases: one for original image data, one for processing (blur + edge detection)
const inputCanvas = document.createElement('canvas');
inputCanvas.width = width;
inputCanvas.height = height;
const inputCtx = inputCanvas.getContext('2d');
inputCtx.drawImage(originalImg, 0, 0, width, height);
const originalImageData = inputCtx.getImageData(0, 0, width, height); // For final color sampling
const processingCanvas = document.createElement('canvas');
processingCanvas.width = width;
processingCanvas.height = height;
const processingCtx = processingCanvas.getContext('2d');
if (blurRadius > 0) {
processingCtx.filter = `blur(${blurRadius}px)`;
}
processingCtx.drawImage(originalImg, 0, 0, width, height);
processingCtx.filter = 'none'; // Reset filter
const blurredImageData = processingCtx.getImageData(0, 0, width, height); // For edge detection
// 2. Point Selection Logic
const points = [];
// Add mandatory corner points
points.push([0, 0], [width - 1, 0], [0, height - 1], [width - 1, height - 1]);
// Add some boundary points for well-defined edges
// Estimate: approx sqrt(numPoints)*0.125 points per side. e.g. for 1500 pts, ~5 points/side
const boundaryPointsPerSide = Math.max(0, Math.floor(Math.sqrt(numPoints) * 0.125));
if (boundaryPointsPerSide > 0) {
for (let i = 1; i <= boundaryPointsPerSide; i++) {
const W_step = width / (boundaryPointsPerSide + 1);
const H_step = height / (boundaryPointsPerSide + 1);
points.push([i * W_step, 0]); // Top edge
points.push([i * W_step, height - 1]); // Bottom edge
points.push([0, i * H_step]); // Left edge
points.push([width - 1, i * H_step]); // Right edge
}
}
// Simple deduplication of corner/boundary points (in case of overlaps from calculations)
const uniquePointsMap = new Map();
const tempUniquePoints = [];
for(const p of points) {
const key = `${Math.round(p[0])},${Math.round(p[1])}`; // Round to avoid float precision key issues
if(!uniquePointsMap.has(key)) {
uniquePointsMap.set(key, true);
tempUniquePoints.push(p);
}
}
points.length = 0;
points.push(...tempUniquePoints);
const pointsToGenerate = Math.max(0, numPoints - points.length);
if (pointsToGenerate > 0) {
if (pointSelectionMode === "random") {
for (let i = 0; i < pointsToGenerate; i++) {
points.push([Math.random() * width, Math.random() * height]);
}
} else if (pointSelectionMode === "grid") {
const aspectRatio = width / height;
// Calculate M (columns) and N (rows) for a grid that roughly matches numPoints
const M_cols = Math.ceil(Math.sqrt(pointsToGenerate * aspectRatio));
const N_rows = Math.ceil(pointsToGenerate / M_cols);
if (M_cols > 0 && N_rows > 0) {
const xStep = width / M_cols;
const yStep = height / N_rows;
for (let i = 0; i < N_rows; i++) {
for (let j = 0; j < M_cols; j++) {
if (points.length < numPoints) {
points.push([
(j + 0.5) * xStep + (Math.random() - 0.5) * xStep * 0.3, // Jitter: 15% of ste
(i + 0.5) * yStep + (Math.random() - 0.5) * yStep * 0.3
]);
} else break;
}
if (points.length >= numPoints) break;
}
}
// Fill any remaining points randomly if grid didn't reach numPoints target
while(points.length < numPoints) {
points.push([Math.random() * width, Math.random() * height]);
}
} else { // "edge_priority" (default mode)
const { edgeMap } = _getEdgeMagnitudeMap(blurredImageData, width, height, edgeThreshold);
const edgePixelCoords = [];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (edgeMap[y * width + x] === 255) { // If it's an edge pixel
edgePixelCoords.push([x, y]);
}
}
}
// Aim for ~70% of remaining points from edges, rest random
const numEdgePointsToAttempt = Math.floor(pointsToGenerate * 0.7);
let actualEdgePointsAdded = 0;
if (edgePixelCoords.length > 0) {
for (let i = 0; i < numEdgePointsToAttempt; i++) {
if (points.length >= numPoints) break;
const randIdx = Math.floor(Math.random() * edgePixelCoords.length);
points.push(edgePixelCoords[randIdx]);
actualEdgePointsAdded++;
}
}
const numRandomPoints = pointsToGenerate - actualEdgePointsAdded;
for (let i = 0; i < numRandomPoints; i++) {
if (points.length >= numPoints) break;
points.push([Math.random() * width, Math.random() * height]);
}
}
}
// Final pass: ensure all points are finite numbers and within image boundaries.
const finalPoints = [];
for(let i = 0; i < points.length; i++) {
let x = points[i][0];
let y = points[i][1];
// Ensure x and y are valid numbers before clamping
if (Number.isFinite(x) && Number.isFinite(y)) {
finalPoints.push([
Math.max(0, Math.min(width - 1, x)),
Math.max(0, Math.min(height - 1, y))
]);
}
}
// Delaunay triangulation requires at least 3 non-collinear points.
if (finalPoints.length < 3) {
console.warn("Not enough valid points for triangulation. Adding fallback random points.");
while(finalPoints.length < 3 && finalPoints.length < numPoints) { // ensure not adding too many
finalPoints.push([Math.random() * (width-1), Math.random() * (height-1)]);
}
// One last check to make sure we always have at least 3 for d3.Delaunay
while(finalPoints.length < 3){
finalPoints.push([Math.random() * (width-1), Math.random() * (height-1)]);
}
}
// 3. Perform Delaunay Triangulation
const delaunay = d3.Delaunay.from(finalPoints);
// 4. Create output canvas and draw the low-poly triangles
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const outCtx = outputCanvas.getContext('2d');
outCtx.imageSmoothingEnabled = false; // Often preferred for crisp triangles
outCtx.clearRect(0, 0, width, height);
for (let i = 0; i < delaunay.triangles.length; i += 3) {
const p1_idx = delaunay.triangles[i];
const p2_idx = delaunay.triangles[i+1];
const p3_idx = delaunay.triangles[i+2];
const t_points = [
finalPoints[p1_idx],
finalPoints[p2_idx],
finalPoints[p3_idx]
];
// Calculate centroid of the triangle to sample color
const cx = (t_points[0][0] + t_points[1][0] + t_points[2][0]) / 3;
const cy = (t_points[0][1] + t_points[1][1] + t_points[2][1]) / 3;
// Clamp coordinates and get integer pixel index for color sampling
const x_sample = Math.floor(Math.max(0, Math.min(width - 1, cx)));
const y_sample = Math.floor(Math.max(0, Math.min(height - 1, cy)));
const originalPixelIndex = (y_sample * width + x_sample) * 4;
const r = originalImageData.data[originalPixelIndex];
const g = originalImageData.data[originalPixelIndex + 1];
const b = originalImageData.data[originalPixelIndex + 2];
// const a = originalImageData.data[originalPixelIndex + 3]; // Alpha, if needed
outCtx.fillStyle = `rgb(${r},${g},${b})`;
outCtx.beginPath();
outCtx.moveTo(t_points[0][0], t_points[0][1]);
outCtx.lineTo(t_points[1][0], t_points[1][1]);
outCtx.lineTo(t_points[2][0], t_points[2][1]);
outCtx.closePath();
outCtx.fill();
// Optional: Add stroke to triangles (can be a parameter)
// outCtx.strokeStyle = "rgba(0,0,0,0.05)";
// outCtx.lineWidth = 0.5;
// outCtx.stroke();
}
return outputCanvas;
}
Apply Changes