JavaScript to achieve magnifier effect (including throttling)

Keywords: IE

requirement analysis

It is required to make a magnifying glass as shown in the figure, with mouse following, turning on and off when right clicking, including transition animation.
The general idea is to bind mouse events and dynamically modify the background position. The difficulty lies in that the calculation of the background position should be considered comprehensively.
In the switch animation part, we choose to use transform: scale(). Compared with the width / height change, the transform can be combined with transform origin to solve the problem of center displacement to the upper left corner.
In terms of performance optimization, a simple throttle function is used to throttle resize and mousemove events. In addition, the dynamic modification of CSS is also as simple as possible according to the situation to ensure that unnecessary modifications are not made.
In detail experience, because the cursor of the magnifier element is set to none, when the magnifier is closed, the element is moved to the window (the second quadrant) as a whole, which solves the problem of mouse disappearing after the magnifier is closed.

Static CSS section

The first thing we need to do is to give a fixed location, so that our magnifying glass can be separated from the document flow and positioned relative to the window.
The magnifying glass has a round frame. Here, use border radius: 50%, and cover the inside and outside with box shadow.
transform: scale needs to give an initial 0 first. If it is initialized in JS, it will trigger an animation.
transition: transform is used for transition animation when switching.
Other attributes such as border, width, height, transform, left, top, background position are all operated through JS.
CSS code is as follows:

#magnifier {
        position: fixed;
        border-radius: 50%;
        box-shadow: 0 0 4px 2px rgba(51, 51, 51, 0.2) inset,
          0 0 4px 2px rgba(51, 51, 51, 0.3);
        background-image: url("1.jpeg");
        background-repeat: no-repeat;
        transform: scale(0, 0);
        transition: transform 0.15s ease-out;
        cursor: none;
      }

Part JS

Continuously monitor the right mouse button: contextmenu event, and use a variable to control the magnifier switch.
If it is currently off, right-click to mount mousemove listener and modify style;
If it is currently on, remove the mousemove listener when you right-click.

First, define a constant. RAD represents the radius of the magnifying glass. FPS sets the maximum number of event triggers per second (for throttling). BORDER is the width of the magnifying glass BORDER. If you want to make components in the future, these can be passed in as props.

const RAD = 200,
  FPS = 125,
  BORDER = 8;

Then define two global variables, toggled to record the opening and closing state of the current magnifying glass, mag is the DOM element corresponding to the magnifying glass.

var toggled = false,
  mag = null;

Listen to the contextmenu when the window is loaded, and initialize some styles.

window.onload = function() {
  document.addEventListener("contextmenu", handleRightClick);
  mag = document.getElementById("magnifier");
  Object.assign(mag.style, {
    width: `${RAD * 2}px`,
    height: `${RAD * 2}px`,
    backgroundSize: `${window.innerWidth}px ${window.innerHeight}px`,
    border: `${BORDER}px solid #fff`
  });
};

The following is the implementation of the contextmenu callback. Note that the backgroundPosition value is the mouse coordinate inversion, and the magnifying glass size and border width should be calculated together.

function handleRightClick(e) {
  e.preventDefault();
  if (toggled) {
    // toggle off
    document.removeEventListener("mousemove", handleMouseMove);
    Object.assign(mag.style, {
      left: `${-RAD * 2}px`,
      top: `${-RAD * 2}px`,
      transform: `scale(0, 0)`
    });
  } else {
    // toggle on
    document.addEventListener("mousemove", handleMouseMove);
    const { clientX: x, clientY: y } = e;
    Object.assign(mag.style, {
      transform: "scale(1, 1)",
      left: `${x - RAD}px`,
      top: `${y - RAD}px`,
      backgroundPosition: `${-x + RAD - BORDER}px ${-y + RAD - BORDER}px`
    });
  }
  toggled = !toggled;
}

Next is the implementation of the mousemove callback. Note that the declaration here is wrapped with a throttle to ensure that the callback points correctly.

const handleMouseMove = throttle(function(e) {
  const { clientX: x, clientY: y } = e;
  Object.assign(mag.style, {
    left: `${x - RAD}px`,
    top: `${y - RAD}px`,
    backgroundPosition: `${-x + RAD - BORDER}px ${-y + RAD - BORDER}px`
  });
}, Math.floor(1000 / FPS));

Part Resize

Write a resize listener again. When the window size changes, only the background size can be changed. It also needs throttling. Because only one property needs to be changed here, Object.assign is not needed.

