Compare commits
7 commits
831cc2942b
...
383a0229b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 383a0229b3 | |||
| 23c5485e52 | |||
| 200ae187c7 | |||
| 00fa298dd5 | |||
| 9272fd43a5 | |||
| 50291182ae | |||
| 64d10b5dbf |
|
|
@ -5,7 +5,7 @@ test('renders the gallery and supports keyboard navigation', async ({
|
|||
}) => {
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.locator('.thumbnail')).toHaveCount(54);
|
||||
await expect(page.locator('.thumbnail')).toHaveCount(47);
|
||||
await expect(page.locator('#frame-image')).toBeVisible();
|
||||
|
||||
const firstAlt = await page.locator('#frame-image').getAttribute('alt');
|
||||
|
|
|
|||
|
|
@ -116,7 +116,9 @@ const renderThumbnail = (photo, isCurrent) => {
|
|||
</a>`;
|
||||
};
|
||||
|
||||
const renderFrame = (photo) => `<figure class="frame-figure">
|
||||
const renderFrame = (
|
||||
photo
|
||||
) => `<figure class="frame-figure" style="--frame-ratio:${photo.aspectRatio}">
|
||||
${renderPicture(
|
||||
photo,
|
||||
frameSizes(photo),
|
||||
|
|
|
|||
|
|
@ -144,25 +144,67 @@ img {
|
|||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
// A full-width column gives the figure a definite width to size against, so
|
||||
// `100%` below resolves to the real available width (not the image's own,
|
||||
// content-collapsed width) even before the photo has loaded.
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-content.is-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
inline-size: 3rem;
|
||||
block-size: 3rem;
|
||||
border: 4px solid rgb(32 33 36 / 20%);
|
||||
border-block-start-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.frame-content.is-loading::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Holds the next photo's <picture> off-screen so the browser fetches it before
|
||||
// the slideshow advances. Kept renderable (not display: none) so loading runs.
|
||||
.frame-preload {
|
||||
position: absolute;
|
||||
inline-size: 0;
|
||||
block-size: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Size the framed photo to its final, contained dimensions up front — width is
|
||||
// capped by the available width (100%) and by the height limit scaled by the
|
||||
// photo's aspect ratio — so the white frame never grows from a tiny box to the
|
||||
// photo's size while the image is still loading. --frame-ratio is set per photo.
|
||||
.frame-figure {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
inline-size: min(100%, calc((100dvh - 5rem) * var(--frame-ratio, 1)));
|
||||
}
|
||||
|
||||
.frame-figure picture {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.frame-figure img {
|
||||
inline-size: auto;
|
||||
max-inline-size: 100%;
|
||||
inline-size: 100%;
|
||||
block-size: auto;
|
||||
max-block-size: calc(100dvh - 5rem);
|
||||
border: var(--border-width) solid var(--color-text);
|
||||
background: var(--color-text);
|
||||
|
|
@ -174,8 +216,7 @@ img {
|
|||
display: none;
|
||||
position: absolute;
|
||||
inset-block-start: var(--space-s);
|
||||
inset-inline-start: var(--space-s);
|
||||
justify-self: start;
|
||||
justify-self: center;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
padding: 0.45rem 0.75rem;
|
||||
|
|
@ -206,7 +247,9 @@ img {
|
|||
}
|
||||
|
||||
.slideshow-toggle {
|
||||
inset-block-start: 50%;
|
||||
inset-inline: auto var(--space-s);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.site-header h1 {
|
||||
|
|
@ -257,6 +300,10 @@ img {
|
|||
padding: var(--space-s);
|
||||
}
|
||||
|
||||
.frame-figure {
|
||||
inline-size: min(100%, calc((100dvh - 12rem) * var(--frame-ratio, 1)));
|
||||
}
|
||||
|
||||
.frame-figure img {
|
||||
max-block-size: calc(100dvh - 12rem);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,44 @@
|
|||
[
|
||||
{
|
||||
"file": "_DSC1190-Edit.jpg",
|
||||
"alt": "Portrait of a man in a burgundy sleeveless shirt standing with folded arms above a city view."
|
||||
"file": "_DSC2821-iceland.jpg",
|
||||
"alt": "A steep slope of rust-coloured grass below a snow-dusted rocky peak, with a dark gravel riverbed in the foreground."
|
||||
},
|
||||
{
|
||||
"file": "_DSC1484.jpg",
|
||||
"alt": "A man smiling while drying a plate in a kitchen."
|
||||
"file": "_DSC2879-iceland.jpg",
|
||||
"alt": "A snow-covered mountain with its summit lost in cloud, a fence line crossing the white plain below."
|
||||
},
|
||||
{
|
||||
"file": "_DSC2975-iceland.jpg",
|
||||
"alt": "A glacier tongue meeting a still, icy lagoon beneath misty dark mountains."
|
||||
},
|
||||
{
|
||||
"file": "_DSC3105-iceland.jpg",
|
||||
"alt": "A snow- and ice-covered river canyon winding toward the horizon under a clear blue sky."
|
||||
},
|
||||
{
|
||||
"file": "2025-NYC-31.jpg",
|
||||
"alt": "Looking up at glass-and-steel skyscrapers converging against a pale sky."
|
||||
},
|
||||
{
|
||||
"file": "2025-NYC-41.jpg",
|
||||
"alt": "A pigeon in sharp close-up gazes toward the camera as glass towers rise behind it under an overcast sky."
|
||||
},
|
||||
{
|
||||
"file": "2025-NYC-175.jpg",
|
||||
"alt": "The rusted wreck of an overturned vehicle lying among bare trees and rocks in stark black-and-white woodland."
|
||||
},
|
||||
{
|
||||
"file": "2025-NYC-194.jpg",
|
||||
"alt": "Bare winter trees growing from a rocky, boulder-strewn hillside in soft black-and-white light."
|
||||
},
|
||||
{
|
||||
"file": "2025-NYC-232.jpg",
|
||||
"alt": "A rocky cliff topped with bare winter trees beneath a pale sky crossed by aeroplane contrails."
|
||||
},
|
||||
{
|
||||
"file": "_DSC1834.jpg",
|
||||
"alt": "A person sitting near dark rocks while waves crash into a sea cave."
|
||||
},
|
||||
{
|
||||
"file": "_DSC2313.jpg",
|
||||
"alt": "Portrait of a man in a teal coat standing among autumn leaves."
|
||||
},
|
||||
{
|
||||
"file": "_DSC2362.jpg",
|
||||
"alt": "Portrait of a man with glasses leaning on a railing in a park."
|
||||
},
|
||||
{
|
||||
"file": "_DSC2674.jpg",
|
||||
"alt": "A person in a hooded jacket walking down a narrow city street."
|
||||
|
|
@ -27,14 +47,6 @@
|
|||
"file": "_DSC8718-Edit.jpg",
|
||||
"alt": "Defocused city lights glowing across a dark night skyline."
|
||||
},
|
||||
{
|
||||
"file": "adam.jpg",
|
||||
"alt": "A man standing against a dark background in a black graphic T-shirt."
|
||||
},
|
||||
{
|
||||
"file": "dorka-1.jpg",
|
||||
"alt": "A woman in a white knit hat sitting outdoors on an autumn day."
|
||||
},
|
||||
{
|
||||
"file": "kezdetazarovonalbol.jpg",
|
||||
"alt": "A small plant stem silhouetted against warm yellow and orange light."
|
||||
|
|
@ -51,10 +63,6 @@
|
|||
"file": "nyomtatas-10.jpg",
|
||||
"alt": "Warm abstract light passing through dried plants."
|
||||
},
|
||||
{
|
||||
"file": "web-1-2.jpg",
|
||||
"alt": "A city panorama seen through a coin-operated viewing telescope."
|
||||
},
|
||||
{
|
||||
"file": "web-1.jpg",
|
||||
"alt": "Black and white stage scene with dancers performing before an audience."
|
||||
|
|
@ -63,14 +71,6 @@
|
|||
"file": "web-2-2.jpg",
|
||||
"alt": "Two zebras walking across a black and white landscape."
|
||||
},
|
||||
{
|
||||
"file": "web-2.jpg",
|
||||
"alt": "Coastal town and hillside beside a blue sea under a bright sky."
|
||||
},
|
||||
{
|
||||
"file": "web-3-2.jpg",
|
||||
"alt": "A woman walking away on a beach while carrying a bag."
|
||||
},
|
||||
{
|
||||
"file": "web-3.jpg",
|
||||
"alt": "Black and white lake scene with a small boat under a bright sun."
|
||||
|
|
@ -103,14 +103,6 @@
|
|||
"file": "web-11.jpg",
|
||||
"alt": "Black and white close-up portrait of a dog looking into the camera."
|
||||
},
|
||||
{
|
||||
"file": "web-12.jpg",
|
||||
"alt": "Two people sitting by water under a vivid orange sunset."
|
||||
},
|
||||
{
|
||||
"file": "web-13.jpg",
|
||||
"alt": "Portrait of a woman standing beside a tree in green summer light."
|
||||
},
|
||||
{
|
||||
"file": "web-14.jpg",
|
||||
"alt": "Golden sunlight reflecting on rippled water."
|
||||
|
|
@ -127,38 +119,14 @@
|
|||
"file": "web-19.jpg",
|
||||
"alt": "A bridge at night with blue lights and long yellow light trails."
|
||||
},
|
||||
{
|
||||
"file": "web-20.jpg",
|
||||
"alt": "A seated man in a dark coat photographed at night with city lights behind him."
|
||||
},
|
||||
{
|
||||
"file": "web-22.jpg",
|
||||
"alt": "Silhouettes seen through large windows during a pink and blue sunset."
|
||||
},
|
||||
{
|
||||
"file": "web-23.jpg",
|
||||
"alt": "Tall clock tower and stone architecture with a bird flying nearby."
|
||||
},
|
||||
{
|
||||
"file": "web-24.jpg",
|
||||
"alt": "Portrait of a woman at night with bridge lights behind her."
|
||||
},
|
||||
{
|
||||
"file": "web-25.jpg",
|
||||
"alt": "Portrait of a smiling man at night with blurred city lights behind him."
|
||||
},
|
||||
{
|
||||
"file": "web-26.jpg",
|
||||
"alt": "Tall bridge lamps receding into a pale sky."
|
||||
},
|
||||
{
|
||||
"file": "web-42.jpg",
|
||||
"alt": "Warm sunset over a grassy field with low hills in the distance."
|
||||
},
|
||||
{
|
||||
"file": "_DSC2636-sterling.jpg",
|
||||
"alt": "A leaf-strewn forest path winding between autumn trees into misty green woodland under soft grey light."
|
||||
},
|
||||
{
|
||||
"file": "_DSC2675-sterling.jpg",
|
||||
"alt": "Tall conifer trees rising into pale morning mist beneath a soft grey sky."
|
||||
|
|
@ -176,8 +144,8 @@
|
|||
"alt": "Sunlit dirt path winding through silhouetted pine trees in soft black-and-white woodland light."
|
||||
},
|
||||
{
|
||||
"file": "20230703_211420_C19E401F.jpg",
|
||||
"alt": "Silhouetted pine trees in a misty forest with soft light filtering through their leaning trunks."
|
||||
"file": "20230703_211414_91173C51.jpg",
|
||||
"alt": "A wire-clamp glass jar holding dark leaves in water rests on a windowsill, a rain-speckled window and blurred city beyond in soft black-and-white light."
|
||||
},
|
||||
{
|
||||
"file": "IMG_3907.JPG",
|
||||
|
|
@ -207,12 +175,16 @@
|
|||
"file": "colour-25.jpg",
|
||||
"alt": "A red road-closed sign and yellow barriers before a bright orange perforated building facade under grey sky."
|
||||
},
|
||||
{
|
||||
"file": "colour-40.jpg",
|
||||
"alt": "Aerial view over a hazy city with railway lines stretching toward the horizon beside a river."
|
||||
},
|
||||
{
|
||||
"file": "colour-41.jpg",
|
||||
"alt": "Aerial view of a hazy city skyline at sunset, with golden light rays piercing through dark clouds."
|
||||
},
|
||||
{
|
||||
"file": "colour-43.jpg",
|
||||
"alt": "A giant floating globe sculpture of Earth rests on water among glass office towers, with a light railway train crossing a bridge under a grey sky."
|
||||
},
|
||||
{
|
||||
"file": "NYC-2025-185.jpg",
|
||||
"alt": "A pale straw cowboy hat hanging on a bare branch against a soft, blurred wintry background."
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -109,4 +109,67 @@ describe('PhotoGallery', () => {
|
|||
|
||||
expect(frame.querySelector('img')?.alt).toBe('Second photo');
|
||||
});
|
||||
|
||||
it('preloads the next photo off-screen without duplicating the frame id', () => {
|
||||
const { gallery, frame, toggle } = setup();
|
||||
new PhotoGallery({ gallery, frame, toggle, autoAdvance: false });
|
||||
|
||||
const preload = document.querySelector('.frame-preload img');
|
||||
expect(preload?.getAttribute('src')).toBe('static/photos/two-2400.jpg');
|
||||
// The off-screen copy must not steal the displayed image's id.
|
||||
expect(preload?.id).toBe('');
|
||||
expect(document.querySelectorAll('#frame-image')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('moves the preload to the next photo as the selection advances', () => {
|
||||
const { gallery, frame, toggle } = setup();
|
||||
new PhotoGallery({ gallery, frame, toggle, autoAdvance: false });
|
||||
|
||||
gallery
|
||||
.querySelector<HTMLAnchorElement>('[data-photo-id="two"]')
|
||||
?.dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true, cancelable: true })
|
||||
);
|
||||
|
||||
// Showing 'two' leaves exactly one framed image and preloads the wrap-around.
|
||||
expect(document.querySelectorAll('#frame-image')).toHaveLength(1);
|
||||
expect(
|
||||
document.querySelector('.frame-preload img')?.getAttribute('src')
|
||||
).toBe('static/photos/one-2400.jpg');
|
||||
});
|
||||
|
||||
it('sets the aspect ratio so the frame reserves the photo size before it loads', () => {
|
||||
const { gallery, frame, toggle } = setup();
|
||||
new PhotoGallery({ gallery, frame, toggle, autoAdvance: false });
|
||||
|
||||
gallery
|
||||
.querySelector<HTMLAnchorElement>('[data-photo-id="one"]')
|
||||
?.dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true, cancelable: true })
|
||||
);
|
||||
|
||||
// Thumbnail markup is 320x240, so the figure carries that ratio for CSS.
|
||||
const figure = frame.querySelector<HTMLElement>('.frame-figure');
|
||||
expect(
|
||||
parseFloat(figure?.style.getPropertyValue('--frame-ratio') ?? '')
|
||||
).toBeCloseTo(320 / 240, 4);
|
||||
});
|
||||
|
||||
it('shows a loading animation for a user load and clears it when ready', () => {
|
||||
const { gallery, frame, toggle } = setup();
|
||||
new PhotoGallery({ gallery, frame, toggle, autoAdvance: false });
|
||||
|
||||
// 'one' is not the preloaded photo, so clicking it builds a fresh figure
|
||||
// whose image is still loading.
|
||||
gallery
|
||||
.querySelector<HTMLAnchorElement>('[data-photo-id="one"]')
|
||||
?.dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true, cancelable: true })
|
||||
);
|
||||
|
||||
expect(frame.classList.contains('is-loading')).toBe(true);
|
||||
|
||||
frame.querySelector('img')?.dispatchEvent(new Event('load'));
|
||||
expect(frame.classList.contains('is-loading')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,13 +63,21 @@ const createFrameFigure = (thumbnail: HTMLAnchorElement): HTMLElement => {
|
|||
element.setAttribute('sizes', sizes);
|
||||
}
|
||||
|
||||
image.id = 'frame-image';
|
||||
// The `frame-image` id is assigned by showFigure when the figure is placed in
|
||||
// the frame, so a figure waiting in the preload holder never duplicates it.
|
||||
if (thumbnail.dataset.frameSrc) {
|
||||
image.src = thumbnail.dataset.frameSrc;
|
||||
}
|
||||
image.removeAttribute('loading');
|
||||
image.decoding = 'async';
|
||||
|
||||
// Reserve the photo's final size before it loads (see --frame-ratio in CSS).
|
||||
const ratio =
|
||||
Number(image.getAttribute('width')) / Number(image.getAttribute('height'));
|
||||
if (Number.isFinite(ratio) && ratio > 0) {
|
||||
figure.style.setProperty('--frame-ratio', String(ratio));
|
||||
}
|
||||
|
||||
figure.append(picture);
|
||||
|
||||
return figure;
|
||||
|
|
@ -78,7 +86,9 @@ const createFrameFigure = (thumbnail: HTMLAnchorElement): HTMLElement => {
|
|||
export class PhotoGallery {
|
||||
private selectedIndex = 0;
|
||||
private timerId: number | null = null;
|
||||
private preloaded: { index: number; figure: HTMLElement } | null = null;
|
||||
private readonly thumbnails: HTMLAnchorElement[];
|
||||
private readonly preloadHolder = document.createElement('div');
|
||||
private readonly reducedMotion = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)'
|
||||
);
|
||||
|
|
@ -92,11 +102,18 @@ export class PhotoGallery {
|
|||
throw new Error('The gallery needs at least one photo.');
|
||||
}
|
||||
|
||||
// Off-screen holder that warms the next photo's image so advancing to it is
|
||||
// instant; see preloadNext.
|
||||
this.preloadHolder.className = 'frame-preload';
|
||||
this.preloadHolder.setAttribute('aria-hidden', 'true');
|
||||
document.body.append(this.preloadHolder);
|
||||
|
||||
this.addEventListeners();
|
||||
|
||||
// The frame for photo 0 is already server-rendered, so just mark the
|
||||
// selected thumbnail; rebuilding it would discard the in-flight LCP image.
|
||||
this.updateSelectedThumbnail(0, false);
|
||||
this.preloadNext();
|
||||
|
||||
if (this.options.autoAdvance !== false && !this.reducedMotion.matches) {
|
||||
this.startAutoAdvance();
|
||||
|
|
@ -201,7 +218,11 @@ export class PhotoGallery {
|
|||
throw new Error('Selected photo is missing.');
|
||||
}
|
||||
|
||||
this.options.frame.replaceChildren(createFrameFigure(thumbnail));
|
||||
// Auto-advance shows the preloaded figure instantly; a user-initiated jump
|
||||
// (which carries a source) may need a download, so it gets the spinner.
|
||||
this.showFigure(this.takeFigure(this.selectedIndex, thumbnail), {
|
||||
loading: options.source !== undefined,
|
||||
});
|
||||
this.updateSelectedThumbnail(
|
||||
this.selectedIndex,
|
||||
options.focusThumbnail ?? false
|
||||
|
|
@ -219,6 +240,63 @@ export class PhotoGallery {
|
|||
source: options.source,
|
||||
});
|
||||
}
|
||||
|
||||
this.preloadNext();
|
||||
}
|
||||
|
||||
// Reuse the figure preloaded for this index when we have it — the browser has
|
||||
// already fetched its image — otherwise build a fresh one.
|
||||
private takeFigure(index: number, thumbnail: HTMLAnchorElement): HTMLElement {
|
||||
if (this.preloaded?.index === index) {
|
||||
const { figure } = this.preloaded;
|
||||
this.preloaded = null;
|
||||
return figure;
|
||||
}
|
||||
|
||||
return createFrameFigure(thumbnail);
|
||||
}
|
||||
|
||||
private showFigure(figure: HTMLElement, options: { loading: boolean }): void {
|
||||
const image = figure.querySelector('img');
|
||||
if (image) {
|
||||
image.id = 'frame-image';
|
||||
}
|
||||
|
||||
this.options.frame.replaceChildren(figure);
|
||||
|
||||
// Show a spinner only while a not-yet-loaded image arrives; a preloaded
|
||||
// (already complete) image is displayed immediately, with no flash.
|
||||
if (options.loading && image && !image.complete) {
|
||||
this.options.frame.classList.add('is-loading');
|
||||
const clear = (): void =>
|
||||
this.options.frame.classList.remove('is-loading');
|
||||
image.addEventListener('load', clear, { once: true });
|
||||
image.addEventListener('error', clear, { once: true });
|
||||
} else {
|
||||
this.options.frame.classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
// Build the photo the slideshow will advance to next and park it off-screen
|
||||
// so the browser downloads it before we need to show it.
|
||||
private preloadNext(): void {
|
||||
if (this.thumbnails.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = wrapIndex(this.selectedIndex + 1, this.thumbnails.length);
|
||||
if (this.preloaded?.index === nextIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thumbnail = this.thumbnails[nextIndex];
|
||||
if (!thumbnail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const figure = createFrameFigure(thumbnail);
|
||||
this.preloaded = { index: nextIndex, figure };
|
||||
this.preloadHolder.replaceChildren(figure);
|
||||
}
|
||||
|
||||
private updateSelectedThumbnail(index: number, shouldFocus: boolean): void {
|
||||
|
|
|
|||
BIN
src/pictures/20230703_211414_91173C51.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
BIN
src/pictures/2025-NYC-175.jpg
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
src/pictures/2025-NYC-194.jpg
Normal file
|
After Width: | Height: | Size: 4 MiB |
BIN
src/pictures/2025-NYC-232.jpg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
src/pictures/2025-NYC-31.jpg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
src/pictures/2025-NYC-41.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
src/pictures/NYC-2025-185.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 3 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 7.4 MiB |
BIN
src/pictures/_DSC2821-iceland.jpg
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
src/pictures/_DSC2879-iceland.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/pictures/_DSC2975-iceland.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
src/pictures/_DSC3105-iceland.jpg
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
BIN
src/pictures/colour-43.jpg
Normal file
|
After Width: | Height: | Size: 4 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 429 KiB |
|
Before Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 820 KiB |