aboutsummaryrefslogtreecommitdiffhomepage
path: root/public
diff options
context:
space:
mode:
Diffstat (limited to 'public')
-rw-r--r--public/bluebey-studio/src/caption.js73
-rw-r--r--public/bluebey-studio/src/panel.js16
2 files changed, 61 insertions, 28 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 },
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 */