Adjust size of element in relation to Background Cover using jQuery

Can you help me solve a challenge I'm facing? I have a website with a fullsize background image, and I need to attach a div to a specific position on the image while ensuring that it scales in the same way as the background image with the "background-size: cover" property.

I've been able to position the div correctly, but resizing it has proven difficult. Here's what I've tried 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);

I thought about calculating a scale value based on the ratios of imageWidth/windowWidth and imageHeight/windowHeight and then using this value for scaling, but I couldn't get it to work...

I would appreciate any help or guidance on this matter.

Answer №1

Tackling background-size:cover Issue

In my attempt to address your concern, I have come up with a solution (or you can consider it as an idea). Feel free to explore the working demo here. Make sure to resize the window to observe the outcome.

Initially, I was puzzled by the use of `transform`, `top:50%`, and `left:50%` for hotspot. Hence, I simplified the approach by adjusting the markup and CSS for better understanding.

In this scenario, `rImage` represents 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;

Within the window resize handler, the aspect ratio of the viewport (`r`) is calculated. Subsequently, we need to determine the image dimensions when resizing the window while keeping the aspect ratio in check. For such calculations using `background-size:cover`, the following formulas are used:

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

Also,

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

For more insights on image dimension calculations concerning `background-size:cover`, refer to this URL.

Upon obtaining the image dimensions, we proceed to map the hot-spot coordinates from the old image to the new image dimensions.

When fitting the image into the viewport, it may be clipped either vertically or horizontally. Therefore, these clipped areas should be considered as offsets when plotting hotspots.

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

Add these offset values to each hotspot's `x,y` coordinates.

-- Insert remaining content here --

Answer №2

Many individuals may not be aware of the CSS units vh and vw, which stand for ViewportHeight and ViewportWidth respectively. I've devised a script that executes once upon pageload (as opposed to other solutions that run with every resize).

This script computes the background-image ratio, applies two CSS rules to overlayContainer, and finishes its task.

Additionally, there is a div labeled #square, included to create a canvas container with a 1:1 aspect ratio. This ensures consistency in vertical and horizontal percentage distances when overlaying elements.

If you are using background-size: cover, refer to this Fiddle.

For background-size: contain, see this Fiddle.

The HTML structure:

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

The required CSS styling:

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

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

/* When positioning overlayed elements, ensure they are all absolutely positioned using percentages only */
/* Refer to provided fiddles for detailed examples */

The JavaScript implementation (using jQuery):

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

/* For cover usage: */
$('#overlayContainer').css({'height': 100/(image.width/image.height)+'vw', 'width': 100/(image.height/image.width)+'vh'})

/* For contain usage: */
$('#overlayContainer').css({'height': 100*(image.height/image.width)+'vw', 'width': 100*(image.width/image.height)+'vh'})

I trust this information proves beneficial.


Update by @LGSon

It came as a surprise to discover a CSS-centric solution lurking within this answer itself, prompting me to include it herein.

Incorporating these 2 lines into the #overlayContainer rule (suitable for both cover and

contain</code) eliminates the need for the script.</p>

<pre><code>width:  calc(100vh * (1920 / 1368));
height: calc(100vw * (1368 / 1920));

Although the script version automatically fetches values, since hotspots typically have a specific location within the background, the image dimensions are usually known.

