Text Collision Detection

2020-02-02 02:01发布

问题:

I am building a web application that draws a set of letters in different fonts on an HTML 5 Canvas using fillText. The user will click somewhere on that canvas and I need to check which letter they clicked on (or if they clicked on a letter at all).

I think I will need to:

  1. Get the vector path for each letter (I have no clue how to do this).
  2. Check if the click point is inside the letter path using some simple collision-detection algorithm.

Is there some easy function to do this that I am missing? Or maybe a library for things like this? If there aren't any libraries, how do I get the path for the letter in a specific font to do the checking myself?

I need to use the actual shape of the letter and not just its bounding box as I don't want the user to be able to click in the middle of an O and it register as a hit.

Any hints in this direction would be appreciated.

回答1:

Logic

You can't handle separate letters on canvas without providing custom logic for it. Everything drawn to the canvas is merged to a soup of pixels.

And unfortunately you can't add text as pure path so you will have to check pixel values. Otherwise you could simply add the text to a new path and use the isPointInPath method for each letter.

One approach

We can't provide full solutions here on SO but here is a basis you can hopefully built on top of to provide basic logic to click single letters on a canvas:

  • Each letter is stored as object incl. its position, size, font and char, but also with a rectangle hit region (see below)
  • Define an array with these objects and then pass them to a render function
  • When you register a click iterate through the array and test against the rectangular hit-region and if inside check the pixel (*)

*) To distinguish between overlapping letters you need to check by priority. You can also render this char onto a separate canvas so you get pixels of only this char. I am not showing this in the demo but you'll get the idea.

Demo

var ltrs = []; /// stores the letter objects

/// Create some random objects

for(;i < 20; i++) {

    /// build the object
    var o = {char: alpha[((alpha.length - 1) * Math.random())|0],
             x:    ((w - 20) * Math.random())|0,
             y:    ((h - 20) * Math.random())|0,
             size: (50 * Math.random() + 16)|0,
             font: fonts[((fonts.length - 1) * Math.random())|0]};

             /// store other things such as color etc.

    /// store it in array
    ltrs.push(o);
}

Then we have some function to render these characters (see demo).

When we then handle clicks we iterate through the object array and check by boundary first to check what letter we are at (picking just a pixel here won't enable us to ID the letter):

demo.onclick = function(e) {

    /// adjust mouse position to be relative to canvas
    var rect = demo.getBoundingClientRect(),
        x = e.clientX - rect.left,
        y = e.clientY - rect.top,
        i = 0, o;

    /// iterate
    for(;o = ltrs[i]; i++) {

        /// is in rectangle? "Older" letters has higher priority here...
        if (x > o.x && x < (o.x + o.rect[2]) &&
            y > o.y && y < (o.y + o.rect[3])) {

            /// it is, check if we actually clicked a letter
            /// This is what you would adopt to be on a separate canvas...    
            if (checkPixel(x, y) === true) {
                setLetterObject(o, '#f00')
                return;
            }
        }
    }
}

The pixel check is straight forward, it picks a single pixel at x/y position and checks its alpha value (or color if you use solid backgrounds):

function checkPixel(x, y) {
    var data = ctx.getImageData(x, y, 1, 1).data;
    return (data[3] !== 0);
}

CLICK HERE FOR ONLINE DEMO

Updated check pixel function:

This updated check is capable of checking letters even if they are overlapping in the same region.

We create a separate canvas to draw the letter in. This isolates the letter and when we pick a pixel we can only get a pixel from that specific letter. It also doesn't matter what background color is as we our off-screen canvas only set pixels for the letter and not background during the check. The overhead is minimal.

function checkPixel(o, x, y) {

    /// create off-screen canvas        
    var oc = document.createElement('canvas'),
        octx = oc.getContext('2d'),
        data,
        oldX = o.x,
        oldY = o.y;

    /// default canvas is 300x150, adjust if letter size is larger *)
    //oc.width = oc.height = 200;

    /// this can be refactored to something better but for demo...
    o.x = 0;
    o.y = 0;

    setLetterObject(octx, o, '#000');

    o.x = oldX;
    o.y = oldY;

    data = octx.getImageData(x - oldX, y - oldY, 1, 1).data;
    return (data[3] !== 0);
}

*) When we create a canvas the default size is 300x150. To avoid re-allocating a new bitmap we just leave it as it is as the memory is already allocated for it and we only need to pick a single pixel from it. If letters has larger pixel size than the default size we will of course need to re-allocate to make the letter fit.

In this demo we temporary override the x and y position. For production you should enable the setLetterObject method to somehow override this as that would be more elegant. But I'll leave it as-is here in the demo as the most important thing is to understand the principle.



回答2:

I'd say that the best option is to actually use pixels that by the way are the most accurate thing you can do (remember that the user is seeing pixels when clicking, nothing more).

Since you cannot just use the color directly (because there can be many text objects with the same color (and may be also other primitives with the same color) you could use instead a separate "pick" canvas for that.

Basically when you draw your objects on the main canvas on the repaint function you also draw them in another hidden canvas with the exact same size, but you draw them using a unique color for each entity. You can therefore have up to 16 millions entity (24-bit) on the canvas and know instantly which one was clicked by keeping a map between color code and the entity itself. By the way this sort of map is often used in CAD applications exactly to speed up picking.

The only somewhat annoying part is that there's no portable way to disable antialiasing when drawing on a canvas so it's possible that the color you'll get back from the pick canvas is not one of the colors you used or, even worse, it's possible that by clicking on the border of an entity a different unrelated entity is considered selected.

This should be a very rare event unless your display is really really crowded and picking is basically random anyway.