Initial gallery view
parent
a2086dc3b1
commit
b6613ea168
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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`;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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: -->
|
|
@ -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
|
|
@ -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
|
||||
|
Loading…
Reference in New Issue