mirror of
https://github.com/StepanovPlaton/AboutMe.git
synced 2026-04-04 12:50:49 +04:00
Initial commit
This commit is contained in:
393
src/utils/particle.ts
Normal file
393
src/utils/particle.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
import type { ParticleConfig } from "@/types/config";
|
||||
import { particleConfig } from "@/config";
|
||||
|
||||
|
||||
const BOUNDARY_OFFSET = 100;
|
||||
|
||||
// 粒子对象类
|
||||
class Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
s: number;
|
||||
r: number;
|
||||
a: number;
|
||||
fn: {
|
||||
x: (x: number, y: number) => number;
|
||||
y: (x: number, y: number) => number;
|
||||
r: (r: number) => number;
|
||||
a: (a: number) => number;
|
||||
};
|
||||
idx: number;
|
||||
img: HTMLImageElement;
|
||||
limitArray: number[];
|
||||
config: ParticleConfig;
|
||||
// 构造函数
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
s: number,
|
||||
r: number,
|
||||
a: number,
|
||||
fn: {
|
||||
x: (x: number, y: number) => number;
|
||||
y: (x: number, y: number) => number;
|
||||
r: (r: number) => number;
|
||||
a: (a: number) => number;
|
||||
},
|
||||
idx: number,
|
||||
img: HTMLImageElement,
|
||||
limitArray: number[],
|
||||
config: ParticleConfig,
|
||||
) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.s = s;
|
||||
this.r = r;
|
||||
this.a = a;
|
||||
this.fn = fn;
|
||||
this.idx = idx;
|
||||
this.img = img;
|
||||
this.limitArray = limitArray;
|
||||
this.config = config;
|
||||
}
|
||||
// 绘制粒子
|
||||
draw(cxt: CanvasRenderingContext2D) {
|
||||
cxt.save();
|
||||
cxt.translate(this.x, this.y);
|
||||
cxt.rotate(this.r);
|
||||
cxt.globalAlpha = this.a;
|
||||
cxt.drawImage(this.img, 0, 0, 40 * this.s, 40 * this.s);
|
||||
cxt.restore();
|
||||
}
|
||||
// 更新粒子位置和状态
|
||||
update() {
|
||||
this.x = this.fn.x(this.x, this.y);
|
||||
this.y = this.fn.y(this.y, this.y);
|
||||
this.r = this.fn.r(this.r);
|
||||
this.a = this.fn.a(this.a);
|
||||
// 如果粒子越界或完全透明,重新调整位置
|
||||
if (
|
||||
this.x > window.innerWidth ||
|
||||
this.x < 0 ||
|
||||
this.y > window.innerHeight + BOUNDARY_OFFSET ||
|
||||
this.y < -BOUNDARY_OFFSET || // 从顶部消失
|
||||
this.a <= 0
|
||||
) {
|
||||
// 如果粒子不做限制
|
||||
if (this.limitArray[this.idx] === -1) {
|
||||
this.resetPosition();
|
||||
}
|
||||
// 否则粒子有限制
|
||||
else {
|
||||
if (this.limitArray[this.idx] > 0) {
|
||||
this.resetPosition();
|
||||
this.limitArray[this.idx]--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 重置粒子位置
|
||||
private resetPosition() {
|
||||
this.r = getRandom("fnr", this.config);
|
||||
if (Math.random() > 0.4) {
|
||||
this.x = getRandom("x", this.config);
|
||||
this.y = window.innerHeight + Math.random() * BOUNDARY_OFFSET; // 从屏幕底部开始
|
||||
this.s = getRandom("s", this.config);
|
||||
this.r = getRandom("r", this.config);
|
||||
this.a = getRandom('a', this.config);
|
||||
} else {
|
||||
this.x = window.innerWidth;
|
||||
this.y = getRandom("y", this.config);
|
||||
this.s = getRandom("s", this.config);
|
||||
this.r = getRandom("r", this.config);
|
||||
this.a = getRandom('a', this.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 粒子列表类
|
||||
class ParticleList {
|
||||
list: Particle[];
|
||||
// 构造函数
|
||||
constructor() {
|
||||
this.list = [];
|
||||
}
|
||||
// 添加粒子
|
||||
push(particle: Particle) {
|
||||
this.list.push(particle);
|
||||
}
|
||||
// 更新所有粒子
|
||||
update() {
|
||||
for (let i = 0, len = this.list.length; i < len; i++) {
|
||||
this.list[i].update();
|
||||
}
|
||||
}
|
||||
// 绘制所有粒子
|
||||
draw(cxt: CanvasRenderingContext2D) {
|
||||
for (let i = 0, len = this.list.length; i < len; i++) {
|
||||
this.list[i].draw(cxt);
|
||||
}
|
||||
}
|
||||
// 获取指定索引的粒子
|
||||
get(i: number) {
|
||||
return this.list[i];
|
||||
}
|
||||
// 获取粒子数量
|
||||
size() {
|
||||
return this.list.length;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取随机值的函数
|
||||
function getRandom(option: string, config: ParticleConfig): any {
|
||||
let ret: any;
|
||||
let random: number;
|
||||
// 根据选项获取随机值
|
||||
switch (option) {
|
||||
case "x":
|
||||
ret = Math.random() * window.innerWidth;
|
||||
break;
|
||||
case "y":
|
||||
ret = window.innerHeight + Math.random() * BOUNDARY_OFFSET; // 初始位置在屏幕底部
|
||||
break;
|
||||
case "s":
|
||||
ret =
|
||||
config.size.min + Math.random() * (config.size.max - config.size.min);
|
||||
break;
|
||||
case "r":
|
||||
ret = Math.random() * 6;
|
||||
break;
|
||||
case "a":
|
||||
ret = config.opacity.min + Math.random() * (config.opacity.max - config.opacity.min);
|
||||
break;
|
||||
case "fnx":
|
||||
random = config.speed.horizontal.min + Math.random() * (config.speed.horizontal.max - config.speed.horizontal.min); // x方向保持较小的随机运动
|
||||
ret = function (x: number, y: number) {
|
||||
return x + random;
|
||||
};
|
||||
break;
|
||||
case "fny":
|
||||
random = -(config.speed.vertical.min + Math.random() * (config.speed.vertical.max - config.speed.vertical.min)); // y方向随机向上运动
|
||||
ret = function (x: number, y: number) {
|
||||
return y + random;
|
||||
};
|
||||
break;
|
||||
case "fnr":
|
||||
ret = function (r: number) {
|
||||
return r + config.speed.rotation * 0.1;
|
||||
};
|
||||
break;
|
||||
case "fna":
|
||||
ret = function (alpha: number) {
|
||||
return alpha - config.speed.fadeSpeed * 0.01;
|
||||
};
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// 粒子管理器类
|
||||
export class ParticleManager {
|
||||
private config: ParticleConfig;
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private ctx: CanvasRenderingContext2D | null = null;
|
||||
private particleList: ParticleList | null = null;
|
||||
private animationId: number | null = null;
|
||||
private img: HTMLImageElement | null = null;
|
||||
private isRunning = false;
|
||||
// 构造函数
|
||||
constructor(config: ParticleConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
// 初始化粒子特效
|
||||
async init(): Promise<void> {
|
||||
if (typeof document === "undefined" || !this.config.enable || this.isRunning) {
|
||||
return;
|
||||
}
|
||||
// 创建图片对象
|
||||
this.img = new Image();
|
||||
this.img.src = "/assets/images/particle.png"; // 使用粒子图片
|
||||
// 等待图片加载完成
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (this.img) {
|
||||
this.img.onload = () => resolve();
|
||||
this.img.onerror = () =>
|
||||
reject(new Error("Failed to load particle image"));
|
||||
}
|
||||
});
|
||||
// 创建画布
|
||||
this.createCanvas();
|
||||
// 创建粒子列表
|
||||
this.createParticleList();
|
||||
// 启动动画循环
|
||||
this.startAnimation();
|
||||
// 标记为运行中
|
||||
this.isRunning = true;
|
||||
}
|
||||
// 创建画布
|
||||
private createCanvas(): void {
|
||||
if (typeof document === "undefined") return;
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.height = window.innerHeight;
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.setAttribute(
|
||||
"style",
|
||||
`position: fixed; left: 0; top: 0; pointer-events: none; z-index: ${this.config.zIndex};`,
|
||||
);
|
||||
this.canvas.setAttribute("id", "canvas_particle");
|
||||
document.body.appendChild(this.canvas);
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
// 监听窗口大小变化
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("resize", this.handleResize.bind(this));
|
||||
}
|
||||
}
|
||||
// 创建粒子列表
|
||||
private createParticleList(): void {
|
||||
if (!this.img || !this.ctx) return;
|
||||
this.particleList = new ParticleList();
|
||||
const limitArray = new Array(this.config.particleNum).fill(
|
||||
this.config.limitTimes,
|
||||
);
|
||||
for (let i = 0; i < this.config.particleNum; i++) {
|
||||
const randomX = getRandom("x", this.config);
|
||||
const randomY = getRandom("y", this.config);
|
||||
const randomS = getRandom("s", this.config);
|
||||
const randomR = getRandom("r", this.config);
|
||||
const randomA = getRandom("a", this.config);
|
||||
const randomFnx = getRandom("fnx", this.config);
|
||||
const randomFny = getRandom("fny", this.config);
|
||||
const randomFnR = getRandom("fnr", this.config);
|
||||
const randomFnA = getRandom("fna", this.config);
|
||||
const particle = new Particle(
|
||||
randomX,
|
||||
randomY,
|
||||
randomS,
|
||||
randomR,
|
||||
randomA,
|
||||
{
|
||||
x: randomFnx,
|
||||
y: randomFny,
|
||||
r: randomFnR,
|
||||
a: randomFnA,
|
||||
},
|
||||
i,
|
||||
this.img,
|
||||
limitArray,
|
||||
this.config,
|
||||
);
|
||||
particle.draw(this.ctx);
|
||||
this.particleList.push(particle);
|
||||
}
|
||||
}
|
||||
// 开始动画
|
||||
private startAnimation(): void {
|
||||
if (!this.ctx || !this.canvas || !this.particleList) return;
|
||||
const animate = () => {
|
||||
if (!this.ctx || !this.canvas || !this.particleList) return;
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.particleList.update();
|
||||
this.particleList.draw(this.ctx);
|
||||
this.animationId = requestAnimationFrame(animate);
|
||||
};
|
||||
this.animationId = requestAnimationFrame(animate);
|
||||
}
|
||||
// 处理窗口大小变化
|
||||
private handleResize(): void {
|
||||
if (this.canvas) {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
}
|
||||
}
|
||||
// 停止粒子特效
|
||||
stop(): void {
|
||||
if (this.animationId && typeof window !== "undefined") {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
if (this.canvas && typeof document !== "undefined") {
|
||||
document.body.removeChild(this.canvas);
|
||||
this.canvas = null;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("resize", this.handleResize.bind(this));
|
||||
}
|
||||
this.isRunning = false;
|
||||
}
|
||||
// 切换粒子特效
|
||||
toggle(): void {
|
||||
if (this.isRunning) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
// 更新配置
|
||||
updateConfig(newConfig: ParticleConfig): void {
|
||||
const wasRunning = this.isRunning;
|
||||
if (wasRunning) {
|
||||
this.stop();
|
||||
}
|
||||
this.config = newConfig;
|
||||
if (wasRunning && newConfig.enable) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
// 获取运行状态
|
||||
getIsRunning(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局粒子管理器实例
|
||||
let globalParticleManager: ParticleManager | null = null;
|
||||
|
||||
// 初始化粒子特效
|
||||
export function initParticle(config: ParticleConfig): void {
|
||||
if (globalParticleManager) {
|
||||
globalParticleManager.updateConfig(config);
|
||||
} else {
|
||||
globalParticleManager = new ParticleManager(config);
|
||||
if (config.enable) {
|
||||
globalParticleManager.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换粒子特效
|
||||
export function toggleParticle(): void {
|
||||
if (globalParticleManager) {
|
||||
globalParticleManager.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
// 停止粒子特效
|
||||
export function stopParticle(): void {
|
||||
if (globalParticleManager) {
|
||||
globalParticleManager.stop();
|
||||
globalParticleManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取粒子特效运行状态
|
||||
export function getParticleStatus(): boolean {
|
||||
return globalParticleManager ? globalParticleManager.getIsRunning() : false;
|
||||
}
|
||||
|
||||
// 包含配置检查、重复初始化检查以及页面加载状态处理
|
||||
export function setupParticleEffects(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
// 初始化函数
|
||||
const init = () => {
|
||||
if (!particleConfig || !particleConfig.enable) return;
|
||||
if ((window as any).particleInitialized) return;
|
||||
initParticle(particleConfig);
|
||||
(window as any).particleInitialized = true;
|
||||
};
|
||||
// 处理页面加载状态
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user