practice_code/web/graduation/src/components/aceternity/SpotlightCard.astro
2025-03-23 01:42:26 +08:00

265 lines
8.7 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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>