diff options
Diffstat (limited to 'public/bluebey-studio/src/animation.js')
| -rw-r--r-- | public/bluebey-studio/src/animation.js | 204 |
1 files changed, 0 insertions, 204 deletions
diff --git a/public/bluebey-studio/src/animation.js b/public/bluebey-studio/src/animation.js deleted file mode 100644 index e569d12..0000000 --- a/public/bluebey-studio/src/animation.js +++ /dev/null @@ -1,204 +0,0 @@ -/** - * The "living" layer: idle body motion, blinking and idle eye drift. - * - * These never touch the saved state. `pose()` returns the base pose with the - * idle offsets added, and `eyeOpen`/`look` are multipliers the caller merges - * into the face parameters, so turning the animation off restores exactly the - * pose and expression the user had before. - */ - -export class Animator { - constructor({ state }) { - this.state = state; - this.time = 0; - this.blinkPhase = -1; - this.blinkDuration = 0.17; - this.blinkTimer = 2 + Math.random() * 1.5; - this.eyeOpen = 1; - this.look = { x: 0, y: 0 }; - this.idleWeight = 0; - this.snotScale = 1; - } - - reset() { - this.eyeOpen = 1; - this.look = { x: 0, y: 0 }; - this.blinkPhase = -1; - this.blinkTimer = 2; - this.time = 0; - this.snotScale = 1; - } - - update(dt) { - const speed = clamp(this.state.anim.speed ?? 1, 0.1, 3); - const step = Math.min(dt, 0.1) * speed; - this.time += step; - - const moving = (this.state.anim.mode ?? 'off') !== 'off'; - this.idleWeight += ((moving ? 1 : 0) - this.idleWeight) * Math.min(1, step * 4); - - // 鼻ちょうちん: while sleeping, the bubble swells on the out-breath and shrinks - // again. It is a real object (see src/main.js), so the motion is published here - // as a plain multiplier the render loop applies to the bubble's scale. - this.snotScale = (this.state.anim.mode ?? 'off') === 'sleep' - ? 0.45 + 0.55 * (0.5 - 0.5 * Math.cos(this.time * 1.3)) - : 1; - - this.updateBlink(step); - this.updateLook(step); - } - - updateBlink(dt) { - if ((this.state.anim.mode ?? 'off') === 'sleep') { - // Asleep: the eyes stay shut, whatever the blink setting says. - this.eyeOpen = 0; - this.blinkPhase = -1; - return; - } - if (!this.state.anim.blink) { - this.eyeOpen = 1; - this.blinkPhase = -1; - return; - } - if (this.blinkPhase >= 0) { - this.blinkPhase += dt / this.blinkDuration; - if (this.blinkPhase >= 1) { - this.blinkPhase = -1; - this.eyeOpen = 1; - const base = Math.max(0.5, this.state.anim.blinkInterval ?? 3.4); - // Blink again sooner sometimes, so it does not look metronomic. - this.blinkTimer = base * (0.55 + Math.random()); - if (Math.random() < 0.22) this.blinkTimer *= 0.35; // occasional double blink - } else { - this.eyeOpen = 1 - Math.pow(Math.sin(Math.PI * this.blinkPhase), 0.8); - } - return; - } - this.blinkTimer -= dt; - if (this.blinkTimer <= 0) this.blinkPhase = 0; - } - - updateLook(dt) { - if (!this.state.anim.lookAround) { - this.look.x = 0; - this.look.y = 0; - return; - } - const t = this.time; - this.look.x = Math.sin(t * 0.37) * 0.3 + Math.sin(t * 0.13 + 1.7) * 0.14; - this.look.y = Math.sin(t * 0.29 + 0.6) * 0.2; - } - - /** Base pose plus the current movement offsets; `pose` is `{ bones, root }`. */ - pose(basePose = {}) { - const bones = { ...(basePose.bones ?? {}) }; - const root = [...(basePose.root ?? [0, 0, 0])]; - const mode = this.state.anim.mode ?? 'off'; - if (mode === 'off' || this.idleWeight <= 0.001) return { bones, root }; - - const w = this.idleWeight; - const t = this.time; - const add = (name, dx, dy, dz) => { - const current = bones[name] ?? [0, 0, 0]; - bones[name] = [current[0] + dx * w, current[1] + dy * w, current[2] + dz * w]; - }; - - if (mode === 'walk') { - // A waddle for a character with no legs: a step bob, a side-to-side rock - // and alternating arms and feet. - // - // Z is the forward/back swing, and it is mirrored between the sides - so the - // SAME value on both arms swings them opposite ways, which is what a walk - // 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. 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); - add('arm.l', 6, 0, phase * 26); - 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); - root[1] += bob * 0.05; - return { bones, root }; - } - - 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 - // 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', 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 }; - } - - if (mode === 'sleep') { - // Slow breathing: the body rises and settles, the flippers drift, and the - // bubble swells and shrinks (that swell is `snotScale`, read by the loop). - const breath = 0.5 - 0.5 * Math.cos(t * 1.3); - add('master', Math.sin(t * 0.65) * 1.1, 0, 0); - add('arm.l', 0, 0, Math.sin(t * 0.9) * 2.5); - add('arm.r', 0, 0, -Math.sin(t * 0.9) * 2.5); - root[1] += breath * 0.03; - return { bones, root }; - } - - // Default: gentle breathing, as if standing there alive. - add('master', Math.sin(t * 1.5) * 1.3, 0, Math.sin(t * 0.81) * 1.6); - add('arm.l', 0, 0, Math.sin(t * 1.28) * 4.5); - add('arm.r', 0, 0, -Math.sin(t * 1.28 + 0.5) * 4.5); - add('hand.l', 0, 0, Math.sin(t * 1.05 + 1) * 3); - add('hand.r', 0, 0, -Math.sin(t * 1.05 + 1) * 3); - root[1] += Math.sin(t * 1.5) * 0.022; - return { bones, root }; - } -} - -/** - * Records the viewport canvas to a WebM blob while the animation plays. - * Chrome and Edge support `MediaRecorder` on `canvas.captureStream()`. - */ -export function createRecorder(canvas) { - if (typeof MediaRecorder === 'undefined' || !canvas.captureStream) return null; - const mimeType = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm'] - .find((type) => MediaRecorder.isTypeSupported?.(type)); - return { canvas, mimeType: mimeType ?? '', recorder: null, chunks: [] }; -} - -export function startRecording(session) { - if (!session) return false; - const stream = session.canvas.captureStream(30); - const recorder = session.mimeType - ? new MediaRecorder(stream, { mimeType: session.mimeType, videoBitsPerSecond: 8000000 }) - : new MediaRecorder(stream); - session.chunks = []; - session.recorder = recorder; - recorder.ondataavailable = (event) => { if (event.data?.size) session.chunks.push(event.data); }; - recorder.start(100); - return true; -} - -export function stopRecording(session) { - return new Promise((resolve) => { - if (!session?.recorder || session.recorder.state === 'inactive') { - resolve(null); - return; - } - const { recorder, chunks } = session; - recorder.onstop = () => resolve(new Blob(chunks, { type: recorder.mimeType || 'video/webm' })); - recorder.stop(); - }); -} - -const clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value)); |
