论坛首页 Web前端技术论坛

运动曲线研究(缓动效果)

浏览 7213 次
精华帖 (1) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-10-14  
刚刚学习了一下网页动画中上的缓动效果,分享一下学习心得。

缓动曲线的概念:

缓动曲线是一个0为起点的连续函数曲线,x轴表示时间变化,y轴表示位移变化。曲线的斜率反映出运动的数度。

缓动效果在Flash动画中比较常见,用于模拟一些现实中常见的运动轨迹,或者制造一些超绚的效果。
而且新版本的Flash中,内置了一些常用的缓动曲线函数。

可惜,Flash的这些曲线函数不是开源的,我们不知道内部如何实现,也就无法将其移植到JS中。感受其绚丽的同时,未免有一丝遗憾。

于是乎,自己琢磨琢磨。

首先,我对Flash的渐变函数接口非常不满。
搞那么多参数干吗?
要描述一个区间的渐变运动特征,只需一个y = f(x)足已。那么一大堆参数,真够罗嗦。
//原理:我们以终点位移为参考,只需要知道中间个点相对于最终位移,我们就能确定运动的规律。
y= f(x)
//约定
//x ∈ [0,1] #将x变化换算成[0,1]是最简单不过的操作
//f(0) = 0 #运动是连续的嘛^_^.
//f(1) != 0 #如果f(1) = 0了,那不就没有运动嘛,中间即使有位移,我也无法计算中间的位移相对于总体位移的比例。

曲线转换
每种类型的渐变都有三种变形

渐入(in)

在过渡的开始提供缓动效果。

渐出(out)

在过渡的结尾提供缓动效果。

渐入渐出(inOut/Both)

在过渡的开始和结尾提供缓动效果。


其中,我们只要知道一个的曲线,其他两个都可以转换生成:
知道渐入曲线之后,将其相对于(0.5,0.5)点绘制镜像,就是一个缓出运动,分段叠加就是一个完整的缓入缓出运动。



首先,常见的加速/减速运动:
初中物理就能搞定。
加速渐变函数为(easeIn):
y=x*x; //y轴比例常数无需考虑

这是一个简单的2次曲线,表现一个渐入运动。
简单的变换一下:y = 1-(1-x)*(1-x) 减速运动(easeOut)
复杂一点:
y = x>0.5? 1-2(1-x)*(1-x) :
2*x*x :
先加速后减速运动(easeBoth)




既然有二次曲线,很自然就想到三次、四次曲线。是的,这些曲线都有类似特征,区别在中间更陡峭,两头平缓(缓入缓出)

接下来,我就想实现一下弹动效果:

这类效果就好像一个甲虫飞到蜘蛛网上,在网上抖动两下,静下来听天由命。
抖动,周期运动,好,我们很快就想到正弦曲线。
方法基本正确,不过我起初还是走弯路了,我自作聪明的想着延长开始的半周期(x轴边形处理,振动让周期先大后小)。
但最终发现效果非常不理想,最后查看yui的实现。模仿一下,走出了这个误区。
我们通常看到的振荡移位效果,都是开始移动了较长位移,给人一种开始的振动周期更长的错觉,振动周期是不需要变化的。
纠正这个错误后,实现曲线函数如下:

y = Math.pow(1024,x-1)*Math.sin(x*((2*(period||1)+0.5)*Math.PI));

利用指数函数的第二象限的渐变特征变形,取处理正弦波形的振幅,达到一个衰减的效果。



趁热打铁,看看yui的其他几类渐变效果:

回退起步效果。
喜欢看动画片的话,你一定记得这个常见的场面,当一个家伙想快跑的时候,一点要先回撤一段距离,能后如突然加速前进。ok要的就是这个效果。
实现其实也很简单,一个二次曲线就可以搞定

y = x*(x-(backDistance||0.1)*4)



撞墙效果

这个名字可能不太合适吧,应该叫撞地效果更合适,鉴于撞墙这个名词更常见一些,也就标题党一回好了:)
玩过弹球吧,弹球的运动规律一定还记得。
对就是这种轨迹。
运功轨迹就是若干条二次曲线的分段拼接。改写一个yui里面的模拟实现。

this.bounceOut = function (x) {
if (x < (1/2.75)) {
return x*x;
} else if (x < (2/2.75)) {
return (x-=(1.5/2.75))*x + .75/7.5625;
} else if (x < (2.5/2.75)) {
return (x-=(2.25/2.75))*x + .9375/7.5625;
}
return (x-=(2.625/2.75))*x + .984375/7.5625;
};
这里手动指出了一大堆参数,其实,这些参数都可以通过计算得出,偷个懒,就这么地吧,^_^
   发表时间:2007-10-14  
