状态切换

SpriteJS支持状态的管理。我们可以给元素设置一组states属性,然后再设置一组actions属性,这样就可以对它们进行状态切换。

states是一个对象,它的每个key表示一个状态ID,对应的值是一组属性:

  1. const states = {
  2. stateA: {
  3. scale: 0.5,
  4. rotate: 45,
  5. },
  6. stateB: {
  7. scale: 1.0,
  8. color: 'green',
  9. rotate: 0,
  10. },
  11. stateC: {
  12. color: 'blue',
  13. rotate: 60,
  14. },
  15. };
  16. sprite.attr('states', states);

有了states以后,我们就可以切换元素的状态:

状态切换 - 图1

  1. const scene = new Scene('#state-basic', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const states = {
  4. stateA: {
  5. bgcolor: 'red',
  6. scale: 0.5,
  7. rotate: 45,
  8. },
  9. stateB: {
  10. scale: 1.0,
  11. bgcolor: 'green',
  12. rotate: 0,
  13. },
  14. stateC: {
  15. scale: 0.8,
  16. bgcolor: 'blue',
  17. rotate: 60,
  18. },
  19. };
  20. const stateNames = Object.keys(states);
  21. const s = new Label(stateNames[0]);
  22. s.attr({
  23. anchor: 0.5,
  24. size: [200, 200],
  25. pos: [770, 300],
  26. font: '64px Arial',
  27. lineHeight: 200,
  28. textAlign: 'center',
  29. fillColor: 'white',
  30. states,
  31. state: stateNames[0],
  32. });
  33. layer.append(s);
  34. let i = 0;
  35. setInterval(() => {
  36. s.attr({state: stateNames[++i % 3]});
  37. }, 1000);

给状态添加 Actions

我们可以在状态切换的时候给状态切换设置行为,方法是设置actions属性。这个属性是一个数组,每个元素是一个action描述对象,包含以下内容:

from,toboth,设置状态切换选择器,action,设置动作timing:

  1. const actions = [
  2. {
  3. from: 'stateA',
  4. to: 'stateB',
  5. action: {
  6. duration: 500,
  7. easing: 'ease-in-out',
  8. },
  9. },{
  10. both: ['stateB', 'stateC'],
  11. action: {
  12. duration: 800,
  13. easing: 'cubic-bezier(0.26, 0.09, 0.37, 0.18)',
  14. },
  15. },{
  16. from: 'stateC',
  17. action: {
  18. duration: 1000,
  19. },
  20. },{
  21. to: 'stateC',
  22. action: {
  23. duration: 500,
  24. }
  25. },
  26. ];
  27. sprite.attr('actions', actions);

Action的匹配规则如下:

当一个状态stateA切换到stateB的时候,优先匹配{from:'stateA', to:'stateB'},如果不存在这个Action选择器,那么匹配{to:'stateB'},如果也不存在,那么匹配{from:'stateA'}{both: ['stateA', 'stateB']}是简写,会生成{from:'stateA', to:'stateB'}{from:'stateB', to:'stateA'}两个选择器。对应的,{both:['stateA']}也会生成{from:'stateA'}{to:'stateA'}两个选择器。

状态切换 - 图2

  1. const scene = new Scene('#state-actions', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const states = {
  4. stateA: {
  5. bgcolor: 'red',
  6. scale: 0.5,
  7. rotate: 45,
  8. },
  9. stateB: {
  10. scale: 1.0,
  11. bgcolor: 'green',
  12. rotate: 0,
  13. },
  14. stateC: {
  15. scale: 0.8,
  16. bgcolor: 'blue',
  17. rotate: 60,
  18. },
  19. };
  20. const stateNames = Object.keys(states);
  21. const actions = [
  22. {
  23. from: 'stateA',
  24. to: 'stateB',
  25. action: {
  26. duration: 500,
  27. easing: 'ease-in-out',
  28. },
  29. },
  30. {
  31. both: ['stateB', 'stateC'],
  32. action: {
  33. duration: 800,
  34. easing: 'cubic-bezier(0.26, 0.09, 0.37, 0.18)',
  35. },
  36. },
  37. {
  38. from: 'stateC',
  39. action: {
  40. duration: 1000,
  41. },
  42. },
  43. {
  44. to: 'stateC',
  45. action: {
  46. duration: 500,
  47. },
  48. },
  49. ];
  50. const s = new Label(stateNames[0]);
  51. s.attr({
  52. anchor: 0.5,
  53. size: [200, 200],
  54. pos: [770, 300],
  55. font: '64px Arial',
  56. lineHeight: 200,
  57. textAlign: 'center',
  58. fillColor: 'white',
  59. states,
  60. actions,
  61. // state: stateNames[0],
  62. state: 'stateC',
  63. });
  64. layer.append(s);
  65. s.attr('state', 'stateA');
  66. let i = 0;
  67. setInterval(() => {
  68. s.attr({state: stateNames[++i % 3]});
  69. }, 1000);

state-change 事件

状态切换的时候,我们可以监听state变化的事件。

一个元素的状态从a变化为b,会触发两个事件,一个是state-from-a,一个是state-to-b。事件参数包括四个属性,分别是:

  • from: 元素的源状态名,即a
  • to: 元素的目的状态名,即b
  • action: 元素切换状态的action对象,该对象是一个动画timing对象,由前面的Action选择器规则匹配出来。
  • animation: 元素切换状态的Animation对象。

action:reversable

当状态从stateA切换到stateB的时候,如果匹配到{from:'stateA', to:'stateB'}的Action并执行动画,此时状态再切换回stateA,如果上一个Action动画还没执行完成,此时默认不会执行{to:'stateB', from:'stateA'}选择器下的Action(或者其他更低优先级的选择器选择的Action),而是反向执行前一个未完成的Action,这样我们做状态双向切换的动画就比较自然。如果我们要强制执行新的Action,可以给{from:'stateA', to:'stateB'}的Action设置一个reversable:false属性,以强制忽略反向Action,执行新的Action。

状态切换 - 图3

  1. const scene = new Scene('#state-reversable', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const button1 = new Label('reversable');
  4. button1.attr({
  5. anchor: 0.5,
  6. font: '32px Arial',
  7. border: [3, 'blue'],
  8. padding: 10,
  9. pos: [500, 300],
  10. state: 'normal',
  11. states: {
  12. hover: {
  13. scale: 1.2,
  14. },
  15. normal: {
  16. scale: 1,
  17. },
  18. },
  19. actions: [
  20. {
  21. both: ['hover', 'normal'],
  22. duration: 500,
  23. },
  24. ],
  25. });
  26. layer.append(button1);
  27. button1.on('mouseenter', function () {
  28. this.attr('state', 'hover');
  29. });
  30. button1.on('mouseleave', function () {
  31. this.attr('state', 'normal');
  32. });
  33. const button2 = button1.cloneNode();
  34. button2.attr({
  35. text: 'not reversable',
  36. x: x => x + 500,
  37. actions: [
  38. {
  39. both: ['hover', 'normal'],
  40. duration: 500,
  41. reversable: false,
  42. },
  43. ],
  44. });
  45. layer.append(button2);
  46. button2.on('mouseenter', function () {
  47. this.attr('state', 'hover');
  48. });
  49. button2.on('mouseleave', function () {
  50. this.attr('state', 'normal');
  51. });

状态序列

我们可以通过resolveStates(states, before, after)方法批量设置一组state,然后让元素从开始state变更到结束state。

每个元素的resolveStates(states, before, after)方法是互斥的,也就是说如果同一时间调用两组resolveState(),spritejs会立即结束前面的动作,执行后一组动作。

状态切换 - 图4

  1. const scene = new Scene('#state-resolveStates', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const button1 = new Label('动作 1');
  4. button1.attr({
  5. anchor: 0.5,
  6. font: '32px Arial',
  7. border: [3, 'blue'],
  8. padding: 10,
  9. pos: [500, 300],
  10. });
  11. layer.append(button1);
  12. const button2 = button1.cloneNode();
  13. button2.attr({
  14. text: '动作 2',
  15. y: y => y + 100,
  16. });
  17. layer.append(button2);
  18. button1.on('mouseenter', () => {
  19. layer.style.cursor = 'pointer';
  20. });
  21. button1.on('mouseleave', () => {
  22. layer.style.cursor = '';
  23. });
  24. button2.on('mouseenter', () => {
  25. layer.style.cursor = 'pointer';
  26. });
  27. button2.on('mouseleave', () => {
  28. layer.style.cursor = '';
  29. });
  30. const block = new Sprite({
  31. pos: [800, 300],
  32. size: [100, 100],
  33. bgcolor: 'red',
  34. state: 'a',
  35. states: {
  36. a: {
  37. scale: 1.0,
  38. rotate: 0,
  39. },
  40. b: {
  41. scale: 1.5,
  42. rotate: 0,
  43. },
  44. c: {
  45. scale: 1.2,
  46. rotate: 180,
  47. },
  48. d: {
  49. scale: 1.0,
  50. rotate: -45,
  51. },
  52. },
  53. actions: [
  54. {
  55. both: ['a', 'b'],
  56. duration: 1000,
  57. },
  58. {
  59. both: ['b', 'c'],
  60. duration: 1000,
  61. },
  62. {
  63. both: ['c', 'd'],
  64. duration: 1000,
  65. },
  66. {
  67. both: ['d', 'a'],
  68. duration: 1000,
  69. },
  70. ],
  71. });
  72. layer.append(block);
  73. button1.on('click', () => {
  74. block.resolveStates(['a', 'b', 'c', 'd', 'a']);
  75. });
  76. button2.on('click', () => {
  77. block.resolveStates(['a', 'd', 'c', 'b', 'a']);
  78. });

内置状态

spritejs为每一个元素内置了一些特定的状态,要实现这些状态的动作效果,只需要直接给这些状态设置初始值即可。内置状态包括以下这些:

  • beforeEnter 当元素或它的父级元素被append到layer上之前的临时状态。
  • afterEnter 当元素或它的父级元素被append到layer上之后的临时状态。
  • beforeExit 当元素或它的父级元素被从layer上remove之前的临时状态。
  • afterExit 当元素或它的父级元素被从layer上remove之后的临时状态。
  • show 当元素被调用hide()方法之前的临时状态,或被调用show()方法之后的临时状态。
  • hide 当元素被调用hide()方法之后的状态。
  • default 元素默认的初始状态。

状态切换 - 图5

  1. const scene = new Scene('#state-toggleEnterExit', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const group = new Group({
  4. display: 'flex',
  5. size: [700, 100],
  6. pos: [770, 300],
  7. anchor: 0.5,
  8. bgcolor: 'grey',
  9. });
  10. layer.append(group);
  11. function addBlock() {
  12. const block = new Sprite({
  13. bgcolor: 'red',
  14. margin: [0, 10, 0, 10],
  15. size: [100, 100],
  16. states: {
  17. beforeEnter: {
  18. translate: [500, 0],
  19. },
  20. afterExit: {
  21. translate: [500, 0],
  22. },
  23. },
  24. });
  25. group.append(block);
  26. }
  27. function removeBlock() {
  28. const children = group.children;
  29. if(children.length > 0) {
  30. const child = children[children.length - 1];
  31. child.remove();
  32. }
  33. }
  34. const button1 = new Label('添加元素');
  35. button1.attr({
  36. anchor: 0.5,
  37. font: '32px Arial',
  38. border: [3, 'blue'],
  39. padding: 10,
  40. pos: [500, 200],
  41. states: {
  42. beforeEnter: {
  43. translate: [-1000, 0],
  44. },
  45. },
  46. });
  47. layer.append(button1);
  48. const button2 = button1.cloneNode();
  49. button2.attr({
  50. text: '删除元素',
  51. x: x => x + 300,
  52. });
  53. layer.append(button2);
  54. button1.on('mouseenter', () => {
  55. layer.style.cursor = 'pointer';
  56. });
  57. button1.on('mouseleave', () => {
  58. layer.style.cursor = '';
  59. });
  60. button2.on('mouseenter', () => {
  61. layer.style.cursor = 'pointer';
  62. });
  63. button2.on('mouseleave', () => {
  64. layer.style.cursor = '';
  65. });
  66. button1.on('click', () => {
  67. addBlock();
  68. });
  69. button2.on('click', () => {
  70. removeBlock();
  71. });

在上面的例子里,我们给元素设置了beforeEnter和afterExit的状态,在append和remove的时候,spritejs会自动触发动作。enterexit行为有默认的action,值为:

  1. [
  2. {
  3. from: 'beforeEnter',
  4. duration: 300,
  5. ease: 'ease-in',
  6. },
  7. {
  8. from: 'beforeExit',
  9. duration: 300,
  10. ease: 'ease-out',
  11. }
  12. ]

我们可以设置元素的actions属性来改写它们。

  1. // 延长动画时间
  2. sprite.attr('actions', [
  3. {
  4. from: 'beforeEnter',
  5. duration: 600,
  6. ease: 'ease-in',
  7. },
  8. {
  9. from: 'beforeExit',
  10. duration: 600,
  11. ease: 'ease-out',
  12. }
  13. ])

除了控制enter和exit之外,我们还可以通过给元素增加hide状态来控制它的显示隐藏,通过它我们可以很方便地实现fade-in和fade-out效果:

状态切换 - 图6

  1. const scene = new Scene('#state-fade', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const sprite = new Sprite({
  4. display: 'flex',
  5. size: [700, 100],
  6. pos: [770, 300],
  7. anchor: 0.5,
  8. bgcolor: 'grey',
  9. states: {
  10. hide: {
  11. opacity: 0,
  12. },
  13. },
  14. });
  15. layer.append(sprite);
  16. const button1 = new Label('显示');
  17. button1.attr({
  18. anchor: 0.5,
  19. font: '32px Arial',
  20. border: [3, 'blue'],
  21. padding: 10,
  22. pos: [500, 200],
  23. states: {
  24. beforeEnter: {
  25. translate: [-1000, 0],
  26. },
  27. },
  28. });
  29. layer.append(button1);
  30. const button2 = button1.cloneNode();
  31. button2.attr({
  32. text: '隐藏',
  33. x: x => x + 300,
  34. });
  35. layer.append(button2);
  36. button1.on('mouseenter', () => {
  37. layer.style.cursor = 'pointer';
  38. });
  39. button1.on('mouseleave', () => {
  40. layer.style.cursor = '';
  41. });
  42. button2.on('mouseenter', () => {
  43. layer.style.cursor = 'pointer';
  44. });
  45. button2.on('mouseleave', () => {
  46. layer.style.cursor = '';
  47. });
  48. button1.on('click', () => {
  49. sprite.show();
  50. });
  51. button2.on('click', () => {
  52. sprite.hide();
  53. });

enterMode 和 exitMode

在使用group的时候,我们可以将子元素一一添加到group上,然后再将group添加到parent上,此时group下的子元素的enter行为会被触发。我们可以通过设置enterMode和exitMode来改变enter/exit行为的触发方式,可选的方式如下:

  • normal 默认值,enter时同时触发自身和子元素的enter,exit时先同时触发自身和子元素的exit
  • onebyone enter时先触发自身的enter,然后根据zOrder顺序依次触发子元素的enter,exit时先根据zOrder顺序依次触发子元素的exit,然后触发自身的exit
  • onebyone-reverse enter时先触发自身的enter,然后根据zOrder的倒序依次触发子元素的enter,exit时先根据zOrder倒序依次触发子元素的exit,然后触发自身的exit

状态切换 - 图7

  1. const scene = new Scene('#state-mode', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const group = new Group({
  4. display: 'flex',
  5. size: [700, 100],
  6. pos: [770, 300],
  7. anchor: 0.5,
  8. bgcolor: 'grey',
  9. enterMode: 'onebyone',
  10. exitMode: 'onebyone-reverse',
  11. });
  12. for(let i = 0; i < 5; i++) {
  13. const block = new Sprite({
  14. size: [100, 100],
  15. bgcolor: 'red',
  16. margin: [0, 10, 0, 10],
  17. states: {
  18. beforeEnter: {
  19. translate: [500, 0],
  20. },
  21. afterExit: {
  22. translate: [500, 0],
  23. },
  24. },
  25. });
  26. group.append(block);
  27. }
  28. const button1 = new Label('批量添加');
  29. button1.attr({
  30. anchor: 0.5,
  31. font: '32px Arial',
  32. border: [3, 'blue'],
  33. padding: 10,
  34. pos: [500, 200],
  35. states: {
  36. beforeEnter: {
  37. translate: [-1000, 0],
  38. },
  39. },
  40. });
  41. layer.append(button1);
  42. const button2 = button1.cloneNode();
  43. button2.attr({
  44. text: '批量移除',
  45. x: x => x + 300,
  46. });
  47. layer.append(button2);
  48. button1.on('mouseenter', () => {
  49. layer.style.cursor = 'pointer';
  50. });
  51. button1.on('mouseleave', () => {
  52. layer.style.cursor = '';
  53. });
  54. button2.on('mouseenter', () => {
  55. layer.style.cursor = 'pointer';
  56. });
  57. button2.on('mouseleave', () => {
  58. layer.style.cursor = '';
  59. });
  60. button1.on('click', () => {
  61. layer.append(group);
  62. });
  63. button2.on('click', () => {
  64. group.remove();
  65. });