2025-08-05 13:52:49 +08:00

290 lines
8.7 KiB
TypeScript
Raw Permalink 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.

import React, { useEffect, useRef, useState } from 'react';
import {
Animated,
Dimensions,
Easing,
StyleSheet,
View
} from 'react-native';
const { width, height } = Dimensions.get('window');
// 粒子类型定义
interface Particle {
id: number;
position: { x: number; y: number };
animation: Animated.CompositeAnimation;
color: string;
size: number;
translateX: Animated.Value;
translateY: Animated.Value;
opacity: Animated.Value;
scale: Animated.Value;
rotation: Animated.Value;
}
// 烟花组件属性
interface FireworksProps {
autoPlay?: boolean;
loop?: boolean;
interval?: number;
particleCount?: number;
colors?: string[];
}
export const Fireworks: React.FC<FireworksProps> = ({
autoPlay = true,
loop = true,
interval = 2000,
particleCount = 80,
colors = [
'#FF5252', '#FF4081', '#E040FB', '#7C4DFF',
'#536DFE', '#448AFF', '#40C4FF', '#18FFFF',
'#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41',
'#FFD740', '#FFAB40', '#FF6E40'
]
}) => {
const [particles, setParticles] = useState<Particle[]>([]);
const [isPlaying, setIsPlaying] = useState(autoPlay);
const particleId = useRef(0);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// 生成随机位置
const getRandomPosition = () => {
const x = 50 + Math.random() * (width - 100);
const y = 100 + Math.random() * (height / 2);
return { x, y };
};
// 创建烟花粒子
const createParticles = (position?: { x: number; y: number }) => {
const pos = position || getRandomPosition();
const newParticles: Particle[] = [];
for (let i = 0; i < particleCount; i++) {
const id = particleId.current++;
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 3;
const size = 3 + Math.random() * 7;
// 动画值
const translateX = new Animated.Value(0);
const translateY = new Animated.Value(0);
const opacity = new Animated.Value(1);
const scale = new Animated.Value(0.1);
const rotation = new Animated.Value(Math.random() * 360);
// 粒子动画
const moveAnimation = Animated.parallel([
// X轴移动
Animated.timing(translateX, {
toValue: Math.cos(angle) * speed * 100,
duration: 1500 + Math.random() * 1000,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
// Y轴移动添加重力效果
Animated.timing(translateY, {
toValue: Math.sin(angle) * speed * 100 + 50, // 向下弯曲
duration: 1500 + Math.random() * 1000,
easing: Easing.quad,
useNativeDriver: true,
}),
// 淡出
Animated.timing(opacity, {
toValue: 0,
duration: 1500 + Math.random() * 500,
easing: Easing.ease,
useNativeDriver: true,
}),
// 缩放效果
Animated.sequence([
Animated.timing(scale, {
toValue: 1.8,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(scale, {
toValue: 1,
duration: 300,
useNativeDriver: true,
})
]),
// 旋转效果
Animated.timing(rotation, {
toValue: (rotation as Animated.Value & { _value: number })._value + 360,
duration: 2000,
easing: Easing.linear,
useNativeDriver: true,
})
]);
// 创建粒子对象
newParticles.push({
id,
position: pos,
animation: moveAnimation,
color: colors[Math.floor(Math.random() * colors.length)],
size,
translateX,
translateY,
opacity,
scale,
rotation
});
}
// 添加新粒子
setParticles(prev => [...prev, ...newParticles]);
// 启动动画并在结束后移除粒子
newParticles.forEach(particle => {
particle.animation.start(() => {
setParticles(prev => prev.filter(p => p.id !== particle.id));
});
});
};
// 开始烟花效果
const startFireworks = () => {
if (!isPlaying) return;
createParticles();
if (loop) {
timerRef.current = setTimeout(() => {
startFireworks();
}, interval);
}
};
// 停止烟花效果
const stopFireworks = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
// 切换播放状态
const togglePlay = () => {
setIsPlaying(prev => {
const newState = !prev;
if (newState) {
startFireworks();
} else {
stopFireworks();
}
return newState;
});
};
// 初始化
useEffect(() => {
if (autoPlay) {
startFireworks();
}
return () => {
stopFireworks();
};
}, [autoPlay, loop, interval]);
return (
<View style={styles.container}>
{/* 渲染所有粒子 */}
{particles.map((particle) => (
<Animated.View
key={particle.id}
style={[
styles.particle,
{
left: particle.position.x,
top: particle.position.y,
backgroundColor: particle.color,
width: particle.size,
height: particle.size,
borderRadius: particle.size / 2,
opacity: particle.opacity,
transform: [
{ translateX: particle.translateX },
{ translateY: particle.translateY },
{ scale: particle.scale },
{
rotate: particle.rotation.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg']
})
}
]
}
]}
/>
))}
{/* 控制面板 */}
{/* <View style={styles.controlPanel}>
<Text style={styles.title}>烟花特效</Text>
<TouchableOpacity
style={[styles.button, isPlaying ? styles.pauseButton : styles.playButton]}
onPress={togglePlay}
>
<Text style={styles.buttonText}>
{isPlaying ? '暂停动画' : '播放动画'}
</Text>
</TouchableOpacity>
</View> */}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFB645',
justifyContent: 'flex-end',
alignItems: 'center',
zIndex: 9999,
},
particle: {
position: 'absolute',
},
controlPanel: {
backgroundColor: 'rgba(0, 0, 0, 0.6)',
padding: 20,
borderRadius: 20,
marginBottom: 40,
alignItems: 'center',
width: '90%',
borderWidth: 1,
borderColor: '#7C4DFF',
},
title: {
color: '#FFFFFF',
fontSize: 28,
fontWeight: 'bold',
marginBottom: 20,
textShadowColor: 'rgba(255, 255, 255, 0.75)',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 10,
},
button: {
paddingVertical: 12,
paddingHorizontal: 30,
borderRadius: 30,
marginVertical: 8,
width: '100%',
alignItems: 'center',
},
playButton: {
backgroundColor: '#4CAF50',
},
pauseButton: {
backgroundColor: '#FF5252',
},
buttonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
});