登录页面的互动效果
鼠标在左侧小猫区域移动时,小猫和三个几何人物的眼睛会跟随鼠标转动。
输入用户名时,小猫会伸长脖子偷看,三个几何人物会呈现吃惊的表情(嘴巴变圆)。
输入密码时,小猫和几何人物会 “避嫌”,恢复到原来的状态。
文章末尾有成品展示
![图片[1]-AI 开发趣味登录页:会偷看的小猫 + 互动几何人物,手把手教你打造同款 VibeCoding 登录界面](https://www.902d.com/wp-content/uploads/2026/06/00867f08db20260617003439.webp)
开发步骤
步骤 1:准备 AI 编程工具
输入以下提示词:
请编写一个登录页面,分为左右两个部分。左侧绘制一只黄色的小猫,风格卡通可爱;右侧包含登录信息区域,有用户名输入框、密码输入框和蓝色的登录按钮。要求从专业 UI 设计角度,确保页面美观、布局合理,使用 HTML、CSS 和 JavaScript 实现。
发送后,AI 会生成基础的登录页代码,将其保存为login.html,用浏览器打开即可看到初始效果(此时小猫眼睛是静态的)。
步骤 2:添加小猫眼睛跟随鼠标的效果
输入提示词:
为登录页左侧的黄色小猫添加交互效果:当鼠标在页面左侧小猫所在区域移动时,小猫的眼睛要跟随鼠标移动,眼睛移动速度与鼠标移动速度匹配,效果自然流畅。请修改代码实现该功能,如需引入额外库请说明。
AI 会在原有代码基础上添加 JavaScript 逻辑,实现眼睛跟随效果。保存后刷新浏览器,即可看到小猫眼睛跟随鼠标转动。
步骤 3:添加小猫偷看用户名、几何人物吃惊的效果
输入提示词:
为登录页添加以下交互:当用户点击并输入用户名时,左侧黄色小猫的眼睛看向用户名输入框,脖子向输入框方向拉长,呈现 “偷看” 效果;同时左侧的三个几何形状人物(分别为紫色长条、黑色小个、橘色圆胖,与小猫同水平线且不遮挡)要呈现吃惊的表情(嘴巴变圆)。当鼠标离开用户名输入框时,小猫和几何人物恢复原状态。请修改代码实现。
AI 会继续完善代码,添加这些交互逻辑。
步骤 4:添加几何人物眼睛跟随鼠标的效果
输入提示词:
让左侧的三个几何人物(紫色长条、黑色小个、橘色圆胖)的眼睛也跟随鼠标移动,与小猫眼睛的跟随效果一致,营造出它们也在 “盯着” 鼠标的感觉。请修改代码实现。
步骤 5:调整细节(如几何人物位置、样式)
如果对几何人物的位置、嘴巴颜色、眼睛样式不满意,可继续发送提示词,例如:
将三个几何人物分散开,橘色圆胖人物适当左移,黑色和紫色人物适当右移;将它们的嘴巴改为白色;把橘色圆胖人物的眼睛样式调整为和紫色人物一样。请修改代码。
通过不断细化提示词,就能逐步实现和趣味登录页啦!
成品模板代码 :
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🐱 集体盯梢 · 登录页</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Quicksand', sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #f9f3e6 0%, #ffe9d4 100%);
padding: 1.5rem;
}
.login-card {
display: flex;
flex-direction: row;
max-width: 1000px;
width: 100%;
background: rgba(255, 250, 240, 0.70);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
border-radius: 56px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25),
inset 0 1px 3px rgba(255, 255, 255, 0.6);
overflow: hidden;
transition: all 0.2s ease;
}
.left-panel {
flex: 1.1;
background: #fef7e9;
background-image: radial-gradient(circle at 20% 30%, #fff6e0 0%, #fcebd5 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1.5rem;
min-height: 380px;
position: relative;
cursor: default;
}
.scene-wrapper {
width: 100%;
max-width: 320px;
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.scene-svg {
width: 100%;
height: auto;
display: block;
filter: drop-shadow(0 12px 18px rgba(200, 140, 60, 0.25));
transition: transform 0.3s ease;
}
.scene-svg:hover {
transform: scale(1.02) rotate(-2deg);
}
.right-panel {
flex: 1;
background: rgba(255, 248, 235, 0.6);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
padding: 2.8rem 2.5rem;
display: flex;
flex-direction: column;
justify-content: center;
border-left: 1px solid rgba(255, 215, 150, 0.3);
}
.login-header {
margin-bottom: 2rem;
}
.login-header h2 {
font-weight: 700;
font-size: 2rem;
letter-spacing: -0.5px;
color: #3d2a1b;
display: flex;
align-items: center;
gap: 0.5rem;
}
.login-header h2 span {
background: #f7d44a;
padding: 0.2rem 0.8rem;
border-radius: 60px;
font-size: 1.2rem;
line-height: 1.4;
color: #3d2a1b;
box-shadow: inset 0 -3px 0 #dbaa2a;
}
.login-header p {
color: #7a5f42;
font-weight: 400;
font-size: 0.95rem;
margin-top: 6px;
letter-spacing: 0.3px;
}
.input-group {
margin-bottom: 1.5rem;
}
.input-group label {
display: block;
font-weight: 600;
font-size: 0.85rem;
color: #5f432b;
margin-bottom: 0.4rem;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.input-field {
width: 100%;
padding: 0.9rem 1.2rem;
font-size: 1rem;
font-family: 'Quicksand', sans-serif;
background: white;
border: 2px solid #f0dcc8;
border-radius: 40px;
outline: none;
transition: all 0.25s ease;
color: #2b1b0f;
font-weight: 500;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
}
.input-field:focus {
border-color: #f5b84b;
box-shadow: 0 0 0 6px rgba(245, 184, 75, 0.15);
background: #fffdf9;
}
.input-field::placeholder {
color: #c0a68a;
font-weight: 400;
opacity: 0.7;
}
.btn-login {
width: 100%;
padding: 0.9rem 1.8rem;
background: #4a8af4;
background: linear-gradient(145deg, #3f7fe0, #2d6bcb);
border: none;
border-radius: 60px;
color: white;
font-weight: 700;
font-size: 1.2rem;
font-family: 'Quicksand', sans-serif;
letter-spacing: 0.8px;
box-shadow: 0 12px 20px -8px rgba(45, 107, 203, 0.35);
cursor: pointer;
transition: all 0.2s ease;
margin-top: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-login:hover {
background: linear-gradient(145deg, #3a72d4, #1f5bb8);
transform: scale(1.01) translateY(-2px);
box-shadow: 0 18px 28px -10px #2d6bcb;
}
.btn-login:active {
transform: scale(0.96);
box-shadow: 0 6px 12px -6px #1f4f9e;
}
.login-footnote {
margin-top: 1.8rem;
font-size: 0.8rem;
color: #a58364;
text-align: center;
border-top: 1px dashed #ecdac8;
padding-top: 1.2rem;
letter-spacing: 0.2px;
}
.login-footnote a {
color: #4a8af4;
font-weight: 600;
text-decoration: none;
border-bottom: 1px dotted transparent;
transition: border 0.2s;
}
.login-footnote a:hover {
border-bottom: 1px dotted #4a8af4;
}
@media (max-width: 700px) {
.login-card {
flex-direction: column;
border-radius: 40px;
max-width: 420px;
}
.left-panel {
padding: 1.5rem 1rem 0.5rem;
min-height: 240px;
border-radius: 40px 40px 0 0;
}
.scene-wrapper {
max-width: 180px;
}
.right-panel {
border-left: none;
border-top: 1px solid rgba(255, 215, 150, 0.3);
padding: 2rem 1.8rem;
}
.login-header h2 {
font-size: 1.7rem;
}
}
@media (max-width: 480px) {
.right-panel {
padding: 1.8rem 1.2rem;
}
.input-field {
padding: 0.8rem 1rem;
font-size: 0.95rem;
}
.btn-login {
font-size: 1rem;
padding: 0.8rem 1.2rem;
}
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
}
</style>
</head>
<body>
<div class="login-card">
<!-- 左侧:场景 -->
<div class="left-panel" id="catContainer">
<div class="scene-wrapper">
<svg class="scene-svg" viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" id="sceneSvg">
<!-- 背景小装饰 -->
<circle cx="40" cy="30" r="6" fill="#FADB9F" opacity="0.4" />
<circle cx="290" cy="40" r="8" fill="#FADB9F" opacity="0.3" />
<circle cx="30" cy="230" r="10" fill="#FADB9F" opacity="0.25" />
<circle cx="300" cy="220" r="7" fill="#FADB9F" opacity="0.3" />
<circle cx="160" cy="20" r="5" fill="#FADB9F" opacity="0.2" />
<!-- ===== 三个几何人物 (重新调整位置) ===== -->
<!-- 1. 紫色长条 (适当右移) 原 translate(40, 140) -> 现 (70, 140) -->
<g id="geo1" transform="translate(70, 140)">
<rect x="0" y="0" width="22" height="60" rx="8" fill="#8B5CF6" stroke="#6D28D9" stroke-width="2" />
<!-- 眼睛 (眼白固定) -->
<circle id="geo1-eye1" cx="7" cy="18" r="4" fill="white" stroke="#4C1D95" stroke-width="1.5" />
<circle id="geo1-eye2" cx="15" cy="18" r="4" fill="white" stroke="#4C1D95" stroke-width="1.5" />
<!-- 瞳孔 -->
<circle id="geo1-pupil1" cx="7" cy="18" r="2" fill="#4C1D95" />
<circle id="geo1-pupil2" cx="15" cy="18" r="2" fill="#4C1D95" />
<!-- 嘴巴 (改为白色) -->
<path id="geo1-mouth" d="M6 32 Q11 38 16 32" stroke="white" stroke-width="2.5" stroke-linecap="round" fill="none" />
</g>
<!-- 2. 黑色小个 (适当右移) 原 translate(95, 150) -> 现 (120, 150) -->
<g id="geo2" transform="translate(120, 150)">
<circle cx="18" cy="18" r="18" fill="#1F2937" stroke="#111827" stroke-width="2" />
<circle id="geo2-eye1" cx="10" cy="14" r="5" fill="white" stroke="#111827" stroke-width="1.5" />
<circle id="geo2-eye2" cx="26" cy="14" r="5" fill="white" stroke="#111827" stroke-width="1.5" />
<circle id="geo2-pupil1" cx="10" cy="14" r="2.5" fill="#111827" />
<circle id="geo2-pupil2" cx="26" cy="14" r="2.5" fill="#111827" />
<!-- 嘴巴 (改为白色) -->
<path id="geo2-mouth" d="M10 28 Q18 34 26 28" stroke="white" stroke-width="2.5" stroke-linecap="round" fill="none" />
</g>
<!-- 3. 橘色圆胖 (适当左移) 原 translate(230, 130) -> 现 (205, 130) -->
<g id="geo3" transform="translate(205, 130)">
<ellipse cx="30" cy="35" rx="28" ry="34" fill="#FB923C" stroke="#EA580C" stroke-width="2.5" />
<!-- 眼睛样式调整为和紫色人物一样 (眼白大小、边框颜色调整) -->
<circle id="geo3-eye1" cx="18" cy="28" r="4" fill="white" stroke="#4C1D95" stroke-width="1.5" />
<circle id="geo3-eye2" cx="42" cy="28" r="4" fill="white" stroke="#4C1D95" stroke-width="1.5" />
<!-- 瞳孔 (颜色与紫色人物一致) -->
<circle id="geo3-pupil1" cx="18" cy="28" r="2" fill="#4C1D95" />
<circle id="geo3-pupil2" cx="42" cy="28" r="2" fill="#4C1D95" />
<!-- 嘴巴 (改为白色) -->
<path id="geo3-mouth" d="M20 48 Q30 56 40 48" stroke="white" stroke-width="2.5" stroke-linecap="round" fill="none" />
</g>
<!-- ===== 小猫 ===== -->
<g id="catGroup">
<ellipse cx="180" cy="160" rx="48" ry="44" fill="#FCD34D" stroke="#E5B83C" stroke-width="3" />
<ellipse cx="180" cy="172" rx="28" ry="26" fill="#FFE380" opacity="0.7" />
<path d="M140 124 L128 82 L158 112 L140 124Z" fill="#FCD34D" stroke="#E5B83C" stroke-width="3" stroke-linejoin="round" />
<path d="M220 124 L232 82 L202 112 L220 124Z" fill="#FCD34D" stroke="#E5B83C" stroke-width="3" stroke-linejoin="round" />
<path d="M142 118 L134 92 L154 112 L142 118Z" fill="#F9A8A8" stroke="#E88282" stroke-width="1.5" opacity="0.8" />
<path d="M218 118 L226 92 L206 112 L218 118Z" fill="#F9A8A8" stroke="#E88282" stroke-width="1.5" opacity="0.8" />
<line x1="138" y1="146" x2="108" y2="138" stroke="#9C7A4A" stroke-width="2.5" stroke-linecap="round" />
<line x1="136" y1="156" x2="102" y2="156" stroke="#9C7A4A" stroke-width="2.5" stroke-linecap="round" />
<line x1="138" y1="166" x2="110" y2="174" stroke="#9C7A4A" stroke-width="2.5" stroke-linecap="round" />
<line x1="222" y1="146" x2="252" y2="138" stroke="#9C7A4A" stroke-width="2.5" stroke-linecap="round" />
<line x1="224" y1="156" x2="258" y2="156" stroke="#9C7A4A" stroke-width="2.5" stroke-linecap="round" />
<line x1="222" y1="166" x2="250" y2="174" stroke="#9C7A4A" stroke-width="2.5" stroke-linecap="round" />
<circle cx="158" cy="144" r="13" fill="#FFFDF5" stroke="#4B3A2A" stroke-width="2.5" />
<circle cx="202" cy="144" r="13" fill="#FFFDF5" stroke="#4B3A2A" stroke-width="2.5" />
<g id="leftEyeGroup">
<circle id="leftPupil" cx="162" cy="144" r="7" fill="#2F2216" />
<circle id="leftHighlight1" cx="165" cy="140" r="3" fill="white" />
<circle id="leftHighlight2" cx="159" cy="148" r="1.5" fill="white" opacity="0.7" />
</g>
<g id="rightEyeGroup">
<circle id="rightPupil" cx="198" cy="144" r="7" fill="#2F2216" />
<circle id="rightHighlight1" cx="201" cy="140" r="3" fill="white" />
<circle id="rightHighlight2" cx="195" cy="148" r="1.5" fill="white" opacity="0.7" />
</g>
<path d="M176 164 L184 164 L180 172 L176 164Z" fill="#F472B6" stroke="#DB2777" stroke-width="1.5" stroke-linejoin="round" />
<path id="catMouth" d="M171 176 Q180 184 180 176 Q180 184 189 176" stroke="#7A5C3A" stroke-width="2.5" fill="none" stroke-linecap="round" />
<ellipse cx="146" cy="168" rx="10" ry="6" fill="#FCA5A5" opacity="0.3" />
<ellipse cx="214" cy="168" rx="10" ry="6" fill="#FCA5A5" opacity="0.3" />
<ellipse cx="150" cy="200" rx="10" ry="7" fill="#FCD34D" stroke="#E5B83C" stroke-width="2" />
<ellipse cx="210" cy="200" rx="10" ry="7" fill="#FCD34D" stroke="#E5B83C" stroke-width="2" />
<circle cx="148" cy="202" r="3" fill="#FAD1A0" />
<circle cx="212" cy="202" r="3" fill="#FAD1A0" />
<path d="M224 188 Q250 180 248 160 Q246 145 232 144" stroke="#E5B83C" stroke-width="7" fill="none" stroke-linecap="round" />
<path d="M224 188 Q250 180 248 160 Q246 145 232 144" stroke="#FCD34D" stroke-width="4" fill="none" stroke-linecap="round" />
<g transform="translate(200, 88)">
<path d="M0 0 L-12 -8 L-8 4 L0 0Z" fill="#F472B6" />
<path d="M0 0 L12 -8 L8 4 L0 0Z" fill="#F472B6" />
<circle cx="0" cy="0" r="4" fill="#EC4899" />
</g>
</g>
<!-- 脖子 -->
<g id="neckGroup">
<ellipse id="neckShape" cx="180" cy="120" rx="18" ry="10" fill="#FCD34D" stroke="#E5B83C" stroke-width="2.5" />
</g>
</svg>
</div>
</div>
<!-- 右侧:登录 -->
<div class="right-panel">
<div class="login-header">
<h2>
<span>🐱</span> 欢迎回来
</h2>
<p>鼠标移动,大家一起盯着你 👀</p>
</div>
<form id="loginForm" autocomplete="off">
<div class="input-group">
<label for="username">用户名</label>
<input type="text" id="username" class="input-field" placeholder="例如: Kitty" required>
</div>
<div class="input-group">
<label for="password">密码</label>
<input type="password" id="password" class="input-field" placeholder="••••••••" required>
</div>
<button type="submit" class="btn-login" id="loginBtn">
<span>登录</span>
<span style="font-size: 1.3rem; line-height: 1;">→</span>
</button>
</form>
<div class="login-footnote">
测试账号:<strong>cat</strong> / 密码 <strong>123456</strong>
<span style="margin:0 4px;">·</span>
<a href="#" onclick="alert('🐱 喵~ 找回密码请发送邮件到 kitty@example.com')">忘记密码?</a>
</div>
</div>
</div>
<script>
(function() {
// ============================================================
// 1. 获取所有需要操作的元素
// ============================================================
const catContainer = document.getElementById('catContainer');
// ---- 小猫眼睛 ----
const leftPupil = document.getElementById('leftPupil');
const rightPupil = document.getElementById('rightPupil');
const leftHighlight1 = document.getElementById('leftHighlight1');
const rightHighlight1 = document.getElementById('rightHighlight1');
const leftHighlight2 = document.getElementById('leftHighlight2');
const rightHighlight2 = document.getElementById('rightHighlight2');
// ---- 小猫脖子 & 嘴巴 ----
const neckShape = document.getElementById('neckShape');
const catMouth = document.getElementById('catMouth');
// ---- 几何人物的瞳孔 ----
const geoPupils = [
document.getElementById('geo1-pupil1'),
document.getElementById('geo1-pupil2'),
document.getElementById('geo2-pupil1'),
document.getElementById('geo2-pupil2'),
document.getElementById('geo3-pupil1'),
document.getElementById('geo3-pupil2')
];
// ---- 几何人物的嘴巴 ----
const geoMouths = [
document.getElementById('geo1-mouth'),
document.getElementById('geo2-mouth'),
document.getElementById('geo3-mouth')
];
// ---- 存储原始状态 ----
const originalMouths = {
cat: catMouth.getAttribute('d'),
geo1: geoMouths[0].getAttribute('d'),
geo2: geoMouths[1].getAttribute('d'),
geo3: geoMouths[2].getAttribute('d')
};
const originalPupilRadii = geoPupils.map(p => p.getAttribute('r'));
// ============================================================
// 2. 眼睛跟随鼠标 (所有角色)
// ============================================================
const eyeBases = {
catLeft: { cx: 158, cy: 144, maxOffset: 6 },
catRight: { cx: 202, cy: 144, maxOffset: 6 },
geo1Left: { cx: 7, cy: 18, maxOffset: 3 },
geo1Right: { cx: 15, cy: 18, maxOffset: 3 },
geo2Left: { cx: 10, cy: 14, maxOffset: 4 },
geo2Right: { cx: 26, cy: 14, maxOffset: 4 },
geo3Left: { cx: 18, cy: 28, maxOffset: 3 }, // 与紫色人物一致
geo3Right: { cx: 42, cy: 28, maxOffset: 3 }
};
const pupilMappings = [
{ el: leftPupil, base: eyeBases.catLeft, highlight: leftHighlight1, h2: leftHighlight2 },
{ el: rightPupil, base: eyeBases.catRight, highlight: rightHighlight1, h2: rightHighlight2 },
{ el: geoPupils[0], base: eyeBases.geo1Left },
{ el: geoPupils[1], base: eyeBases.geo1Right },
{ el: geoPupils[2], base: eyeBases.geo2Left },
{ el: geoPupils[3], base: eyeBases.geo2Right },
{ el: geoPupils[4], base: eyeBases.geo3Left },
{ el: geoPupils[5], base: eyeBases.geo3Right }
];
const highlightMappings = [
{ el: leftHighlight1, base: eyeBases.catLeft, offsetFactor: 0.6 },
{ el: rightHighlight1, base: eyeBases.catRight, offsetFactor: 0.6 },
{ el: leftHighlight2, base: eyeBases.catLeft, offsetFactor: 0.5 },
{ el: rightHighlight2, base: eyeBases.catRight, offsetFactor: 0.5 }
];
let currentDx = 0, currentDy = 0;
let animationFrameId = null;
// 存储高光初始位置
function initHighlights() {
highlightMappings.forEach(({ el }) => {
if (!el._origCx) {
el._origCx = parseFloat(el.getAttribute('cx'));
el._origCy = parseFloat(el.getAttribute('cy'));
}
});
}
function updateAllPupils(dx, dy, peekOffsetX = 0, peekOffsetY = 0) {
const clampedDx = Math.min(1, Math.max(-1, dx));
const clampedDy = Math.min(1, Math.max(-1, dy));
currentDx = clampedDx;
currentDy = clampedDy;
pupilMappings.forEach(({ el, base }) => {
let extraX = 0, extraY = 0;
if (el === leftPupil || el === rightPupil) {
extraX = peekOffsetX;
extraY = peekOffsetY;
}
const offsetX = clampedDx * base.maxOffset + extraX;
const offsetY = clampedDy * base.maxOffset + extraY;
el.setAttribute('cx', base.cx + offsetX);
el.setAttribute('cy', base.cy + offsetY);
});
highlightMappings.forEach(({ el, base, offsetFactor }) => {
let extraX = 0, extraY = 0;
if (el === leftHighlight1 || el === rightHighlight1 || el === leftHighlight2 || el === rightHighlight2) {
extraX = peekOffsetX * 0.6;
extraY = peekOffsetY * 0.6;
}
const offsetX = clampedDx * base.maxOffset * offsetFactor + extraX;
const offsetY = clampedDy * base.maxOffset * offsetFactor + extraY;
if (!el._origCx) {
el._origCx = parseFloat(el.getAttribute('cx'));
el._origCy = parseFloat(el.getAttribute('cy'));
}
el.setAttribute('cx', el._origCx + offsetX);
el.setAttribute('cy', el._origCy + offsetY);
});
}
// 鼠标事件
function handleMouseMove(event) {
const rect = catContainer.getBoundingClientRect();
const mouseX = (event.clientX - rect.left) / rect.width;
const mouseY = (event.clientY - rect.top) / rect.height;
const dx = (mouseX - 0.5) * 2;
const dy = (mouseY - 0.5) * 2;
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
animationFrameId = requestAnimationFrame(() => {
// 使用当前的 peek 偏移 (如果有)
const peekX = window._peekOffsetX || 0;
const peekY = window._peekOffsetY || 0;
updateAllPupils(dx, dy, peekX, peekY);
animationFrameId = null;
});
}
function handleMouseLeave() {
let startTime = null;
const startDx = currentDx;
const startDy = currentDy;
const duration = 200;
function animateReturn(timestamp) {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / duration, 1);
const ease = 1 - (1 - progress) * (1 - progress);
const dx = startDx * (1 - ease);
const dy = startDy * (1 - ease);
const peekX = window._peekOffsetX || 0;
const peekY = window._peekOffsetY || 0;
updateAllPupils(dx, dy, peekX, peekY);
if (progress < 1) {
requestAnimationFrame(animateReturn);
} else {
updateAllPupils(0, 0, peekX, peekY);
}
}
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
requestAnimationFrame(animateReturn);
}
catContainer.addEventListener('mousemove', handleMouseMove);
catContainer.addEventListener('mouseleave', handleMouseLeave);
initHighlights();
updateAllPupils(0, 0, 0, 0);
// ============================================================
// 3. 用户名输入框 "偷看" 模式
// ============================================================
const usernameInput = document.getElementById('username');
let isPeeking = false;
function setPeekState(peek) {
isPeeking = peek;
if (peek) {
window._peekOffsetX = 3;
window._peekOffsetY = 2;
neckShape.setAttribute('ry', '22');
neckShape.setAttribute('cy', '114');
catMouth.setAttribute('d', 'M174 176 Q180 182 186 176');
geoMouths[0].setAttribute('d', 'M8 34 Q11 40 14 34');
geoMouths[1].setAttribute('d', 'M13 30 Q18 38 23 30');
geoMouths[2].setAttribute('d', 'M22 50 Q30 58 38 50');
geoPupils.forEach(p => p.setAttribute('r', '4'));
// 立即更新瞳孔位置
if (currentDx !== undefined && currentDy !== undefined) {
updateAllPupils(currentDx, currentDy, window._peekOffsetX, window._peekOffsetY);
}
} else {
window._peekOffsetX = 0;
window._peekOffsetY = 0;
neckShape.setAttribute('ry', '10');
neckShape.setAttribute('cy', '120');
catMouth.setAttribute('d', originalMouths.cat);
geoMouths[0].setAttribute('d', originalMouths.geo1);
geoMouths[1].setAttribute('d', originalMouths.geo2);
geoMouths[2].setAttribute('d', originalMouths.geo3);
geoPupils.forEach((p, idx) => {
p.setAttribute('r', originalPupilRadii[idx] || '2.5');
});
if (currentDx !== undefined && currentDy !== undefined) {
updateAllPupils(currentDx, currentDy, 0, 0);
}
}
}
usernameInput.addEventListener('focus', function() {
setPeekState(true);
});
usernameInput.addEventListener('input', function() {
if (!isPeeking) {
setPeekState(true);
}
});
usernameInput.addEventListener('blur', function() {
setPeekState(false);
});
// ============================================================
// 4. 登录逻辑
// ============================================================
const loginForm = document.getElementById('loginForm');
const passwordInput = document.getElementById('password');
const loginBtn = document.getElementById('loginBtn');
function handleLogin(event) {
event.preventDefault();
const username = usernameInput.value.trim();
const password = passwordInput.value.trim();
const isValid = (username === 'cat' && password === '123456');
usernameInput.style.borderColor = '';
passwordInput.style.borderColor = '';
if (!username || !password) {
if (!username) {
usernameInput.style.borderColor = '#e6614f';
usernameInput.focus();
} else {
passwordInput.style.borderColor = '#e6614f';
passwordInput.focus();
}
if (!username) usernameInput.style.animation = 'shake 0.3s';
if (!password) passwordInput.style.animation = 'shake 0.3s';
setTimeout(() => {
usernameInput.style.animation = '';
passwordInput.style.animation = '';
}, 300);
return;
}
if (isValid) {
usernameInput.style.borderColor = '#36b37e';
passwordInput.style.borderColor = '#36b37e';
const originalText = loginBtn.innerHTML;
loginBtn.innerHTML = '✅ 登录成功 !';
loginBtn.style.background = 'linear-gradient(145deg, #36b37e, #1f9a6b)';
loginBtn.style.boxShadow = '0 8px 20px -6px #1f9a6b';
setTimeout(() => {
loginBtn.innerHTML = originalText;
loginBtn.style.background = '';
loginBtn.style.boxShadow = '';
usernameInput.style.borderColor = '';
passwordInput.style.borderColor = '';
}, 2200);
console.log('✅ 登录成功 (演示)');
} else {
usernameInput.style.borderColor = '#e6614f';
passwordInput.style.borderColor = '#e6614f';
loginBtn.style.background = 'linear-gradient(145deg, #d45a4a, #b84333)';
loginBtn.innerHTML = '❌ 账号或密码错误';
loginBtn.style.boxShadow = '0 8px 20px -6px #b84333';
setTimeout(() => {
loginBtn.innerHTML = '<span>登录</span><span style="font-size: 1.3rem; line-height: 1;">→</span>';
loginBtn.style.background = '';
loginBtn.style.boxShadow = '';
}, 1800);
usernameInput.style.animation = 'shake 0.35s';
passwordInput.style.animation = 'shake 0.35s';
setTimeout(() => {
usernameInput.style.animation = '';
passwordInput.style.animation = '';
}, 350);
}
}
loginForm.addEventListener('submit', handleLogin);
usernameInput.addEventListener('focus', function() {
this.style.borderColor = '';
this.style.animation = '';
});
passwordInput.addEventListener('focus', function() {
this.style.borderColor = '';
this.style.animation = '';
});
setPeekState(false);
})();
</script>
</body>
</html>
网站名称:玩转网
本文链接:
版权声明:知识共享署名-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)协议进行许可
本站资源仅供个人学习交流,转载时请以超链接形式标明文章原始出处,(如有侵权联系删除)















