事件和坐标

基础用法:事件里,我们介绍了sprite事件的基本原理和用法。但是在一些应用中,我们有一些非矩形的元素,比如一些Path或者利用元素圆角绘制的几何图形。如果是Path,我们可以通过事件参数event.targetPaths来判断元素中的Path路径是否在鼠标坐标之内。但是如果是利用元素圆角样式绘制的圆或圆环,我们就不能那么做了。

事件与自定义事件 - 图1

一个办法是我们可以使用坐标位置到圆心的距离来判断,但是这样比较麻烦,mousemove不断监听,而且还需要通过mouseleave事件处理,否则可能不能判断鼠标是否已经移出元素。

  1. const scene = new Scene('#point-collision', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const c1 = new Sprite();
  4. c1.attr({
  5. anchor: [0.5, 0.5],
  6. border: [100, 'red'],
  7. pos: [770, 300],
  8. borderRadius: 50,
  9. opacity: 0.5,
  10. });
  11. layer.append(c1);
  12. const c2 = new Sprite();
  13. c2.attr({
  14. anchor: [0.5, 0.5],
  15. border: [50, 'rgb(192, 128, 0)'],
  16. size: [100, 100],
  17. pos: [470, 300],
  18. borderRadius: 75,
  19. opacity: 0.5,
  20. });
  21. layer.append(c2);
  22. const c3 = new Sprite();
  23. c3.attr({
  24. anchor: [0.5, 0.5],
  25. border: [20, 'green'],
  26. pos: [1070, 300],
  27. size: [160, 160],
  28. borderRadius: 90,
  29. opacity: 0.5,
  30. });
  31. layer.append(c3);
  32. function isPointCollision(sprite, x, y) {
  33. const {width: borderWidth} = sprite.attr('border'),
  34. width = sprite.contentSize[0];
  35. const bounds = sprite.boundingRect,
  36. [cx, cy] = [bounds[0] + bounds[2] / 2, bounds[1] + bounds[3] / 2];
  37. const distance = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
  38. return distance >= width / 2 && distance <= width / 2 + borderWidth;
  39. }
  40. [c1, c2, c3].forEach((c) => {
  41. c.on('mousemove', (evt) => {
  42. const target = evt.target,
  43. {offsetX, offsetY} = evt;
  44. if(isPointCollision(target, offsetX, offsetY)) {
  45. target.attr('opacity', 1);
  46. } else {
  47. target.attr('opacity', 0.5);
  48. }
  49. });
  50. c.on('mouseleave', (evt) => {
  51. const target = evt.target;
  52. target.attr('opacity', 0.5);
  53. });
  54. });

其实我们有另一个比较好的办法,那就是重新定义元素鼠标的事件响应区域,这可以通过重写元素的pointCollision方法来实现。

事件与自定义事件 - 图2

我们可以继承Sprite创建一个Circle类,然后重新定义一些属性,对于一些不需要配置,根据可配置属性决定的属性,我们可以在init的attr.setDefault里面确定,创建新的精灵类型是另一个话题,我们不在这里详细说。在这里,我们只关注通过重写pointCollision方法,我们给精灵重新指定了响应事件的范围,这样我们就只需简单把事件注册在mouseenter和mouseleave上即可。

  1. const scene = new Scene('#point-collision-override', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. function isPointCollision(circle, x, y) {
  4. const [r1, r2] = circle.attr('r'),
  5. width = circle.contentSize[0];
  6. const bounds = circle.boundingRect,
  7. [cx, cy] = [bounds[0] + bounds[2] / 2, bounds[1] + bounds[3] / 2];
  8. const distance = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
  9. return distance >= width / 2 && distance <= width / 2 + r1 - r2;
  10. }
  11. class Circle extends Sprite {
  12. pointCollision(evt) {
  13. if(!super.pointCollision(evt)) {
  14. return false;
  15. }
  16. const {offsetX, offsetY} = evt;
  17. return isPointCollision(this, offsetX, offsetY);
  18. }
  19. }
  20. Circle.defineAttributes({
  21. init(attr) {
  22. attr.setDefault({
  23. r: [100, 0],
  24. color: 'black',
  25. });
  26. },
  27. r(attr, val) { // 定义半径属性 [外环,内环]
  28. attr.clearCache();
  29. if(!Array.isArray(val)) {
  30. val = [val, 0];
  31. }
  32. const [r1, r2] = val;
  33. attr.set('r', val);
  34. attr.borderRadius = (r1 + r2) / 2;
  35. attr.size = [2 * r2, 2 * r2];
  36. attr.border = {width: r1 - r2, color: attr.color, style: 'solid'};
  37. },
  38. color(attr, val) {
  39. attr.clearCache();
  40. attr.set('color', val);
  41. const [r1, r2] = attr.r;
  42. attr.border = {width: r1 - r2, color: attr.color, style: 'solid'};
  43. },
  44. });
  45. const c1 = new Circle();
  46. c1.attr({
  47. anchor: [0.5, 0.5],
  48. pos: [770, 300],
  49. opacity: 0.5,
  50. r: 100,
  51. color: 'red',
  52. });
  53. layer.append(c1);
  54. const c2 = new Circle();
  55. c2.attr({
  56. anchor: [0.5, 0.5],
  57. color: 'rgb(192, 128, 0)',
  58. r: [100, 50],
  59. pos: [470, 300],
  60. opacity: 0.5,
  61. });
  62. layer.append(c2);
  63. const c3 = new Circle();
  64. c3.attr({
  65. anchor: [0.5, 0.5],
  66. color: 'green',
  67. pos: [1070, 300],
  68. r: [100, 80],
  69. opacity: 0.5,
  70. });
  71. layer.append(c3)
  72. ;[c1, c2, c3].forEach((c) => {
  73. c.on('mouseenter', (evt) => {
  74. evt.target.attr('opacity', 1);
  75. });
  76. c.on('mouseleave', (evt) => {
  77. evt.target.attr('opacity', 0.5);
  78. });
  79. });
  80. window.c1 = c1;

自定义事件

所有的sprite节点都可以通过dispatchEvent方法发送自定义事件。dispatchEvent有四个参数,含义分别如下:

参数名参数类型默认值说明
typeString派发事件的类型
evtArgsObject事件参数
collisionStateBooleanfalse如果这个参数为true,忽略pointCollision,默认判定为命中事件区域
swallowBooleanfalse如果这个参数为true,事件执行完之后不再向后面的元素传播,这个参数只对Group、Layer类型有效

自定义事件可以让我们以松耦合的方式来完成canvas内部与外部文档的交互。

事件与自定义事件 - 图3

  1. ;(async function () {
  2. const chickRes = 'https://p5.ssl.qhimg.com/t01acd5010cb5a500d5.png',
  3. chickJSON = 'https://s2.ssl.qhres.com/static/930e3b2e60496c6e.json';
  4. const scene = new Scene('#custom-event', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  5. const layer = scene.layer();
  6. await scene.preload([chickRes, chickJSON]);
  7. const claw = new Sprite('chickclaw.png');
  8. claw.attr({
  9. anchor: [0.5, 0],
  10. pos: [770, 0],
  11. zIndex: 100,
  12. });
  13. layer.append(claw);
  14. for(let i = 1; i <= 4; i++) {
  15. const chick = new Sprite(`chick0${i}.png`);
  16. chick.attr({
  17. anchor: [0.5, 1],
  18. pos: [300 + (i - 1) * 350, 600],
  19. scale: 0.5,
  20. });
  21. layer.append(chick);
  22. }
  23. let pressed = false;
  24. let moving;
  25. async function moveClaw(speed) {
  26. while(pressed) {
  27. const x0 = claw.attr('x');
  28. const anim = claw.animate([
  29. {x: x0},
  30. {x: x0 + speed},
  31. ], {
  32. duration: 500,
  33. fill: 'forwards',
  34. });
  35. /* eslint-disable no-await-in-loop */
  36. await anim.finished;
  37. /* eslint-enable no-await-in-loop */
  38. }
  39. const x0 = claw.attr('x');
  40. await claw.animate([
  41. {x: x0},
  42. {x: x0 + speed / 5},
  43. ], {
  44. duration: 100,
  45. fill: 'forwards',
  46. easing: 'ease-out',
  47. }).finished;
  48. moving = null;
  49. }
  50. layer.on('buttonDown', async (evt) => {
  51. pressed = true;
  52. const buttonId = evt.buttonId;
  53. if(!moving && buttonId === 'leftBtn') {
  54. moving = moveClaw(-50);
  55. } else if(!moving && buttonId === 'rightBtn') {
  56. moving = moveClaw(50);
  57. } else if(buttonId === 'downBtn') {
  58. await moving;
  59. moving = (async () => {
  60. await claw.animate([
  61. {y: 0},
  62. {y: 400},
  63. ], {
  64. duration: 2000,
  65. fill: 'forwards',
  66. }).finished;
  67. layer.children.forEach((child) => {
  68. if(child !== claw && claw.OBBCollision(child)) {
  69. child.attr('zIndex', 200);
  70. child.animate([
  71. {y: 600},
  72. {y: 200},
  73. ], {
  74. duration: 3000,
  75. fill: 'forwards',
  76. }).finished.then(() => child.remove());
  77. }
  78. });
  79. await claw.animate([
  80. {y: 400},
  81. {y: 0},
  82. ], {
  83. duration: 3000,
  84. fill: 'forwards',
  85. }).finished;
  86. moving = null;
  87. })();
  88. }
  89. });
  90. layer.on('buttonUp', (evt) => {
  91. pressed = false;
  92. });
  93. const ctrl = document.querySelector('#zwwctrl');
  94. ctrl.addEventListener('mousedown', (evt) => {
  95. const target = evt.target;
  96. if(target.tagName === 'BUTTON') {
  97. layer.dispatchEvent('buttonDown', {buttonId: target.id}, true, true);
  98. }
  99. });
  100. document.documentElement.addEventListener('mouseup', (evt) => {
  101. layer.dispatchEvent('buttonUp', {}, true, true);
  102. });
  103. }())

元素和绘图事件

spritejs提供几个系统事件,包括append, remove, update, beforedraw, afterdraw, preload,这些系统事件的触发时机如下:

事件类型事件参数事件说明
append{parent, zOrder}当元素被append到layer上时触发
remove{parent, zOrder}当元素被从layer上remove时触发
update{context, target, renderTime, fromCache}当元素被重新绘制时触发,发生重绘操作有可能是元素本身属性发生改变,也有可能是其他元素属性改变需要重绘,影响到当前元素。target是要绘制的元素,renderTime是当前layer的timeline的时间,fromCache为true,则说明元素缓存未失效
beforedraw{context, target, renderTime, fromCache}当元素被重新绘制时触发,发生重绘操作有可能是元素本身属性发生改变,也有可能是其他元素属性改变需要重绘,影响到当前元素。target是要绘制的元素,renderTime是当前layer的timeline的时间,fromCache为true,则说明元素缓存未失效
afterdraw{context, target, renderTime, fromCache}当元素被重新绘制时触发,发生重绘操作有可能是元素本身属性发生改变,也有可能是其他元素属性改变需要重绘,影响到当前元素。target是要绘制的元素,renderTime是当前layer的timeline的时间,fromCache为true,则说明元素缓存未失效
preload{target, loaded, resources}这个事件只在scene预加载资源时触发,target是当前scene,loaded是已经加载的资源,resources是需要加载的所有资源

beforedrawafterdrawupdate的时机一次是先beforedraw,然后绘制精灵到缓存canvas,然后afterdraw,然后将缓存canvas绘制到输出canvas,然后是update

事件与自定义事件 - 图4

利用afterdraw来处理图片,可以实现更灵活的滤镜。

  1. ;(async function () {
  2. const scene = new Scene('#afterdraw', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  3. const layer = scene.layer();
  4. await scene.preload({
  5. id: 'beauty',
  6. src: 'https://p0.ssl.qhimg.com/t01300d8189b2edf8ca.jpg',
  7. });
  8. const image = new Sprite('beauty');
  9. image.attr({
  10. anchor: [0.5, 0.5],
  11. pos: [770, 300],
  12. scale: [-0.8, 0.8],
  13. // bgcolor: 'red',
  14. });
  15. layer.append(image);
  16. image.on('afterdraw', ({target, context}) => {
  17. const [x, y, width, height] = target.renderRect;
  18. const imageData = context.getImageData(x, y, width, height);
  19. const [cx, cy] = [width / 2, height / 2];
  20. for(let i = 0; i < imageData.data.length; i += 4) {
  21. const x = (i / 4) % width,
  22. y = Math.floor((i / 4) / width);
  23. const dist = Math.sqrt((cx - x) ** 2 + (cy - y) ** 2);
  24. imageData.data[i + 3] = 255 - Math.round(255 * dist / 600);
  25. }
  26. context.putImageData(imageData, x, y);
  27. });
  28. }())

scene事件代理

DOM基本事件实际上是通过scene代理给sprite元素的,我们可以通过scene的delegateEvent方法代理新的事件。如果结合元素的pointCollison检测,可以做一些有趣的事情。

事件与自定义事件 - 图5

注意为了避免污染原生的事件参数,spritejs代理的事件,要拿到原始事件的参数,需要通过event.originalEvent获得

  1. const scene = new Scene('#event-delegate', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. class KeyButton extends Label {
  4. pointCollision(evt) {
  5. return evt.originalEvent.key === this.text;
  6. }
  7. }
  8. KeyButton.defineAttributes({
  9. init(attr) {
  10. attr.setDefault({
  11. font: '42px Arial',
  12. border: {width: 4, color: 'black', style: 'solid'},
  13. width: 50,
  14. height: 50,
  15. anchor: [0.5, 0.5],
  16. textAlign: 'center',
  17. lineHeight: 50,
  18. });
  19. },
  20. });
  21. const keys = [
  22. 'qwertyuiop',
  23. 'asdfghjkl',
  24. 'zxcvbnm',
  25. ];
  26. for(let i = 0; i < 3; i++) {
  27. const keyButtons = [...keys[i]];
  28. for(let j = 0; j < keyButtons.length; j++) {
  29. const key = new KeyButton(keyButtons[j]);
  30. key.attr({
  31. pos: [250 + j * 80, 200 + i * 100],
  32. });
  33. key.on('keydown', (evt) => {
  34. key.attr({
  35. bgcolor: 'grey',
  36. fillColor: 'white',
  37. });
  38. });
  39. key.on('keyup', (evt) => {
  40. key.attr({
  41. bgcolor: 'transparent',
  42. fillColor: 'black',
  43. });
  44. });
  45. layer.append(key);
  46. }
  47. }
  48. const label = new Label('轻敲键盘');
  49. label.attr({
  50. anchor: [0.5, 0],
  51. pos: [770, 50],
  52. font: '42px Arial',
  53. });
  54. layer.append(label);
  55. scene.delegateEvent('keydown', document);
  56. scene.delegateEvent('keyup', document);

屏蔽代理给layer的事件

由于Scene默认代理了几乎所有的mouse、touch事件,这些事件都会被传递给layer,并排发给layer下的所有元素。如果layer的元素很多的话,这也会造成一定的性能开销。

假如明确当前layer不需要响应事件,可以将layer的handleEvent属性设置为false,这样的话scene就不会把事件传给这个layer。不过在layer和layer之下的元素上主动调用dispatchEvent以及前面提到的系统事件还是会正常触发。

  1. const layer = scene.layer('fglayer', {handleEvent: true})