aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/caption.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/caption.js')
-rw-r--r--public/bluebey-studio/src/caption.js73
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 },