Глобальные переменные и графики функций в выражениях After Effects

Глобальные переменные и графики функций в выражениях After Effects

Статья в Notion, с ссылками на документацию. Эта статья своего рода продолжение о маркерах. Я не мог вас оставить без гибкости.

При создании анимации через выражение быстро наскучивает создавать однотипные linear и ease, а ничего другого больше то и нет. Это, пожалуй, один из главных для меня недостатков в сравнении с ключами анимации. У них всё просто, открыл график и настроил усики как заблагорассудиться — «Хочу такую же гибкость». Создавать свои функции анимации сложно и долго. Но к счастью этот вопрос уже решен так или иначе.

Нам известно, что кроме линейной и плавной функций существует также Кривая Безье (Cubic Bezier), которая не реализована по умолчанию, но её же уже кто-то до нас написал. Так вот в ожидании, когда в Adobe наконец разродятся её интеграцией для выражений мы можем уже сейчас её адаптировать для своих нужд.

Спойлер, код там большой и занимает не один десяток строк, столько кода в каждое выражение вставлять было бы полным безрассудством. Потому для этой функции мы создадим глобальную переменную а-ля ООП, ну почти, не ругайтесь. Переменную, которая будет работать по всему документу, а написана в одном месте.

💡 Если вдруг выдает ошибку, а вы все скопировали 1 к 1, то у вас в настройках проекта стоит устаревший Extender Script, нужно поменять на Java Script.

Давайте начнем.

Глобальная переменная. Создайте композицию и назовите её «Переменные». Теперь создайте текстовый слой переименовав его так как будете вызывать в других композициях, ведь вы можете это делать для любых целей, а не только для функции Кривой Безье. Я называю слой cubic-bezier_function.

Следующий код мы будем вписывать для вызова функции, соответственно в другой композиции. Функция eval() преобразует содержимое текста в данные которые будут работать из того далекого места в нужном нам месте.

// Глобальная переменная через текстовый слой eval(comp('Переменные').layer("cubic-bezier_function").text.sourceText)

С помощью метода eval() строку можно преобразовать в массив объектов sourceData, идентичный результатам атрибута sourceData, из которого к отдельным потокам данных можно обращаться как к иерархическим атрибутам данных. Footage — Footage.sourceText (docsforadobe.dev)

Небольшое видео процесса. Создаю две композиции, в одной будут глобальные переменные, в другой они будут вызываться.

Кривая Безье. А вот и та громадная функция, вписываем её в тот текстовый слой, что создавали ранее прямо в текст в коне композиции. Размер, шрифт и другие атрибуты не имеют значения. Или в маркер если вызываете его.

💡 Если впишите как код для свойства «Исходный текст», выдаст ошибку.

