mejoras de UI AnimatedNumber

This commit is contained in:
2025-08-10 19:46:41 -06:00
parent 3c3b19b2ce
commit bec944af4f
2 changed files with 101 additions and 9 deletions

View File

@@ -0,0 +1,84 @@
<template>
<div class="anim-number" :class="[ animating ? (dir === 'inc' ? 'dir-inc' : 'dir-dec') : '', running ? 'run' : '', (!running && animating) ? 'prep' : '', pulse ? 'pulse' : '' ]">
<div class="viewport" ref="viewportEl">
<span class="measure">{{ display }}</span>
<span v-if="animating" class="val old">{{ prev }}</span>
<span class="val current">{{ display }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick, onMounted } from 'vue';
const props = defineProps<{ value: number; durationMs?: number; pulseOnFirst?: boolean }>();
const prev = ref<number>(props.value ?? 0);
const display = ref<number>(props.value ?? 0);
const animating = ref(false);
const running = ref(false);
const dir = ref<'inc' | 'dec'>('inc');
// handled via CSS classes start-inc/start-dec
const pulse = ref(false);
const viewportEl = ref<HTMLElement | null>(null);
onMounted(() => {
if (props.pulseOnFirst) {
pulse.value = true;
setTimeout(() => { pulse.value = false; }, 600);
}
});
watch(() => props.value, async (nv, ov) => {
if (nv === ov) return;
const d = (props.durationMs ?? 1500);
dir.value = nv > prev.value ? 'inc' : 'dec';
// Setup animation
display.value = nv;
animating.value = true;
running.value = false;
await nextTick();
// Trigger run
requestAnimationFrame(() => {
running.value = true;
setTimeout(() => {
// End animation
prev.value = nv;
animating.value = false;
running.value = false;
}, d);
});
});
</script>
<style scoped>
.anim-number { display:inline-block; position: relative; height: 1.2em; overflow: hidden; vertical-align: baseline; }
.viewport { position: relative; display:inline-block; height: 1.2em; }
.measure { visibility: hidden; display:inline-block; }
.val { position: absolute; left: 0; right: 0; top: 0; transform: translateY(0); transition: transform 1.5s ease, opacity 1.5s ease; line-height: 1.2em; will-change: transform, opacity; }
.anim-number.prep .val { transition: none !important; }
.anim-number.dir-inc .current { transform: translateY(-100%); }
.anim-number.dir-dec .current { transform: translateY(100%); }
.anim-number.dir-inc .current,
.anim-number.dir-dec .current { opacity: 0.2; }
.anim-number.dir-inc.run .current,
.anim-number.dir-dec.run .current { transform: translateY(0); opacity: 1; }
.old { opacity: 1; }
.current { opacity: 1; }
/* Increase: old goes down, new comes from top */
.dir-inc.run .old { transform: translateY(100%); opacity: 0; }
.dir-inc.run .current { transform: translateY(0); }
/* Decrease: old goes up, new comes from bottom */
.dir-dec.run .old { transform: translateY(-100%); opacity: 0; }
.dir-dec.run .current { transform: translateY(0); }
/* Emphasis pulse */
.pulse { animation: pulse-pop 0.6s ease; }
@keyframes pulse-pop {
0% { transform: scale(0.9); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
</style>

View File

@@ -7,26 +7,29 @@
<div class="tokens"> <div class="tokens">
<div class="token pill"> <div class="token pill">
<span class="icon">🦃</span> <span class="icon">🦃</span>
<span class="val">{{ player.pavoTokens ?? 0 }}</span> <span class="val"><AnimatedNumber :value="player.pavoTokens ?? 0" /></span>
</div> </div>
<div class="token pill"> <div class="token pill">
<span class="icon">🌽</span> <span class="icon">🌽</span>
<span class="val">{{ player.eloteTokens ?? 0 }}</span> <span class="val"><AnimatedNumber :value="player.eloteTokens ?? 0" /></span>
</div> </div>
<div v-if="(player.shameTokens ?? 0) > 0" class="token pill subtle"> <Transition name="pop">
<div v-if="(player.shameTokens ?? 0) > 0" class="token pill subtle shame-pill">
<span class="icon">😶</span> <span class="icon">😶</span>
<span class="val">{{ player.shameTokens }}</span> <span class="val"><AnimatedNumber :value="player.shameTokens ?? 0" :pulseOnFirst="true" /></span>
</div> </div>
</Transition>
</div> </div>
<div class="score"> <div class="score">
<span class="label">Puntuación</span> <span class="label">Puntuación</span>
<span class="value">{{ displayScore }}</span> <span class="value"><AnimatedNumber :value="displayScore" /></span>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import AnimatedNumber from './AnimatedNumber.vue';
interface PlayerView { interface PlayerView {
sessionId?: string; sessionId?: string;
@@ -58,8 +61,13 @@ const primary = computed(() => props.player.color || '#667eea');
.pill { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; background:#f7f7f7; border:1px solid #eee; } .pill { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; background:#f7f7f7; border:1px solid #eee; }
.pill.subtle { background:#fafafa; color:#666; } .pill.subtle { background:#fafafa; color:#666; }
.icon { font-size: 16px; } .icon { font-size: 16px; }
.val { font-weight: 600; color:#333; } .val { font-weight: 600; color:#333; display:inline-block; min-width: 1ch; }
.score { display:flex; align-items:center; justify-content:space-between; margin-top:6px; padding:8px; border-radius:10px; background:linear-gradient(135deg, color-mix(in srgb, var(--primary) 10%, white) 0%, #ffffff 100%); border:1px solid color-mix(in srgb, var(--primary) 30%, #e6e9ff); } .score { display:flex; align-items:center; justify-content:space-between; margin-top:6px; padding:8px; border-radius:10px; background:linear-gradient(135deg, color-mix(in srgb, var(--primary) 10%, white) 0%, #ffffff 100%); border:1px solid color-mix(in srgb, var(--primary) 30%, #e6e9ff); }
.score .label { font-size:12px; color: var(--primary); font-weight:700; } .score .label { font-size:12px; color: var(--primary); font-weight:700; }
.score .value { font-size:18px; font-weight:800; color: var(--primary); } .score .value { font-size:18px; font-weight:800; color: var(--primary); display:flex; align-items:center; height: 1.2em; line-height: 1.2em; }
/* Emphasis on shame token appear */
.pop-enter-from { opacity: 0; transform: scale(0.9); }
.pop-enter-to { opacity: 1; transform: scale(1); }
.pop-enter-active { transition: all 0.2s ease; }
</style> </style>