109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { StyleSheet, View } from 'react-native';
|
|
import { AppColors } from '../constants/Colors';
|
|
|
|
interface ThemeBackdropProps {
|
|
colors: AppColors;
|
|
}
|
|
|
|
const texturePoints = [
|
|
[4, 6, 2], [12, 14, 1], [20, 9, 2], [26, 19, 1], [34, 8, 2], [42, 16, 1],
|
|
[50, 10, 1], [58, 18, 2], [66, 7, 1], [74, 15, 2], [82, 11, 1], [90, 17, 2],
|
|
[8, 30, 1], [16, 25, 2], [24, 33, 1], [32, 27, 2], [40, 35, 1], [48, 28, 2],
|
|
[56, 36, 1], [64, 26, 2], [72, 34, 1], [80, 29, 2], [88, 37, 1], [94, 31, 2],
|
|
[6, 48, 2], [14, 44, 1], [22, 52, 2], [30, 46, 1], [38, 54, 2], [46, 49, 1],
|
|
[54, 56, 2], [62, 45, 1], [70, 53, 2], [78, 47, 1], [86, 55, 2], [92, 50, 1],
|
|
[10, 70, 1], [18, 64, 2], [26, 72, 1], [34, 67, 2], [42, 74, 1], [50, 68, 2],
|
|
[58, 76, 1], [66, 65, 2], [74, 73, 1], [82, 69, 2], [90, 77, 1], [96, 71, 2],
|
|
];
|
|
|
|
const parseColor = (value: string) => {
|
|
if (value.startsWith('#')) {
|
|
const cleaned = value.replace('#', '');
|
|
const normalized = cleaned.length === 3
|
|
? cleaned.split('').map((c) => `${c}${c}`).join('')
|
|
: cleaned;
|
|
const int = Number.parseInt(normalized, 16);
|
|
return {
|
|
r: (int >> 16) & 255,
|
|
g: (int >> 8) & 255,
|
|
b: int & 255,
|
|
a: 1,
|
|
};
|
|
}
|
|
|
|
const match = value.match(/rgba?\(([^)]+)\)/i);
|
|
if (!match) return { r: 0, g: 0, b: 0, a: 0 };
|
|
const parts = match[1].split(',').map((part) => part.trim());
|
|
return {
|
|
r: Number.parseFloat(parts[0]) || 0,
|
|
g: Number.parseFloat(parts[1]) || 0,
|
|
b: Number.parseFloat(parts[2]) || 0,
|
|
a: parts.length > 3 ? Number.parseFloat(parts[3]) || 0 : 1,
|
|
};
|
|
};
|
|
|
|
const mixColor = (start: string, end: string, ratio: number) => {
|
|
const a = parseColor(start);
|
|
const b = parseColor(end);
|
|
const t = Math.max(0, Math.min(1, ratio));
|
|
const r = Math.round(a.r + (b.r - a.r) * t);
|
|
const g = Math.round(a.g + (b.g - a.g) * t);
|
|
const bl = Math.round(a.b + (b.b - a.b) * t);
|
|
const alpha = a.a + (b.a - a.a) * t;
|
|
return `rgba(${r}, ${g}, ${bl}, ${alpha})`;
|
|
};
|
|
|
|
const ThemeBackdropInner: React.FC<ThemeBackdropProps> = ({ colors }) => {
|
|
const gradientStrips = useMemo(() =>
|
|
Array.from({ length: 18 }).map((_, index, arr) => {
|
|
const ratio = index / (arr.length - 1);
|
|
return mixColor(colors.pageGradientStart, colors.pageGradientEnd, ratio);
|
|
}),
|
|
[colors.pageGradientStart, colors.pageGradientEnd]);
|
|
|
|
return (
|
|
<View pointerEvents="none" style={[StyleSheet.absoluteFill, { backgroundColor: colors.pageBase }]}>
|
|
<View style={styles.gradientLayer}>
|
|
{gradientStrips.map((stripColor, index) => (
|
|
<View
|
|
key={`strip-${index}`}
|
|
style={[styles.gradientStrip, { backgroundColor: stripColor }]}
|
|
/>
|
|
))}
|
|
</View>
|
|
<View style={styles.textureLayer}>
|
|
{texturePoints.map(([x, y, size], index) => (
|
|
<View
|
|
key={index}
|
|
style={[
|
|
styles.noiseDot,
|
|
{
|
|
left: `${x}%`,
|
|
top: `${y}%`,
|
|
width: size,
|
|
height: size,
|
|
borderRadius: size / 2,
|
|
backgroundColor: colors.pageTexture,
|
|
},
|
|
]}
|
|
/>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export const ThemeBackdrop = React.memo(ThemeBackdropInner);
|
|
|
|
const styles = StyleSheet.create({
|
|
gradientLayer: { ...StyleSheet.absoluteFillObject },
|
|
gradientStrip: { flex: 1 },
|
|
textureLayer: {
|
|
...StyleSheet.absoluteFillObject,
|
|
},
|
|
noiseDot: {
|
|
position: 'absolute',
|
|
},
|
|
});
|