28

I have a tricky question: I have a fullsize background over the site I'm working on. Now I want to attach a div to a certain position on the image and also that the div scales in the same way the my background image with the "background-size: cover" property does. So in this example, I have a picture of a city, which covers the browser window and I want my div to overlay one particular building, no matter of the window size.

I already managed to make the div sticking to one position, but cant make it resize properly. What I did so far:

http://codepen.io/EmmieBln/pen/YqWaYZ

var imageWidth = 1920,
    imageHeight = 1368,
    imageAspectRatio = imageWidth / imageHeight,
    $window = $(window);

var hotSpots = [{
    'x': -160,
    'y': -20,
    'height': 400,
    'width': 300
}];

function appendHotSpots() {
    for (var i = 0; i < hotSpots.length; i++) {
        var $hotSpot = $('<div>').addClass('hot-spot');
        $('.container').append($hotSpot);
    }
    positionHotSpots();
}

function positionHotSpots() {
    var windowWidth = $window.width(),
        windowHeight = $window.height(),
        windowAspectRatio = windowWidth / windowHeight,
        $hotSpot = $('.hot-spot');

    $hotSpot.each(function(index) {
        var xPos = hotSpots[index]['x'],
            yPos = hotSpots[index]['y'],
            xSize = hotSpots[index]['width'],
            ySize = hotSpots[index]['height'],
            desiredLeft = 0,
            desiredTop = 0;

        if (windowAspectRatio > imageAspectRatio) {
            yPos = (yPos / imageHeight) * 100;
            xPos = (xPos / imageWidth) * 100;
            xSize = (xSize / imageWidth) * 1000;
            ySize = (ySize / imageHeight) * 1000;
        } else {
            yPos = ((yPos / (windowAspectRatio / imageAspectRatio)) / imageHeight) * 100;
            xPos = ((xPos / (windowAspectRatio / imageAspectRatio)) / imageWidth) * 100;
        }

        $(this).css({
            'margin-top': yPos + '%',
            'margin-left': xPos + '%',
            'width': xSize + 'px',
            'height': ySize + 'px'
        });

    });
}

appendHotSpots();
$(window).resize(positionHotSpots);

My idea was: If (imageWidth / windowWidth) < 1 then set Value for var Scale = (windowWidth / imageWidth) else var Scale ( windowHeight / imageHeight ) and to use the var Scale for transform: scale (Scale,Scale) but I couldnt manage to make this work…

Maybe you guys could help me out…

  • This is a very well-asked question. –  Mar 11 '16 at 14:31
  • My idea: If (imageWidth / windowWidth) < 1 then set Value for var Scale = (windowWidth / imageWidth) else var Scale ( windowHeight / imageHeight ) and to use the var Scale for transform: scale (Scale,Scale) but I couldnt manage to make this work… – Maximilian Becker Mar 11 '16 at 16:29
  • 2
    This is an interesting question, if you don't get any good answers, let me know and I will offer a [bounty](http://stackoverflow.com/help/bounty) to give it more attraction. – Asons Mar 11 '16 at 16:41
  • This may sound silly, but couldn't you just 'blend' it all into one image? What exactly is in the div? If you rendered it all on a canvas you could get the whole thing to resize and maintain ratios etc. –  Mar 12 '16 at 14:26
  • @JᴀʏMᴇᴇ Imagine it's pins on a map, or furniture on a room drawing, all added dynamically ... then it make sense, at least to me – Asons Mar 12 '16 at 20:05
  • @MaximilianBecker May I ask if the answer given is to your satisfaction? – Asons Mar 13 '16 at 20:42
  • @LGSon I am surprised you open a bounty for this, from your profile i see you know SVG. ) – tnt-rox Mar 16 '16 at 07:14
  • @tnt-rox Thanks for reminding me :) ... I have been running into several situations (some being questions here at SO), where a HTML structure needed to adjust to objects in an image, so when finding this question it was a no brainer to offer a bounty. Your answer is the first without a script, so now I start to wonder, how close can one get doing this using CSS only? – Asons Mar 16 '16 at 09:32
  • @LGSon :grin I would like to try with pure css (mindbender) :)) but the problem is flow related, anything other than SVG for vector scaling is unfortunately a DOM hack. Choosing the right node for the job is all important. – tnt-rox Mar 16 '16 at 09:45
  • @LGSon Yeah, the answers posted here really solved the problem! thanks guys! – Maximilian Becker Mar 16 '16 at 12:18
  • @MaximilianBecker That sounds perfect, and don't forget to accept the answer that best solve your question. – Asons Mar 16 '16 at 12:24
  • @tnt-rox I found the **CSS only** solution, no hack ... in this answer, which I updated: http://stackoverflow.com/a/36097410/2827823 – Asons Mar 20 '16 at 11:00
  • 1
    @LGSon... lol, i use vw,vh units all the time, with fallbacks of course. How could I have missed that .... This is a great thread ) – tnt-rox Mar 20 '16 at 11:56

