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 { useWindowDimensions, StyleSheet } from 'react-native';
import { import {
Blur, Blur,
@ -7,12 +7,12 @@ import {
Circle, Circle,
Extrapolate, Extrapolate,
interpolate, interpolate,
interpolateColors,
LinearGradient, LinearGradient,
Path, Path,
RadialGradient, RadialGradient,
usePathValue, usePathValue,
vec, vec,
Group,
} from '@shopify/react-native-skia'; } from '@shopify/react-native-skia';
import { import {
Easing, Easing,
@ -20,6 +20,7 @@ import {
withRepeat, withRepeat,
withTiming, withTiming,
SharedValue, SharedValue,
useDerivedValue,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { import {
@ -31,15 +32,12 @@ import {
ALL_SHAPES_Z, ALL_SHAPES_Z,
} from './shapes'; } from './shapes';
// Number of shapes // Color palettes for each shape (gradient stops)
const SHAPES_COUNT = ALL_SHAPES.length; const COLOR_STOPS = [
{ start: '#FFD700', end: '#FF6B00' }, // Star: Gold → Orange
// Color palettes for each shape (gradient: start, middle, end) { start: '#7C3AED', end: '#EC4899' }, // Plugin: Purple → Pink
const COLOR_PALETTES = [ { start: '#00D9FF', end: '#0EA5E9' }, // Search: Cyan → Blue
['#FFD700', '#FFA500', '#FF6B00'], // Star: Gold → Orange { start: '#FF006E', end: '#FB7185' }, // Heart: Pink → Rose
['#7C3AED', '#A855F7', '#EC4899'], // Plugin: Purple → Pink
['#00D9FF', '#06B6D4', '#0EA5E9'], // Search: Cyan → Blue
['#FF006E', '#F43F5E', '#FB7185'], // Heart: Pink → Rose
]; ];
// ============ 3D UTILITIES ============ // ============ 3D UTILITIES ============
@ -65,144 +63,57 @@ interface ShapeAnimationProps {
scrollX: SharedValue<number>; 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 }) => { export const ShapeAnimation: React.FC<ShapeAnimationProps> = ({ scrollX }) => {
const iTime = useSharedValue(0.0); const iTime = useSharedValue(0.0);
const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const { width: windowWidth, height: windowHeight } = useWindowDimensions();
// Create paths for each color layer with opacity baked in // Pre-compute input range once
const createPathForIndex = (colorIndex: number) => { const shapeWidth = windowWidth;
return usePathValue(skPath => { const inputRange = ALL_SHAPES.map((_, idx) => shapeWidth * idx);
'worklet';
const centerX = windowWidth / 2;
const centerY = windowHeight * 0.65;
const distance = 350;
// Calculate opacity for this color layer // Single optimized path - all 4 shapes batched into one Skia Path
const shapeWidth = windowWidth; const morphPath = usePathValue(skPath => {
const slideStart = colorIndex * shapeWidth; 'worklet';
const prevSlideEnd = (colorIndex - 1) * shapeWidth; const centerX = windowWidth / 2;
const nextSlideStart = (colorIndex + 1) * shapeWidth; const centerY = windowHeight * 0.65;
const distance = 350;
let opacity = 0; for (let i = 0; i < N_POINTS; i++) {
if (colorIndex === 0) { // Interpolate 3D coordinates between all shapes
opacity = interpolate(scrollX.value, [0, shapeWidth], [1, 0], Extrapolate.CLAMP); const baseX = interpolate(scrollX.value, inputRange, ALL_SHAPES_X[i], Extrapolate.CLAMP);
} else if (colorIndex === COLOR_PALETTES.length - 1) { const baseY = interpolate(scrollX.value, inputRange, ALL_SHAPES_Y[i], Extrapolate.CLAMP);
opacity = interpolate(scrollX.value, [prevSlideEnd, slideStart], [0, 1], Extrapolate.CLAMP); const baseZ = interpolate(scrollX.value, inputRange, ALL_SHAPES_Z[i], Extrapolate.CLAMP);
} else {
opacity = interpolate(scrollX.value, [prevSlideEnd, slideStart, nextSlideStart], [0, 1, 0], Extrapolate.CLAMP);
}
// Skip drawing if not visible // Apply 3D rotation
if (opacity < 0.01) return skPath; 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 // Perspective projection
const inputRange = new Array(ALL_SHAPES.length) const scale = distance / (distance + p.z);
.fill(0) const screenX = centerX + p.x * scale;
.map((_, idx) => shapeWidth * idx); const screenY = centerY + p.y * scale;
for (let i = 0; i < N_POINTS; i++) { // Depth-based radius for parallax effect
const baseX = interpolate(scrollX.value, inputRange, ALL_SHAPES_X[i], Extrapolate.CLAMP); const radius = Math.max(0.2, 0.5 * scale);
const baseY = interpolate(scrollX.value, inputRange, ALL_SHAPES_Y[i], Extrapolate.CLAMP); skPath.addCircle(screenX, screenY, radius);
const baseZ = interpolate(scrollX.value, inputRange, ALL_SHAPES_Z[i], Extrapolate.CLAMP); }
let p: Point3D = { x: baseX, y: baseY, z: baseZ }; return skPath;
p = rotateX(p, 0.2); });
p = rotateY(p, iTime.value);
const scale = distance / (distance + p.z); // Interpolate gradient colors based on scroll position
const screenX = centerX + p.x * scale; const gradientColors = useDerivedValue(() => {
const screenY = centerY + p.y * scale; 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 start = interpolateColors(scrollX.value, inputRange, startColors);
const radius = Math.max(0.1, 0.5 * scale * opacity); const end = interpolateColors(scrollX.value, inputRange, endColors);
skPath.addCircle(screenX, screenY, radius);
}
return skPath; return [start, end];
}); });
};
// Create all 4 color layer paths // Rotation animation - infinite loop
const path0 = createPathForIndex(0);
const path1 = createPathForIndex(1);
const path2 = createPathForIndex(2);
const path3 = createPathForIndex(3);
// Rotation animation
useEffect(() => { useEffect(() => {
iTime.value = 0; iTime.value = 0;
iTime.value = withRepeat( iTime.value = withRepeat(
@ -224,7 +135,7 @@ export const ShapeAnimation: React.FC<ShapeAnimationProps> = ({ scrollX }) => {
height: windowHeight, height: windowHeight,
}, },
]}> ]}>
{/* Background radial gradient blurred */} {/* Background glow */}
<Circle <Circle
cx={windowWidth / 2} cx={windowWidth / 2}
cy={windowHeight * 0.65} cy={windowHeight * 0.65}
@ -237,42 +148,12 @@ export const ShapeAnimation: React.FC<ShapeAnimationProps> = ({ scrollX }) => {
<Blur blur={60} /> <Blur blur={60} />
</Circle> </Circle>
{/* Layer 0: Gold (Star) */} {/* Single optimized path with interpolated gradient */}
<Path path={path0} style="fill"> <Path path={morphPath} style="fill">
<LinearGradient <LinearGradient
start={vec(0, windowHeight * 0.4)} start={vec(0, windowHeight * 0.4)}
end={vec(windowWidth, windowHeight * 0.9)} end={vec(windowWidth, windowHeight * 0.9)}
colors={COLOR_PALETTES[0]} colors={gradientColors}
/>
<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]}
/> />
<BlurMask blur={5} style="solid" /> <BlurMask blur={5} style="solid" />
</Path> </Path>

View file

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