<section class="c-container" data-image-magnifier>
    <img src="https://farm5.staticflickr.com/4775/40838850831_cd1ce064f3.jpg" id="test-img" class="c-container__img" srcset="https://farm5.staticflickr.com/4775/40838850831_cd1ce064f3.jpg 480w, https://farm5.staticflickr.com/4775/40838850831_1b5ca41eff_h.jpg 1200w, https://farm5.staticflickr.com/4775/40838850831_ddc946af81_k.jpg 2000w" sizes="100vw" data-magnifier-size="4000px" alt="photo of the wall fresco along the ladder" /> 
</section>
HTML {
  /*using system font-stack*/
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
  font-size: 115%; /*~18px*/
  font-size: calc(16px + (30 - 16) * (100vw - 300px) / (1300 - 300) );
  line-height: 1.5;
  box-sizing: border-box;
}

BODY {
  margin: 0;
  color: #3a3d40;
  background: rgb(253, 254, 253);
}

*, *::before, *::after {
  box-sizing: inherit;
  color: inherit;
}

/*Actual Style*/

.c-container {
  position: relative;
  margin: 8rem auto;
  max-width: 1000px;
  cursor: none;
}

.c-container__img {
  width: 100%;
}

.c-image-magnifier__image {
  -webkit-touch-callout:none;
  -webkit-user-select:none;
  user-select: none;
}

.o-loupe-wrapper {
  position: absolute;
  width: 9.45rem; /*~150px*/
  height: 9.45rem; /*~150px*/
  /*max-width: 18.45rem;*/ /*~300px*/
  /*max-height: 18.45rem;*/ /*~300px*/
  border-radius: 100%;
  border: 5px solid #fff;
  overflow: hidden;  
  pointer-events: none;
  transform: scale(0);
  opacity: 0;
  transition: opacity 0s .15s, transform .3s ease-out;
  box-shadow: 0 5px 7px rgba(0, 0, 0, .7);
  z-index: 999;
}

[data-image-magnifier]:hover .o-loupe-wrapper {
  transform: scale(1);
  opacity: 1;
  transition-timing-function: ease-in;
}

