Responsive image map

I’m trying to create a responsive image map for my game (rectangular hotspots that change size in proportion to the size of the image). I tried many different methods, like this and many different JavaScript and JQuery codes, but I couldn’t make it work. Can someone help me?

Working image map for SugarCube here.

To start with, you need to set up event handlers for anything which could cause the image to resize. Something like this would go in your JavaScript section:

function resizeHandler (event) {
	/* Code to scale image map goes here. */
}

$(window).on("resize", resizeHandler);
$("#ui-bar-toggle").on("click", resizeHandler);

That should trigger the resizeHandler() function for the two most likely reasons that the image would resize. You would put your code in that function which would update the image map if the image’s scale had changed.

Note: You may want to use some “debounce” code in that function to keep it from firing too often, since resizing a window triggers a large number of “resize” events.

If you have some simple sample code which demonstrates the problem, then it might be a bit easier to see exactly what you’re trying to do and suggest something more than that.

Hope that helps! :slight_smile:

What I’m trying to do now is to use davidjbradshaw image-map-resizer.js, but I don’t know how to include it, or how to call the function.

;(function() {
  'use strict'

  function scaleImageMap() {
    function resizeMap() {
      function resizeAreaTag(cachedAreaCoords, idx) {
        function scale(coord) {
          var dimension = 1 === (isWidth = 1 - isWidth) ? 'width' : 'height'
          return (
            padding[dimension] +
            Math.floor(Number(coord) * scalingFactor[dimension])
          )
        }

        var isWidth = 0
        areas[idx].coords = cachedAreaCoords
          .split(',')
          .map(scale)
          .join(',')
      }

      var scalingFactor = {
        width: image.width / image.naturalWidth,
        height: image.height / image.naturalHeight,
      }

      var padding = {
        width: parseInt(
          window.getComputedStyle(image, null).getPropertyValue('padding-left'),
          10
        ),
        height: parseInt(
          window.getComputedStyle(image, null).getPropertyValue('padding-top'),
          10
        ),
      }

      cachedAreaCoordsArray.forEach(resizeAreaTag)
    }

    function getCoords(e) {
      //Normalize coord-string to csv format without any space chars
      return e.coords.replace(/ *, */g, ',').replace(/ +/g, ',')
    }

    function debounce() {
      clearTimeout(timer)
      timer = setTimeout(resizeMap, 250)
    }

    function start() {
      if (
        image.width !== image.naturalWidth ||
        image.height !== image.naturalHeight
      ) {
        resizeMap()
      }
    }

    function addEventListeners() {
      image.addEventListener('load', resizeMap, false) //Detect late image loads in IE11
      window.addEventListener('focus', resizeMap, false) //Cope with window being resized whilst on another tab
      window.addEventListener('resize', debounce, false)
      window.addEventListener('readystatechange', resizeMap, false)
      document.addEventListener('fullscreenchange', resizeMap, false)
    }

    function beenHere() {
      return 'function' === typeof map._resize
    }

    function getImg(name) {
      return document.querySelector('img[usemap="' + name + '"]')
    }

    function setup() {
      areas = map.getElementsByTagName('area')
      cachedAreaCoordsArray = Array.prototype.map.call(areas, getCoords)
      image = getImg('#' + map.name) || getImg(map.name)
      map._resize = resizeMap //Bind resize method to HTML map element
    }

    var /*jshint validthis:true */
      map = this,
      areas = null,
      cachedAreaCoordsArray = null,
      image = null,
      timer = null

    if (!beenHere()) {
      setup()
      addEventListeners()
      start()
    } else {
      map._resize() //Already setup, so just resize map
    }
  }

  function factory() {
    function chkMap(element) {
      if (!element.tagName) {
        throw new TypeError('Object is not a valid DOM element')
      } else if ('MAP' !== element.tagName.toUpperCase()) {
        throw new TypeError(
          'Expected <MAP> tag, found <' + element.tagName + '>.'
        )
      }
    }

    function init(element) {
      if (element) {
        chkMap(element)
        scaleImageMap.call(element)
        maps.push(element)
      }
    }

    var maps

    return function imageMapResizeF(target) {
      maps = [] // Only return maps from this call

      switch (typeof target) {
        case 'undefined':
        case 'string':
          Array.prototype.forEach.call(
            document.querySelectorAll(target || 'map'),
            init
          )
          break
        case 'object':
          init(target)
          break
        default:
          throw new TypeError('Unexpected data type (' + typeof target + ').')
      }

      return maps
    }
  }

  if (typeof define === 'function' && define.amd) {
    define([], factory)
  } else if (typeof module === 'object' && typeof module.exports === 'object') {
    module.exports = factory() //Node for browserfy
  } else {
    window.imageMapResize = factory()
  }

  if ('jQuery' in window) {
    window.jQuery.fn.imageMapResize = function $imageMapResizeF() {
      return this.filter('map')
        .each(scaleImageMap)
        .end()
    }
  }
})()

If I’m reading the instructions correctly, for SugarCube/Twine you could simply copy the above code to your JavaScript section, and then just add this:

$(document).on(":passagedisplay", function (event) {
	$('map').imageMapResize();
});

That should work, though I didn’t look at the code enough to determine if calling it repeatedly would cause any problems. If you start to experience greater and greater slowdown every passage you go to, then the code may need to be modified to handle repeated calls. I wouldn’t worry about that too much unless you notice it happening, though.

Hope that helps! :slight_smile:

1 Like

That was it! It works! I’ve done an example here. The only problem is: For some reason it doesn’t work on the starting passage. Any idea why?

Not exactly sure why, but here’s a simple hack to fix it:

$(document).on(":passagedisplay", function (event) {
	$('map').imageMapResize();
});
setTimeout(function () { $('map').imageMapResize(); }, 250);

The additional line should trigger it the extra time that it needs to work on the first passage.

Enjoy! :wink:

1 Like

Thanks, I’ve updated my example with the full code. Works nicely. We now have an image map resizer for SugarCube! :smiling_face_with_three_hearts: