💾 Archived View for kota.nz › notes › zoooom captured on 2021-11-30 at 20:18:30. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
2021/08/09
Photography is a big hobby of mine -- many of my pictures are here on my site and can be considered CC BY-NC 4.0. On the web version of my site, I thought long and hard about how to display my pictures (on gemini that's all up to the client so I needn't worry about it 😋). I decided on a grid of images with one to three columns (depending on screen width) and varied height for showing different aspect ratios nicely.
The first step was figuring out how to represent my image collection with hugo. Originally, I was creating a blank markdown file for every image and using the frontmatter for storing a filename and "height" value. This kinda worked, but was a pain to create new images and felt like a weird hack. Eventually, I learned about creating and using json data files with hugo. I actually had a really hard time figuring this out. Hugo's documentation is very sparse, and the free hugo templates seem to use massive front-end js libraries just to render some basic text. Very disappointing. I wound up buying this book about Hugo to get a much better grasp of how everything worked. I then created the following layout:
<!DOCTYPE html> <html> {{- partial "head-pics.html" . -}} <body> <div class="container"> {{- partial "nav.html" . -}} <main class="index"> <section class="list"> <h1>{{ .Title }} <div class="rss"></div> </h1> <div class="grid"> {{ range .Site.Data.pics.pictures }} <span class="grid-item" style="grid-row-end: span {{ .height }};"> <picture class="zoom" data-full-size="/pics/{{ .name }}.full.webp"> <source srcset="/pics/{{ .name }}.thumb.webp" type="image/webp"> <source srcset="/pics/{{ .name }}.thumb.jpg" type="image/jpg"> <img src="/pics/{{ .name }}.full.jpg" width="100%" height="100%"> </picture> </span> {{ end }} </div> </section> </main> </div> </body> </html>
{ "pictures": [ { "name": "DSCF8909", "date": "2021:07:11T14:27:33", "height": "2" }, { "name": "DSCF8823", "date": "2021:07:11T13:16:27", "height": "3" }, { "name": "DSCF8822", "date": "2021:07:11T13:15:46", "height": "4" }, ] }
<div class="grid"> <span class="grid-item" style="grid-row-end: span 2;"> <picture data-full-size="/pics/DSCF8909.full.webp"> <source srcset="/pics/DSCF8909.thumb.webp" type="image/webp"> <source srcset="/pics/DSCF8909.thumb.jpg" type="image/jpg"> <img src="/pics/DSCF8909.full.jpg" width="100%" height="100%"> </picture> </span> <div>
The filename has an extension tacked on the end for each of the different stored versions (webp and jpeg thumbnails and full images). The grid-row-end: span 2; is set using the height value, larger values are "taller". Then, with some help from my friend Matthew, I was able to pad out some scss to place the images in a grid.
img { object-fit: cover; object-position: center; cursor: pointer; } .grid { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-auto-rows: 10vw; position: absolute; padding: 0 2%; left: 0; right: 0; @media only screen and (max-width: 850px) { grid-auto-rows: 15vw; grid-template-columns: 1fr 1fr; } @media only screen and (max-width: 500px) { grid-auto-rows: 30vw; grid-template-columns: 1fr; } } .grid-item { padding: 2%; grid-row-end: span 3; img { width: 100%; height: 100%; display: block; border-radius: 10px; } }
If you frequently battle with css you may notice my images are effectively cropped by this css. That's intentional -- I wanted my grid of images to look smooth and orderly, but now as a consequence some of my images have flawed framing. I decided to have my images zoom in when clicked on and "expand" into the proper aspect ratio. My friend Matthew was able to save the day once again by writing me a basic script to zoom an element.
// Outwardly accessable zoom parameters let zoomParams = { padding: [120, 120], zIndex: 2, backgroundColor: "#FFF" }; const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; if (prefersDark) { zoomParams.backgroundColor = "#000"; } else { zoomParams.backgroundColor = "#FFF"; } // Anonymous function so this script won't conflict with any others (() => { // Cache dom const zoomers = document.getElementsByClassName("zoom"); const body = document.getElementsByTagName("body")[0]; // Add the background html to manipulate later const background = document.createElement("div"); background.style.position = "fixed"; background.style.top = "0"; background.style.right = "0"; background.style.bottom = "0"; background.style.left = "0"; background.style.backgroundColor = zoomParams.backgroundColor; background.style.zIndex = zoomParams.zIndex.toString(); background.style.display = "none"; background.style.opacity = "0"; background.style.transition = "0.3s ease"; body.appendChild(background); // Main zooming function const zoooom = (event) => { // Declare variables const element = event.currentTarget; const clone = element.cloneNode(true); const location = element.getBoundingClientRect(); const width = location.width; const height = location.height; const viewportWidth = document.documentElement.clientWidth; const viewportHeight = document.documentElement.clientHeight; const scrollDistance = document.documentElement.scrollTop; // Transform values for zoomed image const scale = Math.min( (viewportWidth - zoomParams.padding[0]) / width, (viewportHeight - zoomParams.padding[1]) / height ); const translateX = (viewportWidth / 2) - location.left - (width / 2); const translateY = (viewportHeight / 2) - location.top - (height / 2); // Declare unzoom function for relevant element const unzoom = () => { // Undo the transform to return cloned element to the original position clone.style.transform = "unset"; background.style.opacity = "0"; // Wait for the cloned element to return setTimeout(() => { // Remove the cloned element clone.remove(); background.style.display = "none"; // Unbind the un-needed events document.removeEventListener("scroll", unzoom) clone.removeEventListener("click", unzoom) background.removeEventListener("click", unzoom) window.removeEventListener("resize", unzoom) }, 300); } // Put the cloned element in the document body.append(clone); // Apply styles to the cloned element clone.style.transition = "0.3s ease"; clone.style.position = "absolute"; clone.style.top = (scrollDistance + location.top).toString() + "px"; clone.style.left = location.left.toString() + "px"; clone.style.width = width.toString() + "px"; clone.style.height = height.toString() + "px"; clone.style.margin = "0"; clone.style.zIndex = (zoomParams.zIndex + 1).toString(); // Show the background background.style.display = "block"; // wait a moment to set the transform to ensure the transitin takes effect setTimeout(() => { clone.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`; background.style.opacity = "1"; }, 50); // Bind scroll, click, and window resize to unzoom document.addEventListener("scroll", unzoom); clone.addEventListener("click", unzoom); background.addEventListener("click", unzoom); window.addEventListener("resize", unzoom); } // Bind events to all zoomable elements for (zoomer of zoomers) { zoomer.addEventListener("click", zoooom); } })();
This script will zoom in elements with class="zoom" and unzoom if you scroll, click, or resize the window. The element is copied and a "background" div is created, then the background is faded in while the copied element is moved and resized with css translate() which is very fast and effecient. Translate has one big drawback due to it's speed, you can't animate the source aspect ratio changing, you can stretch the image, but not "expand" it into the proper ratio. I use this script for images in my notes and on my code page since they don't need the aspect ratio changed.
For my pics page I modified the script to "expand" the image while zooming it in. It looks pretty neat, it's slower than translate(), but now my images show the correct ratio. Additionally this new script swaps out the lower quality thumbnail image with a higher res version once it's zoomed in. Clicking on my photo of a boat shows the effect really well.
// Outwardly accessable zoom parameters let zoomParams = { padding: [40, 40], zIndex: 2, backgroundColor: "#FFF" }; const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; if (prefersDark) { zoomParams.backgroundColor = "#000"; } else { zoomParams.backgroundColor = "#FFF"; } // Anonymous function so this script won't conflict with any others (() => { // Cache dom const zoomers = document.getElementsByClassName("zoom"); const body = document.getElementsByTagName("body")[0]; // Add the background html to manipulate later const background = document.createElement("div"); background.style.position = "fixed"; background.style.top = "0"; background.style.right = "0"; background.style.bottom = "0"; background.style.left = "0"; background.style.backgroundColor = zoomParams.backgroundColor; background.style.zIndex = zoomParams.zIndex.toString(); background.style.display = "none"; background.style.opacity = "0"; background.style.transition = "0.3s ease"; body.appendChild(background); // Main zooming function const zoooom = (event) => { // Declare variables const element = event.currentTarget; const pic = element.getElementsByTagName('img')[0]; const clone = element.cloneNode(true); // Cropped display size const location = element.getBoundingClientRect(); const disWidth = location.width; const disHeight = location.height; // Original image size const natWidth = pic.naturalWidth; const natHeight = pic.naturalHeight; const viewportWidth = Math.max( document.documentElement.clientWidth || 0, window.innerWidth || 0 ); const viewportHeight = Math.max( document.documentElement.clientHeight || 0, window.innerHeight || 0 ); const scrollDistance = document.documentElement.scrollTop; // Transform values for zoomed image const scale = Math.min( (viewportWidth - zoomParams.padding[0]) / natWidth, (viewportHeight - zoomParams.padding[1]) / natHeight ); const scaleWidth = scale * natWidth; const scaleHeight = scale * natHeight; const scaleTop = (viewportHeight / 2) - (scaleHeight / 2) + scrollDistance; const scaleLeft = (viewportWidth / 2) - (scaleWidth / 2); // Declare unzoom function for relevant element const unzoom = () => { // return cloned element to the original position clone.style.transition = "0.3s"; clone.style.top = (scrollDistance + location.top).toString() + "px"; clone.style.left = location.left.toString() + "px"; clone.style.width = disWidth.toString() + "px"; clone.style.height = disHeight.toString() + "px"; background.style.opacity = "0"; // Wait for the cloned element to return setTimeout(() => { // Remove the cloned element clone.remove(); background.style.display = "none"; // Unbind the un-needed events document.removeEventListener("scroll", unzoom) clone.removeEventListener("click", unzoom) background.removeEventListener("click", unzoom) window.removeEventListener("resize", unzoom) }, 300); } // Put the cloned element in the document body.append(clone); // Apply styles to the cloned element clone.style.position = "absolute"; clone.style.top = (scrollDistance + location.top).toString() + "px"; clone.style.left = location.left.toString() + "px"; clone.style.width = disWidth.toString() + "px"; clone.style.height = disHeight.toString() + "px"; clone.style.margin = "0"; clone.style.zIndex = (zoomParams.zIndex + 1).toString(); // Show the background background.style.display = "block"; // wait a moment to set the transform to ensure the transition takes effect setTimeout(() => { clone.style.transition = "1s"; clone.style.top = scaleTop.toString() + "px"; clone.style.left = scaleLeft.toString() + "px"; clone.style.width = scaleWidth.toString() + "px"; clone.style.height = scaleHeight.toString() + "px"; background.style.opacity = "1"; // If there is data for a full size version if (clone.dataset.fullSize) { // Create a new image source and prepend it to the picture element const fullSizeSource = document.createElement("source"); fullSizeSource.srcset = clone.dataset.fullSize; clone.prepend(fullSizeSource); } }, 50); // Bind scroll, click, and window resize to unzoom document.addEventListener("scroll", unzoom); clone.addEventListener("click", unzoom); background.addEventListener("click", unzoom); window.addEventListener("resize", unzoom); } // Bind events to all zoomable elements for (zoomer of zoomers) { zoomer.addEventListener("click", zoooom); } })();