6 Answers6

13

Solution for background-size:cover

I am trying to give you solution(or consider as an idea). You can check working demo here. Resize the window to see the result.

First,I didn't understand why you are using transform,top:50% and left:50%for hotspot. So I tried to solve this using minimal use-case and adjusted your markup and css for my convenience.

Here rImage is the aspect ratio of the original image.

 var imageWidth = 1920;
 var imageHeight = 1368;
 var h = {
   x: imageWidth / 2,
   y: imageHeight / 2,
   height: 100,
   width: 50
 };
 var rImage= imageWidth / imageHeight;

In window resize handler,calculate the aspect ration of viewport r. Next,the trick is to find the dimensions of the image when we resize the window. But,viewport will clip the image to maintain aspect ratio. So to calculate the image dimensions we need some formula.

When using background-size:cover to calculate the dimensions of image,below formulas are used.

if(actual_image_aspectratio <= viewport_aspectratio)
    image_width = width_of_viewport
    image_height = width_ofviewport / actual_image_aspectratio 

And

if(actual_image_aspectratio > viewport_aspectratio)
    image_width = height_of_viewport * actual_image_aspectratio 
    image_height = height_of_viewport

You can refer this URL for more understanding on image dimensions calculation when using background-size:cover.

After getting the dimensions of the image, we need to plot the hot-spot coordinates from actual image to new image dimensions.

To fit the image in viewport image will be clipped on top & bottom / left & right of the image. So we should consider this clipped image size as an offset while plotting hotspots.

offset_top=(image_height-viewport_height)/2
offset_left=(image_width-viewport_width)/2

add this offset values to each hotspot's x,y coordnates

var imageWidth = 1920;
var imageHeight = 1368;
var hotspots = [{
  x: 100,
  y: 200,
  height: 100,
  width: 50
}, {
  x: 300,
  y: 500,
  height: 200,
  width: 100
}, {
  x: 600,
  y: 600,
  height: 150,
  width: 100
}, {
  x: 900,
  y: 550,
  height: 100,
  width: 25
}];
var aspectRatio = imageWidth / imageHeight;

