preface
Transitions are most common in video editing tools. Adding a “transition” effect between two videos or images makes the process smoother and more natural. Common transitions such as gradient transitions, rotations, erases, etc. (iMovie’s transitions are shown below) :
And now many video apps also have the photo album function, you can choose different transitions to create a dynamic photo album:
Transitions are quite different in WebGL compared to editors, and these differences lead to some thoughts:
First, material switching time
In the previous article, I mentioned the switching of two materials, but generally there are more than two pictures in the photo set. How to make the whole switching cycle and no perception? Here is a simple animation example:
To explain this briefly, suppose that our transition effect is to switch from right to left (as shown in the GIF) at the end of each animation round, reassign u_Sampler0 and u_Sampler1, and the first image of each animation round is the next image of the previous animation round. This instantaneous assignment makes the entire animation feel unchanged. This allows for looping of different materials without taking up too much texture space in WebGL (only two textures are needed), which comes from the experience of writing The Slider on the Web side.
The relevant codes are as follows:
// Change the material
function changeTexture(gl, imgList, count) {
var texture0 = gl.createTexture();
var texture1 = gl.createTexture();
if(! texture0 && ! texture1) {console.log('Failed to create the texture object');
return false;
}
var u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0');
if(! u_Sampler0) {console.log('Failed to get the storage location of u_Sampler0');
return false;
}
var u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1');
if(! u_Sampler1) {console.log('Failed to get the storage location of u_Sampler1');
return false;
}
loadTexture(gl, texture0, u_Sampler0, imgList[count%imgList.length], 0);
loadTexture(gl, texture1, u_Sampler1, imgList[(count+1)%imgList.length], 1);
}
// Load the material
function loadTexture(gl, texture, u_Sampler, image, index) {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
gl.activeTexture(gl['TEXTURE'+index])
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.uniform1i(u_Sampler, index);
return true;
}
Copy the code
Two, transition effect switch
A lot of times we use a combination of different transitions. Here are two ideas:
1. Perform transitions in the Shader
Pass in a variable that records the number of transitions, determine the number in the shader code, and switch transitions
precision mediump float;
varying vec2 uv;
uniform float time; // Change time
uniform sampler2D u_Sampler0;
uniform sampler2D u_Sampler1;
uniform float count; // The number of cycles
void main() {
if (count == 1.) {
// The first transition
// Play the first effect
}
else if (count == 2.) {
// The second transition
// Play the second effect}}Copy the code
This approach has obvious disadvantages: first, the file is not granular enough, and one file has multiple effects; Second logic and effect coupling together, not easy to do any combination of different transitions, such as I have 1, 2, 3 kinds of transitions, if it is a independent file storage, I can adjust the order at 123/132/231/213/312/321/1123 /… To control the playing time of each transition. Therefore, the second method is more recommended:
2. Each transition is independent of files and codes
// transition1.glsl
precision mediump float;
varying vec2 uv;
uniform float time;
uniform sampler2D u_Sampler0;
uniform sampler2D u_Sampler1;
void main() {
// ...
}
Copy the code
// transition2.gls
precision mediump float;
varying vec2 uv;
uniform float time;
uniform sampler2D u_Sampler0;
uniform sampler2D u_Sampler1;
void main() {
// ...
}
Copy the code
Then we control the transition in JavaScript:
// Add this code at the bottom of main()
void main() {
function render() {
var img1 = null;
var img2 = null;
// Remove the image one at a time
if (imgList.length > 2) {
img1 = imgList.shift()
img2 = imgList[0]}else {
return;
}
// I added a random logic to switch the second transition when there were three images left.
// File fetching is ignored here
if (imgList.length == 3) {
setShader(gl, VSHADER_SOURCE, FSHADER_SOURCE2);
} else {
setShader(gl, VSHADER_SOURCE, FSHADER_SOURCE);
}
// Set the material
setTexture(gl, img1, img2);
// Time and timeRange are used to determine the time of each round.
GetAnimationTime () to get the progress time from 0 to 1
var todayTime = (function() {
var d = new Date(a); d.setHours(0.0.0.0);
returnd.getTime(); }) ()var duration = 2000;
var startTime = new Date().getTime() - todayTime;
var timeRange = gl.getUniformLocation(gl.program, 'timeRange');
gl.uniform2f(timeRange, startTime, duration);
var time = gl.getUniformLocation(gl.program, 'time');
gl.uniform1f(time, todayTime);
// Since calling setShader resets program, all variables related to gl.program are reassigned
var xxx = gl.getUniformLocation(gl.program, 'xxx');
gl.uniform2f(xxx, 750..1334.);
// Inside loop, each time this round of transitions to play the end
var requestId = 0;
(function loop(requestId) {
var curTime = new Date().getTime() - todayTime;
if (curTime <= startTime + duration) {
gl.uniform1f(time, curTime)
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_FAN, 0.4);
requestId = requestAnimationFrame(loop.bind(this, requestId))
} else {
cancelAnimationFrame(requestId)
render()
}
})(requestId)
}
render()
}
// Change the material
function setTexture(gl, img1, img2) {
var texture0 = gl.createTexture();
var texture1 = gl.createTexture();
var inputImageTexture = gl.getUniformLocation(gl.program, 'inputImageTexture');
var inputImageTexture2 = gl.getUniformLocation(gl.program, 'inputImageTexture2');
loadTexture(gl, texture0, inputImageTexture, img1, 0);
loadTexture(gl, texture1, inputImageTexture2, img2, 1);
}
// Switch different transitions (just change fshader)
function setShader(gl, vshader, fshader) {
if(! initShaders(gl, vshader, fshader)) {console.log('Failed to intialize shaders.');
return; }}Copy the code
3. Material transition mode
Transitions are generally accompanied by switching between two pictures. There are two common switching modes:
- Linear interpolation of pixels in two graphs
mix()
, the switch is softer - According to the time to switch, switch more stiff
1. Linear interpolation
It is generally applicable to the transition field with smooth transition, and the process of alternation of two pictures can be clearly seen:
return mix(texture2D(u_Sampler0, uv), texture2D(u_Sampler1, uv), progress);
Copy the code
2. Switch based on the time
It is generally applicable to the case that the transition changes quickly, and this switch cannot be distinguished by the naked eye.
if (progress < 0.5) {
gl_FragColor = texture2D(u_Sampler0, uv);
} else {
gl_FragColor = texture2D(u_Sampler1, uv);
}
Copy the code
For example, the first transition in the image below switches the texture instantly according to time (but can’t be seen), the latter is a linear interpolation gradient:
Four, animation speed simulation
Basically no transition is as simple as a linear uniform motion, so you have to simulate different velocity curves here. In order to create a better transition effect, there are several steps:
1. Get a real time curve
Assuming the transition is designed by yourself, you can use some preset curves, such as the ones provided here:
Bessel formula of the curve can be obtained directly:
Assuming that the transition effect is provided by others, if the designer uses AE to produce the transition effect, the time change curve corresponding to the relevant movement can be found in AE:
2. Use speed curves
Once we get the curve, the next step of course is to get the mathematical formula and plug it into our variables (progress/time/uv.x, etc.).
The first thing to be clear about is that time in the real world does not get faster or slower, which means that time always moves at a constant speed. It’s just that when we apply the formula to unit time, the result changes in speed (if the X-axis motion is our independent variable, then y can be the dependent variable).
#ifdef GL_ES
precision mediump float;
#endif
# define PI (3.14159265359)
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
float plot(vec2 st, float pct){
return smoothstep( pct0.01, pct, st.y) -
smoothstep( pct, pct+0.01, st.y);
}
float box(vec2 _st, vec2 _size, float _smoothEdges){
_size = vec2(0.5)-_size*0.5;
vec2 aa = vec2(_smoothEdges*0.5);
vec2 uv = smoothstep(_size,_size+aa,_st);
uv *= smoothstep(_size,_size+aa,vec2(1.0)-_st);
return uv.x*uv.y;
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
vec2 boxst = st + . 5;
// The mathematical formula y = f(x)
// The independent variable is st.x and the dependent variable is st.y
float f_x = sin(st.x*PI);
// Calculate the position of each movement of the small square
// The formula is the same as f(x) above, only
// Our dependent variable changed from st.x to FRact (u_time)
// fract(u_time) keeps the time from 0 to 1 forever
// The reason for *.6 is not to move so fast that you cannot see the speed change
boxst.y -= sin(fract(u_time*6.)*PI);
boxst.x -= fract(u_time*6.);
// Draw time curves and squares
float box = box(boxst, vec2(. 08.. 08), 0.001);
float pct = plot(st, f_x);
vec3 color = pct*vec3(0.0.1.0.0.0)+box;
gl_FragColor = vec4(color,1.0);
}
Copy the code
We can use st.x or u_time/progress as the independent variable to get the corresponding motion curve and animation. Now we can try other animation curves:
// Show some code
float f_x = pow(st.x, 2.);
boxst.y -= pow(fract(u_time*6.), 2.);
boxst.x -= fract(u_time*6.);
Copy the code
float f_x = -(pow((st.x1.), 2.) 1.);
boxst.y -= -(pow((fract(u_time*6.)1.), 2.) 1.);
boxst.x -= fract(u_time*6.);
Copy the code
// easeInOutQuint
float f_x = st.x<. 5 ? 16.*pow(st.x, 5.) : 1.+16.*(--st.x)*pow(st.x, 4.);
boxst.y -= fract(u_time*6.) <. 5 ? 16.*pow(fract(u_time*6.), 5.) : 1.+16.* (fract(u_time*6.)1.) *pow(fract(u_time*6.)1..4.);
boxst.x -= fract(u_time*6.);
Copy the code
// easeInElastic
float f_x = ((. 04 -.04/st.x) * sin(25.*st.x) + 1.) *8.;
boxst.y -= ((. 04 -.04/fract(u_time*6.)) * sin(25.*fract(u_time*6.)) + 1.) *8.;
boxst.x -= fract(u_time*6.);
Copy the code
// easeOutElastic
float f_x = (. 04*st.x /(--st.x)*sin(25.*st.x))+2.;
boxst.y -= (. 04*fract(u_time*6.)/(fract(u_time*6.)1.) *sin(25.*fract(u_time*6.))) +2.;
boxst.x -= fract(u_time*6.);
Copy the code
More easing functions:
EasingFunctions = {
// no easing, no acceleration
linear: function (t) { return t },
// accelerating from zero velocity
easeInQuad: function (t) { return t*t },
// decelerating to zero velocity
easeOutQuad: function (t) { return t*(2-t) },
// acceleration until halfway, then deceleration
easeInOutQuad: function (t) { return t<. 5 ? 2*t*t : - 1+ (42 -*t)*t },
// accelerating from zero velocity
easeInCubic: function (t) { return t*t*t },
// decelerating to zero velocity
easeOutCubic: function (t) { return (--t)*t*t+1 },
// acceleration until halfway, then deceleration
easeInOutCubic: function (t) { return t<. 5 ? 4*t*t*t : (t- 1) * (2*t2 -) * (2*t2 -) +1 },
// accelerating from zero velocity
easeInQuart: function (t) { return t*t*t*t },
// decelerating to zero velocity
easeOutQuart: function (t) { return 1-(--t)*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuart: function (t) { return t<. 5 ? 8*t*t*t*t : 1- 8 -*(--t)*t*t*t },
// accelerating from zero velocity
easeInQuint: function (t) { return t*t*t*t*t },
// decelerating to zero velocity
easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuint: function (t) { return t<. 5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t },
// elastic bounce effect at the beginning
easeInElastic: function (t) { return (. 04 - . 04 / t) * sin(25 * t) + 1 },
// elastic bounce effect at the end
easeOutElastic: function (t) { return . 04 * t / (--t) * sin(25 * t) },
// elastic bounce effect at the beginning and end
easeInOutElastic: function (t) { return (t -= . 5) < 0 ? (. 02 + .01 / t) * sin(50 * t) : (. 02 - .01 / t) * sin(50 * t) + 1 },
easeIn: function(t){return function(t){return pow(t, t)}},
easeOut: function(t){return function(t){return 1 - abs(pow(t- 1, t))}},
easeInSin: function (t) { return 1 + sin(PI / 2 * t - PI / 2)},
easeOutSin : function (t) {return sin(PI / 2 * t)},
easeInOutSin: function (t) {return (1 + sin(PI * t - PI / 2)) / 2}}Copy the code
3. Construct a custom speed curve
Custom speed curve we can draw by Bezier curve, how to convert our common bezier curve in CSS into a mathematical formula? This article gives us an idea. By modifying the JavaScript code provided by it, we get the following Shader functions:
float A(float aA1, float aA2) {
return 1.0 - 3.0 * aA2 + 3.0 * aA1;
}
float B(float aA1, float aA2) {
return 3.0 * aA2 - 6.0 * aA1;
}
float C(float aA1) {
return 3.0 * aA1;
}
float GetSlope(float aT, float aA1, float aA2) {
return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
}
float CalcBezier(float aT, float aA1, float aA2) {
return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT;
}
float GetTForX(float aX, float mX1, float mX2) {
float aGuessT = aX;
for (int i = 0; i < 4; ++i) {
float currentSlope = GetSlope(aGuessT, mX1, mX2);
if (currentSlope == 0.0) return aGuessT;
float currentX = CalcBezier(aGuessT, mX1, mX2) - aX;
aGuessT -= currentX / currentSlope;
}
return aGuessT;
}
float KeySpline(float aX, float mX1, float mY1, float mX2, float mY2) {
if (mX1 == mY1 && mX2 == mY2) return aX; // linear
return CalcBezier(GetTForX(aX, mX1, mX2), mY1, mY2);
}
Copy the code
First we get four parameters from the Bezier curve editor, such as bezier-easing-editor or cubic-bezier:
or
The corresponding curve can be obtained by substituting these four numbers and independent variables. For example, we constructed a curve by ourselves:
Then substitute in.1,.96,.89,.17 to get the desired motion curve:
But when we pass in some special values like 0.99,0.14,0,0.27 we get a strange curve:
The desired curve is actually:
This is because the author did not take into account multi-angle tilting when implementing the transformation, and after his update we get more robust code: github.com/gre/bezier-… Again, I convert them to Shader functions:
float sampleValues[11];
const float NEWTON_ITERATIONS = 10.;
const float NEWTON_MIN_SLOPE = 0.001;
const float SUBDIVISION_PRECISION = 0.0000001;
const float SUBDIVISION_MAX_ITERATIONS = 10.;
float A(float aA1, float aA2) {
return 1.0 - 3.0 * aA2 + 3.0 * aA1;
}
float B(float aA1, float aA2) {
return 3.0 * aA2 - 6.0 * aA1;
}
float C(float aA1) {
return 3.0 * aA1;
}
float getSlope(float aT, float aA1, float aA2) {
return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
}
float calcBezier(float aT, float aA1, float aA2) {
return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT;
}
float newtonRaphsonIterate(float aX, float aGuessT, float mX1, float mX2) {
for (float i = 0.; i < NEWTON_ITERATIONS; ++i) {
float currentSlope = getSlope(aGuessT, mX1, mX2);
if (currentSlope == 0.0) {
return aGuessT;
}
float currentX = calcBezier(aGuessT, mX1, mX2) - aX;
aGuessT -= currentX / currentSlope;
}
return aGuessT;
}
float binarySubdivide(float aX, float aA, float aB, float mX1, float mX2) {
float currentX, currentT;
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) {
aB = currentT;
} else {
aA = currentT;
}
for(float i=0.; i<SUBDIVISION_MAX_ITERATIONS; ++i) {
if (abs(currentX)>SUBDIVISION_PRECISION) {
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) {
aB = currentT;
} else{ aA = currentT; }}else {
break; }}return currentT;
}
float GetTForX(float aX, float mX1, float mX2, int kSplineTableSize, float kSampleStepSize) {
float intervalStart = 0.0;
const int lastSample = 10;
int currentSample = 1;
for (int i = 1; i ! = lastSample; ++i) {if (sampleValues[i] <= aX) {
currentSample = i;
intervalStart += kSampleStepSize;
}
}
--currentSample;
// Interpolate to provide an initial guess for t
float dist = (aX - sampleValues[9]) / (sampleValues[10] - sampleValues[9]);
float guessForT = intervalStart + dist * kSampleStepSize;
float initialSlope = getSlope(guessForT, mX1, mX2);
if (initialSlope >= NEWTON_MIN_SLOPE) {
return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
} else if (initialSlope == 0.0) {
return guessForT;
} else {
returnbinarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); }}float KeySpline(float aX, float mX1, float mY1, float mX2, float mY2) {
const int kSplineTableSize = 11;
float kSampleStepSize = 1. / (float(kSplineTableSize) - 1.);
if(! (0. <= mX1 && mX1 <= 1. && 0. <= mX2 && mX2 <= 1.)) {
// bezier x values must be in [0, 1] range
return 0.;
}
if (mX1 == mY1 && mX2 == mY2) return aX; // linear
for (int i = 0; i < kSplineTableSize; ++i) {
sampleValues[i] = calcBezier(float(i)*kSampleStepSize, mX1, mX2);
}
if (aX == 0.) return 0.;
if (aX == 1.) return 1.;
return calcBezier(GetTForX(aX, mX1, mX2, kSplineTableSize, kSampleStepSize), mY1, mY2);
}
Copy the code
Finally, we have the motion curve we want:
With bezier curves as a powerful tool, we can basically meet the needs of any animation rate. For us to achieve elegant natural transition effect to provide a strong guarantee.
Here are two giFs to feel the subtle sensory differences between the transitions caused by uniform and non-uniform motion (the first is uniform, and the second is Bessel curve, GIF will affect the final effect, but you can get a general feeling) :
Related links:
- gist.github.com/gre/1650294
- Greweb. Me / 2012/02 / bez…