From 81a5507e8fe55b55b2569704dea4ce8de85c8863 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 09:56:27 -0700 Subject: [PATCH 1/9] 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 }} From baad04e7a44a06f73dcc67fee5234385a1abfc5b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 10:03:11 -0700 Subject: [PATCH 2/9] Couple'a code formatting tweaks to section_css.thml and single_styles.html --- layouts/partials/resources/section_css.html | 2 +- layouts/partials/single_styles.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/layouts/partials/resources/section_css.html b/layouts/partials/resources/section_css.html index 0801efb..f7f26fd 100644 --- a/layouts/partials/resources/section_css.html +++ b/layouts/partials/resources/section_css.html @@ -5,7 +5,7 @@ {{ else }} {{ $sectionStylesheet = printf "styles/%s.css" .Section }} {{ end }} -{{ with resources.Get $sectionStylesheet }} +{{ with resources.Get $sectionStylesheet }} {{ $sectionCSS = . | fingerprint "md5" }} {{ end }} {{ return $sectionCSS }} diff --git a/layouts/partials/single_styles.html b/layouts/partials/single_styles.html index 287f7c7..dc636b4 100644 --- a/layouts/partials/single_styles.html +++ b/layouts/partials/single_styles.html @@ -1,4 +1,4 @@ {{- range .Resources.Match "*.css" -}} {{- $stylesheet := . | fingerprint "md5" }} - + {{- end -}} From 00d2e9263d9d47e517e99c86607392bf21ebde84 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 18:34:43 -0700 Subject: [PATCH 3/9] Add a badge to the photo list items that have multiple photos in them --- assets/styles/photos.css | 36 ++++++++++++++++++++++-- layouts/photos/li_thumbnail_in_grid.html | 5 ++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/assets/styles/photos.css b/assets/styles/photos.css index aed5539..b90a71d 100644 --- a/assets/styles/photos.css +++ b/assets/styles/photos.css @@ -1,6 +1,8 @@ :root { --date-item-background-color: rgb(var(--lt-gray)); + --photo-grid-item-size: 200px; + --photo-params-background-color: rgb(var(--lt-gray)); --photo-params-container-background-color: rgb(var(--super-lt-gray)); --photo-params-color: rgb(var(--sub-dk-gray)); @@ -28,7 +30,7 @@ .photos.list { display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(var(--photo-grid-item-size), 1fr)); gap: 4px; } @@ -36,6 +38,10 @@ margin-block-end: 0; } +.photos.list > :has(img) { + aspect-ratio: 1; +} + .photos.page > nav { margin-block-end: var(--body-item-spacing); } @@ -43,11 +49,37 @@ .photos.list > a { display: block; line-height: 0; + position: relative; +} +.photos.list > a:hover { + text-decoration: none; } .photos.list > a > img { border-radius: 3px; image-orientation: from-image; + position: absolute; + top: 0; + left: 0; +} + +.photos.list > a > .badge { + align-items: center; + aspect-ratio: 1; + background: rgba(255, 255, 255, 0.6); + border-radius: 4px; + color: rgb(var(--mid-gray)); + display: flex; + left: 4px; + line-height: 0; + padding: 1rem; + position: relative; + top: 4px; + width: max-content; + z-index: 1; +} +.photos.list > a > .badge:hover { + text-decoration: none; } .photos.list > div { @@ -82,7 +114,7 @@ content: "⏵︎"; font-size: 80%; } -@media (max-width: calc(24px + 400px + 4px)) { +@media (max-width: calc(24px + 400px + 9px)) { .photos.list > div > h6 > span { width: max-content; padding-block: calc(0.25 * var(--body-item-spacing)); diff --git a/layouts/photos/li_thumbnail_in_grid.html b/layouts/photos/li_thumbnail_in_grid.html index e35b156..5d9890e 100644 --- a/layouts/photos/li_thumbnail_in_grid.html +++ b/layouts/photos/li_thumbnail_in_grid.html @@ -1,6 +1,11 @@ {{- $thumbnail := partial "photos/thumbnail.html" (dict "Page" . "Width" 600 "Height" 600) -}} {{- $thumbnail = $thumbnail.Crop "600x600" -}} {{- $altText := $thumbnail.Params.alt -}} +{{- $numberOfImages := len (partial "photos/list.html" .) -}} +{{- $hasMultipleImages := gt $numberOfImages 1 -}} + {{ if $hasMultipleImages }} +
{{ $numberOfImages }}
+ {{ end }} {{ . }}
From af957c015033c6a3ae3b4d017d7be917cd08a2f0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 19:45:23 -0700 Subject: [PATCH 4/9] Pull carousels array out into a variable and bail early if there are no carousels --- assets/scripts/photo_carousel.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/assets/scripts/photo_carousel.js b/assets/scripts/photo_carousel.js index d7ae0b2..1b6cc3a 100644 --- a/assets/scripts/photo_carousel.js +++ b/assets/scripts/photo_carousel.js @@ -171,6 +171,16 @@ class PhotoParametersTable { } document.addEventListener("DOMContentLoaded", (event) => { + let allCarousels = Array.from(document.querySelectorAll("figure.carousel")).sort((a, b) => { + const aRect = a.getBoundingClientRect(); + const bRect = a.getBoundingClientRect(); + return aRect.top - bRect.top; + }); + + if (allCarousels.length === 0) { + return; + } + let photoParams = null; const photoParamsTableElement = document.querySelector(".photo-params table"); @@ -178,7 +188,7 @@ document.addEventListener("DOMContentLoaded", (event) => { photoParams = new PhotoParametersTable(photoParamsTableElement); } - document.querySelectorAll("figure.carousel").forEach(carouselElement => { + allCarousels.forEach(carouselElement => { class CarouselItem { element = null; relativeX = 0; From bb655bcf30c6f4c774d44d6394bd40c65fc2d367 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 5 Apr 2023 08:13:27 -0700 Subject: [PATCH 5/9] Move photo_carousel.js to assets/photos/carousel.js --- assets/scripts/{photo_carousel.js => photos/carousel.js} | 0 layouts/photos/single.html | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename assets/scripts/{photo_carousel.js => photos/carousel.js} (100%) diff --git a/assets/scripts/photo_carousel.js b/assets/scripts/photos/carousel.js similarity index 100% rename from assets/scripts/photo_carousel.js rename to assets/scripts/photos/carousel.js diff --git a/layouts/photos/single.html b/layouts/photos/single.html index ee5c676..41e0bf4 100644 --- a/layouts/photos/single.html +++ b/layouts/photos/single.html @@ -74,8 +74,8 @@ {{ define "scripts" }} {{- $photos := partial "photos/list.html" . -}} {{ if gt (len $photos) 1 }} - {{ with resources.Get "scripts/photo_carousel.js" | fingerprint "md5" }} - - {{ end }} + {{- with resources.Get "scripts/photos/carousel.js" | fingerprint "md5" -}} + + {{- end -}} {{ end }} {{ end }} From ad9f83ed39465620080cbb5a6d52a3253f81bdf4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 5 Apr 2023 08:14:19 -0700 Subject: [PATCH 6/9] Refactor carousel code so more of it is in the Carousel class Load the script as a module --- assets/scripts/photos/carousel.js | 212 +++++++++++++++++------------- layouts/photos/single.html | 6 +- 2 files changed, 124 insertions(+), 94 deletions(-) diff --git a/assets/scripts/photos/carousel.js b/assets/scripts/photos/carousel.js index 1b6cc3a..827d9e6 100644 --- a/assets/scripts/photos/carousel.js +++ b/assets/scripts/photos/carousel.js @@ -2,10 +2,109 @@ class Carousel { carousel = null; + #items = null; + #caption = null; constructor(element) { this.carousel = element; + this.#caption = element.querySelector("figcaption"); } + + get items() { + if (this.#items === null) { + const boundingRect = this.carousel.getBoundingClientRect(); + this.#items = Array.from(this.carousel.querySelectorAll("ul > li")).map(item => { + let itemRect = item.getClientRects()[0]; + let relativeX = itemRect.x - boundingRect.x; + return new CarouselItem(item, relativeX); + }); + } + + return this.#items; + } + + get scrollContainer() { + return this.carousel.querySelector("ul"); + } + + setCaption(caption) { + if (!this.#caption) { + return; + } + this.#caption.innerHTML = caption; + } + + scrollTo(index) { + this.scrollContainer.scrollTo(this.items[index].relativeX, 0); + } + + setUpScrollEventListener(photoParametersTable) { + let previousScrollLeft = null; + let isScrollingLeft = true; + + this.scrollContainer.addEventListener("scroll", event => { + const target = event.target; + const scrollLeft = target.scrollLeft; + + const carouselRect = target.getBoundingClientRect(); + const carouselRectHorizontalCenter = carouselRect.width / 2.0; + + if (previousScrollLeft !== null) { + isScrollingLeft = scrollLeft > previousScrollLeft; + } + previousScrollLeft = scrollLeft; + + this.items.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))) { + this.setCaption(item.title); + photoParametersTable.setMakeModel(item.make, item.model); + photoParametersTable.setLocation(item.latitude, item.longitude); + photoParametersTable.setMegapixels(item.megapixels); + photoParametersTable.setSize(item.width, item.height); + photoParametersTable.setISO(item.iso); + photoParametersTable.setFocalLength(item.focalLength); + photoParametersTable.setFNumber(item.fNumber); + photoParametersTable.setExposureTime(item.exposureTime); + } + }); + }); + } +} + +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; } } class PhotoParametersTable { @@ -171,104 +270,35 @@ class PhotoParametersTable { } document.addEventListener("DOMContentLoaded", (event) => { - let allCarousels = Array.from(document.querySelectorAll("figure.carousel")).sort((a, b) => { - const aRect = a.getBoundingClientRect(); - const bRect = a.getBoundingClientRect(); - return aRect.top - bRect.top; - }); - - if (allCarousels.length === 0) { + const carouselElements = document.querySelectorAll("figure.carousel"); + if (carouselElements.length === 0) { return; } - let photoParams = null; + const allCarousels = Array.from(carouselElements).sort((a, b) => { + const aRect = a.getBoundingClientRect(); + const bRect = a.getBoundingClientRect(); + return aRect.top - bRect.top; + }).map(elem => new Carousel(elem)); + let photoParametersTable = null; const photoParamsTableElement = document.querySelector(".photo-params table"); if (photoParamsTableElement) { - photoParams = new PhotoParametersTable(photoParamsTableElement); + photoParametersTable = new PhotoParametersTable(photoParamsTableElement); } - allCarousels.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); - } - }); - }); + allCarousels.forEach(carousel => { + carousel.setUpScrollEventListener(photoParametersTable); }); + + const url = new URL(window.location); + let photoGetParameter = url.searchParams.get("photo"); + if (photoGetParameter) { + try { + photoGetParameter = parseInt(photoGetParameter); + allCarousels[0].scrollTo(photoGetParameter); + } catch (e) { + console.error("Unable to parse 'photo' GET parameter as integer:", e); + } + } }); diff --git a/layouts/photos/single.html b/layouts/photos/single.html index 41e0bf4..90b2404 100644 --- a/layouts/photos/single.html +++ b/layouts/photos/single.html @@ -73,9 +73,9 @@ {{ define "scripts" }} {{- $photos := partial "photos/list.html" . -}} -{{ if gt (len $photos) 1 }} +{{ if gt (len $photos) 1 -}} {{- with resources.Get "scripts/photos/carousel.js" | fingerprint "md5" -}} - + {{- end -}} -{{ end }} +{{- end }} {{ end }} From da994bd6b73e79e1e45c871f03c3275f3046af66 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 5 Apr 2023 08:16:44 -0700 Subject: [PATCH 7/9] Add a JavaScript file for the photos list --- assets/scripts/photos/list.js | 111 ++++++++++++++++++++++++++++++++++ layouts/photos/list.html | 6 ++ 2 files changed, 117 insertions(+) create mode 100644 assets/scripts/photos/list.js diff --git a/assets/scripts/photos/list.js b/assets/scripts/photos/list.js new file mode 100644 index 0000000..370e6d1 --- /dev/null +++ b/assets/scripts/photos/list.js @@ -0,0 +1,111 @@ +// Eryn Wells + +class Item { + static allItems = []; + + element; + + #mouseoverRegionOffsets; + #currentRegion = 0; + + constructor(element) { + this.element = element; + this.setUpEventHandlers(); + + element._item = this; + } + + get title() { + return this.element.getAttribute("title"); + } + + get numberOfImages() { + const numberOfImages = this.element.dataset.numberOfImages; + if (typeof numberOfImages === "undefined" || numberOfImages === null) { + return 1; + } + + try { + return parseInt(numberOfImages); + } catch (e) { + console.error("Unable to parse data-number-of-images attribute:", numberOfImages); + return 1; + } + } + + get mouseoverRegionOffsets() { + let offsets = this.#mouseoverRegionOffsets; + + if (!offsets) { + const numberOfImages = this.numberOfImages; + offsets = new Array(numberOfImages); + + const rect = this.element.getBoundingClientRect(); + const widthOfRegion = rect.width / numberOfImages; + for (let i = 0; i < numberOfImages; i++) { + offsets[i] = i * widthOfRegion; + } + + this.#mouseoverRegionOffsets = offsets; + } + + return offsets; + } + + setUpEventHandlers() { + if (this.numberOfImages <= 1) { + return; + } + + this.element.addEventListener("mousemove", event => { + const offsetX = event.offsetX; + + let indexOfRegion = -1; + for (let regionOffset of this.mouseoverRegionOffsets) { + if (regionOffset > offsetX) { + break; + } + indexOfRegion += 1; + } + + if (indexOfRegion !== this.#currentRegion) { + console.debug(`Mouse moved at ${offsetX}, ${event.offsetY} in cell for '${this.title}' in region ${indexOfRegion}`); + // TODO: Set image to images[indexOfRegion] + this.#currentRegion = indexOfRegion; + } + }); + + this.element.addEventListener("mouseout", event => { + console.debug(`Mouse left cell for '${this.title}'`); + // TODO: Reset to the first image. + }); + } + + addToIntersectionObserver(intersectionObserver) { + intersectionObserver.observe(this.element); + } + + handleIntersectionObservation(entry, observer) { + if (entry.isIntersecting) { + console.debug(`Cell for ${this.title} entered the viewport`); + } else { + console.debug(`Cell for ${this.title} left the viewport`); + } + } +} + +let intersectionObserver; + +document.addEventListener("DOMContentLoaded", event => { + intersectionObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + entry.target._item.handleIntersectionObservation(entry, observer); + }); + }, { threshold: 0.5 }); + + Item.allItems = Array.from(document.querySelectorAll(".photos.list > a")).map(cell => { + const item = new Item(cell) + item.addToIntersectionObserver(intersectionObserver); + return item; + }); +}); diff --git a/layouts/photos/list.html b/layouts/photos/list.html index 9153577..f33bfdd 100644 --- a/layouts/photos/list.html +++ b/layouts/photos/list.html @@ -17,3 +17,9 @@ {{ define "footer" }} {{ partial "footer.html" . }} {{ end }} + +{{ define "scripts" }} +{{- with resources.Get "scripts/photos/list.js" | fingerprint "md5" -}} + +{{- end -}} +{{ end }} From afb59e7862d48f435240026636e6bc5182c90a38 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 6 Apr 2023 08:42:58 -0700 Subject: [PATCH 8/9] Prepare thumbnails of all images in the photo post Add tags for all thumbnails to the photos list entry. I'm hoping with lazy loading and some JavaScript this will not be terrible! Break the logic for preparing a thumbnail into a separate helper partial template so it can be reused. --- .../partials/photos/thumbnail_for_list.html | 28 ++++++++++ layouts/partials/photos/thumbnails.html | 51 +++++++++++++++++++ layouts/photos/li_thumbnail_in_grid.html | 12 ++--- 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 layouts/partials/photos/thumbnail_for_list.html create mode 100644 layouts/partials/photos/thumbnails.html diff --git a/layouts/partials/photos/thumbnail_for_list.html b/layouts/partials/photos/thumbnail_for_list.html new file mode 100644 index 0000000..952218f --- /dev/null +++ b/layouts/partials/photos/thumbnail_for_list.html @@ -0,0 +1,28 @@ +{{ $thumbnailResource := .Image }} + +{{ $orientation := partial "images/orientation_angle.html" $thumbnailResource }} + +{{ $targetWidth := 0 }} +{{ if isset . "Width" }} + {{ $targetWidth = .Width }} +{{ else }} + {{ $targetWidth = $thumbnailResource.Width }} +{{ end }} + +{{ $targetHeight := 0 }} +{{ if isset . "Height" }} + {{ $targetHeight = .Height }} +{{ else }} + {{ $targetHeight = $thumbnailResource.Height }} +{{ end }} + +{{ $thumbnail := false }} +{{ if not (and (eq $orientation 0) + (eq $targetWidth $thumbnailResource.Width) + (eq $targetHeight $thumbnailResource.Height)) }} + {{ $thumbnail = $thumbnailResource.Fit (printf "%dx%d r%d" $targetWidth $targetHeight (sub 360 $orientation)) }} +{{ else }} + {{ $thumbnail = $thumbnailResource }} +{{ end }} + +{{ return $thumbnail }} diff --git a/layouts/partials/photos/thumbnails.html b/layouts/partials/photos/thumbnails.html new file mode 100644 index 0000000..06ddb01 --- /dev/null +++ b/layouts/partials/photos/thumbnails.html @@ -0,0 +1,51 @@ +{{ $page := .Page }} + +{{ $thumbnails := slice }} +{{ with $thumbnailNamesList := $page.Params "thumbnails" }} + {{/* Specify a list of thumbnails to use with the page "thumbnails" param */}} + {{ range $thumbnailNamesList }} + {{ with $page.Resources.GetMatch . }} + {{ $thumbnails = $thumbnails | append . }} + {{ else }} + {{ errorf "No image resources available for %s from thumbnails param" . }} + {{ end }} + {{ end }} +{{ else }} + {{/* Get a list of all the image resources and see if any of them specify + thumbnails for themselves. If they do, use those, otherwise use the image + resource itself. */}} + {{ range $img := $page.Resources.ByType "image" }} + {{ with $thumbnailResourceName := $img.params.thumbnail }} + {{ with $page.Resources.GetMatch $thumbnailResourceName }} + {{ $thumbnails = $thumbnails | append . }} + {{ else }} + {{ errorf "No image resources available for '%s' from thumbnail param for image resources %s" + $thumbnailResourceName + $img.Name }} + {{ end }} + {{ else }} + {{/* TODO: Look for a named image that indicates it's a thumbnail of + another resources. Something like ${imgBasename}_thumbnail.${ext}. + I don't know how easy Hugo makes filename processing, so this could + be painful. */}} + {{ $thumbnails = $thumbnails | append $img }} + {{ end }} + {{ end }} +{{ end }} + +{{ if eq (len $thumbnails) 0 }} + {{ errorf "Couldn't find any thumbnails for %s" $page.Permalink }} +{{ end }} + +{{ $thumbnailOptions := dict }} +{{ if isset . "Width" }}{{ $thumbnailOptions = $thumbnailOptions | merge (dict "Width" .Width) }}{{ end }} +{{ if isset . "Height" }}{{ $thumbnailOptions = $thumbnailOptions | merge (dict "Height" .Height) }}{{ end }} + +{{ $processedThumbnails := slice }} +{{ range $thumbnails }} + {{ $options := $thumbnailOptions | merge "Image" . }} + {{ $processedThumbnail := partial "photos/thumbnail_for_list.html" $options }} + {{ $processedThumbnails = $processedThumbnails | append $processedThumbnail }} +{{ end }} + +{{ return $processedThumbnails }} diff --git a/layouts/photos/li_thumbnail_in_grid.html b/layouts/photos/li_thumbnail_in_grid.html index 5d9890e..d7c9418 100644 --- a/layouts/photos/li_thumbnail_in_grid.html +++ b/layouts/photos/li_thumbnail_in_grid.html @@ -1,11 +1,11 @@ {{- $thumbnail := partial "photos/thumbnail.html" (dict "Page" . "Width" 600 "Height" 600) -}} {{- $thumbnail = $thumbnail.Crop "600x600" -}} -{{- $altText := $thumbnail.Params.alt -}} {{- $numberOfImages := len (partial "photos/list.html" .) -}} {{- $hasMultipleImages := gt $numberOfImages 1 -}} - - {{ if $hasMultipleImages }} -
{{ $numberOfImages }}
- {{ end }} - {{ . }} +
+ {{ if $hasMultipleImages }}
{{ $numberOfImages }}
{{ end }} + {{ range partial "photos/thumbnails.html" (dict "Page" . "Height" 600 "Width" 600) -}} + {{- $altText := .Params.alt -}} + {{ . }} + {{- end }}
From 6dbf3da09b8c23a21b84249446d837a2a0dd001d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 6 Apr 2023 08:48:57 -0700 Subject: [PATCH 9/9] WIP carousel post for the Temple of Hephaestus --- .../photos/2022/temple-of-hephaestus/IMG_3771.jpeg | 3 +++ .../photos/2022/temple-of-hephaestus/IMG_3794.jpeg | 3 +++ content/photos/2022/temple-of-hephaestus/index.md | 14 ++++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 content/photos/2022/temple-of-hephaestus/IMG_3771.jpeg create mode 100644 content/photos/2022/temple-of-hephaestus/IMG_3794.jpeg create mode 100644 content/photos/2022/temple-of-hephaestus/index.md diff --git a/content/photos/2022/temple-of-hephaestus/IMG_3771.jpeg b/content/photos/2022/temple-of-hephaestus/IMG_3771.jpeg new file mode 100644 index 0000000..1e12b53 --- /dev/null +++ b/content/photos/2022/temple-of-hephaestus/IMG_3771.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24f36ad3a467713f846f7816429e646ca433eadf7132c628e34a04dcaa839757 +size 4278990 diff --git a/content/photos/2022/temple-of-hephaestus/IMG_3794.jpeg b/content/photos/2022/temple-of-hephaestus/IMG_3794.jpeg new file mode 100644 index 0000000..dd943f0 --- /dev/null +++ b/content/photos/2022/temple-of-hephaestus/IMG_3794.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0fbece7eb21b370ed75658d5b82501bd6f49737cd5a5db4988cceecf829083b +size 4152181 diff --git a/content/photos/2022/temple-of-hephaestus/index.md b/content/photos/2022/temple-of-hephaestus/index.md new file mode 100644 index 0000000..60d024d --- /dev/null +++ b/content/photos/2022/temple-of-hephaestus/index.md @@ -0,0 +1,14 @@ +--- +title: "Temple of Hephaestus" +date: 2022-07-30T04:30:43-07:00 +series: Greece +categories: Travel +resources: + - title: The Temple of Hephaestus from the Agora + src: IMG_3771.jpeg + - title: The front of the Temple of Hephaestus + src: IMG_3794.jpeg +--- + +This temple is the best preserved building on the site of the Ancient Agora of +Athens, {{< lang gr >}}Αρχαία Αγορά της Αθήνας{{< /lang >}}.