You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, numShards = 150, maxDisplacement = 10, maxRotation = 10, edgeDarkeningFactor = 0.2, edgeStrokeWidth = 1) {
// Check image dimensions
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
const outputCanvas = document.createElement('canvas');
const ctx = outputCanvas.getContext('2d');
if (w === 0 || h === 0) {
console.error("Image has zero width or height.");
outputCanvas.width = Math.max(1, w); // Ensure canvas has at least 1x1
outputCanvas.height = Math.max(1, h);
// Return an empty transparent canvas
ctx.clearRect(0,0,outputCanvas.width, outputCanvas.height);
return outputCanvas;
}
outputCanvas.width = w;
outputCanvas.height = h;
// Source canvas for reliable pixel sampling
const sourceCanvas = document.createElement('canvas');
sourceCanvas.width = w;
sourceCanvas.height = h;
const sourceCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
sourceCtx.drawImage(originalImg, 0, 0, w, h);
ctx.clearRect(0, 0, w, h); // Ensure output canvas is clear
// --- Delaunay Library Loading ---
let DelaunayConstructor;
// Helper to wait for a global variable to be defined
function waitForGlobal(varName, timeout = 5000) {
return new Promise((resolve, reject) => {
let attempts = 0;
const intervalTime = 100;
const maxAttempts = timeout / intervalTime;
const interval = setInterval(() => {
if (typeof window[varName] !== 'undefined') {
clearInterval(interval);
resolve(window[varName]);
} else if (typeof d3 !== 'undefined' && typeof d3[varName] !== 'undefined') { // Special case for d3.Delaunay
clearInterval(interval);
resolve(d3[varName]);
}
else if (attempts++ > maxAttempts) {
clearInterval(interval);
reject(new Error(`Global ${varName} not found after ${timeout}ms`));
}
}, intervalTime);
});
}
// Helper to load a script
function loadScript(src, globalToWaitFor) {
return new Promise(async (resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
// Script tag already exists, just wait for the global
try {
const lib = await waitForGlobal(globalToWaitFor || (src.includes('d3.v7.min.js') ? 'd3' : 'Delaunay'));
resolve(lib);
} catch (e) {
reject(e);
}
return;
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.onload = async () => {
script.remove();
try {
const lib = await waitForGlobal(globalToWaitFor || (src.includes('d3.v7.min.js') ? 'd3' : 'Delaunay'));
resolve(lib);
} catch (e) {
reject(e);
}
};
script.onerror = (err) => {
script.remove();
reject(new Error(`Failed to load script: ${src}. Error: ${err}`));
};
document.head.appendChild(script);
});
}
if (typeof d3 !== 'undefined' && typeof d3.Delaunay !== 'undefined') {
DelaunayConstructor = d3.Delaunay;
} else if (typeof window.Delaunay !== 'undefined') {
DelaunayConstructor = window.Delaunay;
} else {
try {
// Try loading full D3 (which includes Delaunay)
await loadScript('https://d3js.org/d3.v7.min.js', 'Delaunay'); // d3.v7 sets d3.Delaunay
if (typeof d3 !== 'undefined' && typeof d3.Delaunay !== 'undefined') {
DelaunayConstructor = d3.Delaunay;
} else {
throw new Error('d3.Delaunay not found after loading d3.v7.min.js');
}
} catch (e1) {
console.warn("Failed to load D3 with Delaunay (d3.v7.min.js), trying standalone d3-delaunay.js. Reason:", e1.message);
try {
// Standalone d3-delaunay UMD build exposes global `Delaunay`
DelaunayConstructor = await loadScript('https://cdn.jsdelivr.net/npm/d3-delaunay@6', 'Delaunay');
} catch (e2) {
console.error("Failed to load any Delaunay library. Reason:", e2.message);
ctx.drawImage(originalImg, 0, 0, w, h); // Fallback
return outputCanvas;
}
}
}
if (!DelaunayConstructor) {
console.error("Delaunay library could not be initialized. Drawing original image.");
ctx.drawImage(originalImg, 0, 0, w, h);
return outputCanvas;
}
// --- End Delaunay Library Loading ---
// Generate points for triangulation
const numPointsForTriangulation = Math.max(20, Math.floor(numShards * 0.75));
const points = [];
// Add corners (use w-1, h-1 for strict bounds, important for pixel sampling)
points.push([0, 0], [w - 1, 0], [0, h - 1], [w - 1, h - 1]);
const numEdgePoints = Math.max(4, Math.floor(Math.sqrt(numPointsForTriangulation)));
for (let i = 0; i < numEdgePoints; i++) {
points.push([Math.random() * (w - 1), 0]); // Top edge
points.push([Math.random() * (w - 1), h - 1]); // Bottom edge
points.push([0, Math.random() * (h - 1)]); // Left edge
points.push([w - 1, Math.random() * (h - 1)]); // Right edge
}
const currentPointsCount = points.length;
const remainingPointsNeeded = numPointsForTriangulation - currentPointsCount;
if (remainingPointsNeeded > 0) {
for (let i = 0; i < remainingPointsNeeded; i++) {
points.push([Math.random() * (w - 1), Math.random() * (h - 1)]);
}
}
// Ensure at least 3 unique points for Delaunay triangulation
const uniquePoints = Array.from(new Set(points.map(p => p.join(',')))).map(s => s.split(',').map(Number));
if (uniquePoints.length < 3) {
// Add a few more distinct points if necessary
uniquePoints.push([w/3, h/3], [2*w/3, h/3], [w/2, 2*h/3]);
const finalUniquePoints = Array.from(new Set(uniquePoints.map(p => p.join(',')))).map(s => s.split(',').map(Number));
if (finalUniquePoints.length < 3) {
console.error("Not enough unique points for triangulation even after adding more. Drawing original image.");
ctx.drawImage(originalImg, 0, 0, w, h);
return outputCanvas;
}
// Use the augmented unique points
for(let i = 0; i < finalUniquePoints.length; ++i) points[i] = finalUniquePoints[i]; // Truncate or replace start
points.length = finalUniquePoints.length; // ensure points array is only unique points
}
const delaunay = DelaunayConstructor.from(points);
const triangles = delaunay.triangles;
for (let i = 0; i < triangles.length; i += 3) {
const p1Idx = triangles[i];
const p2Idx = triangles[i+1];
const p3Idx = triangles[i+2];
if (p1Idx === undefined || p2Idx === undefined || p3Idx === undefined ||
!points[p1Idx] || !points[p2Idx] || !points[p3Idx]) {
// console.warn("Skipping invalid triangle indices from Delaunay output.");
continue;
}
const v1 = { x: points[p1Idx][0], y: points[p1Idx][1] };
const v2 = { x: points[p2Idx][0], y: points[p2Idx][1] };
const v3 = { x: points[p3Idx][0], y: points[p3Idx][1] };
const centroidX = (v1.x + v2.x + v3.x) / 3;
const centroidY = (v1.y + v2.y + v3.y) / 3;
const sampleX = Math.max(0, Math.min(w - 1, Math.floor(centroidX)));
const sampleY = Math.max(0, Math.min(h - 1, Math.floor(centroidY)));
const pixel = sourceCtx.getImageData(sampleX, sampleY, 1, 1).data;
const r = pixel[0], g = pixel[1], b = pixel[2], a = pixel[3] / 255;
const fillStyle = `rgba(${r},${g},${b},${a})`;
const rotationAngleRad = (Math.random() * 2 - 1) * maxRotation * (Math.PI / 180);
const offsetX = (Math.random() * 2 - 1) * maxDisplacement;
const offsetY = (Math.random() * 2 - 1) * maxDisplacement;
ctx.save();
ctx.translate(centroidX + offsetX, centroidY + offsetY);
ctx.rotate(rotationAngleRad);
// Draw triangle vertices relative to its original centroid, which is now at (0,0)
// in the local transformed coordinate system.
ctx.beginPath();
ctx.moveTo(v1.x - centroidX, v1.y - centroidY);
ctx.lineTo(v2.x - centroidX, v2.y - centroidY);
ctx.lineTo(v3.x - centroidX, v3.y - centroidY);
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
if (edgeDarkeningFactor > 0 && edgeStrokeWidth > 0) {
const strokeR = Math.max(0, Math.floor(r * (1 - edgeDarkeningFactor)));
const strokeG = Math.max(0, Math.floor(g * (1 - edgeDarkeningFactor)));
const strokeB = Math.max(0, Math.floor(b * (1 - edgeDarkeningFactor)));
// Use original alpha for stroke to maintain shard transparency
ctx.strokeStyle = `rgba(${strokeR},${strokeG},${strokeB},${a})`;
ctx.lineWidth = edgeStrokeWidth;
ctx.stroke();
}
ctx.restore();
}
return outputCanvas;
}
Apply Changes