.o-loupe-wrapper__image {
}
;(function () {
  "use strict";
  
  const ENLARGE_IMG_CONTAINER_ATTR = "data-image-magnifier";
 
  const imgMagnifierContainers = Array.from(document.querySelectorAll("[" + ENLARGE_IMG_CONTAINER_ATTR + "]"));
  
  if(!imgMagnifierContainers.length) {
    console.error("None element with " + ENLARGE_IMG_CONTAINER_ATTR + " attribute has been found.");
    return;
  };
  // Classes for JS hooks
  const IMAGE_MAGNIFIER_IMAGE_JS_CLASS = "js-magnifierImg";
  const LOUPE_WRAPPER_JS_CLASS = "js-flyoutLoupe";
  const LOUPE_IMAGE_JS_CLASS = "js-flyoutLoupeImg";
  // Classes for the Style
  const IMAGE_MAGNIFIER_CLASS = "c-image-magnifier";
  const IMAGE_MAGNIFIER_IMAGE_CLASS = "c-image-magnifier__image";
  
  const LOUPE_WRAPPER_CLASS = "o-loupe-wrapper";
  const LOUPE_IMAGE_CLASS = "o-loupe-wrapper__image";
  
  /// this attribute determines the value of the `size` attribute on generated loupe-image
  const LOUPE_IMAGE_SIZE_ATTR = "data-magnifier-size";
  
  /*  ADDING EVENT LISTENERS
  -------------------------------------*/
  document.addEventListener("DOMContentLoaded", onDOMContentLoaded);
 
  
  /*========================================
              EVENT LISTENERS
  ==========================================*/
  
  function onDOMContentLoaded (e) { // TODO: this event may be changed into public init method
    
    imgMagnifierContainers.forEach(function(container) {
       
        container.imgMagnifier = {
          touchStarted: false
        };
      
        container.addEventListener("mouseenter", imageContainerOnMouseEnter);
        container.classList.add(IMAGE_MAGNIFIER_CLASS);
        const magnifierImg = setupMagnifierImg(container, IMAGE_MAGNIFIER_IMAGE_JS_CLASS, IMAGE_MAGNIFIER_IMAGE_CLASS);
      
        container.addEventListener("touchstart", imageContainerOnMouseEnter);
        container.addEventListener("touchmove", imageContainerOnMouseMove);
        container.addEventListener("touchend", imageContainerOnMouseLeave);
      
        container.addEventListener("mousemove", imageContainerOnMouseMove);
        const loupeHTML = _createLoupeImgHTML(container, IMAGE_MAGNIFIER_IMAGE_JS_CLASS);
        container.appendChild(loupeHTML);
    });
    
  };
    
  
  function imageContainerOnMouseEnter (e) {

    const container = e.currentTarget;
    
    // mouse events may fire along with touch events and we just want the touch event
    if(container.imgMagnifier.touchStarted && e.type === "mouseenter") {
      return;
    }
    
    const isTouchEvent = e.type === "touchstart";
    
    if(isTouchEvent) {
      container.imgMagnifier.touchStarted = true;
    };
    
    const loupeImage = container.querySelector("." + LOUPE_IMAGE_JS_CLASS);
    const loupeWrapper = container.querySelector("." + LOUPE_WRAPPER_JS_CLASS);
    
    if(loupeImage) {
      const loupeImageSize = loupeImage.getAttribute(LOUPE_IMAGE_SIZE_ATTR);

      if(loupeImageSize)
      loupeImage.setAttribute("sizes", loupeImageSize);
    };
    
    // offset the loupe a bit differently for touch event so that it's not directly beneath a finger
    const divider = isTouchEvent ? 1.1 : 2;
    
	   loupeWrapper.style.marginLeft = (loupeWrapper.offsetWidth / divider * -1) + "px";
	   loupeWrapper.style.marginTop = (loupeWrapper.offsetHeight / divider * -1) + "px"; 
  };
  
  function imageContainerOnMouseMove (e) {
    debugger;
    const imageContainer = e.currentTarget; // refers to the element event handler has been attached to
    
   	// mouse events may fire along with touch events and we just want the touch event
		if(imageContainer.imgMagnifier.touchStarted && e.type === "mousemove" ){
				return;
		};
    
    const isTouchEvent = e.type === "touchmove";
    
    // const moveX = isTouchEvent ?  e.changedTouches[0].pageX : e.clientX;
    // const moveY = isTouchEvent ? e.changedTouches[0].pageY : e.clientY;
    
    const moveX = isTouchEvent ?  e.touches[0].clientX : e.clientX;
    const moveY = isTouchEvent ? e.touches[0].clientY : e.clientY;
    
    const loupeWrapper = imageContainer.querySelector("." + LOUPE_WRAPPER_JS_CLASS);
    const loupeImage = loupeWrapper.querySelector("." + LOUPE_IMAGE_JS_CLASS);

            const x =  moveX - imageContainer.getBoundingClientRect().left;
						const y = moveY - imageContainer.getBoundingClientRect().top;
 
						const containerWidth = imageContainer.offsetWidth;
						const containerHeight = imageContainer.offsetHeight;
						const loupeImageWidth =	loupeImage.offsetWidth;
						const loupeImageHeight = loupeImage.offsetHeight;
						const loupeWrapperWidth = loupeWrapper.offsetWidth;
						const loupeWrapperHeight = loupeWrapper.offsetHeight;
						const widthFactor = containerWidth / loupeWrapperWidth;
						const heightFactor = containerWidth / loupeWrapperHeight;
 
    
            requestAnimationFrame(function() {
              	loupeWrapper.style.top = y + "px";
							  loupeWrapper.style.left = x + "px";
           			loupeWrapper.scrollLeft = (( x / containerWidth ) * ( loupeImageWidth - loupeWrapperWidth ));
						    loupeWrapper.scrollTop = (( y / containerHeight ) * ( loupeImageHeight - loupeWrapperHeight )) ;  
            });

  };
  
  
  function imageContainerOnMouseLeave (e) {
    const container = e.currentTarget;

    if(container.imgMagnifier.touchStarted){     
		 	container.imgMagnifier.touchStarted = false;
	 	};
  };
  
  
  /*-----------------------------------------
            UTILITY FUNCTIONS
  -------------------------------------------*/
  function _createLoupeImgHTML (container, imgClass) {
    const loupeContainer = document.createElement("div");
    
    const origIMG = container.querySelector("." + imgClass);
    debugger;
     const loupeImg = origIMG ? origIMG.cloneNode() : null;
     loupeContainer.classList.add(LOUPE_WRAPPER_CLASS, LOUPE_WRAPPER_JS_CLASS);
       
     if(loupeImg) {
       loupeImg.removeAttribute("id");
       loupeImg.className = LOUPE_IMAGE_CLASS;
       loupeImg.classList.add(LOUPE_IMAGE_JS_CLASS);
       loupeContainer.appendChild(loupeImg);
     }
     return loupeContainer;
  };
  
  function setupMagnifierImg (imgContainer, imgJSClass, imgStyleClass) {
    const img = imgContainer.querySelector("img");
    if(!img) throw new Error("Image Magnifier: Image was not found");
    img.classList.add(imgStyleClass, imgJSClass);
    return img;
  };
  
})();



/*
Idea from:
https://www.filamentgroup.com/lab/sizes-swap/
GitHub:
https://github.com/filamentgroup/enlarge/blob/updates/src/enlarge.js
*/
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.