skip to Main Content

I’m trying to build a magnifying glass effect. This is where, when you move the cursor over an image, a magnified (ie larger) part of the image underneath the cursor is shown. I’ve gotten close with my coding. An example of what I have is this:

    let zoom = 2;

    window.addEventListener('load', () => {
      const image = document.querySelector('.image'),
        { width, height } = image.getBoundingClientRect(),
        glass = document.createElement('div');

      document.querySelector('.image-wrapper').append(glass);
      glass.classList.add('glass');
      glass.style.backgroundSize = `${zoom * width}px ${zoom * height}px`;
      glass.style.backgroundImage = `url(${image.src})`;

      image.addEventListener('mousemove', (e) => {
        const { offsetX, offsetY } = e,
          glassX = offsetX - glass.offsetWidth / 2,
          glassY = offsetY - glass.offsetHeight / 2;

        glass.style.left = `${glassX}px`;
        glass.style.top = `${glassY}px`;

        glass.style.backgroundPosition = `-${offsetX * zoom}px -${offsetY * zoom}px`;
      });
    });
   body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-color: #f0f0f0;
    }

    .image-wrapper {
      position: relative;
      box-sizing: border-box;
      border: 1px dashed gray;
    }

    .image {
      width: 300px;
      height: auto;
      cursor: none;
    }

    .image-wrapper .glass {
      opacity: 0;
    }

    .image-wrapper:hover .glass {
      opacity: 1;
    }

    .glass {
      position: absolute;
      width: 100px;
      height: 100px;
      border-radius: 50%;
      cursor: none;
      background-repeat: no-repeat;
      pointer-events: none;
      transition: opacity 0.2s;
      border: 1px solid black;
    }
  <div class="image-wrapper">
    <img src="https://i.sstatic.net/JzS3fR2C.jpg" alt="Sample Image" style="opacity: 0.1;" class="image">
  </div>

The source image is this
Fish

While the magnifying glass mostly works it "fails" around the edges. When the cursor gets to the edge of the image I want the glass to show a "semi circle" of the image with the other semi being white. That is I want this:
good

but I get this
bad

How to fix this so that it behaves itself around all 4 edges?

4

