1import * as React from 'react';
2import { LayoutRectangle, View } from 'react-native';
3
4import { NativeLinearGradientPoint, NativeLinearGradientProps } from './NativeLinearGradient.types';
5import { normalizeColor } from './normalizeColor';
6
7export default function NativeLinearGradient({
8  colors,
9  locations,
10  startPoint,
11  endPoint,
12  ...props
13}: NativeLinearGradientProps): React.ReactElement {
14  const [layout, setLayout] = React.useState<LayoutRectangle | null>(null);
15
16  const { width = 1, height = 1 } = layout ?? {};
17
18  // TODO(Bacon): In the future we could consider adding `backgroundRepeat: "no-repeat"`. For more
19  // browser support.
20  const linearGradientBackgroundImage = React.useMemo(() => {
21    return getLinearGradientBackgroundImage(colors, locations, startPoint, endPoint, width, height);
22  }, [colors, locations, startPoint, endPoint, width, height]);
23
24  return (
25    <View
26      {...props}
27      style={[
28        props.style,
29        // @ts-ignore: [ts] Property 'backgroundImage' does not exist on type 'ViewStyle'.
30        { backgroundImage: linearGradientBackgroundImage },
31      ]}
32      onLayout={(event) => {
33        const { x, y, width, height } = event.nativeEvent.layout;
34        const oldLayout = layout ?? { x: 0, y: 0, width: 1, height: 1 };
35        // don't set new layout state unless the layout has actually changed
36        if (
37          x !== oldLayout.x ||
38          y !== oldLayout.y ||
39          width !== oldLayout.width ||
40          height !== oldLayout.height
41        ) {
42          setLayout({ x, y, width, height });
43        }
44
45        if (props.onLayout) {
46          props.onLayout(event);
47        }
48      }}
49    />
50  );
51}
52
53/**
54 * Extracted to a separate function in order to be able to test logic independently.
55 */
56export function getLinearGradientBackgroundImage(
57  colors: number[] | string[],
58  locations?: number[] | null,
59  startPoint?: NativeLinearGradientPoint | null,
60  endPoint?: NativeLinearGradientPoint | null,
61  width: number = 1,
62  height: number = 1
63) {
64  const gradientColors = calculateGradientColors(colors, locations);
65  const angle = calculatePseudoAngle(width, height, startPoint, endPoint);
66  return `linear-gradient(${angle}deg, ${gradientColors.join(', ')})`;
67}
68
69function calculatePseudoAngle(
70  width: number,
71  height: number,
72  startPoint?: NativeLinearGradientPoint | null,
73  endPoint?: NativeLinearGradientPoint | null
74) {
75  const getControlPoints = (): NativeLinearGradientPoint[] => {
76    let correctedStartPoint: NativeLinearGradientPoint = [0, 0];
77    if (Array.isArray(startPoint)) {
78      correctedStartPoint = [
79        startPoint[0] != null ? startPoint[0] : 0.0,
80        startPoint[1] != null ? startPoint[1] : 0.0,
81      ];
82    }
83    let correctedEndPoint: NativeLinearGradientPoint = [0.0, 1.0];
84    if (Array.isArray(endPoint)) {
85      correctedEndPoint = [
86        endPoint[0] != null ? endPoint[0] : 0.0,
87        endPoint[1] != null ? endPoint[1] : 1.0,
88      ];
89    }
90    return [correctedStartPoint, correctedEndPoint];
91  };
92
93  const [start, end] = getControlPoints();
94  start[0] *= width;
95  end[0] *= width;
96  start[1] *= height;
97  end[1] *= height;
98  const py = end[1] - start[1];
99  const px = end[0] - start[0];
100
101  return 90 + (Math.atan2(py, px) * 180) / Math.PI;
102}
103
104function calculateGradientColors(colors: number[] | string[], locations?: number[] | null) {
105  return colors.map((color: number | string, index: number): string | void => {
106    const hexColor = normalizeColor(color);
107    let output = hexColor;
108    if (locations && locations[index]) {
109      const location = Math.max(0, Math.min(1, locations[index]));
110      // Convert 0...1 to 0...100
111      const percentage = location * 100;
112      output += ` ${percentage}%`;
113    }
114    return output;
115  });
116}
117