Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add height to picture directive output #378

Closed
eltigerchino opened this issue Aug 31, 2022 · 2 comments · Fixed by #379
Closed

Add height to picture directive output #378

eltigerchino opened this issue Aug 31, 2022 · 2 comments · Fixed by #379

Comments

@eltigerchino
Copy link

eltigerchino commented Aug 31, 2022

This is a suggestion that builds upon #369

Problem

Currently, the output of the ?picture directive does not export a height and prevents the use of the ?meta directive to retrieve it. This makes it difficult to have a set height on the <img>.

Both width and height are required for the browser to automatically calculate the aspect-ratio of the <img> before the image file is loaded. As a result, the browser can reserve the necessary space and prevent Cumulative Layout Shift (CLS).

Before the ?picture directive was available, the ?meta directive was used to manipulate the strings generated for a <picture> element.
For example, this is an <Image> component in Svelte using the ?meta directive (adapted from this code).

<script lang="ts">
	import { onMount } from 'svelte';

	export let alt: string;
	export let placeholder = '';
	export let dominantColor = '#F8F8F8';
	export let loading: 'eager' | 'lazy' = 'lazy';
	export let decoding: 'async' | 'sync' | 'auto' = 'async';
	export let style = '';
	let className = '';
	export { className as class };

        interface ImageMeta {
	        src: string;
	        width: number;
	        height: number;
	        format: string;
        }

        // expects the output from the directives:
        // ?w=1920;1366;780;414&format=avif;webp;jpg&meta=width;height;src;format
	export let meta: ImageMeta[];

	// if there is only one, vite-imagetools won't wrap the object in an array
	if (!(meta instanceof Array)) meta = [meta];

	// all images by format
	let sources = new Map<string, typeof meta>();
	meta.map((m) => sources.set(m.format, []));
	meta.map((m) => sources.get(m.format)?.push(m));

	// fallback image: first resolution of last format
	let fallback = sources.get([...sources.keys()].slice(-1)[0])?.[0];

	export let sizes = `${fallback?.width}px`;

	// fade-in the image after it has loaded
	let image: HTMLImageElement;
	let hidden = false;

	onMount(() => {
		if (image.complete) return;
		hidden = true;
		image.onload = () => (hidden = false);
	});
</script>

<div
	{style}
	style:background-image={placeholder ? `url(${placeholder})` : ''}
	style:background-color={dominantColor}
	class="img__placeholder {className}"
>
	<picture>
		{#each [...sources.entries()] as [format, meta]}
			<source
				{sizes}
				type="image/{format}"
				srcset={meta.map((m) => `${m.src} ${m.width}w`).join(', ')}
			/>
		{/each}
		<img
			bind:this={image}
			{style}
			class={className}
			class:hidden
			src={fallback?.src}
			width={fallback?.width}
			height={fallback?.height}
			{alt}
			{loading}
			{decoding}
		/>
	</picture>
</div>

<style>
	.img__placeholder,
	img {
		height: auto;
		width: 100%;
	}

	.img__placeholder {
		background-repeat: no-repeat;
		background-size: cover;
		overflow: hidden;
	}

	img {
		display: block;
		object-fit: cover;
		transition: opacity 0.25s ease-out;

		/* hide alt text while image is loading */
		color: transparent;
	}

	.hidden {
		opacity: 0;
	}
</style>

Suggestion

Currently, the ?picture directive outputs something like this:

interface Source {
  src: string;
  w: number;
}

interface Picture {
  sources: Record<string, Source[]>;
  fallback: string;
}

Ideally, the fallback would include a width and height so the aspect-ratio can be calculated.

<script lang="ts">
  export let picture: Picture;
  
  interface Fallback {
    src: string;
    width: number;
    height: number;
  }
</script>

<picture>
  {#each Object.keys(picture.sources) as format}
    <!-- Maybe srcset can be an output option as well for picture? -->
    {@const srcset = picture.sources[format].map((s) => `${s.src} ${s.w}w`).join(', ')}
    <source type="image/{format}" {srcset} />
  {/each}
    <img {...picture.fallback} alt="..."  />
</picture>

What are your thoughts @benmccann ?

Other thoughts

I also wonder if generating a low-quality placeholder base64 string could be included. sharp already has an option for generating base64 strings from images. I've been generating placeholders using the ?w=54&blur=2&jpg directive, but these don't get inlined as base64.

EDIT: low quality placeholder issue already exists #86

@JonasKruckenberg
Copy link
Owner

JonasKruckenberg commented Aug 31, 2022

You can still use the meta directive, but I think this is a reasonable request.

I also wonder if generating a low-quality placeholder base64 string could be included.

This is already tracked here #86

@benmccann
Copy link
Collaborator

A couple resources supporting this suggestion:

I will send a PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants