Image map by alpha channel
Asked Answered
V

3

6
<img src="circle.png" onclick="alert('clicked')"/>

Let's imagine that circle.png is a 400x400 px transparent background image with a circle in the middle.

What I've got now is that the entire image area (400x400px) is clickable. What I would like the have is that only the circle (non transparent pixels) are clickable.

Of course I know that in this example I could use the <map> tag and a circular area, but I'm looking for a general solution which will take into consideration actual image transparency and work for any kind of images (i.e. non regular shapes).

The most complex way I could see is to trace the contour of the image basing on each pixel alpha, convert to a path (maybe simplify) and apply as a map.

Is there a more efficient / straightforward way to do so?

Venable answered 27/6, 2012 at 14:50 Comment(2)
Do you need to do this because the images will be dynamically generated? While I'm certain it could be done in javascript, it's probably going to be slow. Having used the "magic wand" and whatnot in Photoshop, it's not trivial to "get it right" either. Seems like if you are generating images dynamically, use the source data to create an imagemap. If you aren't, then why not do it offline and use an imagemap?Philbin
Yeah that's going to be dynamically as images will be dynamically loaded too...Venable
A
17

Using the canvas tag, you can determine the color value of a pixel under a given spot. You can use the event data to determine the coordinates, then check for transparency. All that remains is loading the image up into a canvas.

First, we'll take care of that:

  var ctx = document.getElementById('canvas').getContext('2d');
  var img = new Image();
  img.onload = function(){
    ctx.drawImage(img,0,0);
  };
  img.src = [YOUR_URL_HERE];

This first bit grabs the canvas element, then creates an Image object. When the image loads, it is drawn on the canvas. Pretty straightforward! Except... if the image is not on the same domain as the code, you're up against the same-domain policy security. In order to get the data of our image, we'll need the image to be locally hosted. You can also base64 encode your image, which is beyond the scope of this answer. (see this url for a tool to do so).

Next, attach your click event to the canvas. When that click comes in, we'll check for transparency and act only for non-transparent click regions:

    if (isTransparentUnderMouse(this, e))
        return;
    // do whatever you need to do
    alert('will do something!');

The magic happens in the function isTransparentUnderMouse, which needs two arguments: the target canvas element (this in the click handler's scope) and the event data (e, in this example). Now we come to the meat:

var isTransparentUnderMouse = function (target, evnt) {
    var l = 0, t = 0;
    if (target.offsetParent) {
        var ele = target;
        do {
            l += ele.offsetLeft;
            t += ele.offsetTop;
        } while (ele = ele.offsetParent);
    }
    var x = evnt.page.x - l;
    var y = evnt.page.y - t;
    var imgdata = target.getContext('2d').getImageData(x, y, 1, 1).data;
    if (
        imgdata[0] == 0 &&
        imgdata[1] == 0 &&
        imgdata[2] == 0 &&
        imgdata[3] == 0
    ){
        return true;
    }
    return false;
};

First, we do some dancing around to get the precise position of the element in question. We're going to use that information to pass to the canvas element. The getImageData will give us, among other things, a data object that contains the RGBA of the location we specified.

If all those values are 0, then we're looking at transparency. If not, there's some color present. -edit- as noted in the comments, the only value we really need to look at is the last, imgdata[3] in the above example. The values are r(0)g(1)b(2)a(3), and transparency is determined by the a, alpha. You could use this same approach to find any color at any opacity that you know the rgba data for.

Try it out here: http://jsfiddle.net/pJ3MD/1/

(note: in my example, I used a base64 encoded image because of the domain security I mentioned. You can ignore that portion of the code, unless you also intend on using base64 encoding)

Same example, with changes to the mouse cursor thrown in for fun: http://jsfiddle.net/pJ3MD/2/

Documentation

Ancipital answered 27/6, 2012 at 15:29 Comment(8)
+1 for the effort you've put into this answer. Also +1 because it's a good answer!Walloon
just a cosmetic improvement for isTransparentUnderMouse: return (imgdata[0] + imgdata[1] + imgdata[2] + imgdata[3] == 0)Venable
Sure! Technically, you only even need to look at imgdata[3], since that is the alpha channel, so you could make it even simpler still.Ancipital
yeah! to make things just perfect, we could use the isTransparentUnderMouse method also in response to mousemove event to toggle switch the cursor to pointer (hand) or arrow accordingly.Venable
See the second example, right about the documentation list :) jsfiddle.net/pJ3MD/2Ancipital
@Chris and I implemented this type of solution today and found a couple of caveats: 1. You have to use FlashCanvas Pro for IE flashcanvas.net, excanvas doesn't support getImageData() neither does FlashCanvas FreeFluorinate
(continued from above) flashcanvas.net. Using a PNG24 with different levels of transparency was actually messing up the RGB values by one digit in IE (with FlashCanvas and w/o for IE9) as well as Safari on Mac. Saving the image without transparency fixed that issue. Finally, the array returned from getImageData() isn't a normal array so you can't join but Array.apply([], imgdata).join('|') worksFluorinate
I saw this approach being used in an HTML5 game, they were using the R channel to determine hit detection surfaces. They had 5 materials barriers could be made from, so a wall of type A would be (0,0,0,1), a wall of type B be would be (1,0,0,1), and so on. All look black on the screen, I thought it was pretty clever.Ancipital
L
4

You can do this using HTML5 canvas. Draw the image in to the canvas, attach a click handler to the canvas, and in the handler, check if the pixel that was clicked on is transparent.

Lexi answered 27/6, 2012 at 15:14 Comment(1)
Ah, we thought alike on this one, I didn't see your answer because it took a minute to work it all out! Cheers!Ancipital
N
1

thank you chris for this great answer

I added a few lines to the code to handle with scaling the canvas

so what I do now is creating a canvas of the exact pixel size the image has that is drawn on it. like (for an Image 220px*120px):

<canvas width="220" height="120" id="mainMenu_item"></canvas>

and scale the canvas using css:

#mainMenu_item{ width:110px; }

and the adjusted isTransparentUnderMouse function looks like:

    var isTransparentUnderMouse = function (target, evnt) {

    var l = 0, t = 0;
    if (target.offsetParent) {
        var ele = target;
        do {
            l += ele.offsetLeft;
            t += ele.offsetTop;
        } while (ele = ele.offsetParent);
    }

    var x = evnt.pageX - l;
    var y = evnt.pageY - t;

    var initialWidth = $(evnt.target).attr('width');
    var clientWidth = evnt.target.clientWidth;
    x = x * (initialWidth/clientWidth);

    var initialHeight = $(evnt.target).attr('height');;
    var clientHeight = evnt.target.clientHeight;
    y = y * (initialHeight/clientHeight);

    var imgdata = target.getContext('2d').getImageData(x, y, 1, 1).data;

    if (
        imgdata[0] == 0 &&
        imgdata[1] == 0 &&
        imgdata[2] == 0 &&
        imgdata[3] == 0
    ){
        return true;
    }
    return false;
};
Nanceenancey answered 29/1, 2013 at 9:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.