aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/presets.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-07 23:09:40 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-07 23:09:40 +0900
commitbc2821adb11a30244fc4663f4fafb756877d9508 (patch)
treefea8d30d2eb1e67f895097dbf7ec4d152361243e /public/bluebey-studio/src/presets.js
parent0d50c5ede0812ba7b67b775c9cd0bf52d7b65e02 (diff)
bluebey-studio: public/ の外へ移動し非公開化
Diffstat (limited to 'public/bluebey-studio/src/presets.js')
-rw-r--r--public/bluebey-studio/src/presets.js892
1 files changed, 0 insertions, 892 deletions
diff --git a/public/bluebey-studio/src/presets.js b/public/bluebey-studio/src/presets.js
deleted file mode 100644
index e4fe49f..0000000
--- a/public/bluebey-studio/src/presets.js
+++ /dev/null
@@ -1,892 +0,0 @@
-/**
- * The complete, serialisable studio state plus the preset libraries.
- *
- * Everything here is plain JSON: "設定を保存" writes exactly this object, and
- * "設定を読み込む" merges it back in, so a look can be archived and shared.
- *
- * Eye parameters are named after the character's own left/right, NOT the
- * viewer's: `left` is the eye on the model's +x side, which is the half of the
- * eye texture with u > 0.5. In a front view that eye appears on the right of
- * the screen.
- */
-
-/**
- * One speech bubble's defaults.
- *
- * A factory rather than a constant, because there are two bubbles (see
- * `caption2`) and they must not share one object - editing one would edit both.
- */
-function captionDefaults() {
- return {
- enabled: false,
- text: 'こんにちは、ぶるべーです。',
- // Anchor of the bubble's box, as a fraction of the image (0..1).
- x: 0.58,
- y: 0.16,
- maxWidth: 0.36, // fraction of the image width for the text column
- bubble: 'round', // round | rect | shout | none
- tail: 'left', // left | right | top | bottom | topLeft | topRight | bottomLeft | bottomRight | none
- fontSize: 34, // pixels at 1x; scales with the export
- lineHeight: 1.42,
- padding: 18,
- radius: 24,
- textColor: '#3f2b52',
- bubbleColor: '#ffffff',
- borderColor: '#55386e',
- borderWidth: 4,
- bold: false,
- align: 'left', // left | center | right
- vertical: false, // 縦書き (columns run right to left)
- font: 'rounded', // rounded (vendored) | system
- };
-}
-
-/**
- * ナレーション (a narration box): a plain rectangular panel of vertical text, the
- * manga narration style. Same shape as a bubble, with a rect box, no tail, and
- * 縦書き on by default.
- */
-function narrationDefaults() {
- return {
- enabled: false,
- text: 'ここで、ぶるべーは考えた。',
- x: 0.06,
- y: 0.08,
- maxWidth: 0.5,
- bubble: 'rect',
- tail: 'none',
- fontSize: 30,
- lineHeight: 1.35,
- padding: 16,
- radius: 8,
- textColor: '#2b2433',
- bubbleColor: '#ffffff',
- borderColor: '#2b2433',
- borderWidth: 3,
- bold: false,
- align: 'left',
- vertical: true,
- font: 'rounded',
- };
-}
-
-/**
- * Per-kind starting size and height for ひげ, applied when a kind is picked. The
- * textured kinds are drawings, so they are drawn at a different scale from the
- * built-in strokes; a kind with no entry resets to the neutral values.
- */
-export const BEARD_KIND_DEFAULTS = {
- scotch: { size: 0.46, offsetY: 74 },
- kaiser: { size: 1, offsetY: 80, spacing: -10 },
-};
-
-/**
- * Per-shape starting values for 髪 (headMark), applied when a shape is picked.
- * `teeth` only matters for ぎざぎざ, so its default is set here rather than on
- * the shared `size`/`teeth` fields.
- */
-export const HEAD_MARK_KIND_DEFAULTS = {
- fringe: { size: 0.83, teeth: 0.06 },
-};
-
-/**
- * 見えない壁 (the invisible wall) settings. The studio offers two, so a corner (or
- * two opposite cuts) can be made. A wall's position is kept *relative to the
- * character*, so moving ぶるべー carries its walls along (see src/clip.js).
- */
-function wallDefaults() {
- return {
- on: false,
- x: 0, y: 0, z: 0, // a point the plane passes through, relative to the character
- yaw: 0, // turn about Y, degrees
- tilt: 0, // lean about X, degrees
- // The guide is the only part of the wall that is ever *drawn* (the wall
- // itself writes depth but no colour, so it hides what is behind it
- // without being visible). `guide` shows that rectangle while placing,
- // which is also when it can be dragged in the viewport.
- guide: false,
- size: 1, // the wall's size (a real width and height, not a clip)
- };
-}
-
-export function defaultState() {
- return {
- version: 1,
- view: {
- projection: 'persp', // persp | ortho
- fov: 30,
- azimuth: 0, // degrees, 0 = looking at the face
- polar: 76, // degrees from +y, 90 = level with the target
- distance: 0, // 0 = fit to the model automatically
- orthoHeight: 0, // 0 = fit to the model automatically
- targetY: 0, // 0 = centre of the model
- // The mouse orbit and pan write their own target back here (see
- // ViewRig.captureInto), so refreshing the view - or sharing a link - keeps
- // the camera the user actually framed.
- targetX: 0,
- targetZ: 0,
- autoRotate: false,
- autoRotateSpeed: 0.8,
- // solid | transparent | preset | image | effect | camera
- // preset = one of the CC0 backdrops in assets/backgrounds
- // image = something the user loaded (data URL)
- // effect = a procedurally drawn manga effect line (no file at all)
- // camera = the phone's camera, for the AR mode
- background: 'solid',
- backgroundColor: '#ffffff',
- backgroundPreset: 'autumn_park',
- backgroundEffect: 'focus', // focus | fall | speed | ellipse
- backgroundImage: null, // data URL, set by 「画像を読み込む」
- backgroundFit: 'cover', // cover | contain | stretch | tile
- backgroundBlur: 0, // px
- backgroundDarken: 0, // 0..1 dark veil, helps the character read
- backgroundScale: 1,
- backgroundOffset: { x: 0, y: 0 },
- // AR: which camera, and whether to mirror (front camera feels mirrored)
- cameraFacing: 'environment', // environment | user
- cameraMirror: false,
- gyro: false, // look around by turning the phone
- },
- render: {
- style: 'real', // real | flat | lineart | outline
- outline: true,
- outlineWidth: 0.022,
- outlineColor: '#2a1e33',
- paper: '#ffffff',
- shadow: true,
- // The soft round blob at the character's feet. Off by default: it sat at the
- // origin and stayed behind when the character was moved. See look.js.
- contactBlob: false,
- pngScale: 2,
- pngTransparent: false,
- svgWidth: 2048,
- lightAzimuth: -38,
- lightElevation: 40,
- lightIntensity: 2.1,
- ambient: 0.9,
- // The colour of the main light and of the ambient (sky) light.
- lightColor: '#ffffff',
- ambientColor: '#ffffff',
- exposure: 1,
- // Body colours. A theme fills these in; the colour pickers then adjust
- // them one part at a time, so a theme is a starting point, not a mode.
- theme: 'original',
- colors: {
- body: '#c8b0f0',
- accent: '#8a4fe0',
- nose: '#7a4fb0',
- leaf: '#a6dd6a',
- vein: '#7fbf3f',
- feet: '#7a4fb0',
- },
- // Mirror the whole character, for laying out a panel that faces its text.
- mirror: false,
- // Shadows: the soft blob under the feet, plus the cast shadow.
- shadowOpacity: 0.22,
- shadowSoftness: 1.6,
- shadowOffset: 0,
- contactShadow: false, // blob only, no cast shadow
- // Lighting environment. 'room' is three's RoomEnvironment; 'gradient' is
- // the tiny procedural sky the studio always had.
- environment: 'gradient', // gradient | room | none
- envIntensity: 1,
- // Hand-drawn line work (SVG export and the outline pass).
- handDrawn: 0, // 0 = off, 1 = a lot
- handDrawnSeed: 7,
- handDrawnScale: 40,
- handDrawnPasses: 1,
- // How the outline is drawn - the two methods take the parts they are good
- // at. See MODEL-GUIDE.md §5 and src/outline.js.
- //
- // 'screen' (the default) hands the leaves to the screen-space edge pass,
- // because a hull cannot outline a shell that thin: the expanded
- // front and back cross inside it and the line breaks up. Every
- // other part keeps its hull, whose line is computed from the
- // geometry and so comes out smooth, where the pixel grid would
- // make it stepped.
- // 'hull' the inverted-hull copy everywhere - cheap, and the way it was
- // done before, but the leaves then come out as a tangle.
- outlineMethod: 'screen',
- outlinePixels: 2.4, // 'screen' only: line width in screen pixels
- // Whether a leaf gets a line where it meets the *body*. There is no right
- // answer here, so it is a setting:
- // on - the body counts as paper, so the leaves are outlined where they
- // emerge from it. Without this a line drawing cannot tell the
- // leaves and the body apart at all (both are paper).
- // off - the body blocks the line, so the leaf simply passes behind it and
- // nothing is drawn along the meeting. Cleaner, but the leaves and
- // the body merge into one white shape.
- leafBodyLine: true,
- // 見えない壁 (the invisible wall). Up to two placeable rectangles that hide
- // whatever is behind them, so the character can be buried in a wall and
- // only the rest of the body shows. See src/clip.js.
- wall: wallDefaults(),
- wall2: wallDefaults(),
- },
- pose: {
- bones: {}, // { boneName: [x, y, z] } in degrees, rest = 0,0,0
- root: [0, 0, 0], // whole-body offset in model units
- },
- anim: {
- mode: 'off', // off | idle | walk
- blink: true,
- blinkInterval: 3.4, // seconds between blinks
- lookAround: false,
- speed: 1,
- },
- face: {
- // 帽子 (a hat on the head). `kind` is one of HAT_LIBRARY in src/hats.js.
- // `x`/`z` slide it sideways / forwards, `height` lifts it off the head;
- // `tiltX` leans it forward/back and `tiltZ` leans it sideways.
- hat: { kind: 'none', custom: false, x: 0, z: 0, height: -0.7, tiltX: 0, tiltZ: 0 },
- eyes: {
- source: 'parametric', // parametric | opened | closed | ... (hand-drawn)
- linked: true, // move both eyes together
- left: { shape: 'open', 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, lidShape: null, lidWidth: null, lidTilt: null, lashes: null, lashAngle: null, lashPos: null },
- right: { shape: 'open', 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, lidShape: null, lidWidth: null, lidTilt: null, lashes: null, lashAngle: null, lashPos: null },
- irisScale: 1,
- lookMax: 1.2,
- highlight: true,
- lidWidth: 14,
- lowerLid: 0,
- lashes: false,
- lashAngle: 0,
- lashPos: 0,
- // How the closing eyelid is drawn: `curve` is the soft, rounded lid; `flat`
- // is a straight lid edge (the hard, narrowed look). `lidTilt` rotates the
- // narrowed eye. See faceArt.js.
- lidShape: 'curve',
- lidTilt: 0,
- heartScale: 2,
- heartColor: '#e0344f',
- // The tears live on each eye (see `eyes.left.tear`), so one eye can cry
- // on its own; only the colour is shared.
- tearColor: '#8fd8ff',
- // ほっぺ (a manga blush) and 頭の模様 (a hair-like head covering) are drawn
- // into the same plate as the eyes and brows. Both are generic devices,
- // not a copy of any one character, and both start off. The blush is always
- // four horizontal strokes, so there is no line-count field.
- cheeks: {
- enabled: false,
- size: 1,
- spacing: 0, // extra gap outwards from the eyes
- offsetY: 0, // extra drop below the eyes
- color: '#f6a6b8',
- },
- // The hair covering's `shape` is one of `off`, `cap`, `fringe`,
- // `fringe-up` or `curve`. `overEyes` puts the hair in front of the eyes
- // (bangs over the face); off leaves the eyes on top.
- headMark: {
- shape: 'off',
- size: 1,
- // ぎざぎざ only: how far the saw teeth reach down.
- teeth: 0.12,
- offsetX: 0,
- offsetY: 0,
- overEyes: true,
- 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.23,
- offsetX: -23,
- offsetY: 0,
- offsetZ: 16,
- color: '#e3ecff',
- },
- // ひげ: a mustache or beard worn under the nose. `shape` is one of `off`,
- // `scotch`, `kaiser`, `apron` or `cat`. `spacing` widens the gap between
- // the cat's left and right whiskers. Starts off.
- beard: {
- shape: 'off',
- size: 1,
- offsetY: 0,
- spacing: 0,
- // ねこ only: how far each whisker reaches (`length`) and how far apart the
- // three lines of one side sit (`lineGap`). Neither changes the thickness.
- length: 1,
- lineGap: 1,
- color: '#1c1624',
- },
- // Eyebrows are drawn into the eye texture. Off by default, because the
- // original character has no eyebrows.
- brow: {
- enabled: false,
- // When true and a drawing (assets/brows/gol-right.png) was loaded, the
- // brow is painted from that file (mirrored) instead of the strokes.
- image: false,
- angle: 0,
- height: 0,
- length: 1,
- thickness: 1,
- curve: 0.14,
- // How wide the nose-side end is: 1 = a plain stroke, 0 = a wedge that
- // comes to a point (the ゴル風 look). `spacing` widens the gap between
- // the two brows.
- taper: 1,
- spacing: 0,
- color: '#55386e',
- },
- // 眼鏡 / サングラス are drawn into the same texture as the eyes and brows,
- // so they travel with the head instead of being a separate 3D mesh. Off
- // by default. `kind` only picks which lens defaults the panel offers; the
- // drawing itself reads the colours and opacities below.
- glasses: {
- enabled: false,
- kind: 'glasses', // glasses | sunglasses
- frameColor: '#2a1e33',
- frameWidth: 1, // multiplier on the frame stroke (EYE_LAYOUT.lidStroke)
- lensColor: '#2b2433',
- lensOpacity: 0.22, // 0..1; 眼鏡 is nearly clear, サングラス dark
- lensGap: 0, // px, + moves the two lenses further apart
- scale: 1, // lens size multiplier
- offsetY: 0, // px, artwork units (positive = down)
- tilt: 0, // degrees, both lenses turn together
- },
- white: '#ffffff',
- iris: '#150e1b',
- line: '#55386e',
- },
- mouth: {
- source: 'parametric', // parametric | original
- visible: true,
- smile: 1, // 0 = straight line, 1 = the original smile, <0 = frown
- width: 1, // the smile line's length (the chord); also the O's width
- thickness: 1,
- open: 0, // 0 = closed lips, 1 = wide open
- tilt: 0, // degrees
- offsetY: 0, // texture pixels
- tongue: 0.96, // 0 hides it; scales the whole tongue
- tonguePos: 0.84, // 0..1 along the smile line
- corners: 0.71, // length of the corner marks, 0 hides them
- cornerAngle: -8, // degrees, tilts the corner marks
- round: 0, // >0 turns the mouth into a round "O"
- color: '#ff1a44',
- innerColor: '#4a0f1e',
- tongueColor: '#ff2d2d',
- cornerColor: '#725497',
- line: '#55386e',
- },
- },
- // ---------------------------------------------------------------- caption
- // The speech bubbles drawn next to the character when exporting. They live in
- // the state so they are saved, shared and undone like everything else. There
- // are two of them, so a line and a reply (or a narrator and a speaker) fit.
- caption: captionDefaults(),
- // The second bubble starts on the other side, so the two do not overlap.
- caption2: { ...captionDefaults(), text: '', x: 0.08, y: 0.44 },
- // A ナレーション box (vertical text, rect, no tail).
- narration: narrationDefaults(),
- // -------------------------------------------------------- 口パク (mouth flap)
- // The mouth moves as if talking, with no sound at all: the device's own
- // speech voices belong to the device, so a recording of them is not ours to
- // hand out. Only the *length* of the text matters here.
- mouthFlap: {
- text: 'こんにちは。小平市のぶるべーです。',
- rate: 1, // how fast the syllables come
- mouthGain: 1, // how wide the mouth opens
- },
- // ----------------------------------------------------------------- look at
- // Where the character should look (world units).
- lookAt: {
- enabled: false,
- x: 0,
- y: 2.6,
- z: 3,
- turnBody: false, // also rotate the whole body a little
- amount: 1,
- // The turn and the gaze are *remembered* rather than derived, so switching
- // the mode (or just 体も向ける) off leaves the character where it is instead
- // of snapping it back to the front. See `applyLookAt` in main.js.
- bodyYawDeg: 0,
- freeze: null,
- },
- // ------------------------------------------------------------------- props
- // 小物. Each entry is a procedural prop from src/props.js.
- props: {
- items: [],
- },
- // -------------------------------------------------------------------- 擬音
- // マンガのオノマトペ (擬音) as stickers over the picture. Each item is a crop
- // of a sheet - `{ id, sheet, sx, sy, sw, sh, x, y, w, rot, flip }` - and only
- // the sheet *name* is stored, never the bitmap, so a shared link stays small
- // (see src/gion.js for the geometry).
- gion: {
- items: [],
- },
- // ------------------------------------------------------------------- story
- // 4コマ / 紙芝居. Each panel is a small state patch (pose, face, caption).
- story: {
- columns: 1,
- gap: 12,
- padding: 20,
- sheetBackground: '#ffffff',
- // How big each captured panel is, as a multiple of the viewport. 2x doubles
- // each side, so the sheet PNG gets four times the pixels per panel.
- scale: 1.5,
- panels: [],
- },
- };
-}
-
-/** Expression presets. Each patches a copy of the default state. */
-export const FACE_PRESETS = [
- {
- id: 'normal',
- label: 'ふつう',
- face: {
- eyes: {
- left: { open: 1, lookX: 0, lookY: 0, closed: 'line' },
- right: { open: 1, lookX: 0, lookY: 0, closed: 'line' },
- irisScale: 1,
- highlight: true,
- },
- // Depth 1 = the hand-drawn original; the corners and tongue below are also
- // the settings that reproduce it (measured against the artwork).
- mouth: { smile: 1, width: 1, thickness: 1, open: 0, tilt: 0, offsetY: 0, tongue: 0.96, tonguePos: 0.84, corners: 0.71, cornerAngle: -8 },
- },
- },
- {
- id: 'laugh',
- label: 'にっこり',
- face: {
- eyes: {
- left: { shape: 'arch', open: 1, lookX: 0, lookY: 0, closed: 'arch' },
- right: { shape: 'arch', open: 1, lookX: 0, lookY: 0, closed: 'arch' },
- highlight: false,
- },
- // A laugh is a wide smile with squeezed-shut eyes. An *open* mouth on a
- // round mascot reads as a shout, so only びっくり keeps the round O.
- mouth: { smile: 1.15, width: 1.14, thickness: 1.05, open: 0, tilt: 0, offsetY: 0, round: 0, tongue: 0.96, corners: 1.25 },
- },
- },
- {
- id: 'eating',
- label: 'たべている',
- face: {
- eyes: {
- left: { open: 1, lookX: 0, lookY: 0.15, closed: 'line' },
- right: { open: 1, lookX: 0, lookY: 0.15, closed: 'line' },
- irisScale: 1,
- highlight: true,
- },
- // A small, cute round mouth (the other round mouth, with びっくり).
- mouth: { smile: 0.2, width: 0.8, thickness: 1.1, open: 0, tilt: 0, offsetY: 6, round: 0.58, tongue: 0.7, corners: 0 },
- },
- },
- {
- id: 'wink',
- label: 'ウインク',
- face: {
- eyes: {
- // The shut eye is the original `eyes-close-tight` artwork: three strokes
- // sharing one vertex, pointing at the nose.
- left: { shape: 'line3', open: 1, lookX: 0, lookY: 0, closed: 'line', closedLines: 3 },
- right: { open: 1, lookX: 0, lookY: 0, closed: 'line', closedLines: 1 },
- irisScale: 1,
- highlight: true,
- },
- mouth: { smile: 1.0, width: 1.02, thickness: 1, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 1 },
- },
- },
- {
- id: 'surprised',
- label: 'びっくり',
- face: {
- eyes: {
- left: { open: 1, lookX: 0, lookY: 0, closed: 'line' },
- right: { open: 1, lookX: 0, lookY: 0, closed: 'line' },
- irisScale: 0.74,
- lookMax: 0.35,
- highlight: true,
- },
- mouth: { smile: 0.2, width: 0.85, thickness: 1.1, open: 0, tilt: 0, offsetY: 0, round: 0.62, tongue: 0.25, corners: 0 },
- },
- },
- {
- id: 'sleepy',
- label: 'ねぼけ',
- face: {
- eyes: {
- left: { open: 0.42, lookX: 0, lookY: -0.22, closed: 'line' },
- right: { open: 0.42, lookX: 0, lookY: -0.22, closed: 'line' },
- irisScale: 1,
- highlight: true,
- // A sleeper's snot bubble, with the defaults from the panel.
- snot: { enabled: true },
- },
- mouth: { smile: 0.62, width: 0.88, thickness: 1, open: 0, tilt: -3, offsetY: 6, tongue: 0, corners: 0.8 },
- },
- },
- {
- id: 'shy',
- label: 'てれ',
- face: {
- eyes: {
- // Blushing, eyes half shut and looking away.
- left: { open: 0.72, lookX: -0.4, lookY: 0.08, closed: 'line' },
- right: { open: 0.72, lookX: -0.4, lookY: 0.08, closed: 'line' },
- cheeks: { enabled: true },
- irisScale: 1,
- highlight: true,
- },
- mouth: { smile: 0.5, width: 0.82, thickness: 1.05, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 0.5 },
- },
- },
- {
- id: 'sidelong',
- label: 'じと目',
- face: {
- eyes: {
- // Flat, narrowed lids tilted inwards - a sideways, unimpressed look.
- // 上まぶたの高さ is now independent of the lower lid, so `open` carries the
- // whole slit (it used to be measured from the raised lower lid).
- left: { open: 0.74, lookX: 0.4, lookY: -0.12, closed: 'line' },
- right: { open: 0.74, lookX: 0.4, lookY: -0.12, closed: 'line' },
- lowerLid: 0.42,
- lidShape: 'flat',
- lidTilt: 12,
- irisScale: 0.92,
- highlight: true,
- },
- mouth: { smile: 0.12, width: 0.78, thickness: 1.1, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 0 },
- },
- },
- {
- id: 'love',
- label: 'すき',
- face: {
- eyes: {
- left: { shape: 'heart', open: 1, lookX: -0.1, lookY: -0.3, closed: 'line', irisShape: 'heart', white: false },
- right: { shape: 'heart', open: 1, lookX: 0.1, lookY: -0.3, closed: 'line', irisShape: 'heart', white: false },
- irisScale: 1,
- // The white behind a heart would poke out around its lobes, so hide it.
- heartColor: '#e0344f',
- highlight: false,
- },
- mouth: { smile: 1.15, width: 1, thickness: 1.05, open: 0.16, tilt: 4, offsetY: 0, tongue: 0.9, corners: 1.3 },
- },
- },
- {
- id: 'grumpy',
- label: 'むっと',
- face: {
- eyes: {
- left: { open: 0.6, lookX: 0, lookY: -0.12, closed: 'line', closedLines: 2 },
- right: { open: 0.6, lookX: 0, lookY: -0.12, closed: 'line', closedLines: 2 },
- irisScale: 0.86,
- highlight: true,
- },
- mouth: { smile: -0.62, width: 0.84, thickness: 1.15, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 0 },
- },
- },
- {
- id: 'cry',
- label: 'ないてる',
- face: {
- eyes: {
- // 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.84, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: -20 },
- right: { open: 0.84, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: 20 },
- irisScale: 1.02,
- lowerLid: 0.42,
- highlight: true,
- },
- mouth: { smile: -0.5, width: 0.72, thickness: 1.25, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 0 },
- },
- },
-];
-
-/**
- * Poses. `bones` holds per-bone XYZ degrees relative to the rest pose.
- *
- * The axes were measured (turn one bone 40° and watch the hand's centre move),
- * and the two sides are NOT the same. That is what used to send the left hand
- * behind while the right one came forward:
- *
- * arm / hand
- * X lift. Up on both sides, so the SAME value is symmetric.
- * Y roll about the flipper - which way the palm faces.
- * Z forward/back. +Z is BACK on the left and FORWARD on the right, so a
- * symmetric pose writes the right-hand Z with the opposite sign, while
- * a walk (opposite arms) writes the SAME sign on both.
- * legsupport
- * X out to the side and up (same value on both sides is symmetric)
- * Y twist (which way the toe points)
- * Z forward/back, mirrored like `arm`
- * It pivots at the middle of the body, so the feet travel a long way for
- * a small angle - keep it under about 15 degrees.
- * master
- * X lean forward Y spin around Z roll sideways
- *
- * `armsupport` still exists in the rig but it is a control bone that pivots at
- * the body's centre, so it swings a hand in a wide arc across the face. Every
- * preset below drives `arm`/`hand` instead.
- */
-export const POSE_PRESETS = [
- { id: 'stand', label: '立ち', pose: { bones: {} } },
- {
- id: 'attention',
- label: 'きおつけ',
- pose: {
- bones: {
- // Both flippers dropped to the sides. A negative X lowers the arm, and
- // the value is the same on both sides (only Z mirrors between them).
- 'arm.l': [-62, 0, 0],
- 'arm.r': [-62, 0, 0],
- },
- },
- },
- {
- id: 'bow',
- label: 'おじぎ',
- pose: { bones: { master: [22, 0, 0], 'arm.l': [-62, 0, 0], 'arm.r': [-62, 0, 0] } },
- },
- {
- id: 'deep-bow',
- label: 'ふかくおじぎ',
- pose: { bones: { master: [42, 0, 0], 'arm.l': [-62, 0, 0], 'arm.r': [-62, 0, 0] } },
- },
- {
- id: 'tilt',
- label: 'くびかしげ',
- pose: {
- bones: {
- master: [0, 0, 16],
- // The roll turns the whole body about its base, which swings the feet up
- // and down, so the legs take the difference. The right-hand leg (on the
- // left of the screen) is the one that is planted: it opens outwards a
- // little and keeps the sole down. The left-hand leg bends back behind it.
- 'legsupport.r': [8, 0, 0],
- 'legsupport.l': [-6, 0, 14],
- },
- },
- },
- {
- id: 'wave',
- label: 'てをふる',
- pose: {
- bones: {
- master: [0, 0, -4],
- // The left flipper goes up and slightly forward. Y is the roll about the
- // flipper's own length, which is what turns the palm to face outwards.
- 'arm.l': [84, 0, -6],
- 'hand.l': [0, -30, 0],
- },
- },
- },
- {
- id: 'banzai',
- label: 'ばんざい',
- pose: {
- bones: {
- master: [-8, 0, 0],
- // Both flippers as far up as the shoulder allows (the pivot sits at the
- // middle of the body, so they cannot reach above the head).
- 'arm.l': [106, 0, -12],
- 'arm.r': [106, 0, 12],
- },
- root: [0, 0.2, 0],
- },
- },
- {
- id: 'stretch',
- label: 'のびをする',
- pose: {
- bones: {
- master: [-16, 0, 0],
- 'arm.l': [112, 0, -8],
- 'arm.r': [112, 0, 8],
- },
- root: [0, 0.08, 0],
- },
- },
- {
- id: 'jump',
- label: 'ジャンプ',
- pose: {
- bones: {
- master: [-8, 0, 0],
- 'arm.l': [68, 0, -10],
- 'arm.r': [68, 0, 10],
- 'legsupport.l': [10, 0, 0],
- 'legsupport.r': [10, 0, 0],
- },
- root: [0, 0.62, 0],
- },
- },
- {
- id: 'leap',
- label: '大ジャンプ',
- pose: {
- bones: {
- master: [-18, 0, 0],
- 'arm.l': [92, 0, -12],
- 'arm.r': [92, 0, 12],
- 'legsupport.l': [14, 0, 0],
- 'legsupport.r': [14, 0, 0],
- },
- root: [0, 1.0, 0],
- },
- },
- {
- id: 'dance',
- label: 'おどる',
- pose: {
- bones: {
- master: [-4, 0, -12],
- // Deliberately uneven: the left flipper is up, the right one is out in
- // front, and the feet step the other way.
- 'arm.l': [78, 0, -14],
- 'arm.r': [14, 0, 34],
- 'legsupport.l': [0, 0, 8],
- 'legsupport.r': [0, 0, 8],
- },
- root: [0, 0.14, 0],
- },
- },
- {
- id: 'tip-over',
- label: 'こてん',
- pose: {
- bones: {
- master: [0, 0, 68],
- 'arm.l': [26, 0, -18],
- 'arm.r': [10, 0, 18],
- },
- root: [0, 0.25, 0],
- },
- },
- {
- id: 'lie',
- label: 'ねている',
- pose: {
- bones: {
- // Flat on its back: X tips the body backwards all the way over, so the
- // face looks up. The root lift keeps the back resting on the ground
- // rather than sinking through it (the body's depth becomes its height).
- master: [-88, 0, 0],
- // Both flippers rest against the body, loose.
- 'arm.l': [-18, 0, 0],
- 'arm.r': [-18, 0, 0],
- },
- root: [0, 1.5, 0],
- },
- },
- { id: 'side', label: 'よこむき', pose: { bones: { master: [0, 92, 0] } } },
- { id: 'turn', label: 'うしろむき', pose: { bones: { master: [0, 180, 0] } } },
- {
- id: 'present',
- label: '右を紹介',
- pose: {
- bones: {
- master: [6, -26, 0],
- // The right flipper sweeps forward to point at whatever is on its right
- // (+Z is forwards on the right); the left one just steps aside.
- 'arm.r': [16, 0, 52],
- 'arm.l': [10, 0, -6],
- },
- },
- },
- {
- id: 'present-left',
- label: '左を紹介',
- pose: {
- bones: {
- master: [6, 26, 0],
- 'arm.l': [16, 0, -52],
- 'arm.r': [10, 0, 6],
- },
- },
- },
-];
-
-/** Deep-merge `patch` into `target` (plain objects only). */
-export function applyPatch(target, patch) {
- for (const [key, value] of Object.entries(patch ?? {})) {
- if (value && typeof value === 'object' && !Array.isArray(value)
- && target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
- applyPatch(target[key], value);
- } else {
- target[key] = Array.isArray(value) ? value.slice() : value;
- }
- }
- return target;
-}
-
-export function cloneState(state) {
- return JSON.parse(JSON.stringify(state));
-}
-
-/**
- * Body colour themes (C-1). Each is a full set of part colours, so picking one
- * is a single assignment; the individual pickers in the panel then let you drift
- * away from it without losing the rest.
- *
- * ぶるべー is a leaf-sprout mascot, so the themes are named after plants and
- * weather rather than after colours.
- */
-export const THEMES = [
- {
- id: 'original',
- label: 'オリジナル',
- colors: { body: '#c8b0f0', accent: '#8a4fe0', nose: '#7a4fb0', leaf: '#a6dd6a', vein: '#7fbf3f', feet: '#7a4fb0' },
- },
- {
- id: 'sakura',
- label: 'さくら',
- colors: { body: '#f7c6d9', accent: '#e0709b', nose: '#c25b7e', leaf: '#b8e08a', vein: '#8dbb54', feet: '#c25b7e' },
- },
- {
- id: 'matcha',
- label: 'まっちゃ',
- colors: { body: '#cfe3ac', accent: '#6f9c3a', nose: '#5c8030', leaf: '#9fd463', vein: '#79ab3c', feet: '#5c8030' },
- },
- {
- id: 'sora',
- label: 'そら',
- colors: { body: '#bcd9f7', accent: '#4a7fd4', nose: '#3f6cae', leaf: '#a8e08a', vein: '#7cbb58', feet: '#3f6cae' },
- },
- {
- id: 'lemon',
- label: 'レモン',
- colors: { body: '#f8e9a6', accent: '#d8a520', nose: '#b3871a', leaf: '#b9de74', vein: '#8fbb45', feet: '#b3871a' },
- },
- {
- id: 'momo',
- label: 'もも',
- colors: { body: '#f9cbae', accent: '#e07a4a', nose: '#bd6238', leaf: '#aede7c', vein: '#84b84e', feet: '#bd6238' },
- },
- {
- id: 'sumi',
- label: 'すみ',
- colors: { body: '#9aa2b8', accent: '#4b5468', nose: '#3c4356', leaf: '#8fbf7a', vein: '#6d9a58', feet: '#3c4356' },
- },
- {
- id: 'yozora',
- label: 'よぞら',
- colors: { body: '#8f8fd0', accent: '#403f8f', nose: '#34336f', leaf: '#7fc7b0', vein: '#5aa08c', feet: '#34336f' },
- },
-];
-
-/** Bubble shapes offered for captions. */
-export const BUBBLE_STYLES = [
- { value: 'round', label: 'まる' },
- { value: 'rect', label: 'しかく' },
- { value: 'shout', label: 'さけぶ' },
- { value: 'whisper', label: 'ささやき' },
- { value: 'think', label: '思考' },
- { value: 'none', label: 'なし' },
-];
-
-/** A couple of ready-made captions, so the feature is discoverable. */
-export const CAPTION_PRESETS = [
- { id: 'greeting', label: 'あいさつ', caption: { enabled: true, text: 'こんにちは、ぶるべーです。' } },
- { id: 'explain', label: '説明', caption: { enabled: true, text: 'ここがポイントです。', bubble: 'rect', tail: 'left' } },
- { id: 'surprise', label: 'びっくり', caption: { enabled: true, text: 'えっ!?', bubble: 'shout', tail: 'left', fontSize: 40, bold: true } },
-];