/**
* Adapted from expo-linear-gradient
* https://github.com/expo/expo/blob/main/packages/expo-linear-gradient/src/LinearGradient.web.tsx
*
* MIT License
* Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
*/
import { normalizeColor } from '@tamagui/core'
import type {
LinearGradientPoint,
LinearGradientProps,
NativeLinearGradientPoint,
} from 'expo-linear-gradient'
export type { LinearGradientPoint, LinearGradientProps } from 'expo-linear-gradient'
import * as React from 'react'
import { View } from 'react-native'
// check if start/end points require dimension-aware angle calculation
function needsDimensionAwareAngle(
start?: NativeLinearGradientPoint | null,
end?: NativeLinearGradientPoint | null
): boolean {
// default is top-to-bottom: start [0.5, 0] to end [0.5, 1]
// for this case (and any case where x values are equal), width doesn't matter
// we only need onLayout when x values differ AND could create non-standard angles
if (!start && !end) return false
const startX = start?.[0] ?? 0.5
const endX = end?.[0] ?? 0.5
// if x coordinates are different, angle depends on aspect ratio
return startX !== endX
}
export function LinearGradient({
colors,
locations,
start,
end,
...props
}: LinearGradientProps) {
const normalizedStart = start
? Array.isArray(start)
? start
: [start.x, start.y]
: undefined
const normalizedEnd = end ? (Array.isArray(end) ? end : [end.x, end.y]) : undefined
const needsLayout = needsDimensionAwareAngle(
normalizedStart as NativeLinearGradientPoint,
normalizedEnd as NativeLinearGradientPoint
)
const [{ height, width }, setLayout] = React.useState({
height: 1,
width: 1,
})
const linearGradientBackgroundImage = React.useMemo(() => {
return getLinearGradientBackgroundImage(
// @ts-expect-error ok
colors,
locations,
normalizedStart as NativeLinearGradientPoint,
normalizedEnd as NativeLinearGradientPoint,
width,
height
)
}, [colors, locations, normalizedStart, normalizedEnd, width, height])
// if we don't need dimension-aware angles, skip the onLayout overhead
if (!needsLayout) {
return (
)
}
return (
{
const { width, height } = event.nativeEvent.layout
setLayout((oldLayout) => {
// don't set new layout state unless the layout has actually changed
if (width !== oldLayout.width || height !== oldLayout.height) {
return { height, width }
}
return oldLayout
})
if (props.onLayout) {
props.onLayout(event)
}
}}
/>
)
}
function getLinearGradientBackgroundImage(
colors: number[] | string[],
locations?: number[] | null,
startPoint?: NativeLinearGradientPoint | null,
endPoint?: NativeLinearGradientPoint | null,
width = 1,
height = 1
) {
const gradientColors = calculateGradientColors(
// @ts-expect-error TODO fix numbers
colors,
locations
)
const angle = calculatePseudoAngle(width, height, startPoint, endPoint)
return `linear-gradient(${angle}deg, ${gradientColors.join(', ')})`
}
function calculatePseudoAngle(
width: number,
height: number,
startPoint?: NativeLinearGradientPoint | null,
endPoint?: NativeLinearGradientPoint | null
) {
const getControlPoints = () => {
let correctedStartPoint: LinearGradientPoint = [0, 0]
if (Array.isArray(startPoint)) {
correctedStartPoint = [
startPoint[0] != null ? startPoint[0] : 0.0,
startPoint[1] != null ? startPoint[1] : 0.0,
]
}
let correctedEndPoint: LinearGradientPoint = [0.0, 1.0]
if (Array.isArray(endPoint)) {
correctedEndPoint = [
endPoint[0] != null ? endPoint[0] : 0.0,
endPoint[1] != null ? endPoint[1] : 1.0,
]
}
return [correctedStartPoint, correctedEndPoint] as const
}
const [start, end] = getControlPoints()
start[0] *= width
end[0] *= width
start[1] *= height
end[1] *= height
const py = end[1] - start[1]
const px = end[0] - start[0]
return 90 + (Math.atan2(py, px) * 180) / Math.PI
}
function calculateGradientColors(colors: string[], locations?: number[] | null) {
return colors.map((color: string, index: number): string | void => {
const output = normalizeColor(color)
if (locations && locations[index] !== undefined) {
const location = Math.max(0, Math.min(1, locations[index]))
// Convert 0...1 to 0...100
const percentage = location * 100
return `${output} ${percentage}%`
}
return output
})
}