diff --git a/app.js b/app.js index f77954c..ce9bee1 100644 --- a/app.js +++ b/app.js @@ -65,6 +65,16 @@ const state = { headlineUnderline: false, headlineStrikethrough: false, headlineColor: '#ffffff', + languageSettings: { + en: { + headlineSize: 100, + subheadlineSize: 50, + position: 'top', + offsetY: 12, + lineHeight: 110 + } + }, + currentLayoutLang: 'en', position: 'top', offsetY: 12, lineHeight: 110, @@ -84,6 +94,8 @@ const state = { } }; +const baseTextDefaults = JSON.parse(JSON.stringify(state.defaults.text)); + // Helper functions to get/set current screenshot settings function getCurrentScreenshot() { if (state.screenshots.length === 0) return null; @@ -102,7 +114,63 @@ function getScreenshotSettings() { function getText() { const screenshot = getCurrentScreenshot(); - return screenshot ? screenshot.text : state.defaults.text; + if (screenshot) { + screenshot.text = normalizeTextSettings(screenshot.text); + return screenshot.text; + } + state.defaults.text = normalizeTextSettings(state.defaults.text); + return state.defaults.text; +} + +function getTextLayoutLanguage(text) { + if (text.currentLayoutLang) return text.currentLayoutLang; + if (text.headlineEnabled !== false) return text.currentHeadlineLang || 'en'; + if (text.subheadlineEnabled) return text.currentSubheadlineLang || 'en'; + return text.currentHeadlineLang || text.currentSubheadlineLang || 'en'; +} + +function getTextLanguageSettings(text, lang) { + if (!text.languageSettings) text.languageSettings = {}; + if (!text.languageSettings[lang]) { + const sourceLang = text.currentLayoutLang || text.currentHeadlineLang || text.currentSubheadlineLang || 'en'; + const sourceSettings = text.languageSettings[sourceLang]; + text.languageSettings[lang] = { + headlineSize: sourceSettings ? sourceSettings.headlineSize : (text.headlineSize || 100), + subheadlineSize: sourceSettings ? sourceSettings.subheadlineSize : (text.subheadlineSize || 50), + position: sourceSettings ? sourceSettings.position : (text.position || 'top'), + offsetY: sourceSettings ? sourceSettings.offsetY : (typeof text.offsetY === 'number' ? text.offsetY : 12), + lineHeight: sourceSettings ? sourceSettings.lineHeight : (text.lineHeight || 110) + }; + } + return text.languageSettings[lang]; +} + +function normalizeTextSettings(text) { + const merged = JSON.parse(JSON.stringify(baseTextDefaults)); + if (text) { + Object.assign(merged, text); + if (text.languageSettings) { + merged.languageSettings = JSON.parse(JSON.stringify(text.languageSettings)); + } + } + + merged.headlines = merged.headlines || { en: '' }; + merged.headlineLanguages = merged.headlineLanguages || ['en']; + merged.currentHeadlineLang = merged.currentHeadlineLang || merged.headlineLanguages[0] || 'en'; + merged.currentLayoutLang = merged.currentLayoutLang || merged.currentHeadlineLang || 'en'; + + merged.subheadlines = merged.subheadlines || { en: '' }; + merged.subheadlineLanguages = merged.subheadlineLanguages || ['en']; + merged.currentSubheadlineLang = merged.currentSubheadlineLang || merged.subheadlineLanguages[0] || 'en'; + + if (!merged.languageSettings) merged.languageSettings = {}; + const languages = new Set([...merged.headlineLanguages, ...merged.subheadlineLanguages]); + if (languages.size === 0) languages.add('en'); + languages.forEach((lang) => { + getTextLanguageSettings(merged, lang); + }); + + return merged; } // Format number to at most 1 decimal place @@ -1311,6 +1379,16 @@ function resetStateToDefaults() { headlineUnderline: false, headlineStrikethrough: false, headlineColor: '#ffffff', + languageSettings: { + en: { + headlineSize: 100, + subheadlineSize: 50, + position: 'top', + offsetY: 12, + lineHeight: 110 + } + }, + currentLayoutLang: 'en', position: 'top', offsetY: 12, lineHeight: 110, @@ -1491,11 +1569,17 @@ function syncUIWithState() { document.getElementById('frame-opacity-value').textContent = formatValue(ss.frame.opacity) + '%'; // Text - const currentHeadline = txt.headlines ? (txt.headlines[txt.currentHeadlineLang || 'en'] || '') : (txt.headline || ''); + const headlineLang = txt.currentHeadlineLang || 'en'; + const subheadlineLang = txt.currentSubheadlineLang || 'en'; + const layoutLang = getTextLayoutLanguage(txt); + const headlineLayout = getTextLanguageSettings(txt, headlineLang); + const subheadlineLayout = getTextLanguageSettings(txt, subheadlineLang); + const layoutSettings = getTextLanguageSettings(txt, layoutLang); + const currentHeadline = txt.headlines ? (txt.headlines[headlineLang] || '') : (txt.headline || ''); document.getElementById('headline-text').value = currentHeadline; document.getElementById('headline-font').value = txt.headlineFont; updateFontPickerPreview(); - document.getElementById('headline-size').value = txt.headlineSize; + document.getElementById('headline-size').value = headlineLayout.headlineSize; document.getElementById('headline-color').value = txt.headlineColor; document.getElementById('headline-weight').value = txt.headlineWeight; // Sync text style buttons @@ -1505,16 +1589,16 @@ function syncUIWithState() { btn.classList.toggle('active', txt[key] || false); }); document.querySelectorAll('#text-position button').forEach(btn => { - btn.classList.toggle('active', btn.dataset.position === txt.position); + btn.classList.toggle('active', btn.dataset.position === layoutSettings.position); }); - document.getElementById('text-offset-y').value = txt.offsetY; - document.getElementById('text-offset-y-value').textContent = formatValue(txt.offsetY) + '%'; - document.getElementById('line-height').value = txt.lineHeight; - document.getElementById('line-height-value').textContent = formatValue(txt.lineHeight) + '%'; - const currentSubheadline = txt.subheadlines ? (txt.subheadlines[txt.currentSubheadlineLang || 'en'] || '') : (txt.subheadline || ''); + document.getElementById('text-offset-y').value = layoutSettings.offsetY; + document.getElementById('text-offset-y-value').textContent = formatValue(layoutSettings.offsetY) + '%'; + document.getElementById('line-height').value = layoutSettings.lineHeight; + document.getElementById('line-height-value').textContent = formatValue(layoutSettings.lineHeight) + '%'; + const currentSubheadline = txt.subheadlines ? (txt.subheadlines[subheadlineLang] || '') : (txt.subheadline || ''); document.getElementById('subheadline-text').value = currentSubheadline; document.getElementById('subheadline-font').value = txt.subheadlineFont || txt.headlineFont; - document.getElementById('subheadline-size').value = txt.subheadlineSize; + document.getElementById('subheadline-size').value = subheadlineLayout.subheadlineSize; document.getElementById('subheadline-color').value = txt.subheadlineColor; document.getElementById('subheadline-opacity').value = txt.subheadlineOpacity; document.getElementById('subheadline-opacity-value').textContent = formatValue(txt.subheadlineOpacity) + '%'; @@ -2345,7 +2429,9 @@ function setupEventListeners() { // Font picker is initialized separately via initFontPicker() document.getElementById('headline-size').addEventListener('input', (e) => { - setTextValue('headlineSize', parseInt(e.target.value) || 100); + const text = getTextSettings(); + const lang = text.currentHeadlineLang || 'en'; + setTextLanguageValue('headlineSize', parseInt(e.target.value) || 100, lang); updateCanvas(); }); @@ -2376,19 +2462,19 @@ function setupEventListeners() { btn.addEventListener('click', () => { document.querySelectorAll('#text-position button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); - setTextValue('position', btn.dataset.position); + setTextLanguageValue('position', btn.dataset.position); updateCanvas(); }); }); document.getElementById('text-offset-y').addEventListener('input', (e) => { - setTextValue('offsetY', parseInt(e.target.value)); + setTextLanguageValue('offsetY', parseInt(e.target.value)); document.getElementById('text-offset-y-value').textContent = formatValue(e.target.value) + '%'; updateCanvas(); }); document.getElementById('line-height').addEventListener('input', (e) => { - setTextValue('lineHeight', parseInt(e.target.value)); + setTextLanguageValue('lineHeight', parseInt(e.target.value)); document.getElementById('line-height-value').textContent = formatValue(e.target.value) + '%'; updateCanvas(); }); @@ -2401,7 +2487,9 @@ function setupEventListeners() { }); document.getElementById('subheadline-size').addEventListener('input', (e) => { - setTextValue('subheadlineSize', parseInt(e.target.value) || 50); + const text = getTextSettings(); + const lang = text.currentSubheadlineLang || 'en'; + setTextLanguageValue('subheadlineSize', parseInt(e.target.value) || 50, lang); updateCanvas(); }); @@ -2789,18 +2877,20 @@ function removeSubheadlineLanguage(lang) { function switchHeadlineLanguage(lang) { const text = getTextSettings(); text.currentHeadlineLang = lang; + text.currentLayoutLang = lang; - // Load text for this language - document.getElementById('headline-text').value = text.headlines[lang] || ''; + // Sync text inputs and layout controls for this language + updateTextUI(text); updateCanvas(); } function switchSubheadlineLanguage(lang) { const text = getTextSettings(); text.currentSubheadlineLang = lang; + text.currentLayoutLang = lang; - // Load text for this language - document.getElementById('subheadline-text').value = text.subheadlines[lang] || ''; + // Sync text inputs and layout controls for this language + updateTextUI(text); updateCanvas(); } @@ -3643,6 +3733,14 @@ function setTextValue(key, value) { setTextSetting(key, value); } +function setTextLanguageValue(key, value, lang = null) { + const text = getTextSettings(); + const targetLang = lang || getTextLayoutLanguage(text); + const settings = getTextLanguageSettings(text, targetLang); + settings[key] = value; + text.currentLayoutLang = targetLang; +} + // Helper function to get text settings for current screenshot function getTextSettings() { return getText(); @@ -3660,10 +3758,19 @@ function loadTextUIFromGlobal() { // Update all text UI elements function updateTextUI(text) { - document.getElementById('headline-text').value = text.headline || ''; + const headlineLang = text.currentHeadlineLang || 'en'; + const subheadlineLang = text.currentSubheadlineLang || 'en'; + const layoutLang = getTextLayoutLanguage(text); + const headlineLayout = getTextLanguageSettings(text, headlineLang); + const subheadlineLayout = getTextLanguageSettings(text, subheadlineLang); + const layoutSettings = getTextLanguageSettings(text, layoutLang); + const headlineText = text.headlines ? (text.headlines[headlineLang] || '') : (text.headline || ''); + const subheadlineText = text.subheadlines ? (text.subheadlines[subheadlineLang] || '') : (text.subheadline || ''); + + document.getElementById('headline-text').value = headlineText; document.getElementById('headline-font').value = text.headlineFont; updateFontPickerPreview(); - document.getElementById('headline-size').value = text.headlineSize; + document.getElementById('headline-size').value = headlineLayout.headlineSize; document.getElementById('headline-color').value = text.headlineColor; document.getElementById('headline-weight').value = text.headlineWeight; // Sync text style buttons @@ -3673,15 +3780,15 @@ function updateTextUI(text) { btn.classList.toggle('active', text[key] || false); }); document.querySelectorAll('#text-position button').forEach(btn => { - btn.classList.toggle('active', btn.dataset.position === text.position); + btn.classList.toggle('active', btn.dataset.position === layoutSettings.position); }); - document.getElementById('text-offset-y').value = text.offsetY; - document.getElementById('text-offset-y-value').textContent = formatValue(text.offsetY) + '%'; - document.getElementById('line-height').value = text.lineHeight; - document.getElementById('line-height-value').textContent = formatValue(text.lineHeight) + '%'; - document.getElementById('subheadline-text').value = text.subheadline || ''; + document.getElementById('text-offset-y').value = layoutSettings.offsetY; + document.getElementById('text-offset-y-value').textContent = formatValue(layoutSettings.offsetY) + '%'; + document.getElementById('line-height').value = layoutSettings.lineHeight; + document.getElementById('line-height-value').textContent = formatValue(layoutSettings.lineHeight) + '%'; + document.getElementById('subheadline-text').value = subheadlineText; document.getElementById('subheadline-font').value = text.subheadlineFont || text.headlineFont; - document.getElementById('subheadline-size').value = text.subheadlineSize; + document.getElementById('subheadline-size').value = subheadlineLayout.subheadlineSize; document.getElementById('subheadline-color').value = text.subheadlineColor; document.getElementById('subheadline-opacity').value = text.subheadlineOpacity; document.getElementById('subheadline-opacity-value').textContent = formatValue(text.subheadlineOpacity) + '%'; @@ -3883,6 +3990,9 @@ function createNewScreenshot(img, src, name, lang, deviceType) { addProjectLanguage(lang); } + const textDefaults = normalizeTextSettings(state.defaults.text); + state.defaults.text = textDefaults; + // Each screenshot gets its own copy of all settings from defaults state.screenshots.push({ image: img, // Keep for legacy compatibility @@ -3891,7 +4001,7 @@ function createNewScreenshot(img, src, name, lang, deviceType) { localizedImages: localizedImages, background: JSON.parse(JSON.stringify(state.defaults.background)), screenshot: JSON.parse(JSON.stringify(state.defaults.screenshot)), - text: JSON.parse(JSON.stringify(state.defaults.text)), + text: JSON.parse(JSON.stringify(textDefaults)), // Legacy overrides for backwards compatibility overrides: {} }); @@ -4899,32 +5009,39 @@ function drawTextToContext(context, dims, txt) { const headlineEnabled = txt.headlineEnabled !== false; const subheadlineEnabled = txt.subheadlineEnabled || false; - const headline = headlineEnabled && txt.headlines ? (txt.headlines[txt.currentHeadlineLang || 'en'] || '') : ''; - const subheadline = subheadlineEnabled && txt.subheadlines ? (txt.subheadlines[txt.currentSubheadlineLang || 'en'] || '') : ''; + const headlineLang = txt.currentHeadlineLang || 'en'; + const subheadlineLang = txt.currentSubheadlineLang || 'en'; + const layoutLang = getTextLayoutLanguage(txt); + const headlineLayout = getTextLanguageSettings(txt, headlineLang); + const subheadlineLayout = getTextLanguageSettings(txt, subheadlineLang); + const layoutSettings = getTextLanguageSettings(txt, layoutLang); + + const headline = headlineEnabled && txt.headlines ? (txt.headlines[headlineLang] || '') : ''; + const subheadline = subheadlineEnabled && txt.subheadlines ? (txt.subheadlines[subheadlineLang] || '') : ''; if (!headline && !subheadline) return; const padding = dims.width * 0.08; - const textY = txt.position === 'top' - ? dims.height * (txt.offsetY / 100) - : dims.height * (1 - txt.offsetY / 100); + const textY = layoutSettings.position === 'top' + ? dims.height * (layoutSettings.offsetY / 100) + : dims.height * (1 - layoutSettings.offsetY / 100); context.textAlign = 'center'; - context.textBaseline = txt.position === 'top' ? 'top' : 'bottom'; + context.textBaseline = layoutSettings.position === 'top' ? 'top' : 'bottom'; let currentY = textY; // Draw headline if (headline) { const fontStyle = txt.headlineItalic ? 'italic' : 'normal'; - context.font = `${fontStyle} ${txt.headlineWeight} ${txt.headlineSize}px ${txt.headlineFont}`; + context.font = `${fontStyle} ${txt.headlineWeight} ${headlineLayout.headlineSize}px ${txt.headlineFont}`; context.fillStyle = txt.headlineColor; const lines = wrapText(context, headline, dims.width - padding * 2); - const lineHeight = txt.headlineSize * (txt.lineHeight / 100); + const lineHeight = headlineLayout.headlineSize * (layoutSettings.lineHeight / 100); // For bottom positioning, offset currentY so lines draw correctly - if (txt.position === 'bottom') { + if (layoutSettings.position === 'bottom') { currentY -= (lines.length - 1) * lineHeight; } @@ -4936,13 +5053,13 @@ function drawTextToContext(context, dims, txt) { // Calculate text metrics for decorations const textWidth = context.measureText(line).width; - const fontSize = txt.headlineSize; + const fontSize = headlineLayout.headlineSize; const lineThickness = Math.max(2, fontSize * 0.05); const x = dims.width / 2 - textWidth / 2; // Draw underline if (txt.headlineUnderline) { - const underlineY = txt.position === 'top' + const underlineY = layoutSettings.position === 'top' ? y + fontSize * 0.9 : y + fontSize * 0.1; context.fillRect(x, underlineY, textWidth, lineThickness); @@ -4950,7 +5067,7 @@ function drawTextToContext(context, dims, txt) { // Draw strikethrough if (txt.headlineStrikethrough) { - const strikeY = txt.position === 'top' + const strikeY = layoutSettings.position === 'top' ? y + fontSize * 0.4 : y - fontSize * 0.4; context.fillRect(x, strikeY, textWidth, lineThickness); @@ -4960,10 +5077,10 @@ function drawTextToContext(context, dims, txt) { // Track where subheadline should start (below the bottom edge of headline) // The gap between headline and subheadline should be (lineHeight - fontSize) // This is the "extra" spacing beyond the text itself - const gap = lineHeight - txt.headlineSize; - if (txt.position === 'top') { + const gap = lineHeight - headlineLayout.headlineSize; + if (layoutSettings.position === 'top') { // For top: lastLineY is top of last line, add fontSize to get bottom, then add gap - currentY = lastLineY + txt.headlineSize + gap; + currentY = lastLineY + headlineLayout.headlineSize + gap; } else { // For bottom: lastLineY is already the bottom of last line, just add gap currentY = lastLineY + gap; @@ -4974,16 +5091,16 @@ function drawTextToContext(context, dims, txt) { if (subheadline) { const subFontStyle = txt.subheadlineItalic ? 'italic' : 'normal'; const subWeight = txt.subheadlineWeight || '400'; - context.font = `${subFontStyle} ${subWeight} ${txt.subheadlineSize}px ${txt.subheadlineFont || txt.headlineFont}`; + context.font = `${subFontStyle} ${subWeight} ${subheadlineLayout.subheadlineSize}px ${txt.subheadlineFont || txt.headlineFont}`; context.fillStyle = hexToRgba(txt.subheadlineColor, txt.subheadlineOpacity / 100); const lines = wrapText(context, subheadline, dims.width - padding * 2); - const subLineHeight = txt.subheadlineSize * 1.4; + const subLineHeight = subheadlineLayout.subheadlineSize * 1.4; // Subheadline starts after headline with gap determined by headline lineHeight // For bottom position, switch to 'top' baseline so subheadline draws downward const subY = currentY; - if (txt.position === 'bottom') { + if (layoutSettings.position === 'bottom') { context.textBaseline = 'top'; } @@ -4993,7 +5110,7 @@ function drawTextToContext(context, dims, txt) { // Calculate text metrics for decorations const textWidth = context.measureText(line).width; - const fontSize = txt.subheadlineSize; + const fontSize = subheadlineLayout.subheadlineSize; const lineThickness = Math.max(2, fontSize * 0.05); const x = dims.width / 2 - textWidth / 2; @@ -5011,7 +5128,7 @@ function drawTextToContext(context, dims, txt) { }); // Restore baseline if we changed it - if (txt.position === 'bottom') { + if (layoutSettings.position === 'bottom') { context.textBaseline = 'bottom'; } } @@ -5206,32 +5323,39 @@ function drawText() { const headlineEnabled = text.headlineEnabled !== false; const subheadlineEnabled = text.subheadlineEnabled || false; + const headlineLang = text.currentHeadlineLang || 'en'; + const subheadlineLang = text.currentSubheadlineLang || 'en'; + const layoutLang = getTextLayoutLanguage(text); + const headlineLayout = getTextLanguageSettings(text, headlineLang); + const subheadlineLayout = getTextLanguageSettings(text, subheadlineLang); + const layoutSettings = getTextLanguageSettings(text, layoutLang); + // Get current language text (only if enabled) - const headline = headlineEnabled && text.headlines ? (text.headlines[text.currentHeadlineLang || 'en'] || '') : ''; - const subheadline = subheadlineEnabled && text.subheadlines ? (text.subheadlines[text.currentSubheadlineLang || 'en'] || '') : ''; + const headline = headlineEnabled && text.headlines ? (text.headlines[headlineLang] || '') : ''; + const subheadline = subheadlineEnabled && text.subheadlines ? (text.subheadlines[subheadlineLang] || '') : ''; if (!headline && !subheadline) return; const padding = dims.width * 0.08; - const textY = text.position === 'top' - ? dims.height * (text.offsetY / 100) - : dims.height * (1 - text.offsetY / 100); + const textY = layoutSettings.position === 'top' + ? dims.height * (layoutSettings.offsetY / 100) + : dims.height * (1 - layoutSettings.offsetY / 100); ctx.textAlign = 'center'; - ctx.textBaseline = text.position === 'top' ? 'top' : 'bottom'; + ctx.textBaseline = layoutSettings.position === 'top' ? 'top' : 'bottom'; let currentY = textY; // Draw headline if (headline) { const fontStyle = text.headlineItalic ? 'italic' : 'normal'; - ctx.font = `${fontStyle} ${text.headlineWeight} ${text.headlineSize}px ${text.headlineFont}`; + ctx.font = `${fontStyle} ${text.headlineWeight} ${headlineLayout.headlineSize}px ${text.headlineFont}`; ctx.fillStyle = text.headlineColor; const lines = wrapText(ctx, headline, dims.width - padding * 2); - const lineHeight = text.headlineSize * (text.lineHeight / 100); + const lineHeight = headlineLayout.headlineSize * (layoutSettings.lineHeight / 100); - if (text.position === 'bottom') { + if (layoutSettings.position === 'bottom') { currentY -= (lines.length - 1) * lineHeight; } @@ -5244,13 +5368,13 @@ function drawText() { // Calculate text metrics for decorations // When textBaseline is 'top', y is at top of text; when 'bottom', y is at bottom const textWidth = ctx.measureText(line).width; - const fontSize = text.headlineSize; + const fontSize = headlineLayout.headlineSize; const lineThickness = Math.max(2, fontSize * 0.05); const x = dims.width / 2 - textWidth / 2; // Draw underline if (text.headlineUnderline) { - const underlineY = text.position === 'top' + const underlineY = layoutSettings.position === 'top' ? y + fontSize * 0.9 // Below text when baseline is top : y + fontSize * 0.1; // Below text when baseline is bottom ctx.fillRect(x, underlineY, textWidth, lineThickness); @@ -5258,7 +5382,7 @@ function drawText() { // Draw strikethrough if (text.headlineStrikethrough) { - const strikeY = text.position === 'top' + const strikeY = layoutSettings.position === 'top' ? y + fontSize * 0.4 // Middle of text when baseline is top : y - fontSize * 0.4; // Middle of text when baseline is bottom ctx.fillRect(x, strikeY, textWidth, lineThickness); @@ -5268,10 +5392,10 @@ function drawText() { // Track where subheadline should start (below the bottom edge of headline) // The gap between headline and subheadline should be (lineHeight - fontSize) // This is the "extra" spacing beyond the text itself - const gap = lineHeight - text.headlineSize; - if (text.position === 'top') { + const gap = lineHeight - headlineLayout.headlineSize; + if (layoutSettings.position === 'top') { // For top: lastLineY is top of last line, add fontSize to get bottom, then add gap - currentY = lastLineY + text.headlineSize + gap; + currentY = lastLineY + headlineLayout.headlineSize + gap; } else { // For bottom: lastLineY is already the bottom of last line, just add gap currentY = lastLineY + gap; @@ -5282,16 +5406,16 @@ function drawText() { if (subheadline) { const subFontStyle = text.subheadlineItalic ? 'italic' : 'normal'; const subWeight = text.subheadlineWeight || '400'; - ctx.font = `${subFontStyle} ${subWeight} ${text.subheadlineSize}px ${text.subheadlineFont || text.headlineFont}`; + ctx.font = `${subFontStyle} ${subWeight} ${subheadlineLayout.subheadlineSize}px ${text.subheadlineFont || text.headlineFont}`; ctx.fillStyle = hexToRgba(text.subheadlineColor, text.subheadlineOpacity / 100); const lines = wrapText(ctx, subheadline, dims.width - padding * 2); - const subLineHeight = text.subheadlineSize * 1.4; + const subLineHeight = subheadlineLayout.subheadlineSize * 1.4; // Subheadline starts after headline with gap determined by headline lineHeight // For bottom position, switch to 'top' baseline so subheadline draws downward const subY = currentY; - if (text.position === 'bottom') { + if (layoutSettings.position === 'bottom') { ctx.textBaseline = 'top'; } @@ -5301,7 +5425,7 @@ function drawText() { // Calculate text metrics for decorations const textWidth = ctx.measureText(line).width; - const fontSize = text.subheadlineSize; + const fontSize = subheadlineLayout.subheadlineSize; const lineThickness = Math.max(2, fontSize * 0.05); const x = dims.width / 2 - textWidth / 2; @@ -5319,7 +5443,7 @@ function drawText() { }); // Restore baseline if we changed it - if (text.position === 'bottom') { + if (layoutSettings.position === 'bottom') { ctx.textBaseline = 'bottom'; } } @@ -5355,25 +5479,35 @@ function roundRect(ctx, x, y, width, height, radius) { } function wrapText(ctx, text, maxWidth) { - const words = text.split(' '); const lines = []; - let currentLine = ''; + const rawLines = String(text).split(/\r?\n/); - words.forEach(word => { - const testLine = currentLine + (currentLine ? ' ' : '') + word; - const metrics = ctx.measureText(testLine); + rawLines.forEach((rawLine) => { + if (rawLine === '') { + lines.push(''); + return; + } - if (metrics.width > maxWidth && currentLine) { + const words = rawLine.split(' '); + let currentLine = ''; + + words.forEach(word => { + const testLine = currentLine + (currentLine ? ' ' : '') + word; + const metrics = ctx.measureText(testLine); + + if (metrics.width > maxWidth && currentLine) { + lines.push(currentLine); + currentLine = word; + } else { + currentLine = testLine; + } + }); + + if (currentLine) { lines.push(currentLine); - currentLine = word; - } else { - currentLine = testLine; } - }); - if (currentLine) { - lines.push(currentLine); - } + }); return lines; } diff --git a/llm.js b/llm.js index 16e752f..dd1cc44 100644 --- a/llm.js +++ b/llm.js @@ -32,6 +32,8 @@ const llmProviders = { storageKey: 'googleApiKey', modelStorageKey: 'googleModel', models: [ + { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash (Preview) ($$)' }, + { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro (Preview) ($$$)' }, { id: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash-Lite ($)' }, { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash ($$)' }, { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro ($$$)' }