Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GradientBorderView } from '@/components/gradient-border/GradientBorderV
import { useColorMode } from '@/design-system';
import { getValueForColorMode } from '@/design-system/color/palettes';
import { InnerShadow } from '@/features/polymarket/components/InnerShadow';
import { RainbowShimmerFill } from '@/features/rnbw-membership/components/RainbowShimmerFill';
import { ShadowLayers } from '@/features/rnbw-membership/components/ShadowLayers';
import { MEMBERSHIP_CARD_BACKGROUND_COLOR } from '@/features/rnbw-membership/membershipCardTheme';
import { getTierPrimaryButtonTheme, getTierSecondaryButtonTheme } from '@/features/rnbw-membership/tierVisuals';
Expand Down Expand Up @@ -39,7 +40,7 @@ export const MembershipTierButtonSurface = memo(function MembershipTierButtonSur
const borderRadius = height / 2;
const shadowLayerBackgroundColor = getValueForColorMode(MEMBERSHIP_CARD_BACKGROUND_COLOR, colorMode);

const { borderGradient, surfaceGradient, surfaceHighlight, shadows } = useMemo(() => {
const { borderGradient, surfaceGradient, surfaceHighlight, shadows, overlay } = useMemo(() => {
const surfaceTheme =
variant === 'primary' ? getTierPrimaryButtonTheme(tier.level).surface : getTierSecondaryButtonTheme(tier.level).surface;

Expand All @@ -48,6 +49,7 @@ export const MembershipTierButtonSurface = memo(function MembershipTierButtonSur
surfaceGradient: getValueForColorMode(surfaceTheme.fill, colorMode),
surfaceHighlight: surfaceTheme.highlight ? getValueForColorMode(surfaceTheme.highlight, colorMode) : null,
shadows: getValueForColorMode(surfaceTheme.shadows, colorMode),
overlay: surfaceTheme.overlay ? getValueForColorMode(surfaceTheme.overlay, colorMode) : null,
};
}, [tier.level, variant, colorMode]);

Expand All @@ -66,13 +68,24 @@ export const MembershipTierButtonSurface = memo(function MembershipTierButtonSur
end={borderGradient.end ?? GRADIENT_END}
style={resolvedContainerStyle}
>
<LinearGradient
colors={surfaceGradient.colors}
locations={surfaceGradient.locations}
start={surfaceGradient.start ?? GRADIENT_START}
end={surfaceGradient.end ?? GRADIENT_END}
style={[StyleSheet.absoluteFill, { borderRadius }]}
/>
{overlay ? (
<RainbowShimmerFill
fillColors={surfaceGradient.colors}
fillLocations={surfaceGradient.locations}
fillStart={surfaceGradient.start ?? GRADIENT_START}
fillEnd={surfaceGradient.end ?? GRADIENT_END}
borderRadius={borderRadius}
overlay={overlay}
/>
) : (
<LinearGradient
colors={surfaceGradient.colors}
locations={surfaceGradient.locations}
start={surfaceGradient.start ?? GRADIENT_START}
end={surfaceGradient.end ?? GRADIENT_END}
style={[StyleSheet.absoluteFill, { borderRadius }]}
/>
)}
{surfaceHighlight && (
<LinearGradient
colors={surfaceHighlight.colors}
Expand Down
101 changes: 101 additions & 0 deletions src/features/rnbw-membership/components/RainbowShimmerFill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { StyleSheet, View } from 'react-native';

import {
Blur,
Canvas,
Group,
Mask,
Paint,
RoundedRect,
Shadow,
LinearGradient as SkiaLinearGradient,
vec,
} from '@shopify/react-native-skia';

import type { GradientColors, GradientLocations, HardLightOverlay } from '@/features/rnbw-membership/tierVisuals';

const DEFAULT_FILL_START = { x: 0, y: 0 };
const DEFAULT_FILL_END = { x: 0, y: 1 };
const MASK_SHADOW_COLOR = '#FFFFFF';

type RainbowShimmerFillProps = {
fillColors: GradientColors;
fillLocations?: GradientLocations;
fillStart?: { x: number; y: number };
fillEnd?: { x: number; y: number };
borderRadius: number;
overlay: HardLightOverlay;
};

export const RainbowShimmerFill = memo(function RainbowShimmerFill({
fillColors,
fillLocations,
fillStart = DEFAULT_FILL_START,
fillEnd = DEFAULT_FILL_END,
borderRadius,
overlay,
}: RainbowShimmerFillProps) {
const [measured, setMeasured] = useState<{ width: number; height: number } | null>(null);

const onLayout = useCallback((e: { nativeEvent: { layout: { width: number; height: number } } }) => {
const { width: w, height: h } = e.nativeEvent.layout;
setMeasured(prev => (prev && prev.width === w && prev.height === h ? prev : { width: w, height: h }));
}, []);

const width = measured?.width ?? 0;
const height = measured?.height ?? 0;
const canDisplay = width > 0 && height > 0;

// Skia wants mutable arrays
const skiaFillColors = useMemo(() => [...fillColors], [fillColors]);
const skiaFillLocations = useMemo(() => (fillLocations ? [...fillLocations] : undefined), [fillLocations]);
const skiaOverlayColors = useMemo(() => [...overlay.colors], [overlay.colors]);
const skiaOverlayLocations = useMemo(() => [...overlay.locations], [overlay.locations]);

return (
<View style={StyleSheet.absoluteFill} onLayout={onLayout} pointerEvents="none">
{canDisplay && (
<Canvas style={StyleSheet.absoluteFill}>
<RoundedRect x={0} y={0} width={width} height={height} r={borderRadius}>
<SkiaLinearGradient
start={vec(width * fillStart.x, height * fillStart.y)}
end={vec(width * fillEnd.x, height * fillEnd.y)}
colors={skiaFillColors}
positions={skiaFillLocations}
/>
</RoundedRect>

<Group layer={<Paint blendMode="hardLight" />}>
<Mask
mask={
<Group>
<RoundedRect x={0} y={0} width={width} height={height} r={borderRadius}>
<Shadow
dx={overlay.maskShadow.dx}
dy={overlay.maskShadow.dy}
blur={overlay.maskShadow.blur}
color={MASK_SHADOW_COLOR}
inner
shadowOnly
/>
<Blur blur={1.5} />
</RoundedRect>
</Group>
}
>
<RoundedRect x={0} y={0} width={width} height={height} r={borderRadius}>
<SkiaLinearGradient
start={vec(0, height / 2)}
end={vec(width, height / 2)}
colors={skiaOverlayColors}
positions={skiaOverlayLocations}
/>
</RoundedRect>
</Mask>
</Group>
</Canvas>
)}
</View>
);
});
11 changes: 10 additions & 1 deletion src/features/rnbw-membership/components/ShadowLayers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ShadowLayersProps = {
borderRadius: number;
backgroundColor: string;
children: ReactNode;
outerGlowLayer?: ReactNode;
style?: StyleProp<ViewStyle>;
};

Expand All @@ -30,9 +31,17 @@ function getShadowLayerStyle(shadowStyle: ShadowStyle): ViewStyle {
return shadowStyle;
}

export const ShadowLayers = memo(function ShadowLayers({ shadows, borderRadius, backgroundColor, children, style }: ShadowLayersProps) {
export const ShadowLayers = memo(function ShadowLayers({
shadows,
borderRadius,
backgroundColor,
children,
outerGlowLayer,
style,
}: ShadowLayersProps) {
return (
<View style={[styles.container, style]}>
{outerGlowLayer}
{shadows.map((shadowStyle, index) => (
<View
key={`${shadowStyle.shadowColor}-${index}`}
Expand Down
77 changes: 77 additions & 0 deletions src/features/rnbw-membership/components/SkiaGradientShadow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { PixelRatio, StyleSheet, View, type LayoutChangeEvent } from 'react-native';

import { Blur, Canvas, RoundedRect, LinearGradient as SkiaLinearGradient, vec } from '@shopify/react-native-skia';

import type { GradientShadow } from '@/features/rnbw-membership/tierVisuals';

const DEFAULT_START = { x: 0, y: 0.5 };
const DEFAULT_END = { x: 1, y: 0.5 };

type SkiaGradientShadowProps = {
borderRadius: number;
shadow: GradientShadow;
};

export const SkiaGradientShadow = memo(function SkiaGradientShadow({ borderRadius, shadow }: SkiaGradientShadowProps) {
const [layout, setLayout] = useState<{ width: number; height: number } | null>(null);

const dx = shadow.dx ?? 0;
const dy = shadow.dy ?? 0;
const start = shadow.start ?? DEFAULT_START;
const end = shadow.end ?? DEFAULT_END;
const padding = useMemo(
() => PixelRatio.roundToNearestPixel(shadow.blur * 2 + Math.max(Math.abs(dx), Math.abs(dy))),
[shadow.blur, dx, dy]
);

const skiaColors = useMemo(() => [...shadow.colors], [shadow.colors]);
const skiaLocations = useMemo(() => (shadow.locations ? [...shadow.locations] : undefined), [shadow.locations]);

const handleLayout = useCallback((event: LayoutChangeEvent) => {
const nextLayout = {
width: PixelRatio.roundToNearestPixel(event.nativeEvent.layout.width),
height: PixelRatio.roundToNearestPixel(event.nativeEvent.layout.height),
};

setLayout(current => (current && current.width === nextLayout.width && current.height === nextLayout.height ? current : nextLayout));
}, []);

const width = layout?.width ?? 0;
const height = layout?.height ?? 0;
const canRender = width > 0 && height > 0;

return (
<View pointerEvents="none" style={StyleSheet.absoluteFill} onLayout={handleLayout}>
{canRender && (
<Canvas
style={[
styles.canvas,
{
top: -padding,
left: -padding,
width: width + padding * 2,
height: height + padding * 2,
},
]}
>
<RoundedRect x={padding + dx} y={padding + dy} width={width} height={height} r={borderRadius} opacity={shadow.opacity}>
<SkiaLinearGradient
start={vec(padding + dx + width * start.x, padding + dy + height * start.y)}
end={vec(padding + dx + width * end.x, padding + dy + height * end.y)}
colors={skiaColors}
positions={skiaLocations}
/>
<Blur blur={shadow.blur} />
</RoundedRect>
</Canvas>
)}
</View>
);
});

const styles = StyleSheet.create({
canvas: {
position: 'absolute',
},
});
41 changes: 33 additions & 8 deletions src/features/rnbw-membership/components/TierBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { Text, useColorMode } from '@/design-system';
import { getValueForColorMode, globalColors } from '@/design-system/color/palettes';
import type { TextSize, TextWeight } from '@/design-system/components/Text/Text';
import { InnerShadow } from '@/features/polymarket/components/InnerShadow';
import { RainbowShimmerFill } from '@/features/rnbw-membership/components/RainbowShimmerFill';
import { ShadowLayers } from '@/features/rnbw-membership/components/ShadowLayers';
import { SkiaGradientShadow } from '@/features/rnbw-membership/components/SkiaGradientShadow';
import { getTierBadgeTheme } from '@/features/rnbw-membership/tierVisuals';
import type { Tier as TierType } from '@/features/rnbw-membership/types';
import { opacity } from '@/framework/ui/utils/opacity';
Expand All @@ -36,7 +38,14 @@ export const TierBadge = memo(function TierBadge({
}) {
const { colorMode } = useColorMode();
const borderRadius = height / 2;
const { fill: badgeGradient, text, shadows: badgeShadows, border: badgeBorderGradient } = getTierBadgeTheme(tier.level);
const {
fill: badgeGradient,
text,
shadows: badgeShadows,
border: badgeBorderGradient,
overlay,
backgroundShadow,
} = getTierBadgeTheme(tier.level);
const {
colors: badgeGradientColors,
locations: badgeGradientLocations,
Expand All @@ -57,6 +66,8 @@ export const TierBadge = memo(function TierBadge({
} = getValueForColorMode(badgeBorderGradient, colorMode);
const shadowStyles = getValueForColorMode(badgeShadows, colorMode);
const textShadowStyle = getValueForColorMode(text.shadow, colorMode);
const resolvedOverlay = overlay ? getValueForColorMode(overlay, colorMode) : null;
const resolvedBackgroundShadow = backgroundShadow ? getValueForColorMode(backgroundShadow, colorMode) : null;
const [firstBadgeGradientColor] = badgeGradientColors;
const shadowLayerBackgroundColor = typeof firstBadgeGradientColor === 'string' ? firstBadgeGradientColor : '#FFFFFF';

Expand All @@ -65,6 +76,9 @@ export const TierBadge = memo(function TierBadge({
shadows={shadowStyles}
borderRadius={borderRadius}
backgroundColor={shadowLayerBackgroundColor}
outerGlowLayer={
resolvedBackgroundShadow ? <SkiaGradientShadow borderRadius={borderRadius} shadow={resolvedBackgroundShadow} /> : null
}
style={styles.shadowStack}
>
<GradientBorderView
Expand All @@ -76,13 +90,24 @@ export const TierBadge = memo(function TierBadge({
style={[styles.tierBadge, { height, borderRadius }]}
>
<InnerShadow color={opacity(globalColors.white100, 0.1)} blur={1} dx={0} dy={3} borderRadius={borderRadius} />
<LinearGradient
colors={badgeGradientColors}
locations={badgeGradientLocations}
start={badgeStart}
end={badgeEnd}
style={StyleSheet.absoluteFill}
/>
{resolvedOverlay ? (
<RainbowShimmerFill
fillColors={badgeGradientColors}
fillLocations={badgeGradientLocations}
fillStart={badgeStart}
fillEnd={badgeEnd}
borderRadius={borderRadius}
overlay={resolvedOverlay}
/>
) : (
<LinearGradient
colors={badgeGradientColors}
locations={badgeGradientLocations}
start={badgeStart}
end={badgeEnd}
style={StyleSheet.absoluteFill}
/>
)}
<GradientText
colors={badgeTextGradientColors}
locations={badgeTextGradientLocations}
Expand Down
Loading
Loading