如果我有两个部分透明的图像(GIF,PNG,SVG等),我该如何检查图像的非透明区域是否相交?
如果有必要,我可以使用画布。该解决方案需要使用支持透明度的所有图像格式。请不要jQuery。
答案 0 :(得分:1)
通过使用2D上下文globalCompositeOperation
,您可以大大提高像素像素重叠测试的速度。
comp操作"destination-in"
只会留下画布上可见的像素和您在其上绘制的图像。因此,您创建一个画布,绘制一个图像,然后将comp操作设置为"destination-in"
,然后绘制第二个图像。如果任何像素重叠,那么它们将具有非零alpha。你所做的只是读取像素,如果它们中的任何一个不为零,你就知道它有重叠。
测试重叠区域中的所有像素将会很慢。您可以让GPU为您做一些数学运算并缩小合成图像。由于像素仅为8位值,因此存在一些损失。这可以通过逐步减少图像并多次渲染结果来克服。每次减少就像计算平均值一样。我缩小了8有效地获得了64像素的平均值。为了阻止范围底部的像素因圆化而消失,我会多次绘制图像。我做了32次,它的效果是将alpha通道乘以32。
可以轻松修改此方法,以便在不影响性能的情况下缩放,倾斜和旋转两个图像。您还可以使用它来测试许多图像,如果所有图像都有像素重叠,则返回true。
像素很小,因此如果在函数中创建测试画布之前缩小图像大小,则可以获得额外的速度。这可以显着提升性能。
有一个标志reuseCanvas
,允许您重复使用工作画布。如果您经常使用测试函数(每秒多次),则将标志设置为true。如果您只是偶尔需要测试,那么将其设置为false。
这种方法适用于需要偶尔进行测试的大型图像,对于小图像和每帧的许多测试(例如在您可能需要测试100张图像的游戏中)并不好。对于快速(几乎完美的像素)碰撞测试,请参阅Radial Perimeter Test 如果您找到有用的答案,请不要忘记upvote。
// Use the options to set quality of result
// Slow but perfect
var slowButPerfect = false;
// if reuseCanvas is true then the canvases are resused saving some time
const reuseCanvas = true;
// hold canvas references.
var pixCanvas;
var pixCanvas1;
// returns true if any pixels are overlapping
// img1,img2 the two images to test
// x,y location of img1
// x1,y1 location of img2
function isPixelOverlap(img1,x,y,img2,x1,y1){
var ax,aw,ay,ah,ctx,canvas,ctx1,canvas1,i,w,w1,h,h1;
w = img1.width;
h = img1.height;
w1 = img2.width;
h1 = img2.height;
// function to check if any pixels are visible
function checkPixels(context,w,h){
var imageData = new Uint32Array(context.getImageData(0,0,w,h).data.buffer);
var i = 0;
// if any pixel is not zero then there must be an overlap
while(i < imageData.length){
if(imageData[i++] !== 0){
return true;
}
}
return false;
}
// check if they overlap
if(x > x1 + w1 || x + w < x1 || y > y1 + h1 || y + h < y1){
return false; // no overlap
}
// size of overlapping area
// find left edge
ax = x < x1 ? x1 : x;
// find right edge calculate width
aw = x + w < x1 + w1 ? (x + w) - ax : (x1 + w1) - ax
// do the same for top and bottom
ay = y < y1 ? y1 : y;
ah = y + h < y1 + h1 ? (y + h) - ay : (y1 + h1) - ay
// Create a canvas to do the masking on
if(!reuseCanvas || pixCanvas === undefined){
pixCanvas = document.createElement("canvas");
}
pixCanvas.width = aw;
pixCanvas.height = ah;
ctx = pixCanvas.getContext("2d");
// draw the first image relative to the overlap area
ctx.drawImage(img1,x - ax, y - ay);
// set the composite operation to destination-in
ctx.globalCompositeOperation = "destination-in"; // this means only pixels
// will remain if both images
// are not transparent
ctx.drawImage(img2,x1 - ax, y1 - ay);
ctx.globalCompositeOperation = "source-over";
// are we using slow method???
if(slowButPerfect){
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // no then release referance
}
return checkPixels(ctx,aw,ah);
}
// now draw over its self to amplify any pixels that have low alpha
for(var i = 0; i < 32; i++){
ctx.drawImage(pixCanvas,0,0);
}
// create a second canvas 1/8th the size but not smaller than 1 by 1
if(!reuseCanvas || pixCanvas1 === undefined){
pixCanvas1 = document.createElement("canvas");
}
ctx1 = pixCanvas1.getContext("2d");
// reduced size rw, rh
rw = pixCanvas1.width = Math.max(1,Math.floor(aw/8));
rh = pixCanvas1.height = Math.max(1,Math.floor(ah/8));
// repeat the following untill the canvas is just 64 pixels
while(rw > 8 && rh > 8){
// draw the mask image several times
for(i = 0; i < 32; i++){
ctx1.drawImage(
pixCanvas,
0,0,aw,ah,
Math.random(),
Math.random(),
rw,rh
);
}
// clear original
ctx.clearRect(0,0,aw,ah);
// set the new size
aw = rw;
ah = rh;
// draw the small copy onto original
ctx.drawImage(pixCanvas1,0,0);
// clear reduction canvas
ctx1.clearRect(0,0,pixCanvas1.width,pixCanvas1.height);
// get next size down
rw = Math.max(1,Math.floor(rw / 8));
rh = Math.max(1,Math.floor(rh / 8));
}
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // release ref
pixCanvas1 = undefined;
}
// check for overlap
return checkPixels(ctx,aw,ah);
}
该演示可让您比较两种方法。显示每个测试的平均时间。 (如果没有完成测试,将显示NaN)
为了获得最佳效果,请查看演示整页。
使用鼠标左键或右键测试重叠。将splat图像移到另一个上以查看重叠结果。在我的机器上,慢速测试大约需要11毫秒,快速测试大约需要0.03毫秒(使用Chrome,在Firefox上快得多)。
我没有花太多时间测试我能够多快地运行它,但是有足够的空间通过减少图像相互绘制的时间来提高速度。在某些时候,微弱的像素将会丢失。
// Use the options to set quality of result
// Slow but perfect
var slowButPerfect = false;
const reuseCanvas = true;
var pixCanvas;
var pixCanvas1;
// returns true if any pixels are overlapping
function isPixelOverlap(img1,x,y,w,h,img2,x1,y1,w1,h1){
var ax,aw,ay,ah,ctx,canvas,ctx1,canvas1,i;
// function to check if any pixels are visible
function checkPixels(context,w,h){
var imageData = new Uint32Array(context.getImageData(0,0,w,h).data.buffer);
var i = 0;
// if any pixel is not zero then there must be an overlap
while(i < imageData.length){
if(imageData[i++] !== 0){
return true;
}
}
return false;
}
// check if they overlap
if(x > x1 + w1 || x + w < x1 || y > y1 + h1 || y + h < y1){
return false; // no overlap
}
// size of overlapping area
// find left edge
ax = x < x1 ? x1 : x;
// find right edge calculate width
aw = x + w < x1 + w1 ? (x + w) - ax : (x1 + w1) - ax
// do the same for top and bottom
ay = y < y1 ? y1 : y;
ah = y + h < y1 + h1 ? (y + h) - ay : (y1 + h1) - ay
// Create a canvas to do the masking on
if(!reuseCanvas || pixCanvas === undefined){
pixCanvas = document.createElement("canvas");
}
pixCanvas.width = aw;
pixCanvas.height = ah;
ctx = pixCanvas.getContext("2d");
// draw the first image relative to the overlap area
ctx.drawImage(img1,x - ax, y - ay);
// set the composite operation to destination-in
ctx.globalCompositeOperation = "destination-in"; // this means only pixels
// will remain if both images
// are not transparent
ctx.drawImage(img2,x1 - ax, y1 - ay);
ctx.globalCompositeOperation = "source-over";
// are we using slow method???
if(slowButPerfect){
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // no then release referance
}
return checkPixels(ctx,aw,ah);
}
// now draw over its self to amplify any pixels that have low alpha
for(var i = 0; i < 32; i++){
ctx.drawImage(pixCanvas,0,0);
}
// create a second canvas 1/8th the size but not smaller than 1 by 1
if(!reuseCanvas || pixCanvas1 === undefined){
pixCanvas1 = document.createElement("canvas");
}
ctx1 = pixCanvas1.getContext("2d");
// reduced size rw, rh
rw = pixCanvas1.width = Math.max(1,Math.floor(aw/8));
rh = pixCanvas1.height = Math.max(1,Math.floor(ah/8));
// repeat the following untill the canvas is just 64 pixels
while(rw > 8 && rh > 8){
// draw the mask image several times
for(i = 0; i < 32; i++){
ctx1.drawImage(
pixCanvas,
0,0,aw,ah,
Math.random(),
Math.random(),
rw,rh
);
}
// clear original
ctx.clearRect(0,0,aw,ah);
// set the new size
aw = rw;
ah = rh;
// draw the small copy onto original
ctx.drawImage(pixCanvas1,0,0);
// clear reduction canvas
ctx1.clearRect(0,0,pixCanvas1.width,pixCanvas1.height);
// get next size down
rw = Math.max(1,Math.floor(rw / 8));
rh = Math.max(1,Math.floor(rh / 8));
}
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // release ref
pixCanvas1 = undefined;
}
// check for overlap
return checkPixels(ctx,aw,ah);
}
function rand(min,max){
if(max === undefined){
max = min;
min = 0;
}
var r = Math.random() + Math.random() + Math.random() + Math.random() + Math.random();
r += Math.random() + Math.random() + Math.random() + Math.random() + Math.random();
r /= 10;
return (max-min) * r + min;
}
function createImage(w,h){
var c = document.createElement("canvas");
c.width = w;
c.height = h;
c.ctx = c.getContext("2d");
return c;
}
function createSplat(w,h,hue){
w = Math.floor(w);
h = Math.floor(h);
var c = createImage(w,h);
var maxSize = Math.min(w,h)/8;
var pow = 5;
while(maxSize > 4 && pow > 0){
var count = Math.min(100,Math.pow(w * h,1/pow) / 2);
while(count-- > 0){
var col = "hsla(";
col += (Math.floor(rand(360)+hue)%360) + ",";
col += Math.floor(rand(25,75)) + "%,";
col += Math.floor(rand(25,75)) + "%,";
col += (Math.random()*0.8+0.2).toFixed(3) + ")";
var size = rand(4,maxSize);
c.ctx.fillStyle = col;
c.ctx.beginPath();
c.ctx.arc(rand(size,w-size*2),rand(size,h - size*2),size,0,Math.PI * 2);
c.ctx.fill();
}
pow -= 1;
maxSize /= 2;
}
return c;
}
var splat1,splat2;
var slowTime = 0;
var slowCount = 0;
var notSlowTime = 0;
var notSlowCount = 0;
var onResize = function(){
ctx.font = "14px arial";
ctx.textAlign = "center";
splat1 = createSplat(rand(w/2, w), rand(h/2, h), 0);
splat2 = createSplat(rand(w/2, w), rand(h/2, h), 100);
}
function display(){
ctx.clearRect(0,0,w,h)
ctx.setTransform(1.8,0,0,1.8,w/2,0);
ctx.fillText("Pixel V pixel test using GPU and 2D Context",0, 14)
ctx.setTransform(1,0,0,1,0,0);
ctx.fillText("Hold Left button for slow and right for faster test",w /2 , 38)
ctx.fillText("Perfect pixels. Mean time : " + (slowTime / slowCount).toFixed(3) + "ms",w /2 , 38 + 14)
ctx.fillText("Almost Perfect pixels. Mean time : "+ (notSlowTime / notSlowCount).toFixed(3) + "ms",w /2 , 38 + 28)
ctx.drawImage(splat1, w / 2 - splat1.width / 2, h / 2 - splat1.height / 2)
ctx.drawImage(splat2, mouse.x - splat2.width / 2, mouse.y - splat2.height / 2);
if(mouse.buttonRaw & 0b101){
if(mouse.buttonRaw & 1){
slowButPerfect = true;
}else{
slowButPerfect = false;
}
var now = performance.now();
var res = isPixelOverlap(
splat1,
w / 2 - splat1.width / 2, h / 2 - splat1.height / 2,
splat1.width, splat1.height,
splat2,
mouse.x - splat2.width / 2, mouse.y - splat2.height / 2,
splat2.width,splat2.height
)
var time = performance.now() - now;
if(mouse.buttonRaw & 1){
slowTime += time;
slowCount += 1;
}else{
notSlowTime = time;
notSlowCount += 1;
}
if(res){
ctx.setTransform(2,0,0,2,mouse.x,mouse.y);
ctx.fillText("Overlap detected",0,0)
ctx.setTransform(1,0,0,1,0,0);
}
//mouse.buttonRaw = 0;
}
}
// Boilerplate code below
const RESIZE_DEBOUNCE_TIME = 100;
var w, h, cw, ch, canvas, ctx, mouse, createCanvas, resizeCanvas, setGlobals, globalTime = 0, resizeCount = 0;
var firstRun = true;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
}
e.preventDefault();
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element = undefined;
m.active = false;
}
}
return mouse;
})();
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
function update1(timer) { // Main update loop
if(ctx === undefined){
return;
}
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update1);
}
requestAnimationFrame(update1);