Transition 过渡动画

spritejs最简单的动画方式是transition动画:

  1. // 让精灵在1s内向右移动50个像素
  2. sprite.transition(1.0).attr({
  3. x: x => x + 50
  4. })

sprite.transition(sec).attr(...)总是返回一个promise,因此我们可以很容易实现连续的动画:

  1. const {Scene, Sprite} = spritejs;
  2. const container = document.getElementById('adaptive');
  3. const scene = new Scene({
  4. container,
  5. width: 1200,
  6. height: 600,
  7. // contextType: '2d',
  8. });
  9. const layer = scene.layer();
  10. (async function () {
  11. const sprite = new Sprite({
  12. anchor: 0.5,
  13. bgcolor: 'red',
  14. pos: [500, 300],
  15. size: [200, 200],
  16. borderRadius: 50,
  17. });
  18. layer.append(sprite);
  19. await sprite.transition(2.0)
  20. .attr({
  21. bgcolor: 'green',
  22. width: width => width + 100,
  23. });
  24. await sprite.transition(1.0)
  25. .attr({
  26. bgcolor: 'orange',
  27. height: height => height + 100,
  28. });
  29. }());

sprite.transition(sec) 本身返回一个Transition对象,它也可以多次设置attr(),每次设置的时候会自动将上一次的transition结束,这样实现类似下面这样的hover效果会很方便:

  1. const {Scene, Sprite, Path, Group} = spritejs;
  2. const container = document.getElementById('adaptive');
  3. const scene = new Scene({
  4. container,
  5. width: 1200,
  6. height: 600,
  7. // contextType: '2d',
  8. });
  9. const layer = scene.layer();
  10. (async function () {
  11. await scene.preload([
  12. 'https://p5.ssl.qhimg.com/t01f47a319aebf27174.png',
  13. 'https://s3.ssl.qhres.com/static/a6a7509c33a290a6.json',
  14. ]);
  15. const robot = new Sprite('guanguan1.png');
  16. robot.attr({
  17. anchor: 0.5,
  18. pos: [710, 210],
  19. scale: 0.4,
  20. rotate: 45,
  21. // zIndex: 2000,
  22. });
  23. layer.append(robot);
  24. const d = 'M235.946483,75.0041277 C229.109329,53.4046689 214.063766,34.845093 195.469876,22.3846101 C175.428247,8.9577702 151.414895,2 127.314132,2 C75.430432,2 31.6212932,32.8626807 18.323944,74.9130141 C8.97646468,77.1439182 2,85.5871171 2,95.7172992 C2,104.709941 7.49867791,112.371771 15.2700334,115.546944 C15.8218133,115.773348 16.6030463,122.336292 16.8270361,123.236385 C22.1235768,144.534892 35.4236577,163.530709 52.5998558,176.952027 C52.6299032,176.976876 52.6626822,177.001726 52.6954612,177.026575 C72.5513428,192.535224 98.5478246,202 127.043705,202 C152.034964,202 176.867791,194.597706 197.428422,180.146527 C215.659011,167.335395 230.201962,148.621202 236.52831,126.969284 C237.566312,123.421373 238.549682,119.685713 239.038636,116.019079 C239.044099,115.983185 239.074146,115.444787 239.082341,115.442025 C246.673412,112.184022 252,104.580173 252,95.7172992 C252,85.6892748 245.15192,77.3371896 235.946483,75.0041277';
  25. const shadowD = 'M82.1534529,43 C127.525552,43 164.306906,33.6283134 164.306906,21.663753 C164.306906,9.6991926 127.525552,0 82.1534529,0 C36.7813537,0 0,9.6991926 0,21.663753 C0,33.6283134 36.7813537,43 82.1534529,43 Z';
  26. const shadow = new Path();
  27. shadow.attr({
  28. d: shadowD,
  29. normalize: true,
  30. fillColor: '#000000',
  31. opacity: 0.05,
  32. pos: [500, 434],
  33. scale: [1.3, 1.2],
  34. });
  35. layer.append(shadow);
  36. const lemon = new Path();
  37. lemon.attr({
  38. d,
  39. normalize: true,
  40. pos: [500, 300],
  41. fillColor: '#fed330',
  42. scale: 1.4,
  43. });
  44. layer.append(lemon);
  45. const lemonGroup = new Group();
  46. lemonGroup.attr({
  47. anchor: 0.5,
  48. pos: [610, 300],
  49. size: [180, 180],
  50. bgcolor: '#faee35',
  51. border: [6, '#fdbd2c'],
  52. borderRadius: 90,
  53. scale: 1.5,
  54. });
  55. layer.append(lemonGroup);
  56. const d2 = 'M0,0L0,100A15,15,0,0,0,50,86.6z';
  57. for(let i = 0; i < 12; i++) {
  58. const t = new Path();
  59. t.attr({
  60. d: d2,
  61. scale: 0.65,
  62. lineWidth: 2,
  63. strokeColor: '#fff',
  64. fillColor: '#f8c32d',
  65. rotate: 30 * i,
  66. });
  67. lemonGroup.append(t);
  68. }
  69. lemonGroup.animate([
  70. {rotate: 360},
  71. ], {
  72. duration: 10000,
  73. iterations: Infinity,
  74. });
  75. const transition = robot.transition(0.3);
  76. lemonGroup.addEventListener('mouseenter', (evt) => {
  77. layer.timeline.playbackRate = 3.0;
  78. transition.attr({
  79. pos: [730, 190],
  80. });
  81. });
  82. lemonGroup.addEventListener('mouseleave', (evt) => {
  83. layer.timeline.playbackRate = 1.0;
  84. transition.attr({
  85. pos: [710, 210],
  86. });
  87. });
  88. }());

