aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio')
-rw-r--r--public/bluebey-studio/src/faceArt.js77
-rw-r--r--public/bluebey-studio/src/gacha.js2
-rw-r--r--public/bluebey-studio/src/main.js8
-rw-r--r--public/bluebey-studio/src/panel.js119
-rw-r--r--public/bluebey-studio/src/presets.js10
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,