演示url (IE不支持)
0 请登录后投票
   发表时间:2007-10-14  
以前也写过加速减速动画代码,也发上来
0 请登录后投票
   发表时间:2007-10-14  
google的code网站里面
搜索tweener项目。ou
里面有很多曲线函数
0 请登录后投票
   发表时间:2007-10-14  
我以前曾经写过Tween类,用来达到类似flash的补间效果。

用法如下:
new Tween(div.style, 'left', new Measure(div.style.left), new Measure('0px'), 3000, 40, Math.sqrt);
表示产生一个补间动画,让div.style.left在3秒内从当前的值变到0px。刷新间隔为40毫秒,运动方程是Math.sqrt。

最后两个参数可以省略,默认间隔是40毫秒,默认方程是Tween.linear,此外Tween上还提供了Tween.quadic方程。

如果运动方程使用自定义函数的话,函数f(x)应该在[0,1]区间上有定义,并满足f(0)=0, f(1)=1。



代码如下:
(代码仅供参考,因为没有包括所有用到的方法,所以不能直接运行)
	Tween.prototype.start = function () {
		this.startTime = new Date().getTime();
		this.before();
		this._task = setTimeout(refresh, this.tick);
		var self = this;
		function refresh() {
			self.refresh();
		}
	}

	Tween.prototype.stop = function () {
		//clearInterval(this._task);
		this.after();
		this.startTime = null;
	}

	Tween.prototype.refresh = function () {
		var offset = new Date().getTime() - this.startTime;
		var x = offset / this.time;
		if (x >= 1) {
			this.stop();
		} else {
			var y = this.f(x);
			var v = this.toValue.minus(this.fromValue).multiply(y).plus(this.fromValue);
			this.assign(v);
			this._task = setTimeout(refresh, this.tick);
		}
		var self = this;
		function refresh() {
			self.refresh();
		}
	}

	Tween.linear = function linear(x) { return x; }
	Tween.quadric = function quadric(x) { return 1 - Math.sqrt(1 - x * x); }

	function Tween(target, field, fromValue, toValue, time, tick, f) {
		
		this.fromValue = fromValue;
		this.toValue = toValue;
		this.time = time;
		this.tick = tick || 40;
		this.f = f || Tween.linear;
		
		this.before = new EventHandler();
		this.after = new EventHandler();

		this.before.add(function () {
			this.assign(this.fromValue);
		});
		this.after.add(function () {
			this.assign(this.toValue);
		});
		
		if (isFunction(field)) {
			this.assign = function (value) {
				field.call(target, value);
			}
		} else if (isFunction(target[field])) {
			this.assign = function (value) {
				target[field](value);
			}
		} else {
			this.assign = function (value) {
				target[field] = value;
			}
		}

	}
	Number.prototype.plus = function (x) {
		assert (isNumber(x));
		return this + x;
	}

	Number.prototype.minus = function (x) {
		assert (isNumber(x));
		return this - x;
	}

	Number.prototype.multiply = function (x) {
		if (isNumber(x))
			return this * x;
		if (x.multiply)
			return x.multiply(this);
		assert (false);
	}

	Number.prototype.divide = function (x) {
		assert (isNumber(x));
		return this / x;
	}
	Measure.prototype.plus = function measure_plus(x) {
		if (this.unit == x.unit)
			return new Measure(this.qt + x.qt, this.unit);
		assert (false, String.concat(this, ' is incompatible with ', x));
	}

	Measure.prototype.minus = function measure_minus(x) {
		if (this.unit == x.unit)
			return new Measure(this.qt - x.qt, this.unit);
		assert (false, String.concat(this.unit, ' is incompatible with ', x.unit));
	}

	// TODO: composite unit
	Measure.prototype.multiply = function measure_multiply(x) {
		if (isNumber(x))
			return new Measure(this.qt * x, this.unit);
		assert (false, String.concat(x));
	}

	Measure.prototype.divide = function measure_divide(x) {
		if (isNumber(x))
			return new Measure(this.qt / x, this.unit);
		else if (Measure.isInstance(x) && this.unit == x.unit)
			return this.qt / x.qt;
		assert (false);
	}

	Measure.prototype.toString = function measure_toString(x) {
		return String.concat(this.qt, this.unit);
	}

	function Measure(qt, unit) {
		if (!isNumber(qt)) {
			var s = /[0-9.+-]*/(qt)[0];
			unit = String.substring(qt, s.length);
			qt = parseFloat(s);
		}
		this.qt = qt;
		this.unit = unit;
	}
0 请登录后投票
   发表时间:2007-10-14  
