summaryrefslogtreecommitdiff
path: root/src/components/AmbientBackgroundArtwork.svelte
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /src/components/AmbientBackgroundArtwork.svelte
init commit
Diffstat (limited to 'src/components/AmbientBackgroundArtwork.svelte')
-rw-r--r--src/components/AmbientBackgroundArtwork.svelte202
1 files changed, 202 insertions, 0 deletions
diff --git a/src/components/AmbientBackgroundArtwork.svelte b/src/components/AmbientBackgroundArtwork.svelte
new file mode 100644
index 0000000..bc9563c
--- /dev/null
+++ b/src/components/AmbientBackgroundArtwork.svelte
@@ -0,0 +1,202 @@
+<script lang="ts">
+ import type { Artwork as JetArtworkType } from '@jet-app/app-store/api/models';
+ import { intersectionObserver } from '@amp/web-app-components/src/actions/intersection-observer';
+ import { buildSrc } from '@amp/web-app-components/src/components/Artwork/utils/srcset';
+ import ResizeDetector from '@amp/web-app-components/src/components/helpers/ResizeDetector.svelte';
+ import { colorAsString } from '~/utils/color';
+
+ export let artwork: JetArtworkType;
+ export let active: boolean = false;
+
+ $: isBackgroundImageLoaded = false;
+ $: backgroundImage = artwork
+ ? buildSrc(
+ artwork.template,
+ {
+ crop: 'sr',
+ width: 400,
+ height: Math.floor(400 / 1.6667),
+ fileType: 'webp',
+ },
+ {},
+ )
+ : undefined;
+
+ $: if (backgroundImage) {
+ const img = new Image();
+ img.onload = () => (isBackgroundImageLoaded = true);
+ img.src = backgroundImage;
+ }
+
+ let resizing = false;
+ const handleResizeUpdate = (e: CustomEvent<{ isResizing: boolean }>) =>
+ (resizing = e.detail.isResizing);
+
+ let isOutOfView = true;
+ const handleIntersectionOberserverUpdate = (
+ isIntersectingViewport: boolean,
+ ) => (isOutOfView = !isIntersectingViewport);
+</script>
+
+{#if backgroundImage}
+ <ResizeDetector on:resizeUpdate={handleResizeUpdate} />
+
+ <div
+ class="container"
+ class:active
+ class:resizing
+ class:loaded={isBackgroundImageLoaded}
+ class:out-of-view={isOutOfView}
+ style:--background-image={`url(${backgroundImage})`}
+ style:--background-color={artwork.backgroundColor &&
+ colorAsString(artwork.backgroundColor)}
+ use:intersectionObserver={{
+ callback: handleIntersectionOberserverUpdate,
+ threshold: 0,
+ }}
+ >
+ <div class="overlay" />
+ </div>
+{/if}
+
+<style>
+ .container {
+ --veil: rgb(240, 240, 240, 0.65);
+ --speed: 0.66s;
+ --aspect-ratio: 16/9;
+ --scale: 1.2;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ aspect-ratio: var(--aspect-ratio);
+ max-height: 900px;
+ opacity: 0;
+
+ /*
+ This stack of background images represents the following three layers, listed front-to-back:
+
+ 1) A gradient from transparent to white that acts as a mask for the entire container.
+ `mask-image` caused too much thrashing and CPU usage when animating and resizing,
+ so we are mimicking its functionality with this top-layer background image.
+ 2) A semi-transparent veil to evenly fade out the bg. Note that this is not technically
+ a gradient, but we are using `linear-gradient` because a regular `rgb` value can't be
+ used in `background-image`.
+ 3) The joe color of the background image that will eventualy be loaded.
+ */
+ background-image: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0) 50%,
+ var(--pageBg) 80%
+ ),
+ linear-gradient(0deg, var(--veil) 0%, var(--veil) 80%),
+ linear-gradient(
+ 0deg,
+ var(--background-color) 0%,
+ var(--background-color) 80%
+ );
+ background-position: center;
+ background-size: 120%;
+
+ /*
+ Blurring via the CSS filter does not extend edge-to-edge of the contents width, but we
+ can mitigate that by ever-so-slightly bumping up the `scale` of content so it bleeds off
+ the page cleanly.
+ */
+ filter: blur(20px) saturate(1.3);
+ transform: scale(var(--scale));
+ transition: opacity calc(var(--speed) * 2) ease-out,
+ background-size var(--speed) ease-in;
+
+ @media (prefers-color-scheme: dark) {
+ --veil: rgba(0, 0, 0, 0.5);
+ }
+ }
+
+ .container.loaded {
+ /*
+ This stack of background images represents the following three layers, listed front-to-back:
+
+ 1) A gradient from transparent to white that acts as a mask for the entire container.
+ `mask-image` caused too much thrashing and CPU usage when animating and resizing,
+ so we are mimicking its functionality with this top-layer background image.
+ 2) A semi-transparent veil to evenly fade out the image. Note that this is not technically
+ a gradient, but we are using `linear-gradient` because a regular `rgb` value can't be
+ used in `background-image`.
+ 3) The actual background image.
+ */
+ background-image: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0) 50%,
+ var(--pageBg) 80%
+ ),
+ linear-gradient(0deg, var(--veil) 0%, var(--veil) 80%),
+ var(--background-image);
+ }
+
+ .container.active {
+ opacity: 1;
+ transition: opacity calc(var(--speed) / 2) ease-in;
+ background-size: 100%;
+ }
+
+ .overlay {
+ position: absolute;
+ z-index: 2;
+ top: 0;
+ left: 0;
+ width: 100%;
+ aspect-ratio: var(--aspect-ratio);
+ max-height: 900px;
+ opacity: 0;
+ background-image: var(--background-image);
+ background-position: 100% 100%;
+ background-size: 250%;
+ filter: brightness(1.3) saturate(0);
+ mix-blend-mode: overlay;
+ will-change: opacity, background-position;
+ animation: shift-background 60s infinite linear alternate;
+ animation-play-state: paused;
+ transition: opacity var(--speed) ease-in;
+ }
+
+ .active .overlay {
+ opacity: 0.3;
+ animation-play-state: running;
+ transition: opacity calc(var(--speed) * 2) ease-in
+ calc(var(--speed) * 2);
+ }
+
+ .active.out-of-view .overlay,
+ .active.resizing .overlay {
+ animation-play-state: paused;
+ opacity: 0;
+ }
+
+ @keyframes shift-background {
+ 0% {
+ background-position: 0% 50%;
+ background-size: 250%;
+ }
+
+ 25% {
+ background-position: 60% 20%;
+ background-size: 300%;
+ }
+
+ 50% {
+ background-position: 100% 50%;
+ background-size: 320%;
+ }
+
+ 75% {
+ background-position: 40% 100%;
+ background-size: 220%;
+ }
+
+ 100% {
+ background-position: 20% 50%;
+ background-size: 300%;
+ }
+ }
+</style>