1
0
Fork 0
photos/web/shared/js/enlarge.js

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;
}
}