Skip to content

Instantly share code, notes, and snippets.

@espretto
Last active January 8, 2020 12:54
Show Gist options
  • Save espretto/34b40b2ccaa0f4f5ae9a79f0e98b79bb to your computer and use it in GitHub Desktop.
Save espretto/34b40b2ccaa0f4f5ae9a79f0e98b79bb to your computer and use it in GitHub Desktop.
js: magnifying glass for images
/**
* magnifying glass for images
* @licence MIT
*/
(function () {
/** avoid subpixel rendering */
function px (n) {
return Math.floor(n) + 'px'
}
/** case convert camelCase to kebab-case */
function kebabCase (str) {
return str.replace(/[a-z](?=[A-Z])/g, '$&-').toLowerCase()
}
/** serialize style object */
function toCssText (obj) {
return Object.keys(obj).map(function (key) {
return kebabCase(key) + ':' + obj[key]
}).join(';')
}
/** bind event listener */
function on (el, ev, fn) {
el.addEventListener(ev, fn)
return function off () { el.removeEventListener(ev, fn) }
}
/** check image element */
function isImage (el) {
return el.nodeType === 1 && el.nodeName === 'IMG'
}
/** delegate dom insertion */
function insert (el) {
return document.body.appendChild(el), el
}
/** delegate dom removal */
function remove (el) {
return el.parentNode.removeChild(el)
}
/** h-ypertext element factory */
function h (tag, obj) {
var el = document.createElement(tag)
if (obj) {
Object.keys(obj).forEach(function (key) {
if (key in el) el[key] = obj[key]
else el.setAttribute(key, obj[key])
})
}
return el
}
/**
* Zoomer - singleton state container
* (there can only be one focused image at any given time)
*/
var Zoomer = {
/** current image element */
img: null,
/** element of magnifying glass */
magnifier: null,
/** dynamic style sheet element */
styleElement: null,
/** cancel event listeners */
cancelListener: null,
/** initialize magnifying glass for a given image element */
init: function (img) {
var fixedCss = toCssText({
zIndex: 10,
position: 'absolute',
pointerEvents: 'none',
border: '1px double #999',
boxShadow: 'inset 1px 1px 3px #ccc, inset -1px -1px 3px #ccc',
backgroundRepeat: 'no-repeat',
backgroundImage: ['url(', img.src, ')'].join('"')
})
this.img = img
this.magnifier = insert(h('div', { className: 'zoomer' }))
this.styleElement = insert(h('style', { innerHTML: '.zoomer {' + fixedCss + '}' }))
this.cancelListener = on(img, 'mousemove', this.onMousemove.bind(this))
},
/** reposition magnifying glass according to mouse position */
onMousemove: function (event) {
var img = event.target
var mouseX = event.pageX
var mouseY = event.pageY
// do not hoist for resize support
var offX = mouseX - img.offsetLeft
var offY = mouseY - img.offsetTop
var imgWidth = img.width
var imgHeight = img.height
var magWidth = imgWidth / 2
var magHeight = imgHeight / 2
// subtract half the magnifier to center it
var bgX = (offX * img.naturalWidth / imgWidth) - magWidth/2
var bgY = (offY * img.naturalHeight / imgHeight) - magHeight/2
this.magnifier.style.cssText = toCssText({
left: px(mouseX - magWidth/2),
top: px(mouseY - magHeight/2),
width: px(magWidth),
height: px(magHeight),
backgroundPosition: [-bgX, -bgY].map(px).join(' ')
})
},
/** cancel event listeners, detach dom elements and nullify references */
reset: function () {
this.cancelListener()
remove(this.magnifier)
remove(this.styleElement)
this.img =
this.magnifier =
this.styleElement =
this.cancelListener = null
}
}
/**
* use event delegation to limit the widget's area
*/
on(document, 'mouseover', function (event) {
if (isImage(event.target)) Zoomer.init(event.target)
})
/**
* since every mouseover event is followed by a mouseout event
* there can only be one focused image at any given time
*/
on(document, 'mouseout', function (event) {
if (event.target === Zoomer.img) Zoomer.reset()
})
}())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment