过渡 Transition

如果我们要给元素增加一些简单的效果,可以通过transition来完成,只要在设置和改变元素的属性前调用transition方法,传入时间和可选的easing参数即可。transition的easing支持css3的easing。

  1. const {Scene, Arc} = spritejs;
  2. const container = document.getElementById('adaptive');
  3. const scene = new Scene({
  4. container,
  5. width: 1200,
  6. height: 600,
  7. });
  8. const layer = scene.layer();
  9. async function createBubble() {
  10. const x = 100 + Math.random() * 1000,
  11. y = 100 + Math.random() * 400;
  12. const r = Math.round(255 * Math.random()),
  13. g = Math.round(255 * Math.random()),
  14. b = Math.round(255 * Math.random());
  15. const fillColor = `rgb(${r},${g},${b})`;
  16. const bubble = new Arc();
  17. bubble.attr({
  18. fillColor,
  19. radius: 25,
  20. x,
  21. y,
  22. });
  23. layer.append(bubble);
  24. await bubble.transition(2.0).attr({
  25. scale: [2.0, 2.0],
  26. opacity: 0,
  27. });
  28. bubble.remove();
  29. }
  30. setInterval(() => {
  31. createBubble();
  32. }, 50);

sprite.transition(...) 返回一个特殊对象(并不是原来的sprite对象),当我们调用.attr方法对它进行属性设置时,它创建一个属性动画。当我们再次对它进行属性设置时,它会结束上一次的动画进入下一段动画,这样我们就可以平滑地进行状态切换。此外我们可以通过调用.reverse方法来让当前transition状态回滚。

  1. const {Scene, Sprite, Label} = spritejs;
  2. const container = document.getElementById('adaptive');
  3. const scene = new Scene({
  4. container,
  5. width: 1200,
  6. height: 600,
  7. });
  8. const layer = scene.layer();
  9. const label = new Label('试试将鼠标移动到左右两个方块上:');
  10. label.attr({
  11. anchor: 0.5,
  12. pos: [400, 50],
  13. fontSize: '2rem',
  14. });
  15. layer.append(label);
  16. const left = new Sprite();
  17. left.attr({
  18. anchor: 0.5,
  19. pos: [300, 300],
  20. size: [200, 200],
  21. bgcolor: 'red',
  22. });
  23. layer.append(left);
  24. const right = left.cloneNode();
  25. right.attr({
  26. pos: [700, 300],
  27. bgcolor: 'green',
  28. });
  29. layer.append(right);
  30. let leftTrans = null;
  31. left.addEventListener('mouseenter', (evt) => {
  32. if(leftTrans) leftTrans.cancel();
  33. leftTrans = left.transition(1.0);
  34. leftTrans.attr({
  35. rotate: 180,
  36. bgcolor: 'green',
  37. });
  38. });
  39. left.addEventListener('mouseleave', (evt) => {
  40. leftTrans.attr({
  41. rotate: 0,
  42. bgcolor: 'red',
  43. });
  44. });
  45. let rightTrans = null;
  46. right.addEventListener('mouseenter', (evt) => {
  47. if(rightTrans) rightTrans.cancel();
  48. rightTrans = right.transition(3.0);
  49. rightTrans.attr({
  50. rotate: 720,
  51. bgcolor: 'red',
  52. });
  53. });
  54. right.addEventListener('mouseleave', (evt) => {
  55. rightTrans.reverse();
  56. });

动画 Animate

在前面的例子里我们已经看过很多动画的用法。事实上,spritejs支持Web Animations API,因此可以让精灵使用.animate方法做出各种复杂的组合动画。

