磁吸立体卡片

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>磁吸立体卡片 - @xiaoyierle</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #e8e8e8 0%, #d1d1d1 50%, #c8c8c8 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
overflow: hidden;
}
.magnetic-area {
width: 600px;
height: 600px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.card {
width: 320px;
height: 450px;
perspective: 1000px;
cursor: pointer;
position: relative;
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
filter: drop-shadow(0 25px 50px rgba(0, 0, 0, 0.15));
}
.card:hover {
filter: drop-shadow(0 35px 70px rgba(0, 0, 0, 0.25));
}
.card-control-area {
position: absolute;
top: -30px;
left: -30px;
right: -30px;
bottom: -30px;
pointer-events: all;
z-index: 50;
}
.card-content {
position: relative;
width: 100%;
height: 100%;
border-radius: 24px;
background: linear-gradient(145deg, #1a1a1a, #000);
transform-style: preserve-3d;
transition: transform 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.5);
pointer-events: all;
}
.card-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
repeating-linear-gradient(
45deg,
transparent,
transparent 1px,
rgba(255, 255, 255, 0.02) 1px,
rgba(255, 255, 255, 0.02) 2px
);
pointer-events: none;
z-index: 1;
}
.card-layer {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.card-bg-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 24px;
transform: translateZ(-50px) scale(1.1);
filter: contrast(1.2) brightness(0.9) saturate(0.8);
transition: transform 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.card-text {
color: #fff;
font-weight: 600;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
letter-spacing: 0.5px;
}
.title {
align-items: flex-end;
justify-content: flex-start;
padding: 0 0 28px 28px;
font-size: 20px;
transform: translateZ(40px);
}
.code {
align-items: flex-end;
justify-content: flex-end;
padding: 0 28px 28px 0;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
font-size: 20px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
transform: translateZ(40px);
}
.icon {
width: 32px;
height: 32px;
fill: rgba(255, 255, 255, 0.9);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
transition: all 0.2s ease;
}
.icon:hover {
fill: #fff;
transform: scale(1.1);
}
.icon-top-right {
align-items: flex-start;
justify-content: flex-end;
padding: 28px;
transform: translateZ(50px);
cursor: pointer;
z-index: 100;
}
.icon-top-right:hover .icon {
fill: #fff;
transform: scale(1.2);
}
.qr-code {
width: 120px;
height: 120px;
border-radius: 12px;
border: 3px solid rgba(255, 255, 255, 0.9);
box-shadow:
0 8px 25px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
transform: translateZ(80px);
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.4));
animation: qrCodeFloat 4s ease-in-out infinite;
}
@keyframes qrCodeFloat {
0%, 100% {
transform: translateZ(80px) scale(1);
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.4));
}
50% {
transform: translateZ(80px) scale(1.05);
filter: drop-shadow(0 0 25px rgba(255, 255, 255, 0.6));
}
}
.card-glare {
position: absolute;
width: 250px;
height: 250px;
background: radial-gradient(
circle,
rgba(255, 255, 255, 0.6) 0%,
rgba(255, 255, 255, 0.3) 20%,
rgba(255, 255, 255, 0.1) 40%,
rgba(255, 255, 255, 0.05) 60%,
transparent 80%
);
mix-blend-mode: overlay;
opacity: 0;
transform: translate(-50%, -50%) scale(1.5);
transition: opacity 0.2s ease;
pointer-events: none;
z-index: 15;
border-radius: 50%;
}
.magnetic-indicator {
position: absolute;
width: 6px;
height: 6px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 20;
}
.magnetic-indicator.active {
opacity: 1;
animation: magneticPulse 1s ease-in-out infinite;
}
@keyframes magneticPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
}
50% {
transform: scale(1.5);
box-shadow: 0 0 0 12px rgba(255, 255, 255, 0);
}
}
.card-content,
.card-layer,
.card-bg-image {
will-change: transform;
}
@media (max-width: 480px) {
.magnetic-area {
width: 400px;
height: 400px;
}
.card {
width: 280px;
height: 390px;
}
.title {
font-size: 18px;
padding: 0 0 24px 24px;
}
.code {
font-size: 18px;
padding: 0 24px 24px 0;
}
.icon-top-right {
padding: 24px;
}
.icon {
width: 28px;
height: 28px;
}
.qr-code {
width: 100px;
height: 100px;
}
}
</style>
<meta name="code-type" content="html">
</head>
<body>
<div class="magnetic-area">
<div class="magnetic-indicator"></div>
<div class="card">
<div class="card-control-area"></div>
<div class="card-content">
<div class="card-layer">
<img class="card-bg-image" src="https://personaltailor.oss-cn-beijing.aliyuncs.com/blog/20250725/upload_5d3f1e6e40a8b8d13a12564118a74207" alt="xiaoyierle Background">
</div>
<div class="card-glare"></div>
<div class="card-layer title">
<span class="card-text">@xiaoyierle</span>
</div>
<div class="card-layer code">
<span class="card-text"></span>
</div>
<div class="card-layer icon-top-right">
<svg class="icon" viewBox="0 0 24 24">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
</div>
<div class="card-layer">
<img class="qr-code" src="https://personaltailor.oss-cn-beijing.aliyuncs.com/blog/20250725/upload_a7c8b6e5463f25daf54e35aebf1e7a5d" alt="xiaoyierle QR Code">
</div>
</div>
</div>
</div>
<script>
const magneticArea = document.querySelector('.magnetic-area');
const card = document.querySelector('.card');
const content = document.querySelector('.card-content');
const controlArea = document.querySelector('.card-control-area');
const indicator = document.querySelector('.magnetic-indicator');
const homeIcon = document.querySelector('.icon-top-right');
const layers = {
title: document.querySelector('.title'),
code: document.querySelector('.code'),
iconTopRight: document.querySelector('.icon-top-right'),
qrCode: document.querySelector('.qr-code'),
bgImage: document.querySelector('.card-bg-image'),
glare: document.querySelector('.card-glare')
};
const config = {
magneticStrength: 0.3,
magneticRadius: 200,
maxRotate: 25,
rotateMultiplier: 1.5,
sensitivity: {
text: 12,
iconSmall: 16,
qrCode: 20,
background: -8
}
};
let isInMagneticField = false;
let isHovering = false;
let animationFrame;
function getDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
function applyMagneticEffect(mouseX, mouseY) {
const areaRect = magneticArea.getBoundingClientRect();
const cardRect = card.getBoundingClientRect();
const areaCenterX = areaRect.left + areaRect.width / 2;
const areaCenterY = areaRect.top + areaRect.height / 2;
const cardCenterX = cardRect.left + cardRect.width / 2;
const cardCenterY = cardRect.top + cardRect.height / 2;
const distance = getDistance(mouseX, mouseY, areaCenterX, areaCenterY);
if (distance < config.magneticRadius) {
isInMagneticField = true;
const magneticForce = (config.magneticRadius - distance) / config.magneticRadius;
const pullStrength = magneticForce * config.magneticStrength;
const deltaX = (mouseX - cardCenterX) * pullStrength;
const deltaY = (mouseY - cardCenterY) * pullStrength;
card.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
indicator.style.left = `${mouseX - areaRect.left}px`;
indicator.style.top = `${mouseY - areaRect.top}px`;
indicator.classList.add('active');
} else {
isInMagneticField = false;
card.style.transform = 'translate(0px, 0px)';
indicator.classList.remove('active');
}
}
function updateCardTransforms(mouseX, mouseY) {
if (!isHovering) return;
const rotateY = mouseX * config.maxRotate * config.rotateMultiplier;
const rotateX = -mouseY * config.maxRotate * config.rotateMultiplier;
const scale = 1 + (Math.abs(mouseX) + Math.abs(mouseY)) * 0.02;
content.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(${scale})`;
const { sensitivity } = config;
layers.title.style.transform = `translateZ(40px) translateX(${mouseX * sensitivity.text}px) translateY(${mouseY * sensitivity.text}px)`;
layers.code.style.transform = `translateZ(40px) translateX(${mouseX * sensitivity.text}px) translateY(${mouseY * sensitivity.text}px)`;
layers.iconTopRight.style.transform = `translateZ(50px) translateX(${mouseX * sensitivity.iconSmall}px) translateY(${mouseY * sensitivity.iconSmall}px)`;
layers.qrCode.style.transform = `translateZ(80px) translateX(${mouseX * sensitivity.qrCode}px) translateY(${mouseY * sensitivity.qrCode}px)`;
layers.bgImage.style.transform = `translateZ(-50px) scale(1.1) translateX(${mouseX * sensitivity.background}px) translateY(${mouseY * sensitivity.background}px)`;
}
document.addEventListener('mousemove', (e) => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
animationFrame = requestAnimationFrame(() => {
applyMagneticEffect(e.clientX, e.clientY);
if (isHovering) {
const cardRect = card.getBoundingClientRect();
const x = e.clientX - cardRect.left;
const y = e.clientY - cardRect.top;
const { width, height } = cardRect;
const mouseX = (x / width) - 0.5;
const mouseY = (y / height) - 0.5;
updateCardTransforms(mouseX, mouseY);
layers.glare.style.left = `${x}px`;
layers.glare.style.top = `${y}px`;
layers.glare.style.opacity = '1';
}
});
});
controlArea.addEventListener('mouseenter', () => {
isHovering = true;
});
controlArea.addEventListener('mouseleave', () => {
isHovering = false;
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
content.style.transform = 'rotateX(0deg) rotateY(0deg) scale(1)';
layers.title.style.transform = 'translateZ(40px)';
layers.code.style.transform = 'translateZ(40px)';
layers.iconTopRight.style.transform = 'translateZ(50px)';
layers.qrCode.style.transform = 'translateZ(80px)';
layers.bgImage.style.transform = 'translateZ(-50px) scale(1.1)';
layers.glare.style.opacity = '0';
});
card.addEventListener('click', (e) => {
if (e.target.closest('.icon-top-right')) {
return;
}
content.style.transform = 'rotateX(0deg) rotateY(0deg) scale(0.95)';
setTimeout(() => {
content.style.transform = 'rotateX(0deg) rotateY(0deg) scale(1)';
console.log('点击卡片');
}, 150);
});
homeIcon.addEventListener('click', (e) => {
e.stopPropagation();
const icon = homeIcon.querySelector('.icon');
icon.style.transform = 'scale(0.9)';
setTimeout(() => {
icon.style.transform = 'scale(1.1)';
console.log('点击卡片');
}, 100);
});
card.addEventListener('selectstart', (e) => {
e.preventDefault();
});
magneticArea.addEventListener('mouseleave', () => {
isInMagneticField = false;
card.style.transform = 'translate(0px, 0px)';
indicator.classList.remove('active');
});
</script>
</body>
</html>
Comments