};
reader.readAsDataURL(file);
});
}
function createHalftoneDataUrl(image) {
const maxDimension = 1800;
const scale = Math.min(1, maxDimension / Math.max(image.width, image.height));
const width = Math.max(1, Math.round(image.width * scale));
const height = Math.max(1, Math.round(image.height * scale));
const sourceCanvas = document.createElement("canvas");
sourceCanvas.width = width;
sourceCanvas.height = height;
const sourceCtx = sourceCanvas.getContext("2d", { willReadFrequently: true });
sourceCtx.drawImage(image, 0, 0, width, height);
const imageData = sourceCtx.getImageData(0, 0, width, height).data;
const output = document.createElement("canvas");
output.width = width;
output.height = height;
const ctx = output.getContext("2d");
ctx.fillStyle = "#f4f4f4";
ctx.fillRect(0, 0, width, height);
for (let y = 0; y < height; y += dotsPerCell) {
for (let x = 0; x < width; x += dotsPerCell) {
const brightness = sampleAverageBrightness(imageData, width, height, x, y, dotsPerCell);
const darkness = 1 - brightness / 255;
if (darkness <= 0.01) continue;
const radius = Math.max(minDot, (dotsPerCell * maxDotScale * darkness) / 2);
ctx.beginPath();
ctx.fillStyle = "#111";
ctx.arc(x + dotsPerCell / 2, y + dotsPerCell / 2, radius, 0, Math.PI * 2);
ctx.fill();
}
}
return output.toDataURL("image/webp", 0.92);
}
function sampleAverageBrightness(data, width, height, startX, startY, size) {
let total = 0;
let count = 0;
const endX = Math.min(startX + size, width);
const endY = Math.min(startY + size, height);
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const idx = (y * width + x) * 4;
const r = data[idx];
const g = data[idx + 1];
const b = data[idx + 2];
total += 0.299 * r + 0.587 * g + 0.114 * b;
count++;
}
}
return count ? total / count : 255;
}