remotion
Version:
Make videos programmatically
576 lines (575 loc) • 21.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertValidInterpolateEasingOption = assertValidInterpolateEasingOption;
exports.assertValidInterpolatePosterizeOption = assertValidInterpolatePosterizeOption;
exports.interpolate = interpolate;
const normalize_number_js_1 = require("./normalize-number.js");
const angleUnits = new Set(['deg', 'rad', 'grad', 'turn']);
const lengthUnits = new Set([
'%',
'cap',
'ch',
'cm',
'cqb',
'cqh',
'cqi',
'cqmax',
'cqmin',
'cqw',
'dvh',
'dvw',
'em',
'ex',
'ic',
'in',
'lh',
'lvh',
'lvw',
'mm',
'pc',
'pt',
'px',
'q',
'rem',
'rlh',
'svh',
'svw',
'vb',
'vh',
'vi',
'vmax',
'vmin',
'vw',
]);
const cssNumberRegex = /^([+-]?(?:\d+\.?\d*|\.\d+))([a-zA-Z%]+)?$/;
const transformOriginKeywords = new Set([
'left',
'center',
'right',
'top',
'bottom',
]);
const transformOriginKeywordOptions = (keyword) => {
if (keyword === 'left') {
return [{ axis: 'x', value: { value: 0, unit: '%' } }];
}
if (keyword === 'right') {
return [{ axis: 'x', value: { value: 100, unit: '%' } }];
}
if (keyword === 'top') {
return [{ axis: 'y', value: { value: 0, unit: '%' } }];
}
if (keyword === 'bottom') {
return [{ axis: 'y', value: { value: 100, unit: '%' } }];
}
return [
{ axis: 'x', value: { value: 50, unit: '%' } },
{ axis: 'y', value: { value: 50, unit: '%' } },
];
};
const transformOriginCenter = { value: 50, unit: '%' };
const stringifyNumber = (value) => {
return String((0, normalize_number_js_1.normalizeNumber)(value));
};
const parseStringInterpolationComponent = (component, value) => {
var _a;
const match = cssNumberRegex.exec(component);
if (match === null) {
throw new TypeError(`Cannot interpolate "${value}" because "${component}" is not a supported scale, translate, or rotate value`);
}
const unit = (_a = match[2]) !== null && _a !== void 0 ? _a : null;
const numberValue = Number(match[1]);
if (!Number.isFinite(numberValue)) {
throw new TypeError(`Cannot interpolate "${value}" because "${component}" is not finite`);
}
if (unit === null) {
return { kind: 'scale', value: numberValue, unit: null };
}
if (angleUnits.has(unit)) {
return { kind: 'rotate', value: numberValue, unit };
}
if (lengthUnits.has(unit)) {
return { kind: 'translate', value: numberValue, unit };
}
throw new TypeError(`Cannot interpolate "${value}" because "${unit}" is not a supported translate or rotate unit`);
};
const parseTransformOriginLengthPercentage = ({ component, value, allowPercentage, }) => {
var _a;
const match = cssNumberRegex.exec(component);
if (match === null) {
throw new TypeError(`Cannot interpolate "${value}" because "${component}" is not a supported transform-origin ${allowPercentage ? 'length-percentage' : 'z length'}`);
}
const unit = (_a = match[2]) !== null && _a !== void 0 ? _a : null;
const numberValue = Number(match[1]);
if (!Number.isFinite(numberValue)) {
throw new TypeError(`Cannot interpolate "${value}" because "${component}" is not finite`);
}
if (unit === null ||
!lengthUnits.has(unit) ||
(!allowPercentage && unit === '%')) {
throw new TypeError(`Cannot interpolate "${value}" because "${component}" is not a supported transform-origin ${allowPercentage ? 'length-percentage' : 'z length'}`);
}
return { value: numberValue, unit };
};
const parseTransformOriginToken = (component, value) => {
const lower = component.toLowerCase();
if (transformOriginKeywords.has(lower)) {
return { type: 'keyword', keyword: lower };
}
return {
type: 'length-percentage',
parsed: parseTransformOriginLengthPercentage({
component,
value,
allowPercentage: true,
}),
};
};
const parseTwoTransformOriginKeywords = (first, second, value) => {
const candidates = [];
for (const firstOption of transformOriginKeywordOptions(first)) {
for (const secondOption of transformOriginKeywordOptions(second)) {
if (firstOption.axis === secondOption.axis) {
continue;
}
candidates.push(firstOption.axis === 'x'
? [firstOption.value, secondOption.value]
: [secondOption.value, firstOption.value]);
}
}
if (candidates.length === 0) {
throw new TypeError(`Cannot interpolate "${value}" because "${first} ${second}" is not a valid transform-origin keyword pair`);
}
return candidates[0];
};
const parseTransformOriginXY = (parts, value) => {
if (parts.length === 1) {
const token = parseTransformOriginToken(parts[0], value);
if (token.type === 'length-percentage') {
return [token.parsed, transformOriginCenter];
}
if (token.keyword === 'top' || token.keyword === 'bottom') {
return [
transformOriginCenter,
transformOriginKeywordOptions(token.keyword)[0].value,
];
}
return [
transformOriginKeywordOptions(token.keyword)[0].value,
transformOriginCenter,
];
}
const first = parseTransformOriginToken(parts[0], value);
const second = parseTransformOriginToken(parts[1], value);
if (first.type === 'length-percentage' &&
second.type === 'length-percentage') {
return [first.parsed, second.parsed];
}
if (first.type === 'keyword' && second.type === 'keyword') {
return parseTwoTransformOriginKeywords(first.keyword, second.keyword, value);
}
const keyword = first.type === 'keyword'
? first
: second.type === 'keyword'
? second
: null;
const length = first.type === 'length-percentage'
? first.parsed
: second.type === 'length-percentage'
? second.parsed
: null;
if (keyword === null || length === null) {
throw new Error('Expected a keyword and a length-percentage value');
}
const keywordIsFirst = first.type === 'keyword';
if (keyword.keyword === 'left' || keyword.keyword === 'right') {
if (!keywordIsFirst) {
throw new TypeError(`Cannot interpolate "${value}" because horizontal transform-origin keywords must come before a length-percentage value`);
}
return [transformOriginKeywordOptions(keyword.keyword)[0].value, length];
}
if (keyword.keyword === 'top' || keyword.keyword === 'bottom') {
return [length, transformOriginKeywordOptions(keyword.keyword)[0].value];
}
return keywordIsFirst
? [transformOriginCenter, length]
: [length, transformOriginCenter];
};
const parseTransformOriginValue = (output, parts) => {
const [x, y] = parseTransformOriginXY(parts.slice(0, 2), output);
const z = parts[2] === undefined
? { value: 0, unit: null }
: parseTransformOriginLengthPercentage({
component: parts[2],
value: output,
allowPercentage: false,
});
return {
kind: 'translate',
values: [x.value, y.value, z.value],
units: [x.unit, y.unit, z.unit],
dimensions: parts[2] === undefined ? 2 : 3,
};
};
const parseStringInterpolationValue = (output) => {
var _a, _b, _c, _d, _e, _f;
var _g, _h, _j, _k, _l, _m;
if (typeof output === 'number') {
if (!Number.isFinite(output)) {
throw new Error(`outputRange must contain only finite numbers, but got [${output}]`);
}
return {
kind: 'scale',
values: [output, output, 1],
units: [null, null, null],
dimensions: 1,
};
}
const parts = output.trim().split(/\s+/);
if (parts.length < 1 || parts.length > 3 || parts[0] === '') {
throw new TypeError(`String outputRange values must contain 1 to 3 components, but got "${output}"`);
}
if (parts.some((part) => transformOriginKeywords.has(part.toLowerCase()))) {
return parseTransformOriginValue(output, parts);
}
const parsed = parts.map((part) => parseStringInterpolationComponent(part, output));
const [{ kind }] = parsed;
for (const part of parsed) {
if (part.kind !== kind) {
throw new TypeError(`Cannot interpolate "${output}" because it mixes ${kind} and ${part.kind} values`);
}
}
if (kind === 'scale') {
const x = parsed[0].value;
const y = (_g = (_a = parsed[1]) === null || _a === void 0 ? void 0 : _a.value) !== null && _g !== void 0 ? _g : x;
const z = (_h = (_b = parsed[2]) === null || _b === void 0 ? void 0 : _b.value) !== null && _h !== void 0 ? _h : 1;
return {
kind,
values: [x, y, z],
units: [null, null, null],
dimensions: parsed.length,
};
}
return {
kind,
values: [parsed[0].value, (_j = (_c = parsed[1]) === null || _c === void 0 ? void 0 : _c.value) !== null && _j !== void 0 ? _j : 0, (_k = (_d = parsed[2]) === null || _d === void 0 ? void 0 : _d.value) !== null && _k !== void 0 ? _k : 0],
units: [parsed[0].unit, (_l = (_e = parsed[1]) === null || _e === void 0 ? void 0 : _e.unit) !== null && _l !== void 0 ? _l : null, (_m = (_f = parsed[2]) === null || _f === void 0 ? void 0 : _f.unit) !== null && _m !== void 0 ? _m : null],
dimensions: parsed.length,
};
};
const serializeStringInterpolationValue = ({ kind, values, units, dimensions, }) => {
if (kind === 'scale') {
return values
.slice(0, dimensions)
.map((value) => stringifyNumber(value))
.join(' ');
}
return values
.slice(0, dimensions)
.map((value, index) => `${stringifyNumber(value)}${units[index]}`)
.join(' ');
};
function interpolateFunction(input, inputRange, outputRange, options) {
const { extrapolateLeft, extrapolateRight, easing } = options;
let result = input;
const [inputMin, inputMax] = inputRange;
const [outputMin, outputMax] = outputRange;
if (result < inputMin) {
if (extrapolateLeft === 'identity') {
return result;
}
if (extrapolateLeft === 'clamp') {
result = inputMin;
}
else if (extrapolateLeft === 'wrap') {
const range = inputMax - inputMin;
result = ((((result - inputMin) % range) + range) % range) + inputMin;
}
else if (extrapolateLeft === 'extend') {
// Noop
}
}
if (result > inputMax) {
if (extrapolateRight === 'identity') {
return result;
}
if (extrapolateRight === 'clamp') {
result = inputMax;
}
else if (extrapolateRight === 'wrap') {
const range = inputMax - inputMin;
result = ((((result - inputMin) % range) + range) % range) + inputMin;
}
else if (extrapolateRight === 'extend') {
// Noop
}
}
if (outputMin === outputMax) {
return outputMin;
}
// Input Range
result = (result - inputMin) / (inputMax - inputMin);
// Easing
result = easing(result);
// Output Range
result = result * (outputMax - outputMin) + outputMin;
return result;
}
function findRange(input, inputRange) {
let i;
for (i = 1; i < inputRange.length - 1; ++i) {
if (inputRange[i] >= input) {
break;
}
}
return i - 1;
}
const defaultEasing = (num) => num;
const shouldExtendRightForEasing = (easing) => {
return easing.remotionShouldExtendRight === true;
};
const resolveEasingForSegment = ({ easing, segmentIndex, }) => {
if (easing === undefined) {
return defaultEasing;
}
if (typeof easing === 'function') {
return easing;
}
// `segmentIndex` is in [0, inputRange.length - 2]; array length was validated above.
return easing[segmentIndex];
};
const interpolateSegment = ({ input, inputRange, outputRange, easing, extrapolateLeft, extrapolateRight, }) => {
return interpolateFunction(input, inputRange, outputRange, {
easing,
extrapolateLeft,
extrapolateRight: input > inputRange[1] &&
extrapolateRight === 'clamp' &&
shouldExtendRightForEasing(easing)
? 'extend'
: extrapolateRight,
});
};
const interpolateNumber = ({ input, inputRange, outputRange, options, }) => {
if (inputRange.length === 1) {
return outputRange[0];
}
const easingOption = options === null || options === void 0 ? void 0 : options.easing;
let extrapolateLeft = 'extend';
if ((options === null || options === void 0 ? void 0 : options.extrapolateLeft) !== undefined) {
extrapolateLeft = options.extrapolateLeft;
}
let extrapolateRight = 'extend';
if ((options === null || options === void 0 ? void 0 : options.extrapolateRight) !== undefined) {
extrapolateRight = options.extrapolateRight;
}
const posterizedInput = (options === null || options === void 0 ? void 0 : options.posterize) === undefined
? input
: Math.floor(input / options.posterize) * options.posterize;
const range = findRange(posterizedInput, inputRange);
const easing = resolveEasingForSegment({
easing: easingOption,
segmentIndex: range,
});
let result = interpolateSegment({
input: posterizedInput,
inputRange: [inputRange[range], inputRange[range + 1]],
outputRange: [outputRange[range], outputRange[range + 1]],
easing,
extrapolateLeft,
extrapolateRight,
});
for (let segmentIndex = 0; segmentIndex < range; segmentIndex++) {
const previousEasing = resolveEasingForSegment({
easing: easingOption,
segmentIndex,
});
if (!shouldExtendRightForEasing(previousEasing)) {
continue;
}
const previousSegmentEnd = inputRange[segmentIndex + 1];
if (posterizedInput <= previousSegmentEnd) {
continue;
}
const continuedSegmentValue = interpolateSegment({
input: posterizedInput,
inputRange: [inputRange[segmentIndex], previousSegmentEnd],
outputRange: [outputRange[segmentIndex], outputRange[segmentIndex + 1]],
easing: previousEasing,
extrapolateLeft,
extrapolateRight: 'extend',
});
result += continuedSegmentValue - outputRange[segmentIndex + 1];
}
return result;
};
const interpolateString = ({ input, inputRange, outputRange, options, }) => {
var _a;
const parsedOutputRange = outputRange.map(parseStringInterpolationValue);
const kind = (_a = parsedOutputRange[0]) === null || _a === void 0 ? void 0 : _a.kind;
if (kind === undefined) {
throw new Error('outputRange must have at least 1 element');
}
for (const parsed of parsedOutputRange) {
if (parsed.kind !== kind) {
throw new TypeError(`Cannot interpolate ${kind} values with ${parsed.kind} values`);
}
}
const dimensions = Math.max(...parsedOutputRange.map((parsed) => parsed.dimensions));
const units = [
null,
null,
null,
];
if (kind !== 'scale') {
for (let axis = 0; axis < dimensions; axis++) {
for (const parsed of parsedOutputRange) {
const unit = parsed.units[axis];
if (unit === null) {
continue;
}
if (units[axis] === null) {
units[axis] = unit;
continue;
}
if (units[axis] !== unit) {
throw new TypeError(`Cannot interpolate ${kind} values with different units on axis ${axis + 1}: ${units[axis]} and ${unit}`);
}
}
if (units[axis] === null) {
throw new TypeError(`Cannot interpolate ${kind} values because axis ${axis + 1} has no unit`);
}
}
}
return serializeStringInterpolationValue({
kind,
values: [0, 0, 0].map((_, axis) => interpolateNumber({
input,
inputRange,
outputRange: parsedOutputRange.map((parsed) => parsed.values[axis]),
options,
})),
units,
dimensions,
});
};
const validateTupleOutputRange = (outputRange) => {
var _a;
const dimensions = (_a = outputRange[0]) === null || _a === void 0 ? void 0 : _a.length;
if (dimensions === undefined) {
throw new Error('outputRange must have at least 1 element');
}
if (dimensions === 0) {
throw new TypeError('outputRange tuples must contain at least 1 number');
}
for (const output of outputRange) {
if (output.length !== dimensions) {
throw new TypeError(`outputRange tuples must all have the same length, but got ${dimensions} and ${output.length}`);
}
for (const value of output) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new TypeError(`outputRange tuples must contain only finite numbers, but got [${output.join(',')}]`);
}
}
}
return dimensions;
};
const interpolateTuple = ({ input, inputRange, outputRange, options, }) => {
const dimensions = validateTupleOutputRange(outputRange);
return new Array(dimensions).fill(true).map((_, axis) => interpolateNumber({
input,
inputRange,
outputRange: outputRange.map((output) => output[axis]),
options,
}));
};
function checkValidInputRange(arr) {
for (let i = 1; i < arr.length; ++i) {
if (!(arr[i] > arr[i - 1])) {
throw new Error(`inputRange must be strictly monotonically increasing but got [${arr.join(',')}]`);
}
}
}
function checkInfiniteRange(name, arr) {
if (arr.length < 1) {
throw new Error(name + ' must have at least 1 element');
}
for (const element of arr) {
if (typeof element !== 'number') {
throw new Error(`${name} must contain only numbers`);
}
if (!Number.isFinite(element)) {
throw new Error(`${name} must contain only finite numbers, but got [${arr.join(',')}]`);
}
}
}
function assertValidInterpolateEasingOption(easing, inputRangeLength) {
if (easing === undefined) {
return;
}
if (typeof easing === 'function') {
return;
}
const expectedLength = inputRangeLength - 1;
if (easing.length !== expectedLength) {
throw new Error(`When easing is an array, it must have one entry per segment between keyframes (length inputRange.length - 1 = ${expectedLength}), but got length ${easing.length}`);
}
for (let i = 0; i < easing.length; i++) {
if (typeof easing[i] !== 'function') {
throw new Error(`easing[${i}] must be a function`);
}
}
}
function assertValidInterpolatePosterizeOption(posterize) {
if (posterize === undefined) {
return;
}
if (typeof posterize !== 'number' ||
!Number.isFinite(posterize) ||
posterize <= 0) {
throw new Error(`posterize must be a positive finite number, but got ${posterize}`);
}
}
function interpolate(input, inputRange, outputRange, options) {
if (typeof input === 'undefined') {
throw new Error('input can not be undefined');
}
if (typeof inputRange === 'undefined') {
throw new Error('inputRange can not be undefined');
}
if (typeof outputRange === 'undefined') {
throw new Error('outputRange can not be undefined');
}
if (inputRange.length !== outputRange.length) {
throw new Error('inputRange (' +
inputRange.length +
') and outputRange (' +
outputRange.length +
') must have the same length');
}
checkInfiniteRange('inputRange', inputRange);
checkValidInputRange(inputRange);
assertValidInterpolateEasingOption(options === null || options === void 0 ? void 0 : options.easing, inputRange.length);
assertValidInterpolatePosterizeOption(options === null || options === void 0 ? void 0 : options.posterize);
if (typeof input !== 'number') {
throw new TypeError('Cannot interpolate an input which is not a number');
}
if (!Array.isArray(outputRange)) {
throw new Error('outputRange must contain only numbers');
}
const hasStringOutput = outputRange.some((output) => typeof output === 'string');
if (hasStringOutput) {
if (!outputRange.every((output) => typeof output === 'string' || typeof output === 'number')) {
throw new TypeError('outputRange must contain only numbers, or supported scale, translate, and rotate strings');
}
return interpolateString({ input, inputRange, outputRange, options });
}
if (outputRange.every((output) => Array.isArray(output))) {
return interpolateTuple({ input, inputRange, outputRange, options });
}
if (!outputRange.every((output) => typeof output === 'number')) {
throw new TypeError('outputRange must contain only numbers, numeric tuples, or supported scale, translate, and rotate strings');
}
checkInfiniteRange('outputRange', outputRange);
return interpolateNumber({ input, inputRange, outputRange, options });
}
/* eslint-enable no-redeclare */