响应 DOM 事件

Scene 自动代理了 mouse 和 touch 相关事件,因此要监听这些事件非常简单,直接使用 spirte.on 方法即可。

移动鼠标:

行为 - 图1

  1. const scene = new Scene('#dom-events', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const s1 = new Sprite();
  4. s1.attr({
  5. anchor: [0.5, 0.5],
  6. pos: [770, 300],
  7. size: [300, 300],
  8. rotate: 45,
  9. bgcolor: '#3c7',
  10. });
  11. layer.append(s1);
  12. s1.on('mouseenter', (evt) => {
  13. s1.attr('border', [4, 'blue']);
  14. });
  15. s1.on('mouseleave', (evt) => {
  16. s1.attr('border', [0, '']);
  17. });
  18. const anchorCross = new Path('M0,10H10,20M10,0V10,20');
  19. anchorCross.attr({
  20. anchor: [0.5, 0.5],
  21. pos: [770, 300],
  22. strokeColor: 'red',
  23. rotate: 45,
  24. lineWidth: 4,
  25. });
  26. layer.append(anchorCross);
  27. const label = new Label('鼠标位置:');
  28. label.attr({
  29. pos: [20, 50],
  30. font: '32px Arial',
  31. lineHeight: 56,
  32. });
  33. layer.append(label);
  34. layer.on('mousemove', (evt) => {
  35. const {x, y, targetSprites} = evt;
  36. label.text = `鼠标位置:\n相对于 layer: ${Math.round(x)}, ${Math.round(y)}`;
  37. if(targetSprites.length && targetSprites.includes(s1)) {
  38. const [offsetX, offsetY] = s1.pointToOffset(x, y).map(Math.round);
  39. label.text += `\n相对于元素:${offsetX}, ${offsetY}`;
  40. }
  41. });

spritejs的事件触发规则是基于坐标位置(主要是mouse和touch坐标),在事件触发的坐标位置上所有精灵元素都会触发相应事件,事件触发的顺序是按照精灵的层级关系,zIndex大的元素先触发,如果两个元素的zIndex相同,那么比较zOrder,即元素添加到layer的次序,zOrder大或者说元素添加次序较晚的先触发。当所有应当触发事件的元素触发完成之后,最后是layer的事件被触发。。当layer的事件触发时,从layer的事件参数中可以拿到targetSprites数组,表示当前layer中之前已经触发过的所有元素的。

上面的例子里,我们通过layer的mousemove事件的targetSprites参数来判断s1是否被触发,如果触发,通过evt.x、evt.y拿到鼠标相对于layer的坐标,然后再通过s1.pointToOffset,可以得到鼠标相对于s1元素的相对坐标(以anchor point为坐标原点)。不过这样做有点麻烦,我们可以直接在s1上也注册mousemove事件,这样我们可以通过事件参数的offsetX、offsetY直接拿到鼠标的相对位置。但是,我们需要解决一个问题——

阻止事件继续传播

我们可以同时在layer和s1上注册mousemove事件,以修改label的提示文字。但是,如果鼠标停留在s1上时,我们不能让layer的mousemove事件仍被触发,否则的话文字就会被覆盖。在sprite提供的事件参数中,stopDispatch()方法正是用来阻止事件从当前的元素向下一个同级元素传播的。利用它就可以实现元素之间的遮挡。

行为 - 图2

  1. const scene = new Scene('#dom-events-stop-dispatch', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. const s1 = new Sprite();
  4. s1.attr({
  5. anchor: [0.5, 0.5],
  6. pos: [770, 300],
  7. size: [300, 300],
  8. rotate: 45,
  9. bgcolor: '#3c7',
  10. });
  11. layer.append(s1);
  12. s1.on('mouseenter', (evt) => {
  13. s1.attr('border', [4, 'blue']);
  14. });
  15. s1.on('mouseleave', (evt) => {
  16. s1.attr('border', [0, '']);
  17. });
  18. const anchorCross = new Path('M0,10H10,20M10,0V10,20');
  19. anchorCross.attr({
  20. anchor: [0.5, 0.5],
  21. pos: [770, 300],
  22. strokeColor: 'red',
  23. rotate: 45,
  24. lineWidth: 4,
  25. });
  26. layer.append(anchorCross);
  27. const label = new Label('鼠标位置:');
  28. label.attr({
  29. pos: [20, 50],
  30. font: '32px Arial',
  31. lineHeight: 56,
  32. });
  33. layer.append(label);
  34. layer.on('mousemove', (evt) => {
  35. const {x, y} = evt;
  36. label.text = `鼠标位置:\n相对于 layer: ${Math.round(x)}, ${Math.round(y)}`;
  37. });
  38. s1.on('mousemove', (evt) => {
  39. const {x, y, offsetX, offsetY} = evt;
  40. label.text = `鼠标位置:\n相对于 layer: ${Math.round(x)}, ${Math.round(y)}\n相对于元素:${Math.round(offsetX)}, ${Math.round(offsetY)}`;
  41. evt.stopDispatch();
  42. });

利用 zIndex 和 stopDispatch 遮挡

比如我们可以利用改变元素的zIndex和stopDispatch来实现元素间的拖拽。

注意下面的例子中stopDispatch()并不会阻止不同级的元素,因此阻止掉sprite元素的mousemove事件,并不会同时阻止掉它所在layer的mousemove事件。

行为 - 图3

  1. const scene = new Scene('#dragdrop', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer('fglayer');
  3. let zIndex = 1;
  4. function draggable(sprite) {
  5. if(sprite.isDraggable) return;
  6. sprite.isDraggable = true;
  7. let x0,
  8. y0,
  9. startPos;
  10. function onMouseMove(evt) {
  11. const dx = evt.x - x0,
  12. dy = evt.y - y0;
  13. sprite.attr('pos', [startPos[0] + dx, startPos[1] + dy]);
  14. evt.stopDispatch();
  15. }
  16. sprite.on('mouseenter', (evt) => {
  17. sprite.attr('border', {width: 6, color: 'blue'});
  18. });
  19. sprite.on('mouseleave', (evt) => {
  20. sprite.attr('border', {width: 0});
  21. });
  22. sprite.on('mousedown', (evt) => {
  23. x0 = evt.x;
  24. y0 = evt.y;
  25. startPos = sprite.attr('pos');
  26. sprite.attr('zIndex', zIndex++);
  27. sprite.off('mousemove', onMouseMove);
  28. sprite.setMouseCapture();
  29. sprite.on('mousemove', onMouseMove);
  30. evt.stopDispatch();
  31. }).on('mouseup', (evt) => {
  32. sprite.off('mousemove', onMouseMove);
  33. sprite.releaseMouseCapture();
  34. });
  35. return sprite;
  36. }
  37. const s1 = new Sprite();
  38. s1.attr({
  39. anchor: [0.5, 0.5],
  40. pos: [770, 300],
  41. size: [200, 200],
  42. rotate: 45,
  43. bgcolor: '#3c7',
  44. });
  45. const s2 = new Sprite();
  46. s2.attr({
  47. anchor: [0.5, 0.5],
  48. pos: [270, 300],
  49. size: [200, 200],
  50. bgcolor: '#c37',
  51. });
  52. const s3 = new Sprite();
  53. s3.attr({
  54. anchor: [0.5, 0.5],
  55. pos: [1270, 300],
  56. size: [200, 200],
  57. bgcolor: '#73c',
  58. });
  59. layer.append(...[s1, s2, s3].map(draggable));
  60. layer.on('mousemove', (evt) => {
  61. if(evt.targetSprites.some(target => target.isDraggable)) {
  62. scene.container.style.cursor = 'move';
  63. } else {
  64. scene.container.style.cursor = 'default';
  65. }
  66. });