1import * as React from 'react';
2import { 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 [{ height, width }, setLayout] = React.useState({
15    height: 1,
16    width: 1,
17  });
18
19  // TODO(Bacon): In the future we could consider adding `backgroundRepeat: "no-repeat"`. For more
20  // browser support.
21  const linearGradientBackgroundImage = React.useMemo(() => {
22    return getLinearGradientBackgroundImage(colors, locations, startPoint, endPoint, width, height);
23  }, [colors, locations, startPoint, endPoint, width, height]);
24
25  return (
26    <View
27      {...props}
28      style={[
29        props.style,
30        // @ts-ignore: [ts] Property 'backgroundImage' does not exist on type 'ViewStyle'.
31        { backgroundImage: linearGradientBackgroundImage },
32      ]}
33      onLayout={(event) => {
34        const { width, height } = event.nativeEvent.layout;
35
36        setLayout((oldLayout) => {
37          // don't set new layout state unless the layout has actually changed
38          if (width !== oldLayout.width || height !== oldLayout.height) {
39            return { height, width };
40          }
41
42          return oldLayout;
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 output = normalizeColor(color);
107    if (locations && locations[index]) {
108      const location = Math.max(0, Math.min(1, locations[index]));
109      // Convert 0...1 to 0...100
110      const percentage = location * 100;
111      return `${output} ${percentage}%`;
112    }
113    return output;
114  });
115}
116