is it possible to use html image maps within an a-frame scene?

The <map> tag places the areas above an image. It won’t project the areas onto a three dimentional object just like that.

I’d recommend an approach where you re-use the areas defined in the <map> tag within an a-frame custom component.

tldr:

I’ve made a component that seems to do the job, and is fairly simple in use:

<a-image material="src: #texture" asourcemap="#some-map"></a-image>
<!-- somewhere else -->
<map id="some-map">
   <area shape="rect" alt="rectangle" coords="0,0, 50,50" href="rect.html">
   <area shape="polygon" alt="poly" coords="0,0, 40,50, 25,0">
</map>

It should work well with the href attribute, but also the <a-image> will emit a signal with the clicked area name.

You can see it working with 3D planes and cylinders here.

end of tldr

0. Gathering <map> data

Simple parsing. Grab the <map> element, iterate through all children, and collect their data with getAttribute():

var map = document.querySelector(selector);
for (let area of map.children) {
   // area.getAttribute("href") - href attribute
   // area.getAttribute("alt") - alt name
   // area.getAttribute("coords") - coordinates array. 
} 

Store them for later use. The coordinates are comma separated strings, so you may need to parseInt() them, manage the order (i.e. [[x1,x1], [x2,y2], [x3, y3]])

1. Make the a-frame entity interactable

React on clicks, and what’s more important – check where the click occurred:

this.el.addEventListener("click", evt => {
   var UVPoint = evt.detail.intersection.uv
})

UV mapping will help us determine which point on the texture was clicked. The UV ranges from <0, 1>, so we will need to re-scale the UVPoint:

// may need waiting for "model-loaded"
let mesh = this.el.getObject3D("mesh")
// this may not be available immidiately
let image = mesh.material.map.image
let x_on_image = UVPoint * image.width
// the y axis goes from <1, 0>
let y_on_image = image.height - UVPoint * image.heigth  

So hey, we got the area coordinates and the point coordinates! There is only one thing left:

2. Determining if an area was clicked

No need to re-invent the wheel here. This SO question on checking if a point is inside a polygon has a simple inside(point, polygon) function. Actually we have everything we need, so the last thing we do is:

  • iterate through the polygons
  • check if the clicked point is inside any of the polygons
  • if positive – do your thing

like this:

var point = [x_on_texture, y_on_texture]
for (var i = 0; i < polygons.length; i++) {
   // polygons need to be [[x1, y1], [x2, y2],...[xn, yn]] here
   if (inside(point, polygons[i]) {
      console.log("polygon", i, "clicked!")
   }
}

If you skipped the tldr section – the above steps are combined in this component and used in this example

3. Old, hacky try

Another way of doing this could be:

  • receive a click on the a-frame entity
  • grab the clicked coordinates like in 1
  • hide the scene
  • check out which <area> is at the coordinates with document.elementFromPoint(x, y);.
  • show the scene
  • create a mouse event with document.createEvent("MouseEvent");
  • dispatch it on the <area> element.

The hide / show trick works really good even on my mobile phone. I was really surprised that the scene wasn’t flickering, freezing, even slowing down.

But document.elementFromPoint(x, y); didn’t work with firefox, and probably any attempt to make it work would be way more time consuming than the 02 steps. Also I believe the trappings would become bigger and case-dependant.

Anyway, here’s the old-answer component:

/* SETUP
<a-scene>
 <a-image press-map>
</a-scene>

<image id="image" sourcemap="map">
<map name="map">
  <area ...>
</map>
*/

AFRAME.registerComponent("press-map", {
  init: function() {
    // the underlying image
    this.img = document.querySelector("#image")
    // react on clicks
    this.el.addEventListener("click", evt => {
      // get the point on the UV
      let uvPoint = evt.detail.intersection.uv 
      // the y is inverted
      let pointOnImage = {
        x: uvPoint.x * this.img.width,
        y: this.img.height - uvPoint.y * this.img.height
      }
      
      // the ugly show-hide bits
      this.el.sceneEl.style.display = "none";
      this.img.style.display = "block";
      // !! grab the <area> at the (x,y) position
      var el = document.elementFromPoint(pointOnImage.x, pointOnImage.y);
      this.el.sceneEl.style.display="block"
      this.img.style.display="none"

      // create and dispatch the event
      var ev = document.createEvent("MouseEvent");
      ev.initMouseEvent(
            "click",
            true /* bubble */, false /* cancelable */,
            window, null,
            x, y, 0, 0, /* coordinates */
            false, false, false, false, /* modifier keys */
            0 /*left*/, null
      );
      el.dispatchEvent(ev);
    }
  }
})

CLICK HERE to find out more related problems solutions.

Leave a Comment

Your email address will not be published.

Scroll to Top