From b6613ea168b82777cd788cc4eee5190a8bb32297 Mon Sep 17 00:00:00 2001 From: Ambrose Chua Date: Sun, 24 May 2020 20:37:14 +0800 Subject: [PATCH] Initial gallery view --- web/Caddyfile | 12 ++++ web/shared/css/base.css | 80 ++++++++++++++++++++++ web/shared/css/gallery.css | 25 +++++++ web/shared/js/gallery.js | 134 +++++++++++++++++++++++++++++++++++++ web/shared/js/view.js | 10 +++ web/view/index.tmpl | 76 +++++++++++++++++++++ web/view/sample.sh | 31 +++++++++ web/view/tmpl.sh | 10 +++ 8 files changed, 378 insertions(+) create mode 100644 web/Caddyfile create mode 100644 web/shared/css/base.css create mode 100644 web/shared/css/gallery.css create mode 100644 web/shared/js/gallery.js create mode 100644 web/shared/js/view.js create mode 100644 web/view/index.tmpl create mode 100755 web/view/sample.sh create mode 100755 web/view/tmpl.sh diff --git a/web/Caddyfile b/web/Caddyfile new file mode 100644 index 0000000..180efe3 --- /dev/null +++ b/web/Caddyfile @@ -0,0 +1,12 @@ +local1:2020 { + root * ./shared + file_server browse + header Access-Control-Allow-Origin "*" + tls internal +} + +local2:2020 { + root * ./view + file_server browse + tls internal +} diff --git a/web/shared/css/base.css b/web/shared/css/base.css new file mode 100644 index 0000000..4da0ed4 --- /dev/null +++ b/web/shared/css/base.css @@ -0,0 +1,80 @@ + +/* + * Typography + */ + +@import url('https://fonts.googleapis.com/css2?family=Faustina:wght@400;550&display=swap'); + +html { + font-family: 'Faustina', serif; + font-weight: 400; +} + +body { + margin: 0rem; + + font-size: 1em; /* 16pt */ + line-height: 1.272; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 550; +} + +h1 { + font-size: 2.0581em; +} + +h2 { + font-size: 1.618em; +} + +h3 { + font-size: 1.272em; +} + +h4, h5, h6 { + font-size: 1.272em; +} + +/* + * Molecules + */ + +.preloaded-thumbnail { + display: block; + position: relative; +} +.preloaded-thumbnail picture:not(.preloaded-thumbnail-image) { + position: absolute; + top: 0; + left: 0; + z-index: 1; +} +.preloaded-thumbnail-image img { + object-fit: cover; +} + +/* + * Organisms + */ + +nav ul { + padding: calc(0.7862rem / 2); + margin: 0; + + list-style: none; + display: flex; + justify-content: flex-end; +} + +nav ul li { + margin: calc(0.7862rem / 2); + + display: block; +} + +header { + padding: 1rem 0.7862rem; +} + diff --git a/web/shared/css/gallery.css b/web/shared/css/gallery.css new file mode 100644 index 0000000..353a364 --- /dev/null +++ b/web/shared/css/gallery.css @@ -0,0 +1,25 @@ +.gallery { + padding: calc(0.7862rem / 2); + + display: flex; + flex-wrap: wrap; +} + +.gallery-item picture { + margin: calc(0.7862rem / 2); + + display: block; +} + +.gallery-item img { + display: block; + height: 280px; /* 320px is native size */ + width: auto; +} + +@media screen and (max-width: 900px) { + .gallery-item img { + height: 160px; /* 160px is native size */ + width: auto; + } +} diff --git a/web/shared/js/gallery.js b/web/shared/js/gallery.js new file mode 100644 index 0000000..ecde1a3 --- /dev/null +++ b/web/shared/js/gallery.js @@ -0,0 +1,134 @@ +class LayoutEngine { + rects = []; + + gap = 6.28333 * 2; + baseHeight = 200; + // maxHeight = 320; + viewportWidth = 1024; + + constructor() { + + } + + // 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 heights for each rectangle + calculate() { + let heights = []; + + let currentWidth = this.gap; + let currentIndex = 0; + + // 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 = Math.ceil(this.baseHeight * this.rects[index].ar) + this.gap; + + // If the next width is too wide, resolve the current rectangles + console.debug(currentWidth, rectWidth); + if (currentWidth + rectWidth > this.viewportWidth) { + const gapTotal = this.gap * (index - currentIndex + 1); + const widthScale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal); + const heightScale = widthScale; + + // Scale up every previous rectangle + const rectHeight = this.baseHeight * widthScale; + for (; currentIndex < index; currentIndex++) { + heights.push(rectHeight); + } + currentWidth = this.gap; + } + + currentWidth += rectWidth; + } + + // Set remainder to a decent height + for (; currentIndex < this.rects.length; currentIndex++) { + heights.push(this.baseHeight) + } + + return heights; + } + + /* + * 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.registerViewport(); + this.registerInitial(); + this.draw(); + } + + // Inform our engine of viewport width + registerViewport() { + window.addEventListener('resize', () => { + if (this.layoutEngine.viewportWidth == window.innerWidth) { + return; + } + this.layoutEngine.viewportWidth = window.innerWidth; + //this.layoutEngine.scheduleUpdate(); + }); + } + + // Register initial elements + registerInitial() { + const galleryItemEles = this.ele.querySelectorAll('.gallery-item'); + galleryItemEles.forEach(ele => { + const ar = ele.dataset.ar; + this.layoutEngine.insert(ar); + }); + } + + // Calculate heights and draw them + draw() { + const heights = this.layoutEngine.calculate(); + const galleryItemEles = this.ele.querySelectorAll('.gallery-item'); + galleryItemEles.forEach((ele, index) => { + const height = heights[index]; + if (!height) { + console.error(`Missing height for element at ${index}`); + return; + } + ele.querySelectorAll("img").forEach(ele => { + ele.style.height = `${height}px`; + }); + }); + } +} diff --git a/web/shared/js/view.js b/web/shared/js/view.js new file mode 100644 index 0000000..417f91e --- /dev/null +++ b/web/shared/js/view.js @@ -0,0 +1,10 @@ +import Gallery from './gallery.js'; + +// Bind elements + +const galleryEle = document.querySelector("main.gallery"); +const gallery = new Gallery(galleryEle); + +// Debug + +window.g = gallery; diff --git a/web/view/index.tmpl b/web/view/index.tmpl new file mode 100644 index 0000000..9bc10ce --- /dev/null +++ b/web/view/index.tmpl @@ -0,0 +1,76 @@ + + + + + + Photos + + + + + +
+

James Birthday 2020

+
+
+ {{ range (datasource "samples") }} + + {{ end }} +
+ + + + + diff --git a/web/view/sample.sh b/web/view/sample.sh new file mode 100755 index 0000000..deefda1 --- /dev/null +++ b/web/view/sample.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +set -e + +sizes=(640 320 160 80) +presizes=(120 60 30) + +rm samples.csv + +echo "Getting file dimensions..." +for f in sample/*unsplash.jpg; do + gm identify -format %f,%w,%h $f >> samples.csv +done + +for size in ${presizes[*]}; do + for f in sample/*unsplash.jpg; do + echo "Compressing thumbnail $f at $size" + gm convert $f -resize x$size -compress jpeg -quality 60 ${f%.jpg}_pre$size.jpg + gm convert $f -resize x$size -compress webp -quality 40 ${f%.jpg}_pre$size.webp + done +done +for size in ${sizes[*]}; do + for f in sample/*unsplash.jpg; do + echo "Compressing $f at $size" + #if [ -f "${f%.jpg}_$size.jpg" ]; then + # continue + #fi + gm convert $f -resize x$size -compress jpeg -quality 70 ${f%.jpg}_$size.jpg + gm convert $f -resize x$size -compress webp -quality 65 ${f%.jpg}_$size.webp + done +done diff --git a/web/view/tmpl.sh b/web/view/tmpl.sh new file mode 100755 index 0000000..a221db9 --- /dev/null +++ b/web/view/tmpl.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +while true; do + gotemplate -f index.tmpl -d samples.csv -o index.html 2>&1 > /dev/null + date + sleep 5 +done +