From 350c1563abc8ab66aaaed8cfbc6d62bdfeadb0da Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:51:26 +0900 Subject: bluebey-studio: 縦書きの長音と句読点を修正、思考の雲をふんわり、照明の種類をシーンへ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 縦書きの「ー」を90度回転して縦棒にし、句読点を字枠の右上へ配置(canvas/SVG両方)。思考の吹き出しは雲をより細かくふんわりさせ、〇を約3倍に。目のグループ名を「左目」「右目」に、照明の種類を「見た目」から「シーン」タブの「画面・光」へ移動。 --- public/bluebey-studio/src/caption.js | 73 +++++++++++++++++++++++++----------- public/bluebey-studio/src/panel.js | 16 ++++---- 2 files changed, 61 insertions(+), 28 deletions(-) (limited to 'public/bluebey-studio/src') 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 . The rotated ones (`ー`) + // get a 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(`${escapeText(ch)}`); + continue; + } + const offset = verticalOffsetOf(ch); const y = top + j * fontSize + metrics.ascent + offset.dy * fontSize; - tspans.push(`${escapeText(chars[j])}`); + tspans.push(`${escapeText(ch)}`); } } - return `${tspans.join('')}`; + const main = tspans.length ? `${tspans.join('')}` : ''; + 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 }, diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index 871c896..0ee94ac 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -592,7 +592,7 @@ export function buildPanel(app, root) { const primary = keys[0]; const write = (fn) => { for (const k of keys) fn(state.face.eyes[k]); }; const touched = () => { app.actions.refresh('face'); sync(); }; - const group = eyeGroup(mode === 'both' ? '両目' : (mode === 'left' ? '左の目' : '右の目'), mode); + const group = eyeGroup(mode === 'both' ? '両目' : (mode === 'left' ? '左目' : '右目'), mode); // --- まぶた (per eye) ------------------------------------------------ // 上まぶたの高さ and 下まぶたの高さ share one opening, so each is held to the @@ -818,7 +818,7 @@ export function buildPanel(app, root) { // 横位置 (per eye) rarely changes, so it gets its own small box. const eyeAdvancedWidgets = {}; for (const key of ['left', 'right']) { - const label = key === 'left' ? '左の目' : '右の目'; + const label = key === 'left' ? '左目' : '右目'; const eyeX = slider({ label: `${label}の横位置`, min: -200, max: 200, step: 1, value: 0, format: (v) => `${Math.round(v)}`, onInput: (value) => { state.face.eyes[key].eyeX = value; app.actions.refresh('face'); }, @@ -845,8 +845,8 @@ export function buildPanel(app, root) { faceSection.add(hint('「まぶた」は目のグループごとに設定できます。「左右を連動させる」を入れると「両目」の設定が1つだけ出て、' + 'まぶた・目の形・大きさ・白目・光彩・目線が両目でそろいます。' - + 'はずすと「左の目」「右の目」の設定がそれぞれに出て、目線のパッドも2つになります。' - + '「左の目」「右の目」は、ぶるべー自身から見た左右です(正面から見ると、画面では左右が入れ替わって見えます)。')); + + 'はずすと「左目」「右目」の設定がそれぞれに出て、目線のパッドも2つになります。' + + '「左目」「右目」は、ぶるべー自身から見た左右です(正面から見ると、画面では左右が入れ替わって見えます)。')); const eyeColors = { white: colorField({ label: '白目', value: '#ffffff', onChange: (v) => { state.face.eyes.white = v; app.actions.refresh('face'); } }), @@ -1907,7 +1907,7 @@ export function buildPanel(app, root) { value: 'gradient', onChange: (value) => { state.render.environment = value; app.actions.refresh('render'); }, }); - lookSection.add(environmentSegment.el); + // 照明の種類は「シーン」タブの「画面・光」に置く(環境の映りこみの元を選ぶものなので)。 lookSection.add(buttons({ items: [{ id: 'resetLook', @@ -2569,13 +2569,15 @@ export function buildPanel(app, root) { swatches: ['#ffffff', '#d9d0f2', '#cfe6ff', '#ffe6cc', '#e8ffe0'], onChange: (v) => { state.render.ambientColor = v; app.actions.refresh('render'); }, }); + lightSection.add(environmentSegment.el); lightSection.add(lightIntensitySlider.el); lightSection.add(ambientSlider.el); lightSection.add(lightColorField.el); lightSection.add(ambientColorField.el); lightSection.add(exposureSlider.el); - lightSection.add(hint('「光の色」でライトの色、「環境光の色」で影側の色を変えられます。' - + 'テクスチャ(環境)の映りこみは「見た目」タブの「環境の映りこみ」で調整できます。')); + lightSection.add(hint('「照明の種類」は環境の映りこみの元(グラデーション/部屋/なし)です。' + + '「光の色」でライトの色、「環境光の色」で影側の色を変えられます。' + + 'テクスチャ(環境)の映りこみの強さは「見た目」タブの「環境の映りこみ」で調整できます。')); /* ----------------------------------------------------------------- camera */ -- cgit v1.3.1