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

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

事件与自定义事件 - 图2

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

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

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

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

  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. });

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

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

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

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

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