Files
geo/apps/ops-web/src/components/login-animation/Pupil.vue
T
root f8b918b9cd style(ops-web/login): redesign login with animated mascot and brand panel
Replace the previous split-screen with a brand panel that hosts an
animated mascot (eyeballs whose pupils track the focused form field and
hide while the password is typed) and decorative grid/circles, plus a
mobile brand mark and footer links. Swap Ant Design form controls for
native inputs styled to match. The mascot is composed from new
AnimatedCharacters / EyeBall / Pupil components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:50:19 +08:00

39 lines
1.3 KiB
Vue

<template>
<div ref="pupilRef" class="pupil" :style="{
width: size + 'px', height: size + 'px',
backgroundColor: pupilColor,
transform: `translate(${pos.x}px, ${pos.y}px)`,
}" />
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = withDefaults(defineProps<{
size?: number; maxDistance?: number; pupilColor?: string
forceLookX?: number; forceLookY?: number
}>(), { size: 12, maxDistance: 5, pupilColor: 'black' })
const mx = ref(0), my = ref(0)
const pupilRef = ref<HTMLDivElement>()
const onMove = (e: MouseEvent) => { mx.value = e.clientX; my.value = e.clientY }
onMounted(() => window.addEventListener('mousemove', onMove))
onUnmounted(() => window.removeEventListener('mousemove', onMove))
const pos = computed(() => {
if (!pupilRef.value) return { x: 0, y: 0 }
if (props.forceLookX !== undefined && props.forceLookY !== undefined)
return { x: props.forceLookX, y: props.forceLookY }
const r = pupilRef.value.getBoundingClientRect()
const dx = mx.value - (r.left + r.width / 2)
const dy = my.value - (r.top + r.height / 2)
const d = Math.min(Math.sqrt(dx ** 2 + dy ** 2), props.maxDistance)
const a = Math.atan2(dy, dx)
return { x: Math.cos(a) * d, y: Math.sin(a) * d }
})
</script>
<style scoped>
.pupil { border-radius: 50%; transition: transform 0.1s ease-out; }
</style>