mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
optimized perf
This commit is contained in:
parent
0f1d736716
commit
4ce14ec4cc
2 changed files with 56 additions and 174 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue