Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
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;
};
};
.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