290 lines
8.7 KiB
TypeScript
290 lines
8.7 KiB
TypeScript
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',
|
||
},
|
||
}); |