1import React, { FunctionComponent, useEffect, useState } from 'react';
2import { LayoutRectangle, View } from 'react-native';
3import normalizeColor from 'react-native-web/src/modules/normalizeColor';
4
5type Props = {
6  colors: number[];
7  locations?: number[] | null;
8  startPoint?: Point | null;
9  endPoint?: Point | null;
10  onLayout?: Function;
11} & React.ComponentProps<typeof View>;
12
13type Point = [number, number];
14
15const NativeLinearGradient: FunctionComponent<Props> = ({
16  colors,
17  locations,
18  startPoint,
19  endPoint,
20  ...props
21}: Props) => {
22  const [layout, setLayout] = useState<LayoutRectangle | null>(null);
23  const [gradientColors, setGradientColors] = useState<string[]>([]);
24  const [pseudoAngle, setPseudoAngle] = useState<number>(0);
25
26  const { width = 1, height = 1 } = layout ?? {};
27  useEffect(() => {
28    const getControlPoints = (): Point[] => {
29      let correctedStartPoint: Point = [0, 0];
30      if (Array.isArray(startPoint)) {
31        correctedStartPoint = [
32          startPoint[0] != null ? startPoint[0] : 0.0,
33          startPoint[1] != null ? startPoint[1] : 0.0,
34        ];
35      }
36      let correctedEndPoint: Point = [0.0, 1.0];
37      if (Array.isArray(endPoint)) {
38        correctedEndPoint = [
39          endPoint[0] != null ? endPoint[0] : 0.0,
40          endPoint[1] != null ? endPoint[1] : 1.0,
41        ];
42      }
43      return [correctedStartPoint, correctedEndPoint];
44    };
45
46    const [start, end] = getControlPoints();
47    start[0] *= width;
48    end[0] *= width;
49    start[1] *= height;
50    end[1] *= height;
51    const py = end[1] - start[1];
52    const px = end[0] - start[0];
53
54    setPseudoAngle(90 + (Math.atan2(py, px) * 180) / Math.PI);
55  }, [width, height, startPoint, endPoint]);
56
57  useEffect(() => {
58    const nextGradientColors = colors.map((color: number, index: number): string => {
59      const hexColor = normalizeColor(color);
60      let output = hexColor;
61      if (locations && locations[index]) {
62        const location = Math.max(0, Math.min(1, locations[index]));
63        // Convert 0...1 to 0...100
64        const percentage = location * 100;
65        output += ` ${percentage}%`;
66      }
67      return output;
68    });
69
70    setGradientColors(nextGradientColors);
71  }, [colors, locations]);
72
73  const colorStyle = gradientColors.join(',');
74  const backgroundImage = `linear-gradient(${pseudoAngle}deg, ${colorStyle})`;
75  // TODO: Bacon: In the future we could consider adding `backgroundRepeat: "no-repeat"`. For more
76  // browser support.
77  return (
78    <View
79      {...props}
80      style={[
81        props.style,
82        // @ts-ignore: [ts] Property 'backgroundImage' does not exist on type 'ViewStyle'.
83        { backgroundImage },
84      ]}
85      onLayout={event => {
86        setLayout(event.nativeEvent.layout);
87        if (props.onLayout) {
88          props.onLayout(event);
89        }
90      }}
91    />
92  );
93};
94
95export default NativeLinearGradient;
96