PehBehBeh

Erfahrungen eines Hobby-Webentwicklers

Pixelgenaue Objekterkennung mit HTML5 Canvas als Hilfsmittel

| 11 Kommentare

Wenn wir uns im Netz bewegen bestehen alle Websites irgendwie aus Kästen. Alles ist in irgendeiner Art und Weise irgendwie rechteckig. Zwar geben uns neue CSS3-Techniken die Möglichkeit unsere Elemente abzurunden, aber dennoch: Sie sind Kästen. Wen man eine schöne (nicht rechteckige) Grafik anklickbar machen möchte – und zwar nicht den gesamten Kasten, in dem sie sich befindet, muss man zum Beispiel auf arbeitsintensive Image Maps oder Flash zurückgreifen.

In diesem Artikel möchte ich euch zeigen, dass es mit Hilfe des Canvas-Elements aus HTML5 auch anders funktioniert. Das Element wird dabei nur als Hilfsmittel genutzt und nicht dazu, die Grafik einzubinden.


Die Idee

Die Idee stammt zugegebenermaßen nicht von mir selbst – die Umsetzung in diesem Artikel jedoch schon. Ich schaute letztens eine interessantes Video über die Erstellung einer Game Engine in JavaScript. Im Video wurde darüber gesprochen Canvas als ein Tool zu benutzen um Pixeldaten aus einem Bild auszulesen – genauer gesagt aus einer PNG-Datei, die Transparenz beinhaltet.

So lässt sich ein zweidimensionales Array aus Nullen und Einsen erstellen, mit Hilfe dessen man leicht entscheiden kann, ob die Maus sich auf oder neben dem Objekt befindet. Dabei ist man noch nicht einmal beschränkt auf Klick-Events. Nette Hover-Effekte lassen sich genauso gut erstellen.

Demonstration

Obwohl es in allen modernen Browsern funktionieren sollte, habe ich die Demo kurz im Firefox abgefilmt, sodass auch wirklich jeder in den Genuss kommen kann. Im Video ist ein Hover-Effekt zu sehen, der die Deckkraft der Bilder verändert.

Link zur Live-Demo »

Setup

HTML

Wie ich anfangs bereits erwähnte, können Bilder ganz normal in die Seite eingebunden werden, wie man es gewohnt ist. In meinem Beispiel habe ich Divs erstellt, die ein Hintergund-Bild besitzen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="main" class="container">
    <div class="clickable arrow"></div>
    <div class="clickable build"></div>
    <div class="clickable building"></div>
    <div class="clickable folder"></div>
    <hr />
    <div class="clickable home"></div>
    <div class="clickable interact"></div>
    <div class="clickable paint"></div>
    <div class="clickable pathing"></div>
    <hr />
    <div class="clickable pen"></div>
    <div class="clickable people"></div>
    <div class="clickable phonebook"></div>
    <div class="clickable plus"></div>
    <hr />
    <div class="clickable printer"></div>
    <div class="clickable school"></div>
    <div class="clickable vault"></div>
    <div class="clickable world"></div>
    <hr />
</div>

CSS

Die CSS-Regeln sind ebenfalls überschaubar. Die Deckkraft wird bei allen Bildern etwas verringert. Im Internet Explorer funktioniert der Effekt eh nicht, deswegen müssen wir uns auch auch nur um die Deckkraft bei den anderen Browsern kümmern.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
hr {
    clear: left;
}
 
.clickable {
    float: left;
    width: 128px;
    height: 128px;
    margin-right: 30px;
    -moz-opacity: 0.5;
    opacity: 0.5;
}
 
.hover {
    -khtml-opacity: 1.0;
    -moz-opacity: 1.0;
    opacity: 1.0;
}
 
.arrow    { background: url('../img/arrow.png');      }
.build    { background: url('../img/build.png');      }
.building { background: url('../img/building.png');   }
.folder   { background: url('../img/folder.png');     }
 
.home      { background: url('../img/home.png');      }
.interact  { background: url('../img/interact.png');  }
.paint     { background: url('../img/paint.png');     }
.pathing   { background: url('../img/pathing.png');   }
 
.pen       { background: url('../img/pen.png');       }
.people    { background: url('../img/people.png');    }
.phonebook { background: url('../img/phonebook.png'); }
.plus      { background: url('../img/plus.png');      }
 
.printer   { background: url('../img/printer.png');   }
.school    { background: url('../img/school.png');    }
.vault     { background: url('../img/vault.png');     }
.world     { background: url('../img/world.png');     }

JavaScript

Ich folgenden JavaScript-Teil werde ich jQuery-Funktionen nutzen, die mir das Leben hier und da etwas einfacher machen. Die Funktionen sind aber keinesfalls für die Umsetzung notwendig.

Insgesamt ist der oben gezeigt Hover-Effekt in drei Teile aufgeteilt:

  1. Die Maus betritt den Div (mouseenter)
  2. Die Maus bewegt sich im Div (mousemove)
  3. Die Maus verlässt den Div (mouseleave)

Natürlich muss jeder selbst wissen, wie man es letzendlich implementiert. Ich möchte lediglich die Grundidee mit diesem Beispiel veranschaulichen.

1. Event: mouseenter

Wenn die Maus den Div betritt, muss das Array erzeugt werden, das die Bild-Daten enthält. Dazu wird ein Canvas-Element erstellt, in dem das Hintergrundbild des Divs gezeichnet wird. Danach können die Pixel ausgelesen werden. Anhand des Alpha-Wertes (jeder vierte Wert) wird die Transparenz abgefragt und eine Clickmap erzeugt (0=nichts, 1=Objekt).

Zum Schluss wird die Clickmap gespeichert, damit wir sie im nächsten Schritt abfragen können.

