react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
581 lines (557 loc) • 15.4 kB
JavaScript
'use strict';
import { CSS_NUMBER_PATTERN, getAngleInDegrees, isPercentage, splitByComma, splitByWhitespace } from "../../utils/index.js";
import { processColor, processColorNumber } from "./colors.js";
const NEWLINE_REGEX = /\n/g;
const WHITESPACE_NORMALIZE_REGEX = /\s+/g;
const GRADIENT_REGEX = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
const COLOR_STOP_PARTS_REGEX = /\S+\([^)]*\)|\S+/g;
const PX_LENGTH_REGEX = new RegExp(`^${CSS_NUMBER_PATTERN}px$`);
const LINEAR_GRADIENT_DIRECTION_REGEX = /^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/;
const DEFAULT_DIRECTION = {
type: 'angle',
value: 180
};
const DEFAULT_RADIAL_SHAPE = 'ellipse';
const DEFAULT_RADIAL_SIZE = 'farthest-corner';
const DEFAULT_RADIAL_POSITION = {
top: '50%',
left: '50%'
};
const MIN_COLOR_STOPS = 2;
const RADIAL_SIZE_KEYWORDS = ['closest-side', 'closest-corner', 'farthest-side', 'farthest-corner'];
export const ERROR_MESSAGES = {
invalidDirection(direction) {
'worklet';
return `Invalid direction "${direction}" in background image.`;
},
invalidPosition(position) {
'worklet';
return `Invalid position "${String(position)}" in background image color stop.`;
},
invalidShape(shape) {
'worklet';
return `Invalid shape "${shape}" in radial gradient.`;
},
invalidSize(size) {
'worklet';
return `Invalid size ${JSON.stringify(size)} in radial gradient.`;
},
invalidTransitionHint(position) {
'worklet';
return `Invalid transition hint "${String(position)}" in background image: a hint must be placed between two color stops.`;
},
notEnoughColorStops(count) {
'worklet';
return `Background image gradient must have at least two color stops, got ${count}.`;
}
};
const getDirectionForKeyword = direction => {
'worklet';
switch (direction.replace(WHITESPACE_NORMALIZE_REGEX, ' ')) {
case 'to top':
return {
type: 'angle',
value: 0
};
case 'to right':
return {
type: 'angle',
value: 90
};
case 'to bottom':
return {
type: 'angle',
value: 180
};
case 'to left':
return {
type: 'angle',
value: 270
};
case 'to top right':
case 'to right top':
return {
type: 'keyword',
value: 'to top right'
};
case 'to bottom right':
case 'to right bottom':
return {
type: 'keyword',
value: 'to bottom right'
};
case 'to top left':
case 'to left top':
return {
type: 'keyword',
value: 'to top left'
};
case 'to bottom left':
case 'to left bottom':
return {
type: 'keyword',
value: 'to bottom left'
};
default:
return null;
}
};
const parseDirection = direction => {
'worklet';
const normalized = direction.toLowerCase();
const angle = getAngleInDegrees(normalized);
if (angle !== null) {
return {
type: 'angle',
value: angle
};
}
return getDirectionForKeyword(normalized);
};
const processDirection = direction => {
'worklet';
if (direction == null) {
return DEFAULT_DIRECTION;
}
const parsed = parseDirection(direction);
if (parsed === null) {
throw new Error(`[Reanimated] ${ERROR_MESSAGES.invalidDirection(direction)}`);
}
return parsed;
};
const isValidPosition = position => {
'worklet';
return typeof position === 'number' || isPercentage(position);
};
const processColorStops = (colorStops, context) => {
'worklet';
const result = [];
for (const colorStop of colorStops) {
const {
color,
positions
} = colorStop;
if (color == null && positions?.length === 1) {
if (!isValidPosition(positions[0])) {
throw new Error(`[Reanimated] ${ERROR_MESSAGES.invalidPosition(positions[0])}`);
}
result.push({
color: null,
position: positions[0]
});
continue;
}
const processedColor = processColor(color, context);
if (!positions?.length) {
result.push({
color: processedColor,
position: null
});
continue;
}
for (const position of positions) {
if (!isValidPosition(position)) {
throw new Error(`[Reanimated] ${ERROR_MESSAGES.invalidPosition(position)}`);
}
result.push({
color: processedColor,
position
});
}
}
for (let i = 0; i < result.length; i++) {
const {
color,
position
} = result[i];
if (color === null && (i === 0 || i === result.length - 1 || result[i - 1].color === null)) {
throw new Error(`[Reanimated] ${ERROR_MESSAGES.invalidTransitionHint(position)}`);
}
}
if (result.length < MIN_COLOR_STOPS) {
throw new Error(`[Reanimated] ${ERROR_MESSAGES.notEnoughColorStops(result.length)}`);
}
return result;
};
const processRadialSize = size => {
'worklet';
if (size == null) {
return DEFAULT_RADIAL_SIZE;
}
if (typeof size === 'string' && RADIAL_SIZE_KEYWORDS.includes(size)) {
return size;
}
if (typeof size === 'object' && size.x != null && size.y != null) {
return {
x: size.x,
y: size.y
};
}
throw new Error(`[Reanimated] ${ERROR_MESSAGES.invalidSize(size)}`);
};
const processRadialShape = shape => {
'worklet';
if (shape == null) {
return DEFAULT_RADIAL_SHAPE;
}
if (shape !== 'circle' && shape !== 'ellipse') {
throw new Error(`[Reanimated] ${ERROR_MESSAGES.invalidShape(shape)}`);
}
return shape;
};
const getPositionFromCSSValue = value => {
'worklet';
if (isPercentage(value)) {
return value;
}
if (PX_LENGTH_REGEX.test(value)) {
return parseFloat(value);
}
return null;
};
const parseCSSColor = (color, context) => {
'worklet';
if (processColorNumber(color) === null) {
return null;
}
return processColor(color, context);
};
const isLengthOrPercentageToken = token => {
'worklet';
return token.endsWith('px') || token.endsWith('%');
};
const parseColorStopsCSSString = (parts, context) => {
'worklet';
const stops = splitByComma(parts.join(','));
const result = [];
let previousWasHint = false;
for (let i = 0; i < stops.length; i++) {
const colorStopParts = stops[i].trim().match(COLOR_STOP_PARTS_REGEX);
if (colorStopParts === null) {
return null;
}
if (colorStopParts.length === 3) {
const color = parseCSSColor(colorStopParts[0], context);
const position1 = getPositionFromCSSValue(colorStopParts[1]);
const position2 = getPositionFromCSSValue(colorStopParts[2]);
if (color === null || position1 === null || position2 === null) {
return null;
}
result.push({
color,
position: position1
});
result.push({
color,
position: position2
});
previousWasHint = false;
} else if (colorStopParts.length === 2) {
const color = parseCSSColor(colorStopParts[0], context);
const position = getPositionFromCSSValue(colorStopParts[1]);
if (color === null || position === null) {
return null;
}
result.push({
color,
position
});
previousWasHint = false;
} else if (colorStopParts.length === 1) {
const position = getPositionFromCSSValue(colorStopParts[0]);
if (position !== null) {
if (previousWasHint || i === 0 || i === stops.length - 1) {
return null;
}
result.push({
color: null,
position
});
previousWasHint = true;
} else {
const color = parseCSSColor(colorStopParts[0], context);
if (color === null) {
return null;
}
result.push({
color,
position: null
});
previousWasHint = false;
}
} else {
return null;
}
}
if (result.length < MIN_COLOR_STOPS) {
return null;
}
return result;
};
const parseLinearGradientCSSString = (content, context) => {
'worklet';
const parts = content.split(',');
const firstPart = parts[0].trim();
let direction = DEFAULT_DIRECTION;
if (getAngleInDegrees(firstPart) !== null || LINEAR_GRADIENT_DIRECTION_REGEX.test(firstPart)) {
const parsed = parseDirection(firstPart);
if (parsed === null) {
return null;
}
direction = parsed;
parts.shift();
}
const colorStops = parseColorStopsCSSString(parts, context);
if (colorStops === null) {
return null;
}
return {
type: 'linear-gradient',
direction,
colorStops
};
};
const HORIZONTAL_POSITION_KEYWORDS = {
left: '0%',
center: '50%',
right: '100%'
};
const VERTICAL_POSITION_KEYWORDS = {
top: '0%',
center: '50%',
bottom: '100%'
};
const getKeywordPosition = (keywords, token) => {
'worklet';
return Object.prototype.hasOwnProperty.call(keywords, token) ? keywords[token] : null;
};
const parseRadialPosition = tokens => {
'worklet';
let top;
let left;
let right;
let bottom;
if (tokens.length === 1) {
const token = tokens[0];
const horizontal = getKeywordPosition(HORIZONTAL_POSITION_KEYWORDS, token);
const vertical = getKeywordPosition(VERTICAL_POSITION_KEYWORDS, token);
if (horizontal !== null) {
left = horizontal;
top = '50%';
} else if (vertical !== null) {
left = '50%';
top = vertical;
} else if (isLengthOrPercentageToken(token)) {
const value = getPositionFromCSSValue(token);
if (value === null) {
return null;
}
left = value;
top = '50%';
}
} else if (tokens.length === 2) {
const [token1, token2] = tokens;
const horizontal1 = getKeywordPosition(HORIZONTAL_POSITION_KEYWORDS, token1);
const vertical1 = getKeywordPosition(VERTICAL_POSITION_KEYWORDS, token1);
const horizontal2 = getKeywordPosition(HORIZONTAL_POSITION_KEYWORDS, token2);
const vertical2 = getKeywordPosition(VERTICAL_POSITION_KEYWORDS, token2);
if (horizontal1 !== null && vertical2 !== null) {
left = horizontal1;
top = vertical2;
} else if (vertical1 !== null && horizontal2 !== null) {
left = horizontal2;
top = vertical1;
} else {
if (horizontal1 !== null) {
left = horizontal1;
} else if (isLengthOrPercentageToken(token1)) {
const value = getPositionFromCSSValue(token1);
if (value === null) {
return null;
}
left = value;
} else {
return null;
}
if (vertical2 !== null) {
top = vertical2;
} else if (isLengthOrPercentageToken(token2)) {
const value = getPositionFromCSSValue(token2);
if (value === null) {
return null;
}
top = value;
} else {
return null;
}
}
} else if (tokens.length === 4) {
for (const [keyword, rawValue] of [[tokens[0], tokens[1]], [tokens[2], tokens[3]]]) {
const value = getPositionFromCSSValue(rawValue);
if (value === null) {
return null;
}
if (keyword === 'left') {
left = value;
} else if (keyword === 'right') {
right = value;
} else if (keyword === 'top') {
top = value;
} else if (keyword === 'bottom') {
bottom = value;
} else {
return null;
}
}
}
if (top != null && left != null) {
return {
top,
left
};
}
if (bottom != null && right != null) {
return {
bottom,
right
};
}
if (top != null && right != null) {
return {
top,
right
};
}
if (bottom != null && left != null) {
return {
bottom,
left
};
}
return null;
};
const parseRadialGradientCSSString = (content, context) => {
'worklet';
let shape = DEFAULT_RADIAL_SHAPE;
let size = DEFAULT_RADIAL_SIZE;
let position = {
...DEFAULT_RADIAL_POSITION
};
const parts = splitByComma(content);
const tokens = splitByWhitespace(parts[0]);
let hasShapeSizeOrPosition = false;
let hasExplicitSingleSize = false;
let hasExplicitShape = false;
while (tokens.length > 0) {
const token = tokens.shift();
if (token === 'circle' || token === 'ellipse') {
shape = token;
hasShapeSizeOrPosition = true;
hasExplicitShape = true;
} else if (RADIAL_SIZE_KEYWORDS.includes(token)) {
size = token;
hasShapeSizeOrPosition = true;
} else if (isLengthOrPercentageToken(token)) {
const sizeX = getPositionFromCSSValue(token);
if (sizeX === null || parseFloat(token) < 0) {
return null;
}
hasShapeSizeOrPosition = true;
size = {
x: sizeX,
y: sizeX
};
const nextToken = tokens[0];
if (nextToken !== undefined && isLengthOrPercentageToken(nextToken)) {
tokens.shift();
const sizeY = getPositionFromCSSValue(nextToken);
if (sizeY === null || parseFloat(nextToken) < 0) {
return null;
}
size = {
x: sizeX,
y: sizeY
};
} else {
hasExplicitSingleSize = true;
}
} else if (token === 'at') {
hasShapeSizeOrPosition = true;
const parsedPosition = parseRadialPosition(tokens.splice(0));
if (parsedPosition === null) {
return null;
}
position = parsedPosition;
}
if (!hasShapeSizeOrPosition) {
break;
}
}
if (hasShapeSizeOrPosition) {
parts.shift();
if (!hasExplicitShape && hasExplicitSingleSize) {
shape = 'circle';
}
if (hasExplicitSingleSize && hasExplicitShape && shape === 'ellipse') {
return null;
}
}
const colorStops = parseColorStopsCSSString(parts, context);
if (colorStops === null) {
return null;
}
return {
type: 'radial-gradient',
shape,
size,
position,
colorStops
};
};
const parseBackgroundImageCSSString = (value, context) => {
'worklet';
const result = [];
for (const gradientString of splitByComma(value.replace(NEWLINE_REGEX, ' '))) {
const match = GRADIENT_REGEX.exec(gradientString.toLowerCase());
if (!match) {
continue;
}
const [, type, content] = match;
const gradient = type === 'radial' ? parseRadialGradientCSSString(content, context) : parseLinearGradientCSSString(content, context);
if (gradient !== null) {
result.push(gradient);
}
}
return result;
};
export const processBackgroundImage = (value, context) => {
'worklet';
if (typeof value === 'string') {
return parseBackgroundImageCSSString(value, context);
}
if (!Array.isArray(value)) {
return;
}
const result = [];
for (const backgroundImage of value) {
if (backgroundImage.type === 'linear-gradient') {
result.push({
type: 'linear-gradient',
direction: processDirection(backgroundImage.direction),
colorStops: processColorStops(backgroundImage.colorStops, context)
});
} else if (backgroundImage.type === 'radial-gradient') {
result.push({
type: 'radial-gradient',
shape: processRadialShape(backgroundImage.shape),
size: processRadialSize(backgroundImage.size),
position: backgroundImage.position ?? {
...DEFAULT_RADIAL_POSITION
},
colorStops: processColorStops(backgroundImage.colorStops, context)
});
}
}
return result;
};
//# sourceMappingURL=backgroundImage.js.map