feat(theme): 图片加载动画、Blur

This commit is contained in:
Ad-closeNN
2026-05-02 13:34:18 +08:00
parent 8ef97d62ff
commit 005d8923e4
6 changed files with 134 additions and 6 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ const { remarkPluginFrontmatter } = await render(entry);
</Icon> </Icon>
</div> </div>
<ImageWrapper src={image} basePath={path.join("content/posts/", getDir(entry.id))} alt="Cover Image of the Post" <ImageWrapper src={image} basePath={path.join("content/posts/", getDir(entry.id))} alt="Cover Image of the Post"
class="w-full h-full"> class="w-full h-full" loader>
</ImageWrapper> </ImageWrapper>
</a>} </a>}
+4 -2
View File
@@ -8,12 +8,14 @@ interface Props {
alt?: string; alt?: string;
position?: string; position?: string;
basePath?: string; basePath?: string;
loader?: boolean;
blur?: boolean;
} }
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import { url } from "../../utils/url-utils"; import { url } from "../../utils/url-utils";
const { id, src, alt, position = "center", basePath = "/" } = Astro.props; const { id, src, alt, position = "center", basePath = "/", loader = false, blur = false } = Astro.props;
const className = Astro.props.class; const className = Astro.props.class;
const isLocal = !( const isLocal = !(
@@ -51,7 +53,7 @@ const originalSrc = isLocal && img ? img.src : isPublic ? url(normalizedPublicSr
const originalWidth = isLocal && img ? img.width : undefined; const originalWidth = isLocal && img ? img.width : undefined;
const originalHeight = isLocal && img ? img.height : undefined; const originalHeight = isLocal && img ? img.height : undefined;
--- ---
<div id={id} class:list={[className, 'overflow-hidden relative']}> <div id={id} class:list={[className, "overflow-hidden relative", loader && "image-loading-shell is-loading", blur && "image-blur-shell is-loading"]}>
<div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div> <div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>
{isLocal && img && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle} data-pswp-src={originalSrc} data-pswp-width={originalWidth} data-pswp-height={originalHeight} data-original-src={originalSrc}/>} {isLocal && img && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle} data-pswp-src={originalSrc} data-pswp-width={originalWidth} data-pswp-height={originalHeight} data-original-src={originalSrc}/>}
{!isLocal && <img src={originalSrc} alt={alt || ""} class={imageClass} style={imageStyle} data-pswp-src={originalSrc} data-original-src={originalSrc}/>} {!isLocal && <img src={originalSrc} alt={alt || ""} class={imageClass} style={imageStyle} data-pswp-src={originalSrc} data-original-src={originalSrc}/>}
+82
View File
@@ -679,6 +679,88 @@ window.onresize = () => {
} }
</script> </script>
<script>
type ImageLoaderState = {
pageLoadRegistered: boolean
swupHookRegistered: boolean
}
const imageLoaderState = (((window as Window & { __blogImageLoader?: ImageLoaderState }).__blogImageLoader ??= {
pageLoadRegistered: false,
swupHookRegistered: false,
}) as ImageLoaderState)
function markImageLoaded(wrapper: HTMLElement) {
if (wrapper.classList.contains("image-blur-shell")) {
window.setTimeout(() => wrapper.classList.remove("is-loading"), 120)
return
}
wrapper.classList.remove("is-loading")
}
function bindImageLoader(wrapper: HTMLElement) {
if (wrapper.dataset.imageLoadingBound === "true") {
return
}
const image = wrapper.querySelector("img")
if (!(image instanceof HTMLImageElement)) {
markImageLoaded(wrapper)
return
}
wrapper.dataset.imageLoadingBound = "true"
if (image.complete) {
markImageLoaded(wrapper)
return
}
image.addEventListener("load", () => markImageLoaded(wrapper), { once: true })
image.addEventListener("error", () => markImageLoaded(wrapper), { once: true })
}
function wrapMarkdownImage(image: HTMLImageElement) {
if (image.closest(".image-loading-shell, .image-blur-shell") || !image.parentNode) {
return
}
const wrapper = document.createElement("span")
wrapper.className = "image-loading-shell image-loading-shell--markdown is-loading"
image.parentNode.insertBefore(wrapper, image)
wrapper.appendChild(image)
}
function initImageLoaders() {
document.querySelectorAll<HTMLImageElement>(".custom-md img:not([data-image-loading-skip])").forEach(wrapMarkdownImage)
document.querySelectorAll<HTMLElement>(".image-loading-shell, .image-blur-shell").forEach(bindImageLoader)
}
function registerImageLoaderSwupHook() {
if (imageLoaderState.swupHookRegistered || !window.swup?.hooks) {
return
}
window.swup.hooks.on("page:view", initImageLoaders)
imageLoaderState.swupHookRegistered = true
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initImageLoaders, { once: true })
} else {
initImageLoaders()
}
if (!imageLoaderState.pageLoadRegistered) {
document.addEventListener("astro:page-load", initImageLoaders)
imageLoaderState.pageLoadRegistered = true
}
if (window.swup?.hooks) {
registerImageLoaderSwupHook()
} else {
document.addEventListener("swup:enable", registerImageLoaderSwupHook, { once: true })
}
</script>
<script> <script>
import PhotoSwipeLightbox from "photoswipe/lightbox" import PhotoSwipeLightbox from "photoswipe/lightbox"
import "photoswipe/style.css" import "photoswipe/style.css"
+1 -1
View File
@@ -54,7 +54,7 @@ const mainPanelTop = siteConfig.banner.enable
<!-- Banner --> <!-- Banner -->
{siteConfig.banner.enable && <div id="banner-wrapper" class={`absolute z-10 w-full transition duration-700 overflow-hidden`} style={`top: -${BANNER_HEIGHT_EXTEND}vh`}> {siteConfig.banner.enable && <div id="banner-wrapper" class={`absolute z-10 w-full transition duration-700 overflow-hidden`} style={`top: -${BANNER_HEIGHT_EXTEND}vh`}>
<ImageWrapper id="banner" alt="Banner image of the blog" class:list={["object-cover h-full transition duration-700 opacity-0 scale-105"]} <ImageWrapper id="banner" alt="Banner image of the blog" class:list={["object-cover h-full transition duration-700 opacity-0 scale-105"]}
src={siteConfig.banner.src} position={siteConfig.banner.position} src={siteConfig.banner.src} position={siteConfig.banner.position} blur
> >
</ImageWrapper> </ImageWrapper>
</div>} </div>}
+2 -2
View File
@@ -373,12 +373,12 @@ const isOutdated = entry.data.outdated;
<!-- always show cover as long as it has one --> <!-- always show cover as long as it has one -->
<!-- 使用自制 showcover 控制器控制头图的出现 --> <!-- 使用自制 showcover 控制器控制头图的出现 -->
{showcover && entry.data.image && {showcover && entry.data.image &&
<ImageWrapper id="post-cover" src={entry.data.image} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl banner-container onload-animation"/> <ImageWrapper id="post-cover" src={entry.data.image} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl banner-container onload-animation" loader/>
} }
<!-- 自制内部头图 customcover--> <!-- 自制内部头图 customcover-->
{customcover && {customcover &&
<ImageWrapper id="post-cover" src={entry.data.customcover} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl banner-container onload-animation"/> <ImageWrapper id="post-cover" src={entry.data.customcover} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl banner-container onload-animation" loader/>
} }
<!-- 头图调试代码 <!-- 头图调试代码
+44
View File
@@ -156,6 +156,50 @@
@apply cursor-zoom-in @apply cursor-zoom-in
} }
.image-loading-shell {
background: color-mix(in oklch, var(--card-bg) 92%, var(--btn-regular-bg) 8%);
}
.image-loading-shell > img {
transition: opacity 0.28s ease, filter 0.55s ease;
}
.image-blur-shell > img {
transition: filter 0.28s ease;
}
.image-loading-shell.is-loading > img {
opacity: 0;
filter: blur(4px);
}
.image-blur-shell.is-loading > img {
filter: blur(3px);
}
.image-loading-shell--markdown {
display: block;
position: relative;
overflow: hidden;
border-radius: 0.75rem;
}
.image-loading-shell--markdown.is-loading {
min-height: 12rem;
}
.custom-md .image-loading-shell--markdown {
margin-top: 2em;
margin-bottom: 2em;
}
.custom-md .image-loading-shell--markdown img {
margin-top: 0;
margin-bottom: 0;
}
::selection { ::selection {
background-color: var(--selection-bg); background-color: var(--selection-bg);
color: inherit; color: inherit;