hax 写道

如果运动方程使用自定义函数的话,函数f(x)应该在[0,1]区间上有定义,并满足f(0)=0, f(1)=1。


这个f(1)=1约束还是可以省去的.
只要f(1) != 0就可以了,我们要的只是一个比例.如果强制f(1)=1,那又多出了一处比例换算.

不过,加上这个约束也有一个好处,那就是方程变换的时候简单一些.

0 请登录后投票
   发表时间:2007-10-14  
可以参考一下script.aculo.us的曲线函数,内置了不少呢
0 请登录后投票
   发表时间:2007-10-15  
jindw 写道
hax 写道

如果运动方程使用自定义函数的话,函数f(x)应该在[0,1]区间上有定义,并满足f(0)=0, f(1)=1。


这个f(1)=1约束还是可以省去的.
只要f(1) != 0就可以了,我们要的只是一个比例.如果强制f(1)=1,那又多出了一处比例换算.

不过,加上这个约束也有一个好处,那就是方程变换的时候简单一些.



是可以省去,直接f'(x) = f(x) / f(1)用f'就可以了。实际上甚至f(0)=0也不必满足,因为总是可以换算到[0,1]区间上。不过做好规约比较严谨一点,你可以提供一个额外的换算函数来协助用户进行方程变换。

function map01(f, x0, x1) {
  x0 = x0 || 0
  x1 = x1 || 1
  x10 = x1 - x0
  f0 = f(x0)
  f1 = f(x1)
  f10 = f1 - f0
  return function (x) {
    return (f(x * x10 + x0) - f0) / f10
  }
}
0 请登录后投票
   发表时间:2007-10-15  
我以前做过一些类似的东西(但不是js的)
利用的就是 物理里面的运动学基础知识.

我把楼主说的那些网页特效当作是一个3维空间内的运动.
如 移动可以看作是  X Y轴上的变化.
淡入淡出可以看作是Z轴上的变化.

常规运动可以分为:
匀速运动
匀加速运动
匀减速运动
"加速度匀速变大"的变加速(加速度<0时为变减速)运动
"加速度匀速减小"的变加速(加速度<0时为变减速)运动

具体是那种运动,由初速度 加速度 时间 当前位置 这4者来决定.

把网页效果用运动学的知识来模拟.

例如弹球运动, 传入位置(高度) 重力加速度, 每次碰撞损失的动能(忽略空气阻力等,只记录碰撞损耗)

程序自动演算出运动情况.

平抛运动 传入位置, XY初速度,  重力加速度, X加速度(<0),

很多复杂的运动 如类似正弦曲线那种 都可以通过对加速度的控制来实现.

0 请登录后投票
   发表时间:2007-10-15  
hax 写道
jindw 写道
hax 写道

如果运动方程使用自定义函数的话,函数f(x)应该在[0,1]区间上有定义,并满足f(0)=0, f(1)=1。


这个f(1)=1约束还是可以省去的.
只要f(1) != 0就可以了,我们要的只是一个比例.如果强制f(1)=1,那又多出了一处比例换算.

不过,加上这个约束也有一个好处,那就是方程变换的时候简单一些.



是可以省去,直接f'(x) = f(x) / f(1)用f'就可以了。实际上甚至f(0)=0也不必满足,因为总是可以换算到[0,1]区间上。不过做好规约比较严谨一点,你可以提供一个额外的换算函数来协助用户进行方程变换。

function map01(f, x0, x1) {
  x0 = x0 || 0
  x1 = x1 || 1
  x10 = x1 - x0
  f0 = f(x0)
  f1 = f(x1)
  f10 = f1 - f0
  return function (x) {
    return (f(x * x10 + x0) - f0) / f10
  }
}


从实现来看,省略f(1) =1的约束是可以简化程序的,而省略f(0) =0,回增加代码。

反正我们需要的是一个高度的相对比例。

我们只要再动画开始计算出 var end = f(1)
能后每点的位移是
y = f(x)*max/end;


如果我们要强制f(1) =1。
那么:  f(x) = f(x)/end;
y=(f(x)/end)*max;

看似代码一样,但是一般的实现来说,最终的换算是在统一的地方,我们完全可以吧这个换算放再这个统一的地方。而让繁多的曲线函数的代码简单一些。

但是,如果我们允许 f(0) !=0.
那么程序就繁杂多了。
var begin = f(0)
y=((f(x)-begin )/(end-begin))*max;


写到这里,都有点怀疑自己的立场了,呵呵,不过,还是坚持我的观点,为什么?呵呵,一个有点差强的理由,省略f(1) ==1 不会影响程序效率,而省略 f(0) ==0则有点
0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics