import React, { useRef, useEffect, useState } from 'react'; import { AlertTriangleIcon, DownloadIcon, ImageIcon, PublishIcon, SparklesIcon, LoaderIcon, ClipboardIcon, CheckIcon, LightbulbIcon, GoogleIcon, EditIcon, MegaphoneIcon, TagIcon, ChevronLeftIcon, ChevronRightIcon, TextQuoteIcon } from '@/components/icons'; import { compositionPresets } from '@/lib/compositions'; import type { TextPosition, AdCopy, SubtitleOutlineStyle, CompositionPreset, BrandData, PriceData, FeatureDetails } from '@/types'; import { positionOptions, subtitleOutlineOptions, priceColorOptions, pricePositionOptions, priceStyleOptions } from '@/lib/options'; interface ImageCanvasProps { imagesB64: string[] | null; textOverlay: string; compositionId: string; textPosition: TextPosition; subtitleOutline: SubtitleOutlineStyle; artStyles: string[]; isLoading: boolean; error: string | null; adCopy: AdCopy | null; isAdCopyLoading: boolean; adCopyError: string | null; onGenerateAds: () => void; brandData: BrandData; priceData: PriceData; featureDetails: FeatureDetails[] | null; // Setters for editing setTextOverlay: (value: string) => void; setCompositionId: (value: string) => void; setTextPosition: (value: TextPosition) => void; setSubtitleOutline: (value: SubtitleOutlineStyle) => void; setPriceData: (value: React.SetStateAction) => void; setFeatureDetails: (value: React.SetStateAction) => void; } const CANVAS_SIZE = 1080; // For Instagram post resolution // --- COLOR HELPER FUNCTIONS --- type RGB = { r: number; g: number; b: number; }; // Heavily simplified color quantization const getProminentColors = (image: HTMLImageElement, count = 5): RGB[] => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d', { willReadFrequently: true }); if (!ctx) return [{ r: 255, g: 255, b: 255 }, {r: 0, g: 0, b: 0}]; const scale = Math.min(100 / image.width, 100 / image.height); canvas.width = image.width * scale; canvas.height = image.height * scale; ctx.drawImage(image, 0, 0, canvas.width, canvas.height); const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; const colorCounts: { [key: string]: { color: RGB; count: number } } = {}; // Bucket colors to reduce dimensionality (8x8x8 cube) for (let i = 0; i < data.length; i += 4) { if(data[i+3] < 128) continue; // Skip transparent/semi-transparent pixels const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; const key = `${Math.round(r/32)}_${Math.round(g/32)}_${Math.round(b/32)}`; if (!colorCounts[key]) { colorCounts[key] = { color: { r, g, b }, count: 0 }; } colorCounts[key].count++; } const sortedColors = Object.values(colorCounts).sort((a, b) => b.count - a.count); return sortedColors.slice(0, count).map(c => c.color); } const rgbToHsl = ({r,g,b}: RGB): [number, number, number] => { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); let h = 0, s = 0, l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [h, s, l]; }; const getPalette = (image: HTMLImageElement, paletteType: string) => { const prominentColors = getProminentColors(image); const sortedByLuminance = [...prominentColors].sort((a, b) => { const lumA = 0.2126 * a.r + 0.7152 * a.g + 0.0722 * a.b; const lumB = 0.2126 * b.r + 0.7152 * b.g + 0.0722 * b.b; return lumB - lumA; }); const darkest = sortedByLuminance[sortedByLuminance.length - 1] || {r:0, g:0, b:0}; const lightest = sortedByLuminance[0] || {r:255, g:255, b:255}; const palette = { fill1: '#FFFFFF', fill2: '#E0E0E0', stroke: '#000000' }; switch(paletteType) { case 'light': palette.fill1 = `rgb(${lightest.r}, ${lightest.g}, ${lightest.b})`; palette.fill2 = `rgba(${(lightest.r + 200)/2}, ${(lightest.g + 200)/2}, ${(lightest.b + 200)/2}, 1)`; palette.stroke = `rgb(${darkest.r}, ${darkest.g}, ${darkest.b})`; break; case 'dark': palette.fill1 = `rgb(${darkest.r}, ${darkest.g}, ${darkest.b})`; palette.fill2 = `rgba(${(darkest.r + 50)/2}, ${(darkest.g + 50)/2}, ${(darkest.b + 50)/2}, 1)`; palette.stroke = `rgb(${lightest.r}, ${lightest.g}, ${lightest.b})`; break; case 'complementary': { const primary = prominentColors[0] || {r:128, g:128, b:128}; const [h, s, l] = rgbToHsl(primary); const compH = (h + 0.5) % 1; const compRgb = hslToRgb(compH, s, Math.max(0.5, l)); palette.fill1 = `rgb(${primary.r}, ${primary.g}, ${primary.b})`; palette.fill2 = `rgb(${compRgb.r}, ${compRgb.g}, ${compRgb.b})`; palette.stroke = `rgb(${lightest.r}, ${lightest.g}, ${lightest.b})`; break; } case 'analogous': { const primary = prominentColors[0] || {r:128, g:128, b:128}; const secondary = prominentColors[1] || primary; palette.fill1 = `rgb(${primary.r}, ${primary.g}, ${primary.b})`; palette.fill2 = `rgb(${secondary.r}, ${secondary.g}, ${secondary.b})`; palette.stroke = `rgb(${lightest.r}, ${lightest.g}, ${lightest.b})`; break; } default: palette.fill1 = `rgb(${lightest.r}, ${lightest.g}, ${lightest.b})`; palette.stroke = `rgb(${darkest.r}, ${darkest.g}, ${darkest.b})`; } // Ensure sufficient contrast for stroke const fillLum = 0.2126 * parseInt(palette.fill1.slice(1,3), 16) + 0.7152 * parseInt(palette.fill1.slice(3,5), 16) + 0.0722 * parseInt(palette.fill1.slice(5,7), 16); const strokeLum = 0.2126 * parseInt(palette.stroke.slice(1,3), 16) + 0.7152 * parseInt(palette.stroke.slice(3,5), 16) + 0.0722 * parseInt(palette.stroke.slice(5,7), 16); if(Math.abs(fillLum - strokeLum) < 50) { palette.stroke = strokeLum > 128 ? '#000000' : '#FFFFFF'; } return palette; } const hslToRgb = (h: number, s: number, l: number): RGB => { let r, g, b; if (s === 0) { r = g = b = l; } else { const hue2rgb = (p: number, q: number, t: number) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; }; // --- FONT HELPER --- const FONT_MAP: { [key: string]: { titleFont: string; subtitleFont: string } } = { 'default': { titleFont: "'Anton', sans-serif", subtitleFont: "'Poppins', sans-serif" }, 'Comic-book': { titleFont: "'Bangers', cursive", subtitleFont: "'Poppins', sans-serif" }, 'Meme': { titleFont: "'Bangers', cursive", subtitleFont: "'Poppins', sans-serif" }, 'Lobster': { titleFont: "'Lobster', cursive", subtitleFont: "'Poppins', sans-serif" }, 'Playfair Display': { titleFont: "'Playfair Display', serif", subtitleFont: "'Poppins', sans-serif" }, 'Old Money': { titleFont: "'Playfair Display', serif", subtitleFont: "'Poppins', sans-serif" }, 'Art Déco': { titleFont: "'Playfair Display', serif", subtitleFont: "'Poppins', sans-serif" }, 'Bauhaus': { titleFont: "'Poppins', sans-serif", subtitleFont: "'Poppins', sans-serif" }, 'Minimalista': { titleFont: "'Poppins', sans-serif", subtitleFont: "'Poppins', sans-serif" }, }; const getFontForStyle = (styles: string[]): { titleFont: string; subtitleFont: string } => { if (!styles || styles.length === 0) return FONT_MAP['default']; for (const style of styles) { for (const key in FONT_MAP) { if (style.includes(key)) { return FONT_MAP[key]; } } } return FONT_MAP['default']; }; // --- END FONT HELPER --- const getWrappedLines = (ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string[] => { const lines: string[] = []; if (!text) return lines; const paragraphs = text.split('\n'); paragraphs.forEach(paragraph => { const words = paragraph.split(' '); let currentLine = ''; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; if (ctx.measureText(testLine).width > maxWidth && currentLine) { lines.push(currentLine); currentLine = word; } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } }); return lines; }; const drawPriceTag = (ctx: CanvasRenderingContext2D, priceData: PriceData) => { if (!priceData || (!priceData.text.trim() && !priceData.modelText.trim()) || priceData.position === 'none') { return; } ctx.save(); const colorOption = priceColorOptions.find(c => c.id === priceData.color) || priceColorOptions[0]; ctx.fillStyle = colorOption.hex; ctx.strokeStyle = 'white'; ctx.lineWidth = 6; const priceText = priceData.text.trim(); const modelText = priceData.modelText.trim(); const priceFontSize = CANVAS_SIZE * 0.06; const modelFontSize = CANVAS_SIZE * 0.035; ctx.font = `900 ${priceFontSize}px 'Poppins', sans-serif`; const priceMetrics = ctx.measureText(priceText); ctx.font = `500 ${modelFontSize}px 'Poppins', sans-serif`; const modelMetrics = ctx.measureText(modelText); const textWidth = Math.max(priceMetrics.width, modelMetrics.width); const priceHeight = priceText ? priceFontSize : 0; const modelHeight = modelText ? modelFontSize : 0; const verticalPadding = priceFontSize * 0.1; const totalTextHeight = priceHeight + modelHeight + (priceText && modelText ? verticalPadding : 0); const horizontalPadding = priceFontSize * 0.5; const verticalPaddingForShape = priceFontSize * 0.4; let tagWidth, tagHeight; if (priceData.style === 'circle' || priceData.style === 'burst') { const diameter = Math.max(textWidth, totalTextHeight) + horizontalPadding * 2; tagWidth = diameter; tagHeight = diameter; } else { // 'tag' tagWidth = textWidth + horizontalPadding * 2; tagHeight = totalTextHeight + verticalPaddingForShape * 2; } const radius = tagWidth / 2; const margin = CANVAS_SIZE * 0.05; let x = 0, y = 0; // Center of the tag switch(priceData.position) { case 'top-left': x = margin + tagWidth / 2; y = margin + tagHeight / 2; break; case 'top-right': x = CANVAS_SIZE - margin - tagWidth / 2; y = margin + tagHeight / 2; break; case 'bottom-left': x = margin + tagWidth / 2; y = CANVAS_SIZE - margin - tagHeight / 2; break; case 'bottom-right': x = CANVAS_SIZE - margin - tagHeight / 2; y = CANVAS_SIZE - margin - tagHeight / 2; break; } // Draw shape ctx.beginPath(); switch(priceData.style) { case 'circle': ctx.arc(x, y, radius, 0, Math.PI * 2); break; case 'tag': ctx.rect(x - tagWidth/2, y - tagHeight/2, tagWidth, tagHeight); break; case 'burst': const points = 12; const inset = 0.7; ctx.translate(x, y); ctx.moveTo(0, 0 - radius); for (let i = 0; i < points; i++) { ctx.rotate(Math.PI / points); ctx.lineTo(0, 0 - (radius * inset)); ctx.rotate(Math.PI / points); ctx.lineTo(0, 0 - radius); } ctx.translate(-x, -y); break; } ctx.closePath(); ctx.stroke(); ctx.fill(); // Draw text ctx.fillStyle = 'white'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; if (priceText && modelText) { const priceY = y - (totalTextHeight / 2) + (priceHeight / 2); const modelY = priceY + (priceHeight / 2) + verticalPadding + (modelHeight / 2); ctx.font = `900 ${priceFontSize}px 'Poppins', sans-serif`; ctx.fillText(priceText, x, priceY); ctx.font = `500 ${modelFontSize}px 'Poppins', sans-serif`; ctx.fillText(modelText, x, modelY); } else { // Only one line of text const singleText = priceText || modelText; const singleFontSize = priceText ? priceFontSize : modelFontSize; const fontWeight = priceText ? '900' : '500'; ctx.font = `${fontWeight} ${singleFontSize}px 'Poppins', sans-serif`; ctx.fillText(singleText, x, y); } ctx.restore(); } const drawFeatureDetails = (ctx: CanvasRenderingContext2D, details: FeatureDetails[]) => { if (!details || details.length === 0) return; ctx.save(); const cornerPositions = [ { x: CANVAS_SIZE * 0.05, y: CANVAS_SIZE * 0.05, align: 'left' as const, baseline: 'top' as const }, { x: CANVAS_SIZE * 0.95, y: CANVAS_SIZE * 0.05, align: 'right' as const, baseline: 'top' as const }, { x: CANVAS_SIZE * 0.95, y: CANVAS_SIZE * 0.95, align: 'right' as const, baseline: 'bottom' as const }, { x: CANVAS_SIZE * 0.05, y: CANVAS_SIZE * 0.95, align: 'left' as const, baseline: 'bottom' as const }, ]; const maxWidth = CANVAS_SIZE * 0.4; details.slice(0, 4).forEach((detail, index) => { const pos = cornerPositions[index]; if (!pos) return; ctx.textAlign = pos.align; ctx.textBaseline = pos.baseline; const titleFontSize = CANVAS_SIZE * 0.025; const descFontSize = CANVAS_SIZE * 0.02; const lineHeight = 1.25; // Get wrapped lines for both title and description ctx.font = `700 ${titleFontSize}px 'Poppins', sans-serif`; const titleLines = getWrappedLines(ctx, detail.title, maxWidth); ctx.font = `400 ${descFontSize}px 'Poppins', sans-serif`; const descLines = getWrappedLines(ctx, detail.description, maxWidth); // Calculate block dimensions const blockWidth = Math.max( ...titleLines.map(line => ctx.measureText(line).width), ...descLines.map(line => ctx.measureText(line).width) ); const titleHeight = titleLines.length * titleFontSize; const descHeight = descLines.length * descFontSize * lineHeight; const spacing = titleFontSize * 0.25; const totalTextHeight = titleHeight + spacing + descHeight; const padding = titleFontSize * 0.75; const boxWidth = blockWidth + padding * 2; const boxHeight = totalTextHeight + padding * 2; let boxX = pos.align === 'left' ? pos.x : pos.x - boxWidth; let boxY = pos.baseline === 'top' ? pos.y : pos.y - boxHeight; // Draw the semi-transparent box ctx.fillStyle = 'rgba(0, 0, 0, 0.65)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect(boxX, boxY, boxWidth, boxHeight, [8]); ctx.fill(); ctx.stroke(); // Draw the text ctx.fillStyle = '#FFFFFF'; let currentY = boxY + padding; // Draw title ctx.font = `700 ${titleFontSize}px 'Poppins', sans-serif`; titleLines.forEach(line => { const lineX = pos.align === 'left' ? boxX + padding : boxX + boxWidth - padding; ctx.fillText(line, lineX, currentY); currentY += titleFontSize; }); currentY += spacing; // Draw description ctx.font = `400 ${descFontSize}px 'Poppins', sans-serif`; ctx.fillStyle = '#E0E0E0'; descLines.forEach(line => { const lineX = pos.align === 'left' ? boxX + padding : boxX + boxWidth - padding; ctx.fillText(line, lineX, currentY); currentY += descFontSize * lineHeight; }); }); ctx.restore(); }; const drawCanvas = ( ctx: CanvasRenderingContext2D, image: HTMLImageElement, text: string, compositionId: string, textPosition: TextPosition, subtitleOutline: SubtitleOutlineStyle, artStyles: string[], brandData: BrandData, priceData: PriceData, featureDetails: FeatureDetails[] | null, ) => { ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); ctx.drawImage(image, 0, 0, CANVAS_SIZE, CANVAS_SIZE); const isIsometricDetailsView = featureDetails && featureDetails.length > 0; const palette = getPalette(image, 'light'); // Use a default palette for brand name if (isIsometricDetailsView && text.trim()) { // Special drawing logic for the product name in "Details" view ctx.save(); const productName = text.split('\n')[0].toUpperCase(); const titleSize = CANVAS_SIZE * 0.03; // A bit larger for a title ctx.font = `700 ${titleSize}px 'Poppins', sans-serif`; const textPalette = getPalette(image, 'dark'); ctx.fillStyle = textPalette.fill1; ctx.strokeStyle = textPalette.stroke; ctx.lineWidth = titleSize * 0.1; ctx.lineJoin = 'round'; ctx.shadowColor = 'rgba(0, 0, 0, 0.6)'; ctx.shadowBlur = 4; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; const margin = CANVAS_SIZE * 0.05; const x = CANVAS_SIZE / 2; const y = margin; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.strokeText(productName, x, y); ctx.fillText(productName, x, y); ctx.restore(); } else if (text.trim()) { // Existing logic for all other text drawing let selectedPreset = compositionPresets.find(p => p.id === compositionId); if (!selectedPreset || compositionId === 'random') { const availablePresets = compositionPresets.filter(p => p.id !== 'random'); selectedPreset = availablePresets[Math.floor(Math.random() * availablePresets.length)]; } const preset = selectedPreset.config; const margin = CANVAS_SIZE * 0.07; const textPalette = getPalette(image, preset.style.palette); const { titleFont, subtitleFont } = getFontForStyle(artStyles); const textLines = text.split('\n'); const titleText = (textLines[0] || '').toUpperCase(); const subtitleText = preset.subtitle ? textLines.slice(1).join('\n') : ''; const maxTextWidth = (textPosition === 'left' || textPosition === 'right') ? CANVAS_SIZE * 0.4 : CANVAS_SIZE * 0.8; // --- Robust Font Size Calculation --- let optimalSize = 10; const maxTextHeight = CANVAS_SIZE * 0.8; for (let currentSize = 250; currentSize >= 10; currentSize -= 5) { // Check width constraints first for both title and subtitle ctx.font = `900 ${currentSize}px ${titleFont}`; const titleLinesForWidthCheck = getWrappedLines(ctx, titleText, maxTextWidth); const isTitleWidthOk = titleLinesForWidthCheck.every(line => ctx.measureText(line).width <= maxTextWidth); ctx.font = `500 ${currentSize * 0.4}px ${subtitleFont}`; const subtitleLinesForWidthCheck = getWrappedLines(ctx, subtitleText, maxTextWidth); const isSubtitleWidthOk = subtitleLinesForWidthCheck.every(line => ctx.measureText(line).width <= maxTextWidth); if (!isTitleWidthOk || !isSubtitleWidthOk) { continue; // Font size too large for width, try smaller } // If width is OK, check height const titleHeight = titleLinesForWidthCheck.length * currentSize * 1.1; const subtitleHeight = subtitleText ? subtitleLinesForWidthCheck.length * (currentSize * 0.4) * 1.2 : 0; const totalHeight = titleHeight + (subtitleHeight > 0 ? subtitleHeight + (currentSize * 0.2) : 0); if (totalHeight <= maxTextHeight) { optimalSize = currentSize; // This size fits both width and height break; } } // --- End Font Size Calculation --- ctx.save(); const titleSize = optimalSize; const subtitleSize = optimalSize * 0.4; if (preset.rotation) { const angle = (Math.random() * 4 - 2) * (Math.PI / 180); ctx.translate(CANVAS_SIZE / 2, CANVAS_SIZE / 2); ctx.rotate(angle); ctx.translate(-CANVAS_SIZE / 2, -CANVAS_SIZE / 2); } ctx.font = `900 ${titleSize}px ${titleFont}`; const titleLines = getWrappedLines(ctx, titleText, maxTextWidth); const titleBlockHeight = titleLines.length * titleSize * 1.1; ctx.font = `500 ${subtitleSize}px ${subtitleFont}`; const subtitleLines = getWrappedLines(ctx, subtitleText, maxTextWidth); const subtitleBlockHeight = subtitleText ? subtitleLines.length * subtitleSize * 1.2 : 0; const totalBlockHeight = titleBlockHeight + (subtitleBlockHeight > 0 ? subtitleBlockHeight + (titleSize * 0.2) : 0); let startX = 0, startY = 0; ctx.textBaseline = 'top'; switch (textPosition) { case 'top': startX = CANVAS_SIZE / 2; startY = margin; ctx.textAlign = 'center'; break; case 'top-right': startX = CANVAS_SIZE - margin; startY = margin; ctx.textAlign = 'right'; // Offset to avoid overlap with feature detail box in the same corner if (featureDetails && featureDetails.length > 0) { startY += CANVAS_SIZE * 0.15; } break; case 'bottom': startX = CANVAS_SIZE / 2; startY = CANVAS_SIZE - margin - totalBlockHeight; ctx.textAlign = 'center'; break; case 'left': startX = margin; startY = (CANVAS_SIZE / 2) - (totalBlockHeight / 2); ctx.textAlign = 'left'; break; case 'right': startX = CANVAS_SIZE - margin; startY = (CANVAS_SIZE / 2) - (totalBlockHeight / 2); ctx.textAlign = 'right'; break; case 'center': default: startX = CANVAS_SIZE / 2; startY = (CANVAS_SIZE / 2) - (totalBlockHeight / 2); ctx.textAlign = 'center'; break; } let currentY = startY; // Draw Title ctx.font = `900 ${titleSize}px ${titleFont}`; titleLines.forEach(line => { const textMetrics = ctx.measureText(line); let xPos = startX; if(ctx.textAlign === 'left') xPos = startX; if(ctx.textAlign === 'center') xPos = startX - textMetrics.width / 2; if(ctx.textAlign === 'right') xPos = startX - textMetrics.width; const drawX = ctx.textAlign === 'center' ? startX : startX; if (preset.style.background) { ctx.fillStyle = preset.style.background.color; const blockPadding = titleSize * (preset.style.background.padding || 0.1); ctx.fillRect( xPos - blockPadding, currentY - blockPadding, textMetrics.width + blockPadding * 2, titleSize * 1.1 + blockPadding * 2 ); } if (preset.style.name === 'gradient-on-block') { const gradient = ctx.createLinearGradient(xPos, currentY, xPos + textMetrics.width, currentY); gradient.addColorStop(0, textPalette.fill1); gradient.addColorStop(1, textPalette.fill2); ctx.fillStyle = gradient; } else { ctx.fillStyle = textPalette.fill1; } ctx.strokeStyle = textPalette.stroke; if (preset.style.forcedStroke) { ctx.strokeStyle = preset.style.forcedStroke; } if (preset.style.name === 'gradient-on-block') { ctx.lineWidth = titleSize * 0.04; } else { ctx.lineWidth = titleSize * 0.05; } const needsFill = ['fill', 'fill-stroke', 'gradient-on-block', 'vertical'].includes(preset.style.name); const needsStroke = ['stroke', 'fill-stroke', 'gradient-on-block'].includes(preset.style.name); if (needsFill) { ctx.fillText(line, drawX, currentY); } if (needsStroke) { ctx.strokeText(line, drawX, currentY); } currentY += titleSize * 1.1; }); // Draw Subtitle if (subtitleText) { currentY += titleSize * 0.2; ctx.font = `500 ${subtitleSize}px ${subtitleFont}`; ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.strokeStyle = 'transparent'; ctx.lineWidth = 0; ctx.fillStyle = textPalette.fill1; ctx.lineJoin = 'round'; switch (subtitleOutline) { case 'white': ctx.strokeStyle = 'white'; ctx.lineWidth = subtitleSize * 0.2; ctx.fillStyle = textPalette.stroke; break; case 'black': ctx.strokeStyle = 'black'; ctx.lineWidth = subtitleSize * 0.2; ctx.fillStyle = textPalette.fill1; break; case 'soft_shadow': ctx.shadowColor = 'rgba(0, 0, 0, 0.7)'; ctx.shadowBlur = subtitleSize * 0.1; ctx.shadowOffsetX = subtitleSize * 0.05; ctx.shadowOffsetY = subtitleSize * 0.05; ctx.fillStyle = textPalette.fill1; break; case 'transparent_box': ctx.fillStyle = textPalette.fill1; break; case 'auto': default: ctx.fillStyle = textPalette.fill1; ctx.strokeStyle = textPalette.stroke; ctx.lineWidth = subtitleSize * 0.15; break; } subtitleLines.forEach(line => { const drawX = ctx.textAlign === 'center' ? startX : startX; if (subtitleOutline === 'transparent_box') { const textMetrics = ctx.measureText(line); const textWidth = textMetrics.width; const textHeight = subtitleSize; const padding = subtitleSize * 0.25; let boxX; switch (ctx.textAlign) { case 'left': boxX = startX; break; case 'right': boxX = startX - textWidth; break; default: boxX = startX - textWidth / 2; break; } const boxRgb = textPalette.stroke.match(/\d+/g)?.map(Number) || [0, 0, 0]; ctx.fillStyle = `rgba(${boxRgb[0]}, ${boxRgb[1]}, ${boxRgb[2]}, 0.6)`; ctx.fillRect(boxX - padding, currentY - (padding / 2), textWidth + (padding * 2), textHeight + padding); ctx.fillStyle = textPalette.fill1; ctx.fillText(line, drawX, currentY); } else if (subtitleOutline === 'soft_shadow') { ctx.fillText(line, drawX, currentY); } else { ctx.strokeText(line, drawX, currentY); ctx.fillText(line, drawX, currentY); } currentY += subtitleSize * 1.2; }); } ctx.restore(); } // Draw Feature Details drawFeatureDetails(ctx, featureDetails || []); // Draw Price Tag drawPriceTag(ctx, priceData); // Draw brand name watermark if (brandData && brandData.name.trim()) { ctx.save(); const brandName = brandData.name.trim(); const brandSize = CANVAS_SIZE * 0.02; ctx.font = `600 ${brandSize}px 'Poppins', sans-serif`; // Use a semi-transparent version of the stroke color from the main palette const brandColor = palette.stroke.startsWith('#') ? `${palette.stroke}B3` // Append 70% opacity in hex : `rgba(0,0,0,0.7)`; // Default fallback ctx.fillStyle = brandColor; ctx.textAlign = 'right'; ctx.textBaseline = 'bottom'; const brandMargin = CANVAS_SIZE * 0.03; ctx.fillText(brandName, CANVAS_SIZE - brandMargin, CANVAS_SIZE - brandMargin); ctx.restore(); } }; const CopyButton: React.FC<{textToCopy: string}> = ({ textToCopy }) => { const [copied, setCopied] = useState(false); const handleCopy = () => { navigator.clipboard.writeText(textToCopy); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return ( ); }; const MarketingSuite: React.FC> = ({ adCopy, isAdCopyLoading, onGenerateAds, adCopyError }) => { if (isAdCopyLoading) { return (

Gerando textos de marketing...

); } if (adCopyError) { // Check for rate limit by looking for the specific phrase from our custom error. const isRateLimitError = adCopyError.includes("excedeu sua cota"); if (isRateLimitError) { return (

Limite Atingido

{adCopyError}

Aguarde o contador no botão principal zerar para tentar novamente.

); } return (

Erro ao Gerar Anúncios

{adCopyError}

); } if (adCopy) { return (

Dica de Estratégia

{adCopy.strategyTip}

Google Ads

{adCopy.google.headlines.map((text, i) => (
T{i+1}: {text}
))} {adCopy.google.descriptions.map((text, i) => (

D{i+1}: {text}

))}

Facebook Ads

Texto Principal:
{adCopy.facebook.primaryText}

Título: {adCopy.facebook.headline}
Descrição: {adCopy.facebook.description}
) } return (

Transforme sua Arte em Anúncios

Gere textos de marketing para Google e Facebook baseados na sua criação, otimizados para conversão.

) }; const FeatureDetailEditor: React.FC<{ details: FeatureDetails[] | null; setDetails: (value: React.SetStateAction) => void; }> = ({ details, setDetails }) => { if (!details) return null; const handleDetailChange = (index: number, field: keyof FeatureDetails, value: string) => { setDetails(currentDetails => { if (!currentDetails) return null; const newDetails = [...currentDetails]; newDetails[index] = { ...newDetails[index], [field]: value }; return newDetails; }); }; return (

Detalhes do Produto

{details.map((detail, index) => (
handleDetailChange(index, 'title', e.target.value)} placeholder={`Título do Detalhe ${index + 1}`} className="w-full p-2 bg-gray-50 rounded-md border border-gray-300 focus:ring-2 focus:ring-purple-500 focus:outline-none text-sm font-semibold" />