Add enlarged photo view
parent
b6613ea168
commit
a9a55c3ef9
|
@ -1,13 +1,21 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Faustina:wght@400;550&display=swap');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Units
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--spacing: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Typography
|
* Typography
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Faustina:wght@400;550&display=swap');
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: 'Faustina', serif;
|
font-family: 'Faustina', serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -19,6 +27,9 @@ body {
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-weight: 550;
|
font-weight: 550;
|
||||||
|
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: greyscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -60,7 +71,7 @@ h4, h5, h6 {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
nav ul {
|
nav ul {
|
||||||
padding: calc(0.7862rem / 2);
|
padding: var(--spacing);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
@ -69,12 +80,16 @@ nav ul {
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul li {
|
nav ul li {
|
||||||
margin: calc(0.7862rem / 2);
|
margin: var(--spacing);
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
font-size: 0.9em;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
padding: 1rem 0.7862rem;
|
padding: 1rem calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
.enlarge {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 100;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
transition: 0.5s opacity;
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enlarge-close, .enlarge-zoom-controls {
|
||||||
|
font-size: 0.9em;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enlarge-close {
|
||||||
|
padding: calc(var(--spacing) * 2);
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enlarge-zoom-controls {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enlarge-zoom-in,
|
||||||
|
.enlarge-zoom-out {
|
||||||
|
padding: calc(var(--spacing) * 2);
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enlarge-contents {
|
||||||
|
position: relative;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enlarge-photo {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.enlarge-preload {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
|
@ -1,25 +1,47 @@
|
||||||
|
:root {
|
||||||
|
--gallery-spacing: var(--spacing);
|
||||||
|
--gallery-base-height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
:root {
|
||||||
|
--gallery-base-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
:root {
|
||||||
|
--gallery-base-height: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
:root {
|
||||||
|
--gallery-base-height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
padding: calc(0.7862rem / 2);
|
padding: var(--gallery-spacing);
|
||||||
|
padding-right: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
margin-right: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
.gallery-item picture {
|
.gallery-item picture {
|
||||||
margin: calc(0.7862rem / 2);
|
margin: var(--gallery-spacing);
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-item img {
|
.gallery-item img {
|
||||||
display: block;
|
display: block;
|
||||||
height: 280px; /* 320px is native size */
|
height: calc(var(--gallery-base-height) * 1.2); /* 320px is native size on desktop, 160px is native size on mobile */
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
object-fit: cover;
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
|
||||||
.gallery-item img {
|
|
||||||
height: 160px; /* 160px is native size */
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.esm.browser.js';
|
||||||
|
|
|
@ -0,0 +1,337 @@
|
||||||
|
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.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', this.updateSize.bind(this), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindTouch() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bindDrag() {
|
||||||
|
this.ele.addEventListener('pointerdown', (e) => {
|
||||||
|
this.mouseDown = true;
|
||||||
|
this.contentsEle.style.cursor = 'grabbing';
|
||||||
|
}, false);
|
||||||
|
this.ele.addEventListener('pointermove', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.mouseDown) {
|
||||||
|
this.pan(e.movementX, e.movementY);
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
this.ele.addEventListener('pointerup', (e) => {
|
||||||
|
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() {
|
||||||
|
// 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() {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,18 @@
|
||||||
class LayoutEngine {
|
class LayoutEngine {
|
||||||
|
/*
|
||||||
rects = [];
|
rects = [];
|
||||||
|
|
||||||
gap = 6.28333 * 2;
|
gap = 12;
|
||||||
baseHeight = 200;
|
baseHeight = 100;
|
||||||
// maxHeight = 320;
|
// maxHeight = 320;
|
||||||
viewportWidth = 1024;
|
viewportWidth = 1024;
|
||||||
|
*/
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.rects = [];
|
||||||
|
this.gap = 12;
|
||||||
|
this.baseHeight = 100;
|
||||||
|
this.viewportWidth = 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register a new rectangle with an aspect ratio of ar at index
|
// Register a new rectangle with an aspect ratio of ar at index
|
||||||
|
@ -27,29 +32,34 @@ class LayoutEngine {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a list of heights for each rectangle
|
// Generate a list of dimensions for each rectangle
|
||||||
calculate() {
|
calculate() {
|
||||||
let heights = [];
|
let dimensions = [];
|
||||||
|
|
||||||
let currentWidth = this.gap;
|
let currentWidth = this.gap;
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
|
|
||||||
|
let lastHeight = this.baseHeight * 1.2;
|
||||||
|
|
||||||
// Behave like a browser: try to fit as many in a row with baseHeight
|
// Behave like a browser: try to fit as many in a row with baseHeight
|
||||||
for (let index = 0; index < this.rects.length; index++) {
|
for (let index = 0; index < this.rects.length; index++) {
|
||||||
// Get the next width to add
|
// Get the next width to add
|
||||||
const rectWidth = Math.ceil(this.baseHeight * this.rects[index].ar) + this.gap;
|
const rectWidth = (this.baseHeight * this.rects[index].ar) + this.gap;
|
||||||
|
|
||||||
// If the next width is too wide, resolve the current rectangles
|
// If the next width is too wide, resolve the current rectangles
|
||||||
console.debug(currentWidth, rectWidth);
|
|
||||||
if (currentWidth + rectWidth > this.viewportWidth) {
|
if (currentWidth + rectWidth > this.viewportWidth) {
|
||||||
const gapTotal = this.gap * (index - currentIndex + 1);
|
const gapTotal = this.gap * (index - currentIndex + 1);
|
||||||
const widthScale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal);
|
const scale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal);
|
||||||
const heightScale = widthScale;
|
|
||||||
|
|
||||||
|
const rectHeight = this.baseHeight * scale;
|
||||||
|
lastHeight = rectHeight;
|
||||||
|
|
||||||
// Scale up every previous rectangle
|
// Scale up every previous rectangle
|
||||||
const rectHeight = this.baseHeight * widthScale;
|
|
||||||
for (; currentIndex < index; currentIndex++) {
|
for (; currentIndex < index; currentIndex++) {
|
||||||
heights.push(rectHeight);
|
dimensions.push({
|
||||||
|
h: rectHeight,
|
||||||
|
w: rectHeight * this.rects[currentIndex].ar,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
currentWidth = this.gap;
|
currentWidth = this.gap;
|
||||||
}
|
}
|
||||||
|
@ -59,17 +69,20 @@ class LayoutEngine {
|
||||||
|
|
||||||
// Set remainder to a decent height
|
// Set remainder to a decent height
|
||||||
for (; currentIndex < this.rects.length; currentIndex++) {
|
for (; currentIndex < this.rects.length; currentIndex++) {
|
||||||
heights.push(this.baseHeight)
|
dimensions.push({
|
||||||
|
h: lastHeight,
|
||||||
|
w: lastHeight * this.rects[currentIndex].ar,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return heights;
|
return dimensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Code for a future implementation that emits events
|
* Code for a future implementation that emits events
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
console.debug("updating layout");
|
console.debug('updating layout');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,44 +104,69 @@ export default class Gallery {
|
||||||
this.ele = ele;
|
this.ele = ele;
|
||||||
this.layoutEngine = new LayoutEngine();
|
this.layoutEngine = new LayoutEngine();
|
||||||
|
|
||||||
|
this.grabGap();
|
||||||
|
this.grabBaseHeight();
|
||||||
|
this.grabViewportWidth();
|
||||||
|
|
||||||
this.registerViewport();
|
this.registerViewport();
|
||||||
this.registerInitial();
|
this.registerInitial();
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract gap values
|
||||||
|
grabGap() {
|
||||||
|
// Simple implementation to guess from padding values
|
||||||
|
const px = window.getComputedStyle(this.ele).getPropertyValue('padding-left').replace('px', '');
|
||||||
|
this.layoutEngine.gap = parseInt(px) * 2 || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract baseHeight
|
||||||
|
grabBaseHeight() {
|
||||||
|
const px = window.getComputedStyle(this.ele).getPropertyValue('--gallery-base-height').replace('px', '');
|
||||||
|
this.layoutEngine.baseHeight = parseInt(px) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract viewport width
|
||||||
|
grabViewportWidth() {
|
||||||
|
this.layoutEngine.viewportWidth = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
// Inform our engine of viewport width
|
// Inform our engine of viewport width
|
||||||
registerViewport() {
|
registerViewport() {
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (this.layoutEngine.viewportWidth == window.innerWidth) {
|
if (this.layoutEngine.viewportWidth == window.innerWidth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.layoutEngine.viewportWidth = window.innerWidth;
|
this.grabBaseHeight();
|
||||||
|
this.grabViewportWidth();
|
||||||
//this.layoutEngine.scheduleUpdate();
|
//this.layoutEngine.scheduleUpdate();
|
||||||
|
this.draw();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register initial elements
|
// Register initial elements
|
||||||
registerInitial() {
|
registerInitial() {
|
||||||
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
||||||
galleryItemEles.forEach(ele => {
|
for (const ele of galleryItemEles) {
|
||||||
const ar = ele.dataset.ar;
|
const ar = ele.dataset.ar;
|
||||||
this.layoutEngine.insert(ar);
|
this.layoutEngine.insert(ar);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate heights and draw them
|
// Calculate dimensions and draw them
|
||||||
draw() {
|
draw() {
|
||||||
const heights = this.layoutEngine.calculate();
|
const dimensions = this.layoutEngine.calculate();
|
||||||
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
||||||
galleryItemEles.forEach((ele, index) => {
|
for (const [index, ele] of galleryItemEles.entries()) {
|
||||||
const height = heights[index];
|
const dimension = dimensions[index];
|
||||||
if (!height) {
|
if (!dimension) {
|
||||||
console.error(`Missing height for element at ${index}`);
|
console.error(`Missing dimensions for element at ${index}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ele.querySelectorAll("img").forEach(ele => {
|
for (const imgEle of ele.querySelectorAll('img')) {
|
||||||
ele.style.height = `${height}px`;
|
imgEle.style.height = `${dimension.h}px`;
|
||||||
});
|
imgEle.style.width = `${dimension.w}px`;
|
||||||
});
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
import Gallery from './gallery.js';
|
import Gallery from './gallery.js';
|
||||||
|
import Enlarge from './enlarge.js';
|
||||||
|
|
||||||
// Bind elements
|
// Bind elements
|
||||||
|
|
||||||
const galleryEle = document.querySelector("main.gallery");
|
const galleryEle = document.querySelector('main.gallery');
|
||||||
const gallery = new Gallery(galleryEle);
|
const gallery = new Gallery(galleryEle);
|
||||||
|
|
||||||
|
const enlargeEles = document.querySelectorAll('[data-enlarge]');
|
||||||
|
const enlarge = new Enlarge(document.body);
|
||||||
|
for (const ele of enlargeEles) {
|
||||||
|
enlarge.register(ele.dataset.enlarge, ele);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enlargePreloadEles = document.querySelectorAll('[data-enlarge-preload-for]');
|
||||||
|
for (const ele of enlargePreloadEles) {
|
||||||
|
enlarge.registerPreload(ele.dataset.enlargePreloadFor, ele);
|
||||||
|
}
|
||||||
|
|
||||||
|
enlarge.watchHash();
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
|
|
||||||
window.g = gallery;
|
window.g = gallery;
|
||||||
|
window.e = enlarge;
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<title>Photos</title>
|
<title>Photos</title>
|
||||||
<link rel="stylesheet" href="https://local1:2020/css/base.css">
|
<link rel="stylesheet" href="https://local1:2020/css/base.css">
|
||||||
<link rel="stylesheet" href="https://local1:2020/css/gallery.css">
|
<link rel="stylesheet" href="https://local1:2020/css/gallery.css">
|
||||||
|
<link rel="stylesheet" href="https://local1:2020/css/enlarge.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
<main class="gallery">
|
<main class="gallery">
|
||||||
{{ range (datasource "samples") }}
|
{{ range (datasource "samples") }}
|
||||||
<div class="gallery-item preloaded-thumbnail" data-ar="{{ div (index . 1) (index . 2) }}">
|
<div class="gallery-item preloaded-thumbnail" data-ar="{{ div (index . 1) (index . 2) }}">
|
||||||
|
<a href="sample/{{ index . 0 }}" data-enlarge="sample/{{ index . 0 }}">
|
||||||
<picture>
|
<picture>
|
||||||
<source srcset="
|
<source srcset="
|
||||||
sample/{{ index . 0 | replaceAll ".jpg" "_320.webp"}} 2x,
|
sample/{{ index . 0 | replaceAll ".jpg" "_320.webp"}} 2x,
|
||||||
|
@ -41,7 +43,8 @@
|
||||||
alt=""
|
alt=""
|
||||||
width="{{ div (index . 1) (index . 2) | mul 320 }}"
|
width="{{ div (index . 1) (index . 2) | mul 320 }}"
|
||||||
height="320"
|
height="320"
|
||||||
loading="lazy">
|
loading="lazy"
|
||||||
|
data-enlarge-preload-for="sample/{{ index . 0 }}">
|
||||||
</picture>
|
</picture>
|
||||||
<picture class="preloaded-thumbnail-image">
|
<picture class="preloaded-thumbnail-image">
|
||||||
<source srcset="
|
<source srcset="
|
||||||
|
@ -66,6 +69,7 @@
|
||||||
height="320"
|
height="320"
|
||||||
>
|
>
|
||||||
</picture>
|
</picture>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -15,17 +15,20 @@ done
|
||||||
for size in ${presizes[*]}; do
|
for size in ${presizes[*]}; do
|
||||||
for f in sample/*unsplash.jpg; do
|
for f in sample/*unsplash.jpg; do
|
||||||
echo "Compressing thumbnail $f at $size"
|
echo "Compressing thumbnail $f at $size"
|
||||||
gm convert $f -resize x$size -compress jpeg -quality 60 ${f%.jpg}_pre$size.jpg
|
if [ -f "${f%.jpg}_pre$size.jpg" ]; then
|
||||||
gm convert $f -resize x$size -compress webp -quality 40 ${f%.jpg}_pre$size.webp
|
continue
|
||||||
|
fi
|
||||||
|
gm convert $f -resize x$size -compress jpeg -quality 40 ${f%.jpg}_pre$size.jpg
|
||||||
|
gm convert $f -resize x$size -compress webp -quality 35 ${f%.jpg}_pre$size.webp
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
for size in ${sizes[*]}; do
|
for size in ${sizes[*]}; do
|
||||||
for f in sample/*unsplash.jpg; do
|
for f in sample/*unsplash.jpg; do
|
||||||
echo "Compressing $f at $size"
|
echo "Compressing $f at $size"
|
||||||
#if [ -f "${f%.jpg}_$size.jpg" ]; then
|
if [ -f "${f%.jpg}_$size.jpg" ]; then
|
||||||
# continue
|
continue
|
||||||
#fi
|
fi
|
||||||
gm convert $f -resize x$size -compress jpeg -quality 70 ${f%.jpg}_$size.jpg
|
gm convert $f -resize x$size -compress jpeg -quality 75 ${f%.jpg}_$size.jpg
|
||||||
gm convert $f -resize x$size -compress webp -quality 65 ${f%.jpg}_$size.webp
|
gm convert $f -resize x$size -compress webp -quality 70 ${f%.jpg}_$size.webp
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
Loading…
Reference in New Issue