This commit is contained in:
Julieñ
2024-05-21 09:41:48 +02:00
parent 4c356a8af2
commit 1bb184cfe4
1646 changed files with 3456 additions and 1 deletions

20
themes/gallerydeluxe/.gitignore vendored Normal file
View 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

View 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.

View 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.

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

View File

@@ -0,0 +1,3 @@
import GalleryDeluxe from '../src/index.js';
GalleryDeluxe.init();

View 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 });
}

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

View 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)';
};

View File

@@ -0,0 +1 @@
# No options for now.

View 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'

View 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 => ../

View 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=

View File

@@ -0,0 +1,4 @@
go 1.19
use .
use ../../gallerydeluxe_starter

View File

@@ -0,0 +1,3 @@
module github.com/bep/gallerydeluxe
go 1.19

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

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

View File

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

View File

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

View 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"

View 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"

View File

@@ -0,0 +1,4 @@
public/
resources/
assets/jsconfig.json
.hugo_build.lock

View 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.

View File

@@ -0,0 +1,18 @@
[![Netlify Status](https://api.netlify.com/api/v1/badges/fe020ffb-29ff-409c-905b-6f00175091d0/deploy-status)](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.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -0,0 +1,4 @@
---
title: The Gallery
headless: true
---

View File

@@ -0,0 +1,5 @@
module github.com/bep/gallerydeluxe_starter
go 1.19
require github.com/bep/gallerydeluxe v0.11.1 // indirect

View 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=

View File

@@ -0,0 +1,4 @@
go 1.19
use .
use ../gallerydeluxe

View 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">&times;</span>
{{ if $params.enable_exif }}
<div id="gd-modal-exif" class="gd-modal-exif"></div>
{{ end }}
</div>
</body>
</html>

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,6 @@
[build]
publish = "public"
command = "hugo --gc --minify -b $DEPLOY_PRIME_URL"
[build.environment]
HUGO_VERSION = "0.104.2"

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB