Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail. GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift. - KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding - react-native-keyboard-controller installiert + KeyboardProvider in Root - Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO) - Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn - DPad-Buttons 60→48, more bg, no scale - useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates - dm.tsx: resolveAvatar wrap (iron.png-Warning) - Mail-error-humanizer + locales Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
1.7 KiB
Vue
58 lines
1.7 KiB
Vue
<template>
|
||
<span ref="spanEl">{{ display }}</span>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
/**
|
||
* AnimatedCounter – zählt von 0 auf einen Zielwert hoch.
|
||
* Verwendet requestAnimationFrame mit Ease-out-Cubic.
|
||
*
|
||
* Props:
|
||
* target – Zielwert (required)
|
||
* duration – Animationsdauer in ms (default 1800)
|
||
* delay – Startverzögerung in ms (default 0)
|
||
* decimals – Nachkommastellen (default 0)
|
||
*/
|
||
const props = withDefaults(defineProps<{
|
||
target: number;
|
||
duration?: number;
|
||
delay?: number;
|
||
decimals?: number;
|
||
}>(), {
|
||
duration: 1800,
|
||
delay: 0,
|
||
decimals: 0,
|
||
});
|
||
|
||
const display = ref(props.decimals > 0 ? (0).toFixed(props.decimals) : '0');
|
||
const spanEl = ref<HTMLElement | null>(null);
|
||
let started = false;
|
||
|
||
function runAnimation() {
|
||
const startTime = performance.now();
|
||
const update = (now: number) => {
|
||
const elapsed = now - startTime;
|
||
const progress = Math.min(elapsed / props.duration, 1);
|
||
const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic
|
||
const current = eased * props.target;
|
||
display.value = props.decimals > 0
|
||
? current.toFixed(props.decimals)
|
||
: Math.round(current).toString();
|
||
if (progress < 1) requestAnimationFrame(update);
|
||
};
|
||
requestAnimationFrame(update);
|
||
}
|
||
|
||
onMounted(() => {
|
||
const observer = new IntersectionObserver(([entry]) => {
|
||
if (entry.isIntersecting && !started) {
|
||
started = true;
|
||
observer.disconnect();
|
||
setTimeout(runAnimation, props.delay);
|
||
}
|
||
}, { threshold: 0.3 });
|
||
if (spanEl.value) observer.observe(spanEl.value);
|
||
onUnmounted(() => observer.disconnect());
|
||
});
|
||
</script>
|