台灯拖拽登录页面
台灯拖拽登录页面完整介绍(含全部源码)
一、页面整体效果说明
1. 整体风格与布局
页面采用暗色调治愈简约风格,页面水平分为两大区域:左侧卡通SVG台灯、右侧毛玻璃登录表单,全屏深色打底,支持鼠标/触屏拖拽拉绳交互,开灯后灯光光晕、表情、表单同步过渡动画,适配电脑、手机移动端。
2. 关灯默认状态(页面加载初始)
- 背景:纯深灰黑色
#111827,无任何发光光晕; - 台灯:灯罩深灰,闭眼横线困倦表情,无环境柔光;
- 登录框:完全透明模糊、位移偏移,鼠标无法点击输入;
- 拉绳:垂直自然下垂,无拉扯形变。
3. 开灯激活状态(拖拽拉绳触发)
- 背景轻微提亮,双层柔光渲染:大范围全局淡绿色模糊光晕+灯罩局部浅黄柔光;
- 视觉切换:灯罩变为浅黄绿色,台灯切换弯眼微笑+红色小嘴开心表情;
- 登录表单平滑滑入页面,消除模糊、透明度拉满,可正常输入账号密码;
- 交互逻辑:拖拽拉绳横向/纵向幅度超过30px松开,自动切换开灯/关灯;小幅拖拽仅回弹,不切换状态。
4. 核心交互动画细节
- 拉绳物理仿真:拖拽时实时生成贝塞尔曲线路径,绳子随拖动方向自然弯曲;
- 弹簧回弹松手后自动缓冲归位,带摩擦力衰减,无生硬回弹;
- 全套过渡:背景色、光晕透明度、灯罩颜色、人脸样式、表单位移模糊均配置缓动动画;
- 鼠标/触屏兼容:
pointer事件适配电脑鼠标、手机手指拖拽。
5. 登录表单细节
- 毛玻璃半透明容器,深色基底搭配淡绿色渐变高光;
- 输入框内凹深色样式,聚焦自动高亮边框;
- 登录按钮悬浮发光、点击缩小反馈;
- 附带忘记密码跳转链接,整体圆角柔和。
二、本地运行展示教程(3种零基础方案)
方案1:记事本直接打开(最简单,无需工具)
- 在桌面右键 → 新建文本文档;
- 复制下方完整全部代码,粘贴到文档内;
- 左上角【文件】→【另存为】:
- 文件名填写
lamp-login.html - 保存类型:所有文件
- 编码:UTF-8
- 文件名填写
- 保存后双击该html文件,自动用浏览器打开即可拖拽交互。
方案2:VS Code 实时预览(推荐可改代码)
- 安装VS Code,拓展商店安装
Live Server; - 新建空白文件,粘贴完整代码,保存为
lamp-login.html; - 右键文件选择「Open with Live Server」,浏览器自动打开,修改代码页面实时刷新。
方案3:在线编辑器(无需下载软件)
打开 RunJS / CodePen,左侧HTML区域粘贴全部代码,点击运行按钮直接在线预览效果。
三、完整可运行源码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cute Lamp Login</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #111827;
transition: background-color 0.7s ease;
}
body.lit {
background-color: #0b1120;
}
.lamp-glow {
position: absolute;
top: 50%;
left: 40%;
transform: translate(-50%, -50%);
width: 800px;
height: 800px;
background-color: rgba(16, 185, 129, 0.1);
filter: blur(120px);
border-radius: 50%;
pointer-events: none;
opacity: 0;
transition: opacity 1s ease;
}
body.lit .lamp-glow {
opacity: 1;
}
.local-glow {
position: absolute;
top: 110px;
left: 50%;
transform: translateX(-50%);
width: 250px;
height: 150px;
background-color: rgba(217, 249, 157, 0.3);
filter: blur(24px);
border-radius: 50%;
pointer-events: none;
opacity: 0;
transition: opacity 0.5s ease;
}
body.lit .local-glow {
opacity: 1;
}
.lamp-shade {
fill: #1F2937;
transition: fill 0.4s ease;
}
body.lit .lamp-shade {
fill: #D9F99D;
}
.lamp-face {
stroke: #9CA3AF;
transition: stroke 0.4s ease;
}
body.lit .lamp-face {
stroke: #1F2937;
}
.face-sleepy {
display: block;
}
.face-lit {
display: none;
}
body.lit .face-sleepy {
display: none;
}
body.lit .face-lit {
display: block;
}
.login-form {
opacity: 0;
transform: translateX(-30px);
filter: blur(10px);
pointer-events: none;
transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
body.lit .login-form {
opacity: 1;
transform: translateX(0);
filter: blur(0);
pointer-events: auto;
}
.cord-knob {
cursor: grab;
touch-action: none;
}
.cord-knob:active {
cursor: grabbing;
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center relative">
<!-- Background glow when lamp is lit -->
<div class="lamp-glow"></div>
<div class="relative flex items-center justify-center gap-12 z-10 w-full max-w-[800px]">
<!-- Lamp Section -->
<div class="relative w-64 h-96 shrink-0 z-10 select-none">
<!-- SVG Lamp Graphic -->
<svg class="absolute inset-0 w-full h-full pointer-events-none overflow-visible" viewBox="0 0 256 384">
<!-- Stand Base -->
<ellipse cx="128" cy="360" rx="45" ry="12" fill="#374151" />
<ellipse cx="128" cy="356" rx="45" ry="12" fill="#4B5563" />
<!-- Stand Pole -->
<rect x="124" y="180" width="8" height="180" fill="#6B7280" />
<!-- Lamp Shade -->
<path class="lamp-shade" d="M 80 40 L 176 40 L 216 180 L 40 180 Z" />
<!-- Lamp Face -->
<g class="lamp-face" stroke-width="4" fill="none" stroke-linecap="round">
<!-- Sleepy Face -->
<g class="face-sleepy">
<line x1="90" y1="110" x2="106" y2="110" />
<line x1="150" y1="110" x2="166" y2="110" />
<line x1="120" y1="135" x2="136" y2="135" />
</g>
<!-- Happy Face -->
<g class="face-lit">
<path d="M 90 110 Q 98 100 106 110" />
<path d="M 150 110 Q 158 100 166 110" />
<path d="M 112 130 C 112 130 112 155 128 155 C 144 155 144 130 144 130 Z" fill="#EF4444" stroke="#1F2937" stroke-width="3" stroke-linejoin="round" />
</g>
</g>
<!-- Cord String -->
<path id="cord-string" d="M 72 180 C 72 180, 72 240, 72 240" stroke="#6B7280" stroke-width="4" fill="none" stroke-linecap="round" />
</svg>
<!-- Pull Cord Knob -->
<div id="cord-knob" class="cord-knob absolute w-5 h-5 bg-gray-300 rounded-full hover:bg-white shadow-lg z-20" style="top: 230px; left: 62px;"></div>
<!-- Local Glow directly behind lamp shade -->
<div class="local-glow"></div>
</div>
<!-- Login Form Section -->
<div class="relative w-[360px] h-[450px] flex items-center justify-center">
<div class="login-form w-full bg-[#1e2532]/80 backdrop-blur-xl p-8 rounded-3xl shadow-[0_0_40px_rgba(0,0,0,0.5)] border border-emerald-500/20 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-emerald-500/5 to-transparent rounded-3xl pointer-events-none"></div>
<h2 class="text-2xl font-bold text-white mb-8 text-center tracking-wide">欢迎回来</h2>
<form class="space-y-5" onsubmit="event.preventDefault()">
<div>
<label class="block text-sm font-medium text-slate-400 mb-2">账号</label>
<input type="text" placeholder="请输入账号" class="w-full bg-[#111827]/80 border border-slate-700 rounded-xl px-4 py-3.5 text-white placeholder-slate-600 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all shadow-inner" />
</div>
<div>
<label class="block text-sm font-medium text-slate-400 mb-2">密码</label>
<input type="password" placeholder="请输入密码" class="w-full bg-[#111827]/80 border border-slate-700 rounded-xl px-4 py-3.5 text-white placeholder-slate-600 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all shadow-inner" />
</div>
<button class="w-full bg-emerald-500 hover:bg-emerald-400 text-slate-900 font-bold py-3.5 rounded-xl transition-all shadow-[0_0_20px_rgba(16,185,129,0.2)] hover:shadow-[0_0_25px_rgba(16,185,129,0.4)] mt-6 active:scale-[0.98]">
登录
</button>
<div class="text-center pt-3">
<a href="#" class="text-sm text-slate-500 hover:text-emerald-400 transition-colors">忘记密码?</a>
</div>
</form>
</div>
</div>
</div>
<script>
const knob = document.getElementById('cord-knob');
const cordString = document.getElementById('cord-string');
const body = document.body;
let isLit = false;
let isDragging = false;
let startY = 0;
let startX = 0;
let currentY = 0;
let currentX = 0;
let velocityY = 0;
let velocityX = 0;
let springAnimationId = null;
const cordStartX = 72;
const cordStartY = 180;
function updatePositions() {
knob.style.transform = `translate(${currentX}px, ${currentY}px)`;
const endX = cordStartX + currentX;
const endY = 240 + currentY;
// cubic bezier curve calculation for the string
const c1X = cordStartX + currentX * 0.1;
const c1Y = cordStartY + 20 + Math.abs(currentX) * 0.8;
const c2X = endX - currentX * 0.1;
const c2Y = endY - 20 + Math.abs(currentX) * 0.5 - currentY * 0.2;
cordString.setAttribute('d', `M ${cordStartX} ${cordStartY} C ${c1X} ${c1Y}, ${c2X} ${c2Y}, ${endX} ${endY}`);
}
function springBack() {
const spring = 0.05;
const friction = 0.90;
function animate() {
if (isDragging) return;
velocityY += -currentY * spring;
velocityX += -currentX * spring;
velocityY *= friction;
velocityX *= friction;
currentY += velocityY;
currentX += velocityX;
updatePositions();
if (Math.abs(currentY) > 0.1 || Math.abs(velocityY) > 0.1 || Math.abs(currentX) > 0.1 || Math.abs(velocityX) > 0.1) {
springAnimationId = requestAnimationFrame(animate);
} else {
currentY = 0;
currentX = 0;
updatePositions();
}
}
if (springAnimationId) cancelAnimationFrame(springAnimationId);
animate();
}
function onPointerDown(e) {
isDragging = true;
startX = e.clientX - currentX;
startY = e.clientY - currentY;
if (springAnimationId) cancelAnimationFrame(springAnimationId);
knob.setPointerCapture(e.pointerId);
}
function onPointerMove(e) {
if (!isDragging) return;
currentX = e.clientX - startX;
currentY = e.clientY - startY;
// Boundaries
if (currentY < 0) currentY = 0;
if (currentY > 80) currentY = 80;
if (currentX < -80) currentX = -80;
if (currentX > 80) currentX = 80;
updatePositions();
}
function onPointerUp(e) {
if (!isDragging) return;
isDragging = false;
knob.releasePointerCapture(e.pointerId);
if (currentY > 30 || Math.abs(currentX) > 30) {
isLit = !isLit;
if (isLit) {
body.classList.add('lit');
} else {
body.classList.remove('lit');
}
}
springBack();
}
knob.addEventListener('pointerdown', onPointerDown);
knob.addEventListener('pointermove', onPointerMove);
knob.addEventListener('pointerup', onPointerUp);
knob.addEventListener('pointercancel', onPointerUp);
updatePositions();
</script>
</body>
</html>
四、核心模块简要解析
- 样式层 CSS
通过给body添加/移除
lit控制全部灯光、表单、台灯样式切换;多层模糊滤镜实现柔和光晕;贝塞尔缓动函数让表单入场更顺滑。 - SVG 矢量台灯 无外部图片,纯矢量绘制灯座、灯杆、灯罩、两套表情,通过display切换睁眼/闭眼。
- JS 拖拽物理逻辑
- pointer事件兼容鼠标、触屏;
- 实时计算贝塞尔曲线渲染拉扯后的绳子;
- 弹簧+摩擦力物理算法实现自然回弹;
- 拖拽阈值判断,控制开灯关灯状态切换。
五、自定义修改小技巧
- 修改灯光颜色:替换
.lamp-glow、.local-glow内rgba色值; - 调整回弹手感:修改JS中
spring(弹力)、friction(阻尼)数值; - 修改表单尺寸:调整
.login-form的宽高; - 关闭发光:删除两个glow盒子及对应css样式;
- 修改文案:直接修改h2、input占位、按钮文字即可。当前文件内容过长,豆包只阅读了前 91%。

屿宁
一名热爱技术、专注于分享知识的开发者。如果您喜欢我的文章,欢迎关注并订阅 RSS。
相关推荐
上一篇
忆柠工具箱