$(window).resize(function() {
  positionHotSpots();
});
var positionHotSpots = function() {
  $('.hotspot').remove();
  var wi = 0,
    hi = 0;
  var r = $('#image').width() / $('#image').height();
  if (aspectRatio <= r) {
    wi = $('#image').width();
    hi = $('#image').width() / aspectRatio;
  } else {
    wi = $('#image').height() * aspectRatio;
    hi = $('#image').height();
  }
  var offsetTop = (hi - $('#image').height()) / 2;
  var offsetLeft = (wi - $('#image').width()) / 2;
  $.each(hotspots, function(i, h) {

    var x = (wi * h.x) / imageWidth;
    var y = (hi * h.y) / imageHeight;

    var ww = (wi * (h.width)) / imageWidth;
    var hh = (hi * (h.height)) / imageHeight;

    var hotspot = $('<div>').addClass('hotspot').css({
      top: y - offsetTop,
      left: x - offsetLeft,
      height: hh,
      width: ww
    });
    $('body').append(hotspot);
  });
};
positionHotSpots();
html,
body {
  height: 100%;
  padding: 0;
  margin: 0;
}
#image {
  height: 100%;
  width: 100%;
  background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Alexanderplatz_Stadtmodell_1.jpg/1920px-Alexanderplatz_Stadtmodell_1.jpg');
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
}
.hotspot {
  position: absolute;
  z-index: 1;
  background: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='image'></div>

Solution for background-size:contain

When using background-size:contain to calculate the dimensions of image, below formulas are used.

if(actual_image_aspectratio <= viewport_aspectratio)
    image_width = height_of_viewport * actual_image_aspectratio 
    image_height = height_of_viewport

And

if(actual_image_aspectratio > viewport_aspectratio)
    image_width = width_of_viewport
    image_height = width_ofviewport / actual_image_aspectratio

To fit the image in viewport additional space will be added on top & bottom / left & right of the image. So we should consider this space as an offset while plotting hotspots.

offset_top=(viewport_height-image_height)/2
offset_left=(viewport_width-image_width)/2

Add this offset values to each hotspot's x,y coordnates

 var imageWidth = 1920;
 var imageHeight = 1368;
 var hotspots = [{
   x: 100,
   y: 200,
   height: 100,
   width: 50
 }, {
   x: 300,
   y: 500,
   height: 200,
   width: 100
 }, {
   x: 600,
   y: 600,
   height: 150,
   width: 100
 }, {
   x: 900,
   y: 550,
   height: 100,
   width: 25
 }];
 var aspectRatio = imageWidth / imageHeight;

 $(window).resize(function() {
   positionHotSpots();
 });
 var positionHotSpots = function() {
   $('.hotspot').remove();
   var wi = 0,
     hi = 0;

   var r = $('#image').width() / $('#image').height();
   if (aspectRatio <= r) {
     wi = $('#image').height() * aspectRatio;
     hi = $('#image').height();

   } else {
     wi = $('#image').width();
     hi = $('#image').width() / aspectRatio;
   }
   var offsetTop = ($('#image').height() - hi) / 2;
   var offsetLeft = ($('#image').width() - wi) / 2;
   $.each(hotspots, function(i, h) {

     var x = (wi * h.x) / imageWidth;
     var y = (hi * h.y) / imageHeight;

     var ww = (wi * (h.width)) / imageWidth;
     var hh = (hi * (h.height)) / imageHeight;

     var hotspot = $('<div>').addClass('hotspot').css({
       top: y + offsetTop,
       left: x + offsetLeft,
       height: hh,
       width: ww
     });
     $('body').append(hotspot);
   });
 };
 positionHotSpots();
html,
body {
  height: 100%;
  padding: 0;
  margin: 0;
}
#image {
  height: 100%;
  width: 100%;
  background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Alexanderplatz_Stadtmodell_1.jpg/1920px-Alexanderplatz_Stadtmodell_1.jpg');
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}
.hotspot {
  position: absolute;
  z-index: 1;
  background: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='image'></div>

Solution for background-size:100% 100%

This is the solution if someone looking for background-size:100% 100% check the working demo here. Resize the window to see the result.

Here we don't need to calculate the image dimensions as the image will always fit to into the div. So we can just calculate the new coordinates of hotspot using height and width of viewport and actualimage.

var imageWidth = 1920;
var imageHeight = 1368;
var hotspots = [{
  x: 100,
  y: 200,
  height: 100,
  width: 50
}, {
  x: 300,
  y: 500,
  height: 200,
  width: 100
}, {
  x: 600,
  y: 600,
  height: 150,
  width: 100
}, {
  x: 900,
  y: 550,
  height: 100,
  width: 25
}];

$(window).resize(function() {
  positionHotSpots();
});


var positionHotSpots = function() {
  $('.hotspot').remove();

  $.each(hotspots, function(i, h) {
    var x = ($('#image').width() * h.x) / imageWidth;
    var y = ($('#image').height() * h.y) / imageHeight;

    var ww = ($('#image').width() * (h.width)) / imageWidth;
    var hh = ($('#image').height() * (h.height)) / imageHeight;
    var hotspot = $('<div>').addClass('hotspot').css({
      top: y,
      left: x,
      height: hh,
      width: ww
    });
    $('body').append(hotspot);
  });

};
positionHotSpots();
html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
}
#image {
  height: 100%;
  width: 100%;
  background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Alexanderplatz_Stadtmodell_1.jpg/1920px-Alexanderplatz_Stadtmodell_1.jpg');
  background-size: 100% 100%;
}
.hotspot {
  position: absolute;
  z-index: 1;
  background: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='image'></div>

Canvas solution

Based on comment by @JayMee , create a canvas with same dimensions as actual image and draw hotspots as rectangles on the canvas.

One advantage in this approach is we don't have to recalculate the hotspot coordinates on resizing window as the hotspot are drawn in image itself.

 var imageWidth = 1920;
 var imageHeight = 1368;
 var hotspots = [{
   x: 100,
   y: 200,
   height: 100,
   width: 50
 }, {
   x: 300,
   y: 500,
   height: 200,
   width: 100
 }, {
   x: 600,
   y: 600,
   height: 150,
   width: 100
 }, {
   x: 900,
   y: 550,
   height: 100,
   width: 25
 }];

 var positionHotSpots = function() {


   var canvas = document.createElement('canvas');
   canvas.height = imageHeight;
   canvas.width = imageWidth;
   var context = canvas.getContext('2d');
   var imageObj = new Image();
   imageObj.onload = function() {

     context.drawImage(imageObj, 0, 0);

     $.each(hotspots, function(i, h) {
       context.rect(h.x, h.y, h.width, h.height);
     });
     context.fillStyle = "red";
     context.fill();
     $('#image').css('background-image', 'url("' + canvas.toDataURL() + '")');
   };
   imageObj.setAttribute('crossOrigin', 'anonymous');
   imageObj.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Alexanderplatz_Stadtmodell_1.jpg/1920px-Alexanderplatz_Stadtmodell_1.jpg';

 };
 positionHotSpots();
html,
body {
  height: 100%;
  padding: 0;
  margin: 0;
}
#image {
  height: 100%;
  width: 100%;
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
}
<!DOCTYPE html>
<html>

