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 currentIndex = 0; let lastHeight = this.baseHeight * 1.2; // Behave like a browser: try to fit as many in a row with baseHeight for (let index = 0; index < this.rects.length; index++) { // Get the next width to add const rectWidth = (this.baseHeight * this.rects[index].ar) + this.gap; // If the next width is too wide, resolve the current rectangles if (currentWidth + rectWidth > this.viewportWidth) { const gapTotal = this.gap * (index - currentIndex + 1); const scale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal); const rectHeight = this.baseHeight * scale; lastHeight = rectHeight; // Scale up every previous rectangle for (; currentIndex < index; currentIndex++) { dimensions.push({ h: rectHeight, w: rectHeight * this.rects[currentIndex].ar, }) } currentWidth = this.gap; } currentWidth += rectWidth; } // Set remainder to a decent height for (; currentIndex < this.rects.length; currentIndex++) { dimensions.push({ h: lastHeight, w: lastHeight * this.rects[currentIndex].ar, }) } 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 = window.innerWidth; } // Inform our engine of viewport width registerViewport() { window.addEventListener('resize', () => { if (this.layoutEngine.viewportWidth == window.innerWidth) { return; } 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 = `${dimension.h}px`; imgEle.style.width = `${dimension.w}px`; } } } }