Blast from the Past: Breaking Down Responsive Image Map Code in HTML and JavaScript
Working on my personal website, I decided to learn about image maps (apparently popular in the 90s). Upon further study, I discovered that maps are not responsive. That lead me to a wonderful repo:
This post will go into better understanding the code and how it works.
To begin, let’s first define what an HTML Image Map is.
Straight from the MDN Web Docs:
The
<map>
HTML element is used with<area>
elements to define an image map (a clickable link area).
Say I have an image of a dog and a cat, and I want to have different click events when I click specifically on the dog, or specifically on the cat. To do this, we set up a parent <map>
that takes in child <area>
elements each with the attribute coords
, defining the coordinates of where that silly dog and cat are in the image.
How do we know what those coordinates are?
We can easily create Image Maps with online Image Map generators.
Here’s one I’ve used that I can vouch for.
Try it out yourself. Pick an image, and place an <area>
to your heart’s desire.
If you tried it out and got some code, it’s not hard to see that image resizing can really screw up our <area>
placement. The coords
that are generated are specific to the original image size (more on this later with naturalWidth
and naturalHeight
). If we wanted to resize this image, that area would still be mapped to those original coordinates — with a resize we are in a different coordinate space altogether.
At first I tried to use the transform CSS property to see if I could modify <area>
elements that way. It did not take very long for me to see that it required so much hard-coding for each <area>
, and if you have a ton of <area>
elements, this isn’t really an efficient approach.
Now, onto the image-map-resizer repo. It uses a script that dynamically updates an image map according to screen size. For simplicity, I will go into the core JavaScript functions that make this script work (ignoring jQuery).
Let’s get into the setup:
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
}
What’s happening here? As each <area>
is being iterated over, a function getCoords
is being called, which reads the coords
attribute data. Then, we get the image that is being mapped in the first place with getImg
. Lastly, as the comment writes, we bind a function called resizeMap to our <map>
by accessing the _resize property of <map>
elements.
Now let’s get into resizeMap
algorithm.
function resizeMap() {
function resizeAreaTag(cachedAreaCoords, idx) {
function scale(coord) {
var dimension = 1 === (isWidth = 1 - isWidth) ? 'width' : 'height'
return ( // 4
padding[dimension] +
Math.floor(Number(coord) * scalingFactor[dimension])
)
}
var isWidth = 0 // 1
areas[idx].coords = cachedAreaCoords
.split(',')
.map(scale)
.join(',')
}
var scalingFactor = { // 2
width: image.width / image.naturalWidth,
height: image.height / image.naturalHeight,
}
var padding = { // 3
width: parseInt(
window.getComputedStyle(image, null).getPropertyValue('padding-left'),
10),
height: parseInt(
window.getComputedStyle(image, null).getPropertyValue('padding-top'), 10),
}
cachedAreaCoordsArray.forEach(resizeAreaTag)
}
(1) First, parse the coordinates into numeric values.
(2) Then we access CSS properties of the image called naturalWidth
and naturalHeight
. Looking at the naturalWidth
docs on MDN:
The
HTMLImageElement
interface's read-onlynaturalWidth
property returns the intrinsic (natural), density-corrected width of the image in CSS pixels.
How does this compare width our good old width
property? width
is the value/default value of width attribute of tag. Same with height
and naturalHeight
, naturally (pun intended).
To figure out how much we need to scale down our width and height, we look at our current width
and height
properties are and divide them by original image dimensions, which is our scalingFactor
object.
(3) Then we get padding (if it exists) by accessing a function on the window getComputedStyle
. (More on getComputedStyle
here.) That gets set equal to padding
.
(4) The return statement gives us the padding dimensions as stored in the the padding
object (width and height, respectively). Lastly, we scale our coordinates by multiplying our area coordinates against our scalingFactor
variable.
And voila! The areas are scaled and mapped to resizes.
Check out my implementation of this code on my site: