Skip to content

Commit

Permalink
refactor: Tune image preloading behavior.
Browse files Browse the repository at this point in the history
  • Loading branch information
darkobits committed Jun 7, 2024
1 parent 650aaf2 commit 8788b59
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 48 deletions.
36 changes: 23 additions & 13 deletions src/web/components/BackgroundImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import useAsyncEffect from 'use-async-effect';

import InspiratContext from 'web/contexts/Inspirat';
import {
BACKGROUND_ANIMATION_INITIAL_SCALE,
BACKGROUND_TRANSITION_DURATION,
BACKGROUND_TRANSITION_FUNCTION,
BACKGROUND_RULE_OVERRIDES
} from 'web/etc/constants';
import { preloadImage } from 'web/lib/utils';
import log from 'web/lib/log';
// import { preloadImage } from 'web/lib/utils';

import classes, { keyframes } from './BackgroundImage.css';

Expand Down Expand Up @@ -61,28 +63,35 @@ export default function BackgroundImage(props: BackgroundImageProps) {

const photoUrls = buildPhotoUrls(photo);

void Promise.race([
preloadImage(photoUrls.lowQuality),
preloadImage(photoUrls.highQuality)
]).then(() => {
if (!isMounted()) return;
setAnyImageReady(true);
setAnimationName(keyframes.zoomOut);
setStyleOverrides(BACKGROUND_RULE_OVERRIDES[photo.id] ?? {});
});
// void Promise.race([
// preloadImage(photoUrls.lowQuality),
// preloadImage(photoUrls.highQuality)
// ]).then(() => {
// if (!isMounted()) return;
// setAnyImageReady(true);
// setAnimationName(keyframes.zoomOut);
// setStyleOverrides(BACKGROUND_RULE_OVERRIDES[photo.id] ?? {});
// });

// TODO: Testing out not waiting to preload images.
setAnyImageReady(true);
setAnimationName(keyframes.zoomOut);
setStyleOverrides(BACKGROUND_RULE_OVERRIDES[photo.id] ?? {});

setLowQualityUrl(photoUrls.lowQuality);
setFullQualityUrl(photoUrls.highQuality);
}, () => {
// DO NOT CLEAR THIS, IT RESETS PHOTO ZOOM AT THE START OF A TRANSITION.
// setAnimationName('none');
}, [photo?.id, isActive]);
}, [photo?.id]);

const srcSet = lowQualityUrl && fullQualityUrl ? [
`${lowQualityUrl} ${Math.round(window.screen.width / 2)}w`,
fullQualityUrl
`${fullQualityUrl} ${Math.round(window.screen.width * 2 * BACKGROUND_ANIMATION_INITIAL_SCALE)}w`
].join(', ') : undefined;

// if (srcSet) log.debug('srcSet', srcSet);

return (
<div
data-testid="BACKGROUND_IMAGE"
Expand Down Expand Up @@ -111,8 +120,9 @@ export default function BackgroundImage(props: BackgroundImageProps) {
>
<img
alt="background"
src={lowQualityUrl ?? undefined}
srcSet={srcSet}
src={lowQualityUrl ?? fullQualityUrl ?? undefined}
sizes="100vw"
className={classes.backgroundImage}
style={{ animationName }}
/>
Expand Down
3 changes: 2 additions & 1 deletion src/web/components/Greeting.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ const classes = {
borderRadius: '24px',
backgroundColor: 'rgba(80, 80, 80, 0.12)',
backdropFilter: 'blur(12px)',
boxShadow: '0px 0px 24px rgba(0, 0, 0, 0.12)'
boxShadow: '0px 0px 24px rgba(0, 0, 0, 0.12)',
userSelect: 'none'
})
};

