From 995cd505d32b32bf14d02ecd609bca1e7099d0ff Mon Sep 17 00:00:00 2001 From: amemya Date: Tue, 7 Jul 2026 11:34:21 +0900 Subject: [PATCH 1/7] fix: improve margin calculation and image alignment logic in canvas --- frontend/src/canvas.ts | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/frontend/src/canvas.ts b/frontend/src/canvas.ts index f53fea1..82da7f5 100644 --- a/frontend/src/canvas.ts +++ b/frontend/src/canvas.ts @@ -20,10 +20,11 @@ export function renderImageToCanvas( exif: ExifData, settings: RenderSettings ) { - // 好みの左右・上の枠の最小太さ(例:幅の2.5%) - const minFramePadding = Math.floor(img.width * 0.025); + // 画像の長辺を基準に余白を計算 + const baseSize = Math.max(img.width, img.height); + const minFramePadding = Math.floor(baseSize * 0.05); // 長辺の5% // 下部のテキスト領域に必要な最小スペース - const minBottomSpace = Math.floor(minFramePadding * 4.5); + const minTextSpace = Math.floor(minFramePadding * 2.5); let targetRatio = 4300 / 3618; if (settings.aspectRatioPreset === "custom") { @@ -45,7 +46,11 @@ export function renderImageToCanvas( } const minCanvasWidth = img.width + (minFramePadding * 2); - const minCanvasHeight = img.height + minFramePadding + minBottomSpace; + + // 配置設定(Top/Center)に関わらず、常に共通の最小キャンバス高さを要求する。 + // これにより設定変更によるキャンバス全体の拡大縮小(画像の相対スケール変化)を防ぐ。 + const requiredMarginY = minFramePadding + minTextSpace; + const minCanvasHeight = img.height + (requiredMarginY * 2); // まず幅を基準に高さを計算 let finalCanvasWidth = minCanvasWidth; @@ -61,12 +66,17 @@ export function renderImageToCanvas( canvas.width = finalCanvasWidth; canvas.height = finalCanvasHeight; - // 余分な高さを計算 - const extraHeight = finalCanvasHeight - minCanvasHeight; - - // 画像の配置位置を計算 (左右中央、上固定または上下中央) + // 画像の配置位置を計算 const drawX = Math.floor((finalCanvasWidth - img.width) / 2); - const drawY = settings.alignment === "center" ? minFramePadding + Math.floor(extraHeight / 2) : minFramePadding; + let drawY = 0; + if (settings.alignment === "center") { + drawY = Math.floor((finalCanvasHeight - img.height) / 2); + } else { + // topの場合は、上と左右の余白を同じにする(コの字均等)と美しくなる。 + // ただし、画像が下がりすぎてテキスト領域が潰れるのを防ぐ。 + const maxDrawY = finalCanvasHeight - img.height - (minFramePadding + minTextSpace); + drawY = Math.max(minFramePadding, Math.min(drawX, maxDrawY)); + } // Enable P3 wide-gamut mode to prevent high-saturation color loss, with a fallback let ctx: CanvasRenderingContext2D | null = null; @@ -95,8 +105,8 @@ export function renderImageToCanvas( // テキストの配置Y座標は、画像の下端とキャンバス下端の中央 const textY = imgBottomY + (bottomSpaceHeight / 2); - // テキストのサイズを(marginではなく)画像自体のサイズを基準にする - const baseScale = Math.min(img.width, img.height); + // テキストのサイズを長辺基準にする + const baseScale = Math.max(img.width, img.height); // Settings for text ctx.fillStyle = settings.textColor; @@ -130,7 +140,7 @@ export function renderImageToCanvas( }; if (topText) { - const titleFontSize = Math.floor(baseScale * 0.035); // 画像サイズの約3.5% + const titleFontSize = Math.floor(baseScale * 0.025); // 長辺の約2.5%に変更 ctx.font = getFontString(titleFontSize, settings.fontFamily); ctx.fillText(topText, canvas.width / 2, textY - (titleFontSize * 0.8)); } @@ -152,7 +162,7 @@ export function renderImageToCanvas( const bottomText = bottomElements.join(separator); if (bottomText) { - const descFontSize = Math.floor(baseScale * 0.025); // 画像サイズの約2.5% + const descFontSize = Math.floor(baseScale * 0.015); // 長辺の約1.5%に変更 ctx.font = getFontString(descFontSize, settings.fontFamily); ctx.globalAlpha = 0.6; ctx.fillStyle = settings.textColor; From e2d85792d4e65ed3726e491966356c545c5e9f0f Mon Sep 17 00:00:00 2001 From: amemya Date: Wed, 8 Jul 2026 10:43:25 +0900 Subject: [PATCH 2/7] fix: ensure top alignment correctly positions image at the top margin --- frontend/src/canvas.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/canvas.ts b/frontend/src/canvas.ts index 4594301..61f6653 100644 --- a/frontend/src/canvas.ts +++ b/frontend/src/canvas.ts @@ -72,10 +72,8 @@ export function renderImageToCanvas( if (settings.alignment === "center") { drawY = Math.floor((finalCanvasHeight - img.height) / 2); } else { - // topの場合は、上と左右の余白を同じにする(コの字均等)と美しくなる。 - // ただし、画像が下がりすぎてテキスト領域が潰れるのを防ぐ。 - const maxDrawY = finalCanvasHeight - img.height - (minFramePadding + minTextSpace); - drawY = Math.max(minFramePadding, Math.min(drawX, maxDrawY)); + // topの場合はシンプルに上部(最小余白)に寄せる + drawY = minFramePadding; } // Enable P3 wide-gamut mode to prevent high-saturation color loss, with a fallback From c28ceebac7e6449eecf670f4a46c3376d81bfad3 Mon Sep 17 00:00:00 2001 From: amemya Date: Wed, 8 Jul 2026 20:26:19 +0900 Subject: [PATCH 3/7] refactor: optimize canvas rendering pipeline and update coordinate transformation logic --- frontend/src/canvas.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/canvas.ts b/frontend/src/canvas.ts index 61f6653..aa57ab3 100644 --- a/frontend/src/canvas.ts +++ b/frontend/src/canvas.ts @@ -20,11 +20,11 @@ export function renderImageToCanvas( exif: ExifData, settings: RenderSettings ) { - // 画像の長辺を基準に余白を計算 + // 画像の長辺を基準に余白を計算(縦構図の窮屈さを防ぐため) const baseSize = Math.max(img.width, img.height); - const minFramePadding = Math.floor(baseSize * 0.05); // 長辺の5% + const minFramePadding = Math.floor(baseSize * 0.025); // 当初のサイズ感(2.5%)に戻す // 下部のテキスト領域に必要な最小スペース - const minTextSpace = Math.floor(minFramePadding * 2.5); + const minTextSpace = Math.floor(minFramePadding * 4.5); let targetRatio = 4300 / 3618; if (settings.aspectRatioPreset === "custom") { @@ -69,11 +69,15 @@ export function renderImageToCanvas( // 画像の配置位置を計算 const drawX = Math.floor((finalCanvasWidth - img.width) / 2); let drawY = 0; + const totalMarginY = finalCanvasHeight - img.height; + if (settings.alignment === "center") { - drawY = Math.floor((finalCanvasHeight - img.height) / 2); + drawY = Math.floor(totalMarginY / 2); } else { - // topの場合はシンプルに上部(最小余白)に寄せる - drawY = minFramePadding; + // Top(ボトムヘビー)の場合: + // 固定の最小余白だと、キャンバスが横に広がった際に上が極端に細く見えてしまうため、 + // 縦の総余白に対して「上1:下2」の比率(総余白の1/3)になるように配置する。 + drawY = Math.max(minFramePadding, Math.floor(totalMarginY / 3)); } // Enable P3 wide-gamut mode to prevent high-saturation color loss, with a fallback @@ -103,8 +107,8 @@ export function renderImageToCanvas( // テキストの配置Y座標は、画像の下端とキャンバス下端の中央 const textY = imgBottomY + (bottomSpaceHeight / 2); - // テキストのサイズを長辺基準にする - const baseScale = Math.max(img.width, img.height); + // テキストのサイズは当初の通り短辺基準に戻す + const baseScale = Math.min(img.width, img.height); // Settings for text ctx.fillStyle = settings.textColor; @@ -138,7 +142,7 @@ export function renderImageToCanvas( }; if (topText) { - const titleFontSize = Math.floor(baseScale * 0.025); // 長辺の約2.5%に変更 + const titleFontSize = Math.floor(baseScale * 0.035); // 当初の比率に戻す ctx.font = getFontString(titleFontSize, settings.fontFamily); ctx.fillText(topText, canvas.width / 2, textY - (titleFontSize * 0.8)); } @@ -160,7 +164,7 @@ export function renderImageToCanvas( const bottomText = bottomElements.join(separator); if (bottomText) { - const descFontSize = Math.floor(baseScale * 0.015); // 長辺の約1.5%に変更 + const descFontSize = Math.floor(baseScale * 0.025); // 当初の比率に戻す ctx.font = getFontString(descFontSize, settings.fontFamily); ctx.globalAlpha = 0.6; ctx.fillStyle = settings.textColor; From 04a04f727c39d8a0291b21010ed5b1cfe4116a25 Mon Sep 17 00:00:00 2001 From: amemya Date: Mon, 13 Jul 2026 14:23:39 +0900 Subject: [PATCH 4/7] refactor: update dependencies and optimize canvas rendering logic --- frontend/package-lock.json | 18 ------------------ frontend/src/canvas.ts | 26 ++++++++++++++++++-------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 69b1ede..f5b181d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -524,9 +524,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -544,9 +541,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -564,9 +558,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -584,9 +575,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -604,9 +592,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -624,9 +609,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/frontend/src/canvas.ts b/frontend/src/canvas.ts index aa57ab3..a8e89fc 100644 --- a/frontend/src/canvas.ts +++ b/frontend/src/canvas.ts @@ -22,7 +22,7 @@ export function renderImageToCanvas( ) { // 画像の長辺を基準に余白を計算(縦構図の窮屈さを防ぐため) const baseSize = Math.max(img.width, img.height); - const minFramePadding = Math.floor(baseSize * 0.025); // 当初のサイズ感(2.5%)に戻す + const minFramePadding = Math.floor(baseSize * 0.02); // 当初のサイズ感(2.5%)に戻す // 下部のテキスト領域に必要な最小スペース const minTextSpace = Math.floor(minFramePadding * 4.5); @@ -70,14 +70,24 @@ export function renderImageToCanvas( const drawX = Math.floor((finalCanvasWidth - img.width) / 2); let drawY = 0; const totalMarginY = finalCanvasHeight - img.height; - + if (settings.alignment === "center") { drawY = Math.floor(totalMarginY / 2); } else { - // Top(ボトムヘビー)の場合: - // 固定の最小余白だと、キャンバスが横に広がった際に上が極端に細く見えてしまうため、 - // 縦の総余白に対して「上1:下2」の比率(総余白の1/3)になるように配置する。 - drawY = Math.max(minFramePadding, Math.floor(totalMarginY / 3)); + // 理想は上余白と横余白を同一にする(コの字均等) + let idealDrawY = drawX; + + // Top配置なので、絶対に Center よりは上に配置する(差をつける)。 + // CenterのY座標の85%を上限とすることで、横幅が極端に広い場合でも + // Centerと同じ位置になってしまう現象を防ぐ。 + const centerDrawY = Math.floor(totalMarginY / 2); + const maxDrawYForText = totalMarginY - minTextSpace; + const maxDrawY = Math.min( + maxDrawYForText, + Math.floor(centerDrawY * 0.85) + ); + + drawY = Math.max(minFramePadding, Math.min(idealDrawY, maxDrawY)); } // Enable P3 wide-gamut mode to prevent high-saturation color loss, with a fallback @@ -127,7 +137,7 @@ export function renderImageToCanvas( if (!family || family.trim() === "") { return `normal ${size}px sans-serif`; } - + const genericFamilies = new Set(CSS_GENERIC_FONTS); const trimmedFamily = family.trim(); @@ -135,7 +145,7 @@ export function renderImageToCanvas( const hasDoubleQuotes = trimmedFamily.startsWith('"') && trimmedFamily.endsWith('"'); const hasSingleQuotes = trimmedFamily.startsWith("'") && trimmedFamily.endsWith("'"); const hasQuotes = hasDoubleQuotes || hasSingleQuotes; - + const escapedFamily = trimmedFamily.replace(/"/g, '\\"'); const safeFamily = (isGeneric || hasQuotes) ? trimmedFamily : `"${escapedFamily}"`; return `normal ${size}px ${safeFamily}, sans-serif`; From 5942476f1859b63a35bff15d2feebb6e69188093 Mon Sep 17 00:00:00 2001 From: amemya Date: Mon, 13 Jul 2026 14:48:27 +0900 Subject: [PATCH 5/7] refactor: extract image Y-coordinate calculation logic into separate function --- frontend/src/canvas.ts | 59 +++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/frontend/src/canvas.ts b/frontend/src/canvas.ts index a8e89fc..033c2ff 100644 --- a/frontend/src/canvas.ts +++ b/frontend/src/canvas.ts @@ -14,6 +14,35 @@ export interface RenderSettings { fontFamily: string; } +/** + * Calculates the Y coordinate for the image based on alignment settings. + */ +function calculateDrawY( + alignment: "top" | "center", + drawX: number, + totalMarginY: number, + minFramePadding: number, + minTextSpace: number +): number { + if (alignment === "center") { + return Math.floor(totalMarginY / 2); + } + + // 理想は上余白と横余白を同一にする(コの字均等) + const idealDrawY = drawX; + + // Top配置なので、絶対に Center よりは上に配置する(差をつける)。 + // CenterのY座標の85%を上限とすることで、横幅が極端に広い場合でも + // Centerと同じ位置になってしまう現象を防ぐ。 + const centerDrawY = Math.floor(totalMarginY / 2); + const maxDrawYForText = totalMarginY - minTextSpace; + const maxDrawY = Math.min( + maxDrawYForText, + Math.floor(centerDrawY * 0.85) + ); + + return Math.max(minFramePadding, Math.min(idealDrawY, maxDrawY)); +} export function renderImageToCanvas( canvas: HTMLCanvasElement, img: HTMLImageElement, @@ -22,7 +51,7 @@ export function renderImageToCanvas( ) { // 画像の長辺を基準に余白を計算(縦構図の窮屈さを防ぐため) const baseSize = Math.max(img.width, img.height); - const minFramePadding = Math.floor(baseSize * 0.02); // 当初のサイズ感(2.5%)に戻す + const minFramePadding = Math.floor(baseSize * 0.02); // 余白サイズ調整 // 下部のテキスト領域に必要な最小スペース const minTextSpace = Math.floor(minFramePadding * 4.5); @@ -68,27 +97,15 @@ export function renderImageToCanvas( // 画像の配置位置を計算 const drawX = Math.floor((finalCanvasWidth - img.width) / 2); - let drawY = 0; const totalMarginY = finalCanvasHeight - img.height; - - if (settings.alignment === "center") { - drawY = Math.floor(totalMarginY / 2); - } else { - // 理想は上余白と横余白を同一にする(コの字均等) - let idealDrawY = drawX; - - // Top配置なので、絶対に Center よりは上に配置する(差をつける)。 - // CenterのY座標の85%を上限とすることで、横幅が極端に広い場合でも - // Centerと同じ位置になってしまう現象を防ぐ。 - const centerDrawY = Math.floor(totalMarginY / 2); - const maxDrawYForText = totalMarginY - minTextSpace; - const maxDrawY = Math.min( - maxDrawYForText, - Math.floor(centerDrawY * 0.85) - ); - - drawY = Math.max(minFramePadding, Math.min(idealDrawY, maxDrawY)); - } + + const drawY = calculateDrawY( + settings.alignment, + drawX, + totalMarginY, + minFramePadding, + minTextSpace + ); // Enable P3 wide-gamut mode to prevent high-saturation color loss, with a fallback let ctx: CanvasRenderingContext2D | null = null; From 83319f4a6bed3389008e0262b59813567412683d Mon Sep 17 00:00:00 2001 From: amemya Date: Mon, 13 Jul 2026 14:59:56 +0900 Subject: [PATCH 6/7] refactor: optimize canvas rendering pipeline and update coordinate transformation logic --- frontend/src/canvas.ts | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/frontend/src/canvas.ts b/frontend/src/canvas.ts index 033c2ff..f439794 100644 --- a/frontend/src/canvas.ts +++ b/frontend/src/canvas.ts @@ -30,7 +30,7 @@ function calculateDrawY( // 理想は上余白と横余白を同一にする(コの字均等) const idealDrawY = drawX; - + // Top配置なので、絶対に Center よりは上に配置する(差をつける)。 // CenterのY座標の85%を上限とすることで、横幅が極端に広い場合でも // Centerと同じ位置になってしまう現象を防ぐ。 @@ -38,9 +38,9 @@ function calculateDrawY( const maxDrawYForText = totalMarginY - minTextSpace; const maxDrawY = Math.min( maxDrawYForText, - Math.floor(centerDrawY * 0.85) + Math.floor(centerDrawY * 0.85) ); - + return Math.max(minFramePadding, Math.min(idealDrawY, maxDrawY)); } export function renderImageToCanvas( @@ -51,7 +51,7 @@ export function renderImageToCanvas( ) { // 画像の長辺を基準に余白を計算(縦構図の窮屈さを防ぐため) const baseSize = Math.max(img.width, img.height); - const minFramePadding = Math.floor(baseSize * 0.02); // 余白サイズ調整 + const minFramePadding = Math.max(20, Math.floor(baseSize * 0.02)); //余白サイズ // 下部のテキスト領域に必要な最小スペース const minTextSpace = Math.floor(minFramePadding * 4.5); @@ -98,12 +98,12 @@ export function renderImageToCanvas( // 画像の配置位置を計算 const drawX = Math.floor((finalCanvasWidth - img.width) / 2); const totalMarginY = finalCanvasHeight - img.height; - + const drawY = calculateDrawY( - settings.alignment, - drawX, - totalMarginY, - minFramePadding, + settings.alignment, + drawX, + totalMarginY, + minFramePadding, minTextSpace ); @@ -157,14 +157,20 @@ export function renderImageToCanvas( const genericFamilies = new Set(CSS_GENERIC_FONTS); - const trimmedFamily = family.trim(); + let trimmedFamily = family.trim(); + + // 外側の引用符を正規化 + if ((trimmedFamily.startsWith('"') && trimmedFamily.endsWith('"')) || + (trimmedFamily.startsWith("'") && trimmedFamily.endsWith("'"))) { + trimmedFamily = trimmedFamily.slice(1, -1); + } + const isGeneric = genericFamilies.has(trimmedFamily.toLowerCase()); - const hasDoubleQuotes = trimmedFamily.startsWith('"') && trimmedFamily.endsWith('"'); - const hasSingleQuotes = trimmedFamily.startsWith("'") && trimmedFamily.endsWith("'"); - const hasQuotes = hasDoubleQuotes || hasSingleQuotes; - const escapedFamily = trimmedFamily.replace(/"/g, '\\"'); - const safeFamily = (isGeneric || hasQuotes) ? trimmedFamily : `"${escapedFamily}"`; + // バックスラッシュとダブルクォートをエスケープ + const escapedFamily = trimmedFamily.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const safeFamily = isGeneric ? trimmedFamily : `"${escapedFamily}"`; + return `normal ${size}px ${safeFamily}, sans-serif`; }; From 4757d944d2cfd7635f52bb9f4c25951259746e59 Mon Sep 17 00:00:00 2001 From: amemya Date: Mon, 13 Jul 2026 15:18:00 +0900 Subject: [PATCH 7/7] refactor: delete some comments --- frontend/src/canvas.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/canvas.ts b/frontend/src/canvas.ts index f439794..f0b751a 100644 --- a/frontend/src/canvas.ts +++ b/frontend/src/canvas.ts @@ -91,7 +91,7 @@ export function renderImageToCanvas( finalCanvasWidth = Math.floor(finalCanvasHeight * targetRatio); } - // ⚠️ CRITICAL: Must be set BEFORE getContext, otherwise context properties (colorSpace) are reset! + // CRITICAL: Must be set BEFORE getContext, otherwise context properties (colorSpace) are reset! canvas.width = finalCanvasWidth; canvas.height = finalCanvasHeight; @@ -175,7 +175,7 @@ export function renderImageToCanvas( }; if (topText) { - const titleFontSize = Math.floor(baseScale * 0.035); // 当初の比率に戻す + const titleFontSize = Math.floor(baseScale * 0.035); ctx.font = getFontString(titleFontSize, settings.fontFamily); ctx.fillText(topText, canvas.width / 2, textY - (titleFontSize * 0.8)); } @@ -197,7 +197,7 @@ export function renderImageToCanvas( const bottomText = bottomElements.join(separator); if (bottomText) { - const descFontSize = Math.floor(baseScale * 0.025); // 当初の比率に戻す + const descFontSize = Math.floor(baseScale * 0.025); ctx.font = getFontString(descFontSize, settings.fontFamily); ctx.globalAlpha = 0.6; ctx.fillStyle = settings.textColor;