Search code examples
javascriptmagnify

Magnifying Glass Effect "failing" at edges


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?


Solution

  • 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>