Example involving 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;     /*  applicable for cover  */
  min-height: 100vh;     /*  applicable for cover  */
  /* max-width:  100vw;      applicable for contain  */
  /* max-height: 100vh;      applicable 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>

Answer №3

Hey there! I decided to take your original concept and make a few tweaks to enhance it.

Instead of using percentages, I opted for pixel values as they seemed more straightforward. Here's the updated code snippet:

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

Now, all we need to do is analyze the viewport proportions to adjust the properties of the div:

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

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

Check out this live demo for a practical example: http://codepen.io/jaimerodas/pen/RaGQVm

Here's a Stack snippet that illustrates the implementation further:

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();
}

// More functions and logic go here

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>

Answer №4

Utilizing css transforms on a single element can significantly enhance performance, regardless of the number of hotspots present. This approach minimizes DOM manipulations and reduces reflows. Additionally, leveraging hardware acceleration is an added bonus.

Here is a high-level overview of the process:

  1. Set up a .hot-spot--container within your image .container

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

  3. Apply transforms to the .hot-spot--container to mimic the behavior of background-size: cover

  4. Periodically readjust based on window resizing

Determine the ratio of your background image:

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

Calculate container ratio during window resize:

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

Compute scaling factors to replicate background-size: cover effect...

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

...and apply the transform to the hot spot container element to adjust its size and position accordingly:

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

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

For hands-on experimentation, refer to this fiddle: https://jsfiddle.net/ovfiddle/a3pdLodm/. Feel free to modify the code to accommodate pixel-based dimensions and positioning for hot spots by accounting for container and image sizes in scale calculations.

Update: To achieve a background-size: contain effect, follow similar calculations with adjustments when the containerRatio is smaller than the bgRatio. See this updated fiddle for details: Updating the background css and reversing the sign will suffice.

Answer №5

Here is a simple jQuery solution using the bgCoverTool plugin to reposition an element based on the scale of its parent's background image.

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

You can see a demo below:

$(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>

Answer №6

An easier and more effective solution for your issue is to incorporate an SVG element, which aligns better with your needs. The great thing about SVG is that everything scales proportionally by default because it is a vector object rather than a document flow object.

Below is a demonstration of this technique

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>SVG Element</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 embed images within the SVG to accomplish your requirements.

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to ensure that form inputs and labels stay aligned?

Let's consider a scenario where there is a web page containing a form: <form> <label for="FirstName">First:</label> <input name="FirstName" type="text"> <label for="MiddleName">Middle:</label> <input n ...

Failed installation of Semantic-ui through npm encountered

Encountering an error while attempting to install semantic-ui through npm for a new project. Here are the version details: $ node -v v16.14.0 $ npm -v 8.10.0 $ npm i semantic-ui npm WARN deprecated <a href="/cdn-cgi/l/email-protection" class="__cf_em ...

What is the best way to position a div to float or hover from the bottom after it has been

I am working on creating a menu where clicking it will reveal a submenu at the bottom, but I'm encountering an issue. Currently, in my code, the submenu is appearing from right to left before moving down. Here is my code: <meta name="viewport" co ...

What is the best way to place an icon in the lower right corner of a design

I have encountered an issue while building the testimonial section below. The problem arises when the amount of text in each block is not the same, causing the quote icon to be displayed improperly in the bottom right corner. Sometimes, it even extends out ...

Ways to verify if the flash plugin has been disabled in the Chrome browser

Is there a way to use JavaScript or jQuery to detect if the flash plugin is blocked in Google Chrome? You can check for a disabled flash plugin using the following code: ((typeof navigator.plugins != "undefined" && typeof navigator.plugins["Shock ...

Having trouble vertically aligning text in Bootstrap 4?

I'm having trouble getting the text to align vertically in the middle and closer to the image. The vertical-align:middle property doesn't seem to be working for me. Additionally, when I resize the screen width, the layout changes from a horizonta ...

Failure to execute Ajax request when searching for a query

My personal GitHub profile viewer React application allows you to search for a profile by username. By default, my own GitHub username is provided on the landing page. However, when trying to search for another user, my ajax call is not functioning properl ...

Enhancing webpage design by dynamically changing borders and headers using JavaScript

I have implemented a fixed positioning for the table headers using the following code: onScroll={() => { document.querySelector('thead').style.transform = `translate(0,${this.scrollRef.scrollTop}px)`; }} However, when I scroll the ta ...

What is the process for utilizing a custom plugin within the <script setup> section of Vue 3?

//CustomPlugin.js const generateRandomValue = (min, max) => { min = Math.ceil(min); max = Math.floor(max); const random = Math.floor(Math.random() * (max - min + 1)) + min; console.log(random); }; export default { install(Vue) { Vue.conf ...

What does dist entail?

I am currently utilizing gulp to create a distribution folder (dist) for my Angular application. After consolidating all the controllers/services JS files and CSS, I am now faced with handling the contents of the bower folder. In an attempt to concatenat ...

Build a React application using ES6 syntax to make local API requests

I'm really struggling to solve this problem, it seems like it should be simple but I just can't figure it out. My ES6 app created with create-react-app is set up with all the templates and layouts, but when trying to fetch data from an API to in ...

Parsing form array/bracket fields into actual arrays with ExpressJS

Is there any middleware available or code snippets that can help me to convert form fields with square brackets (e.g. 'contact[21][name]') into actual arrays for ExpressJS 3? I am looking for something like this: for(key in req.body){ if(ke ...

I'm using SASS in my project, can you provide guidance on customizing the UI with @aws-amplify/ui

I've recently taken over a Next.js (React) project from another developer and they've used .scss files (SASS) for styling. The specific project I'm working on can be found here https://github.com/codebushi/nextjs-starter-dimension My task n ...

Guide to adding new data to a JSON array

I'm currently working on implementing a punishment system using discord.js where the actions taken against users are logged by the Discord bot in a JSON file. The structure of the punishment data is as follows: { "username": "baduser# ...

One of the parameters is converging faster than the other with Gradient Descent

My introduction to univariate linear regression using gradient descent was a hands-on experience in JavaScript. const LEARNING_RATE = 0.000001; let m = 0; let b = 0; const hypothesis = x => m * x + b; const learn = (alpha) => { if (x.length < ...

The functionality of TypeScript's instanceof operator may fail when the method argument is not a simple object

There seems to be an issue with a method that is being called from two different places but returns false for both argument types. Despite checking for the correct types, the problem persists and I am unsure why. Although I have read a similar question on ...

Filtering and displaying a nested array with underscore, jquery, and handlebars: A guide

I am facing the challenge of filtering specific data from a nested structure: data = { grouplist: [ {name: "one", optionlist: [{optionitem:"green"},{optionitem:"red"}]}, {name: "two", optionlist: [{optionitem:"yellow"},{opt ...

The functionality of the document download button using Express.js and node.js appears to be malfunctioning

For my current project, I am aiming to enable users to effortlessly download a document by simply clicking on a designated button. Project Outline: public/client.js console.log('Client-side code running'); const button = document.get ...

Having difficulty accessing the ::after element on Firefox when attempting to click

I've encountered an issue with my HTML and CSS code - it functions perfectly on Chrome, yet behaves differently on Firefox. button#groupToggle { background: 0 0; border: 1px solid #e6e6ed; color: #222; float: none; margin: 0 0 ...

A combination of Webpack CSS modules with Angular templates

I am currently trying to integrate webpack CSS modules into my angular/ionic project. I'm wondering if it's possible for the CSS module class definitions to be correctly appended to an external template instead of an inline template. When I embe ...