Web Animations API

spritejs动画支持的是几乎标准的Web Animations API

每一个sprite有一个animate方法,该方法用来定义并运行动画,它返回一个animation对象,该对象有几种不同的状态,分别如下:

状态 描述
idle 当前动画未开始
pending 当前动画已开始未结束,但元素还未运动或已运动结束
running 当前动画正在运行
paused 当前动画被暂停
finished 当前动画已结束

根据Web Animations API,animate方法有两个参数,分别是动画属性的关键帧序列和一个timeing对象。

timeing对象有以下属性:

属性名 属性类型 初始值 属性描述
delay Number 0 动画多长时间后开始运行,单位是毫秒
endDelay Number 0 动画执行完毕后多长时间之后结束,单位是毫秒
fill 枚举: ‘none’, ‘forwards’, ‘backwards’, ‘both’ ‘none’ 如果这个属性为’none’,那么元素的动画效果只有在’running’和’paused’状态时有效,在其他状态下元素回到动画前状态。如果这个属性为 ‘forwards’,那么动画结束后,元素保持在动画结束时的状态。如果这个属性为’backwards’,那么动画处于开始前pending状态时,元素保持在动画第一帧的状态。如果这个属性为’both’,那么元素在动画开始前保持第一帧状态,并在动画结束后保持最后一帧状态。
iterations Number 1 动画播放的次数,可以是整数,也可以是小数
direction 枚举: ‘default’, ‘reverse’, ‘alternate’, ‘alternate-reverse’ ‘default’ 动画播放的方向,默认是正向播放,如果该属性设置为’reverse’,则动画反向播放,如果设置为alternate,则在iterations > 1的时候正反交替播放
duration Number 0 动画播放一次的时长
easing String ‘linear’ 动画的easing函数,可以是linear, ease, ease-in, ease-out, ease-in-out, step-start, step-end或者cubic-bezier函数比如cubic-bezier(0.42, 0, 0.58, 1)
  1. const {Scene, Sprite} = spritejs;
  2. const container = document.getElementById('adaptive');
  3. const scene = new Scene({
  4. container,
  5. width: 1200,
  6. height: 600,
  7. // contextType: '2d',
  8. });
  9. const layer = scene.layer();
  10. const sprite = new Sprite();
  11. sprite.attr({
  12. anchor: [0.5, 0.5],
  13. pos: [600, 300],
  14. bgcolor: 'red',
  15. size: [50, 50],
  16. borderRadius: 25,
  17. translate: [0, -200],
  18. transformOrigin: [0, 200],
  19. });
  20. sprite.animate([
  21. {rotate: 0},
  22. {rotate: 360},
  23. ], {
  24. duration: 3000,
  25. iterations: Infinity,
  26. });
  27. layer.append(sprite);

动画的 Timeline

