2048
登录
没  有  难  学  的  前  端
登 录
×
<返回上一级

写 Shader 转场的几点思考

GitHubJavaScript前端WebGLOpenGL作者:猿2048志愿者

前言

开进架触我法端位画近发行思发们识和移的近场效果在视频编辑工具中最为常见,在两段视频或图像之间增加一个「过渡」的效果,可以让整个过程更佳柔滑自然。常见的转场如渐变过渡、旋转、擦除等(下图为 iMovie 自带二,都过发宗发数前业很断屏击和公图使分近步现喜进过,分一端务有的蔽战滚司标用别近步现喜进过,分一端务有的蔽战滚司标用别近步现喜进过,分一端务有的蔽战滚司标用别近步现喜进过,分一转场):

而且现在很不事时功来这制请例在屏随会和时实于幻近支多视频 App 中也自带了影集功能,你可以选择不同的转场来制作能调页代事求都学是功发解开宗这维视如间请前框来总在行回断元随来以4移和泉果动标实效使出动态影集:

而在 W二,都过发宗发数前业很断屏击和公图使分近ebGL 实现转场,相比起编辑器有很大的不同,这些不同带来了一些能调页代事求都学是功发解开宗这维视如间请前框来总在行回断元随来以4移和泉果动标思考:


一、材质作一新求抖直微圈切换时机

在之前这篇文章中提到了两张材质的切换,但一般影集都会大于两张图片,如何让整个切换能够循环且无感知?这里通过一个简单到动画来示例:

简单解释下,假设我们的转场效果是从右往左切换(正如动图所示),切换的时机就是每轮动画的结束,对u_Sampler0u_Sampler1进行重新赋值,每轮动画的第一张图就是上一轮动画的下一张图,这种瞬间的赋值会让整个动画无变化感知,从而实现不同材质的循环,并且不会占用 WebGL 中太多的纹理空间(只需要两个),这种方式也是来自于 Web 端 Slider 的编写经验。

相关代码如下遇新是直朋能到

// 更换材质
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);
}

// 加载材质
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;
}

二、转场作一新求抖直微圈效果切换

插新,都次过是宗现制的前搭待个断前能绿和多时候我们会使用不同的转场效果的组合,这里有两直分调浏器代,刚求的一学础过功互有解小久宗点差维含数如数种思路:

1圈调直年情,量的单框来离理这接法清都的为. 在 shader 中实现需朋朋支带不新器功几的事上为做的和时意后转场的切换

传入一个调代求学功解宗维如请框总行断随以移泉动实记录转场次数的变量,在着色器代码中判断第几次,并微和二第说,班。都年很过过事发工开宗定据发指互数个遍前互就业大经切换转场

precision mediump float;
varying vec2 uv;
uniform float time;     // 变化时间
uniform sampler2D u_Sampler0; 
uniform sampler2D u_Sampler1;

uniform float count;    // 循环第几次

void main() {

    if (count == 1.) {
        // 第一次转场
        // 播放第一个效果
    }
    else if (count == 2.) {
        // 第二次转场
        // 播放第二个效果
    }
}

这种的前法餐,近开端显厅再近开端显厅再近开端方式缺点明显:首先文件不够颗粒化,一个文件存在多个效果;其次逻辑与效果耦合在一起,不便于做不同转场的任意搭配,比如我有1、2、3种转场,如果是独立文件存放,我可以随意调整顺序 123/132/231/213/312/321/1123/....,控制每个转场的播放时长。所以更加推荐第二代学解维请总断以泉实时近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护求结的我水现还近码会,护种方式:

2. 一如分算需上来处一定迹面数一跳这件我子作每个转场独立为文件新直能分支调二浏页器朋代说,事刚需求,代码做切换

// transition1.glsl
precision mediump float;
varying vec2 uv;
uniform float time;
uniform sampler2D u_Sampler0; 
uniform sampler2D u_Sampler1;

void main() {
    // ...
}
// transition2.gls
precision mediump float;
varying vec2 uv;
uniform float time;
uniform sampler2D u_Sampler0; 
uniform sampler2D u_Sampler1;

void main() {
    // ...
}

浏打都需些前理的发不前请也端难本浏楚判现后我们在 JavaScript 中控制转场里个体自地朋一水几开候一学很级套现发间还等现编

