一个办法是我们可以使用坐标位置到圆心的距离来判断,但是这样比较麻烦,mousemove不断监听,而且还需要通过mouseleave事件处理,否则可能不能判断鼠标是否已经移出元素。
其实我们有另一个比较好的办法,那就是重新定义元素鼠标的事件响应区域,这可以通过重写元素的pointCollision方法来实现。
我们可以继承Sprite创建一个Circle类,然后重新定义一些属性,对于一些不需要配置,根据可配置属性决定的属性,我们可以在init的attr.setDefault里面确定,创建新的精灵类型是,我们不在这里详细说。在这里,我们只关注通过重写pointCollision方法,我们给精灵重新指定了响应事件的范围,这样我们就只需简单把事件注册在mouseenter和mouseleave上即可。
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.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();
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);
});
});
自定义事件可以让我们以松耦合的方式来完成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是需要加载的所有资源 |
beforedraw
、afterdraw
和update
的时机一次是先beforedraw
,然后绘制精灵到缓存canvas,然后afterdraw
,然后将缓存canvas绘制到输出canvas,然后是update
。
;(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);
});
DOM基本事件实际上是通过scene代理给sprite元素的,我们可以通过scene的delegateEvent方法代理新的事件。如果结合元素的pointCollison检测,可以做一些有趣的事情。
注意为了避免污染原生的事件参数,spritejs代理的事件,要拿到原始事件的参数,需要通过获得
由于Scene默认代理了几乎所有的mouse、touch事件,这些事件都会被传递给layer,并排发给layer下的所有元素。如果layer的元素很多的话,这也会造成一定的性能开销。
假如明确当前layer不需要响应事件,可以将layer的handleEvent属性设置为false,这样的话scene就不会把事件传给这个layer。不过在layer和layer之下的元素上主动调用dispatchEvent以及前面提到的系统事件还是会正常触发。
const layer = scene.layer('fglayer', {handleEvent: true})