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

182 lines
4.3 KiB
JavaScript

class LayoutEngine {
/*
rects = [];
gap = 12;
baseHeight = 100;
// maxHeight = 320;
viewportWidth = 1024;
*/
constructor() {
this.rects = [];
this.gap = 12;
this.baseHeight = 100;
this.viewportWidth = 1024;
}
// Register a new rectangle with an aspect ratio of ar at index
insert(ar=1, index=-1) {
const rect = {
ar: ar,
};
if (index == -1) {
this.rects.push(rect);
} else {
this.rects.splice(index, 0, rect);
}
}
// Unregister a new rectangle at index
pop(index=-1) {
// TODO
}
// Generate a list of dimensions for each rectangle
calculate() {
let dimensions = [];
let currentWidth = this.gap;
let startIndex = 0;
// Behave like a browser: try to fit as many in a row with baseHeight
for (let index = 0; index < this.rects.length; index++) {
// Add this rectangle width
const currentRectWidth = (this.baseHeight * this.rects[index].ar);
currentWidth += currentRectWidth + this.gap;
if (currentWidth > this.viewportWidth) {
console.error("TODO: fix cases where viewport smaller than image");
break;
}
// Get the next width to add
const hasNextRect = index + 1 < this.rects.length;
const nextRectWidth = hasNextRect ? (this.baseHeight * this.rects[index+1].ar) : 0;
const nextRectOverflow = currentWidth + nextRectWidth + this.gap > this.viewportWidth;
// If the next width is too wide, resolve the current rectangles
if (!hasNextRect || nextRectOverflow) {
const gapTotal = this.gap * (index - startIndex + 2);
let scale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal);
if (!hasNextRect) {
scale = Math.min(1.8, scale);
}
const rectHeight = this.baseHeight * scale;
// Scale up every previous rectangle
for (; startIndex <= index; startIndex++) {
dimensions.push({
h: rectHeight,
w: rectHeight * this.rects[startIndex].ar,
})
}
currentWidth = this.gap;
}
}
return dimensions;
}
/*
* Code for a future implementation that emits events
update() {
console.debug('updating layout');
}
// Schedule an update at maximum 10Hz
scheduleUpdate() {
if (this.scheduledUpdate) {
return;
}
this.scheduledUpdate = setTimeout(() => {
this.scheduledUpdate = null;
this.update();
}, 100);
}
*/
}
export default class Gallery {
constructor(ele) {
this.ele = ele;
this.layoutEngine = new LayoutEngine();
this.grabGap();
this.grabBaseHeight();
this.grabViewportWidth();
this.registerViewport();
this.registerInitial();
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 = this.ele.clientWidth;
}
// Inform our engine of viewport width
registerViewport() {
window.addEventListener('resize', () => {
if (this.layoutEngine.viewportWidth == window.innerWidth) {
return;
}
this.recompute();
});
window.addEventListener('load', () => {
this.recompute();
});
setInterval(this.recompute.bind(this), 500);
}
recompute() {
this.grabGap();
this.grabBaseHeight();
this.grabViewportWidth();
//this.layoutEngine.scheduleUpdate();
this.draw();
}
// Register initial elements
registerInitial() {
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
for (const ele of galleryItemEles) {
const ar = ele.dataset.ar;
this.layoutEngine.insert(ar);
}
}
// Calculate dimensions and draw them
draw() {
const dimensions = this.layoutEngine.calculate();
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
for (const [index, ele] of galleryItemEles.entries()) {
const dimension = dimensions[index];
if (!dimension) {
console.error(`Missing dimensions for element at ${index}`);
return;
}
for (const imgEle of ele.querySelectorAll('img')) {
imgEle.style.height = `${Math.floor(dimension.h)}px`;
imgEle.style.width = `${dimension.w}px`;
}
}
}
}