practice_code/web/graduation/src/components/aceternity/SpotlightCard.astro

265 lines
8.7 KiB
Plaintext
Raw Normal View History

2025-03-23 01:42:26 +08:00
---
interface Props {
title: string;
description?: string;
image?: string;
link?: string;
className?: string;
color?: string;
spotlightSize?: 'small' | 'medium' | 'large';
spotlightIntensity?: 'subtle' | 'medium' | 'strong';
}
const {
title,
description,
image,
link,
className = "",
color = "rgba(75, 107, 255, 0.3)", // 默认是蓝色光晕
spotlightSize = 'medium',
spotlightIntensity = 'medium'
} = Astro.props;
const id = `spotlight-card-${Math.random().toString(36).substring(2, 11)}`;
// 计算光晕大小
const sizeMap = {
small: '70%',
medium: '100%',
large: '130%'
};
// 计算光晕强度
const intensityMap = {
subtle: '0.15',
medium: '0.25',
strong: '0.4'
};
const spotlightSizeValue = sizeMap[spotlightSize];
const spotlightIntensityValue = intensityMap[spotlightIntensity];
---
<div
id={id}
class={`spotlight-card group relative overflow-hidden rounded-xl border border-white/10 dark:bg-color-dark-card bg-white p-6 shadow-lg transition-all hover:shadow-xl ${className}`}
style="transform-style: preserve-3d; transform: perspective(1000px);"
data-spotlight-size={spotlightSizeValue}
data-spotlight-intensity={spotlightIntensityValue}
>
<div class="spotlight-primary absolute pointer-events-none inset-0 z-0 transition duration-300 opacity-0"></div>
<div class="spotlight-secondary absolute pointer-events-none inset-0 z-0 transition duration-300 opacity-0"></div>
<div class="spotlight-border absolute pointer-events-none inset-0 z-0 transition duration-300 opacity-0"></div>
{image && (
<div class="mb-4 overflow-hidden rounded-lg">
<div class="h-40 bg-gray-300 dark:bg-gray-700 flex items-center justify-center">
<span class="text-gray-500 dark:text-gray-400">{title} 图片</span>
</div>
</div>
)}
<h3 class="group-hover:text-color-primary-600 dark:group-hover:text-color-primary-400 text-lg font-semibold text-gray-900 dark:text-white mb-2 transition-colors">
{title}
</h3>
{description && (
<p class="text-gray-600 dark:text-gray-400 mb-4 text-sm">
{description}
</p>
)}
{link && (
<a
href={link}
class="text-color-primary-600 hover:text-color-primary-700 dark:text-color-primary-400 dark:hover:text-color-primary-300 font-medium text-sm"
>
查看详情 &rarr;
</a>
)}
<slot />
</div>
<style define:vars={{
spotlightColor: color,
spotlightSize: spotlightSizeValue,
spotlightIntensity: spotlightIntensityValue
}}>
.spotlight-primary {
background: radial-gradient(
circle at center,
var(--spotlightColor) 0%,
transparent 70%
);
filter: blur(5px);
opacity: 0;
/* 确保径向渐变是一个完美的圆 */
border-radius: 50%;
}
.spotlight-secondary {
background: radial-gradient(
circle at center,
rgba(255, 255, 255, 0.1) 0%,
transparent 60%
);
opacity: 0;
/* 确保径向渐变是一个完美的圆 */
border-radius: 50%;
}
.spotlight-border {
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: inherit;
opacity: 0;
}
[data-theme='dark'] .spotlight-primary {
background: radial-gradient(
circle at center,
var(--spotlightColor) 0%,
transparent 70%
);
}
[data-theme='dark'] .spotlight-secondary {
background: radial-gradient(
circle at center,
rgba(255, 255, 255, 0.07) 0%,
transparent 60%
);
}
</style>
<script>
// 等待 DOM 加载完成
document.addEventListener('DOMContentLoaded', () => {
// 获取所有光标效果卡片
const cards = document.querySelectorAll('.spotlight-card');
cards.forEach(card => {
const primaryEffect = card.querySelector('.spotlight-primary');
const secondaryEffect = card.querySelector('.spotlight-secondary');
const borderEffect = card.querySelector('.spotlight-border');
if (!primaryEffect || !secondaryEffect || !borderEffect) return;
// 获取配置
const size = card.getAttribute('data-spotlight-size') || '100%';
const intensity = parseFloat(card.getAttribute('data-spotlight-intensity') || '0.25');
// 上一次鼠标位置
let lastX = 0;
let lastY = 0;
// 平滑因子 (值越小越平滑)
const smoothFactor = 0.15;
// 用于动画的requestAnimationFrame ID
let animationFrameId: number | null = null;
// 鼠标移入时添加效果
card.addEventListener('mouseenter', () => {
// 计算光效尺寸 - 使用卡片对角线长度确保覆盖整个卡片区域
const rect = card.getBoundingClientRect();
const diagonalLength = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
const effectSize = diagonalLength * 1.5 + 'px'; // 增加50%确保覆盖
// 设置光效元素尺寸
(primaryEffect as HTMLElement).style.width = effectSize;
(primaryEffect as HTMLElement).style.height = effectSize;
(secondaryEffect as HTMLElement).style.width = effectSize;
(secondaryEffect as HTMLElement).style.height = effectSize;
// 设置可见度
(primaryEffect as HTMLElement).style.opacity = intensity.toString();
(secondaryEffect as HTMLElement).style.opacity = (intensity * 0.7).toString();
(borderEffect as HTMLElement).style.opacity = (intensity * 0.5).toString();
// 启动平滑动画
if (animationFrameId === null) {
animateSpotlight();
}
});
// 鼠标移出时移除效果
card.addEventListener('mouseleave', () => {
(primaryEffect as HTMLElement).style.opacity = '0';
(secondaryEffect as HTMLElement).style.opacity = '0';
(borderEffect as HTMLElement).style.opacity = '0';
// 重置卡片的 3D 效果
(card as HTMLElement).style.transform = 'perspective(1000px)';
// 取消动画
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
});
// 鼠标移动时更新光标位置和 3D 效果
card.addEventListener('mousemove', (e) => {
const mouseEvent = e as MouseEvent;
const rect = card.getBoundingClientRect();
lastX = mouseEvent.clientX - rect.left;
lastY = mouseEvent.clientY - rect.top;
// 计算 3D 旋转效果
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateY = (lastX - centerX) / 20;
const rotateX = (centerY - lastY) / 20;
// 应用 3D 效果
(card as HTMLElement).style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
});
// 平滑动画函数
function animateSpotlight() {
const rect = card.getBoundingClientRect();
// 获取当前位置
const currentPrimaryStyle = window.getComputedStyle(primaryEffect as Element);
const currentSecondaryStyle = window.getComputedStyle(secondaryEffect as Element);
// 解析当前位置
let currentX = parseInt(currentPrimaryStyle.left || '0');
let currentY = parseInt(currentPrimaryStyle.top || '0');
if (isNaN(currentX)) currentX = rect.width / 2;
if (isNaN(currentY)) currentY = rect.height / 2;
// 计算新位置 (平滑过渡)
const newX = currentX + (lastX - currentX) * smoothFactor;
const newY = currentY + (lastY - currentY) * smoothFactor;
// 应用位置
// 不再在这里设置宽高而是在mouseenter时设置一次
(primaryEffect as HTMLElement).style.left = `${newX}px`;
(primaryEffect as HTMLElement).style.top = `${newY}px`;
(primaryEffect as HTMLElement).style.transform = `translate(-50%, -50%)`;
(secondaryEffect as HTMLElement).style.left = `${newX}px`;
(secondaryEffect as HTMLElement).style.top = `${newY}px`;
(secondaryEffect as HTMLElement).style.transform = `translate(-50%, -50%) scale(1.2)`;
// 边框位置不需要移动,但可以随鼠标位置变化透明度
const distanceFromCenter = Math.sqrt(
Math.pow((newX / rect.width - 0.5) * 2, 2) +
Math.pow((newY / rect.height - 0.5) * 2, 2)
);
// 越靠近边缘,边框越明显
const borderOpacity = Math.min(distanceFromCenter * intensity, intensity * 0.5);
(borderEffect as HTMLElement).style.opacity = borderOpacity.toString();
// 继续动画循环
animationFrameId = requestAnimationFrame(animateSpotlight);
}
});
});
</script>