Initial
20
themes/gallerydeluxe/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
.DS_Store
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
|
||||
public/
|
||||
.hugo_build.lock
|
||||
resources/
|
||||
jsconfig.json
|
||||
21
themes/gallerydeluxe/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Bjørn Erik Pedersen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
48
themes/gallerydeluxe/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
>**Note 1:** See this [Gallery Deluxe Starter](https://github.com/bep/gallerydeluxe_starter) for a fast route to get this up and running.
|
||||
|
||||
>**Note 2:** If you need _multiple_ galleries, see [Galleries Deluxe](https://github.com/bep/galleriesdeluxe).
|
||||
|
||||
<hr/>
|
||||
|
||||
* [Configuration](#configuration)
|
||||
* [Credits](#credits)
|
||||
|
||||
A Hugo Module to show a photo gallery. It's very fast/effective, especially if you have lots of images on display.
|
||||
|
||||
This theme is what you see on [staticbattery.com](https://staticbattery.com/) which, at the time of writing this, [scores 100](https://pagespeed.web.dev/report?url=https%3A%2F%2Fstaticbattery.com%2F&form_factor=mobile) at Google PageSpeed for both mobile and desktop.
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/bep/gallerydeluxe/main/images/tn.jpg">](https://staticbattery.com/)
|
||||
|
||||
## Configuration
|
||||
|
||||
The recommended way to add this to your site is to include it as a Hugo Module. See [Gallery Deluxe Starter](https://github.com/bep/gallerydeluxe_starter) for a starter template. Another example is [staticbattery.com](https://github.com/bep/staticbattery.com).
|
||||
|
||||
|
||||
### Params
|
||||
|
||||
```toml
|
||||
[params]
|
||||
[params.gallerydeluxe]
|
||||
# Shuffle the images in the gallery to give the impression of a new gallery each time.
|
||||
shuffle = false
|
||||
|
||||
# Reverse the order of the images in the gallery.
|
||||
reverse = false
|
||||
|
||||
# Enable Exif data in the gallery.
|
||||
# See https://gohugo.io/content-management/image-processing/#exif-data for how to filter tags.
|
||||
enable_exif = false
|
||||
|
||||
# Optional watermark file for the large images.
|
||||
[params.gallerydeluxe.watermark]
|
||||
image = "images/gopher-hero8.png" # relative to /assets
|
||||
posx = "left" # one of "left", "center", "right"
|
||||
posy = "bottom" # one of "top", "center", "bottom"
|
||||
```
|
||||
|
||||
If you want full control over how your images gets created, create and adjust a copy of [layouts/partials/gallerydeluxe/create-thumbs.html](layouts/partials/gallerydeluxe/create-thumbs.html) into your own project.
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
Credit to Dan Schlosser for the [Pig](https://github.com/schlosser/pig.js) JS library.
|
||||
121
themes/gallerydeluxe/assets/css/gallerydeluxe/styles.css
Normal file
@@ -0,0 +1,121 @@
|
||||
.gd-figure:hover {
|
||||
filter: brightness(1.25);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gd-modal-target {
|
||||
width: 300px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.gd-modal-target:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.gd-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgb(0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.gd-modal-content-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Modal Content (image) */
|
||||
.gd-modal-content {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.gd-modal-content.gd-modal-thumbnail {
|
||||
backdrop-filter: blur(5px);
|
||||
transition: opacity 0.75s ease;
|
||||
z-index: 10;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gd-modal-content.gd-modal-loaded.gd-modal-thumbnail {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gd-modal-exif {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
color: #fff;
|
||||
background-color: #8a8a8a;
|
||||
padding: 0.75rem;
|
||||
opacity: 0.75;
|
||||
filter: drop-shadow(-1px -1px 2px #ccc);
|
||||
}
|
||||
|
||||
.gd-modal-exif-ontimeout {
|
||||
transition: opacity 2s ease-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gd-modal-exif:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gd-modal-exif dl dd {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gd-modal-exif dl dd:after {
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.gd-modal-exif dl dt {
|
||||
display: inline-block;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.gd-modal-close {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 0px;
|
||||
right: 10px;
|
||||
color: #8a8a8a;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 800px) {
|
||||
.gd-modal-close {
|
||||
font-size: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
.gd-modal-close:hover,
|
||||
.gd-modal-close:focus {
|
||||
color: #bbb;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import GalleryDeluxe from '../src/index.js';
|
||||
|
||||
GalleryDeluxe.init();
|
||||
127
themes/gallerydeluxe/assets/js/gallerydeluxe/src/helpers.js
Normal file
@@ -0,0 +1,127 @@
|
||||
'use strict';
|
||||
|
||||
export function newSwiper(el, callback) {
|
||||
const debug = 0 ? console.log.bind(console, '[swiper]') : function () {};
|
||||
|
||||
const simulateTwoFingers = false;
|
||||
|
||||
// Number of pixels between touch start/end for us to consider it a swipe.
|
||||
const moveThreshold = 50;
|
||||
|
||||
const fingerDistance = (touches) => {
|
||||
return Math.hypot(touches[0].pageX - touches[1].pageX, touches[0].pageY - touches[1].pageY);
|
||||
};
|
||||
|
||||
var touch = {
|
||||
touchstart: { x: -1, y: -1, x2: -1, y2: -1, d: -1 },
|
||||
touchmove: { x: -1, y: -1, x2: -1, y2: -1, d: -1 },
|
||||
multitouch: false,
|
||||
};
|
||||
|
||||
// direction returns right, left, up or down or empty if below moveThreshold.
|
||||
touch.direction = function () {
|
||||
if (this.touchmove.x == -1) {
|
||||
// A regular click.
|
||||
return '';
|
||||
}
|
||||
|
||||
let distancex = this.touchmove.x - this.touchstart.x;
|
||||
if (Math.abs(distancex) < moveThreshold) {
|
||||
let distancey = this.touchmove.y - this.touchstart.y;
|
||||
if (Math.abs(distancey) < moveThreshold) {
|
||||
return '';
|
||||
}
|
||||
return distancey > 0 ? 'down' : 'up';
|
||||
}
|
||||
return distancex > 0 ? 'right' : 'left';
|
||||
};
|
||||
|
||||
touch.reset = function () {
|
||||
(this.touchstart.x = -1), (this.touchstart.y = -1);
|
||||
(this.touchmove.x = -1), (this.touchmove.y = -1);
|
||||
(this.touchstart.d = -1), (this.touchmove.d = -1);
|
||||
(this.touchstart.x2 = -1), (this.touchstart.y2 = -1);
|
||||
(this.touchmove.x2 = -1), (this.touchmove.y2 = -1);
|
||||
this.multitouch = false;
|
||||
};
|
||||
|
||||
touch.update = function (event, touches) {
|
||||
this.multitouch = this.multitouch || touches.length > 1;
|
||||
if (touches.length > 1) {
|
||||
this[event.type].d = fingerDistance(touches);
|
||||
this[event.type].x2 = touches[1].pageX;
|
||||
this[event.type].y2 = touches[1].pageY;
|
||||
}
|
||||
this[event.type].x = touches[0].pageX;
|
||||
this[event.type].y = touches[0].pageY;
|
||||
};
|
||||
|
||||
const pinch = function (event, touches) {
|
||||
let scale = 1;
|
||||
if (touches.length === 2) {
|
||||
// Two fingers on screen.
|
||||
if (event.scale) {
|
||||
scale = event.scale;
|
||||
} else {
|
||||
scale = touch.touchmove.d / touch.touchstart.d;
|
||||
scale = Math.round(scale * 100) / 100;
|
||||
}
|
||||
if (scale < 1) {
|
||||
scale = 1;
|
||||
}
|
||||
let distancex =
|
||||
((touch.touchmove.x + touch.touchmove.x2) / 2 - (touch.touchstart.x + touch.touchstart.x2) / 2) * 2;
|
||||
let distancey =
|
||||
((touch.touchmove.y + touch.touchmove.y2) / 2 - (touch.touchstart.y + touch.touchstart.y2) / 2) * 2;
|
||||
|
||||
el.style.transform = `translate3d(${distancex}px, ${distancey}px, 0) scale(${scale})`;
|
||||
el.style.zIndex = 1000;
|
||||
} else {
|
||||
// One finger on screen.
|
||||
el.style.transform = '';
|
||||
el.style.zIndex = '';
|
||||
}
|
||||
};
|
||||
|
||||
var handleTouch = function (event) {
|
||||
debug('event', event.type);
|
||||
if (typeof event !== 'undefined' && typeof event.touches !== 'undefined') {
|
||||
let touches = event.touches;
|
||||
|
||||
if (simulateTwoFingers && touches.length === 1) {
|
||||
touches = [
|
||||
{ pageX: touches[0].pageX, pageY: touches[0].pageY },
|
||||
{ pageX: 0, pageY: 0 },
|
||||
];
|
||||
}
|
||||
switch (event.type) {
|
||||
case 'touchstart':
|
||||
touch.reset();
|
||||
touch.update(event, touches);
|
||||
break;
|
||||
case 'touchmove':
|
||||
touch.update(event, touches);
|
||||
pinch(event, touches);
|
||||
break;
|
||||
case 'touchend':
|
||||
el.style.transform = '';
|
||||
if (!touch.multitouch) {
|
||||
// Only consider a swipe if we have one finger on screen.
|
||||
let direction = touch.direction();
|
||||
debug('direction', direction);
|
||||
if (direction) {
|
||||
callback(direction);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener('touchstart', handleTouch, { passive: true });
|
||||
el.addEventListener('touchmove', handleTouch, { passive: true });
|
||||
el.addEventListener('touchend', handleTouch, { passive: true });
|
||||
}
|
||||
264
themes/gallerydeluxe/assets/js/gallerydeluxe/src/index.js
Normal file
@@ -0,0 +1,264 @@
|
||||
'use strict';
|
||||
|
||||
import * as params from '@params';
|
||||
import { Pig } from './pig';
|
||||
import { newSwiper } from './helpers';
|
||||
|
||||
var debug = 0 ? console.log.bind(console, '[gallery-deluxe]') : function () {};
|
||||
|
||||
let GalleryDeluxe = {
|
||||
init: async function () {
|
||||
// One gallery per page (for now).
|
||||
const galleryId = 'gallerydeluxe';
|
||||
const dataAttributeName = 'data-gd-image-data-url';
|
||||
const container = document.getElementById(galleryId);
|
||||
if (!container) {
|
||||
throw new Error(`No element with id ${galleryId} found.`);
|
||||
}
|
||||
const dataUrl = container.getAttribute(dataAttributeName);
|
||||
if (!dataUrl) {
|
||||
throw new Error(`No ${dataAttributeName} attribute found.`);
|
||||
}
|
||||
|
||||
// The image opened in the lightbox.
|
||||
let activeImage;
|
||||
let exifTimeoutId;
|
||||
|
||||
// Lightbox setup.
|
||||
const modal = document.getElementById('gd-modal');
|
||||
const modalClose = modal.querySelector('#gd-modal-close');
|
||||
|
||||
const preventDefault = function (e) {
|
||||
// For iphone.
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
let imageWrapper = document.createElement('div');
|
||||
imageWrapper.classList.add('gd-modal-content-wrapper');
|
||||
modal.insertBefore(imageWrapper, modal.firstChild);
|
||||
|
||||
const closeModal = (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
imageWrapper.removeEventListener('touchmove', preventDefault);
|
||||
imageWrapper.removeEventListener('gesturestart', preventDefault);
|
||||
|
||||
// Hide the modal.
|
||||
modal.style.display = 'none';
|
||||
// Enable scrolling.
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
|
||||
modalClose.addEventListener('click', function () {
|
||||
closeModal();
|
||||
});
|
||||
|
||||
const swipe = function (direction) {
|
||||
debug('swipe', direction);
|
||||
switch (direction) {
|
||||
case 'left':
|
||||
activeImage = activeImage.next;
|
||||
openActiveImage();
|
||||
break;
|
||||
case 'right':
|
||||
activeImage = activeImage.prev;
|
||||
openActiveImage();
|
||||
break;
|
||||
default:
|
||||
closeModal();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Add some basic swipe logic.
|
||||
newSwiper(imageWrapper, function (direction) {
|
||||
swipe(direction);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
swipe('right');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
swipe('left');
|
||||
break;
|
||||
case 'Escape':
|
||||
closeModal(e);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const openActiveImage = () => {
|
||||
imageWrapper.addEventListener('touchmove', preventDefault);
|
||||
imageWrapper.addEventListener('gesturestart', preventDefault);
|
||||
|
||||
const classLoaded = 'gd-modal-loaded';
|
||||
const classThumbnail = 'gd-modal-thumbnail';
|
||||
|
||||
// Prevent scrolling of the background.
|
||||
document.body.style.overflow = 'hidden';
|
||||
let oldEls = modal.querySelectorAll('.gd-modal-content');
|
||||
let oldElsRemoved = false;
|
||||
|
||||
// Delay the removal of the old elements to make sure we
|
||||
// have a image on screen before we remove the old one,
|
||||
// even on slower connections.
|
||||
const removeOldEls = () => {
|
||||
if (oldElsRemoved) {
|
||||
return;
|
||||
}
|
||||
oldElsRemoved = true;
|
||||
oldEls.forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
};
|
||||
|
||||
if (activeImage) {
|
||||
let modal = document.getElementById('gd-modal');
|
||||
|
||||
if (params.enable_exif) {
|
||||
if (exifTimeoutId) {
|
||||
clearTimeout(exifTimeoutId);
|
||||
}
|
||||
|
||||
let exif = modal.querySelector('#gd-modal-exif');
|
||||
const onTimeOutClass = 'gd-modal-exif-ontimeout';
|
||||
|
||||
let child = exif.lastElementChild;
|
||||
while (child) {
|
||||
exif.removeChild(child);
|
||||
child = exif.lastElementChild;
|
||||
}
|
||||
let dl = document.createElement('dl');
|
||||
exif.appendChild(dl);
|
||||
|
||||
const addTag = (tag, value) => {
|
||||
let dt = document.createElement('dt');
|
||||
dt.innerText = camelToTitle(tag);
|
||||
dl.appendChild(dt);
|
||||
let dd = document.createElement('dd');
|
||||
dd.innerText = value;
|
||||
dl.appendChild(dd);
|
||||
};
|
||||
|
||||
let date = new Date(activeImage.exif.Date);
|
||||
var dateString = new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().split('T')[0];
|
||||
addTag('Date', dateString);
|
||||
let tags = activeImage.exif.Tags;
|
||||
for (const tag in tags) {
|
||||
addTag(tag, tags[tag]);
|
||||
}
|
||||
exif.classList.remove(onTimeOutClass);
|
||||
|
||||
exifTimeoutId = setTimeout(() => {
|
||||
exif.classList.add(onTimeOutClass);
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
let thumbnail = new Image();
|
||||
thumbnail.classList.add('gd-modal-content');
|
||||
thumbnail.width = activeImage.width;
|
||||
thumbnail.height = activeImage.height;
|
||||
thumbnail.style.aspectRatio = activeImage.width / activeImage.height;
|
||||
|
||||
const fullImage = thumbnail.cloneNode(false);
|
||||
|
||||
thumbnail.classList.add(classThumbnail);
|
||||
|
||||
fullImage.src = activeImage.full;
|
||||
thumbnail.src = activeImage['20'];
|
||||
|
||||
thumbnail.onload = function () {
|
||||
if (thumbnail) {
|
||||
imageWrapper.appendChild(thumbnail);
|
||||
removeOldEls();
|
||||
}
|
||||
};
|
||||
|
||||
fullImage.onload = function () {
|
||||
if (fullImage) {
|
||||
imageWrapper.appendChild(fullImage);
|
||||
fullImage.classList.add(classLoaded);
|
||||
if (thumbnail) {
|
||||
thumbnail.classList.add(classLoaded);
|
||||
}
|
||||
removeOldEls();
|
||||
}
|
||||
};
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
removeOldEls();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// Load the gallery.
|
||||
let images = await (await fetch(dataUrl)).json();
|
||||
|
||||
if (params.shuffle) {
|
||||
// Shuffle them to make it more interesting.
|
||||
images = images
|
||||
.map((value) => ({ value, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ value }) => value);
|
||||
} else if (params.reverse) {
|
||||
images = images.reverse();
|
||||
}
|
||||
|
||||
let imagesMap = new Map();
|
||||
let imageData = [];
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
let image = images[i];
|
||||
image.prev = images[(i + images.length - 1) % images.length];
|
||||
image.next = images[(i + 1) % images.length];
|
||||
imageData.push({ filename: image.name, aspectRatio: image.width / image.height, image: image });
|
||||
imagesMap.set(image.name, image);
|
||||
}
|
||||
|
||||
var options = {
|
||||
onClickHandler: function (filename) {
|
||||
debug('onClickHandler', filename);
|
||||
activeImage = imagesMap.get(filename);
|
||||
if (activeImage) {
|
||||
openActiveImage();
|
||||
}
|
||||
},
|
||||
containerId: galleryId,
|
||||
classPrefix: 'gd',
|
||||
spaceBetweenImages: 1,
|
||||
urlForSize: function (filename, size) {
|
||||
return imagesMap.get(filename)[size];
|
||||
},
|
||||
styleForElement: function (filename) {
|
||||
let image = imagesMap.get(filename);
|
||||
if (!image || image.colors.size < 1) {
|
||||
return '';
|
||||
}
|
||||
let colors = image.colors;
|
||||
let first = colors[0];
|
||||
let second = '#ccc';
|
||||
// Some images have only one dominant color.
|
||||
if (colors.length > 1) {
|
||||
second = colors[1];
|
||||
}
|
||||
|
||||
return ` background: linear-gradient(15deg, ${first}, ${second});`;
|
||||
},
|
||||
};
|
||||
|
||||
new Pig(imageData, options).enable();
|
||||
},
|
||||
};
|
||||
|
||||
function camelToTitle(text) {
|
||||
return text.replace(/([A-Z])/g, ' $1').replace(/^./, function (str) {
|
||||
return str.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
export default GalleryDeluxe;
|
||||
936
themes/gallerydeluxe/assets/js/gallerydeluxe/src/pig.js
Normal file
@@ -0,0 +1,936 @@
|
||||
/*! The Pig library is MIT License (MIT) Copyright (c) 2015 Dan Schlosser, see https://github.com/schlosser/pig.js/blob/master/LICENSE.md */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This is a manager for our resize handlers. You can add a callback, disable
|
||||
* all resize handlers, and re-enable handlers after they have been disabled.
|
||||
*
|
||||
* optimizedResize is adapted from Mozilla code:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/Events/resize
|
||||
*/
|
||||
const optimizedResize = (function () {
|
||||
const callbacks = [];
|
||||
let running = false;
|
||||
|
||||
// fired on resize event
|
||||
function resize() {
|
||||
if (!running) {
|
||||
running = true;
|
||||
if (window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame(runCallbacks);
|
||||
} else {
|
||||
setTimeout(runCallbacks, 66);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// run the actual callbacks
|
||||
function runCallbacks() {
|
||||
callbacks.forEach(function (callback) {
|
||||
callback();
|
||||
});
|
||||
|
||||
running = false;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Add a callback to be run on resize.
|
||||
*
|
||||
* @param {function} callback - the callback to run on resize.
|
||||
*/
|
||||
add: function (callback) {
|
||||
if (!callbacks.length) {
|
||||
window.addEventListener('resize', resize);
|
||||
}
|
||||
|
||||
callbacks.push(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables all resize handlers.
|
||||
*/
|
||||
disable: function () {
|
||||
window.removeEventListener('resize', resize);
|
||||
},
|
||||
|
||||
/**
|
||||
* Enables all resize handlers, if they were disabled.
|
||||
*/
|
||||
reEnable: function () {
|
||||
window.addEventListener('resize', resize);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Inject CSS needed to make the grid work in the <head></head>.
|
||||
*
|
||||
* @param {string} containerId - ID of the container for the images.
|
||||
* @param {string} classPrefix - the prefix associated with this library that
|
||||
* should be prepended to classnames.
|
||||
* @param {number} transitionSpeed - animation duration in milliseconds
|
||||
*/
|
||||
function _injectStyle(containerId, classPrefix, transitionSpeed) {
|
||||
const css =
|
||||
'#' +
|
||||
containerId +
|
||||
' {' +
|
||||
' position: relative;' +
|
||||
'}' +
|
||||
'.' +
|
||||
classPrefix +
|
||||
'-figure {' +
|
||||
' background-color: #D5D5D5;' +
|
||||
' overflow: hidden;' +
|
||||
' left: 0;' +
|
||||
' position: absolute;' +
|
||||
' top: 0;' +
|
||||
' margin: 0;' +
|
||||
'}' +
|
||||
'.' +
|
||||
classPrefix +
|
||||
'-figure img {' +
|
||||
' left: 0;' +
|
||||
' position: absolute;' +
|
||||
' top: 0;' +
|
||||
' height: 100%;' +
|
||||
' width: 100%;' +
|
||||
' opacity: 0;' +
|
||||
' transition: ' +
|
||||
(transitionSpeed / 1000).toString(10) +
|
||||
's ease opacity;' +
|
||||
' -webkit-transition: ' +
|
||||
(transitionSpeed / 1000).toString(10) +
|
||||
's ease opacity;' +
|
||||
'}' +
|
||||
'.' +
|
||||
classPrefix +
|
||||
'-figure img.' +
|
||||
classPrefix +
|
||||
'-thumbnail {' +
|
||||
' -webkit-filter: blur(30px);' +
|
||||
' filter: blur(30px);' +
|
||||
' left: auto;' +
|
||||
' position: relative;' +
|
||||
' width: auto;' +
|
||||
'}' +
|
||||
'.' +
|
||||
classPrefix +
|
||||
'-figure img.' +
|
||||
classPrefix +
|
||||
'-loaded {' +
|
||||
' opacity: 1;' +
|
||||
'}';
|
||||
|
||||
const head = document.head || document.getElementsByTagName('head')[0];
|
||||
const style = document.createElement('style');
|
||||
|
||||
style.type = 'text/css';
|
||||
if (style.styleSheet) {
|
||||
// set style for IE8 and below
|
||||
style.styleSheet.cssText = css;
|
||||
} else {
|
||||
style.appendChild(document.createTextNode(css));
|
||||
}
|
||||
|
||||
head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend obj1 with each key in obj2, overriding default values in obj1 with
|
||||
* values in obj2
|
||||
*
|
||||
* @param {object} obj1 - The object to extend.
|
||||
* @param {object} obj2 - The overrides to apply onto obj1.
|
||||
*/
|
||||
function _extend(obj1, obj2) {
|
||||
for (const i in obj2) {
|
||||
if (obj2.hasOwnProperty(i)) {
|
||||
obj1[i] = obj2[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance from `elem` to the top of the page. This is done by
|
||||
* walking up the node tree, getting the offsetTop of each parent node, until
|
||||
* the top of the page.
|
||||
*
|
||||
* @param {object} elem - The element to compute the offset of.
|
||||
* @returns {number} - distance of `elem` to the top of the page
|
||||
*/
|
||||
function _getOffsetTop(elem) {
|
||||
let offsetTop = 0;
|
||||
do {
|
||||
if (!isNaN(elem.offsetTop)) {
|
||||
offsetTop += elem.offsetTop;
|
||||
}
|
||||
elem = elem.offsetParent;
|
||||
} while (elem);
|
||||
return offsetTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of the progressive image grid, inserting boilerplate
|
||||
* CSS and loading image data. Instantiating an instance of the Pig class
|
||||
* does not cause any images to appear however. After instantiating, call the
|
||||
* `enable()` function on the returned instance:
|
||||
*
|
||||
* var pig = new Pig(imageData, opts);
|
||||
* pig.enable();
|
||||
*
|
||||
* @param {array} imageData - An array of metadata about each image to
|
||||
* include in the grid.
|
||||
* @param {string} imageData[0].filename - The filename of the image.
|
||||
* @param {string} imageData[0].aspectRatio - The aspect ratio of the image.
|
||||
* @param {object} options - An object containing overrides for the default
|
||||
* options. See below for the full list of options
|
||||
* and defaults.
|
||||
*
|
||||
* @returns {object} The Pig instance.
|
||||
*/
|
||||
export function Pig(imageData, options) {
|
||||
// Global State
|
||||
this.inRAF = false;
|
||||
this.isTransitioning = false;
|
||||
this.minAspectRatioRequiresTransition = false;
|
||||
this.minAspectRatio = null;
|
||||
this.latestYOffset = 0;
|
||||
this.lastWindowWidth = window.innerWidth;
|
||||
this.scrollDirection = 'down';
|
||||
|
||||
// List of images that are loading or completely loaded on screen.
|
||||
this.visibleImages = [];
|
||||
|
||||
// These are the default settings, which may be overridden.
|
||||
this.settings = {
|
||||
/**
|
||||
* Type: string
|
||||
* Default: 'pig'
|
||||
* Description: The class name of the element inside of which images should
|
||||
* be loaded.
|
||||
*/
|
||||
containerId: 'pig',
|
||||
|
||||
/**
|
||||
* Type: window | HTMLElement
|
||||
* Default: window
|
||||
* Description: The window or HTML element that the grid scrolls in.
|
||||
*/
|
||||
scroller: window,
|
||||
|
||||
/**
|
||||
* Type: string
|
||||
* Default: 'pig'
|
||||
* Description: The prefix associated with this library that should be
|
||||
* prepended to class names within the grid.
|
||||
*/
|
||||
classPrefix: 'pig',
|
||||
|
||||
/**
|
||||
* Type: string
|
||||
* Default: 'figure'
|
||||
* Description: The tag name to use for each figure. The default setting is
|
||||
* to use a <figure></figure> tag.
|
||||
*/
|
||||
figureTagName: 'figure',
|
||||
|
||||
/**
|
||||
* Type: Number
|
||||
* Default: 8
|
||||
* Description: Size in pixels of the gap between images in the grid.
|
||||
*/
|
||||
spaceBetweenImages: 8,
|
||||
|
||||
/**
|
||||
* Type: Number
|
||||
* Default: 500
|
||||
* Description: Transition speed in milliseconds
|
||||
*/
|
||||
transitionSpeed: 500,
|
||||
|
||||
/**
|
||||
* Type: Number
|
||||
* Default: 3000
|
||||
* Description: Height in pixels of images to preload in the direction
|
||||
* that the user is scrolling. For example, in the default case, if the
|
||||
* user is scrolling down, 1000px worth of images will be loaded below
|
||||
* the viewport.
|
||||
*/
|
||||
primaryImageBufferHeight: 1000,
|
||||
|
||||
/**
|
||||
* Type: Number
|
||||
* Default: 100
|
||||
* Description: Height in pixels of images to preload in the direction
|
||||
* that the user is NOT scrolling. For example, in the default case, if
|
||||
* the user is scrolling down, 300px worth of images will be loaded
|
||||
* above the viewport. Images further up will be removed.
|
||||
*/
|
||||
secondaryImageBufferHeight: 300,
|
||||
|
||||
/**
|
||||
* Type: Number
|
||||
* Default: 20
|
||||
* Description: The height in pixels of the thumbnail that should be
|
||||
* loaded and blurred to give the effect that images are loading out of
|
||||
* focus and then coming into focus.
|
||||
*/
|
||||
thumbnailSize: 20,
|
||||
|
||||
/**
|
||||
* Get the URL for an image with the given filename & size.
|
||||
*
|
||||
* @param {string} filename - The filename of the image.
|
||||
* @param {Number} size - The size (height in pixels) of the image.
|
||||
*
|
||||
* @returns {string} The URL of the image at the given size.
|
||||
*/
|
||||
urlForSize: function (filename, size) {
|
||||
return '/img/' + size.toString(10) + '/' + filename;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the a custom style for the container element.
|
||||
*
|
||||
* * @param {string} filename - The filename of the image.
|
||||
*/
|
||||
styleForElement: function (filename) {
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a callback with the filename of the image
|
||||
* which was clicked.
|
||||
*
|
||||
* @param {string} filename - The filename property of the image.
|
||||
*/
|
||||
onClickHandler: null,
|
||||
|
||||
/**
|
||||
* Get the minimum required aspect ratio for a valid row of images. The
|
||||
* perfect rows are maintained by building up a row of images by adding
|
||||
* together their aspect ratios (the aspect ratio when they are placed
|
||||
* next to each other) until that aspect ratio exceeds the value returned
|
||||
* by this function. Responsive reordering is achieved through changes
|
||||
* to what this function returns at different values of the passed
|
||||
* parameter `lastWindowWidth`.
|
||||
*
|
||||
* @param {Number} lastWindowWidth - The last computed width of the
|
||||
* browser window.
|
||||
*
|
||||
* @returns {Number} The minimum aspect ratio at this window width.
|
||||
*/
|
||||
getMinAspectRatio: function (lastWindowWidth) {
|
||||
if (lastWindowWidth <= 640) {
|
||||
return 2;
|
||||
} else if (lastWindowWidth <= 1280) {
|
||||
return 4;
|
||||
} else if (lastWindowWidth <= 1920) {
|
||||
return 5;
|
||||
}
|
||||
return 6;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the image size (height in pixels) to use for this window width.
|
||||
* Responsive resizing of images is achieved through changes to what this
|
||||
* function returns at different values of the passed parameter
|
||||
* `lastWindowWidth`.
|
||||
*
|
||||
* @param {Number} lastWindowWidth - The last computed width of the
|
||||
* browser window.
|
||||
*
|
||||
* @returns {Number} The size (height in pixels) of the images to load.
|
||||
*/
|
||||
getImageSize: function (lastWindowWidth) {
|
||||
if (lastWindowWidth <= 640) {
|
||||
return 100;
|
||||
} else if (lastWindowWidth <= 1920) {
|
||||
return 250;
|
||||
}
|
||||
return 500;
|
||||
},
|
||||
};
|
||||
|
||||
// We extend the default settings with the provided overrides.
|
||||
_extend(this.settings, options || {});
|
||||
|
||||
// Find the container to load images into, if it exists.
|
||||
this.container = document.getElementById(this.settings.containerId);
|
||||
if (!this.container) {
|
||||
console.error('Could not find element with ID ' + this.settings.containerId);
|
||||
}
|
||||
|
||||
this.scroller = this.settings.scroller;
|
||||
|
||||
// Our global reference for images in the grid. Note that not all of these
|
||||
// images are necessarily in view or loaded.
|
||||
this.images = this._parseImageData(imageData);
|
||||
|
||||
// Inject our boilerplate CSS.
|
||||
_injectStyle(this.settings.containerId, this.settings.classPrefix, this.settings.transitionSpeed);
|
||||
|
||||
// Allows for chaining with `enable()`.
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because we may be transitioning a very large number of elements on a
|
||||
* resize, and because we cannot reliably determine when all elements are
|
||||
* done transitioning, we have to approximate the amount of time it will take
|
||||
* for the browser to be expected to complete with a transition. This
|
||||
* constant gives the scale factor to apply to the given transition time. For
|
||||
* example, if transitionTimeoutScaleFactor is 1.5 and transition speed is
|
||||
* given as 500ms, we will wait 750ms before assuming that we are actually
|
||||
* done resizing.
|
||||
*
|
||||
* @returns {Number} Time in milliseconds before we can consider a resize to
|
||||
* have been completed.
|
||||
*/
|
||||
Pig.prototype._getTransitionTimeout = function () {
|
||||
const transitionTimeoutScaleFactor = 1.5;
|
||||
return this.settings.transitionSpeed * transitionTimeoutScaleFactor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gives the CSS property string to set for the transition value, depending
|
||||
* on whether or not we are transitioning.
|
||||
*
|
||||
* @returns {string} a value for the `transition` CSS property.
|
||||
*/
|
||||
Pig.prototype._getTransitionString = function () {
|
||||
if (this.isTransitioning) {
|
||||
return (this.settings.transitionSpeed / 1000).toString(10) + 's transform ease';
|
||||
}
|
||||
|
||||
return 'none';
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the current value for `this.minAspectRatio`, using the
|
||||
* `getMinAspectRatio` function defined in the settings. Then,
|
||||
* `this.minAspectRatioRequiresTransition` will be set, depending on whether
|
||||
* or not the value of this.minAspectRatio has changed.
|
||||
*/
|
||||
Pig.prototype._recomputeMinAspectRatio = function () {
|
||||
const oldMinAspectRatio = this.minAspectRatio;
|
||||
this.minAspectRatio = this.settings.getMinAspectRatio(this.lastWindowWidth);
|
||||
|
||||
if (oldMinAspectRatio !== null && oldMinAspectRatio !== this.minAspectRatio) {
|
||||
this.minAspectRatioRequiresTransition = true;
|
||||
} else {
|
||||
this.minAspectRatioRequiresTransition = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates new instances of the ProgressiveImage class for each of the images
|
||||
* defined in `imageData`.
|
||||
*
|
||||
* @param {array} imageData - An array of metadata about each image to
|
||||
* include in the grid.
|
||||
* @param {string} imageData[0].filename - The filename of the image.
|
||||
* @param {string} imageData[0].aspectRatio - The aspect ratio of the image.
|
||||
*
|
||||
* @returns {Array[ProgressiveImage]} - An array of ProgressiveImage
|
||||
* instances that we created.
|
||||
*/
|
||||
Pig.prototype._parseImageData = function (imageData) {
|
||||
const progressiveImages = [];
|
||||
|
||||
imageData.forEach(
|
||||
function (image, index) {
|
||||
const progressiveImage = new ProgressiveImage(image, index, this);
|
||||
progressiveImages.push(progressiveImage);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
return progressiveImages;
|
||||
};
|
||||
|
||||
/**
|
||||
* This computes the layout of the entire grid, setting the height, width,
|
||||
* translateX, translateY, and transtion values for each ProgessiveImage in
|
||||
* `this.images`. These styles are set on the ProgressiveImage.style property,
|
||||
* but are not set on the DOM.
|
||||
*
|
||||
* This separation of concerns (computing layout and DOM manipulation) is
|
||||
* paramount to the performance of the PIG. While we need to manipulate the
|
||||
* DOM every time we scroll (adding or remove images, etc.), we only need to
|
||||
* compute the layout of the PIG on load and on resize. Therefore, this
|
||||
* function will compute the entire grid layout but will not manipulate the
|
||||
* DOM at all.
|
||||
*
|
||||
* All DOM manipulation occurs in `_doLayout`.
|
||||
*/
|
||||
Pig.prototype._computeLayout = function () {
|
||||
// Constants
|
||||
const wrapperWidth = parseInt(this.container.clientWidth, 10);
|
||||
|
||||
// State
|
||||
let row = []; // The list of images in the current row.
|
||||
let translateX = 0; // The current translateX value that we are at
|
||||
let translateY = 0; // The current translateY value that we are at
|
||||
let rowAspectRatio = 0; // The aspect ratio of the row we are building
|
||||
|
||||
// Compute the minimum aspect ratio that should be applied to the rows.
|
||||
this._recomputeMinAspectRatio();
|
||||
|
||||
// If we are not currently transitioning, and our minAspectRatio has just
|
||||
// changed, then we mark isTransitioning true. If this is the case, then
|
||||
// `this._getTransitionString()` will ensure that each image has a value
|
||||
// like "0.5s ease all". This will cause images to animate as they change
|
||||
// position. (They need to change position because the minAspectRatio has
|
||||
// changed.) Once we determine that the transtion is probably over (using
|
||||
// `this._getTransitionTimeout`) we unset `this.isTransitioning`, so that
|
||||
// future calls to `_computeLayout` will set "transition: none".
|
||||
if (!this.isTransitioning && this.minAspectRatioRequiresTransition) {
|
||||
this.isTransitioning = true;
|
||||
setTimeout(function () {
|
||||
this.isTransitioning = false;
|
||||
}, this._getTransitionTimeout());
|
||||
}
|
||||
|
||||
// Get the valid-CSS transition string.
|
||||
const transition = this._getTransitionString();
|
||||
|
||||
// Loop through all our images, building them up into rows and computing
|
||||
// the working rowAspectRatio.
|
||||
[].forEach.call(
|
||||
this.images,
|
||||
function (image, index) {
|
||||
rowAspectRatio += parseFloat(image.aspectRatio);
|
||||
row.push(image);
|
||||
|
||||
// When the rowAspectRatio exceeeds the minimum acceptable aspect ratio,
|
||||
// or when we're out of images, we say that we have all the images we
|
||||
// need for this row, and compute the style values for each of these
|
||||
// images.
|
||||
if (rowAspectRatio >= this.minAspectRatio || index + 1 === this.images.length) {
|
||||
// Make sure that the last row also has a reasonable height
|
||||
rowAspectRatio = Math.max(rowAspectRatio, this.minAspectRatio);
|
||||
|
||||
// Compute this row's height.
|
||||
const totalDesiredWidthOfImages = wrapperWidth - this.settings.spaceBetweenImages * (row.length - 1);
|
||||
const rowHeight = totalDesiredWidthOfImages / rowAspectRatio;
|
||||
|
||||
// For each image in the row, compute the width, height, translateX,
|
||||
// and translateY values, and set them (and the transition value we
|
||||
// found above) on each image.
|
||||
//
|
||||
// NOTE: This does not manipulate the DOM, rather it just sets the
|
||||
// style values on the ProgressiveImage instance. The DOM nodes
|
||||
// will be updated in _doLayout.
|
||||
row.forEach(
|
||||
function (img) {
|
||||
const imageWidth = rowHeight * img.aspectRatio;
|
||||
|
||||
// This is NOT DOM manipulation.
|
||||
img.style = {
|
||||
width: parseInt(imageWidth, 10),
|
||||
height: parseInt(rowHeight, 10),
|
||||
translateX: translateX,
|
||||
translateY: translateY,
|
||||
transition: transition,
|
||||
};
|
||||
|
||||
// The next image is this.settings.spaceBetweenImages pixels to the
|
||||
// right of this image.
|
||||
translateX += imageWidth + this.settings.spaceBetweenImages;
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
// Reset our state variables for next row.
|
||||
row = [];
|
||||
rowAspectRatio = 0;
|
||||
translateY += parseInt(rowHeight, 10) + this.settings.spaceBetweenImages;
|
||||
translateX = 0;
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
// No space below the last image
|
||||
this.totalHeight = translateY - this.settings.spaceBetweenImages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the DOM to reflect the style values of each image in the PIG,
|
||||
* adding or removing images appropriately.
|
||||
*
|
||||
* PIG ensures that there are not too many images loaded into the DOM at once
|
||||
* by maintaining a buffer region around the viewport in which images are
|
||||
* allowed, removing all images below and above. Because all of our layout
|
||||
* is computed using CSS transforms, removing an image above the buffer will
|
||||
* not cause the gird to reshuffle.
|
||||
*
|
||||
* The primary buffer is the buffer in the direction of the user's scrolling.
|
||||
* (Below if they are scrolling down, above if they are scrolling up.) The
|
||||
* size of this buffer determines the experience of scrolling down the page.
|
||||
*
|
||||
* The secondary buffer is the buffer in the opposite direction of the user's
|
||||
* scrolling. The size of this buffer determines the experience of changing
|
||||
* scroll directions. (Too small, and we have to reload a ton of images above
|
||||
* the viewport if the user changes scroll directions.)
|
||||
*
|
||||
* While the entire grid has been computed, only images within the viewport,
|
||||
* the primary buffer, and the secondary buffer will exist in the DOM.
|
||||
*
|
||||
*
|
||||
* Illustration: the primary and secondary buffers
|
||||
*
|
||||
*
|
||||
* +---------------------------+
|
||||
* | |
|
||||
* | |
|
||||
* | |
|
||||
* | |
|
||||
* + - - - - - - - - - - - - - + -------
|
||||
* | | A
|
||||
* | Secondary Buffer | this.setting.secondaryImageBufferHeight
|
||||
* | | V
|
||||
* +---------------------------+ -------
|
||||
* | | A
|
||||
* | | |
|
||||
* | | |
|
||||
* | Viewport | window.innerHeight
|
||||
* | | |
|
||||
* | | |
|
||||
* | | V
|
||||
* +---------------------------+ -------
|
||||
* | | A
|
||||
* | | |
|
||||
* | | |
|
||||
* | | |
|
||||
* | Primary Buffer | this.settings.primaryImageBufferHeight
|
||||
* | | |
|
||||
* | | |
|
||||
* | | |
|
||||
* | | V
|
||||
* + - - - - - - - - - - - - - + -------
|
||||
* | |
|
||||
* | (Scroll direction) |
|
||||
* | | |
|
||||
* | | |
|
||||
* | V |
|
||||
* | |
|
||||
*
|
||||
*/
|
||||
Pig.prototype._doLayout = function () {
|
||||
// Set the container height
|
||||
this.container.style.height = this.totalHeight + 'px';
|
||||
|
||||
// Get the top and bottom buffers heights.
|
||||
const bufferTop =
|
||||
this.scrollDirection === 'up' ? this.settings.primaryImageBufferHeight : this.settings.secondaryImageBufferHeight;
|
||||
const bufferBottom =
|
||||
this.scrollDirection === 'down' ? this.settings.secondaryImageBufferHeight : this.settings.primaryImageBufferHeight;
|
||||
|
||||
// Now we compute the location of the top and bottom buffers:
|
||||
const containerOffset = _getOffsetTop(this.container);
|
||||
const scrollerHeight = this.scroller === window ? window.innerHeight : this.scroller.offsetHeight;
|
||||
|
||||
// This is the top of the top buffer. If the bottom of an image is above
|
||||
// this line, it will be removed.
|
||||
const minTranslateYPlusHeight = this.latestYOffset - containerOffset - bufferTop;
|
||||
|
||||
// This is the bottom of the bottom buffer. If the top of an image is
|
||||
// below this line, it will be removed.
|
||||
const maxTranslateY = this.latestYOffset - containerOffset + scrollerHeight + bufferBottom;
|
||||
|
||||
// Here, we loop over every image, determine if it is inside our buffers or
|
||||
// no, and either insert it or remove it appropriately.
|
||||
this.images.forEach(
|
||||
function (image) {
|
||||
if (
|
||||
image.style.translateY + image.style.height < minTranslateYPlusHeight ||
|
||||
image.style.translateY > maxTranslateY
|
||||
) {
|
||||
// Hide Image
|
||||
image.hide();
|
||||
} else {
|
||||
// Load Image
|
||||
image.load();
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create our onScroll handler and return it.
|
||||
*
|
||||
* @returns {function} Our optimized onScroll handler that we should attach to.
|
||||
*/
|
||||
Pig.prototype._getOnScroll = function () {
|
||||
const _this = this;
|
||||
|
||||
/**
|
||||
* This function is called on scroll. It computes variables about the page
|
||||
* position and scroll direction, and then calls a _doLayout guarded by a
|
||||
* window.requestAnimationFrame.
|
||||
*
|
||||
* We use the boolean variable _this.inRAF to ensure that we don't overload
|
||||
* the number of layouts we perform by starting another layout while we are
|
||||
* in the middle of doing one.
|
||||
*/
|
||||
const onScroll = function () {
|
||||
// Compute the scroll direction using the latestYOffset and the
|
||||
// previousYOffset
|
||||
const newYOffset = _this.scroller === window ? window.pageYOffset : _this.scroller.scrollTop;
|
||||
_this.previousYOffset = _this.latestYOffset || newYOffset;
|
||||
_this.latestYOffset = newYOffset;
|
||||
_this.scrollDirection = _this.latestYOffset > _this.previousYOffset ? 'down' : 'up';
|
||||
|
||||
// Call _this.doLayout, guarded by window.requestAnimationFrame
|
||||
if (!_this.inRAF) {
|
||||
_this.inRAF = true;
|
||||
window.requestAnimationFrame(function () {
|
||||
_this._doLayout();
|
||||
_this.inRAF = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return onScroll;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable scroll and resize handlers, and run a complete layout computation /
|
||||
* application.
|
||||
*
|
||||
* @returns {object} The Pig instance, for easy chaining with the constructor.
|
||||
*/
|
||||
Pig.prototype.enable = function () {
|
||||
this.onScroll = this._getOnScroll();
|
||||
|
||||
this.scroller.addEventListener('scroll', this.onScroll);
|
||||
|
||||
this.onScroll();
|
||||
this._computeLayout();
|
||||
this._doLayout();
|
||||
|
||||
const windowWidth = () => {
|
||||
return this.scroller === window ? window.innerWidth : this.scroller.offsetWidth;
|
||||
};
|
||||
|
||||
optimizedResize.add(
|
||||
function () {
|
||||
this.lastWindowWidth = windowWidth();
|
||||
this._computeLayout();
|
||||
this._doLayout();
|
||||
let newWindowWidth = windowWidth();
|
||||
if (newWindowWidth !== this.lastWindowWidth) {
|
||||
// This happens on orientation change on mobile devices.
|
||||
this.lastWindowWidth = newWindowWidth;
|
||||
this._computeLayout();
|
||||
this._doLayout();
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove all scroll and resize listeners.
|
||||
*
|
||||
* @returns {object} The Pig instance.
|
||||
*/
|
||||
Pig.prototype.disable = function () {
|
||||
this.scroller.removeEventListener('scroll', this.onScroll);
|
||||
optimizedResize.disable();
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* This class manages a single image. It keeps track of the image's height,
|
||||
* width, and position in the grid. An instance of this class is associated
|
||||
* with a single image figure, which looks like this:
|
||||
*
|
||||
* <figure class="pig-figure" style="transform: ...">
|
||||
* <img class="pig-thumbnail pig-loaded" src="/path/to/thumbnail/image.jpg" />
|
||||
* <img class="pig-loaded" src="/path/to/500px/image.jpg" />
|
||||
* </figure>
|
||||
*
|
||||
* However, this element may or may not actually exist in the DOM. The actual
|
||||
* DOM element may loaded and unloaded depending on where it is with respect
|
||||
* to the viewport. This class is responsible for managing the DOM elements,
|
||||
* but does not include logic to determine _when_ the DOM elements should
|
||||
* be removed.
|
||||
*
|
||||
* This class also manages the blur-into-focus load effect. First, the
|
||||
* <figure> element is inserted into the page. Then, a very small thumbnail
|
||||
* is loaded, stretched out to the full size of the image. This pixelated
|
||||
* image is then blurred using CSS filter: blur(). Then, the full image is
|
||||
* loaded, with opacity:0. Once it has loaded, it is given the `pig-loaded`
|
||||
* class, and its opacity is set to 1. This creates an effect where there is
|
||||
* first a blurred version of the image, and then it appears to come into
|
||||
* focus.
|
||||
*
|
||||
* @param {array} singleImageData - An array of metadata about each image to
|
||||
* include in the grid.
|
||||
* @param {string} singleImageData[0].filename - The filename of the image.
|
||||
* @param {string} singleImageData[0].aspectRatio - The aspect ratio of the
|
||||
* image.
|
||||
* @param {number} index - Index of the image in the list of images
|
||||
* @param {object} pig - The Pig instance
|
||||
*
|
||||
* @returns {object} The Pig instance, for easy chaining with the constructor.
|
||||
*/
|
||||
function ProgressiveImage(singleImageData, index, pig) {
|
||||
// Global State
|
||||
this.existsOnPage = false; // True if the element exists on the page.
|
||||
|
||||
// Instance information
|
||||
this.aspectRatio = singleImageData.aspectRatio; // Aspect Ratio
|
||||
this.filename = singleImageData.filename; // Filename
|
||||
this.index = index; // The index in the list of images
|
||||
|
||||
// The Pig instance
|
||||
this.pig = pig;
|
||||
|
||||
this.classNames = {
|
||||
figure: pig.settings.classPrefix + '-figure',
|
||||
thumbnail: pig.settings.classPrefix + '-thumbnail',
|
||||
loaded: pig.settings.classPrefix + '-loaded',
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the image element associated with this ProgressiveImage into the DOM.
|
||||
*
|
||||
* This function will append the figure into the DOM, create and insert the
|
||||
* thumbnail, and create and insert the full image.
|
||||
*/
|
||||
ProgressiveImage.prototype.load = function () {
|
||||
// Create a new image element, and insert it into the DOM. It doesn't
|
||||
// matter the order of the figure elements, because all positioning
|
||||
// is done using transforms.
|
||||
this.existsOnPage = true;
|
||||
this._updateStyles();
|
||||
this.pig.container.appendChild(this.getElement());
|
||||
|
||||
// We run the rest of the function in a 100ms setTimeout so that if the
|
||||
// user is scrolling down the page very fast and hide() is called within
|
||||
// 100ms of load(), the hide() function will set this.existsOnPage to false
|
||||
// and we can exit.
|
||||
setTimeout(
|
||||
function () {
|
||||
// The image was hidden very quickly after being loaded, so don't bother
|
||||
// loading it at all.
|
||||
if (!this.existsOnPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show thumbnail
|
||||
if (!this.thumbnail) {
|
||||
this.thumbnail = new Image();
|
||||
|
||||
this.thumbnail.src = this.pig.settings.urlForSize(this.filename, this.pig.settings.thumbnailSize);
|
||||
this.thumbnail.className = this.classNames.thumbnail;
|
||||
this.thumbnail.onload = function () {
|
||||
// We have to make sure thumbnail still exists, we may have already been
|
||||
// deallocated if the user scrolls too fast.
|
||||
if (this.thumbnail) {
|
||||
this.thumbnail.className += ' ' + this.classNames.loaded;
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this.getElement().appendChild(this.thumbnail);
|
||||
}
|
||||
|
||||
// Show full image
|
||||
if (!this.fullImage) {
|
||||
this.fullImage = new Image();
|
||||
this.fullImage.src = this.pig.settings.urlForSize(
|
||||
this.filename,
|
||||
this.pig.settings.getImageSize(this.pig.lastWindowWidth)
|
||||
);
|
||||
this.fullImage.onload = function () {
|
||||
// We have to make sure fullImage still exists, we may have already been
|
||||
// deallocated if the user scrolls too fast.
|
||||
if (this.fullImage) {
|
||||
this.fullImage.className += ' ' + this.classNames.loaded;
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this.getElement().appendChild(this.fullImage);
|
||||
}
|
||||
}.bind(this),
|
||||
100
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the figure from the DOM, removes the thumbnail and full image, and
|
||||
* deletes the this.thumbnail and this.fullImage properties off of the
|
||||
* ProgressiveImage object.
|
||||
*/
|
||||
ProgressiveImage.prototype.hide = function () {
|
||||
// Remove the images from the element, so that if a user is scrolling super
|
||||
// fast, we won't try to load every image we scroll past.
|
||||
if (this.getElement()) {
|
||||
if (this.thumbnail) {
|
||||
this.thumbnail.src = '';
|
||||
this.getElement().removeChild(this.thumbnail);
|
||||
delete this.thumbnail;
|
||||
}
|
||||
|
||||
if (this.fullImage) {
|
||||
this.fullImage.src = '';
|
||||
this.getElement().removeChild(this.fullImage);
|
||||
delete this.fullImage;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the image from the DOM.
|
||||
if (this.existsOnPage) {
|
||||
this.pig.container.removeChild(this.getElement());
|
||||
}
|
||||
|
||||
this.existsOnPage = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the DOM element associated with this ProgressiveImage. We default to
|
||||
* using this.element, and we create it if it doesn't exist.
|
||||
*
|
||||
* @returns {HTMLElement} The DOM element associated with this instance.
|
||||
*/
|
||||
ProgressiveImage.prototype.getElement = function () {
|
||||
if (!this.element) {
|
||||
this.element = document.createElement(this.pig.settings.figureTagName);
|
||||
this.element.className = this.classNames.figure;
|
||||
|
||||
let style = this.pig.settings.styleForElement(this.filename);
|
||||
if (this.style) {
|
||||
this.element.style = style;
|
||||
}
|
||||
|
||||
if (this.pig.settings.onClickHandler !== null) {
|
||||
this.element.addEventListener(
|
||||
'click',
|
||||
function () {
|
||||
this.pig.settings.onClickHandler(this.filename);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
this._updateStyles();
|
||||
}
|
||||
|
||||
return this.element;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the style attribute to reflect this style property on this object.
|
||||
*/
|
||||
ProgressiveImage.prototype._updateStyles = function () {
|
||||
this.getElement().style.transition = this.style.transition;
|
||||
this.getElement().style.width = this.style.width + 'px';
|
||||
this.getElement().style.height = this.style.height + 'px';
|
||||
this.getElement().style.transform = 'translate3d(' + this.style.translateX + 'px,' + this.style.translateY + 'px, 0)';
|
||||
};
|
||||
1
themes/gallerydeluxe/config.toml
Normal file
@@ -0,0 +1 @@
|
||||
# No options for now.
|
||||
25
themes/gallerydeluxe/exampleSite/config.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
baseURL = "https://example.com"
|
||||
|
||||
disableKinds = ["taxonomy", "term", "section"]
|
||||
|
||||
[module]
|
||||
[[module.imports]]
|
||||
path = "github.com/bep/gallerydeluxe_starter"
|
||||
|
||||
[params]
|
||||
# Set to enable Plausible Analytics.
|
||||
plausible_domain = ""
|
||||
|
||||
[params.gallerydeluxe]
|
||||
shuffle = false
|
||||
reverse = true
|
||||
enable_exif = false
|
||||
|
||||
[imaging]
|
||||
resampleFilter = "CatmullRom"
|
||||
quality = 71
|
||||
anchor = "smart"
|
||||
[imaging.exif]
|
||||
disableDate = false
|
||||
disableLatLong = true
|
||||
includeFields = 'Artist|LensModel|FNumber|ExposureTime'
|
||||
7
themes/gallerydeluxe/exampleSite/go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module github.com/bep/gallerydeluxe/exampleSite
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/bep/gallerydeluxe_starter v0.0.0-20230920153932-f3090f96b66b // indirect
|
||||
|
||||
replace github.com/bep/gallerydeluxe => ../
|
||||
4
themes/gallerydeluxe/exampleSite/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/bep/gallerydeluxe_starter v0.0.0-20221107164209-385ac3b706df h1:dublYQedMsn8L7qO1oY27rW80PXtOUGYyjZIrS3W19E=
|
||||
github.com/bep/gallerydeluxe_starter v0.0.0-20221107164209-385ac3b706df/go.mod h1:Nrtm45JsdYaVpiqjGNpiED+EzShbisGb1uEh+9MfYE8=
|
||||
github.com/bep/gallerydeluxe_starter v0.0.0-20230920153932-f3090f96b66b h1:dObVWoz3+9/nvpQ5TEUcVEIbg+49KHftmXzu//2fWGQ=
|
||||
github.com/bep/gallerydeluxe_starter v0.0.0-20230920153932-f3090f96b66b/go.mod h1:Wx0L2/N2m8fS3rziG0kwbcdgYdI+gzZt4OiF73gqdCs=
|
||||
4
themes/gallerydeluxe/exampleSite/hugo.work
Normal file
@@ -0,0 +1,4 @@
|
||||
go 1.19
|
||||
|
||||
use .
|
||||
use ../../gallerydeluxe_starter
|
||||
3
themes/gallerydeluxe/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/bep/gallerydeluxe
|
||||
|
||||
go 1.19
|
||||
BIN
themes/gallerydeluxe/images/screenshot.jpg
Normal file
|
After Width: | Height: | Size: 359 KiB |
BIN
themes/gallerydeluxe/images/tn.jpg
Normal file
|
After Width: | Height: | Size: 200 KiB |
@@ -0,0 +1,44 @@
|
||||
{{/* 20, 100, 250, and 500 height */}}
|
||||
{{ $full := . }}
|
||||
{{ with site.Params.gallerydeluxe.watermark }}
|
||||
{{ $opts := . }}
|
||||
{{ with resources.Get .image }}
|
||||
{{ $watermark := . }}
|
||||
{{ $fullWidth := $full.Width }}
|
||||
{{ $watermarkMaxWidth := div $fullWidth 6 }}
|
||||
{{ if gt $watermark.Width $watermarkMaxWidth }}
|
||||
{{ $watermark = $watermark.Resize (printf "%dx" $watermarkMaxWidth) }}
|
||||
{{ end }}
|
||||
{{ $watermarkWidth := $watermark.Width }}
|
||||
{{ $watermarkHeight := $watermark.Height }}
|
||||
{{ $posx := $opts.posx | default "right" }}
|
||||
{{ $posy := $opts.posy | default "bottom" }}
|
||||
{{ $padding := 20 }}
|
||||
{{ $x := $padding }}
|
||||
{{ $y := $padding }}
|
||||
{{ if eq $posx "center" }}
|
||||
{{ $x = div $fullWidth 2 }}
|
||||
{{ $x = sub $x (div $watermarkWidth 2) }}
|
||||
{{ end }}
|
||||
{{ if eq $posy "center" }}
|
||||
{{ $y = div $full.Height 2 }}
|
||||
{{ $y = sub $y (div $watermarkHeight 2) }}
|
||||
{{ end }}
|
||||
{{ if eq $posx "right" }}
|
||||
{{ $x = sub $fullWidth (add $watermarkWidth $padding) }}
|
||||
{{ end }}
|
||||
{{ if eq $posy "bottom" }}
|
||||
{{ $y = sub $full.Height (add $watermarkHeight $padding) }}
|
||||
{{ end }}
|
||||
{{ $full = $full.Filter (images.Overlay $watermark $x $y ) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ $m := dict
|
||||
"full" $full
|
||||
"20" (.Resize "x20")
|
||||
"100" (.Resize "x100")
|
||||
"250" (.Resize "x250")
|
||||
"500" (.Resize "x500")
|
||||
}}
|
||||
|
||||
{{ return $m }}
|
||||
@@ -0,0 +1,9 @@
|
||||
{{ $js := resources.Get "js/gallerydeluxe/cdn/index.js" }}
|
||||
{{ $js = $js | js.Build (dict "params" site.Params.gallerydeluxe "minify" hugo.IsProduction) }}
|
||||
{{ $styles := resources.Get "css/gallerydeluxe/styles.css" }}
|
||||
{{ if hugo.IsProduction}}
|
||||
{{ $js = $js | fingerprint }}
|
||||
{{ $styles = $styles | minify | fingerprint }}
|
||||
{{ end }}
|
||||
<script src="{{ $js.RelPermalink }}" defer></script>
|
||||
<link rel="stylesheet" href="{{ $styles.RelPermalink }}"></link>
|
||||
@@ -0,0 +1,76 @@
|
||||
{{/* init.hmlt takes either a slice of .images or a .sourcePath that points to a bundle with images.
|
||||
An .id will be calculated if not provided. This will be used to create the URL to the data file.
|
||||
*/}}
|
||||
|
||||
{{ $images := slice }}
|
||||
{{ $galleryIDBase := .id }}
|
||||
{{ with .images }}
|
||||
{{ $images = . }}
|
||||
{{ if not $galleryIDBase }}
|
||||
{{ range $images }}
|
||||
{{ $galleryIDBase = printf "%s%s" $galleryIDBase .RelPermalink }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ $sourcePath := .sourcePath | default "images" }}
|
||||
{{ if not $galleryIDBase }}
|
||||
{{ $galleryIDBase = $sourcePath }}
|
||||
{{ end }}
|
||||
{{ $gallery := site.GetPage $sourcePath }}
|
||||
{{ $images = $gallery.Resources.ByType "image" }}
|
||||
{{ end }}
|
||||
{{ $galleryID := $galleryIDBase | crypto.FNV32a }}
|
||||
{{ $imageDataUrl := "" }}
|
||||
{{ $s := slice }}
|
||||
{{ $params := site.Params.gallerydeluxe }}
|
||||
{{ with $images }}
|
||||
{{ range . }}
|
||||
{{ $thumbs := partial "gallerydeluxe/create-thumbs.html" . }}
|
||||
{{ $full := $thumbs.full }}
|
||||
{{ $20 := (index $thumbs "20") }}
|
||||
{{ $colors := slice }}
|
||||
{{ $exif := dict }}
|
||||
{{ if $params.enable_exif }}
|
||||
{{/* Workaround for https://github.com/gohugoio/hugo/issues/10345 */}}
|
||||
{{ $tags := newScratch }}
|
||||
{{ range $k, $v := .Exif.Tags }}
|
||||
{{ $tags.Set $k $v }}
|
||||
{{ end }}
|
||||
{{ $exif = dict
|
||||
"Tags" $tags.Values
|
||||
"Date" .Exif.Date
|
||||
"Lat" .Exif.Lat
|
||||
"Long" .Exif.Long
|
||||
}}
|
||||
{{ end }}
|
||||
{{ if (ge hugo.Version "0.104") }}
|
||||
{{/* .Colors method was added in Hugo 0.104.0 */}}
|
||||
{{ $colors = $20.Colors }}
|
||||
{{ end }}
|
||||
{{ $m := dict
|
||||
"name" .Name
|
||||
"full" $full.RelPermalink
|
||||
"exif" $exif
|
||||
"width" $full.Width
|
||||
"height" $full.Height
|
||||
"colors" $colors
|
||||
"20" $20.RelPermalink
|
||||
"100" (index $thumbs "100").RelPermalink
|
||||
"250" (index $thumbs "250").RelPermalink
|
||||
"500" (index $thumbs "500").RelPermalink
|
||||
}}
|
||||
{{ $s = $s | append $m }}
|
||||
{{ end }}
|
||||
{{ $r := $s | jsonify | resources.FromString (printf "%d-gallery.json" $galleryID ) }}
|
||||
{{ if hugo.IsProduction }}
|
||||
{{ $r = $r | minify | fingerprint }}
|
||||
{{ end }}
|
||||
{{ $imageDataUrl = $r.RelPermalink }}
|
||||
{{ else }}
|
||||
{{ errorf "gallerydeluxe: No images provided. Either 'images' as as slice of images or 'sourcePath' must be set to point to a valid Hugo bundle with JPG images in it." }}
|
||||
{{ end }}
|
||||
|
||||
{{ return (dict
|
||||
"imageDataUrl" $imageDataUrl
|
||||
)
|
||||
}}
|
||||
20
themes/gallerydeluxe/netlify.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[build]
|
||||
publish = "exampleSite/public"
|
||||
command = "hugo --gc -s exampleSite --minify"
|
||||
|
||||
[context.production.environment]
|
||||
HUGO_VERSION = "0.109.0"
|
||||
|
||||
[context.deploy-preview]
|
||||
command = "hugo -s exampleSite --minify -D -F -b $DEPLOY_PRIME_URL"
|
||||
|
||||
[context.deploy-preview.environment]
|
||||
HUGO_VERSION = "0.109.0"
|
||||
|
||||
[context.branch-deploy]
|
||||
command = "hugo -s exampleSite --minify --gc -b $DEPLOY_PRIME_URL"
|
||||
|
||||
[context.branch-deploy.environment]
|
||||
HUGO_VERSION = "0.109.0"
|
||||
|
||||
|
||||
12
themes/gallerydeluxe/theme.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
name = "Gallery Deluxe"
|
||||
license = "MIT"
|
||||
licenselink = "https://github.com/bep/gallerydeluxe/blob/main/LICENSE"
|
||||
description = "Fast gallery suitable for lots of images."
|
||||
homepage = "https://github.com/bep/gallerydeluxe"
|
||||
demosite = "https://staticbattery.com/"
|
||||
tags = ["Gallery", "Responsive"]
|
||||
min_version = "0.90.0"
|
||||
[author]
|
||||
name = "Bjørn Erik Pedersen"
|
||||
homepage = "http://bep.is"
|
||||
4
themes/gallerydeluxe_starter/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
public/
|
||||
resources/
|
||||
assets/jsconfig.json
|
||||
.hugo_build.lock
|
||||
21
themes/gallerydeluxe_starter/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Bjørn Erik Pedersen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
18
themes/gallerydeluxe_starter/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
[](https://app.netlify.com/sites/gallerydeluxe/deploys)
|
||||
|
||||
This is a starter project for the [Gallery Deluxe](https://github.com/bep/gallerydeluxe) Hugo Module. You need [Hugo](https://gohugo.io/getting-started/installing/) and [Go](https://go.dev/dl/) to run this project.
|
||||
|
||||
1. Click on use "Use this template" and give your new GitHub project a suitable name.
|
||||
1. Edit `go.mod` and replace the path with your new GitHub project's path[^1].
|
||||
1. Edit `config.toml` etc. to match your setup and replace the images inside `content/images` with your own.
|
||||
1. Add your custom logo in [layouts/partials/logo.html](layouts/partials/logo.html)
|
||||
|
||||
This starter projects can be previewd at [gallerydeluxe.netlify.app](https://gallerydeluxe.netlify.app/). A bigger gallery can be found at [staticbattery.com](https://staticbattery.com/).
|
||||
|
||||
**Note:** This isn't a _theme_; it's meant to be used as a standalone Hugo project. You can edit/add/move files in this project as you please.
|
||||
|
||||
## Update theme
|
||||
|
||||
Run `hugo mod get -u` to update to a newer version of Gallery Deluxe if one exists.
|
||||
|
||||
[^1]: I wish GitHub's template project feature had support for variable replacements.
|
||||
28
themes/gallerydeluxe_starter/assets/css/styles.css
Normal file
@@ -0,0 +1,28 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #101010;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
|
||||
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 2;
|
||||
color: #fff;
|
||||
text-shadow: 2px 2px;
|
||||
}
|
||||
|
||||
.gd-modal-loaded .logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
cursor: pointer;
|
||||
color: #ffe0c9;
|
||||
}
|
||||
BIN
themes/gallerydeluxe_starter/assets/images/gopher-hero8.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
69
themes/gallerydeluxe_starter/config.toml
Normal file
@@ -0,0 +1,69 @@
|
||||
baseURL = "https://www.example.com/"
|
||||
title = "Gallery Deluxe"
|
||||
disableKinds = ["section", "taxonomy", "term"]
|
||||
|
||||
[params]
|
||||
# Set to enable Plausible Analytics.
|
||||
plausible_domain = ""
|
||||
|
||||
[params.gallerydeluxe]
|
||||
|
||||
# Shuffle the images in the gallery to give the impression of a new gallery each page load.
|
||||
shuffle = false
|
||||
|
||||
# Reverse the order of the images in the gallery.
|
||||
reverse = true
|
||||
|
||||
# Enable Exif data in the gallery.
|
||||
# See https://gohugo.io/content-management/image-processing/#exif-data for how to filter tags.
|
||||
enable_exif = true
|
||||
|
||||
# Optional watermark file for the large images.
|
||||
[params.gallerydeluxe.watermark]
|
||||
image = "images/gopher-hero8.png" # relative to /assets
|
||||
posx = "left" # one of "left", "center", "right"
|
||||
posy = "bottom" # one of "top", "center", "bottom"
|
||||
|
||||
[build]
|
||||
[[build.cacheBusters]]
|
||||
source = 'content/.*\.(png|jpg|jpeg)'
|
||||
target = '(json)'
|
||||
[[build.cacheBusters]]
|
||||
source = 'layouts/.*'
|
||||
target = '(json)'
|
||||
|
||||
[caches]
|
||||
[caches.images]
|
||||
dir = ':cacheDir/gallerydeluxe'
|
||||
maxAge = "4320h" # 6 months.
|
||||
|
||||
[imaging]
|
||||
resampleFilter = "CatmullRom"
|
||||
quality = 71
|
||||
anchor = "smart"
|
||||
[imaging.exif]
|
||||
disableDate = false
|
||||
disableLatLong = true
|
||||
includeFields = 'Artist|LensModel|FNumber|ExposureTime'
|
||||
|
||||
[server]
|
||||
[[server.headers]]
|
||||
for = '/**'
|
||||
[server.headers.values]
|
||||
Referrer-Policy = 'strict-origin-when-cross-origin'
|
||||
|
||||
[module]
|
||||
[[module.mounts]]
|
||||
source = "assets"
|
||||
target = "assets"
|
||||
[[module.mounts]]
|
||||
source = "layouts"
|
||||
target = "layouts"
|
||||
[[module.mounts]]
|
||||
source = "content"
|
||||
target = "content"
|
||||
[[module.mounts]]
|
||||
source = "static"
|
||||
target = "static"
|
||||
[[module.imports]]
|
||||
path = "github.com/bep/gallerydeluxe"
|
||||
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 359 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 174 KiB |
4
themes/gallerydeluxe_starter/content/images/index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: The Gallery
|
||||
headless: true
|
||||
---
|
||||
5
themes/gallerydeluxe_starter/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/bep/gallerydeluxe_starter
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/bep/gallerydeluxe v0.11.1 // indirect
|
||||
18
themes/gallerydeluxe_starter/go.sum
Normal file
@@ -0,0 +1,18 @@
|
||||
github.com/bep/gallerydeluxe v0.6.0 h1:HM32hGtRP93rRoSdlLne/IzjmJBIbtiuCkSbj/tBtho=
|
||||
github.com/bep/gallerydeluxe v0.6.0/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
github.com/bep/gallerydeluxe v0.7.0 h1:X+HoxJQOmpi08Re7wJRk0QsrLbNKYIgAxupmQQ66xKc=
|
||||
github.com/bep/gallerydeluxe v0.7.0/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
github.com/bep/gallerydeluxe v0.8.1 h1:PD3x1GVebSrfAxHDsebt8Trc9aZmF+PA4mXw6l/bABo=
|
||||
github.com/bep/gallerydeluxe v0.8.1/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
github.com/bep/gallerydeluxe v0.9.0 h1:l/FD9aUq78Yy/ltXF0CTAk3FwInCXXxIwCDKLNkoqjU=
|
||||
github.com/bep/gallerydeluxe v0.9.0/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
github.com/bep/gallerydeluxe v0.9.1 h1:Ca78oSIglf4gPu7yopQ4M/fsqiPQLqvXNMT2xX3KYRQ=
|
||||
github.com/bep/gallerydeluxe v0.9.1/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
github.com/bep/gallerydeluxe v0.9.2 h1:5dep3RdyiwUpMqzAjrwoKgM0O9y8fvR9J5QZk13/lZk=
|
||||
github.com/bep/gallerydeluxe v0.9.2/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
github.com/bep/gallerydeluxe v0.10.0 h1:0CXOArpCD6S0XdhFpXuShnlqx8VJBCrWhKdgi/xdRjE=
|
||||
github.com/bep/gallerydeluxe v0.10.0/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
github.com/bep/gallerydeluxe v0.11.0 h1:A5me+REJREWnHTC73yKeuXcC7g3V3s0DjgY7EDMwJMQ=
|
||||
github.com/bep/gallerydeluxe v0.11.0/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
github.com/bep/gallerydeluxe v0.11.1 h1:Bls2hBDY7WJ01TKtbmQT0Cl/Nwy8S/r5yj5qBaP7elo=
|
||||
github.com/bep/gallerydeluxe v0.11.1/go.mod h1:sUfRcjREo6DwxPo0sMp0TAxNZiGreOY6DY2dWGpSeR8=
|
||||
4
themes/gallerydeluxe_starter/hugo.work
Normal file
@@ -0,0 +1,4 @@
|
||||
go 1.19
|
||||
|
||||
use .
|
||||
use ../gallerydeluxe
|
||||
36
themes/gallerydeluxe_starter/layouts/index.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,
|
||||
initial-scale=1.0" />
|
||||
<title>{{ .Title }}</title>
|
||||
<link rel="icon" href="favicon.svg" />
|
||||
{{ partial "gallerydeluxe/head.html" . }}
|
||||
{{ partial "head.html" . }}
|
||||
</head>
|
||||
|
||||
<a class="logo" href="{{ site.Home.RelPermalink }}">
|
||||
{{ partial "logo.html" . }}
|
||||
</a>
|
||||
|
||||
{{/* init.hmlt takes either a slice of .images or a .sourcePath that points to a bundle with images.
|
||||
An .id will be calculated if not provided. This will be used to create the URL to the data file.
|
||||
*/}}
|
||||
{{ $bundle := site.GetPage "images" }}
|
||||
{{ $images := $bundle.Resources.ByType "image" }}
|
||||
{{ $gallery := partial "gallerydeluxe/init.html" (dict "sourcePath" "images") }}
|
||||
{{ $params := site.Params.gallerydeluxe }}
|
||||
{{/* We currently only support 1 gallery per page, which is create4 by an element with id 'gallerydeluxe',
|
||||
and a valid data url.
|
||||
*/}}
|
||||
<body id="gallerydeluxe" data-gd-image-data-url="{{ $gallery.imageDataUrl }}">
|
||||
<div id="gd-modal" class="gd-modal">
|
||||
<span id="gd-modal-close" class="gd-modal-close">×</span>
|
||||
{{ if $params.enable_exif }}
|
||||
<div id="gd-modal-exif" class="gd-modal-exif"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
10
themes/gallerydeluxe_starter/layouts/partials/head.html
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
{{ $css := resources.Get "css/styles.css" }}
|
||||
{{ if hugo.IsProduction}}
|
||||
{{ $css = $css | minify | fingerprint }}
|
||||
{{ end }}
|
||||
<link rel="stylesheet" href="{{ $css.RelPermalink }}"></link>
|
||||
{{ with site.Params.plausible_domain }}
|
||||
<script defer data-domain="{{ . }}" src="https://plausible.io/js/plausible.js"></script>
|
||||
{{ end }}
|
||||
|
||||
10
themes/gallerydeluxe_starter/layouts/partials/logo.html
Normal file
|
After Width: | Height: | Size: 11 KiB |
6
themes/gallerydeluxe_starter/netlify.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[build]
|
||||
publish = "public"
|
||||
command = "hugo --gc --minify -b $DEPLOY_PRIME_URL"
|
||||
|
||||
[build.environment]
|
||||
HUGO_VERSION = "0.104.2"
|
||||
5
themes/gallerydeluxe_starter/static/favicon.svg
Normal file
|
After Width: | Height: | Size: 9.1 KiB |