Expand Down
10 changes: 2 additions & 8 deletions src/web/components/dev-tools/DevTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,12 @@ export const DevTools = () => {
getCurrentPhotoFromCollection({ offset: dayOffset + 1 }).then(photo => {
if (!photo) return;
const { lowQuality, highQuality } = buildPhotoUrls(photo);
return Promise.all([
preloadImage(lowQuality),
preloadImage(highQuality)
]);
return Promise.all([preloadImage(lowQuality), preloadImage(highQuality)]);
}),
getCurrentPhotoFromCollection({ offset: dayOffset - 1 }).then(photo => {
if (!photo) return;
const { lowQuality, highQuality } = buildPhotoUrls(photo);
return Promise.all([
preloadImage(lowQuality),
preloadImage(highQuality)
]);
return Promise.all([preloadImage(lowQuality), preloadImage(highQuality)]);
})
]);
}, [showDevTools, dayOffset]);
Expand Down
6 changes: 5 additions & 1 deletion src/web/contexts/Inspirat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ export function InspiratProvider(props: React.PropsWithChildren) {
// This is where IMGIX configuration for low and high quality versions of
// photos is defined.
return buildPhotoUrlSrcSet(photo.urls.full, {
// blend: 'FA653D80',
q: 100,
w: Math.round(window.screen.width / 2),
h: Math.round(window.screen.height / 2)
// Adds a color overlay. Can be useful for debugging.
// blend: 'FA653D',
// blendMode: 'overlay'
}, {
w: Math.round(window.screen.width * 2 * BACKGROUND_ANIMATION_INITIAL_SCALE),
Expand Down
51 changes: 26 additions & 25 deletions src/web/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,12 @@ export function onClickAndHold(interval: number, cb: (e: React.MouseEvent | Reac
return handler;
}

type PreloadImageCacheValue = {
promise: Promise<string | ErrorEvent>;
state: 'LOADING' | 'SUCCESS' | 'ERROR';
};

const preloadImageCache = new Map<string, 'LOADING' | 'SUCCESS' | 'ERROR'>();
const preloadImageCache = new Map<string, PreloadImageCacheValue>();


/**
Expand All @@ -177,35 +181,37 @@ const preloadImageCache = new Map<string, 'LOADING' | 'SUCCESS' | 'ERROR'>();
*/
export async function preloadImage(imgUrl: string) {
if (!preloadImageCache.has(imgUrl)) {
preloadImageCache.set(imgUrl, 'LOADING');
}

return new Promise<string | ErrorEvent>((resolve, reject) => {
const img = new Image();

img.addEventListener('load', () => {
preloadImageCache.set(imgUrl, 'SUCCESS');
resolve(imgUrl);
const imagePromise = new Promise<string | ErrorEvent>((resolve, reject) => {
const img = new Image();

img.addEventListener('load', () => {
preloadImageCache.set(imgUrl, { promise: imagePromise, state: 'SUCCESS' });
resolve(imgUrl);
});

img.addEventListener('error', event => {
preloadImageCache.set(imgUrl, { promise: imagePromise, state: 'ERROR' });
const message = event.error?.message ?? 'Unknown Error';
reject(new Error(`[preloadImage] Failed to load image: ${message}`, { cause: event.error }));
});

// N.B. Setting this property will cause the browser to fetch the image.
img.src = imgUrl;
});

img.addEventListener('error', event => {
preloadImageCache.set(imgUrl, 'ERROR');
const message = event.error?.message ?? 'Unknown Error';
reject(new Error(`[preloadImage] Failed to load image: ${message}`, { cause: event.error }));
});
preloadImageCache.set(imgUrl, { promise: imagePromise, state: 'LOADING' });
}

// N.B. Setting this property will cause the browser to fetch the image.
img.src = imgUrl;
});
return preloadImageCache.get(imgUrl)?.promise;
}

/**
* Predicate which returns `true` immediately if there are any images that are
* currently preloading.
*/
preloadImage.isLoadingImages = () => {
const states = new Set(preloadImageCache.values());
return states.has('LOADING');
const states = [...preloadImageCache.values()].map(cacheValue => cacheValue.state);
return states.includes('LOADING');
};

/**
Expand Down Expand Up @@ -293,11 +299,6 @@ export function buildPhotoUrlSrcSet(url: string, lqOptions ={}, fullOptions = {}
return {
lowQuality: updateImgixQueryParams(url, {
q: QUALITY_LQIP,
w: Math.round(window.screen.width / 2),
h: Math.round(window.screen.height / 2),
// Adds a color overlay. Can be useful for debugging.
// blend: 'FA653D',
// blendMode: 'overlay',
...lqOptions
}),
highQuality: updateImgixQueryParams(url, {
Expand Down

0 comments on commit 8788b59

Please sign in to comment.