Skip to content

Instantly share code, notes, and snippets.

@TikiCat7
Created February 18, 2025 07:57
Show Gist options
  • Select an option

  • Save TikiCat7/787d5ecd7f250f6ceb2c697308b1d67c to your computer and use it in GitHub Desktop.

Select an option

Save TikiCat7/787d5ecd7f250f6ceb2c697308b1d67c to your computer and use it in GitHub Desktop.
Shiny pokemon cards using mix-blend-mode in RN 0.77
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,
},
});
@TikiCat7
Copy link
Author

Example of card image + foil image
charizard_foil
charizard

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment