diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-06 13:51:26 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-06 13:51:26 +0900 |
| commit | 350c1563abc8ab66aaaed8cfbc6d62bdfeadb0da (patch) | |
| tree | 82da8be6b305a1f2ff04b9f343104b8cb32ea7d4 /public/bluebey-studio/src/caption.js | |
| parent | ee08d3491b487dea16855eec17bb5e0a2dfecd25 (diff) | |
bluebey-studio: 縦書きの長音と句読点を修正、思考の雲をふんわり、照明の種類をシーンへ
縦書きの「ー」を90度回転して縦棒にし、句読点を字枠の右上へ配置(canvas/SVG両方)。思考の吹き出しは雲をより細かくふんわりさせ、〇を約3倍に。目のグループ名を「左目」「右目」に、照明の種類を「見た目」から「シーン」タブの「画面・光」へ移動。
Diffstat (limited to 'public/bluebey-studio/src/caption.js')
| -rw-r--r-- | public/bluebey-studio/src/caption.js | 73 |
1 files changed, 52 insertions, 21 deletions
diff --git a/public/bluebey-studio/src/caption.js b/public/bluebey-studio/src/caption.js index fd418da..d0318e7 100644 --- a/public/bluebey-studio/src/caption.js +++ b/public/bluebey-studio/src/caption.js @@ -99,10 +99,17 @@ const COLUMN_END_FORBIDDEN = new Set(['「', '『', '(', '〔', '【', '〈', */ const VERTICAL_OFFSETS = new Map(); for (const ch of SMALL_KANA) VERTICAL_OFFSETS.set(ch, { dx: 0.12, dy: -0.12 }); -VERTICAL_OFFSETS.set('ー', { dx: 0.22, dy: -0.3 }); +// 縦書きの句読点は、横書きの左下ではなく、字枠の右上に置く。 +for (const ch of HANGING_PUNCTUATION) VERTICAL_OFFSETS.set(ch, { dx: 0.3, dy: -0.3 }); for (const ch of '「『(〔') VERTICAL_OFFSETS.set(ch, { dx: 0.14, dy: -0.12 }); for (const ch of '」』)〕') VERTICAL_OFFSETS.set(ch, { dx: -0.14, dy: 0.12 }); +/** + * Characters drawn turned a quarter turn when stacked, so they read the way + * 縦書き sets them: the prolonged sound mark `ー` becomes an upright bar. + */ +const VERTICAL_ROTATED = new Set(['ー', '~', '〜', '―']); + /** Offset used for the characters that need no special placement. */ const NO_OFFSET = Object.freeze({ dx: 0, dy: 0 }); @@ -406,14 +413,26 @@ function drawText(ctx, layout, state, font) { ctx.textBaseline = 'alphabetic'; if (layout.vertical) { - // Columns run right to left; each glyph sits on its own line, upright. + // Columns run right to left; each glyph sits in its own cell. `ー` and its + // like are turned a quarter turn so they read as vertical strokes. for (let i = 0; i < lines.length; i++) { const chars = [...lines[i].text]; const x = left + (lines.length - 1 - i) * step; for (let j = 0; j < chars.length; j++) { - const offset = verticalOffsetOf(chars[j]); + const ch = chars[j]; + if (VERTICAL_ROTATED.has(ch)) { + ctx.save(); + ctx.translate(x + fontSize / 2, top + j * fontSize + fontSize / 2); + ctx.rotate(Math.PI / 2); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(ch, 0, 0); + ctx.restore(); + continue; + } + const offset = verticalOffsetOf(ch); const y = top + j * fontSize + metrics.ascent + offset.dy * fontSize; - ctx.fillText(chars[j], x + offset.dx * fontSize, y); + ctx.fillText(ch, x + offset.dx * fontSize, y); } } return; @@ -452,24 +471,36 @@ function liveText(state, layout, font, fill, places) { const top = box.y + padding; if (layout.vertical) { - const attributes = [ + // The stacked glyphs share one left-anchored <text>. The rotated ones (`ー`) + // get a <text> of their own, centred on the cell and turned a quarter turn. + const base = [ `font-family="${escapeAttribute(fontFamilyStack(state, Boolean(font)))}"`, `font-size="${formatNumber(fontSize, places)}"`, state.bold ? 'font-weight="bold"' : '', `fill="${escapeAttribute(fill)}"`, - 'text-anchor="start"', ].filter(Boolean).join(' '); const tspans = []; + const rotated = []; for (let i = 0; i < lines.length; i++) { const chars = [...lines[i].text]; const x = left + (lines.length - 1 - i) * step; for (let j = 0; j < chars.length; j++) { - const offset = verticalOffsetOf(chars[j]); + const ch = chars[j]; + if (VERTICAL_ROTATED.has(ch)) { + const cx = x + fontSize / 2; + const cy = top + j * fontSize + fontSize / 2; + rotated.push(`<text ${base} text-anchor="middle" dy="0.36em"` + + ` transform="rotate(90 ${formatNumber(cx, places)} ${formatNumber(cy, places)})"` + + ` x="${formatNumber(cx, places)}" y="${formatNumber(cy, places)}">${escapeText(ch)}</text>`); + continue; + } + const offset = verticalOffsetOf(ch); const y = top + j * fontSize + metrics.ascent + offset.dy * fontSize; - tspans.push(`<tspan x="${formatNumber(x + offset.dx * fontSize, places)}" y="${formatNumber(y, places)}">${escapeText(chars[j])}</tspan>`); + tspans.push(`<tspan x="${formatNumber(x + offset.dx * fontSize, places)}" y="${formatNumber(y, places)}">${escapeText(ch)}</tspan>`); } } - return `<text ${attributes}>${tspans.join('')}</text>`; + const main = tspans.length ? `<text ${base} text-anchor="start">${tspans.join('')}</text>` : ''; + return `${main}${rotated.join('')}`; } const attributes = [ @@ -789,10 +820,11 @@ function thinkEdges(box, scale) { const cy = y + h / 2; const ring = roundedRectRing(box, Math.min(w, h) * 0.28); const perimeter = ringLength(ring); - const pitch = Math.max(scale * 8, Math.min(w, h) * 0.42); + // Fluffier: more scallops, each bulging further, so it reads as a soft cloud. + const pitch = Math.max(scale * 6, Math.min(w, h) * 0.30); const count = Math.max(6, Math.round(perimeter / pitch)); const points = sampleRing(ring, count); - const amp = Math.min((perimeter / count) * 0.5, Math.min(w, h) * 0.18); + const amp = Math.min((perimeter / count) * 0.55, Math.min(w, h) * 0.26); const edges = []; for (let i = 0; i < points.length; i++) { @@ -834,17 +866,16 @@ function thoughtTrail(box, state, scale, outer) { x: dir.x < 0 ? box.x : dir.x > 0 ? box.x + box.w : box.x + box.w / 2, y: dir.y < 0 ? box.y : dir.y > 0 ? box.y + box.h : box.y + box.h / 2, }; - const span = Math.min(resolveFontSize(state, scale) * 0.9, Math.min(box.w, box.h) * 0.5); - const room = span - outer; - if (!(room > 0)) return []; - - const r1 = room * 0.22; - const r2 = room * 0.13; - const r3 = room * 0.07; - const gap = room * 0.05; + // Sized from the font, not the leftover room, so the puffs stay visible at any + // bubble size; they start just outside the cloud (`outer`) and shrink away. + const unit = resolveFontSize(state, scale); + const r1 = unit * 0.6; + const r2 = unit * 0.42; + const r3 = unit * 0.26; + const gap = unit * 0.5; const d1 = outer + r1; - const d2 = d1 + r1 + gap + r2; - const d3 = d2 + r2 + gap + r3; + const d2 = d1 + r1 + gap; + const d3 = d2 + r2 + gap; return [ { x: anchor.x + nx * d1, y: anchor.y + ny * d1, r: r1 }, { x: anchor.x + nx * d2, y: anchor.y + ny * d2, r: r2 }, |
