@@ -9,9 +9,11 @@ import {
99 shareHashtags ,
1010 shareText ,
1111 uiText ,
12- } from "./i18n.js" ;
12+ } from "./i18n.js?v=18 " ;
1313
1414const ROUTE_URL = "./dawn-to-night.json" ;
15+ const OPTIMIZED_PHOTO_DIR = "media/photos" ;
16+ const THUMBNAIL_DIR = "media/thumbs" ;
1517const THEME_ORDER = [ "Thermal" , "Birds" , "Wildlife" , "Human" , "Weather" , "Ambient" , "Water" ] ;
1618
1719const THEME_META = {
@@ -244,17 +246,70 @@ function clearNode(node) {
244246 }
245247}
246248
247- function preloadImage ( src ) {
248- return new Promise ( ( resolve ) => {
249+ let keyboardHintTimer ;
250+ let progressFrame = 0 ;
251+ let progressTrackWidth = 0 ;
252+ const imageLoadCache = new Map ( ) ;
253+
254+ function syncProgressTrackWidth ( ) {
255+ progressTrackWidth = ui . track . clientWidth || progressTrackWidth ;
256+ }
257+
258+ if ( "ResizeObserver" in window ) {
259+ const progressTrackResizeObserver = new ResizeObserver ( syncProgressTrackWidth ) ;
260+ progressTrackResizeObserver . observe ( ui . track ) ;
261+ } else {
262+ window . addEventListener ( "resize" , syncProgressTrackWidth , { passive : true } ) ;
263+ }
264+
265+ requestAnimationFrame ( syncProgressTrackWidth ) ;
266+
267+ function preloadImage ( src , fallbackSrc = "" ) {
268+ if ( ! src ) return Promise . resolve ( null ) ;
269+
270+ const cacheKey = fallbackSrc ? `${ src } ::${ fallbackSrc } ` : src ;
271+ if ( imageLoadCache . has ( cacheKey ) ) return imageLoadCache . get ( cacheKey ) ;
272+
273+ const promise = new Promise ( ( resolve ) => {
249274 const img = new Image ( ) ;
250275 img . onload = ( ) => resolve ( src ) ;
251- img . onerror = ( ) => resolve ( null ) ;
276+ img . onerror = ( ) => {
277+ if ( fallbackSrc && fallbackSrc !== src ) {
278+ preloadImage ( fallbackSrc ) . then ( resolve ) ;
279+ } else {
280+ resolve ( null ) ;
281+ }
282+ } ;
252283 img . src = src ;
284+ } ) . then ( ( loaded ) => {
285+ if ( ! loaded ) imageLoadCache . delete ( cacheKey ) ;
286+ return loaded ;
253287 } ) ;
288+
289+ imageLoadCache . set ( cacheKey , promise ) ;
290+ return promise ;
254291}
255292
256- let keyboardHintTimer ;
257- let progressFrame = 0 ;
293+ function originalImageSrc ( imagePath ) {
294+ return imagePath ? encodeURI ( `../${ imagePath } ` ) : "" ;
295+ }
296+
297+ function optimizedPhotoSrc ( stop ) {
298+ return stop . imagePath ? `${ OPTIMIZED_PHOTO_DIR } /${ encodeURIComponent ( stop . id ) } .webp` : "" ;
299+ }
300+
301+ function thumbnailPhotoSrc ( stop ) {
302+ return stop . imagePath ? `${ THUMBNAIL_DIR } /${ encodeURIComponent ( stop . id ) } .webp` : "" ;
303+ }
304+
305+ function imageSourcesForStop ( stop ) {
306+ if ( ! stop || ! stop . imagePath ) return { primary : "" , fallback : "" } ;
307+
308+ return {
309+ primary : optimizedPhotoSrc ( stop ) ,
310+ fallback : originalImageSrc ( stop . imagePath ) ,
311+ } ;
312+ }
258313
259314function showKeyboardHints ( ) {
260315 ui . keyboardHints . classList . add ( "visible" ) ;
@@ -352,7 +407,7 @@ function hslPalette(hue, saturation, lightness) {
352407 } ;
353408}
354409
355- function derivePaletteFromImage ( src , fallbackPalette ) {
410+ function derivePaletteFromImage ( src , fallbackPalette , fallbackSrc = "" ) {
356411 return new Promise ( ( resolve ) => {
357412 if ( ! src ) {
358413 resolve ( fallbackPalette ) ;
@@ -403,7 +458,13 @@ function derivePaletteFromImage(src, fallbackPalette) {
403458 resolve ( fallbackPalette ) ;
404459 }
405460 } ;
406- image . onerror = ( ) => resolve ( fallbackPalette ) ;
461+ image . onerror = ( ) => {
462+ if ( fallbackSrc && fallbackSrc !== src ) {
463+ derivePaletteFromImage ( fallbackSrc , fallbackPalette ) . then ( resolve ) ;
464+ } else {
465+ resolve ( fallbackPalette ) ;
466+ }
467+ } ;
407468 image . src = src ;
408469 } ) ;
409470}
@@ -441,7 +502,7 @@ function setBackdropNote(stop) {
441502 ui . backdropNoteBody . textContent = localized . fieldNote || localized . description ;
442503}
443504
444- function setBackdropPrint ( stop , src ) {
505+ function setBackdropPrint ( stop , src , fallbackSrc = "" ) {
445506 const expectedId = stop . id ;
446507 const localized = displayStop ( stop ) ;
447508 const caption = copy ( ) . printCaption ( localized . title , localized . theme , localized . timeOfDay ) ;
@@ -455,7 +516,7 @@ function setBackdropPrint(stop, src) {
455516 return ;
456517 }
457518
458- preloadImage ( src ) . then ( ( loaded ) => {
519+ preloadImage ( src , fallbackSrc ) . then ( ( loaded ) => {
459520 const selected = state . stops [ state . index ] ;
460521 if ( ! selected || selected . id !== expectedId ) return ;
461522
@@ -595,16 +656,22 @@ function initialStopIndex(stops) {
595656
596657function updateAtmosphere ( stop ) {
597658 const fallbackPalette = semanticPaletteForStop ( stop ) ;
598- const imageSrc = stop . imagePath ? encodeURI ( `../ ${ stop . imagePath } ` ) : "" ;
659+ const { primary : imageSrc , fallback : fallbackSrc } = imageSourcesForStop ( stop ) ;
599660 const expectedId = stop . id ;
600661 const localized = displayStop ( stop ) ;
601662
602663 applyDerivedPalette ( fallbackPalette ) ;
603- setBackdropImage ( imageSrc , localized . title ) ;
664+ setBackdropImage ( "" , localized . title ) ;
604665 setBackdropNote ( stop ) ;
605- setBackdropPrint ( stop , imageSrc ) ;
666+ setBackdropPrint ( stop , imageSrc , fallbackSrc ) ;
606667
607- derivePaletteFromImage ( imageSrc , fallbackPalette ) . then ( ( palette ) => {
668+ preloadImage ( imageSrc , fallbackSrc ) . then ( ( loaded ) => {
669+ const selected = state . stops [ state . index ] ;
670+ if ( ! selected || selected . id !== expectedId ) return ;
671+ setBackdropImage ( loaded , localized . title ) ;
672+ } ) ;
673+
674+ derivePaletteFromImage ( imageSrc , fallbackPalette , fallbackSrc ) . then ( ( palette ) => {
608675 const selected = state . stops [ state . index ] ;
609676 if ( ! selected || selected . id !== expectedId ) return ;
610677 applyDerivedPalette ( palette ) ;
@@ -729,7 +796,10 @@ function buildChipCarousel() {
729796 photo . alt = "" ;
730797 photo . loading = "lazy" ;
731798 photo . decoding = "async" ;
732- photo . src = encodeURI ( `../${ stop . imagePath } ` ) ;
799+ photo . setAttribute ( "fetchpriority" , "low" ) ;
800+ photo . dataset . fallbackSrc = originalImageSrc ( stop . imagePath ) ;
801+ photo . src = thumbnailPhotoSrc ( stop ) ;
802+ photo . addEventListener ( "error" , onChipPhotoError ) ;
733803 chip . appendChild ( photo ) ;
734804 }
735805
@@ -785,7 +855,6 @@ function updateLocalizedSurface() {
785855 buildChipCarousel ( ) ;
786856 const stop = currentStop ( ) ;
787857 if ( stop ) {
788- const imageSrc = stop . imagePath ? encodeURI ( `../${ stop . imagePath } ` ) : "" ;
789858 const localized = displayStop ( stop ) ;
790859 const stopNumber = getStopNumber ( state . index ) ;
791860 ui . eyebrow . textContent = copy ( ) . eyebrow ( stopNumber ) ;
@@ -796,9 +865,7 @@ function updateLocalizedSurface() {
796865 ui . miniMeta . textContent = `${ localized . theme } - ${ localized . timeOfDay } ` ;
797866 ui . desc . textContent = localized . fieldNote || localized . description ;
798867 ui . credit . textContent = creditForStop ( stop ) ;
799- setBackdropImage ( imageSrc , localized . title ) ;
800- setBackdropNote ( stop ) ;
801- setBackdropPrint ( stop , imageSrc ) ;
868+ updateAtmosphere ( stop ) ;
802869 updateScenePhoto ( stop ) ;
803870 updateShareTargets ( stop ) ;
804871 }
@@ -928,18 +995,30 @@ function setActiveTheme(theme, selectFirst = false) {
928995 } ) ;
929996}
930997
931- function setPhoto ( img , frame , imagePath , sourceTitle , displayTitle = sourceTitle ) {
932- const expectedPath = imagePath || "" ;
998+ function onChipPhotoError ( e ) {
999+ const img = e . currentTarget ;
1000+ const fallbackSrc = img . dataset . fallbackSrc ;
1001+
1002+ if ( fallbackSrc ) {
1003+ img . removeAttribute ( "data-fallback-src" ) ;
1004+ img . src = fallbackSrc ;
1005+ } else {
1006+ img . remove ( ) ;
1007+ }
1008+ }
1009+
1010+ function setPhoto ( img , frame , stop , displayTitle = stop . title ) {
1011+ const expectedId = stop . id ;
9331012 const matchesSelectedStop = ( ) => {
9341013 const selected = state . stops [ state . index ] ;
935- return selected && selected . title === sourceTitle && ( selected . imagePath || "" ) === expectedPath ;
1014+ return selected && selected . id === expectedId ;
9361015 } ;
9371016
9381017 img . alt = displayTitle || "" ;
9391018 frame . dataset . fallbackLabel = displayTitle || copy ( ) . routeSpecimen ;
9401019 img . classList . remove ( "is-loaded" ) ;
9411020
942- if ( ! imagePath ) {
1021+ if ( ! stop . imagePath ) {
9431022 if ( matchesSelectedStop ( ) ) {
9441023 img . removeAttribute ( "src" ) ;
9451024 img . alt = "" ;
@@ -951,8 +1030,8 @@ function setPhoto(img, frame, imagePath, sourceTitle, displayTitle = sourceTitle
9511030
9521031 img . classList . remove ( "is-fallback" ) ;
9531032 frame . classList . remove ( "has-fallback" ) ;
954- const src = encodeURI ( `../ ${ imagePath } ` ) ;
955- preloadImage ( src ) . then ( ( loaded ) => {
1033+ const { primary : src , fallback : fallbackSrc } = imageSourcesForStop ( stop ) ;
1034+ preloadImage ( src , fallbackSrc ) . then ( ( loaded ) => {
9561035 if ( ! matchesSelectedStop ( ) ) return ;
9571036
9581037 if ( loaded ) {
@@ -972,8 +1051,8 @@ function setPhoto(img, frame, imagePath, sourceTitle, displayTitle = sourceTitle
9721051
9731052function updateScenePhoto ( stop ) {
9741053 const localized = displayStop ( stop ) ;
975- setPhoto ( ui . scenePhoto , ui . sceneFrame , stop . imagePath , stop . title , localized . title ) ;
976- setPhoto ( ui . miniScenePhoto , ui . miniFrame , stop . imagePath , stop . title , localized . title ) ;
1054+ setPhoto ( ui . scenePhoto , ui . sceneFrame , stop , localized . title ) ;
1055+ setPhoto ( ui . miniScenePhoto , ui . miniFrame , stop , localized . title ) ;
9771056}
9781057
9791058function creditForStop ( stop ) {
@@ -1117,9 +1196,8 @@ function updatePlayIcon() {
11171196
11181197function setProgressVisual ( ratio ) {
11191198 const normalized = Math . max ( 0 , Math . min ( 1 , ratio || 0 ) ) ;
1120- const trackWidth = ui . track . getBoundingClientRect ( ) . width || 0 ;
11211199 ui . track . style . setProperty ( "--progress-ratio" , normalized . toFixed ( 4 ) ) ;
1122- ui . track . style . setProperty ( "--progress-x" , `${ normalized * trackWidth } px` ) ;
1200+ ui . track . style . setProperty ( "--progress-x" , `${ ( normalized * progressTrackWidth ) . toFixed ( 2 ) } px` ) ;
11231201}
11241202
11251203function paintProgressFromAudio ( ) {
0 commit comments