import React from 'react'; import { Svg, G, Circle, } from 'react-native-svg'; import { View, Animated, Easing } from 'react-native'; import { clamp } from 'common/utils/mathx'; import type { ProgressData } from 'src/helpers/progressData'; const AnimatedContainer = Animated.createAnimatedComponent(Circle); const getOffset = (d: number, progress: number) => Math.round(d * Math.PI * clamp(1 - progress, 0, 1)); export type ConcentricCirclesStyles = { diameter: number; strokeBgColor: string; }; export type ConcentricCirclesProps = { styles: ConcentricCirclesStyles, model: { grade: number, progress: Readonly<ProgressData>[], }, }; const GradeColors = [ ['#FFE487', '#FF6887', '#FF37A5', '#FC68FF', '#B14EFF'], ['#FFB987', '#FF7E87', '#FF5387', '#6096FF', '#7660FF'], ['#FF9A87', '#FF6887', '#FA7171', '#5E4FFF', '#8C69FF'], ]; export default function ConcentricCircles(this: never, props: ConcentricCirclesProps) { const { styles, model } = props; const { grade, progress } = model; const { diameter, strokeBgColor } = styles; const diameterPercentage = v => { return (diameter / 100) * v; }; const strokeWidth = Math.round(diameterPercentage(5)); const strokeColor = GradeColors[grade] || GradeColors[GradeColors.length - 1]; const boxSize = diameter + strokeWidth * 2; return ( <View style={{ width: diameter, height: diameter }}> <Svg width="100%" height="100%" viewBox={`0 0 ${boxSize} ${boxSize}`} fill="none"> <G x={strokeWidth} y={strokeWidth}> {progress.map((p, i) => { const dec = (progress.length - 1) - i; const radiusDecremented = (diameter - diameterPercentage(21.3) * dec) / 2; const nextProgress = progress[i + 1]?.current; const step = p.progress < 1 || nextProgress === 0 ? (1 / p.max) : null; const sColor = p.progress !== 0 ? strokeColor[i] : strokeBgColor; const sBgColor = p.progress === 1 ? strokeColor[i] : strokeBgColor; return ( <ConcentricCircle key={i} strokeColor={sColor} strokeBgColor={sBgColor} strokeWidth={strokeWidth} radiusDecremented={radiusDecremented} radius={diameter / 2} progress={p.progress} animationStep={step} /> ); })} </G> </Svg> </View> ); } type ConcentricCircleProps = { strokeBgColor: string; strokeColor: string; strokeWidth: number; radiusDecremented: number; radius: number; progress: number; animationStep: number; }; export function ConcentricCircle(props: ConcentricCircleProps) { const { strokeBgColor, strokeColor, strokeWidth, radiusDecremented, radius, progress, animationStep } = props; const diameter = radiusDecremented * 2; const length = Math.round(diameter * Math.PI); const offset = React.useMemo(() => { if (!animationStep) { return null; } const startProgress = progress - animationStep; return new Animated.Value(getOffset(diameter, startProgress)); }, [animationStep, diameter, progress]); React.useEffect(() => { if (offset != null) { Animated.timing(offset, { toValue: getOffset(diameter, progress), duration: 1500, easing: Easing.inOut(Easing.ease), }).start(); } }, [progress]); return ( <> <Circle cx={radius} cy={radius} r={radiusDecremented} strokeWidth={strokeWidth} stroke={strokeBgColor} fill="none" /> <AnimatedContainer fill="none" cx={radius} cy={radius} r={radiusDecremented} strokeWidth={strokeWidth} stroke={strokeColor} strokeDasharray={length} strokeDashoffset={offset} origin={`${radius}, ${radius}`} rotation={-90} /> </> ); }