UNPKG

remotion

Version:

Make videos programmatically

576 lines (575 loc) • 21.8 kB
"use strict"; 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 */