Created
February 18, 2025 07:57
-
-
Save TikiCat7/787d5ecd7f250f6ceb2c697308b1d67c to your computer and use it in GitHub Desktop.
Shiny pokemon cards using mix-blend-mode in RN 0.77
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { StyleSheet, Platform } from "react-native"; | |
| import Animated, { | |
| Easing, | |
| ReduceMotion, | |
| SharedValue, | |
| useAnimatedStyle, | |
| useSharedValue, | |
| withSpring, | |
| withTiming, | |
| } from "react-native-reanimated"; | |
| import { Gesture, GestureDetector } from "react-native-gesture-handler"; | |
| interface CardWithTiltEffectProps { | |
| blendMode: string; | |
| opacity: SharedValue<number>; | |
| isExplode: SharedValue<boolean>; | |
| pokemonName: string; | |
| } | |
| const getPokemonImage = (name: string) => { | |
| const images = { | |
| roxanne: require("@/assets/images/roxanne.png"), | |
| pikachu: require("@/assets/images/pikachu.png"), | |
| rayquaza: require("@/assets/images/rayquaza.png"), | |
| charizard: require("@/assets/images/charizard.png"), | |
| pikachuv: require("@/assets/images/pikachuv.png"), | |
| }; | |
| return images[name as keyof typeof images]; | |
| }; | |
| const getPokemonFoilImage = (name: string) => { | |
| const images = { | |
| roxanne: require("@/assets/images/roxanne_foil.png"), | |
| pikachu: require("@/assets/images/pikachu_foil.png"), | |
| rayquaza: require("@/assets/images/rayquaza_foil.png"), | |
| pikachuv: require("@/assets/images/pikachuv_foil.png"), | |
| }; | |
| return images[name as keyof typeof images]; | |
| }; | |
| const TIMING_CONFIG = { | |
| duration: 700, | |
| easing: Easing.inOut(Easing.quad), | |
| reduceMotion: ReduceMotion.System, | |
| }; | |
| export function CardWithTiltEffect({ | |
| blendMode, | |
| opacity, | |
| isExplode, | |
| pokemonName, | |
| }: CardWithTiltEffectProps) { | |
| const rotateX = useSharedValue(0); | |
| const rotateY = useSharedValue(0); | |
| const shinyX = useSharedValue(0); | |
| const shinyY = useSharedValue(0); | |
| const hoverGesture = Gesture.Hover() | |
| .enabled(Platform.OS === "web") | |
| .onBegin((e) => { | |
| rotateX.value = withSpring(0); | |
| rotateY.value = withSpring(0); | |
| }) | |
| .onUpdate((e) => { | |
| rotateY.value = Math.min(Math.max((e.x - 150) / 10, -25), 25); | |
| rotateX.value = Math.min(Math.max((e.y - 100) / 10, -25), 25); | |
| shinyX.value = withSpring((e.x / 300) * 100); | |
| shinyY.value = withSpring((e.y / 400) * 100); | |
| }) | |
| .onFinalize(() => { | |
| rotateX.value = withSpring(0); | |
| rotateY.value = withSpring(0); | |
| shinyX.value = withSpring(50); | |
| shinyY.value = withSpring(50); | |
| }); | |
| const panGesture = Gesture.Pan() | |
| .enabled(Platform.OS !== "web") | |
| .onBegin((_) => { | |
| rotateX.value = withSpring(0); | |
| rotateY.value = withSpring(0); | |
| }) | |
| .onUpdate((e) => { | |
| rotateY.value = Math.min(Math.max((e.x - 150) / 10, -25), 25); | |
| rotateX.value = Math.min(Math.max((e.y - 100) / 10, -25), 25); | |
| shinyX.value = withSpring((e.x / 300) * 100); | |
| shinyY.value = withSpring((e.y / 400) * 100); | |
| }) | |
| .onFinalize(() => { | |
| rotateX.value = withSpring(0); | |
| rotateY.value = withSpring(0); | |
| shinyX.value = withSpring(50); | |
| shinyY.value = withSpring(50); | |
| }); | |
| const tapGesture = Gesture.Tap() | |
| .onBegin((e) => { | |
| rotateY.value = Math.min(Math.max((e.y - 150) / 10, -25), 25); | |
| rotateX.value = Math.min(Math.max((e.x - 200) / 10, -25), 25); | |
| shinyX.value = withSpring((e.x / 300) * 100); | |
| shinyY.value = withSpring((e.y / 400) * 100); | |
| }) | |
| .onFinalize(() => { | |
| rotateX.value = withSpring(0); | |
| rotateY.value = withSpring(0); | |
| shinyX.value = withSpring(50); | |
| shinyY.value = withSpring(50); | |
| }); | |
| const gesture = Gesture.Simultaneous( | |
| tapGesture, | |
| Platform.OS === "web" ? hoverGesture : panGesture, | |
| ); | |
| const animatedStyle = useAnimatedStyle(() => { | |
| return { | |
| transform: [ | |
| { perspective: 1000 }, | |
| { rotateX: `${rotateX.value}deg` }, | |
| { rotateY: `${rotateY.value}deg` }, | |
| ], | |
| }; | |
| }, [isExplode, blendMode]); | |
| const base = useAnimatedStyle(() => { | |
| const translateY = withTiming(isExplode.get() ? 120 : 0, TIMING_CONFIG); | |
| const translateX = withTiming(isExplode.get() ? -45 : 0, TIMING_CONFIG); | |
| const rotateZ = withTiming( | |
| isExplode.get() ? "-15deg" : "0deg", | |
| TIMING_CONFIG, | |
| ); | |
| const rotateY = withTiming( | |
| isExplode.get() ? "15deg" : "0deg", | |
| TIMING_CONFIG, | |
| ); | |
| const scale = withTiming(isExplode.get() ? 0.5 : 1, TIMING_CONFIG); | |
| const skewX = withTiming(isExplode.get() ? "45deg" : "0deg", TIMING_CONFIG); | |
| const perspective = withTiming(isExplode.get() ? 1000 : 0, TIMING_CONFIG); | |
| if (isExplode.get()) { | |
| return { | |
| zIndex: 1, | |
| transform: [ | |
| { translateX }, | |
| { translateY }, | |
| { skewX }, | |
| { scale }, | |
| { rotateZ }, | |
| { rotateY }, | |
| { perspective }, | |
| ], | |
| position: "absolute", | |
| borderRadius: 16, | |
| shadowColor: "#000", | |
| shadowOffset: { | |
| width: 4, | |
| height: 4, | |
| }, | |
| shadowOpacity: 0.35, | |
| shadowRadius: 5, | |
| elevation: 8, | |
| backfaceVisibility: "hidden", | |
| }; | |
| } else { | |
| return { | |
| transform: [ | |
| { translateX }, | |
| { translateY }, | |
| { skewX }, | |
| { scale }, | |
| { rotateZ }, | |
| { rotateY }, | |
| ], | |
| }; | |
| } | |
| }, [isExplode, blendMode]); | |
| const foil = useAnimatedStyle(() => { | |
| const translateY = withTiming(isExplode.get() ? 50 : 0, TIMING_CONFIG); | |
| const translateX = withTiming(isExplode.get() ? -20 : 0, TIMING_CONFIG); | |
| const rotateZ = withTiming( | |
| isExplode.get() ? "-15deg" : "0deg", | |
| TIMING_CONFIG, | |
| ); | |
| const rotateY = withTiming( | |
| isExplode.get() ? "15deg" : "0deg", | |
| TIMING_CONFIG, | |
| ); | |
| const scale = withTiming(isExplode.get() ? 0.5 : 1, TIMING_CONFIG); | |
| const skewX = withTiming(isExplode.get() ? "45deg" : "0deg", TIMING_CONFIG); | |
| const opacity = withTiming(isExplode.get() ? 1 : 0.7, TIMING_CONFIG); | |
| const perspective = withTiming(isExplode.get() ? 1000 : 0, TIMING_CONFIG); | |
| if (isExplode.get()) { | |
| return { | |
| zIndex: 1, | |
| transform: [ | |
| { translateX }, | |
| { translateY }, | |
| { skewX }, | |
| { scale }, | |
| { rotateZ }, | |
| { rotateY }, | |
| { perspective }, | |
| ], | |
| mixBlendMode: "normal", | |
| opacity, | |
| position: "absolute", | |
| borderRadius: 16, | |
| shadowColor: "#000", | |
| shadowOffset: { | |
| width: 4, | |
| height: 4, | |
| }, | |
| shadowOpacity: 0.35, | |
| shadowRadius: 5, | |
| elevation: 8, | |
| backfaceVisibility: "hidden", | |
| }; | |
| } else | |
| return { | |
| position: "absolute", | |
| top: 0, | |
| left: 0, | |
| right: 0, | |
| bottom: 0, | |
| mixBlendMode: "overlay", | |
| opacity, | |
| transform: [ | |
| { translateX }, | |
| { translateY }, | |
| { skewX }, | |
| { scale }, | |
| { rotateZ }, | |
| { rotateY }, | |
| ], | |
| }; | |
| }); | |
| const shine = useAnimatedStyle(() => { | |
| const rotateZ = withTiming( | |
| isExplode.get() ? "-13deg" : "0deg", | |
| TIMING_CONFIG, | |
| ); | |
| const scale = withTiming(isExplode.get() ? 1 : 1, TIMING_CONFIG); | |
| const skewX = withTiming(isExplode.get() ? "45deg" : "0deg", TIMING_CONFIG); | |
| const width = withTiming(isExplode.get() ? "70%" : "200%", TIMING_CONFIG); | |
| const height = withTiming(isExplode.get() ? "50%" : "200%", TIMING_CONFIG); | |
| const top = withTiming(isExplode.get() ? 10 : "-50%", TIMING_CONFIG); | |
| const left = withTiming(isExplode.get() ? 10 : "-50%", TIMING_CONFIG); | |
| if (isExplode.get()) { | |
| return { | |
| position: "absolute", | |
| width, | |
| height, | |
| top, | |
| left, | |
| zIndex: 1, | |
| mixBlendMode: blendMode, | |
| transform: [ | |
| { translateX: 25 }, | |
| { translateY: 25 }, | |
| { skewX }, | |
| { scale }, | |
| { rotateZ }, | |
| { perspective: 1000 }, | |
| ], | |
| }; | |
| } else { | |
| return { | |
| position: "absolute", | |
| width, | |
| height, | |
| top, | |
| left, | |
| mixBlendMode: blendMode, | |
| transform: [ | |
| { skewX }, | |
| { scale }, | |
| { rotateZ }, | |
| { perspective: 1 }, | |
| { translateX: `${shinyX.value / 5}%` }, | |
| { translateY: `${shinyY.value / 10}%` }, | |
| ], | |
| }; | |
| } | |
| }, [isExplode, blendMode]); | |
| const wrapper = useAnimatedStyle(() => { | |
| if (isExplode.get()) { | |
| return { | |
| isolation: "isolate", | |
| overflow: "visible", | |
| }; | |
| } else { | |
| return { | |
| isolation: "isolate", | |
| width: "100%", | |
| height: "100%", | |
| overflow: "hidden", | |
| borderRadius: 16, | |
| }; | |
| } | |
| }, [isExplode, blendMode]); | |
| return ( | |
| <GestureDetector gesture={gesture}> | |
| <Animated.View style={[styles.cardContainer, animatedStyle]}> | |
| <Animated.View style={wrapper}> | |
| <Animated.Image | |
| source={getPokemonImage(pokemonName)} | |
| style={[styles.card, base]} | |
| /> | |
| <Animated.Image | |
| source={getPokemonFoilImage(pokemonName)} | |
| style={[styles.card, foil]} | |
| /> | |
| <Animated.Image | |
| source={require("@/assets/images/shiny.png")} | |
| style={[ | |
| shine, | |
| { | |
| opacity, | |
| // @ts-expect-error it's ok | |
| mixBlendMode: blendMode, | |
| }, | |
| ]} | |
| /> | |
| </Animated.View> | |
| </Animated.View> | |
| </GestureDetector> | |
| ); | |
| } | |
| const styles = StyleSheet.create({ | |
| explodedMaskContainer: { | |
| isolation: "isolate", | |
| overflow: "visible", | |
| }, | |
| explodedCard: { | |
| position: "absolute", | |
| borderRadius: 16, | |
| shadowColor: "#000", | |
| shadowOffset: { | |
| width: 4, | |
| height: 4, | |
| }, | |
| shadowOpacity: 0.35, | |
| shadowRadius: 5, | |
| elevation: 8, | |
| backfaceVisibility: "hidden", | |
| }, | |
| cardContainer: { | |
| width: 286, | |
| height: 400, | |
| position: "relative", | |
| backgroundColor: "transparent", | |
| }, | |
| maskContainer: { | |
| width: "100%", | |
| height: "100%", | |
| overflow: "hidden", | |
| borderRadius: 16, | |
| }, | |
| card: { | |
| width: "100%", | |
| height: "100%", | |
| }, | |
| foil: { | |
| position: "absolute", | |
| top: 0, | |
| left: 0, | |
| right: 0, | |
| bottom: 0, | |
| mixBlendMode: "overlay", | |
| opacity: 0.7, | |
| }, | |
| }); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example of card image + foil image

