aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-03 13:51:53 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-03 13:51:53 +0900
commit7892cc6f46cdfcd6dfbbdc94d34cbf0c68f29fba (patch)
treecc353614365d2a2b56055b46e8b3c9e8d930ad12 /public/bluebey-studio
parentc3046436182754c550d447d689c28af86d06b316 (diff)
public/bluebey-studio: 寝ているポーズと鼻ちょうちんを追加し、動きを修正
- ポーズ例に「ねている」を追加(体を横に倒した姿勢) - 「顔」タブに「鼻ちょうちん」を追加。鼻の横に泡を描き、大きさ・位置 ・色を調整できる。寝ている顔に使う - 「歩く」の腕と脚の位相を逆にし、右手が前に出るときは左足が前に出る ように修正 - 「てをふる」の腕の上げ幅を抑え、手が顔にめり込まないように修正
Diffstat (limited to 'public/bluebey-studio')
-rw-r--r--public/bluebey-studio/src/animation.js17
-rw-r--r--public/bluebey-studio/src/face.js2
-rw-r--r--public/bluebey-studio/src/faceArt.js89
-rw-r--r--public/bluebey-studio/src/panel.js39
-rw-r--r--public/bluebey-studio/src/presets.js24
5 files changed, 162 insertions, 9 deletions
diff --git a/public/bluebey-studio/src/animation.js b/public/bluebey-studio/src/animation.js
index 47f1cfb..3233e43 100644
--- a/public/bluebey-studio/src/animation.js
+++ b/public/bluebey-studio/src/animation.js
@@ -97,7 +97,9 @@ export class Animator {
// wants. X (which the first version used) is the *lift*, so opposite signs
// there just raised one flipper and dropped the other one.
// `legsupport` pivots at the middle of the body, so the legs take a much
- // smaller angle than the arms.
+ // smaller angle than the arms. The legs swing on the OPPOSITE phase to the
+ // arms, so when a hand comes forward it is the opposite foot that steps out
+ // (the same value as the arms put them in step, i.e. a "same-side" waddle).
const phase = Math.sin(t * 2.4);
const bob = Math.abs(Math.sin(t * 2.4));
add('master', -2, 0, phase * 4);
@@ -105,8 +107,8 @@ export class Animator {
add('arm.r', 6, 0, phase * 26);
add('hand.l', 0, 0, phase * 10);
add('hand.r', 0, 0, phase * 10);
- add('legsupport.l', 0, 0, phase * 9);
- add('legsupport.r', 0, 0, phase * 9);
+ add('legsupport.l', 0, 0, -phase * 9);
+ add('legsupport.r', 0, 0, -phase * 9);
root[1] += bob * 0.05;
return { bones, root };
}
@@ -114,10 +116,13 @@ export class Animator {
if (mode === 'wave') {
// One flipper is held up and sweeps back and forth, with the hand lagging a
// little behind it; the body and the other flipper rock along gently. The
- // +84 X holds the arm up on its own, so the wave reads from a plain stance.
+ // lift is kept modest: raising the arm much further folds the flipper over
+ // the top of the head, where its inner edge cuts into the face, so the X
+ // here (and its swing) stop well short of that. The Z swing is what reads
+ // as the wave, and it is biased slightly *back* for the same reason.
const wave = Math.sin(t * 3.2);
- add('arm.l', 84 + wave * 14, 0, wave * 12);
- add('hand.l', 0, 0, Math.sin(t * 3.2 + 0.6) * 22);
+ add('arm.l', 66 + wave * 8, 0, 6 + wave * 10);
+ add('hand.l', 0, 0, Math.sin(t * 3.2 + 0.6) * 14);
add('arm.r', 0, 0, Math.sin(t * 1.6 + 1) * 4);
add('master', Math.sin(t * 1.6) * 1.2, 0, Math.sin(t * 1.6 + 0.4) * 3);
return { bones, root };
diff --git a/public/bluebey-studio/src/face.js b/public/bluebey-studio/src/face.js
index 18f1436..196ff84 100644
--- a/public/bluebey-studio/src/face.js
+++ b/public/bluebey-studio/src/face.js
@@ -197,6 +197,8 @@ export class Face {
// the other shared eye fields (see `drawEyes` in src/faceArt.js).
cheeks: p.eyes.cheeks,
headMark: p.eyes.headMark,
+ // 鼻ちょうちん is shared by the whole face as well.
+ snot: p.eyes.snot,
// 眼鏡 / サングラス are shared by both eyes and drawn into this same
// texture, so they travel with the other shared eye fields.
glasses: p.eyes.glasses,
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index 61bfa0d..1beedad 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -178,6 +178,22 @@ export const HEAD_MARK_LAYOUT = {
};
/**
+ * 鼻ちょうちん: the little snot bubble that grows and shrinks while someone sleeps.
+ *
+ * It hangs off the nose, which is a *separate mesh* in front of the plate, so the
+ * bubble sits out to the side of it (a bubble drawn straight below would run off
+ * the plate's bottom edge). The neck starts at the nose and reaches out to the
+ * bubble, one thin wisp, the way manga draws it.
+ */
+export const SNOT_LAYOUT = {
+ cx: 511.5, // the face midline, the same centre the mouth uses
+ cy: 336, // just below the eye row, level with the nose
+ offset: 122, // how far out to the side the bubble hangs, clear of the nose
+ radius: 48, // bubble radius at size 1
+ lineWidth: 6,
+};
+
+/**
* 眼鏡 / サングラス, drawn into the same texture as the eyes and brows.
*
* Not measured off any artwork - the character has no glasses - but the numbers
@@ -839,10 +855,77 @@ function drawHeadCover(ctx, spec, limits) {
* The shared extras (ほっぺ and 頭の模様) are drawn once per face, before the eyes,
* so the eyes, brows and glasses win any overlap and the cheeks are not doubled.
*/
-function drawFaceExtras(ctx, style) {
+function drawFaceExtras(ctx, style, lineOnly) {
drawCheeks(ctx, style.cheeks);
// The head covering fills out to the plate's real edges, so it needs `limits`.
drawHeadCover(ctx, style.headMark, style.limits ?? WINDOW_LIMITS);
+ drawSnotBubble(ctx, style.snot, lineOnly);
+}
+
+/**
+ * 鼻ちょうちん: a bubble hanging off the nose, joined to it by a thin neck. The
+ * neck starts at the nose (which the nose mesh hides, so it reads as growing out
+ * from under it) and swells into the round bubble out to the side. In a line
+ * drawing the bubble is left empty and only its outline is inked.
+ */
+function drawSnotBubble(ctx, spec, lineOnly) {
+ if (!spec?.enabled) return;
+ const size = clamp(spec.size ?? 1, 0.2, 3);
+ const dx = spec.offsetX ?? 0;
+ const dy = spec.offsetY ?? 0;
+ const r = SNOT_LAYOUT.radius * size;
+ const cx = SNOT_LAYOUT.cx + SNOT_LAYOUT.offset + dx;
+ const cy = SNOT_LAYOUT.cy + dy;
+ const color = spec.color ?? '#e3ecff';
+ const line = spec.line ?? '#55386e';
+ const lw = Math.max(1, SNOT_LAYOUT.lineWidth * size);
+
+ // The nose end sits on the midline; the bubble hangs out to one side.
+ const nx = SNOT_LAYOUT.cx + dx;
+ const ny = SNOT_LAYOUT.cy + dy - r * 0.1;
+ const dist = Math.hypot(cx - nx, cy - ny) || 1;
+ const ux = (cx - nx) / dist;
+ const uy = (cy - ny) / dist;
+ // Where the neck meets the bubble: a little inside its edge, so the two merge.
+ const reach = Math.max(0, dist - r * 0.72);
+ const ex = nx + ux * reach;
+ const ey = ny + uy * reach;
+
+ ctx.save();
+ ctx.lineJoin = 'round';
+ ctx.lineCap = 'round';
+ // The neck: a wedge from a narrow end at the nose to a wide end at the bubble.
+ const px = -uy;
+ const py = ux;
+ const nW = lw * 0.55;
+ const bW = r * 0.5;
+ ctx.beginPath();
+ ctx.moveTo(nx + px * nW, ny + py * nW);
+ ctx.lineTo(ex + px * bW, ey + py * bW);
+ ctx.lineTo(ex - px * bW, ey - py * bW);
+ ctx.lineTo(nx - px * nW, ny - py * nW);
+ ctx.closePath();
+ if (lineOnly) {
+ ctx.strokeStyle = line;
+ ctx.lineWidth = lw;
+ ctx.stroke();
+ } else {
+ ctx.fillStyle = color;
+ ctx.fill();
+ ctx.strokeStyle = line;
+ ctx.lineWidth = lw * 0.7;
+ ctx.stroke();
+ }
+ // The bubble itself, over the neck's wide end.
+ circlePath(ctx, cx, cy, r);
+ if (!lineOnly) {
+ ctx.fillStyle = color;
+ ctx.fill();
+ }
+ ctx.strokeStyle = line;
+ ctx.lineWidth = lw;
+ ctx.stroke();
+ ctx.restore();
}
/**
@@ -1030,8 +1113,8 @@ export function drawEyes(ctx, p) {
const lowerLid = clamp(style.lowerLid ?? 0, 0, 1);
// ほっぺ and 頭の模様 belong to the whole face, so they are drawn once here
- // rather than inside the per-eye loop below.
- drawFaceExtras(ctx, style);
+ // rather than inside the per-eye loop below. 鼻ちょうちん comes along too.
+ drawFaceExtras(ctx, style, line);
for (const layout of EYE_LAYOUT.eyes) {
const spec = (p.eyes ?? {})[layout.key] ?? {};
diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js
index babeb59..bad4059 100644
--- a/public/bluebey-studio/src/panel.js
+++ b/public/bluebey-studio/src/panel.js
@@ -927,6 +927,41 @@ export function buildPanel(app, root) {
+ '「ツートン」は頭を左右2色に分けます(2色目を使います)。'
+ '決まったキャラクターの髪ではなく、よくあるマンガの記号として用意しました。'));
+ // --- 鼻ちょうちん: the snot bubble of a sleeping face.
+ faceSection = section(tabFace, '鼻ちょうちん');
+ const snotToggle = check({
+ label: '鼻ちょうちんを出す',
+ value: false,
+ onChange: (value) => { state.face.eyes.snot.enabled = value; app.actions.refresh('face'); },
+ });
+ faceSection.add(controlRow(null, snotToggle.el, { wide: true }));
+ const snotSize = slider({
+ label: '大きさ', min: 0.4, max: 2.2, step: 0.01, value: 1, format: (v) => v.toFixed(2),
+ onInput: (v) => { state.face.eyes.snot.size = v; app.actions.refresh('face'); },
+ });
+ faceSection.add(snotSize.el);
+ const snotBox = details(faceSection.body, 'こまかい設定(位置・色)');
+ const snotSliders = {
+ offsetX: slider({
+ label: 'よこの位置', min: -160, max: 160, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
+ onInput: (v) => { state.face.eyes.snot.offsetX = v; app.actions.refresh('face'); },
+ }),
+ offsetY: slider({
+ label: 'たかさ', min: -160, max: 160, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
+ onInput: (v) => { state.face.eyes.snot.offsetY = v; app.actions.refresh('face'); },
+ }),
+ };
+ for (const widget of Object.values(snotSliders)) snotBox.add(widget.el);
+ const snotColor = colorField({
+ label: '泡の色', value: '#e3ecff',
+ swatches: ['#e3ecff', '#ffffff', '#cfe0ff', '#bfe8d0'],
+ onChange: (v) => { state.face.eyes.snot.color = v; app.actions.refresh('face'); },
+ });
+ snotBox.add(snotColor.el);
+ snotBox.add(hint('寝ている顔の横に、鼻から出る泡を描きます。'
+ + '「ねている」ポーズと組み合わせると、いねむりしているように見えます。'
+ + '泡は鼻の横に出るので、位置は「よこの位置」「たかさ」で調整できます。'));
+
// --- mouth
faceSection = section(tabFace, '口');
@@ -2355,6 +2390,10 @@ export function buildPanel(app, root) {
for (const [key, widget] of Object.entries(headMarkSliders)) widget.set(state.face.eyes.headMark[key]);
headMarkColor.set(state.face.eyes.headMark.color);
headMarkColor2.set(state.face.eyes.headMark.color2 ?? '#ffffff');
+ snotToggle.set(state.face.eyes.snot.enabled === true);
+ snotSize.set(state.face.eyes.snot.size);
+ for (const [key, widget] of Object.entries(snotSliders)) widget.set(state.face.eyes.snot[key]);
+ snotColor.set(state.face.eyes.snot.color);
});
syncers.push(() => {
diff --git a/public/bluebey-studio/src/presets.js b/public/bluebey-studio/src/presets.js
index b14a6e4..d967af2 100644
--- a/public/bluebey-studio/src/presets.js
+++ b/public/bluebey-studio/src/presets.js
@@ -209,6 +209,16 @@ export function defaultState() {
color: '#8a4fe0',
color2: '#ffffff',
},
+ // 鼻ちょうちん: the snot bubble someone sleeps with. Drawn into the same
+ // plate as the eyes, out to the side of the nose. Starts off.
+ snot: {
+ enabled: false,
+ size: 1,
+ offsetX: 0,
+ offsetY: 0,
+ color: '#e3ecff',
+ line: '#55386e',
+ },
// Eyebrows are drawn into the eye texture. Off by default, because the
// original character has no eyebrows.
brow: {
@@ -623,6 +633,20 @@ export const POSE_PRESETS = [
root: [0, 0.25, 0],
},
},
+ {
+ id: 'lie',
+ label: 'ねている',
+ pose: {
+ bones: {
+ // Rolled right over onto its side (Z is the sideways roll) - the body lies
+ // along the ground with the face turned up towards the camera.
+ master: [0, 0, 88],
+ // Both flippers rest against the body, loose.
+ 'arm.l': [-30, 0, 0],
+ 'arm.r': [-30, 0, 0],
+ },
+ },
+ },
{ id: 'side', label: 'よこむき', pose: { bones: { master: [0, 92, 0] } } },
{ id: 'turn', label: 'うしろむき', pose: { bones: { master: [0, 180, 0] } } },
{