window.onresize = throttle(function() {
  mag.style.backgroundSize = `${window.innerWidth}px ${window.innerHeight}px`;
}, Math.floor(1000 / FPS));

Throttling part

Finally, the implementation of throttle throttler, which only saves time in the closure. The reason why we do not use the timeout type throttle here is that it will cause redundant displacement.

function throttle(fun, delay) {
  let last = Date.now();
  return function() {
    let ctx = this,
      args = arguments,
      now = Date.now();
    if (now - last > delay) {
      fun.apply(ctx, args);
      last = now;
    }
  };
}

The code of the timeout type throttler is as follows, which is not applicable in this project. You can see that no matter what we do, it will execute one more callback at the end, which is triggered by timeout. This will make the mousemove callback of the magnifying glass more than once, resulting in an unknown displacement.

function throttle(fun, delay) {
  let last = Date.now(),
    timeout;
  return function() {
    let context = this,
      args = arguments,
      now = Date.now();
    clearTimeout(timeout);
    if (now - last > delay) {
      fun.apply(context, arguments);
      last = now;
    } else {
      timeout = setTimeout(() => {
        fun.apply(context, args);
      }, delay);
    }
  };
}

Full HTML & CSS code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Magnifer Test</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      html,
      body {
        height: 100vh;
        background: #0ff;
        font-size: 60px;
      }
      #magnifier {
        position: fixed;
        border-radius: 50%;
        box-shadow: 0 0 4px 2px rgba(51, 51, 51, 0.2) inset,
          0 0 4px 2px rgba(51, 51, 51, 0.3);
        background-image: url("1.jpeg");
        background-repeat: no-repeat;
        transform: scale(0, 0);
        transition: transform 0.15s ease-out;
        cursor: none;
      }
    </style>
  </head>
  <body>
    <h1>1</h1>
    <h1>2</h1>
    <h1>3</h1>
    <h1>4</h1>
    <h1>5</h1>
    <h1>6</h1>
    <h1>7</h1>
    <h1>8</h1>
    <h1>9</h1>
    <h1>10</h1>
    <div id="magnifier"></div>
  </body>
  <script src="index.js"></script>
</html>

Full JS code

const RAD = 200,
  FPS = 125,
  BORDER = 8;
var toggled = false,
  mag = null;

function handleRightClick(e) {
  e.preventDefault();
  if (toggled) {
    // toggle off
    document.removeEventListener("mousemove", handleMouseMove);
    Object.assign(mag.style, {
      left: `${-RAD * 2}px`,
      top: `${-RAD * 2}px`,
      transform: `scale(0, 0)`
    });
  } else {
    // toggle on
    document.addEventListener("mousemove", handleMouseMove);
    const { clientX: x, clientY: y } = e;
    Object.assign(mag.style, {
      transform: "scale(1, 1)",
      left: `${x - RAD}px`,
      top: `${y - RAD}px`,
      backgroundPosition: `${-x + RAD - BORDER}px ${-y + RAD - BORDER}px`
    });
  }
  toggled = !toggled;
}

const handleMouseMove = throttle(function(e) {
  const { clientX: x, clientY: y } = e;
  console.log({ x, y });
  Object.assign(mag.style, {
    left: `${x - RAD}px`,
    top: `${y - RAD}px`,
    backgroundPosition: `${-x + RAD - BORDER}px ${-y + RAD - BORDER}px`
  });
}, Math.floor(1000 / FPS));

window.onload = function() {
  document.addEventListener("contextmenu", handleRightClick);
  mag = document.getElementById("magnifier");
  Object.assign(mag.style, {
    width: `${RAD * 2}px`,
    height: `${RAD * 2}px`,
    backgroundSize: `${window.innerWidth}px ${window.innerHeight}px`,
    border: `${BORDER}px solid #fff`
  });
};

window.onresize = throttle(function() {
  Object.assign(mag.style, {
    backgroundSize: `${window.innerWidth}px ${window.innerHeight}px`
  });
});

function throttle(fun, delay) {
  let last = Date.now();
  return function() {
    let ctx = this,
      args = arguments,
      now = Date.now();
    if (now - last > delay) {
      fun.apply(ctx, args);
      last = now;
    }
  };
}

Published 4 original articles, won praise 2, visited 985
Private letter follow

Posted by myharshdesigner on Tue, 18 Feb 2020 04:56:57 -0800