原文地址:Hit Region Detection For HTML5 Canvas And How To Listen To Click Events On Canvas Shapes 作者:Anton Lavrenov
你是否需要一個在Canvas畫布上的任意圖形的點擊事件監聽(譯者注:類似於任意一個DOM元素的點擊監聽事件)?但是Canvas沒有此類監聽器的API。你只能在整個Canvas畫布上進行事件監聽,而不是在畫布上的任意一個元素。我將描述2種方法來解決這個問題。
注意!我將不會使用
addHitRegion API
,因為現在(作者寫文章時間為2017年)這個api是不穩定的,並且並沒有被完整的支持。但是你可以了解下。
讓我們從簡單的canvas畫布圖形開始。假設我們在一個頁面上繪制了幾個圓圈(circle)。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const circles = [
{
x: 40,
y: 40,
radius: 10,
color: 'rgb(255,0,0)'
},
{
x: 70,
y: 70,
radius: 10,
color: 'rgb(0,255,0)'
}
];
circles.forEach(circle => {
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI, false);
ctx.fillStyle = circle.color;
ctx.fill();
});
效果如下:
現在我們只能簡單在整個Canvas畫布上監聽點擊事件:
canvas.addEventListener('click', () => {
console.log('canvas click');
});
但是我們想監聽其中人一個圓圈的點擊事件。我們該怎么做?怎么檢測到我們點擊了其中的一個圓圈?
方式1 - 利用數學的力量
當擁有圓圈的坐標和尺寸(半徑)信息,我們可以利用數學方式通過簡單計算來檢測在任意一個圓圈上的點擊。我們所需要的就是獲取到鼠標點擊位置的坐標信息,並且跟所有的圓圈逐一進行相交檢測:
function isIntersect(point, circle) {
return Math.sqrt((point.x-circle.x) ** 2 + (point.y - circle.y) ** 2) < circle.radius;
}
canvas.addEventListener('click', (e) => {
const pos = {
x: e.clientX,
y: e.clientY
};
circles.forEach(circle => {
if (isIntersect(mousePoint, circle)) {
alert('click on circle: ' + circle.id);
}
});
});
這種方式非常普遍,並在許多項目中廣泛使用。你可以輕松找到更加復雜集合圖形的數學函數,比如矩形,橢圓,多邊形。。。
這種方式非常棒,當你的畫布上沒有大量的圖形時,他可能是非常快的。
但是這種方式很難處理那些非常復雜的幾何圖形。比如說,你正在使用具有二次曲線的線。
方式2 - 模擬點擊區域
點擊區域的想法很簡單 - 我們只需要獲取點擊區域的像素,並且找到擁有相同顏色的圖形即可。
function hasSameColor(color, circle) {
return circle.color === color;
}
canvas.addEventListener('click', (e) => {
const mousePos = {
x: e.clientX - canvas.offsetTop,
y: e.clientY - canvas.offsetLeft
};
// get pixel under cursor
const pixel = ctx.getImageData(mousePos.x, mousePos.y, 1, 1).data;
// create rgb color for that pixel
const color = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`;
// find a circle with the same colour
circles.forEach(circle => {
if (hasSameColor(color, circle)) {
alert('click on circle: ' + circle.id);
}
});
});
但是這種方式可能無效,因為不同的圖形可能擁有相同的顏色。為了避免這種問題,我們應該創建一個'點擊圖形'
canvas畫布.它將跟主canvas畫布擁有幾乎相同的圖形,並且每一個圖形都擁有唯一的顏色。因此我們需要對每一個圓圈生成隨機的顏色。
// colorsHash for saving references of all created circles
const colorsHash = {};
function getRandomColor() {
const r = Math.round(Math.random() * 255);
const g = Math.round(Math.random() * 255);
const b = Math.round(Math.random() * 255);
return `rgb(${r},${g},${b})`;
}
const circles = [{
id: '1', x: 40, y: 40, radius: 10, color: 'rgb(255,0,0)'
}, {
id: '2', x: 100, y: 70, radius: 10, color: 'rgb(0,255,0)'
}];
// generate unique colors
circles.forEach(circle => {
// repeat until we find trully unique colour
while(true) {
const colorKey = getRandomColor();
// if colours is unique
if (!colorsHash[colorKey]) {
// set color for hit canvas
circle.colorKey = colorKey;
// save reference
colorsHash[colorKey] = circle;
return;
}
}
});
然后,我們需要繪制每個圖形2次。第一次在主畫布上(可見的),然后在'點擊'畫布上(不可見)。
circles.forEach(circle => {
// draw on "scene" canvas first
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI, false);
ctx.fillStyle = circle.color;
ctx.fill();
// then draw on offscren "hit" canvas
hitCtx.beginPath();
hitCtx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI, false);
hitCtx.fillStyle = circle.colorKey;
hitCtx.fill();
});
現在當你點擊主canvas時,你需要做的就是獲取到你點擊處的一個像素,然后在'點擊'canvas上找到跟主cavnas同樣位置的一像素的顏色。
實例代碼如下:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const hitCanvas = document.createElement('canvas');
const hitCtx = hitCanvas.getContext('2d');
const colorsHash = {};
function getRandomColor() {
const r = Math.round(Math.random() * 255);
const g = Math.round(Math.random() * 255);
const b = Math.round(Math.random() * 255);
return `rgb(${r},${g},${b})`;
}
const circles = [{
id: '1', x: 40, y: 40, radius: 10, color: 'rgb(255,0,0)'
}, {
id: '2', x: 100, y: 70, radius: 10, color: 'rgb(0,255,0)'
}];
circles.forEach(circle => {
while(true) {
const colorKey = getRandomColor();
if (!colorsHash[colorKey]) {
circle.colorKey = colorKey;
colorsHash[colorKey] = circle;
return;
}
}
});
circles.forEach(circle => {
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI, false);
ctx.fillStyle = circle.color;
ctx.fill();
hitCtx.beginPath();
hitCtx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI, false);
hitCtx.fillStyle = circle.colorKey;
hitCtx.fill();
});
function hasSameColor(color, shape) {
return shape.color === color;
}
canvas.addEventListener('click', (e) => {
const mousePos = {
x: e.clientX - canvas.offsetLeft,
y: e.clientY - canvas.offsetTop
};
const pixel = hitCtx.getImageData(mousePos.x, mousePos.y, 1, 1).data;
const color = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`;
const shape = colorsHash[color];
if (shape) {
alert('click on circle: ' + shape.id);
}
});
那種方式更好?
這需要視情況而定。第二種方式最主要的瓶頸在於你需要繪制2次。因此性能可能下降2倍!但是我們可以簡化hitCanvas的繪制。你可以跳過shadows或者strokes繪制,你可以簡化很多圖形,比如,用矩形來代替文本。簡化繪制后的方式可能是非常快的。因為從canvas上去一像素和從一個顏色hash對象(colorsHash)中取值是非常快的操作。
它們能一起使用嗎?
當然。一些canvas庫就是結合了上邊2種方式。它們以這種方式工作:
對於每一個圖形,你必須計算簡化后的矩形邊界(x,y, width, height)。然后你使用第一種方式計算點擊位置和邊界矩形的相交來篩選相關的圖形。然后,你可以繪制hitcanvas,並且用第二種方式來檢測相交,從而得到更加准確的結果。
為什么不使用SVG?
因為有時候Canvas可以表現得更好,更適合您的高級任務。當然,這取決於任務。因此Canvas VS SVG不在本文討論的范圍內。當你使用canvas並且進行點擊檢測你必學使用一些東西,不是嗎?
其他事件如何檢測?比如mousemove,mouseenter等等?
您只需要在以上描述的方法中添加一些額外的代碼。一旦你可以100%檢測到鼠標下方的圖形,你就可以模擬所有其他事件。
有什么好的開箱即用的解決方案么?
當然。只需要去google搜索 html5 canvas framework。但是我個人推薦 http://konvajs.github.io/.。我幾乎忘記了,我是這個庫的維護者。Konva只使用了第二種方式,支持所有我們經常在dom元素上的mouse 和touch事件(還有更多,比如drag和drop)。