<head>
  <script src="https://code.jquery.com/jquery-2.1.4.js"></script>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>

<body>
  <div id='image'></div>
</body>

</html>
Pang
  • 9,564
  • 146
  • 81
  • 122
Pavan Teja
  • 3,192
  • 1
  • 15
  • 22
  • How/Where do I set a custom x/y position for both your answers? – Asons Mar 11 '16 at 20:57
  • at the beginning of the script next to image dimensions.`var h = { x: CUSTOMX, y: CUSTOMY, height: 100, width: 50};` – Pavan Teja Mar 11 '16 at 21:02
  • I would be happy if you could merge the 2 answers and read out from the `background-size` value whether it has `cover` or `100%`. Also a version that would target `background-size: contain;` would be interesting. I will award a [bounty](http://stackoverflow.com/help/bounty) as soon as it is possible to start one, for an answer doing exactly that. – Asons Mar 12 '16 at 20:12
  • Also if the `var h = { x: imageWidth / 2, y: imageHeight / 2, height: 100, width: 50 };` could be an array of hotspots would be very interesting. – Asons Mar 12 '16 at 20:15
  • @LGSon merged the answers,implemented for `background-size:contains` and array of hotspots – Pavan Teja Mar 13 '16 at 07:00
9

Okay, so not a lot of people know about the CSS units vh and vw (meaning ViewportHeight and ViewportWidth). I've created a script that runs one time at pageload (unlike some other answers that run at every resize).

It calculates the ratio of the background-image, adds two CSS rules to overlayContainer, and it's done.

There's also a div #square in there, the purpose of which is that we have a container with a ratio of 1:1 as a canvas. This ratio ensures that when you're making the overlaying elements, vertical and horizontal percentual distances are the same.

For background-size: cover, see this Fiddle.

For background-size: contain, see this Fiddle.

The HTML:

<div id="overlayContainer">
  <div id="square">
    <!-- Overlaying elements here -->
  </div>
</div>

The CSS:

#overlayContainer{
  position: absolute; /* Fixed if the background-image is also fixed */
  min-width:  100vw; /* When cover is applied */
  min-height: 100vh; /* When cover is applied */
  max-width:  100vw; /* When contain is applied */
  max-height: 100vh; /* When contain is applied */
  top:  50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

#square{
  position: relative;
  padding-bottom: 100%;
}