Answers


  1. Chosen as BEST ANSWER

    After some gnashing of teeth I got it to work by changing the mousemove event's code to:

        const { offsetX, offsetY } = e,
              gw = glass.offsetWidth / 2,
              gh = glass.offsetHeight / 2;
    
        glass.style.left = `${offsetX - gw}px`;
        glass.style.top  = `${offsetY - gh}px`;
    
        const x = -Math.floor((offsetX / width)  * (width * zoom)  - gw),
              y = -Math.floor((offsetY / height) * (height * zoom) - gh);
        
        glass.style.backgroundPosition = `${x}px ${y}px`;
    

  2. The issue you’re seeing is that the background of glass isn’t aligned correctly.

    The glass div is positioned from its top-left corner. Your code corrects for this by subtracting half of glass‘s width and height when calculating its new position:

    glassX = offsetX - glass.offsetWidth / 2,
    glassY = offsetY - glass.offsetHeight / 2;
    

    However, the background-position is also positioned relative to the top-left corner of glass. If the top-left corner were at the same position as the cursor, this would work great. But remember that you corrected glass‘s position above so the cursor would be at its center. Therefore, you’ll need to move the background-position "back" to where it would be.

    So when you’re calculating background-positon:

    glass.style.backgroundPosition = `-${offsetX * zoom}px -${offsetY * zoom}px`;
    

    you need to add glass.offsetWidth / 2 and glass.offsetHeight / 2 back in:

    glass.style.backgroundPosition = `-${offsetX * zoom - glass.offsetWidth / 2}px -${offsetY * zoom - glass.offsetHeight / 2}px`;
    

    But wait! The expressions you’re using in your template string could produce negative numbers when near the top or left edges of the image! Since you included a - in the template string, this would generate something like --13.2px…, which isn’t valid CSS.

    This is a simple fix, however. Just move the negation inside the expression, like so:

    glass.style.backgroundPosition = `${-offsetX * zoom + glass.offsetWidth / 2}px ${-offsetY * zoom + glass.offsetHeight / 2}px`;
    

    Now we won’t generate invalid CSS.

    Working snippet:

        let zoom = 2;
    
        window.addEventListener('load', () => {
          const image = document.querySelector('.image'),
            { width, height } = image.getBoundingClientRect(),
            glass = document.createElement('div');
    
          document.querySelector('.image-wrapper').append(glass);
          glass.classList.add('glass');
          glass.style.backgroundSize = `${zoom * width}px ${zoom * height}px`;
          glass.style.backgroundImage = `url(${image.src})`;
    
          image.addEventListener('mousemove', (e) => {
            const { offsetX, offsetY } = e,
              glassX = offsetX - glass.offsetWidth / 2,
              glassY = offsetY - glass.offsetHeight / 2;
    
            glass.style.left = `${glassX}px`;
            glass.style.top = `${glassY}px`;
    
            glass.style.backgroundPosition = `${-offsetX * zoom + glass.offsetWidth / 2}px ${-offsetY * zoom + glass.offsetHeight / 2}px`;
          });
        });
       body {
          display: flex;
          justify-content: center;
          align-items: center;
          height: 100vh;
          background-color: #f0f0f0;
        }
    
        .image-wrapper {
          position: relative;
          box-sizing: border-box;
          border: 1px dashed gray;
        }
    
        .image {
          width: 300px;
          height: auto;
          cursor: none;
        }
    
        .image-wrapper .glass {
          opacity: 0;
        }
    
        .image-wrapper:hover .glass {
          opacity: 1;
        }
    
        .glass {
          position: absolute;
          width: 100px;
          height: 100px;
          border-radius: 50%;
          cursor: none;
          background-repeat: no-repeat;
          pointer-events: none;
          transition: opacity 0.2s;
          border: 1px solid black;
        }
      <div class="image-wrapper">
        <img src="https://i.sstatic.net/JzS3fR2C.jpg" alt="Sample Image" style="opacity: 0.1;" class="image">
      </div>
    Login or Signup to reply.
  3. Page layout note: most of the calculations that you’re currently doing in JS in order to calculate layout positioning and sizing can be done purely in CSS using variables and calc() expressions, requiring drastically less JS while also running faster.

    Working in your own solution, but implement in CSS rather than JS:

    const zoom = 2;
    const image = document.querySelector('.image');
    image.onload = function addSpyGlass() {
      const { width, height } = image.getBoundingClientRect()
    
      // Let's do as _little_ as possible here: get the values,
      // hand them over to CSS, and do literally nothing else:
    
      const glass = document.createElement('div');
      glass.classList.add('glass');
    
      const { style } = glass;
      style.setProperty(`--w`, width);
      style.setProperty(`--h`, height);
      style.setProperty(`--zoom`, zoom);
      style.setProperty(`--src`, `url(${image.src}`);
    
      image.addEventListener('mousemove', ({ offsetX, offsetY}) => {
        style.setProperty(`--x`, offsetX);
        style.setProperty(`--y`, offsetY);
        // That's it. That's all we do.
      });
    
      image.parentNode.append(glass);
    }
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-color: #f0f0f0;
    
      .image-wrapper {
        position: relative;
        box-sizing: border-box;
        border: 1px dashed gray;
    
        .image {
          width: 300px;
          height: auto;
          cursor: none;
          opacity: 0.1;
        }
    
        &:hover .glass {
          opacity: 1;
        }
    
        /*
          let's do some math in CSS instead.
          I prefer to keep my CSS variables
          unit-less so I can quickly rewrite
          things if I need to, but you can 
          bake in the unit if you don't want
          those "... * 1px" everywhere, of course.
        */
    
        .glass {
          opacity: 0;
          pointer-events: none;
          transition: opacity 0.2s;
          border: 1px solid black;
          border-radius: 50%;
          cursor: none;
          background-repeat: no-repeat;
    
          --d: 100;
          width: calc(var(--d) * 1px);
          height: calc(var(--d) * 1px);
    
          --x: 0;
          --y: 0;
          position: absolute;
          top: calc((var(--y) - var(--d) / 2) * 1px);
          left: calc((var(--x) - var(--d) / 2) * 1px);
    
          --src: "";
          background-image: var(--src);
    
          --w: 0;
          --h: 0;
          --zoom: 2;
          background-size:
            calc(var(--zoom) * var(--w) * 1px)
            calc(var(--zoom) * var(--h) * 1px);
    
          --bg-x: calc(0 - (var(--x) * var(--zoom) - var(--d) / 2));
          --bg-y: calc(0 - (var(--y) * var(--zoom) - var(--d) / 2));
          background-position:
            calc(1px * var(--bg-x))
            calc(1px * var(--bg-y));
        }
      }
    }
    <div class="image-wrapper">
      <img src="https://i.sstatic.net/JzS3fR2C.jpg" alt="Sample Image" class="image">
    </div>
    Login or Signup to reply.
  4. Based on your approach, I made two tweaks to fix the issue:

    1. Define glassSize property so you could use it in both CSS and JavaScript calculations:
    // glass size, in px
    const glassSize = 100;
    
    // assign the CSS variable to .image-wrapper
    document.querySelector('.image-wrapper').style.setProperty('--glass-size', `${glassSize}px`)
    
    .glass {
      /* use variable instead of hardcoded 100px */
      width: var(--glass-size);
      height: var(--glass-size);
    }
    
    1. Add the correct offset to the background position of .glass.
    glass.style.backgroundPosition = `${-(offsetX * zoom - glassSize / 2)}px ${-(offsetY * zoom - glassSize / 2)}px`;
    

    You want the offset of the background position to be offset by half of the glassSize, so the edges of the image can be offset correctly.

    Notice it’s better to put the negative sign inside of calculation: ${-(x)} instead of -${x}, so when x became a negative value (e.g. -42), the -${x} won’t become something like --42 which is not going be interpreted correctly.

    let zoom = 2;
    const glassSize = 100;
    
    window.addEventListener('load', () => {
      const image = document.querySelector('.image'),
        {
          width,
          height
        } = image.getBoundingClientRect(),
        glass = document.createElement('div');
    
      document.querySelector('.image-wrapper').append(glass);
      document.querySelector('.image-wrapper').style.setProperty('--glass-size', `${glassSize}px`)
      glass.classList.add('glass');
      glass.style.backgroundSize = `${zoom * width}px ${zoom * height}px`;
      glass.style.backgroundImage = `url(${image.src})`;
    
      image.addEventListener('mousemove', (e) => {
        const {
          offsetX,
          offsetY
        } = e,
        glassX = offsetX - glass.offsetWidth / 2,
          glassY = offsetY - glass.offsetHeight / 2;
    
        glass.style.left = `${glassX}px`;
        glass.style.top = `${glassY}px`;
    
        glass.style.backgroundPosition = `${-(offsetX * zoom - glassSize / 2)}px ${-(offsetY * zoom - glassSize / 2)}px`;
      });
    });
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-color: #f0f0f0;
    }
    
    .image-wrapper {
      position: relative;
      box-sizing: border-box;
      border: 1px dashed gray;
    }
    
    .image {
      width: 300px;
      height: auto;
      cursor: none;
    }
    
    .image-wrapper .glass {
      opacity: 0;
    }
    
    .image-wrapper:hover .glass {
      opacity: 1;
    }
    
    .glass {
      position: absolute;
      width: var(--glass-size);
      height: var(--glass-size);
      border-radius: 50%;
      cursor: none;
      background-repeat: no-repeat;
      pointer-events: none;
      transition: opacity 0.2s;
      border: 1px solid black;
    }
    <div class="image-wrapper">
      <img src="https://i.sstatic.net/JzS3fR2C.jpg" alt="Sample Image" style="opacity: 0.1;" class="image">
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search