349 lines
7.9 KiB
JavaScript
349 lines
7.9 KiB
JavaScript
export default class Enlarge {
|
|
/*
|
|
prefix = '#view-';
|
|
ele = null;
|
|
closeEle = null;
|
|
contentsEle = null;
|
|
photoEle = null;
|
|
preloadEle = null;
|
|
|
|
zoom = 0;
|
|
x = 0;
|
|
y = 0;
|
|
|
|
mouseDown = false;
|
|
|
|
preservedScroll = {};
|
|
preloadEles = {};
|
|
|
|
openCloseTimeout = null;
|
|
*/
|
|
|
|
constructor(ele, prefix='#view-') {
|
|
this.showing = false;
|
|
this.prefix = prefix;
|
|
this.zoom = 0;
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.mouseDown = false;
|
|
this.preservedScroll = {};
|
|
this.preloadEles = {};
|
|
|
|
this.constructWrapper();
|
|
this.bindZoomControls();
|
|
|
|
this.bindSize();
|
|
this.bindWindow();
|
|
|
|
this.bindTouch();
|
|
this.bindDrag();
|
|
this.bindScroll();
|
|
|
|
ele.appendChild(this.ele);
|
|
}
|
|
|
|
constructWrapper() {
|
|
this.ele = document.createElement('div');
|
|
this.ele.classList.add('enlarge');
|
|
this.ele.style.display = 'none';
|
|
this.ele.style.opacity = '0';
|
|
|
|
this.zoomControlsEle = document.createElement('div');
|
|
this.zoomControlsEle.classList.add('enlarge-zoom-controls');
|
|
this.ele.appendChild(this.zoomControlsEle);
|
|
|
|
this.zoomInEle = document.createElement('a');
|
|
this.zoomInEle.classList.add('enlarge-zoom-in');
|
|
this.zoomInEle.innerText = 'Zoom +';
|
|
this.zoomInEle.href = '#';
|
|
this.zoomControlsEle.appendChild(this.zoomInEle);
|
|
|
|
this.zoomOutEle = document.createElement('a');
|
|
this.zoomOutEle.classList.add('enlarge-zoom-out');
|
|
this.zoomOutEle.innerText = 'Zoom -';
|
|
this.zoomOutEle.href = '#';
|
|
this.zoomControlsEle.appendChild(this.zoomOutEle);
|
|
|
|
this.closeEle = document.createElement('a');
|
|
this.closeEle.classList.add('enlarge-close');
|
|
this.closeEle.innerText = 'Close';
|
|
this.closeEle.href = '#';
|
|
this.ele.appendChild(this.closeEle);
|
|
|
|
this.contentsEle = document.createElement('div');
|
|
this.contentsEle.classList.add('enlarge-contents');
|
|
this.ele.appendChild(this.contentsEle);
|
|
|
|
this.preloadEle = document.createElement('img');
|
|
this.preloadEle.classList.add('enlarge-preload');
|
|
this.preloadEle.src = '';
|
|
this.contentsEle.appendChild(this.preloadEle);
|
|
|
|
this.photoEle = document.createElement('img');
|
|
this.photoEle.classList.add('enlarge-photo');
|
|
this.photoEle.src = '';
|
|
this.contentsEle.appendChild(this.photoEle);
|
|
}
|
|
|
|
bindZoomControls() {
|
|
this.zoomInEle.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.zoomAt(this.zoom * +0.25, this.viewportSize.w / 2, this.viewportSize.h / 2);
|
|
this.draw();
|
|
}, false);
|
|
this.zoomOutEle.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.zoomAt(this.zoom * -0.25, this.viewportSize.w / 2, this.viewportSize.h / 2);
|
|
this.draw();
|
|
}, false);
|
|
}
|
|
|
|
bindSize() {
|
|
this.preloadEle.addEventListener('load', this.updateSize.bind(this), false);
|
|
this.photoEle.addEventListener('load', this.updateSize.bind(this), false);
|
|
}
|
|
|
|
bindWindow() {
|
|
window.addEventListener('resize', () => {
|
|
if (this.showing) {
|
|
this.updateSize();
|
|
}
|
|
}, false);
|
|
}
|
|
|
|
bindTouch() {
|
|
|
|
}
|
|
|
|
bindDrag() {
|
|
this.contentsEle.addEventListener('pointerdown', (e) => {
|
|
e.preventDefault();
|
|
this.mouseDown = true;
|
|
this.contentsEle.style.cursor = 'grabbing';
|
|
}, false);
|
|
this.contentsEle.addEventListener('pointermove', (e) => {
|
|
e.preventDefault();
|
|
if (this.mouseDown) {
|
|
this.pan(e.movementX, e.movementY);
|
|
this.draw();
|
|
}
|
|
}, false);
|
|
this.contentsEle.addEventListener('pointerup', (e) => {
|
|
e.preventDefault();
|
|
this.mouseDown = false;
|
|
this.contentsEle.style.cursor = 'grab';
|
|
}, false);
|
|
}
|
|
|
|
bindScroll() {
|
|
this.ele.addEventListener('wheel', (e) => {
|
|
e.preventDefault();
|
|
// Assume full screen
|
|
this.zoomAt(e.deltaY * -0.002 * this.zoom, e.clientX, e.clientY);
|
|
this.draw();
|
|
}, false);
|
|
}
|
|
|
|
openHash() {
|
|
const hash = window.location.hash;
|
|
if (!hash.startsWith(this.prefix)) {
|
|
this.close();
|
|
return;
|
|
}
|
|
const id = hash.replace(this.prefix, '');
|
|
this.open(id);
|
|
}
|
|
|
|
watchHash() {
|
|
window.addEventListener('hashchange', this.openHash.bind(this), false);
|
|
this.openHash();
|
|
}
|
|
|
|
setPreload(id) {
|
|
// https://caniuse.com/#feat=mdn-javascript_operators_optional_chaining
|
|
let preload = null;
|
|
if (this.preloadEles[id]) {
|
|
preload = this.preloadEles[id].currentSrc;
|
|
}
|
|
|
|
if (preload) {
|
|
this.preloadEle.src = preload;
|
|
this.preloadEle.display = 'block';
|
|
} else {
|
|
this.preloadEle.src = '';
|
|
this.preloadEle.display = 'none';
|
|
}
|
|
}
|
|
|
|
setPhoto(id) {
|
|
this.zoom = 0;
|
|
this.x = 0;
|
|
this.y = 0;
|
|
if (id) {
|
|
this.photoEle.src = id;
|
|
} else {
|
|
this.photoEle.src = '';
|
|
}
|
|
}
|
|
|
|
// Update the size of the image
|
|
updateSize() {
|
|
this.contentsEle.style.width = `${this.size.w}px`;
|
|
this.contentsEle.style.height = `${this.size.h}px`;
|
|
console.debug(`updated size to ${this.size.w}, ${this.size.h}`);
|
|
this.draw();
|
|
}
|
|
|
|
draw() {
|
|
this.zoom = Enlarge.clampZoom(this.zoom, this.baseScale);
|
|
this.y = Enlarge.clamp(this.y, this.viewportSize.h, this.size.h * this.zoom * this.baseScale);
|
|
this.x = Enlarge.clamp(this.x, this.viewportSize.w, this.size.w * this.zoom * this.baseScale);
|
|
this.contentsEle.style.transform = `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.zoom * this.baseScale})`;
|
|
}
|
|
|
|
pan(x, y) {
|
|
this.x += x;
|
|
this.y += y;
|
|
}
|
|
|
|
zoomAt(by, x, y) {
|
|
const newZoom = Enlarge.clampZoom(this.zoom + by, this.baseScale);
|
|
// Change ratio
|
|
const changedRatio = newZoom / this.zoom;
|
|
// Rescale existing offset
|
|
this.x *= changedRatio;
|
|
this.y *= changedRatio;
|
|
// Rescale cursor offset
|
|
this.x -= (changedRatio - 1) * x;
|
|
this.y -= (changedRatio - 1) * y;
|
|
// Update new zoom
|
|
this.zoom = newZoom;
|
|
}
|
|
|
|
static clamp(translation, viewport, size) {
|
|
if (viewport > size) {
|
|
// Ensure centred
|
|
return (viewport - size) / 2;
|
|
}
|
|
if (translation > 0) {
|
|
// Start side
|
|
return 0;
|
|
} else if (translation < (viewport - size)) {
|
|
// End side
|
|
return viewport - size;
|
|
} else {
|
|
return translation;
|
|
}
|
|
}
|
|
|
|
static clampZoom(zoom, baseScale) {
|
|
// Cap high resolution scale at 1.5
|
|
// Cap low resolution zoom at 1.5
|
|
const maxZoom = Math.max(1.5 / baseScale, 1.5);
|
|
return Math.min(Math.max(zoom, 1), maxZoom);
|
|
}
|
|
|
|
get baseScale() {
|
|
if (this.aspectRatio.ar > this.viewportSize.w / this.viewportSize.h) {
|
|
// Photo is clamped to width
|
|
return this.viewportSize.w / this.size.w;
|
|
} else {
|
|
// Photo is clamped to height
|
|
return this.viewportSize.h / this.size.h;
|
|
}
|
|
}
|
|
|
|
get viewportSize() {
|
|
return {
|
|
w: this.ele.offsetWidth,
|
|
h: this.ele.offsetHeight,
|
|
};
|
|
}
|
|
|
|
get aspectRatio() {
|
|
return {
|
|
ar: this.size.w / this.size.h,
|
|
};
|
|
}
|
|
|
|
get size() {
|
|
if (this.photoEle.naturalWidth > 0) {
|
|
return {
|
|
w: this.photoEle.naturalWidth,
|
|
h: this.photoEle.naturalHeight,
|
|
};
|
|
} else {
|
|
return {
|
|
w: this.preloadEle.naturalWidth,
|
|
h: this.preloadEle.naturalHeight,
|
|
};
|
|
}
|
|
}
|
|
|
|
open(id) {
|
|
this.setPreload(id);
|
|
this.setPhoto(id);
|
|
|
|
this.show();
|
|
}
|
|
|
|
close() {
|
|
this.setPreload(null);
|
|
this.setPhoto(null);
|
|
|
|
this.hide();
|
|
}
|
|
|
|
show() {
|
|
this.showing = true;
|
|
|
|
// Capture scroll position
|
|
this.preservedScroll.x = window.scrollX;
|
|
this.preservedScroll.y = window.scrollY;
|
|
|
|
this.ele.style.display = 'block';
|
|
this.ele.style.pointerEvents = 'all';
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
if (this.openCloseTimeout) {
|
|
clearTimeout(this.openCloseTimeout);
|
|
}
|
|
this.openCloseTimeout = setTimeout(() => {
|
|
this.ele.style.opacity = '1.0';
|
|
}, 100);
|
|
}
|
|
|
|
hide() {
|
|
this.showing = false;
|
|
|
|
// Restore scroll position
|
|
window.scrollTo(this.preservedScroll.x, this.preservedScroll.y);
|
|
|
|
this.ele.style.opacity = '0';
|
|
this.ele.style.pointerEvents = 'none';
|
|
document.body.style.overflow = 'visible';
|
|
|
|
if (this.openCloseTimeout) {
|
|
clearTimeout(this.openCloseTimeout);
|
|
}
|
|
this.openCloseTimeout = setTimeout(() => {
|
|
this.ele.style.pointerEvents = 'all';
|
|
this.ele.style.display = 'none';
|
|
}, 1000);
|
|
}
|
|
|
|
register(id, ele) {
|
|
// Simple solution
|
|
//ele.href = this.prefix + id;
|
|
// Semantic preservation solution
|
|
ele.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = this.prefix + id;
|
|
}, false);
|
|
}
|
|
|
|
registerPreload(id, ele) {
|
|
this.preloadEles[id] = ele;
|
|
}
|
|
}
|