/* When placing overlaying elements, make them all absolutely positioned, and work with percentages only */
/* Look at my Fiddles for examples */

The JavaScript (jQuery):

var image = new Image()
image.src = $('body').css('background-image').replace(/url\((['"])?(.*?)\1\)/gi,'$2').split(',')[0]

/* When cover is applied, use this: */
$('#overlayContainer').css({'height':100/(image.width/image.height)+'vw','width':100/(image.height/image.width)+'vh'})

/* When contain is applied, use this: */
$('#overlayContainer').css({'height':100*(image.height/image.width)+'vw','width':100*(image.width/image.height)+'vh'})

Hope this helps


Update by @LGSon

I didn't expect to find a CSS only solution, though here it is, hiding itself in this answer, and therefore I decided to add it into the same.

By adding these 2 lines to the #overlayContainer rule (works for both cover and contain), the script can be dropped.

width:  calc(100vh * (1920 / 1368));
height: calc(100vw * (1368 / 1920));

Of course the script version has the advantage of automatically get the values, though since the hotspot(s) has a specific location point in the background, the image size will most likely be known.

Sample with background-size: cover

html, body {
  height: 100%;
  overflow: hidden;
}

body {
  margin: 0;
  background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Alexanderplatz_Stadtmodell_1.jpg/1920px-Alexanderplatz_Stadtmodell_1.jpg');
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
}

#overlayContainer {
  position: absolute;
  width:  calc(100vh * (1920 / 1368));
  height: calc(100vw * (1368 / 1920));
  min-width:  100vw;     /*  for cover    */
  min-height: 100vh;     /*  for cover    */
  /* max-width:  100vw;      for contain  */
  /* max-height: 100vh;      for contain  */
  top:  50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

#square {
  position: relative;
  padding-bottom: 100%;
}

#square div {
  position: absolute;
  top: 19.75%;
  left: 49.75%;
  width: 4.75%;
  height: 4.75%;
  background-color: rgba(255,0,0,.7);
  border-radius: 50%;
}
<div id="overlayContainer">
  <div id="square">
    <div></div>
  </div>
</div>
Gust van de Wal
  • 5,211
  • 1
  • 24
  • 48
  • I'm impressed, looks really good so far, will play with it later, thanks, +1. Could you also add a solution for `background-size: contain`? – Asons Mar 19 '16 at 07:51
  • Please also explain the purpose of the extra `square` div (I tested without and saw what id does, but it would be good to have it in the answer) – Asons Mar 19 '16 at 10:14
  • I've added an option for `background-size: contain` to the answer, and now explain the purpose of `#square` – Gust van de Wal Mar 20 '16 at 08:18
  • Thank you very much ... and you don't need 2 different calc script, they return the same result, switching between max/min width does the trick. – Asons Mar 20 '16 at 09:46
  • Hope it was okay, me updating your answer – Asons Mar 20 '16 at 11:02
  • The edit is fine by me. Good thing you noticed that there is no difference in the way you calculate the values! – Gust van de Wal Mar 20 '16 at 11:45
  • @Gust nice job, let me know when you looking for work )) – tnt-rox Mar 20 '16 at 11:58
  • @tnt-rox If he is to busy, you can pass work forward to me as I have some spare time now :) – Asons Mar 20 '16 at 18:27
  • @tnt-rox I'm very busy with school and my jobs, so you can indeed pass work forward to LGSon. Please use PM of you wish to contact each other from now on :) – Gust van de Wal Mar 20 '16 at 21:47
  • @LGSon: glad to see bounty seeding has been working out for you, and nice one cracking the css-only approach! For some silly reason I was stuck on trying to use image dimensions from data attributes... which is only part of a draft spec afaik :\ – Oleg Mar 22 '16 at 09:47
  • @o.v. For the CSS only, _Gust_ did the hard part, I only needed to convert script to CSS calc ... About bounty, thanks, and I really wanted to give you one here instead of at the SPA, as yours is a really good one too, though had some inner issues as the bounty doubles for every new :o ... but learned that know and will start at 150 next time :) – Asons Mar 22 '16 at 10:18
  • 2
    Awesome pure CSS solution! Thanks so much! Great stuff! – Garavani Sep 17 '18 at 15:22
