feat: add thumbhash loading to image
Some checks failed
Deploy to SFTP Server / build (push) Failing after 1m18s
Some checks failed
Deploy to SFTP Server / build (push) Failing after 1m18s
This commit is contained in:
parent
82eb0657e2
commit
5d59e2171d
11
package.json
11
package.json
@ -16,22 +16,15 @@
|
|||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"astro": "^4.5.5",
|
"astro": "^4.5.5",
|
||||||
"astro-i18n-aut": "^0.7.0",
|
"astro-i18n-aut": "^0.7.0",
|
||||||
"astro-imagetools": "^0.9.0",
|
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
|
||||||
"rehype-slug": "^6.0.0",
|
|
||||||
"rehype-stringify": "^10.0.0",
|
|
||||||
"rehype-toc": "^3.0.2",
|
|
||||||
"remark-toc": "^9.0.0",
|
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
"svelte-gestures": "^4.0.0",
|
"svelte-gestures": "^4.0.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"thumbhash": "^0.1.1",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@afordin/unocss-preset-token": "0.1.0-beta.0",
|
|
||||||
"@astrojs/sitemap": "^3.1.1",
|
"@astrojs/sitemap": "^3.1.1",
|
||||||
"@iconify-json/tabler": "^1.1.109",
|
"@iconify-json/tabler": "^1.1.109",
|
||||||
"@igor.dvlpr/astro-post-excerpt": "^2.1.0",
|
|
||||||
"@types/markdown-it": "^13.0.7",
|
"@types/markdown-it": "^13.0.7",
|
||||||
"@unocss/preset-icons": "^0.58.8",
|
"@unocss/preset-icons": "^0.58.8",
|
||||||
"@unocss/reset": "^0.58.8",
|
"@unocss/reset": "^0.58.8",
|
||||||
@ -45,4 +38,4 @@
|
|||||||
"unplugin-icons": "^0.18.5",
|
"unplugin-icons": "^0.18.5",
|
||||||
"vite-plugin-glsl": "^1.3.0"
|
"vite-plugin-glsl": "^1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1132
pnpm-lock.yaml
1132
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -48,6 +48,7 @@ const link = translatePath(`/${collection}/${slug.split("/")[0]}`);
|
|||||||
cover && (
|
cover && (
|
||||||
<a href={link}>
|
<a href={link}>
|
||||||
<Image
|
<Image
|
||||||
|
hash
|
||||||
src={cover}
|
src={cover}
|
||||||
alt={"cover for " + title}
|
alt={"cover for " + title}
|
||||||
class="right-0 h-full object-cover object-center rounded-none border-l border-neutral"
|
class="right-0 h-full object-cover object-center rounded-none border-l border-neutral"
|
||||||
|
@ -6,10 +6,32 @@ interface Props {
|
|||||||
alt: string;
|
alt: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
|
hash?: boolean;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
}
|
}
|
||||||
|
import { rgbaToThumbHash } from "thumbhash";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
const { src: image, alt, maxWidth } = Astro.props;
|
const { src: image, hash = true, alt, maxWidth } = Astro.props;
|
||||||
|
|
||||||
|
let thumbhash = "";
|
||||||
|
|
||||||
|
if (hash) {
|
||||||
|
const scaleFactor = 100 / Math.max(image.width, image.height);
|
||||||
|
|
||||||
|
const smallWidth = Math.floor(image.width * scaleFactor);
|
||||||
|
const smallHeight = Math.floor(image.height * scaleFactor);
|
||||||
|
|
||||||
|
const smallImg = await sharp(image.fsPath)
|
||||||
|
.resize(smallWidth, smallHeight)
|
||||||
|
.withMetadata()
|
||||||
|
.raw()
|
||||||
|
.ensureAlpha()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const buffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg);
|
||||||
|
thumbhash = Buffer.from(buffer).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
const sizes = [
|
const sizes = [
|
||||||
{
|
{
|
||||||
@ -33,6 +55,8 @@ const sizes = [
|
|||||||
<AstroImage
|
<AstroImage
|
||||||
src={image}
|
src={image}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
|
data-thumbhash={thumbhash}
|
||||||
|
pictureAttributes={{ class: hash ? "block h-full" : "" }}
|
||||||
class={Astro.props.class}
|
class={Astro.props.class}
|
||||||
widths={sizes.map((size) => size.width)}
|
widths={sizes.map((size) => size.width)}
|
||||||
sizes={sizes
|
sizes={sizes
|
||||||
|
@ -13,7 +13,7 @@ const t = useTranslations(Astro.url);
|
|||||||
classes="googley-eye-target relative rounded-diag-md border border-neutral gradient grid grid-cols-[250px_1fr] h-[180px] mt-8"
|
classes="googley-eye-target relative rounded-diag-md border border-neutral gradient grid grid-cols-[250px_1fr] h-[180px] mt-8"
|
||||||
>
|
>
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<Image src={MaxImg} alt="its mee" maxWidth={700} />
|
<Image src={MaxImg} alt="its mee" hash={false} maxWidth={700} />
|
||||||
<div class="eye right">
|
<div class="eye right">
|
||||||
<GoogleyEye client:load />
|
<GoogleyEye client:load />
|
||||||
</div>
|
</div>
|
||||||
|
46
src/components/Thumbhash.astro
Normal file
46
src/components/Thumbhash.astro
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import { thumbHashToRGBA, rgbaToDataURL } from "thumbhash";
|
||||||
|
|
||||||
|
function show(img: HTMLImageElement) {
|
||||||
|
img.style.opacity = "1";
|
||||||
|
img.style.filter = "blur(0px)";
|
||||||
|
setTimeout(() => {
|
||||||
|
img.parentNode.style.background = "";
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-thumbhash]").forEach((entry) => {
|
||||||
|
const parent = entry?.parentNode as HTMLPictureElement;
|
||||||
|
const img = entry as HTMLImageElement;
|
||||||
|
|
||||||
|
if (parent?.nodeName !== "PICTURE") return;
|
||||||
|
|
||||||
|
const hash = img.getAttribute("data-thumbhash");
|
||||||
|
if (!hash) return;
|
||||||
|
|
||||||
|
const decodedString = atob(hash);
|
||||||
|
|
||||||
|
// Create Uint8Array from decoded string
|
||||||
|
const buffer = new Uint8Array(decodedString.length);
|
||||||
|
for (let i = 0; i < decodedString.length; i++) {
|
||||||
|
buffer[i] = decodedString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = thumbHashToRGBA(buffer);
|
||||||
|
const dataURL = rgbaToDataURL(image.w, image.h, image.rgba);
|
||||||
|
|
||||||
|
parent.style.background = `url(${dataURL})`;
|
||||||
|
parent.style.backgroundSize = "cover";
|
||||||
|
|
||||||
|
img.style.opacity = "0";
|
||||||
|
img.style.filter = "blur(5px)";
|
||||||
|
img.style.transition = "opacity 0.6s ease, filter 0.8s ease";
|
||||||
|
|
||||||
|
const sources = parent.querySelectorAll("source");
|
||||||
|
|
||||||
|
img.onload = () => show(img);
|
||||||
|
for (const source of sources) {
|
||||||
|
source.addEventListener("load", () => show(img));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
@ -8,6 +8,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { title, width = "compact" } = Astro.props;
|
const { title, width = "compact" } = Astro.props;
|
||||||
|
import Thumbhash from "@components/Thumbhash.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@ -126,6 +127,7 @@ const { title, width = "compact" } = Astro.props;
|
|||||||
<footer>
|
<footer>
|
||||||
<LanguagePicker />
|
<LanguagePicker />
|
||||||
</footer>
|
</footer>
|
||||||
|
<Thumbhash />
|
||||||
<style>
|
<style>
|
||||||
.layout-compact {
|
.layout-compact {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
Loading…
Reference in New Issue
Block a user