Шейдерная анимация в WebGL строится вокруг управления параметрами GPU-программы — uniform-переменными, которые передаются в GLSL-код из JavaScript. Popmotion выступает как слой временной и физической анимации, преобразующий дискретные изменения состояния в непрерывные кривые движения.
Основная идея заключается в разделении ответственности:
Такой подход позволяет отказаться от ручного
requestAnimationFrame-контроля анимации и передать
управление кривыми, инерцией и интерполяцией специализированному
движку.
Типичная схема взаимодействия выглядит следующим образом:
Ключевая особенность — Popmotion не знает о WebGL, он работает только с данными.
Фрагментный шейдер, реагирующий на время и амплитуду:
precision mediump float;
uniform float u_time;
uniform float u_amplitude;
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float wave = sin(uv.x * 10.0 + u_time) * u_amplitude;
float intensity = 0.5 + 0.5 * wave;
gl_FragColor = vec4(vec3(intensity), 1.0);
}
Здесь вся анимация завязана на u_time и
u_amplitude, которые будут управляться из JavaScript.
Popmotion позволяет анимировать объект состояния:
import { animate } from 'popmotion';
const uniforms = {
u_amplitude: 0,
u_time: 0
};
Далее запускается анимация:
animate({
from: 0,
to: 1,
duration: 2000,
onUpdate: v => {
uniforms.u_amplitude = v;
}
});
Каждое обновление значения v синхронизируется с
uniform-переменной.
WebGL требует постоянного обновления кадра, даже если Popmotion не изменяет значение в данный момент.
function render(time) {
uniforms.u_time = time * 0.001;
gl.uniform1f(locations.u_time, uniforms.u_time);
gl.uniform1f(locations.u_amplitude, uniforms.u_amplitude);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
Popmotion отвечает за изменение параметров, а render loop — за их применение.
Popmotion поддерживает физические модели, которые особенно полезны для шейдерной динамики.
import { spring } from 'popmotion';
spring({
from: 0,
to: 1,
stiffness: 80,
damping: 12,
onUpdate: v => {
uniforms.u_amplitude = v;
}
}).start();
Физическая модель позволяет добиться эффекта «живого» шейдера, где волны и искажения реагируют инерционно.
Шейдерная анимация часто управляется мышью или касаниями.
window.addEventListener('mousemove', (e) => {
const x = e.clientX / window.innerWidth;
const y = e.clientY / window.innerHeight;
uniforms.u_mouseX = x;
uniforms.u_mouseY = y;
});
В шейдере:
uniform vec2 u_mouse;
float dist = distance(uv, u_mouse);
float ripple = sin(dist * 20.0 - u_time * 3.0);
Popmotion в таком случае может сглаживать движение мыши:
import { animate } from 'popmotion';
animate({
from: { x: 0, y: 0 },
to: { x: targetX, y: targetY },
onUpdate: v => {
uniforms.u_mouseX = v.x;
uniforms.u_mouseY = v.y;
}
});
Шейдеры часто используют HSL/HSV преобразования. Popmotion может анимировать компоненты цвета независимо.
GLSL:
vec3 colorA = vec3(0.2, 0.6, 1.0);
vec3 colorB = vec3(1.0, 0.3, 0.5);
vec3 color = mix(colorA, colorB, u_mix);
Jav * aScript:
animate({
from: 0,
to: 1,
duration: 3000,
repeat: Infinity,
onUpdate: v => {
uniforms.u_mix = v;
}
});
Это создаёт непрерывную морфинг-анимацию между двумя цветовыми состояниями.
Сложные эффекты строятся через композицию нескольких uniform-каналов:
Popmotion позволяет анимировать их независимо:
animate({
from: 0,
to: 1,
duration: 1500,
onUpdate: v => uniforms.u_noiseStrength = v
});
animate({
from: 1,
to: 0,
duration: 3000,
repeat: Infinity,
onUpdate: v => uniforms.u_distortion = v
});
Так создаётся многослойная динамика, где разные аспекты шейдера живут собственной временной линией.
Popmotion поддерживает последовательности значений:
import { keyframes } from 'popmotion';
keyframes({
values: [0, 1, 0.5, 1],
times: [0, 0.3, 0.6, 1],
duration: 4000,
onUpdate: v => {
uniforms.u_amplitude = v;
}
}).start();
Это позволяет задавать нелинейные паттерны поведения шейдеров, включая пульсации, всплески и затухающие волны.
При высокой частоте обновлений важно минимизировать количество
вызовов gl.uniform*.
Практика:
Пример паттерна:
const state = {
amplitude: 0,
mix: 0,
distortion: 0
};
function syncUniforms() {
gl.uniform1f(locations.u_amplitude, state.amplitude);
gl.uniform1f(locations.u_mix, state.mix);
gl.uniform1f(locations.u_distortion, state.distortion);
}
Popmotion обновляет state, WebGL синхронизирует GPU.
Шейдерная анимация часто опирается на noise-функции:
float noise(vec2 p) {
return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
}
Popmotion может управлять параметрами шума:
animate({
from: 0,
to: 10,
duration: 5000,
onUpdate: v => uniforms.u_noiseScale = v
});
В результате изменяется «зернистость» движения без изменения самой функции шума.
Popmotion spring-модель часто используется совместно с периодическими функциями шейдера:
GLSL:
float wave = sin(u_time * u_speed + uv.x * 5.0);
wave *= u_amplitude;
Jav * aScript:
spring({
from: 0.2,
to: 1.0,
stiffness: 60,
damping: 15,
onUpdate: v => uniforms.u_amplitude = v
}).start();
Получается эффект живого отклика системы на изменение параметров.
В сложных сценах используется несколько WebGL программ:
Popmotion может управлять глобальным состоянием:
const globalState = {
intensity: 0,
blur: 0
};
И затем распределять его по разным шейдерам:
animate({
from: 0,
to: 1,
onUpdate: v => {
globalState.intensity = v;
globalState.blur = v * 0.5;
}
});
Шейдеры позволяют модифицировать ощущение времени:
float warpedTime = u_time + sin(u_time) * 0.5;
Popmotion может анимировать коэффициент искажения времени:
animate({
from: 0,
to: 2,
duration: 6000,
onUpdate: v => uniforms.u_timeWarp = v
});
Это создаёт эффект ускорения и замедления визуальных процессов без изменения реального времени render loop.
Комбинация нескольких анимаций Popmotion позволяет создавать сложные визуальные паттерны:
Каждый слой управляется отдельной анимационной кривой, но все они сходятся в одном GPU-проходе, формируя единый визуальный результат.