Years after the fact, I realized that an optimal way to position an element over a particular region of an image is to use an SVG, add the image to the SVG, and position SVG elements over the points of interest in the image.
This site has an example in which an image of a building has floors stacked on top of it so that hovering a floor (e.g. floor 22) highlights that floor:
Here's the relevant markup:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="4.5 0 980.9 545.9" x="0px" y="0px">
<g>
<image height="546" overflow="visible" transform="matrix(0.9999 0 0 0.9999 4.4815 -0.9215)" width="981" href="https://www.360statestreet.com/wp-content/themes/statestreet360/images/floorplans/bg-floor_plans.jpg"/>
</g>
<g id="Floors">
<polygon class="floor Floor7" fill="rgba(102,199,198,0.8)" id="Floor7" opacity="0" points="424,309 450,319 449,320 624,385 632,383 663,394 715,376 715,389 665,409 629,398 624,398 449,333 450,332 424,321" values="th"/>
<polygon class="floor Floor8" fill="rgba(102,199,198,0.8)" id="Floor8" opacity="0" points="424,297 450,307 449,310 624,373 632,372 663,383 715,364 715,376 666,396 629,384 624,385 450,323 450,320 424,310" values="th"/>
<polygon class="floor Floor9" fill="rgba(102,199,198,0.8)" id="Floor9" opacity="0" points="424,287 450,297 450,299 625,361 633,360 665,371 715,352 715,365 667,382 630,372 624,374 450,311 450,308 424,299" values="th"/>
<polygon class="floor Floor10" fill="rgba(102,199,198,0.8)" id="Floor10" opacity="0" points="424,275 450,284 450,287 627,349 634,345 668,358 715,340 715,353 668,372 630,359 628,361 450,300 450,296 424,287" values="th"/>
<polygon class="floor Floor11" fill="rgba(102,199,198,0.8)" id="Floor11" opacity="0" points="424,263 450,271 450,275 627,337 634,334 667,345 715,327 715,340 667,358 635,346 627,349 450,287 450,284 424,275" values="th"/>
<polygon class="floor Floor12" fill="rgba(102,199,198,0.8)" id="Floor12" opacity="0" points="424,252 450,261 450,264 627,324 635,322 667,332 715,315 715,327 668,346 635,334 627,337 450,276 450,274 424,265" values="th"/>
<polygon class="floor Floor13" fill="rgba(102,199,198,0.8)" id="Floor13" opacity="0" points="424,240 451,249 451,251 628,310 635,308 668,319 715,301 715,315 668,333 635,322 628,325 450,265 450,263 424,254" values="th"/>
<polygon class="floor Floor14" fill="rgba(102,199,198,0.8)" id="Floor14" opacity="0" points="424,228 451,237 451,240 628,297 635,296 668,307 715,289 715,302 668,320 634,309 628,310 450,252 450,250 424,241" values="th"/>
<polygon class="floor Floor15" fill="rgba(102,199,198,0.8)" id="Floor15" opacity="0" points="424,216 451,225 451,227 628,284 634,284 668,295 715,277 715,290 670,307 634,295 628,298 450,240 450,238 424,229" values="th"/>
<polygon class="floor Floor16" fill="rgba(102,199,198,0.8)" id="Floor16" opacity="0" points="424,203 451,213 451,216 628,272 634,271 668,282 715,265 715,277 669,295 634,284 628,286 450,228 450,225 424,216" values="th"/>
<polygon class="floor Floor17" fill="rgba(102,199,198,0.8)" id="Floor17" opacity="0" points="424,192 451,203 451,205 628,260 634,259 668,270 715,253 715,265 669,282 634,271 628,273 450,216 450,213 424,204" values="th"/>
<polygon class="floor Floor18" fill="rgba(102,199,198,0.8)" id="Floor18" opacity="0" points="424,182 451,191 451,193 629,248 634,246 668,257 715,240 715,253 669,270 634,258 628,261 450,205 450,202 424,194" values="th"/>
<polygon class="floor Floor19" fill="rgba(102,199,198,0.8)" id="Floor19" opacity="0" points="424,171 451,179 451,181 629,235 630,233 668,245 715,228 715,240 669,257 634,246 628,249 450,193 450,190 424,183" values="th"/>
<polygon class="floor Floor20" fill="rgba(102,199,198,0.8)" id="Floor20" opacity="0" points="424,160 451,167 451,170 629,224 632,222 668,232 715,215 715,228 669,245 634,234 628,237 450,182 450,179 424,171" values="th"/>
<polygon class="floor Floor21" fill="rgba(102,199,198,0.8)" id="Floor21" opacity="0" points="424,148 450,156 451,160 629,212 633,209 668,220 715,204 715,216 669,233 634,222 628,225 450,172 450,168 424,160" values="st"/>
<polygon class="floor Floor22" fill="rgba(102,199,198,0.8)" id="Floor22" opacity="0" points="424,138 450,144 451,147 629,200 633,198 668,207 715,191 715,205 669,221 634,212 628,214 450,162 450,158 424,150" values="nd"/>
<polygon class="floor Floor23" fill="rgba(102,199,198,0.8)" id="Floor23" opacity="0" points="424,126 452,135 451,136 629,187 633,185 668,195 715,181 715,194 669,211 634,200 628,202 450,149 450,147 424,139" values="rd"/>
<polygon class="floor Floor24" fill="rgba(102,199,198,0.8)" id="Floor24" opacity="0" points="424,116 452,124 451,125 629,176 633,173 668,183 715,166 715,180 669,195 634,187 628,189 450,137 450,135 424,129" values="th"/>
<polygon class="floor Floor25" fill="rgba(102,199,198,0.8)" id="Floor25" opacity="0" points="424,103 452,111 451,113 629,162 633,160 668,170 715,155 715,168 669,183 634,173 628,176 450,127 450,125 424,117" values="th"/>
<polygon class="floor Floor26" fill="rgba(102,199,198,0.8)" id="Floor26" opacity="0" points="424,91 452,99 451,101 629,150 633,148 668,157 715,142 715,156 669,171 634,161 628,164 450,115 450,112 424,105" values="th"/>
<polygon class="floor Floor27" fill="rgba(102,199,198,0.8)" id="Floor27" opacity="0" points="424,80 452,88 451,90 629,138 634,135 668,144 715,130 715,143 669,158 634,149 628,151 450,103 450,100 424,93" values="th"/>
<polygon class="floor Floor28" fill="rgba(102,199,198,0.8)" id="Floor28" opacity="0" points="425,69 452,76 451,78 629,126 634,123 668,132 715,118 715,131 669,145 634,136 628,139 450,91 450,88 424,81" values="th"/>
<polygon class="floor Floor29" fill="rgba(102,199,198,0.8)" id="Floor29" opacity="0" points="449,59 449,63 448,65 513,81 525,78 557,86 557,92 622,108 632,109 681,95 681,94 681,104 715,112 664,128 630,120 622,121 449,76 446,72 426,66" values="th"/>
<polygon class="floor Floor30" fill="rgba(102,199,198,0.8)" id="Floor30" opacity="0" points="451,54 513,70 527,67 556,73 557,79 623,95 681,81 680,95 632,109 622,109 556,93 555,85 526,79 513,82 451,66" values="th"/>
<polygon class="floor Floor31" fill="rgba(102,199,198,0.8)" id="Floor31" opacity="0" points="451,40 513,55 527,52 556,59 557,65 623,81 680,68 681,81 632,94 622,96 556,79 555,73 526,67 513,70 451,53" values="st"/>
</g>
</svg>
The building image is stored in the image
tag, and each floor gets a polygon
positioned over the image. SVG resize rules handle the SVG sizing, and all children elements scale with the rescaled image!