////////////// CUBIC-BEZIER НАЧАЛО ФУНКЦИИ // Эти значения устанавливаются эмпирическим путем с помощью тестов (компромисс: производительность или точность) var NEWTON_ITERATIONS = 4; var NEWTON_MIN_SLOPE = 0.001; var SUBDIVISION_PRECISION = 0.0000001; var SUBDIVISION_MAX_ITERATIONS = 10; var kSplineTableSize = 11; var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); var float32ArraySupported = false; function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } function C (aA1) { return 3.0 * aA1; } // Возвращается x(t) дано t, x1, и x2, или y(t) дано t, y1, и y2. function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT; } // Возвращается dx/dt дано t, x1, и x2, или dy/dt дано t, y1, и y2. function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } function binarySubdivide (aX, aA, aB) { var currentX, currentT, i = 0; do { currentT = aA + (aB - aA) / 2.0; currentX = calcBezier(currentT, mX1, mX2) - aX; if (currentX > 0.0) { aB = currentT; } else { aA = currentT; } } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); return currentT; } function BezierEasing (mX1, mY1, mX2, mY2) { // Validate arguments if (arguments.length !== 4) { throw new Error("BezierEasing требует 4 аргумента."); } for (var i=0; i<4; ++i) { if (typeof arguments[i] !== "number" || isNaN(arguments[i]) || !isFinite(arguments[i])) { throw new Error("BezierEasing аргументы должны быть целыми числами."); } } if (mX1 < 0 || mX1 > 1 || mX2 < 0 || mX2 > 1) { throw new Error("BezierEasing x значение долдно быть в пределах [0, 1]."); } var mSampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); function newtonRaphsonIterate (aX, aGuessT) { for (var i = 0; i < NEWTON_ITERATIONS; ++i) { var currentSlope = getSlope(aGuessT, mX1, mX2); if (currentSlope === 0.0) return aGuessT; var currentX = calcBezier(aGuessT, mX1, mX2) - aX; aGuessT -= currentX / currentSlope; } return aGuessT; } function calcSampleValues () { for (var i = 0; i < kSplineTableSize; ++i) { mSampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); } } function getTForX (aX) { var intervalStart = 0.0; var currentSample = 1; var lastSample = kSplineTableSize - 1; for (; currentSample != lastSample && mSampleValues[currentSample] <= aX; ++currentSample) { intervalStart += kSampleStepSize; } --currentSample; // Interpolate to provide an initial guess for t var dist = (aX - mSampleValues[currentSample]) / (mSampleValues[currentSample+1] - mSampleValues[currentSample]); var guessForT = intervalStart + dist * kSampleStepSize; var initialSlope = getSlope(guessForT, mX1, mX2); if (initialSlope >= NEWTON_MIN_SLOPE) { return newtonRaphsonIterate(aX, guessForT); } else if (initialSlope === 0.0) { return guessForT; } else { return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize); } } var _precomputed = false; function precompute() { _precomputed = true; if (mX1 != mY1 || mX2 != mY2) calcSampleValues(); } var f = function (aX) { if (!_precomputed) precompute(); if (mX1 === mY1 && mX2 === mY2) return aX; // linear // Поскольку числа JavaScript неточны, мы должны гарантировать правильность крайних значений. if (aX === 0) return 0; if (aX === 1) return 1; return calcBezier(getTForX(aX), mY1, mY2); }; f.getControlPoints = function() { return [{ x: mX1, y: mY1 }, { x: mX2, y: mY2 }]; }; var args = [mX1, mY1, mX2, mY2]; var str = "BezierEasing("+args.join()+")"; f.toString = function () { return str; }; var css = "cubic-bezier("+args.join()+")"; f.toCSS = function () { return css; }; f.toString = function () { return args; }; return f; } ////////////// CUBIC-BEZIER КОНЕЦ ФУНКЦИИ

Теперь пришло время манипуляций. Туда же где у нас вписана функция через eval() мы вписываем следующий код. Я использовал маркер для наглядных манипуляции, вы можете смело заменить его чем заблагорассудиться.

var markerTime = thisLayer.marker.key(1); // Маркер для анимации var AniTimeIn = markerTime.duration; // Длительность var AniStart = markerTime.time; // Время начала var AniStop = markerTime.time + AniTimeIn; // Время окончания var newValues = BezierEasing(.25,0,0,1); // Сюда добавьте свою функцию графика cubic-bezier.com var AniTimerange = linear(time, AniStart, AniStop, 0, 1); // Сопоставление AniStart и AniStop к 0-1 так как функция BezierEasing ожидает значение между 0 и 1 // Функции анимации OutX = linear(newValues(AniTimerange),0,1, 0, 1000); OutY = linear(newValues(AniTimerange),0,1, 0, 1000); [OutX, OutY]

Создаем переменную newValues в которой определяем аргументы функции BezierEasing. Далее создаем переменную AniTimerange присваивая время анимации по маркеру. В переменные OutX и OutY соответственно ставим функцию анимации newValues() где время по которому идет это переменная AniTimerange, которая будет менять значения от 0 до 1 (100%), которые будут определены 4 и 5 аргументами.

Для построения графиков и получения их значений можете использовать удобный онлайн инструмент cubic-bezier.com или при наличии плагин Flow. Вписывать значения функции на 6 строке для BezierEasing(.25,0,0,1).

Вписывать свои значения для анимации можно в последних переменных OutX и OutY. 0 и 1 естественно не трогаем, так как они определяют своего рода процент выполнения, изменяйте только два последних аргумента. Время анимации задается маркером в функции AniTimerange вторым и третьим аргументом, тут его меняйте при необходимости.

Теперь вы можете анимировать выражениями по любому графику функций, не ограничиваясь лишь линейной и плавной.

Функционируйте и процветайте 🖖

Начать дискуссию