Files
handsomezhuzhu.github.io/docs/.vitepress/cursor-rain/index.js
2025-09-28 23:12:40 +08:00

316 lines
10 KiB
JavaScript
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.

'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var gsap = require('gsap');
class CursorRainEffect {
constructor(options = {}) {
this.rainContainer = null;
this.drops = [];
this.isInitialized = false;
this.mouseMoveHandler = null;
this.timeoutId = null;
this.animationPool = [];
this.options = {
maxDrops: options.maxDrops ?? 50,
dropSize: options.dropSize ?? [2, 8],
color: options.color ?? 'rgba(173, 216, 230, 0.8)',
duration: options.duration ?? [0.8, 1.5],
delay: options.delay ?? 100,
enabled: options.enabled ?? true,
zIndex: options.zIndex ?? 9999,
container: options.container ?? document.body
};
this.container = this.options.container;
}
init() {
if (this.isInitialized) {
return;
}
this.createRainContainer();
this.setupEventListeners();
this.preCreateDrops();
this.isInitialized = true;
}
destroy() {
if (!this.isInitialized) {
return;
}
this.cleanup();
this.isInitialized = false;
}
enable() {
this.options.enabled = true;
if (this.isInitialized) {
this.setupEventListeners();
}
}
disable() {
this.options.enabled = false;
this.removeEventListeners();
}
updateOptions(newOptions) {
this.options = { ...this.options, ...newOptions };
if (newOptions.container && newOptions.container !== this.container) {
this.container = newOptions.container;
if (this.isInitialized) {
this.destroy();
this.init();
}
}
}
createRainContainer() {
this.rainContainer = document.createElement('div');
this.rainContainer.className = 'cursor-rain-container';
this.rainContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: ${this.options.zIndex};
overflow: hidden;
`;
this.container.appendChild(this.rainContainer);
}
preCreateDrops() {
// Pre-create drop elements for better performance
for (let i = 0; i < this.options.maxDrops; i++) {
const drop = this.createDropElement();
this.animationPool.push(drop);
if (this.rainContainer) {
this.rainContainer.appendChild(drop);
}
}
}
createDropElement() {
const drop = document.createElement('div');
drop.className = 'rain-drop';
drop.style.cssText = `
position: absolute;
background: ${this.options.color};
border-radius: 50% 50% 50% 50% / 90% 90% 10% 10%;
pointer-events: none;
opacity: 0;
transform-origin: center bottom;
`;
return drop;
}
setupEventListeners() {
if (!this.options.enabled) {
return;
}
this.removeEventListeners();
this.mouseMoveHandler = this.throttle((e) => {
this.createRainAtPosition(e.clientX, e.clientY);
}, 16); // ~60fps throttling
document.addEventListener('mousemove', this.mouseMoveHandler, { passive: true });
}
removeEventListeners() {
if (this.mouseMoveHandler) {
document.removeEventListener('mousemove', this.mouseMoveHandler);
this.mouseMoveHandler = null;
}
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
createRainAtPosition(x, y) {
if (!this.rainContainer || this.drops.length >= this.options.maxDrops) {
return;
}
// Get available drop from pool
const dropElement = this.getAvailableDrop();
if (!dropElement) {
return;
}
const size = this.randomBetween(this.options.dropSize[0], this.options.dropSize[1]);
const duration = this.randomBetween(this.options.duration[0], this.options.duration[1]);
// Add some randomness to position
const offsetX = this.randomBetween(-20, 20);
const offsetY = this.randomBetween(-10, 10);
const finalX = x + offsetX;
const finalY = y + offsetY;
const drop = {
element: dropElement,
x: finalX,
y: finalY,
size,
isAnimating: true
};
this.drops.push(drop);
// Set initial position and size
gsap.gsap.set(dropElement, {
x: finalX - size / 4, // 调整X偏移因为宽度变小了
y: finalY - size / 2,
width: size * 0.3, // 宽度减小到原来的30%,让雨滴更细
height: size * 4, // 高度增加到4倍让雨滴更长
opacity: 0,
scaleY: 0.1,
rotation: this.randomBetween(-15, 15)
});
// Animate the raindrop - continuous fall with gradual rotation change
const initialRotation = this.randomBetween(-15, 15); // 初始随机角度
const fallDistance = window.innerHeight - finalY + 100;
const tl = gsap.gsap.timeline({
onComplete: () => {
this.returnDropToPool(drop);
}
});
// 快速出现
tl.to(dropElement, {
opacity: 1,
scaleY: 1,
duration: 0.1,
ease: 'power2.out'
})
// 连续下落:一次性完成整个下落过程,同时角度逐渐变垂直
.to(dropElement, {
y: `+=${fallDistance}`, // 一次性完成所有下落距离
x: `+=${initialRotation * 0.3}`, // 轻微的水平漂移
scaleY: 1.5, // 逐渐拉长
opacity: 0.3, // 逐渐变透明
rotation: 0, // 角度从初始角度平滑变为垂直
duration: duration,
ease: 'power1.in' // 重力加速效果
}, 0.05);
}
getAvailableDrop() {
return this.animationPool.find(drop => gsap.gsap.getTweensOf(drop).length === 0) || null;
}
returnDropToPool(drop) {
const index = this.drops.indexOf(drop);
if (index > -1) {
this.drops.splice(index, 1);
}
drop.isAnimating = false;
// Reset the element
gsap.gsap.set(drop.element, {
opacity: 0,
x: 0,
y: 0,
scaleY: 1,
rotation: 0
});
}
randomBetween(min, max) {
return Math.random() * (max - min) + min;
}
throttle(func, delay) {
let timeoutId = null;
let lastExecTime = 0;
return (...args) => {
const currentTime = Date.now();
if (currentTime - lastExecTime > delay) {
func(...args);
lastExecTime = currentTime;
}
else {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = window.setTimeout(() => {
func(...args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}
cleanup() {
this.removeEventListeners();
// Kill all GSAP animations
this.drops.forEach(drop => {
gsap.gsap.killTweensOf(drop.element);
});
this.drops = [];
this.animationPool = [];
if (this.rainContainer && this.rainContainer.parentNode) {
this.rainContainer.parentNode.removeChild(this.rainContainer);
this.rainContainer = null;
}
}
}
/**
* Create a new cursor rain effect instance
*/
function createCursorRainEffect(options) {
return new CursorRainEffect(options);
}
/**
* Initialize cursor rain effect with default options
* This is a convenience function for quick setup
*/
function initCursorRain(options) {
const effect = createCursorRainEffect(options);
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
effect.init();
});
}
else {
// DOM is already ready
effect.init();
}
return effect;
}
/**
* VitePress compatible initialization
* This function ensures the effect works correctly in VitePress environment
*/
function initCursorRainForVitePress(options) {
const effect = createCursorRainEffect({
container: document.body,
zIndex: 1000, // Lower z-index to avoid conflicts with VitePress UI
...options
});
// Handle VitePress page navigation
const initEffect = () => {
// Small delay to ensure VitePress has finished rendering
setTimeout(() => {
effect.init();
}, 100);
};
// Handle both initial load and client-side navigation
if (typeof window !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initEffect);
}
else {
initEffect();
}
// Handle VitePress client-side navigation
window.addEventListener('popstate', () => {
effect.destroy();
initEffect();
});
// Handle programmatic navigation (if using Vue Router)
if (window.history && window.history.pushState) {
const originalPushState = window.history.pushState;
window.history.pushState = function (...args) {
originalPushState.apply(window.history, args);
effect.destroy();
initEffect();
};
}
}
return effect;
}
// Default export for convenience
var index = {
createCursorRainEffect,
initCursorRain,
initCursorRainForVitePress,
CursorRainEffect
};
exports.CursorRainEffect = CursorRainEffect;
exports.createCursorRainEffect = createCursorRainEffect;
exports.default = index;
exports.initCursorRain = initCursorRain;
exports.initCursorRainForVitePress = initCursorRainForVitePress;