4

Ok, so I tried to use your original idea, and modified only a few bits here and there.

Instead of using percentages, I found it easier to use pixel values. So:

$(this).css({
  'margin-top': yPos + 'px',
  'margin-left': xPos + 'px',
  'width': xSize + 'px',
  'height': ySize + 'px'
});

Then, all we have to do is check the proportion of the viewport to see how we have to modify the div's properties

if (windowAspectRatio > imageAspectRatio) {
  var ratio = windowWidth / imageWidth;
} else {
  var ratio = windowHeight / imageHeight;
}

xPos = xPos * ratio;
yPos = yPos * ratio;
xSize = xSize * ratio;
ySize = ySize * ratio;

Working example: http://codepen.io/jaimerodas/pen/RaGQVm

Stack snippet

var imageWidth = 1920,
    imageHeight = 1368,
    imageAspectRatio = imageWidth / imageHeight,
    $window = $(window);

var hotSpots = [{
  x: -210,
  y: -150,
  height: 250,
  width: 120
}, {
  x: 240,
  y: 75,
  height: 85,
  width: 175
}];

function appendHotSpots() {
  for (var i = 0; i < hotSpots.length; i++) {
    var $hotSpot = $('<div>').addClass('hot-spot');
    $('.container').append($hotSpot);
  }
  positionHotSpots();
}



function positionHotSpots() {
  var windowWidth = $window.width(),
    windowHeight = $window.height(),
    windowAspectRatio = windowWidth / windowHeight,
    $hotSpot = $('.hot-spot');

  $hotSpot.each(function(index) {
    var cambio = 1,
        xPos = hotSpots[index]['x'],
        yPos = hotSpots[index]['y'],
        xSize = hotSpots[index]['width'],
        ySize = hotSpots[index]['height'],
        desiredLeft = 0,
        desiredTop = 0;
    
    if (windowAspectRatio > imageAspectRatio) {
      var ratio = windowWidth / imageWidth;
    } else {
      var ratio = windowHeight / imageHeight;
    }
    
    xPos = xPos * ratio;
    yPos = yPos * ratio;
    xSize = xSize * ratio;
    ySize = ySize * ratio;

    $(this).css({
      'margin-top': yPos + 'px',
      'margin-left': xPos + 'px',
      'width': xSize + 'px',
      'height': ySize + 'px'
    });

  });
}

appendHotSpots();
$(window).resize(positionHotSpots);
html, body {
  margin: 0;
  width: 100%;
  height: 100%;
}

.container {
  width: 100%;
  height: 100%;
  position: relative;
  background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Alexanderplatz_Stadtmodell_1.jpg/1920px-Alexanderplatz_Stadtmodell_1.jpg);
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
}

.hot-spot {
  background-color: red;
  border-radius: 0;
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  opacity: 0.8;
  content: "";
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="container"></div>
Asons
  • 84,923
  • 12
  • 110
  • 165
user372495
  • 700
  • 4
  • 11
4

Relying on css transforms and applying it to a single element gives you much better performance regardless of the number of hotspots (fewer DOM manipulations and much fewer re-flows). Hardware acceleration is also a nice-to-have :)

First, meta-code:

  1. Create a .hot-spot--container inside your image .container

  2. Create .hot-spot and position/size them within the .hot-spot--container

  3. Transform .hot-spot--container mimicking background-size: cover behaviour

  4. Repeat #3 whenever there's a re-size

Calculate your bg image ratio:

var bgHeight = 1368;
var bgWidth = 1920;
var bgRatio = bgHeight / bgWidth;

Whenever the window is re-sized, re-calculate container ratio:

var containerHeight = $container.height();
var containerWidth = $container.width();
var containerRatio = containerHeight / containerWidth;

Calculate scale factors to mimic background-size: cover behaviour...

if (containerRatio > bgRatio) {
    //fgHeight = containerHeight
    //fgWidth = containerHeight / bgRatio
    xScale = (containerHeight / bgRatio) / containerWidth
} else {
    //fgHeight = containerWidth / bgRatio
    //fgWidth = containerWidth 
    yScale = (containerWidth * bgRatio) / containerHeight
}

...and apply the transform to the hot spot container element, essentially re-sizing and re-positioning it "in sync" with the background:

var transform = 'scale(' + xScale + ', ' + yScale + ')';

$hotSpotContainer.css({
    'transform': transform
});

Fiddled: https://jsfiddle.net/ovfiddle/a3pdLodm/ (you can play with the preview window pretty effectively. Note the code can be adjusted to take pixel-based dimensions and positioning for hot spots, you'll just have to consider container and image sizes when calculating scale values)

Update: the background-size: contain behaviour uses the same calculation except when the containerRatio is smaller than the bgRatio. Updating the background css and flipping the sign around is enough.

Oleg
  • 24,465
  • 8
  • 61
  • 91
  • 1
    So simple, yet efficient. You just qualified yourself for a second bounty. If you could help me with the pixel-based version and one for `background-size: contain` (with percent/pixel), you got it. – Asons Mar 14 '16 at 09:01
  • Please let me know if you have the possibilities or not to help me with the above request. .. and +1 from me, I played a little with it before and it appears rock solid with no lagging :) – Asons Mar 14 '16 at 15:57
  • @LGSon: thanks for the comment :) I'll update the answer with the `contain` version – Oleg Mar 15 '16 at 09:02
  • @o.v. Nice examples, but why you use `content:"";` and `z-index:1;` on `.hot-spot` it's a bit odd...?? – Roko C. Buljan Mar 15 '16 at 10:13
  • @RokoC.Buljan: you're right, that was lazily copied from the original codepen :) – Oleg Mar 15 '16 at 20:33
  • Started one more bounty, will wait a few days though before assigning it, to see if someone might even comes up with a CSS only solution :) – Asons Mar 18 '16 at 09:33
3

Below is a jQuery solution,the bgCoverTool plugin repositions an element based on the scale of the parent's background image.

//bgCoverTool Properties
$('.hot-spot').bgCoverTool({
  parent: $('#container'),
  top: '100px',
  left: '100px',
  height: '100px',
  width: '100px'})

Demo:

