diff --git a/frontend/code/components/image.ts b/frontend/code/components/image.ts index a28a758b..cc20afdd 100644 --- a/frontend/code/components/image.ts +++ b/frontend/code/components/image.ts @@ -20,7 +20,6 @@ export type ImageState = ComponentState & { export class ImageComponent extends ComponentBase { private imageElement: HTMLImageElement; - private isLoading: boolean = false; private resizeObserver: ResizeObserver; createElement(context: ComponentStatesUpdateContext): HTMLElement { @@ -57,15 +56,7 @@ export class ImageComponent extends ComponentBase { deltaState.imageUrl !== undefined && this.imageElement.src !== deltaState.imageUrl ) { - // Until the image is loaded and we get access to its resolution, - // let it fill the entire space. This is the correct size for all - // `fill_mode`s except `"fit"` anyway, so there's no harm in setting - // it now rather than later. (SVGs might temporarily render content - // outside of the viewbox, but the only way to prevent that would be - // to make the image invisible until loaded.) - this.isLoading = true; - this.imageElement.style.width = "100%"; - this.imageElement.style.height = "100%"; + this.element.classList.add("rio-loading"); this.imageElement.src = deltaState.imageUrl; @@ -96,12 +87,12 @@ export class ImageComponent extends ComponentBase { } private _onLoad(): void { - this.isLoading = false; + this.element.classList.remove("rio-loading"); this._updateSize(); } private _updateSize(): void { - if (this.isLoading) { + if (this.element.classList.contains("rio-loading")) { // While loading a new image, the size is set to 100%. Don't // overwrite it. return; diff --git a/frontend/css/components/image.scss b/frontend/css/components/image.scss index eb8a756f..61e5b3ee 100644 --- a/frontend/css/components/image.scss +++ b/frontend/css/components/image.scss @@ -1,5 +1,26 @@ @import "../utils"; +// A loading animation inspired by the "skeleton" effect +@mixin loading-animation-shimmer { + background: linear-gradient( + 90deg, + var(--rio-local-bg) 25%, + var(--rio-local-bg-active) 45%, + var(--rio-local-bg-variant) 60%, + var(--rio-local-bg) 75% + ); + background-size: 400%; + animation: shimmer 1.5s infinite linear; +} +@keyframes shimmer { + 0% { + background-position-x: 100%; + } + 100% { + background-position-x: 0%; + } +} + .rio-image { pointer-events: none; @@ -10,6 +31,19 @@ @include kill-size-request-with-absolute(); } + // Until the image is loaded and we get access to its resolution, let it + // fill the entire space. This is the correct size for all `fill_mode`s + // except `"fit"` anyway, so there's no harm in setting it now rather than + // later. (SVGs might temporarily render content outside of the viewbox, but + // the only way to prevent that would be to make the image invisible until + // loaded.) + &.rio-loading img { + width: 100%; + height: 100%; + + @include loading-animation-shimmer(); + } + // Error icon svg { pointer-events: auto;