sprite所属的layer上有一个timeline属性,这是一个Timeline对象,所有layer上运行的动画使用这个timeline对象来获得时间线,这样当我们改变layer的时间线的时候,我们就能影响到所有元素的动画时间。

  1. const {Scene, Sprite} = spritejs;
  2. const container = document.getElementById('adaptive');
  3. const scene = new Scene({
  4. container,
  5. width: 1200,
  6. height: 600,
  7. // contextType: '2d',
  8. });
  9. const layer = scene.layer();
  10. (async function () {
  11. await scene.preload({id: 'snow', src: 'https://p5.ssl.qhimg.com/t01bfde08606e87f1fe.png'});
  12. const [speed1, speed2, speed4, halfSpeed, pause, reversePlay]
  13. = document.querySelectorAll('#speed1, #speed2, #speed4, #halfSpeed, #pause, #reversePlay');
  14. const timeline = layer.timeline;
  15. speed1.addEventListener('click', (evt) => {
  16. timeline.playbackRate = 1.0;
  17. });
  18. speed2.addEventListener('click', (evt) => {
  19. timeline.playbackRate = 2.0;
  20. });
  21. speed4.addEventListener('click', (evt) => {
  22. timeline.playbackRate = 4.0;
  23. });
  24. halfSpeed.addEventListener('click', (evt) => {
  25. timeline.playbackRate = 0.5;
  26. });
  27. pause.addEventListener('click', (evt) => {
  28. timeline.playbackRate = 0;
  29. });
  30. reversePlay.addEventListener('click', (evt) => {
  31. timeline.playbackRate = -1.0;
  32. });
  33. function addRandomSnow() {
  34. const snow = new Sprite('snow');
  35. const x0 = 20 + Math.random() * 1100,
  36. y0 = -100;
  37. snow.attr({
  38. anchor: [0.5, 0.5],
  39. pos: [x0, y0],
  40. size: [50, 50],
  41. });
  42. snow.animate([
  43. {x: x0 - 10},
  44. {x: x0 + 10},
  45. ], {
  46. duration: 1000,
  47. fill: 'forwards',
  48. direction: 'alternate',
  49. iterations: Infinity,
  50. easing: 'ease-in-out',
  51. });
  52. const dropAnim = snow.animate([
  53. {y: -100, rotate: 0},
  54. {y: 700, rotate: 360},
  55. ], {
  56. duration: 10000,
  57. fill: 'forwards',
  58. });
  59. dropAnim.finished.then(() => {
  60. snow.remove();
  61. });
  62. layer.append(snow);
  63. }
  64. setInterval(addRandomSnow, 200);
  65. }());

回放playbackRate < 0的时候,动画回复到初始状态然后结束,因此旧的雪花往上飘,而新的雪花动画一开始就结束了,所以看不到新雪花从上方飘落。

使用第三方动画库

如果不喜欢Web Animation API这种动画形式的话,spritejs的Timeline还能够很方便地与第三方库一同使用。这里以TweenJS为例:

  1. const {Scene, Sprite} = spritejs;
  2. const container = document.getElementById('adaptive');
  3. const scene = new Scene({
  4. container,
  5. width: 1200,
  6. height: 600,
  7. // contextType: '2d',
  8. });
  9. const layer = scene.layer();
  10. const sprite = new Sprite();
  11. sprite.attr({
  12. anchor: [0.5, 0.5],
  13. pos: [600, 300],
  14. bgcolor: 'rgb(128, 0, 255)',
  15. size: [100, 100],
  16. });
  17. layer.append(sprite);
  18. const coords = {rotate: 0};
  19. /* globals TWEEN */
  20. new TWEEN.Tween(coords)
  21. .to({rotate: 360}, 5000)
  22. .easing(TWEEN.Easing.Quadratic.Out)
  23. .onUpdate(() => {
  24. const rotate = coords.rotate,
  25. radian = Math.PI * rotate / 180,
  26. red = 128 + Math.round(127 * Math.sin(radian)),
  27. green = Math.round(rotate) % 128,
  28. blue = 128 + Math.round(127 * Math.cos(radian));
  29. const bgcolor = `rgb(${red}, ${green}, ${blue})`;
  30. sprite.attr({rotate, bgcolor});
  31. })
  32. .repeat(Infinity)
  33. .start();
  34. function animate() {
  35. requestAnimationFrame(animate);
  36. TWEEN.update(layer.timeline.currentTime);
  37. }
  38. requestAnimationFrame(animate);
  39. const [speed1, speed2, speed4, halfSpeed, pause, reversePlay]
  40. = document.querySelectorAll('#tweenjs-speed1, #tweenjs-speed2, #tweenjs-speed4, #tweenjs-halfSpeed, #tweenjs-pause, #tweenjs-reversePlay');
  41. const timeline = layer.timeline;
  42. speed1.addEventListener('click', (evt) => {
  43. timeline.playbackRate = 1.0;
  44. });
  45. speed2.addEventListener('click', (evt) => {
  46. timeline.playbackRate = 2.0;
  47. });
  48. speed4.addEventListener('click', (evt) => {
  49. timeline.playbackRate = 4.0;
  50. });
  51. halfSpeed.addEventListener('click', (evt) => {
  52. timeline.playbackRate = 0.5;
  53. });
  54. pause.addEventListener('click', (evt) => {
  55. timeline.playbackRate = 0;
  56. });
  57. reversePlay.addEventListener('click', (evt) => {
  58. timeline.playbackRate = -1;
  59. });