/** * 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 }) }