过渡 Transition

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

效果 - 图1

  1. const scene = new Scene('#transition', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. async function createBubble() {
  4. const x = 100 + Math.random() * 1340,
  5. y = 100 + Math.random() * 400;
  6. const r = Math.round(255 * Math.random()),
  7. g = Math.round(255 * Math.random()),
  8. b = Math.round(255 * Math.random());
  9. const bgcolor = `rgb(${r},${g},${b})`;
  10. const bubble = new Sprite();
  11. bubble.attr({
  12. anchor: 0.5,
  13. bgcolor,
  14. size: [50, 50],
  15. x,
  16. y,
  17. borderRadius: 25,
  18. });
  19. layer.append(bubble);
  20. await bubble.transition(2.0).attr({
  21. scale: [2.0, 2.0],
  22. opacity: 0,
  23. });
  24. bubble.remove();
  25. }
  26. setInterval(() => {
  27. createBubble();
  28. }, 50);

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

试试将鼠标移动到左右两个方块上:

效果 - 图2

  1. const scene = new Scene('#transition-toggle', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const left = new Sprite();
  4. left.attr({
  5. anchor: 0.5,
  6. pos: [400, 300],
  7. size: [200, 200],
  8. bgcolor: 'red',
  9. });
  10. layer.append(left);
  11. const right = left.cloneNode();
  12. right.attr({
  13. pos: [900, 300],
  14. bgcolor: 'green',
  15. });
  16. layer.append(right);
  17. let leftTrans = null;
  18. left.on('mouseenter', (evt) => {
  19. leftTrans = left.transition(1.0);
  20. leftTrans.attr({
  21. rotate: 180,
  22. bgcolor: 'green',
  23. });
  24. });
  25. left.on('mouseleave', (evt) => {
  26. leftTrans.attr({
  27. rotate: 0,
  28. bgcolor: 'red',
  29. });
  30. });
  31. let rightTrans = null;
  32. right.on('mouseenter', (evt) => {
  33. rightTrans = right.transition(3.0);
  34. rightTrans.attr({
  35. rotate: 720,
  36. bgcolor: 'red',
  37. });
  38. });
  39. right.on('mouseleave', (evt) => {
  40. rightTrans.reverse();
  41. });

动画 Animate

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

效果 - 图3

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

  1. ;(async function () {
  2. const birdsJsonUrl = 'https://s5.ssl.qhres.com/static/5f6911b7b91c88da.json';
  3. const birdsRes = 'https://p.ssl.qhimg.com/d/inn/c886d09f/birds.png';
  4. const scene = new Scene('#animations', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  5. const layer = scene.layer('fglayer');
  6. 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';
  7. await scene.preload([birdsRes, birdsJsonUrl]);
  8. const path = new Path();
  9. path.attr({
  10. anchor: [0.5, 0.5],
  11. pos: [770, 300],
  12. path: {d, trim: true},
  13. lineWidth: 26,
  14. lineCap: 'round',
  15. gradients: {
  16. strokeColor: {
  17. vector: [0, 400, 400, 0],
  18. colors: [{
  19. offset: 0,
  20. color: 'rgba(255,0,0,1)',
  21. }, {
  22. offset: 0.5,
  23. color: 'rgba(255,0,0,0)',
  24. }, {
  25. offset: 1,
  26. color: 'rgba(255,0,0,0)',
  27. }],
  28. },
  29. fillColor: {
  30. vector: [0, 0, 400, 400],
  31. colors: [{
  32. offset: 0,
  33. color: 'rgba(255,0,0,0.7)',
  34. }, {
  35. offset: 1,
  36. color: 'rgba(255,255,0,0.7)',
  37. }],
  38. },
  39. },
  40. });
  41. layer.append(path);
  42. const s = new Sprite('bird1.png');
  43. const pathSize = path.pathSize;
  44. s.attr({
  45. anchor: [0.5, 0.5],
  46. pos: [770 - pathSize[0] / 2, 300 - pathSize[1] / 2],
  47. size: [80, 50],
  48. offsetPath: path.svg.d,
  49. zIndex: 200,
  50. });
  51. s.animate([
  52. {offsetDistance: 0},
  53. {offsetDistance: 1},
  54. ], {
  55. duration: 6000,
  56. iterations: Infinity,
  57. });
  58. let i = 0;
  59. setInterval(() => {
  60. s.textures = [`bird${i++ % 3 + 1}.png`];
  61. }, 100);
  62. const startTime = Date.now();
  63. const T = 6000;
  64. requestAnimationFrame(function next() {
  65. const p = Math.PI * 2 * (Date.now() - startTime) / T;
  66. const colors = [
  67. {offset: 0, color: 'rgba(255,0,0,1)'},
  68. {offset: 0.5 + 0.5 * Math.abs(Math.sin(p)), color: 'rgba(255,0,0,0)'},
  69. {offset: 1, color: 'rgba(255,0,0,0)'},
  70. ];
  71. const gradients = path.attr('gradients');
  72. gradients.strokeColor.colors = colors;
  73. path.attr({gradients});
  74. requestAnimationFrame(next);
  75. });
  76. layer.appendChild(s);
  77. }())

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

playbackRate: 1.0

效果 - 图4

通过控制playbackRate可以控制layer上的所有动画的播放速度,该属性也会影响到layer的draw方法中的时间参数,对自定义绘图中依赖于时间轴的也可以产生影响。

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

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

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

阴影 shadow

spritejs可以通过shadow属性给元素设置阴影:

效果 - 图5

  1. const scene = new Scene('#shadow', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const p = new Path('M0,0H200V200z');
  4. p.attr({
  5. fillColor: 'red',
  6. pos: [700, 150],
  7. rotate: 60,
  8. shadow: {
  9. offset: [15, 15],
  10. blur: 10,
  11. color: '#999',
  12. },
  13. });
  14. layer.append(p);

滤镜 filter

除了设置shadow外,spritejs支持canvas滤镜,能够方便地给元素添加各种滤镜。

效果 - 图6

  1. ;(async function () {
  2. const images = [
  3. {id: 'girl1', src: 'https://p5.ssl.qhimg.com/t01feb7d2e05533ca2f.jpg'},
  4. {id: 'girl2', src: 'https://p5.ssl.qhimg.com/t01deebfb5b3ac6884e.jpg'},
  5. ];
  6. const scene = new Scene('#filters', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  7. const layer = scene.layer('fglayer');
  8. const y1 = 50,
  9. y2 = 320;
  10. function applyFilters(id, filters, y, scale = 1) {
  11. filters.forEach((f, i) => {
  12. const s = new Sprite();
  13. const textures = {id},
  14. filter = {};
  15. if(f.length === 2) {
  16. filter[f[0]] = f[1];
  17. }
  18. s.attr({
  19. textures,
  20. pos: [50 + i * 250, y],
  21. scale,
  22. filter,
  23. });
  24. layer.append(s);
  25. });
  26. }
  27. await scene.preload(...images);
  28. const filters1 = [
  29. [],
  30. ['brightness', '150%'],
  31. ['grayscale', '50%'],
  32. ['blur', '12px'],
  33. ['dropShadow', [15, 15, 5, '#033']],
  34. ['hueRotate', 45],
  35. ];
  36. applyFilters('girl1', filters1, y1, 0.5);
  37. const filters2 = [
  38. [],
  39. ['invert', '100%'],
  40. ['opacity', '70%'],
  41. ['saturate', '20%'],
  42. ['sepia', '100%'],
  43. ['hueRotate', 135],
  44. ];
  45. applyFilters('girl2', filters2, y2);
  46. }())

渐变 gradient

spritejs支持三种渐变,分别为两种标准的canvas渐变:linearGradientradialGradient,以及一种微信小程序的特殊的渐变:circularGradient。spritejs中设置渐变很简单,只需要设置gradients属性,其中指定需要应用渐变的属性,目前支持border、bgcolor、fillColor和strokeColor四种属性的渐变。通过传递vector参数表示渐变的类型,根据个数自动识别为对应的渐变类型。

效果 - 图7

  1. const scene = new Scene('#gradients', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const box = new Sprite();
  4. box.attr({
  5. border: 10,
  6. gradients: {
  7. border: {
  8. vector: [0, 0, 170, 170],
  9. colors: [
  10. {offset: 0, color: 'red'},
  11. {offset: 0.5, color: 'yellow'},
  12. {offset: 1, color: 'green'},
  13. ],
  14. },
  15. bgcolor: {
  16. vector: [0, 150, 150, 0],
  17. colors: [
  18. {offset: 0, color: '#fff'},
  19. {offset: 0.5, color: 'rgba(33, 33, 77, 0.7)'},
  20. {offset: 1, color: 'rgba(128, 45, 88, 0.5)'},
  21. ],
  22. },
  23. },
  24. pos: [150, 50],
  25. size: [150, 150],
  26. borderRadius: 15,
  27. });
  28. layer.append(box);
  29. const label = new Label('Hello SpriteJS~~');
  30. label.attr({
  31. lineWidth: 6,
  32. gradients: {
  33. fillColor: {
  34. vector: [35, 35, 50, 350, 350, 600],
  35. colors: [
  36. {offset: 0, color: '#777'},
  37. {offset: 0.5, color: '#ccc'},
  38. {offset: 1, color: '#333'},
  39. ],
  40. },
  41. },
  42. pos: [500, 50],
  43. font: '106px Arial',
  44. });
  45. layer.append(label);
  46. const path = new Path();
  47. path.attr({
  48. path: {
  49. 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',
  50. trim: true,
  51. transform: {scale: 0.7, rotate: 30},
  52. },
  53. gradients: {
  54. fillColor: {
  55. vector: [200, 200, 0, 0],
  56. colors: [
  57. {offset: 0, color: 'red'},
  58. {offset: 0.5, color: 'yellow'},
  59. {offset: 1, color: 'green'},
  60. ],
  61. },
  62. },
  63. anchor: [0.5, 0.5],
  64. pos: [700, 360],
  65. });
  66. layer.append(path);