const ASPECTS = { '16:9': [1280, 720], '9:16': [1080, 1920], '1:1': [1080, 1080] }; const TEXT_COLORS = ['#ffffff', '#fde047', '#6c4cf1']; const PRESETS = [ { name: 'Violet', bg: ['#7c3aed', '#4c1d95'], accent: '#fde047' }, { name: 'Sunset', bg: ['#f97316', '#be123c'], accent: '#ffffff' }, { name: 'Ocean', bg: ['#0ea5e9', '#1e3a8a'], accent: '#fde047' }, { name: 'Midnight', bg: ['#1e293b', '#0f172a'], accent: '#6c4cf1' }, { name: 'Emerald', bg: ['#059669', '#064e3b'], accent: '#ffffff' }, ]; function wrapText(ctx, text, maxWidth) { const words = text.split(' '); const lines = []; let line = ''; for (const w of words) { const test = line ? line + ' ' + w : w; if (ctx.measureText(test).width > maxWidth && line) { lines.push(line); line = w; } else { line = test; } } if (line) lines.push(line); return lines; } function ThumbnailStudioModal({ idea, topic, onClose }) { const [imgSrc, setImgSrc] = React.useState(null); const [aspect, setAspect] = React.useState('16:9'); const [text, setText] = React.useState(idea?.text || topic || ''); const [preset, setPreset] = React.useState(PRESETS[0]); const [textColor, setTextColor] = React.useState('#ffffff'); const [textPos, setTextPos] = React.useState('bottom'); const [fontScale, setFontScale] = React.useState(1); const [watermark, setWatermark] = React.useState(true); const canvasRef = React.useRef(null); const imgElRef = React.useRef(null); const fileInputRef = React.useRef(null); function onFile(e) { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.onload = () => { imgElRef.current = img; setImgSrc(reader.result); }; img.src = reader.result; }; reader.readAsDataURL(file); } React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const [w, h] = ASPECTS[aspect]; canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, w, h); if (imgElRef.current) { const img = imgElRef.current; const scale = Math.max(w / img.width, h / img.height); const iw = img.width * scale, ih = img.height * scale; ctx.drawImage(img, (w - iw) / 2, (h - ih) / 2, iw, ih); ctx.fillStyle = 'rgba(0,0,0,0.18)'; ctx.fillRect(0, 0, w, h); } else { const grad = ctx.createLinearGradient(0, 0, w, h); grad.addColorStop(0, preset.bg[0]); grad.addColorStop(1, preset.bg[1]); ctx.fillStyle = grad; ctx.fillRect(0, 0, w, h); // subtle radial glow + grain-free vignette for depth, avoids flat/amateur look const glow = ctx.createRadialGradient(w * 0.5, h * 0.35, 0, w * 0.5, h * 0.35, w * 0.7); glow.addColorStop(0, 'rgba(255,255,255,0.16)'); glow.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = glow; ctx.fillRect(0, 0, w, h); const vig = ctx.createRadialGradient(w / 2, h / 2, h * 0.35, w / 2, h / 2, h * 0.85); vig.addColorStop(0, 'rgba(0,0,0,0)'); vig.addColorStop(1, 'rgba(0,0,0,0.35)'); ctx.fillStyle = vig; ctx.fillRect(0, 0, w, h); } const scrimH = h * 0.42; const scrimGrad = ctx.createLinearGradient(0, textPos === 'top' ? 0 : h - scrimH, 0, textPos === 'top' ? scrimH : h); scrimGrad.addColorStop(0, textPos === 'top' ? 'rgba(0,0,0,0.65)' : 'rgba(0,0,0,0)'); scrimGrad.addColorStop(1, textPos === 'top' ? 'rgba(0,0,0,0)' : 'rgba(0,0,0,0.65)'); ctx.fillStyle = scrimGrad; ctx.fillRect(0, textPos === 'top' ? 0 : h - scrimH, w, scrimH); const fontSize = Math.round(w * 0.085 * fontScale); ctx.font = `900 ${fontSize}px Inter, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const lines = wrapText(ctx, text.toUpperCase(), w * 0.84); const lineH = fontSize * 1.12; const totalH = lines.length * lineH; let startY = textPos === 'top' ? fontSize + 24 : textPos === 'center' ? h / 2 - totalH / 2 + lineH / 2 : h - totalH - 36 + lineH / 2; lines.forEach((line, i) => { const y = startY + i * lineH; ctx.save(); ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = fontSize * 0.18; ctx.shadowOffsetY = fontSize * 0.05; ctx.lineWidth = fontSize * 0.09; ctx.strokeStyle = 'rgba(0,0,0,0.7)'; ctx.strokeText(line, w / 2, y); ctx.shadowColor = 'transparent'; ctx.fillStyle = textColor; ctx.fillText(line, w / 2, y); ctx.restore(); }); // accent underline bar for a designed, non-generic look if (lines.length) { const barW = w * 0.14, barH = Math.max(4, fontSize * 0.07); const barY = startY + (lines.length - 1) * lineH + lineH * 0.62; ctx.fillStyle = preset.accent; ctx.fillRect(w / 2 - barW / 2, barY, barW, barH); } if (watermark) { ctx.font = `700 ${Math.round(w * 0.028)}px Inter, sans-serif`; ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(255,255,255,0.85)'; ctx.strokeStyle = 'rgba(0,0,0,0.4)'; ctx.lineWidth = 2; const wmText = '#Tagly'; ctx.strokeText(wmText, w - 24, h - 22); ctx.fillText(wmText, w - 24, h - 22); } }, [imgSrc, aspect, text, preset, textColor, textPos, fontScale, watermark]); function download() { const canvas = canvasRef.current; const url = canvas.toDataURL('image/png'); const a = document.createElement('a'); a.href = url; a.download = `tagly-thumbnail-${(topic || 'thumb').replace(/\s+/g, '-')}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } return (