事件和坐标
在基础用法:事件里,我们介绍了sprite事件的基本原理和用法。但是在一些应用中,我们有一些非矩形的元素,比如一些Path或者利用元素圆角绘制的几何图形。如果是Path,我们可以通过事件参数event.targetPaths来判断元素中的Path路径是否在鼠标坐标之内。但是如果是利用元素圆角样式绘制的圆或圆环,我们就不能那么做了。
一个办法是我们可以使用坐标位置到圆心的距离来判断,但是这样比较麻烦,mousemove不断监听,而且还需要通过mouseleave事件处理,否则可能不能判断鼠标是否已经移出元素。
const scene = new Scene('#point-collision', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
const layer = scene.layer();
const c1 = new Sprite();
c1.attr({
anchor: [0.5, 0.5],
border: [100, 'red'],
pos: [770, 300],
borderRadius: 50,
opacity: 0.5,
});
layer.append(c1);
const c2 = new Sprite();
c2.attr({
anchor: [0.5, 0.5],
border: [50, 'rgb(192, 128, 0)'],
size: [100, 100],
pos: [470, 300],
borderRadius: 75,
opacity: 0.5,
});
layer.append(c2);
const c3 = new Sprite();
c3.attr({
anchor: [0.5, 0.5],
border: [20, 'green'],
pos: [1070, 300],
size: [160, 160],
borderRadius: 90,
opacity: 0.5,
});
layer.append(c3);
function isPointCollision(sprite, x, y) {
const {width: borderWidth} = sprite.attr('border'),
width = sprite.contentSize[0];
const bounds = sprite.boundingRect,
[cx, cy] = [bounds[0] + bounds[2] / 2, bounds[1] + bounds[3] / 2];
const distance = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
return distance >= width / 2 && distance <= width / 2 + borderWidth;
}
[c1, c2, c3].forEach((c) => {
c.on('mousemove', (evt) => {
const target = evt.target,
{offsetX, offsetY} = evt;
if(isPointCollision(target, offsetX, offsetY)) {
target.attr('opacity', 1);
} else {
target.attr('opacity', 0.5);
}
});
c.on('mouseleave', (evt) => {
const target = evt.target;
target.attr('opacity', 0.5);
});
});
其实我们有另一个比较好的办法,那就是重新定义元素鼠标的事件响应区域,这可以通过重写元素的pointCollision方法来实现。
我们可以继承Sprite创建一个Circle类,然后重新定义一些属性,对于一些不需要配置,根据可配置属性决定的属性,我们可以在init的attr.setDefault里面确定,创建新的精灵类型是另一个话题,我们不在这里详细说。在这里,我们只关注通过重写pointCollision方法,我们给精灵重新指定了响应事件的范围,这样我们就只需简单把事件注册在mouseenter和mouseleave上即可。
const scene = new Scene('#point-collision-override', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
const layer = scene.layer();
function isPointCollision(circle, x, y) {
const [r1, r2] = circle.attr('r'),
width = circle.contentSize[0];
const bounds = circle.boundingRect,
[cx, cy] = [bounds[0] + bounds[2] / 2, bounds[1] + bounds[3] / 2];
const distance = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
return distance >= width / 2 && distance <= width / 2 + r1 - r2;
}
class Circle extends Sprite {
pointCollision(evt) {
if(!super.pointCollision(evt)) {
return false;
}
const {offsetX, offsetY} = evt;
return isPointCollision(this, offsetX, offsetY);
}
}
Circle.defineAttributes({
init(attr) {
attr.setDefault({
r: [100, 0],
color: 'black',
});
},
r(attr, val) { // 定义半径属性 [外环,内环]
attr.clearCache();
if(!Array.isArray(val)) {
val = [val, 0];
}
const [r1, r2] = val;
attr.set('r', val);
attr.borderRadius = (r1 + r2) / 2;
attr.size = [2 * r2, 2 * r2];
attr.border = {width: r1 - r2, color: attr.color, style: 'solid'};
},
color(attr, val) {
attr.clearCache();
attr.set('color', val);
const [r1, r2] = attr.r;
attr.border = {width: r1 - r2, color: attr.color, style: 'solid'};
},
});
const c1 = new Circle();
c1.attr({
anchor: [0.5, 0.5],
pos: [770, 300],
opacity: 0.5,
r: 100,
color: 'red',
});
layer.append(c1);
const c2 = new Circle();
c2.attr({
anchor: [0.5, 0.5],
color: 'rgb(192, 128, 0)',
r: [100, 50],
pos: [470, 300],
opacity: 0.5,
});
layer.append(c2);
const c3 = new Circle();
c3.attr({
anchor: [0.5, 0.5],
color: 'green',
pos: [1070, 300],
r: [100, 80],
opacity: 0.5,
});
layer.append(c3)
;[c1, c2, c3].forEach((c) => {
c.on('mouseenter', (evt) => {
evt.target.attr('opacity', 1);
});
c.on('mouseleave', (evt) => {
evt.target.attr('opacity', 0.5);
});
});
window.c1 = c1;
自定义事件
所有的sprite节点都可以通过dispatchEvent方法发送自定义事件。dispatchEvent有四个参数,含义分别如下:
参数名 | 参数类型 | 默认值 | 说明 |
---|---|---|---|
type | String | 派发事件的类型 | |
evtArgs | Object | 事件参数 | |
collisionState | Boolean | false | 如果这个参数为true ,忽略pointCollision,默认判定为命中事件区域 |
swallow | Boolean | false | 如果这个参数为true ,事件执行完之后不再向后面的元素传播,这个参数只对Group、Layer类型有效 |
自定义事件可以让我们以松耦合的方式来完成canvas内部与外部文档的交互。
;(async function () {
const chickRes = 'https://p5.ssl.qhimg.com/t01acd5010cb5a500d5.png',
chickJSON = 'https://s2.ssl.qhres.com/static/930e3b2e60496c6e.json';
const scene = new Scene('#custom-event', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
const layer = scene.layer();
await scene.preload([chickRes, chickJSON]);
const claw = new Sprite('chickclaw.png');
claw.attr({
anchor: [0.5, 0],
pos: [770, 0],
zIndex: 100,
});
layer.append(claw);
for(let i = 1; i <= 4; i++) {
const chick = new Sprite(`chick0${i}.png`);
chick.attr({
anchor: [0.5, 1],
pos: [300 + (i - 1) * 350, 600],
scale: 0.5,
});
layer.append(chick);
}
let pressed = false;
let moving;
async function moveClaw(speed) {
while(pressed) {
const x0 = claw.attr('x');
const anim = claw.animate([
{x: x0},
{x: x0 + speed},
], {
duration: 500,
fill: 'forwards',
});
/* eslint-disable no-await-in-loop */
await anim.finished;
/* eslint-enable no-await-in-loop */
}
const x0 = claw.attr('x');
await claw.animate([
{x: x0},
{x: x0 + speed / 5},
], {
duration: 100,
fill: 'forwards',
easing: 'ease-out',
}).finished;
moving = null;
}
layer.on('buttonDown', async (evt) => {
pressed = true;
const buttonId = evt.buttonId;
if(!moving && buttonId === 'leftBtn') {
moving = moveClaw(-50);
} else if(!moving && buttonId === 'rightBtn') {
moving = moveClaw(50);
} else if(buttonId === 'downBtn') {
await moving;
moving = (async () => {
await claw.animate([
{y: 0},
{y: 400},
], {
duration: 2000,
fill: 'forwards',
}).finished;
layer.children.forEach((child) => {
if(child !== claw && claw.OBBCollision(child)) {
child.attr('zIndex', 200);
child.animate([
{y: 600},
{y: 200},
], {
duration: 3000,
fill: 'forwards',
}).finished.then(() => child.remove());
}
});
await claw.animate([
{y: 400},
{y: 0},
], {
duration: 3000,
fill: 'forwards',
}).finished;
moving = null;
})();
}
});
layer.on('buttonUp', (evt) => {
pressed = false;
});
const ctrl = document.querySelector('#zwwctrl');
ctrl.addEventListener('mousedown', (evt) => {
const target = evt.target;
if(target.tagName === 'BUTTON') {
layer.dispatchEvent('buttonDown', {buttonId: target.id}, true, true);
}
});
document.documentElement.addEventListener('mouseup', (evt) => {
layer.dispatchEvent('buttonUp', {}, true, true);
});
}())
元素和绘图事件
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是需要加载的所有资源 |
beforedraw
、afterdraw
和update
的时机一次是先beforedraw
,然后绘制精灵到缓存canvas,然后afterdraw
,然后将缓存canvas绘制到输出canvas,然后是update
。
利用afterdraw
来处理图片,可以实现更灵活的滤镜。
;(async function () {
const scene = new Scene('#afterdraw', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
const layer = scene.layer();
await scene.preload({
id: 'beauty',
src: 'https://p0.ssl.qhimg.com/t01300d8189b2edf8ca.jpg',
});
const image = new Sprite('beauty');
image.attr({
anchor: [0.5, 0.5],
pos: [770, 300],
scale: [-0.8, 0.8],
// bgcolor: 'red',
});
layer.append(image);
image.on('afterdraw', ({target, context}) => {
const [x, y, width, height] = target.renderRect;
const imageData = context.getImageData(x, y, width, height);
const [cx, cy] = [width / 2, height / 2];
for(let i = 0; i < imageData.data.length; i += 4) {
const x = (i / 4) % width,
y = Math.floor((i / 4) / width);
const dist = Math.sqrt((cx - x) ** 2 + (cy - y) ** 2);
imageData.data[i + 3] = 255 - Math.round(255 * dist / 600);
}
context.putImageData(imageData, x, y);
});
}())
scene事件代理
DOM基本事件实际上是通过scene代理给sprite元素的,我们可以通过scene的delegateEvent方法代理新的事件。如果结合元素的pointCollison检测,可以做一些有趣的事情。
注意为了避免污染原生的事件参数,spritejs代理的事件,要拿到原始事件的参数,需要通过event.originalEvent
获得
const scene = new Scene('#event-delegate', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
const layer = scene.layer();
class KeyButton extends Label {
pointCollision(evt) {
return evt.originalEvent.key === this.text;
}
}
KeyButton.defineAttributes({
init(attr) {
attr.setDefault({
font: '42px Arial',
border: {width: 4, color: 'black', style: 'solid'},
width: 50,
height: 50,
anchor: [0.5, 0.5],
textAlign: 'center',
lineHeight: 50,
});
},
});
const keys = [
'qwertyuiop',
'asdfghjkl',
'zxcvbnm',
];
for(let i = 0; i < 3; i++) {
const keyButtons = [...keys[i]];
for(let j = 0; j < keyButtons.length; j++) {
const key = new KeyButton(keyButtons[j]);
key.attr({
pos: [250 + j * 80, 200 + i * 100],
});
key.on('keydown', (evt) => {
key.attr({
bgcolor: 'grey',
fillColor: 'white',
});
});
key.on('keyup', (evt) => {
key.attr({
bgcolor: 'transparent',
fillColor: 'black',
});
});
layer.append(key);
}
}
const label = new Label('轻敲键盘');
label.attr({
anchor: [0.5, 0],
pos: [770, 50],
font: '42px Arial',
});
layer.append(label);
scene.delegateEvent('keydown', document);
scene.delegateEvent('keyup', document);
屏蔽代理给layer的事件
由于Scene默认代理了几乎所有的mouse、touch事件,这些事件都会被传递给layer,并排发给layer下的所有元素。如果layer的元素很多的话,这也会造成一定的性能开销。
假如明确当前layer不需要响应事件,可以将layer的handleEvent属性设置为false,这样的话scene就不会把事件传给这个layer。不过在layer和layer之下的元素上主动调用dispatchEvent以及前面提到的系统事件还是会正常触发。
const layer = scene.layer('fglayer', {handleEvent: true})