Mit den jQuery-Funktionen ist alles recht simpel und schick gelöst. Nur mit der Abfrage des Hintergrundbildes bin ich noch nicht ganz zufrieden. Genaueres dazu hatte ich bereits letztens in einem anderen Artikel geäußert.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$('.clickable').mouseenter(function(e) {
    var obj = $(this);
 
    var canvas = document.createElement('canvas');
    var context = canvas.getContext('2d');
 
    var img = new Image();
    img.onload = function() {
        // Bild nachzeichnen & ImageData auslesen
        canvas.width  = img.width;
        canvas.height = img.height;
        context.drawImage(img, 0, 0);
 
        var imageData = context.getImageData(0, 0, img.width, img.height);
 
        // Clickmap erstellen
        var clickMap = [];
        for (var i=0, n=imageData.data.length; i<n; i+=4) {
            var row = Math.floor((i/4)/img.width);
            var col = (i/4) - (row * img.width);
 
            if (!clickMap[row]) clickMap[row] = [];
            clickMap[row][col] = (imageData.data[i+3] == 0) ? 0 : 1;
        }
 
        // Clickmap speichern
        obj.data('clickMap', clickMap);
    }
 
    // Bild-Quelle angeben und onload-Event auslösen
    img.src = obj.css('backgroundImage').replace(/\"/g, '').slice(4, -1);
});

2. Event: mousemove

Wenn wir die Maus über dem Div bewegen wird abgefragt ob wir uns auf einem sichtbaren Pixel befinden. Wenn dies der Fall ist, wird dem Div eine CSS-Klasse hinzugefügt bzw. wieder entfernt, wenn der Cursor sich auf einem transparenten Pixel befindet. In diesem Beispiel ändert die CSS-Klasse die Deckkraft.

Bei der Berechnung der Cursor-Position werden Seitenabstand und Scrollweite berücksichtigt. Die Offset-Werte müssen gerundet werden, da es im Firefox zu Dezimalwerten kommen kann.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$('.clickable').mousemove(function(e) {
    var obj = $(this);
 
    if ("undefined" === typeof(obj.data('clickMap'))) {
        return;
    }
 
    var offset = obj.offset();
    var xPos = e.clientX - Math.round(offset.left) + $(window).scrollLeft();
    var yPos = e.clientY - Math.round(offset.top)  + $(window).scrollTop();
 
    if (obj.data('clickMap')[yPos][xPos]) {
        obj.addClass('hover');
    } else {
        obj.removeClass('hover');
    }
});

3. Event: mouseleave

Da der Fall auftreten könnte, das jemand die Maus besonders Schnell aus dem Div zieht oder das Bild am Rande des Divs einen sichtbaren Pixel besitzt sollte die CSS-Klasse auf jeden Fall entfernt werden, sobald man den Div verlässt.

1
2
3
$('.clickable').mouseleave(function(e) {
    $(this).removeClass('hover');
});

Fazit

Das war’s. Hat mich zwar einige Zeit gekostet diesen Effekt zu basteln, aber wenn man es jetzt so sieht, steckt nicht wirklich viel dahinter. Ich hoffe, dass ich euch damit weiterhelfen oder zumindest inspirieren konnte. Ich finde es zunmindest erstaunlich, wie neue Web-Technologien uns die Arbeit erleichtern können.

Dir hat der Artikel gefallen?
Dann abonniere doch den RSS-Feed!
  • Saubere Arbeit! Das gefällt mir sehr gut und die Menge an Code ist sehr überschaubar. Ich habe diese Methode in meiner Firefox-Erweiterung für den Farbwähler verwendet, aber auf diese Idee wäre ich nicht gekommen 🙂
    Es muss eben nicht immer „:hover“ sein und zukünftig dürfte diese Technik bestimmt Anklang finden.

  • Pierre Kircher

    Super Arbeit genau davon hatte der Jquery Macher vor 1en jahr in seiner Rede bei Google über javascript Gameengines gesprochen

    danke hast mir sehr geholfen .

    tile maps sind nun weit einfacher zu sterilierien.

  • Phillip

    Hey, das ist genau das was ich für eine Website gesucht habe, jedoch funktioniert es bei mir nicht …

    Könntest du deshalb die Projektdateien hochladen? Mit den Bildern und dem gesamten Code?
    Wäre echt top 😉

  • In der Live-Demo kannst du dir die Dateien ansehen…

  • Merlin

    Super
    Finde ich echt klasse.
    Was muss man machen um es nicht auf ein Background sondern um es auf ein img tag anzuwenden? Ein Tipp oder etwas Hilfe wäre genial.

    Danke vorab

  • Merlin

    Sry 🙂 schon selbst heraus gefunden. Manchmal muss man nur um Hilfe bitten um selber drauf zu kommen.

    Danke trotzdem für dieses Skript.

  • Merlin

    Ich habe dazu noch mal eine Frage
    wie bekomme ich die clientHeight bzw. clientWidth in das skript?
    Egal was ich bisher versucht habe ich bekomme immer 0 oder aber die Breite bzw. Höhe von dem Bild und nicht wie es dargestellt ist. Sollte es also kleiner sein, sagen wir 50%, bekommt das Skript durch img.width trotzdem den Pixelwert von dem 100% Bild und img.clientWidth funktioniert leider nicht.

    Ich würde mich über jede Hilfe sehr freuen.
    Danke

  • Wenn ich dich richtig verstanden habe, sollte es mit $('img').attr('width'); bzw. $('img').attr('height'); funktionieren.

    http://api.jquery.com/attr/

  • Pingback: Darf ich vorstellen: Meine DevZone()

  • Peter

    Danke, sehr hübsch!

  • Pingback: 11design.de | Pearltrees()