From 7329ba8c8901ee095ad7316b8554c5e44431eebc Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:46:21 +0900 Subject: bluebey-studio: 縦書きのセリフとナレーション枠、コック帽改善、小物に鉛筆とコップなど MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit セリフを縦書き可能にし、縦書きのナレーションボックスを追加。ハートでも下まぶたを閉じられるようにし、ふつうの目の瞳を少し鼻側へ。HairPlateは髪が無いとき非表示(iPhoneの色ずれ対策)。コック帽をトック帽に、鉛筆とコップを小物に追加。 --- public/bluebey-studio/src/caption.js | 84 +++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 6 deletions(-) (limited to 'public/bluebey-studio/src/caption.js') diff --git a/public/bluebey-studio/src/caption.js b/public/bluebey-studio/src/caption.js index 15f5bf3..1543f0e 100644 --- a/public/bluebey-studio/src/caption.js +++ b/public/bluebey-studio/src/caption.js @@ -118,12 +118,19 @@ export function layoutCaption(caption, { width, height, scale = 1, font = null } const requested = toNonNegative(state.padding, DEFAULT_PADDING) * resolvedScale; const text = typeof state.text === 'string' ? state.text : ''; - const block = layoutBlock(font, text, { - fontSize, - maxWidth: resolveColumn(state, outWidth, requested), - lineHeight, - align: alignOf(state), - }); + const vertical = state.vertical === true; + const block = vertical + ? layoutVertical(text, { + fontSize, + lineHeight, + maxHeight: resolveColumn(state, outHeight, requested), + }) + : layoutBlock(font, text, { + fontSize, + maxWidth: resolveColumn(state, outWidth, requested), + lineHeight, + align: alignOf(state), + }); // Padding gives way before the bubble does, so a caption that cannot fit with // its usual breathing room still fits inside the image. @@ -169,6 +176,7 @@ export function layoutCaption(caption, { width, height, scale = 1, font = null } lineHeight, padding, scale: resolvedScale, + vertical, usedOutlines: Boolean(font), }; } @@ -275,6 +283,9 @@ export function captionToSvg(caption, { width, height, scale = 1, font = null, r const text = typeof state.text === 'string' ? state.text : ''; let usedOutlines = Boolean(font) && text.trim() !== '' && hasGlyphs(font, text); + // Vertical text falls back to ``: the outline path lays glyphs out + // horizontally, and a column of glyphs reads fine as live text. + if (layout.vertical) usedOutlines = false; const parts = [ '${escapeText(chars[j])}`); + } + } + return `${tspans.join('')}`; + } + const attributes = [ `font-family="${escapeAttribute(fontFamilyStack(state, Boolean(font)))}"`, `font-size="${formatNumber(fontSize, places)}"`, @@ -430,6 +474,34 @@ function layoutBlock(font, text, options) { return wrapByCount(text, options); } +/** + * Vertical (縦書き) layout: each paragraph becomes one column, characters stacked + * top to bottom, columns running right to left. A paragraph longer than the + * available height is split into more columns. + */ +function layoutVertical(text, { fontSize, lineHeight, maxHeight }) { + const limit = Number.isFinite(maxHeight) && maxHeight > 0 + ? Math.max(1, Math.floor(maxHeight / fontSize)) + : Infinity; + const columns = []; + for (const paragraph of String(text).split(/\r\n|\r|\n/)) { + const chars = [...paragraph]; + if (chars.length === 0) { + columns.push(''); + continue; + } + for (let i = 0; i < chars.length; i += limit) { + columns.push(chars.slice(i, i + limit).join('')); + } + } + const longest = columns.reduce((max, column) => Math.max(max, [...column].length), 0); + return { + lines: columns.map((column) => ({ text: column, width: fontSize })), + width: columns.length * fontSize * lineHeight, + height: longest * fontSize, + }; +} + /** Text column in pixels: a fraction of the image, never wider than the image. */ function resolveColumn(state, outWidth, padding) { const fraction = toNonNegative(state.maxWidth, DEFAULT_MAX_WIDTH); -- cgit v1.3.1