optimized perf

This commit is contained in:
tapframe 2026-01-06 00:00:33 +05:30
parent 0f1d736716
commit 4ce14ec4cc
2 changed files with 56 additions and 174 deletions

View file

@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react';
import React, { useEffect } from 'react';
import { useWindowDimensions, StyleSheet } from 'react-native';
import {
Blur,
@ -7,12 +7,12 @@ import {
Circle,
Extrapolate,
interpolate,
interpolateColors,
LinearGradient,
Path,
RadialGradient,
usePathValue,
vec,
Group,
} from '@shopify/react-native-skia';
import {
Easing,
@ -20,6 +20,7 @@ import {
withRepeat,
withTiming,
SharedValue,
useDerivedValue,
} from 'react-native-reanimated';
import {
@ -31,15 +32,12 @@ import {
ALL_SHAPES_Z,
} from './shapes';
// Number of shapes
const SHAPES_COUNT = ALL_SHAPES.length;
// Color palettes for each shape (gradient: start, middle, end)
const COLOR_PALETTES = [
['#FFD700', '#FFA500', '#FF6B00'], // Star: Gold → Orange
['#7C3AED', '#A855F7', '#EC4899'], // Plugin: Purple → Pink
['#00D9FF', '#06B6D4', '#0EA5E9'], // Search: Cyan → Blue
['#FF006E', '#F43F5E', '#FB7185'], // Heart: Pink → Rose
// Color palettes for each shape (gradient stops)
const COLOR_STOPS = [
{ start: '#FFD700', end: '#FF6B00' }, // Star: Gold → Orange
{ start: '#7C3AED', end: '#EC4899' }, // Plugin: Purple → Pink
{ start: '#00D9FF', end: '#0EA5E9' }, // Search: Cyan → Blue
{ start: '#FF006E', end: '#FB7185' }, // Heart: Pink → Rose
];
// ============ 3D UTILITIES ============
@ -65,144 +63,57 @@ interface ShapeAnimationProps {
scrollX: SharedValue<number>;
}
// Single colored path component
const ColoredPath = ({
morphPath,
colorIndex,
scrollX,
windowWidth,
windowHeight,
}: {
morphPath: any;
colorIndex: number;
scrollX: SharedValue<number>;
windowWidth: number;
windowHeight: number;
}) => {
const colors = COLOR_PALETTES[colorIndex];
// Create opacity value using Skia's interpolate inside usePathValue pattern
const opacityPath = usePathValue((skPath) => {
'worklet';
// Calculate opacity based on scroll position
const shapeWidth = windowWidth;
const slideStart = colorIndex * shapeWidth;
const slideMid = slideStart;
const slideEnd = (colorIndex + 1) * shapeWidth;
const prevSlideEnd = (colorIndex - 1) * shapeWidth;
// Opacity peaks at 1 when on this slide, fades to 0 on adjacent slides
let opacity = 0;
if (colorIndex === 0) {
// First slide: 1 at start, fade out to next
opacity = interpolate(
scrollX.value,
[0, shapeWidth],
[1, 0],
Extrapolate.CLAMP
);
} else if (colorIndex === COLOR_PALETTES.length - 1) {
// Last slide: fade in from previous, stay at 1
opacity = interpolate(
scrollX.value,
[prevSlideEnd, slideMid],
[0, 1],
Extrapolate.CLAMP
);
} else {
// Middle slides: fade in from previous, fade out to next
opacity = interpolate(
scrollX.value,
[prevSlideEnd, slideMid, slideEnd],
[0, 1, 0],
Extrapolate.CLAMP
);
}
// Store opacity in path for use - we'll read it via a trick
// This is a workaround since we can't directly animate opacity
skPath.addCircle(-1000 - opacity * 100, -1000, 1); // Hidden marker
return skPath;
});
return (
<Group opacity={1}>
<Path path={morphPath} style="fill">
<LinearGradient
start={vec(0, windowHeight * 0.4)}
end={vec(windowWidth, windowHeight * 0.9)}
colors={colors}
/>
<BlurMask blur={5} style="solid" />
</Path>
</Group>
);
};
export const ShapeAnimation: React.FC<ShapeAnimationProps> = ({ scrollX }) => {
const iTime = useSharedValue(0.0);
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
// Create paths for each color layer with opacity baked in
const createPathForIndex = (colorIndex: number) => {
return usePathValue(skPath => {
'worklet';
const centerX = windowWidth / 2;
const centerY = windowHeight * 0.65;
const distance = 350;
// Pre-compute input range once
const shapeWidth = windowWidth;
const inputRange = ALL_SHAPES.map((_, idx) => shapeWidth * idx);
// Calculate opacity for this color layer
const shapeWidth = windowWidth;
const slideStart = colorIndex * shapeWidth;
const prevSlideEnd = (colorIndex - 1) * shapeWidth;
const nextSlideStart = (colorIndex + 1) * shapeWidth;
// Single optimized path - all 4 shapes batched into one Skia Path
const morphPath = usePathValue(skPath => {
'worklet';
const centerX = windowWidth / 2;
const centerY = windowHeight * 0.65;
const distance = 350;
let opacity = 0;
if (colorIndex === 0) {
opacity = interpolate(scrollX.value, [0, shapeWidth], [1, 0], Extrapolate.CLAMP);
} else if (colorIndex === COLOR_PALETTES.length - 1) {
opacity = interpolate(scrollX.value, [prevSlideEnd, slideStart], [0, 1], Extrapolate.CLAMP);
} else {
opacity = interpolate(scrollX.value, [prevSlideEnd, slideStart, nextSlideStart], [0, 1, 0], Extrapolate.CLAMP);
}
for (let i = 0; i < N_POINTS; i++) {
// Interpolate 3D coordinates between all shapes
const baseX = interpolate(scrollX.value, inputRange, ALL_SHAPES_X[i], Extrapolate.CLAMP);
const baseY = interpolate(scrollX.value, inputRange, ALL_SHAPES_Y[i], Extrapolate.CLAMP);
const baseZ = interpolate(scrollX.value, inputRange, ALL_SHAPES_Z[i], Extrapolate.CLAMP);
// Skip drawing if not visible
if (opacity < 0.01) return skPath;
// Apply 3D rotation
let p: Point3D = { x: baseX, y: baseY, z: baseZ };
p = rotateX(p, 0.2); // Fixed X tilt
p = rotateY(p, iTime.value); // Animated Y rotation
// Input range for all shapes
const inputRange = new Array(ALL_SHAPES.length)
.fill(0)
.map((_, idx) => shapeWidth * idx);
// Perspective projection
const scale = distance / (distance + p.z);
const screenX = centerX + p.x * scale;
const screenY = centerY + p.y * scale;
for (let i = 0; i < N_POINTS; i++) {
const baseX = interpolate(scrollX.value, inputRange, ALL_SHAPES_X[i], Extrapolate.CLAMP);
const baseY = interpolate(scrollX.value, inputRange, ALL_SHAPES_Y[i], Extrapolate.CLAMP);
const baseZ = interpolate(scrollX.value, inputRange, ALL_SHAPES_Z[i], Extrapolate.CLAMP);
// Depth-based radius for parallax effect
const radius = Math.max(0.2, 0.5 * scale);
skPath.addCircle(screenX, screenY, radius);
}
let p: Point3D = { x: baseX, y: baseY, z: baseZ };
p = rotateX(p, 0.2);
p = rotateY(p, iTime.value);
return skPath;
});
const scale = distance / (distance + p.z);
const screenX = centerX + p.x * scale;
const screenY = centerY + p.y * scale;
// Interpolate gradient colors based on scroll position
const gradientColors = useDerivedValue(() => {
const startColors = COLOR_STOPS.map(c => c.start);
const endColors = COLOR_STOPS.map(c => c.end);
// Scale radius by opacity for smooth color transition
const radius = Math.max(0.1, 0.5 * scale * opacity);
skPath.addCircle(screenX, screenY, radius);
}
const start = interpolateColors(scrollX.value, inputRange, startColors);
const end = interpolateColors(scrollX.value, inputRange, endColors);
return skPath;
});
};
return [start, end];
});
// Create all 4 color layer paths
const path0 = createPathForIndex(0);
const path1 = createPathForIndex(1);
const path2 = createPathForIndex(2);
const path3 = createPathForIndex(3);
// Rotation animation
// Rotation animation - infinite loop
useEffect(() => {
iTime.value = 0;
iTime.value = withRepeat(
@ -224,7 +135,7 @@ export const ShapeAnimation: React.FC<ShapeAnimationProps> = ({ scrollX }) => {
height: windowHeight,
},
]}>
{/* Background radial gradient blurred */}
{/* Background glow */}
<Circle
cx={windowWidth / 2}
cy={windowHeight * 0.65}
@ -237,42 +148,12 @@ export const ShapeAnimation: React.FC<ShapeAnimationProps> = ({ scrollX }) => {
<Blur blur={60} />
</Circle>
{/* Layer 0: Gold (Star) */}
<Path path={path0} style="fill">
{/* Single optimized path with interpolated gradient */}
<Path path={morphPath} style="fill">
<LinearGradient
start={vec(0, windowHeight * 0.4)}
end={vec(windowWidth, windowHeight * 0.9)}
colors={COLOR_PALETTES[0]}
/>
<BlurMask blur={5} style="solid" />
</Path>
{/* Layer 1: Purple (Plugin) */}
<Path path={path1} style="fill">
<LinearGradient
start={vec(0, windowHeight * 0.4)}
end={vec(windowWidth, windowHeight * 0.9)}
colors={COLOR_PALETTES[1]}
/>
<BlurMask blur={5} style="solid" />
</Path>
{/* Layer 2: Cyan (Search) */}
<Path path={path2} style="fill">
<LinearGradient
start={vec(0, windowHeight * 0.4)}
end={vec(windowWidth, windowHeight * 0.9)}
colors={COLOR_PALETTES[2]}
/>
<BlurMask blur={5} style="solid" />
</Path>
{/* Layer 3: Pink (Heart) */}
<Path path={path3} style="fill">
<LinearGradient
start={vec(0, windowHeight * 0.4)}
end={vec(windowWidth, windowHeight * 0.9)}
colors={COLOR_PALETTES[3]}
colors={gradientColors}
/>
<BlurMask blur={5} style="solid" />
</Path>

View file

@ -300,8 +300,8 @@ const OnboardingScreen = () => {
<StatusBar barStyle="light-content" backgroundColor="#0A0A0A" translucent />
<View style={styles.fullScreenContainer}>
{/* Shape Animation Background */}
<ShapeAnimation scrollX={scrollX} />
{/* Shape Animation Background - iOS only */}
{Platform.OS === 'ios' && <ShapeAnimation scrollX={scrollX} />}
{/* Header */}
<Animated.View
@ -417,12 +417,12 @@ const styles = StyleSheet.create({
slide: {
width,
flex: 1,
justifyContent: 'flex-start', // Align to top
justifyContent: Platform.OS === 'ios' ? 'flex-start' : 'center', // Top on iOS, center on Android
paddingHorizontal: 32,
paddingTop: '20%', // Push text down slightly from header
paddingTop: Platform.OS === 'ios' ? '20%' : 0, // Padding only on iOS
},
textContainer: {
alignItems: 'flex-start',
alignItems: 'flex-start', // Text always left-aligned
},
title: {
fontSize: 52,
@ -444,6 +444,7 @@ const styles = StyleSheet.create({
lineHeight: 24,
color: 'rgba(255, 255, 255, 0.4)',
maxWidth: 300,
textAlign: 'left', // Always left-aligned text
},
footer: {
paddingHorizontal: 24,