直接上html代码保存为.html直接运行既可看到效果如果有帮助请点个赞吧谢谢!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0, user-scalableno title趣味登录/title link hrefhttps://fonts.googleapis.com/css2?familyInter:opsz,wght14..32,300;400;500;600;700displayswap relstylesheet style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Inter, sans-serif; background: #eef2f5; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .login-container { width: 100%; max-width: 1360px; height: 85vh; min-height: 680px; background: #fff; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.3); border-radius: 48px; overflow: hidden; display: flex; flex-direction: row; } .mascot-area { flex: 1.2; background: linear-gradient(135deg, #2B2D42 0%, #1E1F33 50%, #252746 100%); display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; cursor: crosshair; } .canvas-wrapper { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } #mascotCanvas { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; aspect-ratio: 800 / 600; box-shadow: 0 0 0 2px rgba(255,255,255,0.05); border-radius: 32px; } .form-area { flex: 0.9; background: white; display: flex; align-items: center; justify-content: center; padding: 2rem; overflow-y: auto; } .form-card { max-width: 400px; width: 100%; } .form-header { margin-bottom: 32px; } .login-badge { font-size: 18px; font-weight: 600; color: #1A1A1A; margin-bottom: 36px; } .welcome-title { font-size: 34px; font-weight: 700; color: #111; margin-bottom: 8px; } .subtitle { font-size: 15px; color: #6B6B6B; } .input-group { margin-bottom: 20px; } .input-label { font-size: 13px; font-weight: 500; color: #222; margin-bottom: 8px; display: block; } .input-field { width: 100%; padding: 14px 16px; font-size: 15px; font-family: Inter, sans-serif; border: 1.5px solid #E2E2E2; border-radius: 16px; outline: none; transition: all 0.2s; } .input-field:focus { border-color: #3B3B3B; box-shadow: 0 0 0 3px rgba(59, 59, 59, 0.1); } .password-wrapper { position: relative; } .toggle-pwd { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; font-size: 22px; } .row-options { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; margin-top: 8px; } .checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #444; cursor: pointer; } .forgot-link { font-size: 13px; font-weight: 500; color: #2563EB; background: none; border: none; cursor: pointer; } .login-btn { width: 100%; background: white; border: 1.5px solid #222; border-radius: 16px; padding: 14px; font-weight: 600; font-size: 15px; color: #111; cursor: pointer; margin-bottom: 12px; transition: 0.2s; } .login-btn:hover { background: #f8f8f8; transform: scale(0.98); } .google-btn { width: 100%; background: white; border: 1.5px solid #222; border-radius: 16px; padding: 12px; display: flex; align-items: center; justify-content: center; gap: 12px; cursor: pointer; } .google-icon { width: 22px; height: 22px; border: 1px solid #E0E0E0; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-weight: 700; color: #4285F4; } .signup-text { text-align: center; margin-top: 28px; font-size: 14px; color: #666; } .signup-link { font-weight: 700; color: #111; background: none; border: none; cursor: pointer; font-size: 14px; } media (max-width: 860px) { .login-container { flex-direction: column; border-radius: 32px; height: auto; } .mascot-area { min-height: 380px; } } /style /head body div classlogin-container div classmascot-area div classcanvas-wrapper canvas idmascotCanvas width800 height600/canvas /div /div div classform-area div classform-card div classform-header div classlogin-badgeLogin Page/div div classwelcome-titleWelcome back!/div div classsubtitlePlease enter your details/div /div div classinput-group label classinput-labelEmail/label input typeemail idemailInput classinput-field /div div classinput-group label classinput-labelPassword/label div classpassword-wrapper input typepassword idpasswordInput classinput-field placeholderEnter password button classtoggle-pwd idtogglePasswordBtn️/button /div /div div classrow-options label classcheckbox-label input typecheckbox Remember for 30 days /label button classforgot-linkForgot password?/button /div button classlogin-btnLog in/button div classgoogle-btn div classgoogle-iconG/div spanLog in with Google/span /div div classsignup-text Dont have an account? button classsignup-linkSign Up/button /div /div /div /div script (function(){ const canvas document.getElementById(mascotCanvas); let ctx canvas.getContext(2d); // 眼睛状态 (归一化 -1..1, x -1 表示闭眼转身) let currentLook { x: 0, y: 0 }; let targetLook { x: 0, y: 0 }; let animId null; const ANIM_DURATION 220; let idlePointer { x: 0, y: 0 }; let isEmailFocused false; let isPasswordFocused false; const emailInput document.getElementById(emailInput); const passwordInput document.getElementById(passwordInput); const togglePwdBtn document.getElementById(togglePasswordBtn); const WATCH_RIGHT { x: 0.92, y: 0.08 }; const CLOSE_EYES { x: -1.5, y: 0.0 }; function lerpOffset(a, b, t) { return { x: a.x (b.x - a.x) * t, y: a.y (b.y - a.y) * t }; } function animateToTarget(newTarget) { if (animId) cancelAnimationFrame(animId); const start { ...currentLook }; const end { ...newTarget }; const startTime performance.now(); function step(now) { const elapsed now - startTime; let t Math.min(1, elapsed / ANIM_DURATION); const ease 1 - Math.pow(1 - t, 3); currentLook lerpOffset(start, end, ease); drawCanvas(); if (t 1) { animId requestAnimationFrame(step); } else { currentLook { ...end }; drawCanvas(); animId null; } } animId requestAnimationFrame(step); } function computeTargetLook() { if (isPasswordFocused) return { ...CLOSE_EYES }; if (isEmailFocused) return { ...WATCH_RIGHT }; return { x: idlePointer.x, y: idlePointer.y }; } function updateFocusTarget() { animateToTarget(computeTargetLook()); } // 鼠标交互 function updateIdlePointerFromMouse(clientX, clientY) { const rect canvas.getBoundingClientRect(); if (rect.width 0 || rect.height 0) return; let localX (clientX - rect.left) / rect.width; let localY (clientY - rect.top) / rect.height; localX Math.min(1.0, Math.max(0.0, localX)); localY Math.min(1.0, Math.max(0.0, localY)); let nx (localX - 0.5) * 2; let ny (localY - 0.5) * 2; nx Math.min(1.0, Math.max(-1.0, nx)); ny Math.min(1.0, Math.max(-1.0, ny)); idlePointer { x: nx, y: ny }; if (!isEmailFocused !isPasswordFocused) { if (animId) cancelAnimationFrame(animId); currentLook { x: nx, y: ny }; targetLook { x: nx, y: ny }; drawCanvas(); } } function onCanvasMouseMove(e) { updateIdlePointerFromMouse(e.clientX, e.clientY); } function onCanvasMouseLeave() { idlePointer { x: 0, y: 0 }; if (!isEmailFocused !isPasswordFocused) { if (animId) cancelAnimationFrame(animId); currentLook { x: 0, y: 0 }; targetLook { x: 0, y: 0 }; drawCanvas(); } } // 焦点事件 function onEmailFocus() { isEmailFocused true; isPasswordFocused false; updateFocusTarget(); } function onEmailBlur() { isEmailFocused false; updateFocusTarget(); } function onPasswordFocus() { isPasswordFocused true; isEmailFocused false; updateFocusTarget(); } function onPasswordBlur() { isPasswordFocused false; updateFocusTarget(); } let passwordVisible false; function togglePasswordVisibility() { passwordVisible !passwordVisible; passwordInput.type passwordVisible ? text : password; togglePwdBtn.innerHTML passwordVisible ? : ️; } // ---------- 绘图核心直接放大人物不使用缩放变换避免遮挡 ---------- // 所有坐标基于800x600画布人物尺寸直接增大且保证图层顺序蓝(最后层) → 黑 → 橙 → 黄(最前) // 为了让所有人物完整显示且互不遮挡调整位置和大小 function roundedRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x r, y); ctx.lineTo(x w - r, y); ctx.quadraticCurveTo(x w, y, x w, y r); ctx.lineTo(x w, y h - r); ctx.quadraticCurveTo(x w, y h, x w - r, y h); ctx.lineTo(x r, y h); ctx.quadraticCurveTo(x, y h, x, y h - r); ctx.lineTo(x, y r); ctx.quadraticCurveTo(x, y, x r, y); ctx.closePath(); ctx.fill(); } function isClosingEyesMode(look) { return look.x -1.0; } // 点状眼睛 (橙色/黄色) function drawDotEyes(ctx, cx, cy, spacing, dotSize, look, maxShift 6) { const isClose isClosingEyesMode(look); let dx 0, dy 0; if (!isClose) { dx look.x * maxShift; dy look.y * maxShift; dx Math.min(maxShift, Math.max(-maxShift, dx)); dy Math.min(maxShift, Math.max(-maxShift, dy)); } const leftX cx - spacing/2; const rightX cx spacing/2; const eyeY cy; if (!isClose) { ctx.fillStyle #000000; ctx.beginPath(); ctx.arc(leftX dx, eyeY dy, dotSize/2, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(rightX dx, eyeY dy, dotSize/2, 0, Math.PI*2); ctx.fill(); } else { ctx.beginPath(); ctx.moveTo(leftX - dotSize*0.6, eyeY); ctx.lineTo(leftX dotSize*0.6, eyeY); ctx.lineWidth 2.8; ctx.strokeStyle #000; ctx.stroke(); ctx.beginPath(); ctx.moveTo(rightX - dotSize*0.6, eyeY); ctx.lineTo(rightX dotSize*0.6, eyeY); ctx.stroke(); } } // 大眼白眼睛 (蓝色/黑色) function drawGooglyEyes(ctx, cx, cy, eyeSize, pupilSize, gap, look) { const isClose isClosingEyesMode(look); const maxShift (eyeSize - pupilSize) / 2; let dx 0, dy 0; if (!isClose) { dx look.x * maxShift; dy look.y * maxShift; dx Math.min(maxShift, Math.max(-maxShift, dx)); dy Math.min(maxShift, Math.max(-maxShift, dy)); } const leftEyeX cx - gap/2 - eyeSize/2; const rightEyeX cx gap/2 eyeSize/2; const eyeY cy; ctx.fillStyle #FFFFFF; ctx.beginPath(); ctx.arc(leftEyeX eyeSize/2, eyeY, eyeSize/2, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(rightEyeX eyeSize/2, eyeY, eyeSize/2, 0, Math.PI*2); ctx.fill(); if (!isClose) { ctx.fillStyle #000; ctx.beginPath(); ctx.arc(leftEyeX eyeSize/2 dx, eyeY dy, pupilSize/2, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(rightEyeX eyeSize/2 dx, eyeY dy, pupilSize/2, 0, Math.PI*2); ctx.fill(); } else { ctx.beginPath(); ctx.moveTo(leftEyeX 6, eyeY); ctx.lineTo(leftEyeX eyeSize - 6, eyeY); ctx.lineWidth 3; ctx.stroke(); ctx.beginPath(); ctx.moveTo(rightEyeX 6, eyeY); ctx.lineTo(rightEyeX eyeSize - 6, eyeY); ctx.stroke(); } } // 绘制所有小人放大比例并重新调整位置确保无遮挡且显眼 function drawMascots(look) { // 地面基准线 (底部对齐) const groundY 530; // 所有人物X轴偏移量使整体位于画布中央 (原始范围约60~280放大后中心右移) const offsetX 140; // 向右偏移让群组居中 // 1) 蓝色 (最高最后层) ctx.save(); ctx.shadowColor rgba(0,0,0,0.3); ctx.shadowBlur 14; ctx.fillStyle #3958D0; // 放大尺寸宽120高280 const blueW 120, blueH 280; const blueX 70 offsetX; const blueY groundY - blueH; roundedRect(ctx, blueX, blueY, blueW, blueH, 50); // 蓝色眼睛位置 const blueEyeCenterX blueX blueW/2; const blueEyeY blueY 68; drawGooglyEyes(ctx, blueEyeCenterX, blueEyeY, 34, 14, 24, look); ctx.restore(); // 2) 黑色 (位于蓝色右侧略前) ctx.save(); ctx.shadowBlur 12; ctx.fillStyle #1B1B24; const blackW 88, blackH 220; const blackX 140 offsetX; const blackY groundY - blackH; roundedRect(ctx, blackX, blackY, blackW, blackH, 40); const blackEyeCenterX blackX blackW/2; const blackEyeY blackY 62; drawGooglyEyes(ctx, blackEyeCenterX, blackEyeY, 28, 12, 18, look); ctx.restore(); // 3) 橙色 (矮胖放在蓝色和黑色前方但不遮挡眼睛) ctx.save(); ctx.shadowBlur 14; ctx.fillStyle #FF8A4C; const orangeW 140, orangeH 96; const orangeX 40 offsetX; const orangeY groundY - orangeH; roundedRect(ctx, orangeX, orangeY, orangeW, orangeH, 60); const orangeEyeCenterX orangeX orangeW/2; const orangeEyeY orangeY 42; drawDotEyes(ctx, orangeEyeCenterX, orangeEyeY, 34, 9, look, 7); ctx.restore(); // 4) 黄色 (最前最右侧) ctx.save(); ctx.shadowBlur 12; ctx.fillStyle #FFD54F; const yellowW 100, yellowH 130; const yellowX 210 offsetX; const yellowY groundY - yellowH; roundedRect(ctx, yellowX, yellowY, yellowW, yellowH, 48); const yellowEyeCenterX yellowX yellowW/2; const yellowEyeY yellowY 44; drawDotEyes(ctx, yellowEyeCenterX, yellowEyeY, 24, 8, look, 6); // 嘴巴 const mouthShift look.x * 2.5; ctx.beginPath(); ctx.moveTo(yellowX yellowW/2 - 18 mouthShift, yellowY 70); ctx.lineTo(yellowX yellowW/2 18 mouthShift, yellowY 70); ctx.lineWidth 3.5; ctx.strokeStyle #4a3b1c; ctx.stroke(); ctx.restore(); } function drawCanvas() { if (!ctx) return; const w canvas.width, h canvas.height; ctx.clearRect(0, 0, w, h); // 背景渐变 const grad ctx.createLinearGradient(0, 0, w, h); grad.addColorStop(0, #2B2D42); grad.addColorStop(0.6, #1E1F33); grad.addColorStop(1, #252746); ctx.fillStyle grad; ctx.fillRect(0, 0, w, h); // 绘制卡通人物 drawMascots(currentLook); } // 事件绑定与初始化 function init() { emailInput.addEventListener(focus, onEmailFocus); emailInput.addEventListener(blur, onEmailBlur); passwordInput.addEventListener(focus, onPasswordFocus); passwordInput.addEventListener(blur, onPasswordBlur); togglePwdBtn.addEventListener(click, togglePasswordVisibility); canvas.addEventListener(mousemove, onCanvasMouseMove); canvas.addEventListener(mouseleave, onCanvasMouseLeave); togglePwdBtn.addEventListener(mousedown, (e) e.preventDefault()); idlePointer { x: 0, y: 0 }; currentLook { x: 0, y: 0 }; targetLook { x: 0, y: 0 }; drawCanvas(); window.addEventListener(resize, () drawCanvas()); // 观察父容器尺寸变化 (canvas本身尺寸固定但CSS显示可能变化重绘即可) if (window.ResizeObserver) { const ro new ResizeObserver(() drawCanvas()); ro.observe(canvas.parentElement); } } init(); })(); /script /body /html