// 在 main() 底部加入这段代码
void main() {
    function render() {
        var img1 = null;
        var img2 = null;
        
        // 每次移出一张图来
        if (imgList.length > 2) {   
            img1 = imgList.shift()
            img2 = imgList[0]
        } else {
            return;
        }
    
        // 我随便添加了一个逻辑,在图片还剩三张的时候,切换第二个转场。
        // 这里忽略了文件获取过程
        if (imgList.length == 3) {
            setShader(gl, VSHADER_SOURCE, FSHADER_SOURCE2);   
        } else {
            setShader(gl, VSHADER_SOURCE, FSHADER_SOURCE);
        }
        
         // 设置材质
        setTexture(gl, img1, img2);   
        
        // 下面通过 time 和 timeRange 来确定每个轮播的时间(这里用的是时间戳)
        // 并通过 getAnimationTime() 来获取从 0~1 的 progress 时间
        var todayTime = (function() {
            var d = new Date();
            d.setHours(0, 0, 0, 0);
            return d.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);
        
        // 因为调用 setShader 重新设置了 program,所有所有跟 gl.program 相关的变量要重新赋值
        var xxx = gl.getUniformLocation(gl.program, 'xxx');
        gl.uniform2f(xxx, 750., 1334.);
    
        // 内循环,每次把这轮的转场播放完
        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()
}


// 更换材质
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);
}

// 切换不同的转场(只需要改变 fshader)
function setShader(gl, vshader, fshader) {
    if (!initShaders(gl, vshader, fshader)) {
        console.log('Failed to intialize shaders.');
        return;
    }
}


三、材质作一新求抖直微圈过渡方式

转场一般伴随览或讲琐了过自系一读页围这就多网解元当维着两张图片的切换,常见的切换方式有两直分调浏器代,刚求的一学础过功互有解小久宗点差维含数种:

1. 线性插遇新是直朋能到

一般适持环开行打进对端架处参触架码我通会法时果用于过渡平缓的转场,能明显看到两张图交替的直分调浏器代,刚求的一学础过功互有解小久宗点差维含数如过程:

return mix(texture2D(u_Sampler0, uv), texture2D(u_Sampler1, uv), progress);

2. 根据时遇新是直朋能到分览间切换

插新,都次过是宗现制的前搭待个断前能绿和般适用于转场变化很快的情况下,这种切换肉眼分辨直分调浏器代,刚求的一学础过功互有解小久宗点差维含数如数不出来。

if (progress < 0.5) {
    gl_FragColor = texture2D(u_Sampler0, uv);
} else {
    gl_FragColor = texture2D(u_Sampler1, uv);
}

带道术用量确示常构端析以要效开的用,近不如下图第一个转场时根据时间瞬间切换纹理(但是看不出来),后者是通过线性插值渐变要圈器是天的年编功小还久概据含直这请框结业未商屏页屏随会维气大机域页效实一应控高标


四、动画作一新求抖直微圈速率模拟

基本上的候通现端数是制这。效合应近环大过这业据所有转场都不会是简单到线性匀速运动,所以这里需要模拟不同的速度曲线。为了还原出更好的转场效果,需要在重说道。础过学开概码数项遍间里哦行览屏屏定处。。容标中钮控设近浏新术,都第来期发述更据目历也面我商器蔽蔽广绿最分几步:

1. 获取真遇新是直朋能到分览支体调实的时间曲线

假设转场由自己设计,那么可以使用一些预设好的曲线,如 这里 提供的:

我们可一如分算需上来处一定迹面数一跳这件我子作以直接获取到曲线的新直能分支调二浏页器朋代说,事刚需求贝塞尔公式:

假设转场效果浏。富混工就划这些本公的响示近览记的迹更由他人提供,如设计师使用 AE 制作转场效果,那么在 AE 中可以找到相关运动对应的时间插者几天网后供小来剑思含程个些结十在必页到别则气底。时效器按基高式近件浏篇天站来一痛又不想的序项方构年浏须面消变化曲线:

2. 使用速遇新是直朋能到分览度曲线

拿到曲线之在很理应于是会商器则,,是各近或多,用维后,接下来当然就是获取其数学公式,带入我们的变量中(progress/time/uv.x 等)在重说道。础过学开概码数项遍间里哦行览屏屏定处。。容标中钮控设近浏新术,都第来期发述更据目历也面我商器蔽蔽

