import { CHAR_SETS, type AsciiSettings } from './ascii-shared'; export class AsciiExporter { static downloadPNG(canvas: HTMLCanvasElement, filename = 'ascii-art.png') { const link = document.createElement('a'); link.download = filename; link.href = canvas.toDataURL('image/png'); link.click(); } static downloadText(content: string, filename: string) { const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); URL.revokeObjectURL(link.href); } static async copyToClipboard(text: string): Promise { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); } else { // Fallback const textArea = document.createElement("textarea"); textArea.value = text; textArea.style.position = "fixed"; textArea.style.left = "-9999px"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); } } static generateText( img: HTMLImageElement, settings: AsciiSettings, widthCols: number, heightRows: number ): string { const { pixels } = this.getPixels(img, widthCols, heightRows); let output = ""; const charSet = CHAR_SETS[settings.charSet] || CHAR_SETS.standard; const charCount = charSet.length; for (let y = 0; y < heightRows; y++) { for (let x = 0; x < widthCols; x++) { const i = (y * widthCols + x) * 4; const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; // const a = pixels[i + 3]; // Ignore alpha for now // 1. Adjust Color (Exposure, Contrast, Saturation, Gamma) const adjColor = this.adjustColor(r, g, b, settings); // 2. Calculate Luma from adjusted color let luma = (0.2126 * adjColor.r + 0.7152 * adjColor.g + 0.0722 * adjColor.b) / 255; // 3. Map to Char if (settings.invert) luma = 1.0 - luma; const charIndex = Math.floor(luma * (charCount - 1) + 0.5); const char = charSet[Math.max(0, Math.min(charCount - 1, charIndex))]; output += char; } output += "\n"; } return output; } static generateHTML( img: HTMLImageElement, settings: AsciiSettings, widthCols: number, heightRows: number ): string { const { pixels } = this.getPixels(img, widthCols, heightRows); let output = `
`;
        const charSet = CHAR_SETS[settings.charSet] || CHAR_SETS.standard;
        const charCount = charSet.length;

        for (let y = 0; y < heightRows; y++) {
            for (let x = 0; x < widthCols; x++) {
                const i = (y * widthCols + x) * 4;
                const r = pixels[i];
                const g = pixels[i + 1];
                const b = pixels[i + 2];

                // 1. Calculate Luma
                let luma = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;

                // 2. Apply Adjustments
                // Note: For color mode, we might want to keep original color 
                // but apply luma to alpha or just use luma for char selection.
                // The shader uses `color` argument. 

                // Adjust color for display
                const adjColor = this.adjustColor(r, g, b, settings);

                // Recalculate luma from adjusted color for char selection
                let finalLuma = (0.2126 * adjColor.r + 0.7152 * adjColor.g + 0.0722 * adjColor.b) / 255;

                // Adjust luma curve for char selection (gamma/contrast/exposure) explicitly?
                // Actually `adjustColor` does that.

                if (settings.invert) finalLuma = 1.0 - finalLuma;

                const charIndex = Math.floor(finalLuma * (charCount - 1) + 0.5);
                const char = charSet[Math.max(0, Math.min(charCount - 1, charIndex))];

                // Escape HTML
                const safeChar = char === '<' ? '<' : char === '>' ? '>' : char === '&' ? '&' : char;

                const colorStyle = `color: rgb(${Math.round(adjColor.r)}, ${Math.round(adjColor.g)}, ${Math.round(adjColor.b)})`;
                output += `${safeChar}`;
            }
            output += "
"; } output += "
"; return output; } private static getPixels(img: HTMLImageElement, width: number, height: number) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error("Canvas 2D context not available"); // High quality downscaling ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(img, 0, 0, width, height); return { pixels: ctx.getImageData(0, 0, width, height).data, ctx }; } private static adjustColor(r: number, g: number, b: number, settings: AsciiSettings): { r: number, g: number, b: number } { let rNorm = r / 255; let gNorm = g / 255; let bNorm = b / 255; // Exposure rNorm *= settings.exposure; gNorm *= settings.exposure; bNorm *= settings.exposure; // Contrast rNorm = (rNorm - 0.5) * settings.contrast + 0.5; gNorm = (gNorm - 0.5) * settings.contrast + 0.5; bNorm = (bNorm - 0.5) * settings.contrast + 0.5; // Saturation const luma = 0.2126 * rNorm + 0.7152 * gNorm + 0.0722 * bNorm; rNorm = luma + (rNorm - luma) * settings.saturation; gNorm = luma + (gNorm - luma) * settings.saturation; bNorm = luma + (bNorm - luma) * settings.saturation; // Gamma rNorm = Math.pow(Math.max(0, rNorm), settings.gamma); gNorm = Math.pow(Math.max(0, gNorm), settings.gamma); bNorm = Math.pow(Math.max(0, bNorm), settings.gamma); return { r: Math.max(0, Math.min(255, rNorm * 255)), g: Math.max(0, Math.min(255, gNorm * 255)), b: Math.max(0, Math.min(255, bNorm * 255)) }; } }