1
0
Fork 0

Initial gallery view

main
Ambrose Chua 2020-05-24 20:37:14 +08:00
parent a2086dc3b1
commit b6613ea168
Signed by: ambrose
GPG Key ID: BC367D33F140B5C2
8 changed files with 378 additions and 0 deletions

12
web/Caddyfile Normal file
View File

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

80
web/shared/css/base.css Normal file
View File

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

View File

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

134
web/shared/js/gallery.js Normal file
View File

@ -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`;
});
});
}
}

10
web/shared/js/view.js Normal file
View File

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

76
web/view/index.tmpl Normal file
View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photos</title>
<link rel="stylesheet" href="https://local1:2020/css/base.css">
<link rel="stylesheet" href="https://local1:2020/css/gallery.css">
</head>
<body>
<nav>
<ul>
<li><a href="#download">Download</a></li>
<li><a href="manage">Manage</a></li>
</ul>
</nav>
<header>
<h1>James Birthday 2020</h1>
</header>
<main class="gallery">
{{ range (datasource "samples") }}
<div class="gallery-item preloaded-thumbnail" data-ar="{{ div (index . 1) (index . 2) }}">
<picture>
<source srcset="
sample/{{ index . 0 | replaceAll ".jpg" "_320.webp"}} 2x,
sample/{{ index . 0 | replaceAll ".jpg" "_160.webp"}} 1x,
" type="image/webp" media="(max-width: 900px)">
<source srcset="
sample/{{ index . 0 | replaceAll ".jpg" "_640.webp"}} 2x,
sample/{{ index . 0 | replaceAll ".jpg" "_320.webp"}} 1x,
" type="image/webp">
<source srcset="
sample/{{ index . 0 | replaceAll ".jpg" "_320.jpg"}} 2x,
sample/{{ index . 0 | replaceAll ".jpg" "_160.jpg"}} 1x,
" media="(max-width: 900px)">
<source srcset="
sample/{{ index . 0 | replaceAll ".jpg" "_640.jpg"}} 2x,
sample/{{ index . 0 | replaceAll ".jpg" "_320.jpg"}} 1x,
">
<img src="sample/{{ index . 0 | replaceAll ".jpg" "_320.jpg"}}"
alt=""
width="{{ div (index . 1) (index . 2) | mul 320 }}"
height="320"
loading="lazy">
</picture>
<picture class="preloaded-thumbnail-image">
<source srcset="
sample/{{ index . 0 | replaceAll ".jpg" "_pre60.webp"}} 2x,
sample/{{ index . 0 | replaceAll ".jpg" "_pre30.webp"}} 1x,
" type="image/webp" media="(max-width: 900px)">
<source srcset="
sample/{{ index . 0 | replaceAll ".jpg" "_pre120.webp"}} 2x,
sample/{{ index . 0 | replaceAll ".jpg" "_pre60.webp"}} 1x,
" type="image/webp">
<source srcset="
sample/{{ index . 0 | replaceAll ".jpg" "_pre60.jpg"}} 2x,
sample/{{ index . 0 | replaceAll ".jpg" "_pre30.jpg"}} 1x,
" media="(max-width: 900px)">
<source srcset="
sample/{{ index . 0 | replaceAll ".jpg" "_pre120.jpg"}} 2x,
sample/{{ index . 0 | replaceAll ".jpg" "_pre60.jpg"}} 1x,
">
<img src="sample/{{ index . 0 | replaceAll ".jpg" "_pre30.jpg"}}"
alt=""
width="{{ div (index . 1) (index . 2) | mul 320 }}"
height="320"
>
</picture>
</div>
{{ end }}
</main>
<!-- This gallery works without JavaScript! -->
<script type="module" src="https://local1:2020/js/view.js" async></script>
</body>
</html>
<!-- vim: set ft=html: -->

31
web/view/sample.sh Executable file
View File

@ -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

10
web/view/tmpl.sh Executable file
View File

@ -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