From f5fd3589224e91036855ce48f67994f80d08f699 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 09:56:27 -0700 Subject: [PATCH 1/4] 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 a053e7d14d0befd9f4a2786e13178f6d9c7e78c1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 10:03:11 -0700 Subject: [PATCH 2/4] 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 0504ba54e3ddd0729318fce46b1d9eb5c80945b8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 10:01:03 -0700 Subject: [PATCH 3/4] Carousel script debugging --- assets/scripts/photo_carousel.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/assets/scripts/photo_carousel.js b/assets/scripts/photo_carousel.js index d7ae0b2..db4c3b3 100644 --- a/assets/scripts/photo_carousel.js +++ b/assets/scripts/photo_carousel.js @@ -164,6 +164,7 @@ class PhotoParametersTable { #getElementWithQuerySelector(query) { const element = this.tableElement.querySelector(query); if (!element) { + console.error(`Couldn't find ${query} element in`, this.tableElement); return null; } return element; @@ -216,13 +217,19 @@ document.addEventListener("DOMContentLoaded", (event) => { let carouselItems = Array.from(carouselElement.querySelectorAll("ul > li")).map(item => { let itemRect = item.getClientRects()[0]; let relativeX = itemRect.x - carouselRect.x; + console.debug("Item at relative x:", relativeX); return new CarouselItem(item, relativeX); }); + let previousTimeoutID = null; let previousScrollLeft = null; let isScrollingLeft = true; carouselElement.querySelector("ul").addEventListener("scroll", event => { + if (previousTimeoutID) { + clearTimeout(previousTimeoutID); + } + const target = event.target; const scrollLeft = target.scrollLeft; @@ -236,6 +243,8 @@ document.addEventListener("DOMContentLoaded", (event) => { } previousScrollLeft = scrollLeft; + console.debug("isScrollingLeft =", isScrollingLeft); + carouselItems.forEach(item => { const itemRelativeXCoordinate = isScrollingLeft ? item.relativeX - scrollLeft : item.relativeMaxX - scrollLeft; @@ -245,6 +254,8 @@ document.addEventListener("DOMContentLoaded", (event) => { if ( (isScrollingLeft && (!itemWasLeftOfContainerCenter && itemIsLeftOfContainerCenter)) || (!isScrollingLeft && (itemWasLeftOfContainerCenter && !itemIsLeftOfContainerCenter))) { + console.debug("Item crossed container center", item); + if (captionElement) { captionElement.innerHTML = item.title; } @@ -259,6 +270,11 @@ document.addEventListener("DOMContentLoaded", (event) => { photoParams.setExposureTime(item.exposureTime); } }); + + previousTimeoutID = setTimeout(() => { + console.debug(carouselItems); + previousScrollLeft = null; + }, 500); }); }); }); From 4a647e854f443aedb7cbaeafb1edf25fd3792370 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 2 Apr 2023 10:05:29 -0700 Subject: [PATCH 4/4] Very roughly implement a 3 column grid on the
element --- assets/styles/page.css | 15 +++++++++++++++ assets/styles/photos.css | 1 + assets/styles/root.css | 5 ----- layouts/partials/resources/type_css.html | 9 +++++++++ layouts/partials/single_styles.html | 3 +++ 5 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 assets/styles/page.css create mode 100644 layouts/partials/resources/type_css.html diff --git a/assets/styles/page.css b/assets/styles/page.css new file mode 100644 index 0000000..12bf56f --- /dev/null +++ b/assets/styles/page.css @@ -0,0 +1,15 @@ +/* Eryn Wells */ + +@layer template { + +main { + display: grid; + gap: var(--body-item-spacing) 0; + grid-template-columns: var(--body-item-spacing) var(--content-width) var(--body-item-spacing); + margin: var(--body-item-spacing) auto 0 auto; +} +main > * { + grid-column: 2 / 3; +} + +} diff --git a/assets/styles/photos.css b/assets/styles/photos.css index aed5539..cc3e874 100644 --- a/assets/styles/photos.css +++ b/assets/styles/photos.css @@ -94,6 +94,7 @@ .photos.page > figure.carousel { position: relative; + grid-column: 1 / 4; max-width: 100%; width: 100%; } diff --git a/assets/styles/root.css b/assets/styles/root.css index f6c49f9..cea755a 100644 --- a/assets/styles/root.css +++ b/assets/styles/root.css @@ -384,16 +384,11 @@ img.circular { main { box-sizing: border-box; max-width: calc(var(--content-width) + 2 * var(--body-item-spacing)); - margin: var(--body-item-spacing) auto; - padding-inline: var(--body-item-spacing); width: 100%; } -main > :first-child, main > article > :first-child { margin-block-start: 0; } -main > :not(:last-child), main > article > :not(:last-child) { margin-block-end: var(--body-item-spacing); } -main > :last-child, main > article > :last-child { margin-block-end: 0; } nav.bulleted > li:first-child::before { diff --git a/layouts/partials/resources/type_css.html b/layouts/partials/resources/type_css.html new file mode 100644 index 0000000..0c6b4be --- /dev/null +++ b/layouts/partials/resources/type_css.html @@ -0,0 +1,9 @@ +{{ $typeCSS := dict }} +{{ $stylesheet := "" }} +{{ if not .IsHome }} + {{ $stylesheet = printf "styles/%s.css" .Kind }} + {{ with resources.Get $stylesheet }} + {{ $typeCSS = . | fingerprint "md5" }} + {{ end }} +{{ end }} +{{ return $typeCSS }} diff --git a/layouts/partials/single_styles.html b/layouts/partials/single_styles.html index dc636b4..9a10695 100644 --- a/layouts/partials/single_styles.html +++ b/layouts/partials/single_styles.html @@ -1,3 +1,6 @@ +{{ with resources.Get "styles/page.css" | fingerprint "md5" }} + +{{ end }} {{- range .Resources.Match "*.css" -}} {{- $stylesheet := . | fingerprint "md5" }}