react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
279 lines (239 loc) • 6.9 kB
text/typescript
;
import { ReanimatedError, type ValueProcessor } from '../../../../common';
import {
arcToCubic,
lineToCubic,
NUMBER_PATTERN,
PATH_COMMAND_LENGTHS,
quadraticToCubic,
reflectControlPoint,
SEGMENT_PATTERN,
} from '../../../utils';
export const ERROR_MESSAGES = {
invalidSVGPathCommand: (command: unknown, args: unknown) =>
`Invalid SVG Path command: ${JSON.stringify(command)} ${JSON.stringify(args)}`,
invalidSVGPathStart: (command: string) =>
`Invalid SVG Path: Path must start with "M" or "m", but found "${command}"`,
};
type PathCommand = [string, ...number[]];
export const processSVGPath = ((d) => {
let pathSegments: PathCommand[] = parsePathString(d);
pathSegments = normalizePath(pathSegments);
return pathSegments.flatMap((subArr) => subArr).join(' ');
}) satisfies ValueProcessor<string, string>;
function processPathSegment(
command: string,
args: number[],
pathSegments: PathCommand[]
): void {
let type = command.toLowerCase();
if (pathSegments.length === 0 && type !== 'm') {
throw new ReanimatedError(ERROR_MESSAGES.invalidSVGPathStart(command));
}
let argsStartIdx = 0;
if (type === 'm' && args.length > PATH_COMMAND_LENGTHS['m']) {
pathSegments.push([command, ...args.slice(0, PATH_COMMAND_LENGTHS['m'])]);
argsStartIdx += PATH_COMMAND_LENGTHS['m'];
type = 'l'; // If m has more than 2 (length['m']) arguments, use them in implicit l commands
command = command === 'm' ? 'l' : 'L';
}
do {
if (args.length - argsStartIdx < PATH_COMMAND_LENGTHS[type]) {
throw new ReanimatedError(
ERROR_MESSAGES.invalidSVGPathCommand(command, args.slice(argsStartIdx))
);
}
pathSegments.push([
command,
...args.slice(argsStartIdx, argsStartIdx + PATH_COMMAND_LENGTHS[type]),
]);
argsStartIdx += PATH_COMMAND_LENGTHS[type];
} while (args.length - argsStartIdx !== 0);
}
function parsePathString(d: string): PathCommand[] {
const pathSegments: PathCommand[] = [];
d.replace(SEGMENT_PATTERN, (_, command: string, argsString: string) => {
const numbers = argsString.match(NUMBER_PATTERN);
const args = numbers ? numbers.map(Number) : [];
processPathSegment(command, args, pathSegments);
return '';
});
return pathSegments;
}
function normalizePath(pathSegments: PathCommand[]): PathCommand[] {
const absoluteSegments = absolutizePath(pathSegments);
const out: PathCommand[] = [];
const state: PathState = {
curX: 0,
curY: 0,
startX: 0,
startY: 0,
lastCubicCtrlX: 0,
lastCubicCtrlY: 0,
lastQuadCtrlX: 0,
lastQuadCtrlY: 0,
};
for (const seg of absoluteSegments) {
let cmd = seg[0];
let args = [...seg.slice(1)] as number[];
if (cmd === 'S') {
const [rX, rY] = reflectControlPoint(
state.curX,
state.curY,
state.lastCubicCtrlX,
state.lastCubicCtrlY
);
cmd = 'C';
args = [rX, rY, args[0], args[1], args[2], args[3]];
} else if (cmd === 'T') {
const [rX, rY] = reflectControlPoint(
state.curX,
state.curY,
state.lastQuadCtrlX,
state.lastQuadCtrlY
);
cmd = 'Q';
args = [rX, rY, args[0], args[1]];
}
if (cmd === 'H') {
cmd = 'L';
args = [args[0], state.curY];
}
if (cmd === 'V') {
cmd = 'L';
args = [state.curX, args[0]];
}
const result = handlers[cmd as keyof typeof handlers](state, args);
out.push(...result);
const isCubic = cmd === 'C';
const isQuad = cmd === 'Q';
if (!isCubic) {
state.lastCubicCtrlX = state.curX;
state.lastCubicCtrlY = state.curY;
}
if (!isQuad) {
state.lastQuadCtrlX = state.curX;
state.lastQuadCtrlY = state.curY;
}
}
return out;
}
function absolutizePath(pathSegments: PathCommand[]): PathCommand[] {
let curX = 0,
curY = 0;
let startX = 0,
startY = 0;
return pathSegments.map((seg) => {
const cmd = seg[0];
const upperCmd = cmd.toUpperCase();
const isRelative = cmd != upperCmd;
const args = seg.slice(1) as number[];
if (isRelative) {
if (upperCmd === 'A') {
args[5] += curX;
args[6] += curY;
} else if (upperCmd === 'V') {
args[0] += curY;
} else if (upperCmd === 'H') {
args[0] += curX;
} else {
for (let i = 0; i < args.length; i += 2) {
args[i] += curX;
args[i + 1] += curY;
}
}
}
switch (upperCmd) {
case 'Z':
curX = startX;
curY = startY;
break;
case 'H':
curX = args[0];
break;
case 'V':
curY = args[0];
break;
case 'M':
curX = args[0];
curY = args[1];
startX = curX;
startY = curY;
break;
default:
// For L, C, S, Q, T, A the last pair is the new cursor
if (args.length >= 2) {
curX = args[args.length - 2];
curY = args[args.length - 1];
}
}
return [upperCmd, ...args];
});
}
type PathState = {
curX: number;
curY: number;
startX: number;
startY: number;
lastCubicCtrlX: number;
lastCubicCtrlY: number;
lastQuadCtrlX: number;
lastQuadCtrlY: number;
};
const handlers = {
M: (state: PathState, args: number[]): PathCommand[] => {
const [x, y] = args;
state.curX = state.startX = x;
state.curY = state.startY = y;
return [['M', x, y]];
},
L: (state: PathState, args: number[]): PathCommand[] => {
const [x, y] = args;
const cubic = lineToCubic(state.curX, state.curY, x, y);
state.curX = x;
state.curY = y;
return [['C', ...cubic]];
},
Q: (state: PathState, args: number[]): PathCommand[] => {
const [qcx, qcy, x, y] = args;
const cubic = quadraticToCubic(state.curX, state.curY, qcx, qcy, x, y);
state.lastQuadCtrlX = qcx;
state.lastQuadCtrlY = qcy;
state.curX = x;
state.curY = y;
return [['C', ...cubic]];
},
C: (state: PathState, args: number[]): PathCommand[] => {
const [_cp1x, _cp1y, cp2x, cp2y, x, y] = args;
state.lastCubicCtrlX = cp2x;
state.lastCubicCtrlY = cp2y;
state.curX = x;
state.curY = y;
return [['C', ...args]];
},
A: (state: PathState, args: number[]): PathCommand[] => {
const cubics = arcToCubic(
state.curX,
state.curY,
args[0],
args[1],
args[2],
args[3],
args[4],
args[5],
args[6]
);
if (cubics.length === 0) return [];
const last = cubics[cubics.length - 1];
state.lastCubicCtrlX = last[2];
state.lastCubicCtrlY = last[3];
state.curX = last[4];
state.curY = last[5];
return cubics.map((c) => ['C', ...c]);
},
Z: (state: PathState): PathCommand[] => {
state.curX = state.startX;
state.curY = state.startY;
return [['Z']];
},
};