From 6c0b20f4a2b1d3197f2d1728e7789ed2c9add286 Mon Sep 17 00:00:00 2001 From: meetsu <96637888+klNuno@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:22:59 +0000 Subject: [PATCH 1/4] perf(companion): play occasional gestures on one timer and draw slow breaths in sub-pixel steps --- src/bots.css | 9 + src/components/agents/RobotFace.svelte | 9 +- .../companion/CapsuleCharacter.svelte | 9 +- .../companion/CompanionCharacter.svelte | 22 ++- .../companion/JellyCharacter.svelte | 11 +- .../companion/RetroCharacter.svelte | 20 ++- src/lib/companion/beats.test.ts | 88 +++++++++ src/lib/companion/beats.ts | 167 ++++++++++++++++++ src/lib/companion/stepped.test.ts | 56 ++++++ src/lib/companion/stepped.ts | 131 ++++++++++++++ 10 files changed, 504 insertions(+), 18 deletions(-) create mode 100644 src/lib/companion/beats.test.ts create mode 100644 src/lib/companion/beats.ts create mode 100644 src/lib/companion/stepped.test.ts create mode 100644 src/lib/companion/stepped.ts diff --git a/src/bots.css b/src/bots.css index 06a8b2b..5f5f3b5 100644 --- a/src/bots.css +++ b/src/bots.css @@ -199,3 +199,12 @@ :root[data-dormant] *::after { animation-play-state: paused !important; } + +/* + * A robot out of view (`data-offscreen`, `onscreen` in `lib/companion/beats.ts`): + * scrolled away in a list or clipped, its endless animations wait. + */ +[data-offscreen], +[data-offscreen] * { + animation-play-state: paused !important; +} diff --git a/src/components/agents/RobotFace.svelte b/src/components/agents/RobotFace.svelte index ba1f39a..8c4fb21 100644 --- a/src/components/agents/RobotFace.svelte +++ b/src/components/agents/RobotFace.svelte @@ -15,6 +15,7 @@
@@ -51,7 +56,7 @@ /* The rock: a tilt back and forth while it breathes, slower and further over asleep. */ .f-awake .rig, .f-typing .rig, .f-game .rig { animation: rock 4.4s ease-in-out infinite; } - .f-sleep .rig, .f-calm .rig, .f-rest .rig { animation: nap 6s ease-in-out infinite; } + .f-sleep .rig, .f-calm .rig, .f-rest .rig { animation: bots-capsule-nap-6000 6s steps(1, end) infinite; } .t-working .rig { animation: hum 1.2s ease-in-out infinite; } .f-groove .rig { animation: groove 0.5s ease-in-out infinite alternate; } .f-happy .rig { animation: hop 1.2s ease-in-out infinite; } @@ -70,7 +75,7 @@ .dizzy .rig :global(.cap) { animation: whirl 0.5s linear infinite; } @keyframes rock { 0%, 100% { transform: rotate(0) scale(1, 1); } 50% { transform: rotate(6deg) scale(1.01, 1.025); } } - @keyframes nap { 0%, 100% { transform: rotate(-9deg) translateY(2%); } 50% { transform: rotate(-7deg) translateY(2%) scale(1.01, 1.02); } } + /* The doze: `bots-capsule-nap-6000`, written in steps by `stepped.ts`. */ @keyframes hum { 0%, 100% { transform: rotate(0) translateY(0); } 25% { transform: rotate(2deg) translateY(-1.5%); } 75% { transform: rotate(-2deg) translateY(-1.5%); } } @keyframes groove { from { transform: rotate(-6deg); } to { transform: rotate(6deg) translateY(-1.5%); } } @keyframes hop { 0%, 60%, 100% { transform: translateY(0) rotate(0); } 30% { transform: translateY(-9%) rotate(8deg); } } diff --git a/src/components/companion/CompanionCharacter.svelte b/src/components/companion/CompanionCharacter.svelte index 021741e..0aaf153 100644 --- a/src/components/companion/CompanionCharacter.svelte +++ b/src/components/companion/CompanionCharacter.svelte @@ -27,7 +27,9 @@ import type { Component } from 'svelte'; import { characterOf, type Act, type CharacterProps, type Face, type Tone } from '../../lib/companion/character'; import type { Mood } from '../../lib/companion/mood'; + import { beat, onscreen } from '../../lib/companion/beats'; import { boxPaint, isBox } from '../../lib/companion/skin'; + import { useSteppedLoops } from '../../lib/companion/stepped'; import type { Robot, RobotFamily } from '../../lib/robots'; import { strings } from '../../lib/strings'; import BoxTop from './BoxTop.svelte'; @@ -141,6 +143,9 @@ const look = $derived(face === 'typing' ? { x: 0, y: 1 } : face === 'sleep' || face === 'yawn' || !gaze ? { x: 0, y: 0 } : { x: clamp(gaze.x), y: clamp(gaze.y) }); const label = $derived(alarm ? strings.companion.moods.calling : strings.companion.moods[mood]); const family = $derived(characterOf(skin)); + // Its breath is drawn in steps (`stepped.ts`), its blink played only while it shows (`beats.ts`). + useSteppedLoops(); + const blinks = $derived(!['sleep', 'yawn', 'happy', 'rest', 'cheer', 'startled'].includes(face)); {#if skin && family !== 'box'} @@ -168,12 +173,13 @@ style:height="{size}px" role="img" aria-label={label} + use:onscreen >
-