aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/animation.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/animation.js')
-rw-r--r--public/bluebey-studio/src/animation.js204
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));