UNPKG

heart-progress

Version:

A customizable animated heart progress component for React Native.

155 lines (144 loc) 4.02 kB
import React, { useRef, useEffect, useState } from "react"; import { View, Text, StyleSheet, Animated, Easing } from "react-native"; import Svg, { Path, Defs, ClipPath, G, LinearGradient, Stop, } from "react-native-svg"; const WAVE_WIDTH = 120; const WAVE_HEIGHT = 20; const WAVE_AMPLITUDE = 8; const WAVE_SPEED = 2000; // ms for one loop function getWavePath(phase, fillPercent) { const baseY = 102 - 86 * fillPercent; let path = `M0,${baseY}`; for (let x = 0; x <= WAVE_WIDTH; x += 1) { const y = baseY + Math.sin((x / WAVE_WIDTH) * 2 * Math.PI + phase) * WAVE_AMPLITUDE; path += ` L${x},${y}`; } path += ` L${WAVE_WIDTH},102 L0,102 Z`; return path; } const HeartProgress = ({ percentage = 50, size = 120, Username = "", gradientColors = ["#ff6b6b", "#e63946"], backgroundColor = "#eee", waveAmplitude = 8, waveSpeed = 2000, textColor = "#000", fontSize = 16, usernameFontSize = 18, showPercentage = true, showUsername = true, style = {}, }) => { const pct = Math.max(0, Math.min(percentage, 100)); const fillPercent = pct / 100; const [phase, setPhase] = useState(0); const phaseAnim = useRef(new Animated.Value(0)).current; useEffect(() => { const anim = Animated.loop( Animated.timing(phaseAnim, { toValue: 2 * Math.PI, duration: waveSpeed, easing: Easing.linear, useNativeDriver: false, }) ); anim.start(); const id = phaseAnim.addListener(({ value }) => setPhase(value)); return () => { anim.stop(); phaseAnim.removeListener(id); }; }, [phaseAnim, waveSpeed]); function getWavePath(phase, fillPercent) { const baseY = 102 - 86 * fillPercent; let path = `M0,${baseY}`; for (let x = 0; x <= WAVE_WIDTH; x += 1) { const y = baseY + Math.sin((x / WAVE_WIDTH) * 2 * Math.PI + phase) * waveAmplitude; path += ` L${x},${y}`; } path += ` L${WAVE_WIDTH},102 L0,102 Z`; return path; } return ( <View style={[styles.container, { width: size, height: size * 1.1 }, style]} > <Svg width={size} height={size * 1.1} viewBox="0 0 120 110"> <Defs> <ClipPath id="heartClip"> <Path d="M60 100 C 20 70, 0 40, 30 25 A 25 25 0 0 1 60 40 A 25 25 0 0 1 90 25 C 120 40, 100 70, 60 100 Z" /> </ClipPath> <LinearGradient id="heartGradient" x1="0" y1="0" x2="0" y2="1"> <Stop offset="0%" stopColor={gradientColors[0]} stopOpacity="1" /> <Stop offset="100%" stopColor={gradientColors[1]} stopOpacity="1" /> </LinearGradient> </Defs> {/* Heart background */} <Path d="M60 100 C 20 70, 0 40, 30 25 A 25 25 0 0 1 60 40 A 25 25 0 0 1 90 25 C 120 40, 100 70, 60 100 Z" fill={backgroundColor} /> {/* Animated wave fill */} <G clipPath="url(#heartClip)"> <Path d={getWavePath(phase, fillPercent)} fill="url(#heartGradient)" opacity={0.9} /> </G> </Svg> <View style={styles.textContainer}> {showUsername && ( <Text style={[ styles.username, { color: textColor, fontSize: usernameFontSize }, ]} > {Username} </Text> )} {showPercentage && ( <Text style={[styles.percentage, { color: textColor, fontSize }]}> {pct}% </Text> )} </View> </View> ); }; const styles = StyleSheet.create({ container: { position: "relative", alignItems: "center", justifyContent: "center", }, textContainer: { position: "absolute", top: "50%", transform: [{ translateY: -10 }], alignItems: "center", }, username: { fontSize: 18, fontWeight: "bold", color: "#333", }, percentage: { fontSize: 24, fontWeight: "bold", color: "#FF6B6B", }, }); export default HeartProgress;