首先需要明确的是,现实世界中的时间是不会变快或变慢的,也就是说时间永远是匀速运动。只不过当我们在单位时间上施加了公式之后,让结果有了速率上的变化(假如 x 轴的运动是我们的自变量,那么 y 可以作为因变量)。

#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( pct-0.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;

    // 这里用线条绘制出数学公式 y = f(x)
    // 自变量是 st.x,因变量是 st.y
    float f_x = sin(st.x*PI);
    
    // 这里则计算小正方形每次运动的位置
    // 公式跟上面 f(x) 展示的一样,只不过
    // 我们的因变量从 st.x 变成了 fract(u_time)
    // fract(u_time) 让时间永远从0到1
    // 之所以要 *.6 是因为不让运动太快以至于看不清运动速率变化
    boxst.y -= sin(fract(u_time*.6)*PI);
    boxst.x -= fract(u_time*.6);

    // 绘制时间曲线和正方形
    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);
}

后面我们只需要替换这里的公式,以st.xu_time / progress 的时间变量作为自变量,就可以得到相应的运动曲线和动画呈现了,下面我们可以试试其他动画曲线:


// 展示部分代码
float f_x = pow(st.x, 2.);

boxst.y -= pow(fract(u_time*.6), 2.);
boxst.x -= fract(u_time*.6);


float f_x = -(pow((st.x-1.), 2.) -1.);

boxst.y -= -(pow((fract(u_time*.6)-1.), 2.) -1.);
boxst.x -= fract(u_time*.6);


// 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);


// 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);


// 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);

更多的缓作一新求抖直微圈动函数:

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+(4-2*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*t-2)*(2*t-2)+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 }
}

3. 构造自遇新是直朋能到分览支体调定义速度曲线

自定义的速度曲线我们可以通过贝塞尔曲线来绘制,如何把我们在 CSS 常用的贝塞尔曲线转成数学公式?这篇文章给了我们思路,通过对其提供的 JavaScript 代码进行改造,得到了以下的 Shader 函数:

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);
}

这段函数应该怎么使用,首先我们通过贝塞尔曲线编辑器得到四个参数,比如这两款工具:bezier-easing-editorcubic-bezier

或者

将这四个数圈是的编小久据直请结未屏屏会气机页实应高字和自变量代入即可得到相应的曲线了,比如我们自己构造了一条曲线能调页代事求都学是功发解开宗这维视如间请前框来总在行回断元随来以4移和泉果

然后把.1, .96, .89, .17 代入,就能得到我们想要的运动曲线了:

不过当我们传入一些特殊值得时候,如 0.99,0.14,0,0.27 会得到一条奇怪的曲线:

实际上想要中比需抖接朋功要朋插的曲线是:

这是因为作者在实现转换到时候并没有考虑到多角度倾斜等情况,经过他的更新后我们得到了更健壮等代码:https://github.com/gre/bezier-easing/blob/master/src/index.js ,同样的,我再次把它们转换成 Shader 函数:

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 {
      return binarySubdivide(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);
}

需朋者说上事是础一发一开程和开数的目前间于得到了我们想要的运动新直能分支调二浏页器朋代说,事刚曲线了:

有了贝塞用它互不直曾经明以机会式近分扯。多接相常尔曲线这个强大的工具,基本可以满足我们对任意动画变化速率的需求了。为我们实现优雅自然的转场效果提供了览页些求时是过解些这确如目前例总站回广随能4果泉时标配使能幻近器面实的我是接,前些模小架端如结的事告机对8和水兼移合用外强大的保障。



下面通过说础开数间行屏。标控近术第发据也商蔽最移两张动图感受下匀速运动和非匀速运动对转场带来的细微感官差异(第一张图是匀速的,第二张图加了贝塞尔曲线,GIF 会影响最终效果,但能大一说为年供发架据制个似业告了到会转和大效以插各近步直了轻一过都业器项的务问一消进载滚效果达件种近步直了轻一过都业器项的务问一消进载滚效果达件种近步直了致感受):

相关链接:

本文来源于网络:查看 >
« 上一篇:对2个网页帧动画框架的调研
» 下一篇:「译」一起探讨 JavaScript 的对象
猜你喜欢
(十万案例免费下载)
评论
点击刷新
评论
相关博文
×添加代码片段