From ee7c87a56252ec295119823a2821e5c1f8ec5163 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:37:46 +0900 Subject: bluebey-studio: ぎざぎざの歯の深さ、形の大きさ、涙スイッチ、まぶたの高さを整理 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/bluebey-studio/src/faceArt.js | 77 ++++++++++++++--------- public/bluebey-studio/src/gacha.js | 2 + public/bluebey-studio/src/main.js | 8 ++- public/bluebey-studio/src/panel.js | 119 +++++++++++++++++++++-------------- public/bluebey-studio/src/presets.js | 10 +-- 5 files changed, 132 insertions(+), 84 deletions(-) diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js index 5e99be5..01890b5 100644 --- a/public/bluebey-studio/src/faceArt.js +++ b/public/bluebey-studio/src/faceArt.js @@ -573,15 +573,19 @@ function lidPath(ctx, eye, amount, lower, flat = false) { } /** The "eyes shut" artwork: a line, a chevron or a happy arch. */ -function drawShutEye(ctx, eye, spec, style) { +function drawShutEye(ctx, eye, spec, style, shapeScale = 1, maxHalf = Infinity) { const layout = EYE_LAYOUT.closedLine; const { line, width } = style; + // 形の大きさ, clamped so the shape stays inside the white of the eye when it is + // shown (`maxHalf` is Infinity otherwise). + const k = (unitHalf) => Math.min(shapeScale, maxHalf / unitHalf); if (spec.closed === 'chevron') { // The chevron points *at* the nose, matching the original artwork. - const vertex = { x: eye.cx + eye.towardNose * layout.vertex, y: eye.cy + layout.offsetY - 8 }; - const armX = eye.cx - eye.towardNose * layout.armX; - const spread = layout.spread * (spec.spread ?? 1); + const scale = k(layout.armX); + const vertex = { x: eye.cx + eye.towardNose * layout.vertex * scale, y: eye.cy + layout.offsetY * scale - 8 * scale }; + const armX = eye.cx - eye.towardNose * layout.armX * scale; + const spread = layout.spread * (spec.spread ?? 1) * scale; strokePolyline(ctx, [vertex, { x: armX, y: vertex.y - spread }], { color: line, width }); strokePolyline(ctx, [vertex, { x: armX, y: vertex.y + spread }], { color: line, width }); return; @@ -591,8 +595,7 @@ function drawShutEye(ctx, eye, spec, style) { // A real "3": an upper bowl and a lower bowl that meet at a single pinch on // the nose side. Stacking two C's instead made the two lobes sit on top of // each other; joining them at a point is what makes it read as the digit. - // `length` scales the whole shape. - const r = 34 * (spec.length ?? 1); + const r = 34 * k(1.7 * 34); // Which way the digit faces. It used to follow the nose side, which made the // pair a mirror image; each eye can now be set either way, because a pair of // mirrored 3s does not always read the way you want. @@ -609,10 +612,11 @@ function drawShutEye(ctx, eye, spec, style) { } if (spec.closed === 'arch') { - const edges = { x: eye.r, y: EYE_LAYOUT.arch.edgesUp }; + const scale = k(eye.r); + const edges = { x: eye.r * scale, y: EYE_LAYOUT.arch.edgesUp * scale }; const points = quadraticPoints( { x: eye.cx - edges.x, y: eye.cy + edges.y }, - { x: eye.cx, y: eye.cy + edges.y - EYE_LAYOUT.arch.apexUp * 2 }, + { x: eye.cx, y: eye.cy + edges.y - EYE_LAYOUT.arch.apexUp * 2 * scale }, { x: eye.cx + edges.x, y: eye.cy + edges.y }, 24, ); @@ -625,17 +629,19 @@ function drawShutEye(ctx, eye, spec, style) { // strokes share one endpoint instead - a ">" or bird's-foot shape, which is // how manga draws a happily squeezed-shut eye. const count = clamp(Math.round(spec.closedLines ?? 1), 1, 3); + const scale = k(layout.length / 2); if (count >= 2) { - const vertex = { x: eye.cx + eye.towardNose * layout.vertex, y: eye.cy + layout.offsetY - 8 }; + const vertex = { x: eye.cx + eye.towardNose * layout.vertex * scale, y: eye.cy + layout.offsetY * scale - 8 * scale }; // Reach as far as the single line does, not just a short stub: the arms are // the *long* form of the same eyelid the one-line version covers. - const outX = -eye.towardNose * layout.armSpan; + const outX = -eye.towardNose * layout.armSpan * scale; + const spread = layout.spread * scale; const arms = count === 2 - ? [{ x: outX, y: -layout.spread }, { x: outX, y: layout.spread }] + ? [{ x: outX, y: -spread }, { x: outX, y: spread }] : [ - { x: outX, y: -layout.spread }, - { x: -eye.towardNose * Math.hypot(layout.armSpan, layout.spread), y: 0 }, - { x: outX, y: layout.spread }, + { x: outX, y: -spread }, + { x: -eye.towardNose * Math.hypot(layout.armSpan, layout.spread) * scale, y: 0 }, + { x: outX, y: spread }, ]; for (const arm of arms) { strokePolyline(ctx, [vertex, { x: vertex.x + arm.x, y: vertex.y + arm.y }], { color: line, width }); @@ -643,11 +649,11 @@ function drawShutEye(ctx, eye, spec, style) { return; } - const half = (layout.length / 2) * (spec.length ?? 1); - const midX = eye.cx - eye.towardNose * layout.offsetX; - const midY = eye.cy + layout.offsetY; + const half = (layout.length / 2) * (spec.length ?? 1) * scale; + const midX = eye.cx - eye.towardNose * layout.offsetX * scale; + const midY = eye.cy + layout.offsetY * scale; const angle = ((layout.slantDeg * (spec.slant ?? 1) * eye.towardNose) * Math.PI) / 180; - const bow = (layout.bow ?? 0) * (spec.slant ?? 1); + const bow = (layout.bow ?? 0) * (spec.slant ?? 1) * scale; const from = { x: midX - Math.cos(angle) * half, y: midY - Math.sin(angle) * half }; const to = { x: midX + Math.cos(angle) * half, y: midY + Math.sin(angle) * half }; const points = quadraticPoints(from, { x: midX, y: midY + bow * 2 }, to, 20); @@ -868,11 +874,12 @@ function drawHeadCover(ctx, spec, limits) { const boundary = (x) => { const t = clamp((x - cx) / halfSpan, -1, 1); // ぎざぎざ: a jagged, saw-toothed hairline. A triangle wave across the front - // gives the teeth, and `size` scales how far they reach down. + // gives the teeth; `teeth` is how far they reach down (its own slider), so + // the 大きさ slider only moves the hairline up and down. if (shape === 'fringe') { const teeth = 8; const saw = Math.abs((((t * teeth) % 2) + 2) % 2 - 1); // 0..1 triangle - return base + 200 * size * saw; + return base + (spec.teeth ?? 0.12) * 600 * saw; } // はちわれ: the same V upside down - a widow's peak. if (shape === 'fringe-up') return base - 320 * Math.max(0, 1 - Math.abs(t) * 1.5); @@ -946,10 +953,11 @@ export function drawHeadCoverWrap(ctx, spec, layout) { // the three peaks and two valleys of an "M". `t` is -1..1 across the front. if (shape === 'fringe') { // ぎざぎざ: a jagged, saw-toothed hairline. A triangle wave across the front - // gives the teeth, and `size` scales how far they reach down. + // gives the teeth; `teeth` is how far they reach down (its own slider), so + // the 大きさ slider only moves the hairline up and down. const teeth = 8; const saw = Math.abs((((t * teeth) % 2) + 2) % 2 - 1); // 0..1 triangle - return base + 0.20 * size * saw; + return base + (spec.teeth ?? 0.12) * saw; } // はちわれ: the same V upside down - a widow's peak. if (shape === 'fringe-up') return base - 0.18 * Math.max(0, 1 - Math.abs(t) * 1.5); @@ -1337,6 +1345,14 @@ export function drawEyes(ctx, p) { const isHeart = (spec.irisShape ?? 'circle') === 'heart'; // The lower lid is per eye (falling back to the shared value). const lowerLid = clamp(spec.lowerLid ?? style.lowerLid ?? 0, 0, 1); + // 形の大きさ: scales the drawn shape (shut lines, the 3, the arch, the heart). + const shapeScale = clamp(spec.shapeScale ?? 1, 0.4, 2); + // `white` is tri-state: an open eye shows its white, a shut eye or a heart + // does not, unless the switch forces it one way. + const showWhite = spec.white != null ? spec.white === true : (open > 0.02 && !isHeart); + // While the white is shown, a shape that would poke out of the eyeball is + // scaled back inside it; `shapeHalfLimit` is the half-size it may reach. + const shapeHalfLimit = showWhite ? eye.r * 0.92 : Infinity; drawBrow(ctx, eye, spec, style, limits); @@ -1346,7 +1362,8 @@ export function drawEyes(ctx, p) { // amount, the height and the tilt are all *per eye*, because one eye crying // while the other does not is a real expression. const drawTear = () => { - const amount = clamp(spec.tear ?? 0, 0, 1.6); + if (spec.tearOn !== true) return; + const amount = clamp(spec.tear ?? 0.3, 0, 1.6); if (amount <= 0.01) return; const size = EYE_LAYOUT.tear.size * amount; const tearX = eye.cx + clamp(spec.tearX ?? 0, -400, 400) - layout.towardNose * EYE_LAYOUT.tear.offsetX; @@ -1376,8 +1393,8 @@ export function drawEyes(ctx, p) { if (open <= 0.02) { // A shut eye can still be given a white backing (the "白目" switch), which // reads as a line drawn across an eyeball rather than a bare line. - if (!line && spec.white === true) fillCircle(ctx, eye.cx, eye.cy, eye.r, white); - drawShutEye(ctx, eye, spec, lineStyle); + if (!line && showWhite) fillCircle(ctx, eye.cx, eye.cy, eye.r, white); + drawShutEye(ctx, eye, spec, lineStyle, shapeScale, shapeHalfLimit); // The glasses sit over the shut-eye artwork too, but under the tear below. drawGlasses(ctx, eye, line, style, limits); drawTear(); @@ -1400,11 +1417,7 @@ export function drawEyes(ctx, p) { dy = (dy / dist) * eyeTravel; } - // The white of the eye. `spec.white` is per eye and tri-state: `null` keeps - // the usual behaviour (an open eye shows its white, a heart does not), while - // true/false force it on or off - so an open eye can lose its white, and a - // shut eye or a heart can gain it. - const showWhite = spec.white == null ? !isHeart : spec.white === true; + // The white of the eye (see `showWhite` above). if (!line && showWhite) fillCircle(ctx, eye.cx, eye.cy, eye.r, white); const irisX = eye.cx + dx; const irisY = eye.cy + dy; @@ -1414,7 +1427,9 @@ export function drawEyes(ctx, p) { // A heart is not an eyeball, so it may grow well past the eye: the plate // keeps the whole canvas, and the eyes plate is drawn over the mouth, so a // big heart has room. The cap only stops it from running off the canvas. - const heartRadius = Math.min(irisRadius * heartScale, EYE_LAYOUT.radius * 2.1); + // When the white is shown, it is capped to the eyeball instead. + let heartRadius = Math.min(irisRadius * heartScale * shapeScale, EYE_LAYOUT.radius * 2.1); + if (showWhite) heartRadius = Math.min(heartRadius, shapeHalfLimit); fillHeart(ctx, irisX, irisY, heartRadius, line ? lineColor : heartColor); } else { fillCircle(ctx, irisX, irisY, irisRadius, line ? lineColor : irisColor); diff --git a/public/bluebey-studio/src/gacha.js b/public/bluebey-studio/src/gacha.js index 2b3877e..b1df7a4 100644 --- a/public/bluebey-studio/src/gacha.js +++ b/public/bluebey-studio/src/gacha.js @@ -318,6 +318,8 @@ export function randomFace(random) { const amount = round2(rand(random, mood.tear[0], mood.tear[1])); eyes.left.tear = amount; eyes.right.tear = amount; + eyes.left.tearOn = true; + eyes.right.tearOn = true; } if (mood.lowerLid) { diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js index 0008d48..6b8d005 100644 --- a/public/bluebey-studio/src/main.js +++ b/public/bluebey-studio/src/main.js @@ -1638,8 +1638,12 @@ async function init() { // eyes show their white; the shared 光彩 switch decides the glint). white: own.white ?? null, highlight: own.highlight ?? null, - // The tears are per eye, so one eye can cry on its own. - tear: own.tear ?? 0, + // 形の大きさ scales the drawn eye shape (the shut lines, the 3, the arch) + // and the heart. + shapeScale: own.shapeScale ?? 1, + // The tears are per eye, and only drawn when their own switch is on. + tearOn: own.tearOn === true, + tear: own.tear ?? 0.3, tearY: own.tearY ?? 0, // Where this eye sits, and where its teardrop hangs, can be nudged one // side at a time (an asymmetric face). diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index d95826f..7d6c303 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -523,10 +523,14 @@ export function buildPanel(app, root) { const group = eyeGroup(mode === 'both' ? '両目' : (mode === 'left' ? '左の目' : '右の目')); const open = slider({ - label: '目の開き', min: 0, max: 1, step: 0.01, value: 1, + label: '上まぶたの高さ', min: 0, max: 1, step: 0.01, value: 1, format: (v) => (v < 0.02 ? '閉じ' : v > 0.98 ? '開き' : `${Math.round(v * 100)}%`), onInput: (v) => { write((e) => { e.open = v; }); touched(); }, }); + const lowerLid = slider({ + label: '下まぶたの高さ', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), + onInput: (v) => { write((e) => { e.lowerLid = v; }); touched(); }, + }); const shape = segmented({ label: '目の形', options: [ @@ -539,6 +543,10 @@ export function buildPanel(app, root) { value: 'open', onChange: (v) => { write((e) => applyEyeShape(e, v)); touched(); }, }); + const shapeScale = slider({ + label: '形の大きさ', min: 0.5, max: 1.8, step: 0.01, value: 1, format: (v) => v.toFixed(2), + onInput: (v) => { write((e) => { e.shapeScale = v; }); touched(); }, + }); const closedLines = segmented({ label: '線の数', options: [ @@ -569,10 +577,6 @@ export function buildPanel(app, root) { value: true, onChange: (v) => { write((e) => { e.highlight = v; }); touched(); }, }); - const eyeX = slider({ - label: '横位置', min: -200, max: 200, step: 1, value: 0, format: (v) => `${Math.round(v)}`, - onInput: (v) => { write((e) => { e.eyeX = v; }); touched(); }, - }); const pad = xyPad({ value: { x: 0, y: 0 }, center: 'eye', @@ -580,6 +584,11 @@ export function buildPanel(app, root) { }); // 涙: per eye. Linked, one set drives both - size and height match, while the // sideways position and the tilt mirror so the pair stays symmetric. + const tearOn = check({ + label: '涙を出す', + value: false, + onChange: (v) => { write((e) => { e.tearOn = v; }); touched(); }, + }); const writeTear = (patch) => { Object.assign(state.face.eyes[primary], patch); if (mode === 'both') { @@ -591,7 +600,7 @@ export function buildPanel(app, root) { touched(); }; const tear = slider({ - label: '涙', min: 0, max: 1.6, step: 0.01, value: 0, format: (v) => v.toFixed(2), + label: '涙の大きさ', min: 0.3, max: 1.6, step: 0.01, value: 0.3, format: (v) => v.toFixed(2), onInput: (v) => writeTear({ tear: v }), }); const tearY = slider({ @@ -608,36 +617,44 @@ export function buildPanel(app, root) { }); group.add(open); + group.add(lowerLid); group.add(shape); + group.add(shapeScale); group.add(closedLines); group.add(threeFlip); group.add(controlRow(null, white.el, { wide: true })); group.add(controlRow(null, highlight.el, { wide: true })); - group.add(eyeX); - group.add(controlRow('目線', h('div', { class: 'pad-wrap' }, pad.el, - h('p', { class: 'hint', text: 'ドラッグで目線。ダブルクリックで正面に戻ります。' })))); + group.add(controlRow(null, tearOn.el, { wide: true })); group.add(tear); group.add(tearY); group.add(tearX); group.add(tearTilt); + group.add(controlRow('目線', h('div', { class: 'pad-wrap' }, pad.el, + h('p', { class: 'hint', text: 'ドラッグで目線。ダブルクリックで正面に戻ります。' })))); const syncOne = () => { const e = state.face.eyes[primary]; const shapeValue = eyeShapeOf(e); open.set(e.open); + lowerLid.set(e.lowerLid ?? state.face.eyes.lowerLid ?? 0); shape.set(shapeValue); + shapeScale.set(e.shapeScale ?? 1); closedLines.set(String(e.closedLines ?? 1)); closedLines.el.style.display = shapeValue === 'line' ? '' : 'none'; threeFlip.set(e.threeFlip ? 'flip' : 'normal'); threeFlip.el.style.display = shapeValue === 'three' ? '' : 'none'; white.set(eyeWhiteOf(e)); highlight.set(eyeHighlightOf(e)); - eyeX.set(e.eyeX ?? 0); - pad.set({ x: e.lookX ?? 0, y: e.lookY ?? 0 }); - tear.set(e.tear ?? 0); + const tearShown = e.tearOn === true; + tearOn.set(tearShown); + for (const widget of [tear, tearY, tearX, tearTilt]) { + widget.el.style.display = tearShown ? '' : 'none'; + } + tear.set(e.tear ?? 0.3); tearY.set(e.tearY ?? 0); tearX.set(e.tearX ?? 0); tearTilt.set(e.tearTilt ?? 0); + pad.set({ x: e.lookX ?? 0, y: e.lookY ?? 0 }); }; return { group, sync: syncOne }; }; @@ -651,40 +668,20 @@ export function buildPanel(app, root) { faceSection.add(leftControls.group); faceSection.add(rightControls.group); - // 下まぶた (per eye) lives in こまかい設定. + // 横位置 (per eye) rarely changes, so it lives in こまかい設定. const eyeAdvancedWidgets = {}; for (const key of ['left', 'right']) { const label = key === 'left' ? '左の目' : '右の目'; - const lowerLid = slider({ - label: `${label}の下まぶた`, min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), - onInput: (value) => { state.face.eyes[key].lowerLid = value; app.actions.refresh('face'); }, + 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'); }, }); - eyeAdvancedWidgets[key] = { lowerLid }; - tearWidgets.push(lowerLid.el); + eyeAdvancedWidgets[key] = { eyeX }; + tearWidgets.push(eyeX.el); } - // One button to put every eye setting back to the artwork's defaults. The - // artwork *source* is kept: resetting the numbers should not throw away a - // hand-drawn image the user loaded. - faceSection.add(buttons({ - items: [{ - id: 'eyesReset', - label: '目の設定をリセット', - onClick: () => { - const source = state.face.eyes.source; - state.face.eyes = { ...defaultState().face.eyes, source }; - app.actions.refresh('face'); - sync(); - }, - }], - })); - - faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、開き・形・白目・光彩・目線が両目でそろいます。' - + 'はずすと「左の目」「右の目」の設定がそれぞれに出て、目線のパッドも2つになります。' - + '涙のよこ位置と傾きは左右で反転し、線対称になります。')); - // 目を細める (and its undo) grouped with the lid shape and tilt that shape the - // squint, so the whole narrowed-eye look sits together. + // squint. It sits right under the lid-height sliders it drives. const squintGroup = eyeGroup('目を細める'); squintGroup.add(buttons({ items: [ @@ -693,10 +690,11 @@ export function buildPanel(app, root) { label: '目を細める', onClick: () => { // The upper lid comes partway down and the lower lid rises to meet it. - // `open` is per eye (kept in step here); `lowerLid` is shared. + // Both lids are per eye, so both eyes are set here. state.face.eyes.left.open = 0.5; state.face.eyes.right.open = 0.5; - state.face.eyes.lowerLid = 0.5; + state.face.eyes.left.lowerLid = 0.5; + state.face.eyes.right.lowerLid = 0.5; app.actions.refresh('face'); sync(); }, @@ -707,7 +705,8 @@ export function buildPanel(app, root) { onClick: () => { state.face.eyes.left.open = 1; state.face.eyes.right.open = 1; - state.face.eyes.lowerLid = 0; + state.face.eyes.left.lowerLid = 0; + state.face.eyes.right.lowerLid = 0; app.actions.refresh('face'); sync(); }, @@ -733,6 +732,25 @@ export function buildPanel(app, root) { + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。')); faceSection.add(squintGroup); + // One button to put every eye setting back to the artwork's defaults. The + // artwork *source* is kept: resetting the numbers should not throw away a + // hand-drawn image the user loaded. + faceSection.add(buttons({ + items: [{ + id: 'eyesReset', + label: '目の設定をリセット', + onClick: () => { + const source = state.face.eyes.source; + state.face.eyes = { ...defaultState().face.eyes, source }; + app.actions.refresh('face'); + sync(); + }, + }], + })); + + faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、上まぶた・下まぶた・形・白目・光彩・目線が両目でそろいます。' + + 'はずすと「左の目」「右の目」の設定がそれぞれに出て、目線のパッドも2つになります。')); + const eyeAdvanced = details(faceSection.body, 'こまかい設定(瞳・色・まぶたの太さ・左右べつの設定・手描き)'); const irisScaleSlider = slider({ label: '瞳の大きさ', min: 0.4, max: 1.4, step: 0.01, value: 1, format: (v) => v.toFixed(2), @@ -761,8 +779,8 @@ export function buildPanel(app, root) { const eyeColorBox = details(faceSection.body, '色(白目・瞳・まぶたの線)'); for (const widget of Object.values(eyeColors)) eyeColorBox.add(widget.el); - // 下まぶた (per eye) lives here with the other extras. - const tearBox = details(eyeAdvanced.body, '下まぶた(左右べつ)'); + // 横位置 (per eye) lives here with the other extras. + const tearBox = details(eyeAdvanced.body, '横位置(左右べつ)'); for (const el of tearWidgets) tearBox.add(el); const eyeSource = selectField({ @@ -1122,7 +1140,7 @@ export function buildPanel(app, root) { { value: 'curve', label: 'カーブ' }, ], value: 'off', - onChange: (value) => { state.face.eyes.headMark.shape = value; app.actions.refresh('face'); }, + onChange: (value) => { state.face.eyes.headMark.shape = value; app.actions.refresh('face'); sync(); }, }); faceSection.add(headMarkShape.el); @@ -1138,6 +1156,10 @@ export function buildPanel(app, root) { label: '大きさ', min: 0.05, max: 2.2, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.eyes.headMark.size = v; app.actions.refresh('face'); }, }), + teeth: slider({ + label: '歯の深さ', min: 0.02, max: 0.4, step: 0.01, value: 0.12, format: (v) => v.toFixed(2), + onInput: (v) => { state.face.eyes.headMark.teeth = v; app.actions.refresh('face'); }, + }), offsetX: slider({ label: 'よこの位置', min: -260, max: 260, step: 1, value: 0, format: (v) => `${Math.round(v)}`, onInput: (v) => { state.face.eyes.headMark.offsetX = v; app.actions.refresh('face'); }, @@ -1148,6 +1170,8 @@ export function buildPanel(app, root) { }), }; faceSection.add(headMarkSliders.size.el); + faceSection.add(headMarkSliders.teeth.el); + //「歯の深さ」はぎざぎざのときだけ意味があるので、それ以外では隠します。 const headMarkBox = details(faceSection.body, 'こまかい設定(位置・色)'); headMarkBox.add(headMarkSliders.offsetX.el); headMarkBox.add(headMarkSliders.offsetY.el); @@ -1163,7 +1187,7 @@ export function buildPanel(app, root) { }); headMarkBox.add(headMarkColor.el); headMarkBox.add(headMarkColor2.el); - headMarkBox.add(hint('「かぶせ」は頭にかぶせる丸い髪、「ぎざぎざ」は生え際がギザギザにとがった髪、' + headMarkBox.add(hint('「かぶせ」は頭にかぶせる丸い髪、「ぎざぎざ」は生え際がギザギザにとがった髪(「歯の深さ」でギザギザの深さを変えられます)、' + '「はちわれ」は中央が鋭く持ち上がった生え際、「カーブ」はドラえもんのように単純にカーブした髪です。' + '「たかさ」で生え際を目の中心あたりまで下げられます。' + '「髪を目の前に出す」を切ると、髪が目の後ろに回ります。')); @@ -2747,7 +2771,7 @@ export function buildPanel(app, root) { for (const key of ['left', 'right']) { const eye = state.face.eyes[key]; const w = eyeAdvancedWidgets[key]; - w.lowerLid.set(eye.lowerLid ?? state.face.eyes.lowerLid ?? 0); + w.eyeX.set(eye.eyeX ?? 0); } irisScaleSlider.set(state.face.eyes.irisScale); @@ -2786,6 +2810,7 @@ export function buildPanel(app, root) { for (const [key, widget] of Object.entries(cheekSliders)) widget.set(state.face.eyes.cheeks[key]); cheekColor.set(state.face.eyes.cheeks.color); headMarkShape.set(state.face.eyes.headMark.shape ?? 'off'); + headMarkSliders.teeth.el.style.display = (state.face.eyes.headMark.shape === 'fringe') ? '' : 'none'; headMarkOverEyes.set(state.face.eyes.headMark.overEyes !== false); for (const [key, widget] of Object.entries(headMarkSliders)) widget.set(state.face.eyes.headMark[key]); headMarkColor.set(state.face.eyes.headMark.color); diff --git a/public/bluebey-studio/src/presets.js b/public/bluebey-studio/src/presets.js index 35d1276..66f4fab 100644 --- a/public/bluebey-studio/src/presets.js +++ b/public/bluebey-studio/src/presets.js @@ -200,8 +200,8 @@ export function defaultState() { eyes: { source: 'parametric', // parametric | opened | closed | ... (hand-drawn) linked: true, // move both eyes together - left: { open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null }, - right: { open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null }, + left: { open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0.3, tearOn: false, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null, shapeScale: 1, lowerLid: null }, + right: { open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0.3, tearOn: false, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null, shapeScale: 1, lowerLid: null }, irisScale: 1, lookMax: 1.2, highlight: true, @@ -234,6 +234,8 @@ export function defaultState() { headMark: { shape: 'off', size: 1, + // ぎざぎざ only: how far the saw teeth reach down. + teeth: 0.12, offsetX: 0, offsetY: 0, overEyes: true, @@ -540,8 +542,8 @@ export const FACE_PRESETS = [ // Big tears that hang low and lean outwards - the classic "crying hard" // look. The sideways position and the tilt are mirrored between the eyes, // so the pair stays symmetric. - left: { open: 0.72, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearY: 26, tearTilt: -20 }, - right: { open: 0.72, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearY: 26, tearTilt: 20 }, + left: { open: 0.72, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: -20 }, + right: { open: 0.72, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: 20 }, irisScale: 1.02, lowerLid: 0.42, highlight: true, -- cgit v1.3.1