Last active
January 31, 2021 17:21
-
-
Save gschoppe/037e5dabb53e6c8521889020d2741a23 to your computer and use it in GitHub Desktop.
A simple class to convert a div, or other block-level element, into a zoom and crop interface for images. Sample Implementation: https://codepen.io/gschoppe/pen/xxRxwaO
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function CropInterface(el, w, h, resolutionMultiple) { | |
// handle parameters | |
this.el = el; | |
if (!w) { w = 100; } | |
this.w = w; | |
if (!h) { h = 100; } | |
this.h = h; | |
this.r = w / h; | |
if (!resolutionMultiple) { resolutionMultiple = 1; } | |
this.resolutionMultiple = resolutionMultiple; | |
// for later | |
this.dims = {'w': 0, 'h': 0}; | |
this.focus = {'x': 0.5, 'y': 0.5}; | |
this.dragging = false; | |
this.lastMouse = null; | |
this.maxZoom = 10; | |
// build interface | |
this.el.classList.add('gs-crop-interface'); | |
// - image wrapper | |
this.frame = document.createElement('DIV'); | |
this.frame.classList.add('gs-crop-frame'); | |
this.frame.style.width = this.w + "px"; | |
this.frame.style.height = this.h + "px"; | |
this.el.appendChild(this.frame); | |
// - image | |
this.image = document.createElement('IMG'); | |
this.image.classList.add('gs-crop-image'); | |
this.frame.appendChild(this.image); | |
// - zoom | |
this.zoom = document.createElement('INPUT'); | |
this.zoom.setAttribute('type' , 'range'); | |
this.zoom.setAttribute('min' , 0); | |
this.zoom.setAttribute('max' , 1); | |
this.zoom.setAttribute('step' , 0.001); | |
this.zoom.setAttribute('value', 0); | |
this.zoom.classList.add('gs-crop-zoom'); | |
this.zoom.style.width = this.w + "px"; | |
this.el.appendChild(this.zoom); | |
this.setPosition = function() { | |
var z = this.getZoom(); | |
var i = { | |
'w': this.dims.w * z, | |
'h': this.dims.h * z | |
}; | |
var max = { | |
'x': (i.w - this.w), | |
'y': (i.h - this.h) | |
} ; | |
var x = i.w * this.focus.x - this.w / 2; | |
var y = i.h * this.focus.y - this.h / 2; | |
if (x < 0) x = 0; | |
if (y < 0) y = 0; | |
if (x > max.x) x = max.x; | |
if (y > max.y) y = max.y; | |
// fix focus position | |
this.focus.x = (x + this.w / 2) / i.w; | |
this.focus.y = (y + this.h / 2) / i.h; | |
// set image position | |
this.image.style.left = '-' + x + 'px'; | |
this.image.style.top = '-' + y + 'px'; | |
}; | |
var dragStart = (e) => { | |
e.preventDefault(); | |
let mouse = {}; | |
this.dragging = true; | |
this.frame.classList.add('active'); | |
if (e.touches && e.touches[0]) { | |
mouse = { | |
'x': e.touches[0].pageX, | |
'y': e.touches[0].pageY | |
}; | |
} else { | |
mouse = { | |
'x': e.clientX, | |
'y': e.clientY | |
}; | |
} | |
this.lastMouse = mouse; | |
}; | |
var dragEnd = (e) => { | |
e.preventDefault(); | |
this.dragging = false; | |
this.frame.classList.remove('active'); | |
this.lastMouse = null; | |
}; | |
var dragMove = (e) => { | |
e.preventDefault(); | |
let mouse = {}; | |
if(this.dragging) { | |
var z = this.getZoom(); | |
if (e.touches && e.touches[0]) { | |
mouse = { | |
'x': e.touches[0].pageX, | |
'y': e.touches[0].pageY | |
}; | |
} else { | |
mouse = { | |
'x': e.clientX, | |
'y': e.clientY | |
}; | |
} | |
let deltaX = (mouse.x - this.lastMouse.x) / (this.dims.w * z); | |
let deltaY = (mouse.y - this.lastMouse.y) / (this.dims.h * z); | |
this.focus.x -= deltaX; | |
if (this.focus.x > 1) this.focus.x = 1; | |
if (this.focus.x < 0) this.focus.x = 0; | |
this.focus.y -= deltaY; | |
if (this.focus.y > 1) this.focus.y = 1; | |
if (this.focus.y < 0) this.focus.y = 0; | |
if (deltaX || deltaY) { | |
this.setPosition(); | |
} | |
this.lastMouse = mouse; | |
} | |
}; | |
this.image.ontouchstart = dragStart; | |
this.image.ontouchend = dragEnd; | |
this.image.ontouchmove = dragMove; | |
this.image.ontouchcancel = (e) => {e.preventDefault();}; | |
this.image.onmousedown = dragStart; | |
this.image.onmouseup = dragEnd; | |
this.image.onmousemove = dragMove; | |
this.getImageUrl = function() { | |
return this.image.src; | |
} | |
this.getDimensions = function() { | |
return { | |
'w' : this.w * this.resolutionMultiple, | |
'h' : this.h * this.resolutionMultiple | |
}; | |
} | |
this.getFocus = function() { | |
return this.focus; | |
} | |
this.getZoom = function() { | |
return 1 + Math.pow( this.zoom.value, 2 ) * (this.maxZoom - 1); | |
}; | |
this.processZoom = function() { | |
var z = this.getZoom(); | |
this.image.style.width = this.dims.w * z + 'px'; | |
this.image.style.height = this.dims.h * z + 'px'; | |
this.setPosition(); | |
}; | |
this.zoom.oninput = this.processZoom.bind(this); | |
this.zoom.onchange = this.processZoom.bind(this); | |
this.loadImage = function(imageUrl) { | |
this.focus = {'x': 0.5, 'y': 0.5}; | |
this.zoom.value = 0; | |
this.image.style.width = null; | |
this.image.style.height = null; | |
this.image.onload = () => { | |
this.dims = { | |
'w' : this.image.width, | |
'h' : this.image.height | |
} | |
if (!this.dims.h) {this.dims.h = 1} | |
var img_r = this.dims.w / this.dims.h; | |
// scale image dimensions to cover frame | |
if (this.r > img_r) { | |
this.dims.w = this.w; | |
this.dims.h = this.w/img_r; | |
} else { | |
this.dims.w = this.h * img_r; | |
this.dims.h = this.h; | |
} | |
this.processZoom(); | |
}; | |
this.image.src = imageUrl; | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.gs-crop-interface, | |
.gs-crop-interface::before, | |
.gs-crop-interface::after | |
.gs-crop-interface *, | |
.gs-crop-interface *::before, | |
.gs-crop-interface *::after { | |
box-sizing: border-box; | |
} | |
.gs-crop-interface .gs-crop-frame { | |
position: relative; | |
display: block; | |
overflow: hidden; | |
} | |
.gs-crop-interface .gs-crop-frame.active { | |
overflow: visible; | |
} | |
.gs-crop-interface .gs-crop-frame.active::before { | |
background-image: none; | |
background-color: #fff; | |
} | |
.gs-crop-interface .gs-crop-frame.active::after { | |
border-color: #000; | |
} | |
.gs-crop-interface .gs-crop-frame.active * { | |
opacity: 0.4; | |
} | |
.gs-crop-interface .gs-crop-frame::before, | |
.gs-crop-interface .gs-crop-frame::after { | |
display: block; | |
position: absolute; | |
content: ' '; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
} | |
.gs-crop-interface .gs-crop-frame::before { | |
background: url() center center no-repeat; | |
background-size: contain; | |
background-image: url('https://lh3.googleusercontent.com/EbXw8rOdYxOGdXEFjgNP8lh-YAuUxwhOAe2jhrz3sgqvPeMac6a6tHvT35V6YMbyNvkZL4R_a2hcYBrtfUhLvhf-N2X3OB9cvH4uMw=w1064-v0'); | |
opacity: 0.3; | |
z-index: 0; | |
} | |
.gs-crop-interface .gs-crop-frame * { | |
position: absolute; | |
background-color: #fff; | |
z-index: 50; | |
cursor: move; | |
} | |
.gs-crop-interface .gs-crop-frame::after { | |
border: 1px solid #aaa; | |
pointer-events: none; | |
z-index: 100; | |
} | |
.gs-crop-interface .gs-crop-zoom { | |
display: block; | |
margin: 10px auto; | |
z-index: 50; | |
cursor: pointer; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment