From 81a5507e8fe55b55b2569704dea4ce8de85c8863 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 09:56:27 -0700 Subject: [PATCH] Implement a photo carousel The layout of this is all CSS with scroll-snap. (Neat!) Also implement a JavaScript scroll event listener that detects when photos scroll and updates the caption and photo data. Refactor a few parts of the photo_exif_table into partials so they can be reused for the carousel items. --- assets/scripts/photo_carousel.js | 264 ++++++++++++++++++++++++ assets/styles/photos.css | 58 ++++++ layouts/partials/photo_exif_table.html | 10 +- layouts/partials/photos/latitude.html | 4 + layouts/partials/photos/longitude.html | 4 + layouts/partials/photos/megapixels.html | 8 + layouts/photos/single.html | 66 ++++-- 7 files changed, 393 insertions(+), 21 deletions(-) create mode 100644 assets/scripts/photo_carousel.js create mode 100644 layouts/partials/photos/latitude.html create mode 100644 layouts/partials/photos/longitude.html create mode 100644 layouts/partials/photos/megapixels.html diff --git a/assets/scripts/photo_carousel.js b/assets/scripts/photo_carousel.js new file mode 100644 index 0000000..d7ae0b2 --- /dev/null +++ b/assets/scripts/photo_carousel.js @@ -0,0 +1,264 @@ +// Eryn Wells + +class Carousel { + carousel = null; + + constructor(element) { + this.carousel = element; + } +} + +class PhotoParametersTable { + tableElement; + + #makeModelElement; + #latitudeElement; + #longitudeElement; + #megapixelsElement; + #widthElement; + #heightElement; + #isoElement; + #focalLengthElement; + #fNumberElement; + #exposureTimeElement; + + constructor(element) { + this.tableElement = element; + } + + setMakeModel(make, model) { + if (!this.#makeModelElement) { + this.#makeModelElement = this.#getElementWithQuerySelector("td.make-model"); + } + + if (!this.#makeModelElement) { + return; + } + + let makeModel = ""; + if (make && model) { + return `${make} ${model}`; + } else if (make) { + return make; + } else if (model) { + return model; + } else { + return ""; + } + + this.#makeModelElement.innerHTML = makeModel; + } + + setLocation(latitude, longitude) { + let latitudeElement = this.#latitudeElement; + if (!latitudeElement) { + latitudeElement = this.#getElementWithQuerySelector("td.location > data.latitude"); + this.#latitudeElement = latitudeElement; + } + + let longitudeElement = this.#longitudeElement; + if (!longitudeElement) { + longitudeElement = this.#getElementWithQuerySelector("td.location > data.longitude"); + this.#longitudeElement = longitudeElement; + } + + if (!latitudeElement || !longitudeElement) { + return; + } + + latitudeElement.innerHTML = latitude; + longitudeElement.innerHTML = longitude; + } + + setMegapixels(megapixels) { + let megapixelsElement = this.#megapixelsElement; + if (!megapixelsElement) { + megapixelsElement = this.#getElementWithQuerySelector("td.size > data.megapixels"); + this.#megapixelsElement = megapixelsElement; + } + + if (!megapixelsElement) { + return; + } + + megapixelsElement.innerHTML = megapixels; + } + + setSize(width, height) { + let widthElement = this.#widthElement; + if (!widthElement) { + widthElement = this.#getElementWithQuerySelector("td.size > data.width"); + this.#widthElement = widthElement; + } + + let heightElement = this.#heightElement; + if (heightElement) { + heightElement = this.#getElementWithQuerySelector("td.size > data.height"); + this.#heightElement = heightElement; + } + + if (!widthElement || !heightElement) { + return; + } + + widthElement.innerHTML = width; + heightElement.innerHTML = height; + } + + setISO(iso) { + let isoElement = this.#isoElement; + if (!isoElement) { + isoElement = this.#getElementWithQuerySelector("td.iso"); + this.#isoElement = isoElement; + } + + if (!isoElement) { + return; + } + + isoElement.innerHTML = `ISO ${iso}`; + } + + setFocalLength(focalLength) { + let focalLengthElement = this.#focalLengthElement; + if (!focalLengthElement) { + focalLengthElement = this.#getElementWithQuerySelector("td.focal-length"); + this.#focalLengthElement = focalLengthElement; + } + + if (!focalLengthElement) { + return; + } + + focalLengthElement.innerHTML = `${focalLength} mm`; + } + + setFNumber(f) { + let fNumberElement = this.#fNumberElement; + if (!fNumberElement) { + fNumberElement = this.#getElementWithQuerySelector("td.f-number"); + this.#fNumberElement = fNumberElement; + } + + if (!fNumberElement) { + return; + } + + fNumberElement.innerHTML = `ƒ${f}`; + } + + setExposureTime(exposureTime) { + let exposureTimeElement = this.#exposureTimeElement; + if (!exposureTimeElement) { + exposureTimeElement = this.#getElementWithQuerySelector("td.exposure-time"); + this.#exposureTimeElement = exposureTimeElement; + } + + if (!exposureTimeElement) { + return; + } + + exposureTimeElement.innerHTML = `${exposureTime} s`; + } + + #getElementWithQuerySelector(query) { + const element = this.tableElement.querySelector(query); + if (!element) { + return null; + } + return element; + } +} + +document.addEventListener("DOMContentLoaded", (event) => { + let photoParams = null; + + const photoParamsTableElement = document.querySelector(".photo-params table"); + if (photoParamsTableElement) { + photoParams = new PhotoParametersTable(photoParamsTableElement); + } + + document.querySelectorAll("figure.carousel").forEach(carouselElement => { + class CarouselItem { + element = null; + relativeX = 0; + isLeftOfContainerCenter = false; + + constructor(element, relativeX) { + this.element = element; + this.relativeX = relativeX + } + + get relativeMaxX() { + const rect = this.element.getBoundingClientRect(); + return this.relativeX + rect.width; + } + + get title() { return this.element.dataset.title; } + get latitude() { return this.element.dataset.latitude; } + get longitude() { return this.element.dataset.longitude; } + get megapixels() { return this.element.dataset.megapixels; } + get width() { return this.element.dataset.width; } + get height() { return this.element.dataset.height; } + get make() { return this.element.dataset.make; } + get model() { return this.element.dataset.model; } + get iso() { return this.element.dataset.iso; } + get focalLength() { return this.element.dataset.focalLength; } + get fNumber() { return this.element.dataset.fNumber; } + get exposureTime() { return this.element.dataset.exposureTime; } + } + + let captionElement = carouselElement.querySelector("figcaption"); + + let carouselRect = carouselElement.getBoundingClientRect(); + let carouselRectHorizontalCenter = carouselRect.width / 2.0; + + let carouselItems = Array.from(carouselElement.querySelectorAll("ul > li")).map(item => { + let itemRect = item.getClientRects()[0]; + let relativeX = itemRect.x - carouselRect.x; + return new CarouselItem(item, relativeX); + }); + + let previousScrollLeft = null; + let isScrollingLeft = true; + + carouselElement.querySelector("ul").addEventListener("scroll", event => { + const target = event.target; + const scrollLeft = target.scrollLeft; + + if (previousScrollLeft === null) { + carouselRect = target.getBoundingClientRect(); + carouselRectHorizontalCenter = carouselRect.width / 2.0; + } + + if (previousScrollLeft !== null) { + isScrollingLeft = scrollLeft > previousScrollLeft; + } + previousScrollLeft = scrollLeft; + + carouselItems.forEach(item => { + const itemRelativeXCoordinate = isScrollingLeft ? item.relativeX - scrollLeft : item.relativeMaxX - scrollLeft; + + const itemWasLeftOfContainerCenter = item.isLeftOfContainerCenter; + const itemIsLeftOfContainerCenter = itemRelativeXCoordinate < carouselRectHorizontalCenter; + item.isLeftOfContainerCenter = itemIsLeftOfContainerCenter; + + if ( (isScrollingLeft && (!itemWasLeftOfContainerCenter && itemIsLeftOfContainerCenter)) + || (!isScrollingLeft && (itemWasLeftOfContainerCenter && !itemIsLeftOfContainerCenter))) { + if (captionElement) { + captionElement.innerHTML = item.title; + } + + photoParams.setMakeModel(item.make, item.model); + photoParams.setLocation(item.latitude, item.longitude); + photoParams.setMegapixels(item.megapixels); + photoParams.setSize(item.width, item.height); + photoParams.setISO(item.iso); + photoParams.setFocalLength(item.focalLength); + photoParams.setFNumber(item.fNumber); + photoParams.setExposureTime(item.exposureTime); + } + }); + }); + }); +}); diff --git a/assets/styles/photos.css b/assets/styles/photos.css index e13bb06..aed5539 100644 --- a/assets/styles/photos.css +++ b/assets/styles/photos.css @@ -5,6 +5,8 @@ --photo-params-container-background-color: rgb(var(--super-lt-gray)); --photo-params-color: rgb(var(--sub-dk-gray)); --photo-params-border-color: rgb(var(--super-lt-gray)); + + --carousel-gradient-full-opaque: var(--background); } @media (prefers-color-scheme: dark) { :root { @@ -90,6 +92,62 @@ } } +.photos.page > figure.carousel { + position: relative; + max-width: 100%; + width: 100%; +} + +.photos.page > figure.carousel > ul { + align-items: stretch; + display: flex; + flex-flow: row nowrap; + gap: 0 calc(0.5 * var(--body-item-spacing)); + margin: 0; + overflow-x: scroll; + scroll-snap-type: x mandatory; + padding: 0; + padding-inline: var(--body-item-spacing); +} + +.photos.page > figure.carousel > ul::-webkit-scrollbar { + background: transparent; + width: 0; +} + +.photos.page > figure.carousel > .shadow { + position: absolute; + top: 0; + height: 100%; + width: var(--body-item-spacing); + z-index: 10; +} + +.photos.page > figure.carousel > .shadow.left { + background: linear-gradient( + to right, + rgba(var(--carousel-gradient-full-opaque), 1), + rgba(var(--carousel-gradient-full-opaque), 0)); + left: 0; +} + +.photos.page > figure.carousel > .shadow.right { + background: linear-gradient( + to left, + rgba(var(--carousel-gradient-full-opaque), 1), + rgba(var(--carousel-gradient-full-opaque), 0)); + right: 0; +} + +.photos.page > figure.carousel > ul > li { + scroll-snap-align: center; + margin: 0; + width: var(--content-width); + flex-shrink: 0; + list-style: none; + padding: 0; +} + .photo-params { width: 100%; } diff --git a/layouts/partials/photo_exif_table.html b/layouts/partials/photo_exif_table.html index 7f18b6d..55552db 100644 --- a/layouts/partials/photo_exif_table.html +++ b/layouts/partials/photo_exif_table.html @@ -9,10 +9,8 @@ {{ if and .Lat .Long }} - {{ $lat := float .Lat }}{{ $latDir := cond (eq $lat 0) "" (cond (gt $lat 0) "N" "S") }} - {{ .Lat | lang.FormatNumber (cond (ne $lat 0) 3 0) }}º{{ $latDir }}, - {{ $long := float .Long }}{{ $longDir := cond (eq $long 0) "" (cond (gt $long 0) "E" "W") }} - {{ .Long | lang.FormatNumber (cond (ne $long 0) 3 0) }}º{{ $longDir }} + {{ partial "photos/latitude.html" . }}, + {{ partial "photos/longitude.html" . }} {{ end }} {{ if and .Tags.PixelXDimension .Tags.PixelYDimension }} @@ -20,8 +18,8 @@ {{ $widthpx := .Tags.PixelXDimension }} {{ $heightpx := .Tags.PixelYDimension }} {{ if and (gt $widthpx 0) (gt $heightpx 0) }} - {{ $megapixels := div (mul $widthpx $heightpx) 1e6 }} - {{ $megapixels | lang.FormatNumber 0 }} MP • {{ $widthpx }} × {{ $heightpx }} + {{ partial "photos/megapixels.html" .Tags }}{{ + $widthpx }} × {{ $heightpx }} {{ end }} {{ end }} diff --git a/layouts/partials/photos/latitude.html b/layouts/partials/photos/latitude.html new file mode 100644 index 0000000..6c1842d --- /dev/null +++ b/layouts/partials/photos/latitude.html @@ -0,0 +1,4 @@ +{{ $lat := float .Lat }} +{{ $latDir := cond (eq $lat 0) "" (cond (gt $lat 0) "N" "S") }} +{{ $formattedLat := printf "%sº%s" (.Lat | lang.FormatNumber (cond (ne $lat 0) 3 0)) $latDir }} +{{ return $formattedLat }} diff --git a/layouts/partials/photos/longitude.html b/layouts/partials/photos/longitude.html new file mode 100644 index 0000000..8fe93d1 --- /dev/null +++ b/layouts/partials/photos/longitude.html @@ -0,0 +1,4 @@ +{{ $long := float .Long }} +{{ $longDir := cond (eq $long 0) "" (cond (gt $long 0) "E" "W") }} +{{ $formattedLong := printf "%sº%s" (.Long | lang.FormatNumber (cond (ne $long 0) 3 0)) $longDir }} +{{ return $formattedLong }} diff --git a/layouts/partials/photos/megapixels.html b/layouts/partials/photos/megapixels.html new file mode 100644 index 0000000..401bedd --- /dev/null +++ b/layouts/partials/photos/megapixels.html @@ -0,0 +1,8 @@ +{{ $widthpx := .PixelXDimension }} +{{ $heightpx := .PixelYDimension }} +{{ $megapixels := 0 }} +{{ if and (gt $widthpx 0) (gt $heightpx 0) }} + {{ $megapixels = div (mul $widthpx $heightpx) 1e6 }} +{{ end }} +{{ $formattedMegapixels := printf "%.0f MP" $megapixels }} +{{ return $formattedMegapixels }} diff --git a/layouts/photos/single.html b/layouts/photos/single.html index 2eab4b2..ee5c676 100644 --- a/layouts/photos/single.html +++ b/layouts/photos/single.html @@ -12,29 +12,56 @@ {{ errorf "Missing photo from photos page %q" .Path }} {{ end }} +{{- $firstImage := index $photos 0 -}} {{ if eq (len $photos) 1 }} - {{- $img := index $photos 0 -}} -
{{ . }}
- - {{ .Content }} - - {{- if .Params.photo_details | default true -}} - {{- partial "photo_exif_table.html" $img.Exif -}} - - {{- if in ($.Site.BaseURL | string) "localhost" -}} - {{- partial "development/photo_exif_table.html" $img.Exif -}} - {{- end -}} - {{- end -}} -{{ else }}
-
+{{ else }} + {{ end }} +
+ {{ .Content }} +
+ +{{- if .Params.photo_details | default true -}} + {{- partial "photo_exif_table.html" $firstImage.Exif -}} + + {{- if in ($.Site.BaseURL | string) "localhost" -}} + {{- partial "development/photo_exif_table.html" $firstImage.Exif -}} + {{- end -}} +{{- end -}} +
{{ partial "footer_tags.html" . }}
@@ -43,3 +70,12 @@ {{ define "footer" }} {{ partial "footer.html" . }} {{ end }} + +{{ define "scripts" }} +{{- $photos := partial "photos/list.html" . -}} +{{ if gt (len $photos) 1 }} + {{ with resources.Get "scripts/photo_carousel.js" | fingerprint "md5" }} + + {{ end }} +{{ end }} +{{ end }}