我们既可以使用spritejs提供的animate动画,也可以使用其他方式,比如原生的setInterval或requestAnimationFrame。此外一些动画库提供的Tween动画,也可以很容易地结合spritejs使用。

  1. const {Scene, Path, Sprite, Gradient} = spritejs;
  2. const container = document.getElementById('adaptive');
  3. const scene = new Scene({
  4. container,
  5. width: 1200,
  6. height: 600,
  7. });
  8. const layer = scene.layer();
  9. const birdsJsonUrl = 'https://s5.ssl.qhres.com/static/5f6911b7b91c88da.json';
  10. const birdsRes = 'https://p.ssl.qhimg.com/d/inn/c886d09f/birds.png';
  11. (async function () {
  12. await scene.preload([birdsRes, birdsJsonUrl]);
  13. const d = 'M480,437l-29-26.4c-103-93.4-171-155-171-230.6c0-61.6,48.4-110,110-110c34.8,0,68.2,16.2,90,41.8C501.8,86.2,535.2,70,570,70c61.6,0,110,48.4,110,110c0,75.6-68,137.2-171,230.8L480,437Z';
  14. const gradient = new Gradient({
  15. vector: [280, 470, 680, 70],
  16. colors: [{
  17. offset: 0,
  18. color: 'rgba(255,0,0,1)',
  19. }, {
  20. offset: 0.5,
  21. color: 'rgba(255,0,0,0)',
  22. }, {
  23. offset: 1,
  24. color: 'rgba(255,0,0,0)',
  25. }],
  26. });
  27. const path = new Path({
  28. d,
  29. lineWidth: 26,
  30. fillColor: 'red',
  31. strokeColor: gradient,
  32. // lineJoin: 'bevel',
  33. });
  34. layer.append(path);
  35. window.path = path;
  36. const bird = new Sprite('bird1.png');
  37. bird.attr({
  38. anchor: [0.5, 0.5],
  39. size: [65, 45],
  40. offsetPath: d,
  41. zIndex: 200,
  42. });
  43. layer.append(bird);
  44. bird.animate([
  45. {offsetDistance: 0},
  46. {offsetDistance: 1},
  47. ], {
  48. duration: 6000,
  49. iterations: Infinity,
  50. });
  51. let i = 0;
  52. setInterval(() => {
  53. bird.attributes.texture = `bird${i++ % 3 + 1}.png`;
  54. }, 100);
  55. const startTime = Date.now();
  56. const T = 6000;
  57. requestAnimationFrame(function next() {
  58. const p = Math.PI * 2 * (Date.now() - startTime) / T;
  59. const colors = [
  60. {offset: 0, color: 'rgba(255,0,0,1)'},
  61. {offset: 0.5 + 0.5 * Math.abs(Math.sin(p)), color: 'rgba(255,0,0,0)'},
  62. {offset: 1, color: 'rgba(255,0,0,0)'},
  63. ];
  64. const gradients = new Gradient({
  65. vector: [280, 470, 680, 70],
  66. colors,
  67. });
  68. path.attr({strokeColor: gradients});
  69. requestAnimationFrame(next);
  70. });
  71. }());

比起使用原生timer或者第三方库,直接使用spritejs提供的animate动画有一个额外的好处,就是它默认基于layer的timeline。也就是说我们可以通过控制layer的timeline来控制动画播放的速度,方便地加速、减速、暂停甚至回放动画。

  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. });
  8. const layer = scene.layer();
  9. const birdsJsonUrl = 'https://s5.ssl.qhres.com/static/5f6911b7b91c88da.json';
  10. const birdsRes = 'https://p.ssl.qhimg.com/d/inn/c886d09f/birds.png';
  11. (async function () {
  12. const timeline = layer.timeline;
  13. const playbackRate = document.getElementById('playbackRate');
  14. const speedUp = document.getElementById('speedUp');
  15. const slowDown = document.getElementById('slowDown');
  16. const pause = document.getElementById('pause');
  17. const resume = document.getElementById('resume');
  18. function updateSpeed() {
  19. playbackRate.innerHTML = `playbackRate: ${timeline.playbackRate.toFixed(1)}`;
  20. }
  21. speedUp.addEventListener('click', () => {
  22. timeline.playbackRate += 0.5;
  23. updateSpeed();
  24. });
  25. slowDown.addEventListener('click', () => {
  26. timeline.playbackRate -= 0.5;
  27. updateSpeed();
  28. });
  29. pause.addEventListener('click', () => {
  30. timeline.playbackRate = 0;
  31. updateSpeed();
  32. });
  33. resume.addEventListener('click', () => {
  34. timeline.playbackRate = 1.0;
  35. updateSpeed();
  36. });
  37. await scene.preload([birdsRes, birdsJsonUrl]);
  38. for(let i = 0; i < 10; i++) {
  39. if(i !== 5 && i !== 9) {
  40. const bird = new Sprite('bird1.png');
  41. bird.attr({
  42. anchor: [0.5, 0.5],
  43. pos: [-50, 100 + (i % 5) * 80],
  44. scale: 0.6,
  45. });
  46. layer.append(bird);
  47. bird.animate([
  48. {texture: 'bird1.png'},
  49. {texture: 'bird2.png'},
  50. {texture: 'bird3.png'},
  51. {texture: 'bird1.png'},
  52. ], {
  53. duration: 500,
  54. iterations: Infinity,
  55. easing: 'step-end',
  56. });
  57. const delay = i < 5 ? Math.abs(2 - i) * 300 : (4 - Math.abs(7 - i)) * 300;
  58. bird.animate([
  59. {x: -50},
  60. {x: 1100},
  61. {x: -50},
  62. ], {
  63. delay,
  64. duration: 6000,
  65. // direction: 'alternate',
  66. iterations: Infinity,
  67. });
  68. bird.animate([
  69. {scale: [0.6, 0.6]},
  70. {scale: [-0.6, 0.6]},
  71. {scale: [0.6, 0.6]},
  72. ], {
  73. delay,
  74. duration: 6000,
  75. iterations: Infinity,
  76. easing: 'step-end',
  77. });
  78. }
  79. }
  80. }());

layer的timeline是TimeLine类的一个对象,TimeLine类定义于sprite-timeline,这是一个独立的库,也可以单独作于其他方式的动画。

spritejs动画功能非常丰富,关于动画的其他内容,可参考高级用法:动画

滤镜 Filter

spritejs支持canvas滤镜,能够方便地给元素添加各种滤镜。

  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. });
  8. const layer = scene.layer();
  9. const images = [
  10. {id: 'girl1', src: 'https://p5.ssl.qhimg.com/t01feb7d2e05533ca2f.jpg'},
  11. {id: 'girl2', src: 'https://p5.ssl.qhimg.com/t01deebfb5b3ac6884e.jpg'},
  12. ];
  13. (async function () {
  14. function applyFilters(texture, filters, y, scale = 1) {
  15. filters.forEach((filter, i) => {
  16. const s = new Sprite();
  17. s.attr({
  18. texture,
  19. pos: [100 + i * 150, y],
  20. scale,
  21. filter,
  22. });
  23. layer.append(s);
  24. });
  25. }
  26. await scene.preload(...images);
  27. applyFilters('girl1', [
  28. 'none',
  29. 'brightness(150%)',
  30. 'grayscale(50%)',
  31. 'blur(12px)',
  32. 'drop-shadow(15, 15, 5, #033)',
  33. 'hue-rotate(45)',
  34. ], 100, 0.3);
  35. applyFilters('girl2', [
  36. 'none',
  37. 'invert(100%)',
  38. 'opacity(70%)',
  39. 'saturate(20%)',
  40. 'sepia(100%)',
  41. 'hue-rotate(135)',
  42. ], 300, 0.6);
  43. }());

渐变 Gradient

与旧版不同,SpriteJS Next 通过Gradient类创建渐变,根据vector参数个数不同,分别创建LinearGradient和RadialGradient。

目前版本暂时有个缺陷,不能给同一个元素的stroke和fill同时设置渐变,后续版本中可能会修正。

  1. const {Scene, Sprite, Gradient, Label, Path} = 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 box = new Sprite();
  11. box.attr({
  12. borderWidth: 10,
  13. borderColor: new Gradient({
  14. vector: [0, 0, 170, 170],
  15. colors: [
  16. {offset: 0, color: 'red'},
  17. {offset: 0.5, color: 'yellow'},
  18. {offset: 1, color: 'green'},
  19. ],
  20. }),
  21. // borderColor: 'green',
  22. bgcolor: new Gradient({
  23. vector: [0, 150, 150, 0],
  24. colors: [
  25. {offset: 0, color: '#fff'},
  26. {offset: 0.5, color: 'rgba(33, 33, 77, 0.7)'},
  27. {offset: 1, color: 'rgba(128, 45, 88, 0.5)'},
  28. ],
  29. }),
  30. pos: [150, 50],
  31. size: [150, 150],
  32. borderRadius: 15,
  33. });
  34. layer.append(box);
  35. const label = new Label('Hello SpriteJS~~');
  36. label.attr({
  37. lineWidth: 6,
  38. fillColor: new Gradient({
  39. vector: [35, 35, 50, 350, 350, 600],
  40. colors: [
  41. {offset: 0, color: '#777'},
  42. {offset: 0.5, color: '#ccc'},
  43. {offset: 1, color: '#333'},
  44. ],
  45. }),
  46. pos: [500, 50],
  47. font: '48px Arial',
  48. });
  49. layer.append(label);
  50. const path = new Path();
  51. path.attr({
  52. d: 'M480,50L423.8,182.6L280,194.8L389.2,289.4L356.4,430L480,355.4L480,355.4L603.6,430L570.8,289.4L680,194.8L536.2,182.6Z',
  53. normalize: true,
  54. rotate: 30,
  55. scale: 0.7,
  56. fillColor: new Gradient({
  57. vector: [300, 300, 100, 100],
  58. colors: [
  59. {offset: 0, color: 'red'},
  60. {offset: 0.5, color: 'yellow'},
  61. {offset: 1, color: 'green'},
  62. ],
  63. }),
  64. pos: [700, 360],
  65. });
  66. layer.append(path);