From e308029b45020ef7675a77be1a119b48b0d630d9 Mon Sep 17 00:00:00 2001 From: ChisatoNishikigi73 Date: Sun, 29 Sep 2024 19:30:49 +0800 Subject: [PATCH 1/6] fix: Fixed logo color mismatch variable, so manual setting is used --- src/components/Init.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Init.tsx b/src/components/Init.tsx index c19a274..cd65635 100644 --- a/src/components/Init.tsx +++ b/src/components/Init.tsx @@ -139,7 +139,7 @@ export function Init() {
@@ -169,7 +169,7 @@ export function Init() {
- + {`LOADING - ${Math.round(progress)}%`}
From 39e608de7456baa3074be9f047f9ad24a7561737 Mon Sep 17 00:00:00 2001 From: ChisatoNishikigi73 Date: Sun, 29 Sep 2024 19:33:50 +0800 Subject: [PATCH 2/6] feat: Preliminary completion of particle system --- src/components/ParticleFactory.tsx | 548 +++++++++++++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 src/components/ParticleFactory.tsx diff --git a/src/components/ParticleFactory.tsx b/src/components/ParticleFactory.tsx new file mode 100644 index 0000000..39ec8d9 --- /dev/null +++ b/src/components/ParticleFactory.tsx @@ -0,0 +1,548 @@ +import React, { useRef, useState, useEffect } from 'react'; +// TODO : 在粒子生成动画中应用鼠标影响 +// TODO : caven尺寸改变时只需要让粒子改变 + +const logos = [ + { label: "kazimierz", url: "/images/03-world/infected.png" }, + { label: "rhine", url: "/images/03-world/nomadic_city.png" }, + { label: "rhodes", url: "/images/03-world/originium_arts.png" }, + { label: "victoria", url: "/images/03-world/originiums.png" }, + { label: "yan", url: "/images/03-world/reunion.png" }, + { label: "test", url: "favicon.svg" }, +]; + +const width = 400; +const height = 400; + +const animateTime = 40; +const opacityStep = 1 / animateTime; + +const Radius = 30; +const Inten = 0.2; +const LargeRadius = 400; +const LargeInten = 0.00005; + +/** 粒子半径 */ +const BaseParticleRadius = 0.5; +/** 粒子密度 */ +const ParticleDensity = 10; +/** 整体缩放因子 */ +let scale = 4; + +/** 粒子类 */ +class Particle { + x: number; + y: number; + totalX: number; + totalY: number; + mx?: number; + my?: number; + vx?: number; + vy?: number; + time: number; + r: number; + color: number[]; + opacity: number; + initialX: number; + initialY: number; + progress: number; + animationProgress: number; + animationDuration: number; + initialOpacity: number; + offsetX: number; + offsetY: number; + grayColor: number; + + constructor(totalX: number, totalY: number, time: number, color: number[]) { + this.x = totalX; + this.y = totalY; + this.totalX = totalX; + this.totalY = totalY; + this.time = time; + this.r = BaseParticleRadius * scale; + this.color = [...color]; + this.opacity = 0; + const angle = Math.random() * Math.PI * 2; + const radius = Math.random() * Math.min(width, height) / 2; + this.initialX = width / 2 + Math.cos(angle) * radius; + this.initialY = height / 2 + Math.sin(angle) * radius; + this.x = this.initialX; + this.y = this.initialY; + this.initialOpacity = 0; + this.opacity = this.initialOpacity; + this.progress = 0; + this.animationProgress = 0; + this.animationDuration = 3400; + this.offsetX = (Math.random() - 0.5) * 1; + this.offsetY = (Math.random() - 0.5) * 1; + this.grayColor = Math.round(0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]); + } + + draw(ctx: CanvasRenderingContext2D, isGrayscale: boolean) { + const centerX = width / 2; + const centerY = height / 2; + const scaledX = centerX + (this.x - centerX) * scale; + const scaledY = centerY + (this.y - centerY) * scale; + + if (isGrayscale) { + ctx.fillStyle = `rgba(${this.grayColor}, ${this.grayColor}, ${this.grayColor}, ${this.opacity})`; + } else { + ctx.fillStyle = `rgba(${this.color[0]}, ${this.color[1]}, ${this.color[2]}, ${this.opacity})`; + } + ctx.beginPath(); + ctx.arc(scaledX, scaledY, this.r, 0, Math.PI * 2); + ctx.fill(); + } + + update(deltaTime: number, mouseX?: number, mouseY?: number) { + if (this.animationProgress < this.animationDuration) { + this.animationProgress += deltaTime; + const progress = Math.min(this.animationProgress / this.animationDuration, 1); + const easeProgress = easeOutLog(progress); + this.x = lerp(this.initialX, this.totalX, easeProgress); + this.y = lerp(this.initialY, this.totalY, easeProgress); + this.opacity = lerp(this.initialOpacity, 1, easeProgress); + } else { + this.mx = this.totalX - this.x; + this.my = this.totalY - this.y; + this.vx = this.mx / this.time; + this.vy = this.my / this.time; + + if (mouseX !== undefined && mouseY !== undefined) { + const centerX = width / 2; + const centerY = height / 2; + const scaledMouseX = centerX + (mouseX - centerX) / scale; + const scaledMouseY = centerY + (mouseY - centerY) / scale; + let dx = scaledMouseX - this.x; + let dy = scaledMouseY - this.y; + let distance = Math.sqrt(dx ** 2 + dy ** 2); + + if (distance < LargeRadius) { + let largeDisPercent = LargeRadius / distance; + largeDisPercent = largeDisPercent > 7 ? 7 : largeDisPercent; + let largeAngle = Math.atan2(dy, dx); + let largeCos = Math.cos(largeAngle); + let largeSin = Math.sin(largeAngle); + let largeRepX = largeCos * largeDisPercent * -LargeInten; + let largeRepY = largeSin * largeDisPercent * -LargeInten; + this.vx += largeRepX; + this.vy += largeRepY; + } + + let disPercent = Radius / distance; + disPercent = disPercent > 7 ? 7 : disPercent; + let angle = Math.atan2(dy, dx); + let cos = Math.cos(angle); + let sin = Math.sin(angle); + let repX = cos * disPercent * -Inten; + let repY = sin * disPercent * -Inten; + this.vx += repX; + this.vy += repY; + } + + this.x += this.vx!; + this.y += this.vy!; + } + + if (this.opacity < 1) this.opacity += opacityStep; + } + + change(x: number, y: number, color: number[]) { + this.totalX = x; + this.totalY = y; + this.color = [...color]; + this.time = animateTime; + } + + updateTransition(targetParticle: Particle | undefined, progress: number) { + if (!targetParticle) return; + + this.totalX = lerp(this.totalX, targetParticle.totalX, progress); + this.totalY = lerp(this.totalY, targetParticle.totalY, progress); + this.color = this.color.map((c, i) => lerp(c, targetParticle.color[i], progress)); + } +} + +function lerp(start: number, end: number, t: number): number { + return start * (1 - t) + end * t; +} + +function easeOutLog(t: number): number { + return 1 - Math.pow(2, -10 * t); +} + +/** Logo图片类 */ +class LogoImg { + src: string; + name: string; + particleData: Particle[]; + isLoaded: boolean; + constructor(src: string, name: string) { + this.src = src; + this.name = name; + this.particleData = []; + this.isLoaded = false; + + if (src.endsWith('.svg')) { + this.loadSVG(src); + } else { + this.loadImage(src); + } + } + + loadImage(src: string) { + let img = new Image(); + img.crossOrigin = ""; + img.src = src; + img.onload = () => { + const tmp_canvas = document.createElement("canvas"); + const tmp_ctx = tmp_canvas.getContext("2d"); + const imgW = width * scale; + const imgH = ~~(width * (img.height / img.width) * scale); + tmp_canvas.width = imgW; + tmp_canvas.height = imgH; + tmp_ctx?.drawImage(img, 0, 0, imgW, imgH); + const imgData = tmp_ctx?.getImageData(0, 0, imgW, imgH).data; + tmp_ctx?.clearRect(0, 0, imgW, imgH); + + for (let y = 0; y < imgH; y += ParticleDensity) { + for (let x = 0; x < imgW; x += ParticleDensity) { + const index = (x + y * imgW) * 4; + const r = imgData![index]; + const g = imgData![index + 1]; + const b = imgData![index + 2]; + const a = imgData![index + 3]; + const sum = r + g + b + a; + if (sum >= 100) { + const offsetX = (Math.random() * 2 - 1) / scale; + const offsetY = (Math.random() * 2 - 1) / scale; + const particle = new Particle((x / scale) + offsetX, (y / scale) + offsetY, animateTime, [r, g, b, a]); + this.particleData.push(particle); + } + } + } + window.dispatchEvent(new CustomEvent('logoImageLoaded', { detail: { name: this.name } })); + }; + } + + loadSVG(src: string) { + fetch(src) + .then(response => response.text()) + .then(svgText => { + const svg = new Blob([svgText], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(svg); + const img = new Image(); + img.src = url; + img.onload = () => { + const tmp_canvas = document.createElement("canvas"); + const tmp_ctx = tmp_canvas.getContext("2d"); + const imgW = width * scale; + const imgH = ~~(width * (img.height / img.width) * scale); + tmp_canvas.width = imgW; + tmp_canvas.height = imgH; + tmp_ctx?.drawImage(img, 0, 0, imgW, imgH); + const imgData = tmp_ctx?.getImageData(0, 0, imgW, imgH).data; + tmp_ctx?.clearRect(0, 0, imgW, imgH); + + for (let y = 0; y < imgH; y += ParticleDensity) { + for (let x = 0; x < imgW; x += ParticleDensity) { + const index = (x + y * imgW) * 4; + const r = imgData![index]; + const g = imgData![index + 1]; + const b = imgData![index + 2]; + const a = imgData![index + 3]; + const sum = r + g + b + a; + if (sum >= 100) { + const offsetX = (Math.random() * 2 - 1) / scale; + const offsetY = (Math.random() * 2 - 1) / scale; + const particle = new Particle((x / scale) + offsetX, (y / scale) + offsetY, animateTime, [r, g, b, a]); + this.particleData.push(particle); + } + } + } + window.dispatchEvent(new CustomEvent('logoImageLoaded', { detail: { name: this.name } })); + }; + }); + } +} + +// 画布类 +class ParticleCanvas { + canvasEle: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; + width: number; + height: number; + ParticleArr: Particle[]; + mouseX?: number; + mouseY?: number; + currentLogo: LogoImg | null; + particleAreaWidth: number; + particleAreaHeight: number; + lastUpdateTime: number; + debug: boolean; + isGrayscale: boolean; + particleAreaX: number; + particleAreaY: number; + transitionProgress: number; + isTransitioning: boolean; + targetParticles: Particle[]; + private animationFrameId: number | null = null; + private scale: number; + + constructor( + target: HTMLCanvasElement, + particleAreaWidth: number, + particleAreaHeight: number, + isGrayscale: boolean, + particleAreaX?: number, + particleAreaY?: number, + initialScale: number = 4 + ) { + this.canvasEle = target; + this.ctx = target.getContext("2d") as CanvasRenderingContext2D; + this.width = target.width; + this.height = target.height; + this.ParticleArr = []; + this.currentLogo = null; + this.particleAreaWidth = particleAreaWidth; + this.particleAreaHeight = particleAreaHeight; + this.lastUpdateTime = performance.now(); + this.debug = true; + this.isGrayscale = isGrayscale; + this.particleAreaX = particleAreaX ?? this.width - this.particleAreaWidth - 50; + this.particleAreaY = particleAreaY ?? (this.height - this.particleAreaHeight) / 2; + this.transitionProgress = 0; + this.isTransitioning = false; + this.targetParticles = []; + this.scale = initialScale; + scale = initialScale; + + this.canvasEle.addEventListener("mousemove", this.handleMouseMove); + this.canvasEle.addEventListener("mouseleave", this.handleMouseLeave); + } + + handleMouseMove = (e: MouseEvent) => { + const { left, top } = this.canvasEle.getBoundingClientRect(); + this.mouseX = e.clientX - left; + this.mouseY = e.clientY - top; + } + + handleMouseLeave = () => { + this.mouseX = undefined; + this.mouseY = undefined; + } + + changeImg(img: LogoImg) { + if (this.currentLogo && this.currentLogo !== img) { + this.startTransition(img); + } else { + this.currentLogo = img; + this.updateParticles(); + } + } + + updateParticles() { + if (!this.currentLogo) { + return; + } + if (this.ParticleArr.length === 0) { + this.ParticleArr = this.currentLogo.particleData.map( + (item) => new Particle(item.totalX, item.totalY, animateTime, item.color) + ); + } else { + this.startTransition(this.currentLogo); + } + } + + startTransition(newLogo: LogoImg) { + this.isTransitioning = true; + this.transitionProgress = 0; + this.targetParticles = newLogo.particleData.map( + (item) => new Particle(item.totalX, item.totalY, animateTime, item.color) + ); + this.currentLogo = newLogo; + } + + drawCanvas() { + const currentTime = performance.now(); + const deltaTime = currentTime - this.lastUpdateTime; + this.lastUpdateTime = currentTime; + + this.ctx.clearRect(0, 0, this.width, this.height); + + if (this.ParticleArr.length > 0) { + const particleAreaX = this.particleAreaX; + const particleAreaY = this.particleAreaY; + + let relativeMouseX = this.mouseX !== undefined ? this.mouseX - particleAreaX : undefined; + let relativeMouseY = this.mouseY !== undefined ? this.mouseY - particleAreaY : undefined; + + if (this.isTransitioning) { + this.transitionProgress += deltaTime / 1000; + if (this.transitionProgress >= 1) { + this.isTransitioning = false; + this.ParticleArr = this.targetParticles; + this.targetParticles = []; + } else { + this.ParticleArr.forEach((particle, index) => { + particle.updateTransition(this.targetParticles[index], this.transitionProgress); + }); + } + } + + this.ctx.save(); + this.ctx.translate(particleAreaX, particleAreaY); + + this.ParticleArr.forEach(particle => { + particle.update(deltaTime, relativeMouseX, relativeMouseY); + particle.draw(this.ctx, this.isGrayscale); + }); + + if (this.debug && this.mouseX !== undefined && this.mouseY !== undefined) { + this.ctx.beginPath(); + this.ctx.arc(relativeMouseX!, relativeMouseY!, LargeRadius, 0, Math.PI * 2); + this.ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.arc(relativeMouseX!, relativeMouseY!, Radius, 0, Math.PI * 2); + this.ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)'; + this.ctx.stroke(); + } + + this.ctx.restore(); + } else if (this.currentLogo && this.currentLogo.particleData.length > 0) { + this.updateParticles(); + } + + this.animationFrameId = window.requestAnimationFrame(() => this.drawCanvas()); + } + + toggleDebug() { + this.debug = !this.debug; + } + + setGrayscale(isGrayscale: boolean) { + this.isGrayscale = isGrayscale; + } + + stop() { + if (this.animationFrameId) { + window.cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = null; + } + this.canvasEle.removeEventListener("mousemove", this.handleMouseMove); + this.canvasEle.removeEventListener("mouseleave", this.handleMouseLeave); + } + + changeScale(newScale: number) { + scale = newScale; + this.ParticleArr.forEach(particle => { + particle.r = BaseParticleRadius * scale; + }); + } +} + +interface ParticleSystemProps { + activeLabel?: string; + width: number; + height: number; + isGrayscale: boolean; + particleAreaX?: number; + particleAreaY?: number; + scale?: number; +} + +const ParticleFactory: React.FC = ({ + activeLabel, + width, + height, + isGrayscale, + particleAreaX, + particleAreaY, + scale: initialScale, +}) => { + const [activeLogo, setActiveLogo] = useState(null); + const [logoImgs, setLogoImgs] = useState([]); + const [particleCanvas, setParticleCanvas] = useState(null); + const canvasRef = useRef(null); + + useEffect(() => { + const newLogoImgs = logos.map(item => new LogoImg(item.url, item.label)); + setLogoImgs(newLogoImgs); + + if (canvasRef.current) { + const particleAreaWidth = width / 3; + const particleAreaHeight = height / 2; + + if (particleCanvas) { + particleCanvas.stop(); + } + + const newParticleCanvas = new ParticleCanvas( + canvasRef.current, + particleAreaWidth, + particleAreaHeight, + isGrayscale, + particleAreaX, + particleAreaY, + initialScale + ); + setParticleCanvas(newParticleCanvas); + newParticleCanvas.drawCanvas(); + } + + return () => { + if (particleCanvas) { + particleCanvas.stop(); + } + }; + }, [width, height, isGrayscale, particleAreaX, particleAreaY, initialScale]); + + useEffect(() => { + if (activeLabel && logoImgs.length > 0 && particleCanvas) { + const selectedLogo = logoImgs.find(logo => logo.name === activeLabel); + if (selectedLogo && selectedLogo.isLoaded) { + particleCanvas.changeImg(selectedLogo); + } + } + }, [activeLabel, logoImgs, particleCanvas]); + + const handleLogoClick = (logoItem: LogoImg) => { + setActiveLogo(logoItem); + if (particleCanvas) { + particleCanvas.changeImg(logoItem); + } + }; + + useEffect(() => { + if (logoImgs.length > 0 && particleCanvas) { + const defaultLogo = logoImgs.find(logo => logo.name === "test"); + if (defaultLogo) { + handleLogoClick(defaultLogo); + } else { + console.log(`logo not found`); + } + } + }, [logoImgs, particleCanvas]); + + useEffect(() => { + if (particleCanvas) { + particleCanvas.setGrayscale(isGrayscale); + } + }, [isGrayscale, particleCanvas]); + + const toggleDebug = () => { + if (particleCanvas) { + particleCanvas.toggleDebug(); + } + }; + + return ( +
+ +
+ ); +}; + +export default ParticleFactory; \ No newline at end of file From 51113c6043aacd9a840a82208a3cb54e13693bdd Mon Sep 17 00:00:00 2001 From: ChisatoNishikigi73 Date: Sun, 29 Sep 2024 21:44:16 +0800 Subject: [PATCH 3/6] fix: Comment out the debug information in the init phase --- src/components/Init.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Init.tsx b/src/components/Init.tsx index cd65635..cfbd153 100644 --- a/src/components/Init.tsx +++ b/src/components/Init.tsx @@ -185,7 +185,7 @@ export function Init() {
-
@@ -199,7 +199,7 @@ export function Init() { {$isInitialized ? "背景图片加载成功" : "正在加载背景图片..."}

加载进度:{Math.round(progress)}%

-
+
*/}
); } \ No newline at end of file From c9dad2ca325887c1912d664ea71c3ee6e42b89f8 Mon Sep 17 00:00:00 2001 From: ChisatoNishikigi73 Date: Sun, 29 Sep 2024 23:38:31 +0800 Subject: [PATCH 4/6] fix: update particle factory --- src/components/ParticleFactory.tsx | 199 ++++++++++++++++++++--------- 1 file changed, 142 insertions(+), 57 deletions(-) diff --git a/src/components/ParticleFactory.tsx b/src/components/ParticleFactory.tsx index 39ec8d9..775eec1 100644 --- a/src/components/ParticleFactory.tsx +++ b/src/components/ParticleFactory.tsx @@ -1,34 +1,50 @@ -import React, { useRef, useState, useEffect } from 'react'; // TODO : 在粒子生成动画中应用鼠标影响 // TODO : caven尺寸改变时只需要让粒子改变 -const logos = [ - { label: "kazimierz", url: "/images/03-world/infected.png" }, - { label: "rhine", url: "/images/03-world/nomadic_city.png" }, - { label: "rhodes", url: "/images/03-world/originium_arts.png" }, - { label: "victoria", url: "/images/03-world/originiums.png" }, - { label: "yan", url: "/images/03-world/reunion.png" }, - { label: "test", url: "favicon.svg" }, -]; +import React, { useRef, useState, useEffect } from 'react'; +// 全局常量 const width = 400; const height = 400; - const animateTime = 40; const opacityStep = 1 / animateTime; - const Radius = 30; -const Inten = 0.2; +const Inten = 0.25; const LargeRadius = 400; const LargeInten = 0.00005; - -/** 粒子半径 */ -const BaseParticleRadius = 0.5; -/** 粒子密度 */ -const ParticleDensity = 10; +const BaseParticleRadius = 0.6; +const ParticleDensity = 7; + +// 全局变量 +/** 粒子对鼠标的敏感度 */ +let mouseSensitivity = 5; +/** 明度阈值 (0-255) */ +let brightnessThreshold = 100; +/** 透明度阈值 (0-255) */ +let alphaThreshold = 16; /** 整体缩放因子 */ let scale = 4; +// Logo 数据 +const logos = [ + { label: "kazimierz", url: "/images/03-world/infected.png" }, + { label: "rhine", url: "/images/03-world/nomadic_city.png" }, + { label: "rhodes", url: "/images/03-world/originium_arts.png" }, + { label: "victoria", url: "/images/03-world/originiums.png" }, + { label: "yan", url: "/images/03-world/reunion.png" }, + { label: "island", url: "/images/rhodes_island.png" }, +]; + +// 辅助函数 +function lerp(start: number, end: number, t: number): number { + return start * (1 - t) + end * t; +} + +function easeOutLog(t: number): number { + // TODO: We need a better function + return 1 - Math.pow(1.8, -12 * t); +} + /** 粒子类 */ class Particle { x: number; @@ -52,6 +68,8 @@ class Particle { offsetX: number; offsetY: number; grayColor: number; + exitVx?: number; + exitVy?: number; constructor(totalX: number, totalY: number, time: number, color: number[]) { this.x = totalX; @@ -63,7 +81,7 @@ class Particle { this.color = [...color]; this.opacity = 0; const angle = Math.random() * Math.PI * 2; - const radius = Math.random() * Math.min(width, height) / 2; + const radius = Math.random() * Math.min(width, height) / 1.2; // 粒子初始位置范围 this.initialX = width / 2 + Math.cos(angle) * radius; this.initialY = height / 2 + Math.sin(angle) * radius; this.x = this.initialX; @@ -72,7 +90,7 @@ class Particle { this.opacity = this.initialOpacity; this.progress = 0; this.animationProgress = 0; - this.animationDuration = 3400; + this.animationDuration = 3000; // 出场动画持续时间 this.offsetX = (Math.random() - 0.5) * 1; this.offsetY = (Math.random() - 0.5) * 1; this.grayColor = Math.round(0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]); @@ -123,8 +141,8 @@ class Particle { let largeAngle = Math.atan2(dy, dx); let largeCos = Math.cos(largeAngle); let largeSin = Math.sin(largeAngle); - let largeRepX = largeCos * largeDisPercent * -LargeInten; - let largeRepY = largeSin * largeDisPercent * -LargeInten; + let largeRepX = largeCos * largeDisPercent * -LargeInten * mouseSensitivity; + let largeRepY = largeSin * largeDisPercent * -LargeInten * mouseSensitivity; this.vx += largeRepX; this.vy += largeRepY; } @@ -134,8 +152,8 @@ class Particle { let angle = Math.atan2(dy, dx); let cos = Math.cos(angle); let sin = Math.sin(angle); - let repX = cos * disPercent * -Inten; - let repY = sin * disPercent * -Inten; + let repX = cos * disPercent * -Inten * mouseSensitivity; + let repY = sin * disPercent * -Inten * mouseSensitivity; this.vx += repX; this.vy += repY; } @@ -163,14 +181,6 @@ class Particle { } } -function lerp(start: number, end: number, t: number): number { - return start * (1 - t) + end * t; -} - -function easeOutLog(t: number): number { - return 1 - Math.pow(2, -10 * t); -} - /** Logo图片类 */ class LogoImg { src: string; @@ -212,8 +222,8 @@ class LogoImg { const g = imgData![index + 1]; const b = imgData![index + 2]; const a = imgData![index + 3]; - const sum = r + g + b + a; - if (sum >= 100) { + const brightness = Math.max(r, g, b); + if (brightness >= brightnessThreshold && a >= alphaThreshold) { const offsetX = (Math.random() * 2 - 1) / scale; const offsetY = (Math.random() * 2 - 1) / scale; const particle = new Particle((x / scale) + offsetX, (y / scale) + offsetY, animateTime, [r, g, b, a]); @@ -288,6 +298,10 @@ class ParticleCanvas { targetParticles: Particle[]; private animationFrameId: number | null = null; private scale: number; + private exitAnimationDuration: number = 1000; // 离场动画持续时间 + private newImageDelay: number = 100; // 新图片加载延迟 + private isExiting: boolean = false; + private nextLogo: LogoImg | null = null; constructor( target: HTMLCanvasElement, @@ -307,7 +321,7 @@ class ParticleCanvas { this.particleAreaWidth = particleAreaWidth; this.particleAreaHeight = particleAreaHeight; this.lastUpdateTime = performance.now(); - this.debug = true; + this.debug = false; this.isGrayscale = isGrayscale; this.particleAreaX = particleAreaX ?? this.width - this.particleAreaWidth - 50; this.particleAreaY = particleAreaY ?? (this.height - this.particleAreaHeight) / 2; @@ -334,33 +348,38 @@ class ParticleCanvas { changeImg(img: LogoImg) { if (this.currentLogo && this.currentLogo !== img) { - this.startTransition(img); + this.nextLogo = img; + this.triggerExitAnimation(); } else { - this.currentLogo = img; - this.updateParticles(); + this.loadNewImage(img); } } - updateParticles() { - if (!this.currentLogo) { - return; - } - if (this.ParticleArr.length === 0) { - this.ParticleArr = this.currentLogo.particleData.map( - (item) => new Particle(item.totalX, item.totalY, animateTime, item.color) - ); - } else { - this.startTransition(this.currentLogo); - } + triggerExitAnimation() { + this.isExiting = true; + this.transitionProgress = 0; + + this.ParticleArr.forEach(particle => { + const angle = Math.random() * Math.PI * 2; + const speed = Math.random() * 2 + 2; + particle.exitVx = Math.cos(angle) * speed; + particle.exitVy = Math.sin(angle) * speed; + }); + + setTimeout(() => { + if (this.nextLogo) { + this.loadNewImage(this.nextLogo); + this.nextLogo = null; + } + }, this.exitAnimationDuration + this.newImageDelay); } - startTransition(newLogo: LogoImg) { - this.isTransitioning = true; - this.transitionProgress = 0; - this.targetParticles = newLogo.particleData.map( + loadNewImage(img: LogoImg) { + this.currentLogo = img; + this.ParticleArr = img.particleData.map( (item) => new Particle(item.totalX, item.totalY, animateTime, item.color) ); - this.currentLogo = newLogo; + this.isExiting = false; } drawCanvas() { @@ -390,11 +409,28 @@ class ParticleCanvas { } } + if (this.isExiting) { + this.transitionProgress += deltaTime / this.exitAnimationDuration; + if (this.transitionProgress >= 1) { + this.ParticleArr = []; + } else { + this.ParticleArr.forEach(particle => { + particle.x += particle.exitVx!; + particle.y += particle.exitVy!; + particle.opacity = Math.max(0, 1 - this.transitionProgress); + }); + } + } else { + // 正常更新粒子 + this.ParticleArr.forEach(particle => { + particle.update(deltaTime, relativeMouseX, relativeMouseY); + }); + } + this.ctx.save(); this.ctx.translate(particleAreaX, particleAreaY); this.ParticleArr.forEach(particle => { - particle.update(deltaTime, relativeMouseX, relativeMouseY); particle.draw(this.ctx, this.isGrayscale); }); @@ -412,7 +448,10 @@ class ParticleCanvas { this.ctx.restore(); } else if (this.currentLogo && this.currentLogo.particleData.length > 0) { - this.updateParticles(); + // 如果当前没有粒子但有新的 logo 数据,则创建新的粒子 + this.ParticleArr = this.currentLogo.particleData.map( + (item) => new Particle(item.totalX, item.totalY, animateTime, item.color) + ); } this.animationFrameId = window.requestAnimationFrame(() => this.drawCanvas()); @@ -441,6 +480,14 @@ class ParticleCanvas { particle.r = BaseParticleRadius * scale; }); } + + setBrightnessThreshold(threshold: number) { + brightnessThreshold = Math.max(0, Math.min(255, threshold)); + } + + setAlphaThreshold(threshold: number) { + alphaThreshold = Math.max(0, Math.min(255, threshold)); + } } interface ParticleSystemProps { @@ -451,6 +498,9 @@ interface ParticleSystemProps { particleAreaX?: number; particleAreaY?: number; scale?: number; + brightnessThreshold?: number; + alphaThreshold?: number; + debug?: boolean; } const ParticleFactory: React.FC = ({ @@ -461,12 +511,17 @@ const ParticleFactory: React.FC = ({ particleAreaX, particleAreaY, scale: initialScale, + brightnessThreshold: initialBrightnessThreshold, + alphaThreshold: initialAlphaThreshold, + debug = true, }) => { const [activeLogo, setActiveLogo] = useState(null); const [logoImgs, setLogoImgs] = useState([]); const [particleCanvas, setParticleCanvas] = useState(null); const canvasRef = useRef(null); + const particleCanvasRef = useRef(null); + // 初始化效果 useEffect(() => { const newLogoImgs = logos.map(item => new LogoImg(item.url, item.label)); setLogoImgs(newLogoImgs); @@ -489,16 +544,42 @@ const ParticleFactory: React.FC = ({ initialScale ); setParticleCanvas(newParticleCanvas); + particleCanvasRef.current = newParticleCanvas; + newParticleCanvas.debug = debug; + if (initialBrightnessThreshold !== undefined) { + newParticleCanvas.setBrightnessThreshold(initialBrightnessThreshold); + } + if (initialAlphaThreshold !== undefined) { + newParticleCanvas.setAlphaThreshold(initialAlphaThreshold); + } newParticleCanvas.drawCanvas(); + + // 将 ParticleCanvas 实例暴露到全局对象 + (window as any).particleCanvas = newParticleCanvas; + + // 添加控制台指令 + (window as any).changeLogoByLabel = (label: string) => { + const selectedLogo = newLogoImgs.find(logo => logo.name === label); + if (selectedLogo) { + newParticleCanvas.changeImg(selectedLogo); + console.log(`切换到标签: ${label}`); + } else { + console.log(`未找到标签: ${label}`); + } + }; } return () => { if (particleCanvas) { particleCanvas.stop(); } + // 清理全局对象 + delete (window as any).particleCanvas; + delete (window as any).changeLogoByLabel; }; - }, [width, height, isGrayscale, particleAreaX, particleAreaY, initialScale]); + }, [width, height, isGrayscale, particleAreaX, particleAreaY, initialScale, initialBrightnessThreshold, initialAlphaThreshold, debug]); + // 处理 activeLabel 变化 useEffect(() => { if (activeLabel && logoImgs.length > 0 && particleCanvas) { const selectedLogo = logoImgs.find(logo => logo.name === activeLabel); @@ -508,6 +589,7 @@ const ParticleFactory: React.FC = ({ } }, [activeLabel, logoImgs, particleCanvas]); + // 处理 Logo 点击 const handleLogoClick = (logoItem: LogoImg) => { setActiveLogo(logoItem); if (particleCanvas) { @@ -515,9 +597,10 @@ const ParticleFactory: React.FC = ({ } }; + // 设置 Logo useEffect(() => { if (logoImgs.length > 0 && particleCanvas) { - const defaultLogo = logoImgs.find(logo => logo.name === "test"); + const defaultLogo = logoImgs.find(logo => logo.name === activeLabel); if (defaultLogo) { handleLogoClick(defaultLogo); } else { @@ -526,12 +609,14 @@ const ParticleFactory: React.FC = ({ } }, [logoImgs, particleCanvas]); + // 处理灰度模式变化 useEffect(() => { if (particleCanvas) { particleCanvas.setGrayscale(isGrayscale); } }, [isGrayscale, particleCanvas]); + // 切换调试模式 const toggleDebug = () => { if (particleCanvas) { particleCanvas.toggleDebug(); From 3cb77975075cfc74215e332168d6ea088b601e34 Mon Sep 17 00:00:00 2001 From: ChisatoNishikigi73 Date: Mon, 30 Sep 2024 02:20:38 +0800 Subject: [PATCH 5/6] feat: Add particle effects to the world page --- public/images/rhodes_island.png | Bin 0 -> 15294 bytes src/_types/ArknightsConfig.ts | 10 ++- src/components/ParticleFactory.tsx | 2 +- src/pages/_views/03-World.tsx | 118 ++++++++++++++++++----------- 4 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 public/images/rhodes_island.png diff --git a/public/images/rhodes_island.png b/public/images/rhodes_island.png new file mode 100644 index 0000000000000000000000000000000000000000..71fdf9cca41328dfe4d8ec07922d571eeadf5404 GIT binary patch literal 15294 zcmX9lby!pX*TjI)1EjmVq$NiP8zCLi4bt7+A|Y&a3#c?mcOxL6L+KbIB^}c7?)!WH zxVwAqbDmT8)aRUYleD!|3Gry~(9qBbA!^FHXlUpdsQ)WKY}9W)L46nMhU=+j;)8~U zPxjx1j+R$Ig@z{W2~h^?2NoO_I{PfEWwj8usJCvlRCQHl@7-KHJ~4`ti2Qi)H_A5O z(#*0mW!X{soh#D~jbs|ZVmntB;(Cvk$bK%cBOsNh!BMrK{*J9s?S=c}6USax`3t5? z5Kg{a)Y}y6<~_1cR79!bVHo5tV_z-EAXgKH{owN}|#NU3uqGV4kIl~TlJ3(?F$T49S6=B$z@G06#K2n;g zg)G_=(^dM-Vx0|+4%R@pRry5?F^ogH08NnNp?z`t)6IbeqOgi-AxIzGz?cYNW@GlO zm2Wz%Mq42J?}@^4bFx@7f61_maWDoBv9u5WH9)o zXq}+LAH6!FhOj;#WNANM*<@ab_<0b~_KmEDiU>u*oC%l;dw2{2pvN-*N@^4+wMo#% z8aT3kwpMXDo;;fEUF8MXP?2{a$Ci%g0s50^7|&30k&~d*=F!{rV<_F`zR!XAhzo^D zHZWHh7mOH63gOz*Mc}i8Pr7t{NY|&ULF3{81JD8JKbU?(HG6fm^{3qOV)h3J#OmH0 zVZ=Tec^?H?ApY%TfoQZrq9nQkxnSAoepgFE3$0@VKI)CXxvsM#51k(ZDt-3t_xFSUMg)k0^%Ri}&v!s`&P5g>8nkY;m{s*EvSCEn~ zRY&T(lAyro+r_^B@&SRp`1dF^T>?1qHsu~uzvphn>$~FqhefDkPvUz;X&B}Ot*ZI&d)uRnyIR3(x7 zE4gXx8xmK}9M9oPbq*=!k&L_V6t8{Pm^cXGmr>=hf>wQh8C(|&d0P%IRjDv4m9(rDZSA2eR2i;S?4 zX7oU>VxkhrSqZDUBZAeSwE=_t0x}oQIpE;`z=U}Nm?}v_Fi7SUETbFnf#@%oZai+T zDgYHZyZU4)*l$s6;n(xI&XyZv!~!d}^4A;R{J!!1LCJX)$pJ%HEdV#q z?0z3C7D(GWs6rdwwd@(-=x31#a{LuJ=1UZWqO(eA@8@CHv~BZB`6KdVcR~eS=pkHreZ+6k{ z3X;NXGV`~o;sG@Xcf_EXdS{`x^LZFq8l07u-DsGf{yKW8+fD%$2dF#`k5Uw%nbgSx z=hzD$?TQ3x8Ni-ojMHUqQRJ;#G&|frb^jxcZ3g>s@D+FVh3Xic=$(0@o1567U7U*^faCwxzS+kicX&Pm)e@xIx11-lrzw(_&B z)@%R`WgwnN7tqULi)dX6sv?WAf~LH*T3e+t-YGxxWEn2KQkOB@ z7Far?F01bMs`gw!Lpdm&&GYY)-Rkx>{28wKe1Dkjc1x9OD8!D}FB*sjmh*zF4QjPk z{tKgYD{TVyn9p;&lboJmyn7ZB;X-VPyWj+%3Gt26G`Sd(P)&?-R2Hx)^7K>)BVUq{F1*pW5qG zbc=fCql`qoQ^|Ri$pO^~JD65HGtv!d@G8P$(fZ77Nu#N&w!i+=T5LTT7az)#JyD5% z1umE&ryjf>n&u!Fg=L@x|A5K&*1P)VZ3#$GIH?LRN(_~3{zVFv{M3xT$*^~Xk{xJt z99)|@vlOd!zP~WcRoX=ie$23d-kKon&rO~I__E%D`hWNHI7uJ*Km_xAHmNM#;~6AnffsD z;DS<(qA3Yi)o<8$DGCXh05z_3{A9&9;lGJ{&7-Lp38Oz!P zdm3%6zDFoOm>?|Sao{9{yAn2naJ{^S^%v6{+7MfLz2CT92xd5UZ`?%dRNZhBub>!&n}mVM421ta*QSM5X^B zUTD*|yg#wmx341^C=R0MmDGaJVTq7)sVt>z6=de>o#21cqq3keJrwT$MdA01^UV4# zE1>*F!=;`$$In$_x$89yoE1F#OK#^mcQ=n=dYGXC~*!G(c z)8`sVcTSt6(36SbujKjz4Q!}HYZj>0SMRVQ=0rlJu@IyYSlr;2!_EaA^P4X@?B=w- zGm+GPa#G$4FhgGO+g0r*@c^^%uX4e~Z@{5Wmd)kFq29{Y{mRlh76a|Xx32+V=4A3l zO&mKLz`y{?ST2o`3{({G-stX^DeY+w{vor3n3wPrk{W+mFt)1D?^~GgYE(`Du>22~ zvGu`%&#TUu6r~;kuFkzTV2UVm^!IZCgD;u2t8gTn=DY2z8TpT2cO0MYY$(w6etmOj z&|rs>fUbS9N6oR&mYpSqbW<+#t?K}O#k-y)?}vfU-j(b2=V9yVY;*l4U)E{2K8+>m z@|>r57Y^iZ=Pv-GgW{s_=XO&GdQ2vfsGPS+@@1WZxseGm{nIi;WIRGQ??M8a>09G0 zv8lmr&{4ba>CnuA3%3Oy65-w}t?Lf=XWR2TR}7rW%?pE$7iYF^sjhOm*U z`X9(C{@}-zObE*MxiWMxGu3<^3_#(atJ855yE*VFs1vfRF|jI(aI?^0%gEUUD$ldr zKl<2LE9FrzSc#4l{z1x|fTK*&OA{ET0&BDbWU= zdu-eI#!;wj;bfs4lS0Y)L^i&|zo~&gl*;F>Q0j_6k;AY?$tpTqHzD`i8o`zpH`|SY z3Fv$60%u78$zNU&C8Xp{j49;Tb7PPJ=v#tXmTWweZ#xs0<|kv|upFYf<5(_KX0$tx z(uQOn6HJEnt2+~f&xTj0vQejAqw^pD=ztXyUulu$LTA} z3;PY$c_I91)r-TP@7R%BRGFEVYuz)2EzeVu?w=oUT8n5gLB)|P(mcs``HFpaPTG;(FZu-7)q`(i&BU7Ra zRl;_C5g~{Pj52V!Qi+tGSw{?g-(wkjhpRamDNp-Y|DOjt3_&{W-_UBfTIO4sa}`E7 z+l=iFHt}KTzN0+U)K;u%k!+r+juXN}X#xB7`%By8nu}$Bv+Jm!J1iM&IqNciLCsq# zj(5vbC?4AQO~u>mSzGEq>06_O5tl*VUbC2#bk{(bP?j+vLs_!i2-Tzj`_){6v}=QI zKM*H9sPuYx67}lE@pDZLk5}TajMnXKsAIe`|)y05k6Sv+J z^^z^NHEqU*`T@TSDMr&`)`9tpB`5yl=gyAKx4nVAfQ8RltUw#g%xj56GL|gKF}$I6 z%jK|~fU*sSAj{~W<*X5m2E-!+$G>NP-Y|=z^g*NJqwvCyFh^x^*Wi8J^;k1^^2W6B z2oUEtefsBEUtM9ms)g5~BvvFKS>xe^u@)u`D)V=`#0kBVKaUBkm~!G>l+vE5ZGq2o z#-;0-oOu8JJ3p&a5hcH=TWN=_`vo{6LL+7m0_uL!=NflsUk7hyB4{wrDHMayNox^1 zQR_+`pD<9Br;$u>mQ!36)2`z9PgmZK!sh!`tZ%>ZHLpnC^&LwQG>TDYk|9zab_Is_ zyDD*d+Gz8+1Yau(Dj%!wPU)ymsoRfvTMO}oGGJlZam$R$jK40*4SKV~U@rNXI`^^# z8+C@#h#7yW84_zsfKUPp6K5j{Rc7$lZJWN70FT?IcIm1w`A`zAO$8r!(lGr0_-oXAIIP~LFuaSFt}eGDcS3py=em`K6M@M(KH@M z-m>gjLNPWJ5o8s91gQ24Yl-`l@?!B*r880Fj3;$BX9=0z(T5LG#Bjc3&Z&K~EfuIO zaC&@nm%?}C;1~wzkAU1d`aGD;bVsA7b=YCRBx(n)nM|bt_v`r*uO5*M=H<$*@Gu$2W1%cLfPb}|}!Q?jag76HO#f1a{Ah_8m~6Ci)>wmhJN z$f_&)H#qLe_-NjElAL=RA}Rbk62;3UVVL+|Wgg(#$m zs|!F0B@0<`Jy%(b3tYQTiHiyfMbKfcp)(KNh=3dMpte!ChnXWrni!rG^HUw*$ThAV zVTGD-!{5LxtfL+uGIn>iZPSx)vd(V=<06~;q80KufL(l1pU{)WKin|T#-D{o%|J7E zKQzVrgCoa7K8bb&(Tgo3UDhRvWp$c>MOEEZjl~hpJ<)+hZVngiH>)#HnkU=ytClAF z$VnuI4K-H>W4mWj1j$truHlC)w=0Z00ie~>^4Bu8NKF`aw`eVQHdcU6@1j(=E(e;W zS}^}!O656DL1Uj@pVk1wydXPp#PhR02L`inpW3bY_{GyFjXyzE{@jFt)F->x^CLaG zQ1fk?#a6s%S;c~tBi>ta#E^F86`xrgH}&RaxSS(28d*`!2;aQc(C2V_2B2$W13_~$ z`lk*Na;M#Wytpy{)yU4ewTS!65iOa|p9#H8Ip1C3-Eja}7Jmi6S+F%~ z@?b*uPBwiU2shP<)5q4D9=UfxmWK34Xj7ehvk?rEL6XOE0@svD7;a*h5Rmd7F~m3+ z$v!(DHldlD#&+M}-FSEC)N6YeYVLPkEP4#e}Cw@bZGSEaw?8FwlMM2Wib z4CV#6?Js+HYYV+E<|0ez#dz=W1@|3HgjEpUk!;{cpBLvfx$d?c-04T_*fhJq2^LEG ztg1G3x5{thFMtnvt>N1C0M5$O_ortCA>~0{*Z2n`?Y9clqVX8{tgs;Jg7M+O+hG@(7#bPP?b3=wBSC zf)0i(V1<`mQfDG%AL!9#S(=2B1xBNiwBp{DOH@X)GSSFLUot{|;=TLBCh9E?#2h`|S?) zUb&GPI&{vWo}0tDbF5{_c9!nW@6y{@3`B&oJ$>?FR|y!y6z!I9rrMOU6D(WjX+!SKE>jIAd~hHnoF05K%?mun*1eRE)ooA!LVO?V)&z_Kw@ zOn1a3vtBk8E+$=pN?=y&GA9mvZOiE{KECWG?2#OKmxFGNqNCVIY(U zJ8O27_`~1P)!>g!d>&PbPkWyeI45pl!z<&AUrp8q0S&KVd_jPhe-9}UT?O(#@lc2? z!ko2sFU^L2WqCE-{rzDcJv)}^*zWzUwqG0$6# z3YIFNJWSNT_NjTl8`XIp{yT2{Co!<)V#g)r*<40?H zV*S)IQ@gEUD{sX;(rQsDw3&X}MP>k!!EpaOo8V~(_3YlbnxOGXgeVhz3z(Tq3p-;0 z0iIwN^3cq8G$DZX!g7g;Iz(qJix~qVrHA)Ke4sW)GPNMl|H^mQRts^01?Hxk-3l0y zi#0}9;=t_?e~xDYX%{?~F+|k$apT75rac}{Na5u-V85L5uoSOfZS4nKF|Y zFr$**ihP|Q04(QozKbm^r~%UwNtpr7Y+t%9eiZMID}v$HHF{&}^Pov!4JH@^mH{(; zi6~*?tGWP6j1i{Hz;b6W$fMImAE>y4Rc@W{P_90kbx?j1md#WGqj9j9^3uwmnP%(J zJ)4q%t0L#y3H@-0ioXgwbNP?(97_CYyo#WzVoI!NGMD)E0pMGC2*ssm`6L)vCF&KH z(L8eB=*tACpraYaIN^$g<;4UJkYnbqV9W?}RrpA={PUNfe^jxK`H zPc3FC09Qolibd!cV4iu0_LH0)xgot_7rGZ^@A)+?jr8?@gdM(?w%$bP<0Q~^9QO90c85|s zKwk3ma+JV)No=F{*OTH5IUW=2a?U=(-&kC;{}hj7cLlh8H#ium9jLWhAW7Q}H44{qGd#L{KgbDStc2hgpovgfU%`|a&b>QFdCSxTM=o_!OiAY3xJ zbUs3I$qcjov+njAUxN>m(m(=DP~cr--F0<-3nJ2e7bAzu6yR3PV9OllY{3W@JynC8 zukqyXd%$ByOt&;f=AqHsNtGy1;eMGHPVwV~HL{rd6vT%241_<3DuXG#5@)?e-$qanJM7D8Jb)urg5~tQ&nOQ$55C0p<310K}Dw^lNu&1 z3IHRw9YsYp{hQp>y^h>S(aa<^WF?lz2^A(Ibn>nlVfg-EaZkF@JbvG{NUh;-f##Z^ zY3c$>mg&&2&wucGED=(*WDHVC!&dWD7b_T_Epf_QtY)M8IsMp7mtw&L=u6Og;c(h`^W0|+Wna;(WMj93O(61{8K=s}c6aPF-`v-%w?#(?w97JHI!j`(0u6)9i;2w}ek)H~|5?;Y>M8zOlWv8u5HmgC z1Tz7OxdR>d2(Nxd!}d~MutS-l|8u-!bf&c=TeiJU;3t?9UI&F`zR})|N~+6;g`Y?+ zdu-^c0GPlps5_W8$`n;~Qz=~ja7?G{0RW|gt_26D3~D0Z)6aJ#>RgAIBip`;D^t1Z zaeTtOf&noUv-!RuZj*#{1YVlV2~df%1EHE~%grHd&&;V@StZaoCJs6hHnj3)++iG( zMq92u#w}yAp68h%%GW1a<4x@bb<9k5<1U5K{Tmx?ZsdT-Mykx;OV=1_OdwQ2p}WF? z1^zagrve%+E8AsprIpDFFHr`RJrr}VX6uD;GJnl93I!Wsn?EaZv_xNpZd<7}5R zSUq4d~rK8a0Rctw{fgGsvgL_KjkGv9OXIyO)m+e1l#4Av{}TG3JF-PKiR*)p%1UYl~Pk;QPLJ#yh_xVaaYHE!d zS8s%{gB4Ut8J9OQX~0T}-^ECNCCcpYzHlY|2*p6^IwRJ3RH40GKD5tt8?}bCqf|D< zfDGi^+(J8~3ePBb$2zglD^L0K>P7bwG>CLx*Tc`dgj zcOB9lh5=5i(P-RK)%JR#*)r#zPE_2QN0iWHRQ?=4{#Qv?NB?rd;n)Le*l$`fkRmOF zk2CS_{uR9WFENy?Ah!I~)Ny*f=jUMvQ+b0L#b-)mtd|o>FCC#*8x>8h_jP9a|JG@G zyO28Qlz8U0Ij4$R<4e!hJF%kFyD~W57OPPBIjx|nC<(GZ@a=g_-6dxoTEqKzD?m?G zKdpBIwzejqOtyq&$NC-lKmI$_0|t^Z&?G%lgFev31%4QYn7wINhh=AdN-p_xlUyF* zoYTR^AP1}L$k#O$Sr@#FF~*-`2Hr<=bvkAG7X2X|id7;-vnms%wp73Uk|E}~*vQW( zH=@j#M>z>=*-(Tmbn9q6i;k5sLY!q!7>-3{lw4Qx)i{OzPX6f1TEvUnlQyCGI^Kag z&<;n4IQ16;E;z7VeTV!l?@$#MnD1*OmY_M+@`&;WW>s7wxaPUMXhe2oaq=7|51NwP z{!$V27J`W2(gYmiP^rIz81*)9j$+d+zr(BuImz^{9r=#x1@IGidOT3WG$LXnd1qSe z=;~~(0x{p~ej!Qw(rLmL!bl^bP23&wEnm{*+)eC7?~noNK8axb)77{W~|+IZMaz7DN-p;y*%jvhNshnri`R303XR zz?pVhmQy{9)8i-YCLkBh8(zTgPItIuQ~(siI!mnI1OAublDeE@C_hv?b(lbSro?Vd z!~&WrGxw{;jJggAQl9h4Dt?t3<#8Oetb}e~=>b!UGp~G+qz5ZU1YYzYN)9nF5R^jn zWq!@LLajyIvY1V55eC0)<3$}JeJo^DjlwIGS>4KOMRxh7 znwiN;{f*wSNL9c5zXcMgvG`ltkEP;tJOR|vXf-*bQKa0rjum&owa>)qX$mCEmwny= zhj*hsvTe#`H-WOF4NL%PLfiq)&iItKn#gEo@&sklRG3;-G29H}_oZ8ZS!v=fRBE$+ z#*d!c1S5Ot06MBPs+NoLCKgM1(PJeYh7x|+UVh8SLmR)}P_rEaVDzW0WyLr|^Oiy? zdU6O61gI^4@^bvygAPOwMdl3qYwlBdf!xZD^=}-#@D+C7OM0A%tfds)j5$t1r`1U8 zAB4NMfA2zH=lXT?Bu7TAjrfm}ntv4)tf$A*f;C}TN*wRx)s~J;bVpt>42vGH?#kph zbYibc8f@e5p2(C71InTgL>i5XOu96{AnNH>QE2Nak0jd~hfxG|9A$AKZyZj9yG1>$ z#U~$D#N}Lm1(n;s+m2|DBD-6@@*{hXr?Qm1VFPbXng8+cscvw2aA@RSeF&;2XoBHR ze-}WsgI{2(ZM_`VfI{CAQVf4<6LiY48kJO8y@BMl;9)<%hm4CwGLg1?}4zKtyAC!&|>(iYcy)V~T2-mB-D?xLW z*>N#xym$H=;{Bd?yfv(gG-G?z>3Pt-a9)5Q6;Ccd`{C9gvQc85lep!wdH02``@by! z-4o+tGVa*F3lsLQ58B?yQ@y_ZQl|8Y=>B8-&uIhNdUXkSK&KiskIM;ZP1{8xFv&c7 zKj8(1M;dkgyEvr~DIJR!;o}b$ct$72U~Pn}8z+TWC7Go8-9IcEWNwp5ivQJU;hNa4 zeh^|Fw^d%Didfz&cD}3{7H+|Gp(DQ5+`@ao@DvWSdL8eA%Bl=2Xd0Wkdyl{zC2cpPz_4@e=-ApJ>)M-Wb>XedSP~1&H$YwuY>o;a(OtPL zM=&T)y3L&~PxoCcyZ34hI-VEI%B(ZS8r{o2zMK{Lv5M!(YYk7a2pSD9s^e2%Rww2^ zQ9Z>Mmu!Hmb+LOSuqgmXTm!1^%8odU5kf$^mk%f{cTblPC8-T#^>I z5a@Qra#`83yYF2sZLcZrw0>erR3URy&rBPUWV>Gisi$fczibVpv7Bkdu#kv~*% zne-wpt_w%E$f%dYPR4P2mFs}4qs_ut8Kk@(Xecf0_ySxU`ymMIF6ROaEKP%wjywz3%d_^zu>!??z(@TAjF^>NBfm+ylxB8IQcH`reN0X(m zQ90dI$p|GcJ-DDss*gC&{YfReJb7Ej*d-EnqI14Vw&nQ|HqUxpse#bSTW{CBEB6K) z)9^C19`e#TJ19{v}Lyjnad&FrtX6T|FFo6ig}(_ z9FCn%Q5P_r8E3I0ca$<`IHw%t0zof7LSxc3zMv|%fgLE>pTg&RdzhX-=}>zy4~E-dgt9udz(|ivu@WURO)RYjHy?EDePCS3+l@HnQ*fFEftxHDjSR~3S zvO3Xn+GlBYzJo1?6!Q`jU@u2yybN3oee|g4jlzM(Y@~Q&zs?U*>Fb_!=t`Tw`1%2) z%b;PZr9_|Kuh2)-)(<_espF|F2@vofMVR`h>Qtr=wq)+ilZ_S8`k~ zcHbNjNi+G!Po{^_$Ofq+{H-Ma`W2^bGiR9+HoHEh);;2>76kNRoxiVzOIz;V{T)+( zy`Ku%+yu$RKc)1_X3xv+dwWcI;@M|}(XsMIGyr*?yO2JKI}5IQmB{#{okWf@RkYec z5;j2?1A_x+a$Zv*jB_KdX`2&k5oh&t6Yc5T^oA}(r0=J1>ae4I=D&6+aWd@c&HmJf z%Ec^P2!dcr;X|cAx>8_P$Suy6(4pYOK82iK?~|QdulwbL!6B8{SlA-nX75cz%|U$W zXA*&WSkacFeR7?z8BNc3saq|R_F|rT*_-i8TSDB}S>COXTZ>6eqg2Cz(#jUuF9i`9Fg4>rw`(oNaTno!>4?_n5l$&FnEl*#Pvko7{TT}< zBix7Yy6#yJHMgn1d*^RNfyCI0c~P8=BngY5AHPhaq9Q5_%EIR8&I8yt5(N+cQM`h6 zLe_ce8E^RMvR$lJSVKRb4bz|R@3b-DHS&4F)y#_$q$BkPG|wA<=h62@_fhBTK;ULb zare^1AS`r%rV$Mr@#w4FX^W$IWQc|M`a|UiR+N5f#fJxogo+hid{_>j`3T*kEX}iI zWJOaFA(-NLs<4SdO8-syss7@MvdEq6+q*uO*1MRqGM%0I%j@T9>%R+?k`4Sl92<}tGoIBokS9pYCps#6{hYp6xIfRx*WjC>GeI45he z=`^CCB zExs*fL6m-v8kDHY(o~EU}=2Ir4Hgqq`c0&j!g4yn-+1)gRyJ$86s{;Jy!Lp4-3 zXkTr7Fe>Ew(_YfdW5OFoi2@PDyZq;~X>(aks9hUpJRzIRz3G)-`q0A=>!ZpD>OW^gf=WNdR9Efd2{)35gq@*XyTDQ&X9nP5^&wW4%gTrhAJW*+1;S?dU(y-ovB`VULVF-^+qe|88aV^l(gu6{-QMuk1VFdKUS7*lv7#2`r0wGp|q=a zohtko3K>6=5KK(eJDJZZH!ufo2qmVH-iNTj6a0hotQqdkm(*(wz}^p|0FfX z%Z`3pbwi*V{9a2%tNbP9=VQr#j)Q^9I?Tgi8JI?($57VL)KTH38-iOP1~y2d^qBCV zQ_2$d?fd(T&swCDeem+zvTa(*w6vhwy;M1nokU(%f5Co__dVlmm}~Jm*GJ%oDdAPM z=YOl|J--y_7vA&}0H>k-njBf?+1`I&p{)^_A>YLe{r6!zz(TA( zf7|CM`L2V6bAn>SIJf^#kc}{%OQ&LbNa9Jm!SqKM=vw3&{0ybEj+KTuY-N9y_(=)=v}id z!`9MX<00e_#j%^^)qF+TS0sH`06CI44)R&S-+Jxu-g;LbwjqOO2z#=~_CwCRoS>ny z_N@0UUUzvhH-*gQ!re%N3JgB!kpju!oA>5h2-}5iZl4j1y2a6}<`d z$Uj?aS5g6HxB#P%V(a2gsvA910Phrk?d#sd1)K>@hLx0lKF%mMJo@Re4-`MHe5ZUq z?zxKyqN*S!+ytEcN=wsQUKz|tc?HzcLuOnrW$RdQq*J${1$nL~_0<@JPiqZ}$@O=D^gH~{|!BVhyUw}@>9xSw=m0E5*` zwVnL%m^x6VFD=gjvCefWXD085rSXFAQ^nKPY{fs>r~;Euq^!M>YTIEru01$(Ky4TULk2q_H=Y zQ^`6gIo~lYH5`FRbtzzLV>3~?<7_J014T-6r)IqrsR2}%X;&kV1}Fay!d{FyLZ`Vw z?q5^9kI>CXC?+`BXA8P)B`f zo(?(z@8sS8yxC#`sN0Y|pnQdW_@J@(^75YtWn8{a4z#Fe+Yq4psI@mBg4@UaWj1?) zB0AH*jv6}nm60ivgkMu8tVY8M9bsT8y4+(l7f80%KszXkS0300u^nYe%A$_OFFTBT zuH#qZ+VU=MQ-S5>`Oo9p9ZBuw(rrn`{LOWq9ra-q7b4e)K0#J{-gi;vESSuA>NG2y z&v%9%KTbGL8Uyg=W10A)5=lwk2|~jkjJ77!S;Pg>vj{2KfWCXyqt?yMwnX2bb>1(E z*Ag~h3uP~TyCLl~BaBE}7f$?Y7W2%GWy29dEIs`W-h9yqZ{ZepY`$$di$v9*{@+}- z%9zCZ9!hVXK6}&f!_x`Q8kNWKZQ;Flt=9&S;9ozi+Rqqfthj~y&sZePTfKu zt=xeAm$15&l$b0}gK(_(y!!XEy0^JZ-DZ(yaf`*w`^x|)>9L9^7F$W|G5d#sD> zA2BpD?9-5@XRI4E107FG2Uj6J^yT@i&7jH|xKgmPfBC_DgnsLAIo;)uW|@C{7}VJA zKvk%bxmSK#(&1y*5o&l5&ghRIJ~}6-2BFKX|z18$rp)^(b60 z5cL-dzy~I~lc*n7#qe>+4nq!1RQ9H9Z<{5j1J!|cP}WOKwtb;j63t}Qpcq`n?1^6B z5gDh-Wkn9+-gs1=@~&s=mYjy4{TU)v5Y~2y)c(-Aky-v!Z>A925I6Ny`7Ij;7)bHf z>dn6_SIt&LeJf+uC$?;{;P*TUXv!M-!s0Z{h~=? zIUg`GJocv+2t&h*%HvzJ3Fjr1q-x~0r${O!^#(Gi8H(!DoJIa_ck5`9oq1~5mg%lV1U-qZ|0KkTRFTl7f1D%L^ljFPbLUTL zaAKUd>oPNwEY@7ySmPmQm)50H;ILN$`S9=+hU9b~@4Ez45pQcc7*yoLqL~@Qx^+r{ zp9`8c&SJscTywcT2z|Ofm=mL1 zdRF8yi620-6~GC{LSL$8gY)w6v{FtJTmM*t%yPWTWE}x1si=$?#sr0bv|{gK{Oe#w zjzLb|g|mZIX;%Nk?)hnsV+(+X=MqydH2!0XZZo3oXM0(hO$fSRAeSsbw!Xgp0gg<@ iY?ps8>S@AhU(J{7AoEh&P}B#xXb=@GG! = ({ scale: initialScale, brightnessThreshold: initialBrightnessThreshold, alphaThreshold: initialAlphaThreshold, - debug = true, + debug = false, }) => { const [activeLogo, setActiveLogo] = useState(null); const [logoImgs, setLogoImgs] = useState([]); diff --git a/src/pages/_views/03-World.tsx b/src/pages/_views/03-World.tsx index fe66b71..ececb1c 100644 --- a/src/pages/_views/03-World.tsx +++ b/src/pages/_views/03-World.tsx @@ -5,6 +5,7 @@ import { directions } from "../../components/store/lineDecoratorStore.ts"; import { IconArrow } from "../../components/SvgIcons.tsx"; import PortraitBottomGradientMask from "../../components/PortraitBottomGradientMask"; import config from "../../../arknights.config.tsx"; +import ParticleFactory from '../../components/ParticleFactory.tsx'; const items = config.rootPage.WORLD.items; @@ -156,7 +157,7 @@ function List({ onItemSelect }: { onItemSelect: (index: number) => void }) { const handleMouseLeave = () => { setActiveImage(null); isFirstMove.current = true; - }; + }; useEffect(() => { const animatePosition = () => { @@ -214,30 +215,32 @@ function List({ onItemSelect }: { onItemSelect: (index: number) => void }) { }, []); return ( -
- {memoizedItems} - {activeImage && ( - Active item - )} +
+
+ {memoizedItems} + {activeImage && ( + Active item + )} +
); } @@ -405,6 +408,23 @@ export default function World() { const world = useRef(null) const [selectedItemIndex, setSelectedItemIndex] = useState(null); const [active, setActive] = useState(false); + const [windowSize, setWindowSize] = useState({ width: 0, height: 0 }); + const [isWorldReady, setIsWorldReady] = useState(false); + + useEffect(() => { + const handleResize = () => { + setWindowSize({ width: window.innerWidth, height: window.innerHeight }); + }; + + // 初始设置 + handleResize(); + + // 添加事件监听器 + window.addEventListener('resize', handleResize); + + // 清理函数 + return () => window.removeEventListener('resize', handleResize); + }, []); useEffect(() => { const isActive = $viewIndex === 3 && $readyToTouch; @@ -414,6 +434,19 @@ export default function World() { setActive(isActive); }, [$viewIndex, $readyToTouch]) + useEffect(() => { + if (active && windowSize.width > 0 && windowSize.height > 0) { + // 给一个小延迟,确保其他元素都已经渲染完成 + const timer = setTimeout(() => { + setIsWorldReady(true); + }, 100); + + return () => clearTimeout(timer); + } else { + setIsWorldReady(false); // 确保在非活动状态下停止粒子系统 + } + }, [active, windowSize]); + const handleItemSelect = useCallback((index: number) => { setSelectedItemIndex(index); }, []); @@ -453,27 +486,22 @@ export default function World() { onNext={handleNext} /> )} -
- WORLD -
- ) From 96611f674066be7ad200896326c38613f2f114fd Mon Sep 17 00:00:00 2001 From: ChisatoNishikigi73 Date: Mon, 30 Sep 2024 02:32:45 +0800 Subject: [PATCH 6/6] fix: fix particles name --- src/components/ParticleFactory.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ParticleFactory.tsx b/src/components/ParticleFactory.tsx index 425b6a0..f5603d7 100644 --- a/src/components/ParticleFactory.tsx +++ b/src/components/ParticleFactory.tsx @@ -27,11 +27,11 @@ let scale = 4; // Logo 数据 const logos = [ - { label: "kazimierz", url: "/images/03-world/infected.png" }, - { label: "rhine", url: "/images/03-world/nomadic_city.png" }, - { label: "rhodes", url: "/images/03-world/originium_arts.png" }, - { label: "victoria", url: "/images/03-world/originiums.png" }, - { label: "yan", url: "/images/03-world/reunion.png" }, + { label: "infected", url: "/images/03-world/infected.png" }, + { label: "nomadic_city", url: "/images/03-world/nomadic_city.png" }, + { label: "originium_arts", url: "/images/03-world/originium_arts.png" }, + { label: "originiums", url: "/images/03-world/originiums.png" }, + { label: "reunion", url: "/images/03-world/reunion.png" }, { label: "island", url: "/images/rhodes_island.png" }, ];