All files / app/detail/components AdditiveBubble.tsx

100% Statements 19/19
100% Branches 0/0
100% Functions 4/4
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                          1x     2x 2x 2x     2x     2x     2x 2x 2x 2x   2x   2x                   2x                 2x                   2x               2x           1x                                   1x                                        
import { useEffect } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withRepeat,
  withSequence,
  withSpring,
} from 'react-native-reanimated';
import { Text } from 'tamagui';
 
import type { Additive } from '@/src/lib/supabase';
 
interface AdditiveBubbleProps {
  additive: Additive;
  index: number;
  total: number;
  onPress: (additive: Additive) => void;
}
 
// 柔和的橙黄色调色板
const BUBBLE_COLORS = ['#FFB347', '#FFA500', '#FF8C42', '#FFD700', '#FDB45C', '#FF9966', '#FFAA33'];
 
export function AdditiveBubble({ additive, index, total, onPress }: AdditiveBubbleProps) {
  const scale = useSharedValue(1);
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
 
  // 选择颜色
  const color = BUBBLE_COLORS[index % BUBBLE_COLORS.length];
 
  // 计算气泡大小
  const size = 60 + Math.random() * 40;
 
  // 计算气泡位置(圆形排列)
  const angle = (index / total) * Math.PI * 2;
  const radius = 80 + Math.random() * 30;
  const x = Math.cos(angle) * radius;
  const y = Math.sin(angle) * radius;
 
  useEffect(() => {
    // 轻微的浮动动画
    scale.value = withRepeat(
      withSequence(
        withSpring(1.05, { damping: 2 }),
        withSpring(0.95, { damping: 2 }),
        withSpring(1, { damping: 2 })
      ),
      -1,
      true
    );
 
    translateX.value = withRepeat(
      withSequence(
        withSpring(Math.random() * 10 - 5, { damping: 5 }),
        withSpring(0, { damping: 5 })
      ),
      -1,
      true
    );
 
    translateY.value = withRepeat(
      withSequence(
        withSpring(Math.random() * 10 - 5, { damping: 5 }),
        withSpring(0, { damping: 5 })
      ),
      -1,
      true
    );
  }, []);
 
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: x + translateX.value },
      { translateY: y + translateY.value },
      { scale: scale.value },
    ],
  }));
 
  return (
    <Animated.View
      style={[styles.bubble, animatedStyle, { backgroundColor: color, width: size, height: size }]}
    >
      <TouchableOpacity
        style={styles.bubbleContent}
        onPress={() => onPress(additive)}
        activeOpacity={0.7}
      >
        <Text
          fontSize="$2"
          fontWeight="bold"
          color="white"
          textAlign="center"
          numberOfLines={2}
          style={styles.bubbleText}
        >
          {additive.name}
        </Text>
      </TouchableOpacity>
    </Animated.View>
  );
}
 
const styles = StyleSheet.create({
  bubble: {
    position: 'absolute',
    borderRadius: 100,
    justifyContent: 'center',
    alignItems: 'center',
  },
  bubbleContent: {
    width: '100%',
    height: '100%',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 8,
  },
  bubbleText: {
    textShadowColor: 'rgba(0, 0, 0, 0.3)',
    textShadowOffset: { width: 0, height: 1 },
    textShadowRadius: 2,
  },
});