feat: add idle yawn animation when user is inactive (F-4)

After 30s without user input on Elements tab, producer sprites cycle a
subtle yawn (translateY + scale) every 8s. Resets immediately on any
pointer/touch/key/scroll event. Implemented via wrapper div so
CharacterSprite internals are untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 03:53:20 +09:00
parent c638679502
commit 2b752e9e1f
2 changed files with 66 additions and 7 deletions

39
src/lib/useIdleMood.ts Normal file
View File

@@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
const IDLE_AFTER_MS = 30_000;
const TICK_MS = 2_000;
export type Mood = 'awake' | 'idle';
const ACTIVITY_EVENTS: Array<keyof WindowEventMap> = [
'pointerdown',
'touchstart',
'keydown',
'scroll',
];
export function useIdleMood(idleAfterMs: number = IDLE_AFTER_MS): Mood {
const [mood, setMood] = useState<Mood>('awake');
useEffect(() => {
let lastActivity = Date.now();
const onActivity = () => {
lastActivity = Date.now();
setMood((prev) => (prev === 'awake' ? prev : 'awake'));
};
ACTIVITY_EVENTS.forEach((evt) =>
window.addEventListener(evt, onActivity, { passive: true })
);
const tick = window.setInterval(() => {
if (Date.now() - lastActivity >= idleAfterMs) {
setMood((prev) => (prev === 'idle' ? prev : 'idle'));
}
}, TICK_MS);
return () => {
ACTIVITY_EVENTS.forEach((evt) => window.removeEventListener(evt, onActivity));
window.clearInterval(tick);
};
}, [idleAfterMs]);
return mood;
}