$(function() {
  $('.hot-spot').bgCoverTool();
});
html,
body {
  height: 100%;
  padding: 0;
  margin: 0;
}
#container {
  height: 100%;
  width: 100%;
  background: url('https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Alexanderplatz_Stadtmodell_1.jpg/1920px-Alexanderplatz_Stadtmodell_1.jpg');
  background-size: cover;
  background-repeat: no-repeat;
  position: relative;
}
.hot-spot {
  position: absolute;
  z-index: 1;
  background: red;
  left: 980px;
  top: 400px;
  height: 40px;
  width: 40px;
  opacity: 0.7;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>BG Cover Tool</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  <script type="text/javascript" charset="utf-8">
    //bgCoverTool jQuery plugin
    (function($) {
      $.bgCoverTool = function(element, options) {
        var $element = $(element),
          imgsize = {};
        var defaults = {
          parent: $element.parent(),
          top: $element.css('top'),
          left: $element.css('left'),
          height: $element.css('height'),
          width: $element.css('width')
        };
        var plugin = this;
        plugin.settings = {};
        plugin.init = function() {
          plugin.settings = $.extend({}, defaults, options);
          var tempurl = plugin.settings.parent.css('background-image').slice(4, -1)
          .replace('"', '').replace('"', '');
          var tempimg = new Image();
          var console = console || {
            error: function() {}
          };
          if (plugin.settings.parent.css('background-size') != "cover") {
            return false;
          }
          if (typeof tempurl !== "string") {
            return false;
          }
          if (plugin.settings.top == "auto" || plugin.settings.left == "auto") {
            console.error("#" + $element.attr('id') + " needs CSS values for 'top' and 'left'");
            return false;
          }
          $(tempimg).on('load', function() {
            imgsize.width = this.width;
            imgsize.height = this.height;
            imageSizeDetected(imgsize.width, imgsize.height);
          });
          $(window).on('resize', function() {
            if ('width' in imgsize && imgsize.width != 0) {
              imageSizeDetected(imgsize.width, imgsize.height);
            }
          });
          tempimg.src = tempurl;
        };
        var imageSizeDetected = function(w, h) {
          var scale_h = plugin.settings.parent.width() / w,
            scale_v = plugin.settings.parent.height() / h,
            scale = scale_h > scale_v ? scale_h : scale_v;
          $element.css({
            top: parseInt(plugin.settings.top, 10) * scale,
            left: parseInt(plugin.settings.left, 10) * scale,
            height: parseInt(plugin.settings.height, 10) * scale,
            width: parseInt(plugin.settings.width, 10) * scale
          });

        };
        plugin.init();
      };
      /**
       * @param {options} object Three optional properties are parent, top and left.
       */
      $.fn.bgCoverTool = function(options) {
        return this.each(function() {
          if (undefined == $(this).data('bgCoverTool')) {
            var plugin = new $.bgCoverTool(this, options);
            $(this).data('bgCoverTool', plugin);
          }
        });
      }
    })(jQuery);
  </script>
</head>

<body>
  <div id="container">
    <div class="hot-spot"></div>
  </div>
</body>

</html>
TargunTech
  • 1,132
  • 11
  • 19
3

A far simpler/better approach to you problem is to use an SVG element, it is better suited to your requirement. The cool thing about SVG is everything will scale proportionally by default because it is a vector object not a document flow object.

This example will demonstrate the technique

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>SVG</title>
        <style type="text/css" media="screen">
            body {
                background: #eee;
                margin: 0;
            }
            svg {
                display: block;
                border: 1px solid #ccc;
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: #fff;
            }
            .face {
                stroke: #000;
                stroke-width: 20px;
                stroke-linecap: round
            }
        </style>
    </head>
    <body>
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="-350 -250 700 500">
            <circle r="200" class="face" fill="red"/>
            <path fill="none" class="face" transform="translate(-396,-230)" d="M487.41,282.411c-15.07,36.137-50.735,61.537-92.333,61.537 c-41.421,0-76.961-25.185-92.142-61.076"/>
            <circle id="leftEye" cx="-60" cy="-50" r="20" fill="#00F"/>
            <circle id="rightEye" cx="60" cy="-50" r="20" fill="#00F"/>
        </svg>
        <script type="text/javascript">
            document.getElementById('leftEye').addEventListener('mouseover', function (e) {
                alert('Left Eye');
            });
            document.getElementById('rightEye').addEventListener('mouseover', function (e) {
                alert('Right Eye');
            });
        </script>
    </body>
</html>

You can add images to SVG to achieve what you need.

https://jsfiddle.net/tnt1/3f23amue/

tnt-rox
  • 5,400
  • 2
  • 38
  • 52