feat: make thumbhash work with image slider
Some checks failed
Deploy to SFTP Server / build (push) Has been cancelled
Some checks failed
Deploy to SFTP Server / build (push) Has been cancelled
This commit is contained in:
parent
68431e6b9c
commit
96c053d5ff
@ -70,8 +70,8 @@ picture.thumb::before {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
width: 30px;
|
width: 20px;
|
||||||
height: 30px;
|
height: 20px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@ -81,7 +81,6 @@ picture.thumb::before {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
picture.thumb-loading::before {
|
picture.thumb-loading::before {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ const link = translatePath(`/${collection}/${slug.split("/")[0]}`);
|
|||||||
<a href={link}>
|
<a href={link}>
|
||||||
<Image
|
<Image
|
||||||
hash
|
hash
|
||||||
|
loader={false}
|
||||||
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"
|
||||||
|
@ -1,40 +1,20 @@
|
|||||||
---
|
---
|
||||||
import type { ImageMetadata } from "astro";
|
import type { ImageMetadata } from "astro";
|
||||||
import { Picture as AstroImage } from "astro:assets";
|
import { Picture as AstroImage } from "astro:assets";
|
||||||
|
import { generateThumbHash } from "@helpers/image";
|
||||||
interface Props {
|
interface Props {
|
||||||
src: ImageMetadata;
|
src: ImageMetadata;
|
||||||
alt: string;
|
alt: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
hash?: boolean;
|
hash?: boolean;
|
||||||
|
loader?: boolean;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
}
|
}
|
||||||
import { rgbaToThumbHash } from "thumbhash";
|
|
||||||
|
|
||||||
const { src: image, hash = true, alt, maxWidth } = Astro.props;
|
const { src: image, loader = true, hash = true, alt, maxWidth } = Astro.props;
|
||||||
|
|
||||||
let thumbhash = "";
|
let thumbhash = hash ? await generateThumbHash(image) : "";
|
||||||
|
|
||||||
const s = await import("sharp");
|
|
||||||
const sharp = s.default;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
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 = [
|
||||||
{
|
{
|
||||||
@ -59,7 +39,9 @@ const sizes = [
|
|||||||
src={image}
|
src={image}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
data-thumbhash={thumbhash}
|
data-thumbhash={thumbhash}
|
||||||
pictureAttributes={{ class: hash ? "block h-full relative" : "" }}
|
pictureAttributes={{
|
||||||
|
class: `${hash ? "block h-full relative" : ""} ${loader ? "thumb" : ""}`,
|
||||||
|
}}
|
||||||
class={Astro.props.class}
|
class={Astro.props.class}
|
||||||
widths={sizes.map((size) => size.width)}
|
widths={sizes.map((size) => size.width)}
|
||||||
sizes={sizes
|
sizes={sizes
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
let slot: HTMLDivElement;
|
let slot: HTMLDivElement;
|
||||||
let images: HTMLImageElement[];
|
let images: (HTMLPictureElement | HTMLImageElement)[];
|
||||||
|
|
||||||
export let title: string;
|
export let title: string;
|
||||||
|
|
||||||
@ -10,17 +8,24 @@
|
|||||||
let height: number;
|
let height: number;
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
|
||||||
function hide(img: HTMLImageElement) {
|
function hide(img: HTMLPictureElement) {
|
||||||
img.classList.remove("active");
|
img.classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(img: HTMLImageElement) {
|
function show(img: HTMLPictureElement) {
|
||||||
img.classList.add("active");
|
img.classList.add("active");
|
||||||
if (img?.alt) altText = img.alt;
|
const _img = img.querySelector("img");
|
||||||
|
if (!_img) return;
|
||||||
|
_img.addEventListener("load", () => {
|
||||||
|
img.classList.remove("thumb-loading");
|
||||||
|
_img.style.opacity = "1";
|
||||||
|
console.log("loaded");
|
||||||
|
});
|
||||||
|
if (_img?.alt) altText = _img.alt;
|
||||||
else altText = "";
|
else altText = "";
|
||||||
height = img.getBoundingClientRect().height;
|
height = _img.getBoundingClientRect().height;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
height = img.getBoundingClientRect().height;
|
height = _img.getBoundingClientRect().height;
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,8 +38,8 @@
|
|||||||
show(images[index]);
|
show(images[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (slot) {
|
$: if (slot && !images?.length) {
|
||||||
images = Array.from(slot.querySelectorAll("img"));
|
images = Array.from(slot.querySelectorAll("picture"));
|
||||||
if (images?.length) {
|
if (images?.length) {
|
||||||
images.forEach(hide);
|
images.forEach(hide);
|
||||||
show(images[index]);
|
show(images[index]);
|
||||||
@ -52,23 +57,14 @@
|
|||||||
<div
|
<div
|
||||||
class="wrapper grid overflow-hidden rounded-xl border border-light"
|
class="wrapper grid overflow-hidden rounded-xl border border-light"
|
||||||
class:title
|
class:title
|
||||||
|
class:not-loaded={!loaded}
|
||||||
class:loaded
|
class:loaded
|
||||||
style={`--height:${height}px`}
|
style={`--height:${height}px`}
|
||||||
>
|
>
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="flex items-center p-x-4 bg">
|
<div class="flex items-center p-x-4 bg justify-between">
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="images" bind:this={slot}>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
<div class="p-2 flex place-content-between bg">
|
|
||||||
<p>{index + 1}/{images?.length}</p>
|
|
||||||
{#if altText}
|
|
||||||
<p class="text-right">{altText}</p>
|
|
||||||
{/if}
|
|
||||||
<div class="overflow-hidden rounded-md bg-light gap-2 flex p-1">
|
<div class="overflow-hidden rounded-md bg-light gap-2 flex p-1">
|
||||||
<button
|
<button
|
||||||
class="flex-1 i-tabler-arrow-left"
|
class="flex-1 i-tabler-arrow-left"
|
||||||
@ -82,6 +78,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="images" bind:this={slot}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div class="p-2 flex place-content-between bg">
|
||||||
|
<p>
|
||||||
|
{#if images?.length}
|
||||||
|
{index + 1}/{images?.length}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{#if altText}
|
||||||
|
<p class="flex-1 text-center">{altText}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -102,11 +113,15 @@
|
|||||||
height: calc(var(--height) + 40px);
|
height: calc(var(--height) + 40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.not-loaded .images :global(picture):first-child {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
.wrapper.title.loaded {
|
.wrapper.title.loaded {
|
||||||
height: calc(var(--height) + 80px);
|
height: calc(var(--height) + 80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.images :global(img) {
|
.images :global(picture) {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,13 @@ 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" hash={false} maxWidth={700} />
|
<Image
|
||||||
|
src={MaxImg}
|
||||||
|
alt="its mee"
|
||||||
|
hash={false}
|
||||||
|
maxWidth={700}
|
||||||
|
loader={false}
|
||||||
|
/>
|
||||||
<div class="eye right">
|
<div class="eye right">
|
||||||
<GoogleyEye client:load />
|
<GoogleyEye client:load />
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,6 +26,7 @@ import screenshot_geometry_nodes from "./images/screenshot-geometry-nodes.jpg"
|
|||||||
import screenshot_houdini from "./images/screenshot-houdini.jpg"
|
import screenshot_houdini from "./images/screenshot-houdini.jpg"
|
||||||
import screenshot_unreal from "./images/screenshot-unreal.jpg"
|
import screenshot_unreal from "./images/screenshot-unreal.jpg"
|
||||||
import screenshot_davinci from "./images/screenshot-davinci.jpg"
|
import screenshot_davinci from "./images/screenshot-davinci.jpg"
|
||||||
|
import Plantarium from "./images/plantarium.png"
|
||||||
import ImageGallery from "@components/ImageGallery.svelte"
|
import ImageGallery from "@components/ImageGallery.svelte"
|
||||||
|
|
||||||
<ImageGallery client:load/>
|
<ImageGallery client:load/>
|
||||||
@ -74,7 +75,7 @@ In der Beispielgrafik haben wir zwei `input-color` nodes die jeweils eine Farbe
|
|||||||
|
|
||||||
Das coole ist das man dieses System sehr generisch gestalten kann und zum beispiel eine `generate-stem`, `generate-branches` oder eine `add-leaves` node programmieren kann. Aufgrund der
|
Das coole ist das man dieses System sehr generisch gestalten kann und zum beispiel eine `generate-stem`, `generate-branches` oder eine `add-leaves` node programmieren kann. Aufgrund der
|
||||||
|
|
||||||
<img src="/projects/plantarium/screenshot-plantarium.png" alt="Plantariums uses nodes to create plants"/>
|
<Image src={Plantarium} alt="Plantariums uses nodes to create plants"/>
|
||||||
|
|
||||||
<ImageSlider title="Beispiele von Node Systemen" client:load>
|
<ImageSlider title="Beispiele von Node Systemen" client:load>
|
||||||
<Image src={screenshot_geometry_nodes} alt="Blenders uses nodes to create geometry"/>
|
<Image src={screenshot_geometry_nodes} alt="Blenders uses nodes to create geometry"/>
|
||||||
|
34
src/helpers/image.ts
Normal file
34
src/helpers/image.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
let loadingSharp = false;
|
||||||
|
import { rgbaToThumbHash } from "thumbhash";
|
||||||
|
|
||||||
|
export async function generateThumbHash(image: { width: number, height: number }) {
|
||||||
|
if (!loadingSharp) {
|
||||||
|
loadingSharp = true;
|
||||||
|
setTimeout(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis["sharp"] = (await import("sharp")).default;
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const sharp = globalThis["sharp"] as typeof import("sharp");
|
||||||
|
|
||||||
|
if (!sharp) return;
|
||||||
|
|
||||||
|
const scaleFactor = 100 / Math.max(image.width, image.height);
|
||||||
|
|
||||||
|
const smallWidth = Math.floor(image.width * scaleFactor);
|
||||||
|
const smallHeight = Math.floor(image.height * scaleFactor);
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const smallImg = await sharp(image.fsPath)
|
||||||
|
.resize(smallWidth, smallHeight)
|
||||||
|
.withMetadata()
|
||||||
|
.raw()
|
||||||
|
.ensureAlpha()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const buffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg);
|
||||||
|
return Buffer.from(buffer).toString("base64");
|
||||||
|
}
|
@ -123,7 +123,7 @@ const { title, width = "compact" } = Astro.props;
|
|||||||
<main id="main-content" class="flex flex-col mt-4xl gap-y-2xl">
|
<main id="main-content" class="flex flex-col mt-4xl gap-y-2xl">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer class="px-4">
|
||||||
<LanguagePicker />
|
<LanguagePicker />
|
||||||
</footer>
|
</footer>
|
||||||
<style>
|
<style>
|
||||||
@ -157,7 +157,6 @@ const { title, width = "compact" } = Astro.props;
|
|||||||
const img = entry;
|
const img = entry;
|
||||||
|
|
||||||
if (parent?.nodeName !== "PICTURE") return;
|
if (parent?.nodeName !== "PICTURE") return;
|
||||||
parent.classList.add("thumb");
|
|
||||||
|
|
||||||
const hash = img.getAttribute("data-thumbhash");
|
const hash = img.getAttribute("data-thumbhash");
|
||||||
if (!hash) return;
|
if (!hash) return;
|
||||||
@ -178,18 +177,31 @@ const { title, width = "compact" } = Astro.props;
|
|||||||
parent.style.background = `url(${dataURL})`;
|
parent.style.background = `url(${dataURL})`;
|
||||||
parent.style.backgroundSize = "cover";
|
parent.style.backgroundSize = "cover";
|
||||||
|
|
||||||
if (img.complete) return;
|
if (img.complete || parent.complete) return;
|
||||||
|
let count = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
count++;
|
||||||
|
if (count > 20) {
|
||||||
|
clearInterval(interval);
|
||||||
|
show(img);
|
||||||
|
}
|
||||||
|
if (img.complete || parent.complete) {
|
||||||
|
clearInterval(interval);
|
||||||
|
show(img);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
img.addEventListener(
|
||||||
|
"load",
|
||||||
|
() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
show(img);
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
img.style.opacity = "0";
|
img.style.opacity = "0";
|
||||||
img.style.filter = "blur(5px)";
|
|
||||||
img.style.transition = "opacity 0.6s ease, filter 0.8s ease";
|
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>
|
</script>
|
||||||
|
@ -43,7 +43,7 @@ const otherPosts = posts.filter((post) => featuredPost !== post).slice(0, 3);
|
|||||||
<span class="i-tabler-circle-arrow-right-thin"></span>
|
<span class="i-tabler-circle-arrow-right-thin"></span>
|
||||||
<ArrowA />
|
<ArrowA />
|
||||||
{featuredProject && <HeroCard post={featuredProject} />}
|
{featuredProject && <HeroCard post={featuredProject} />}
|
||||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
<div class="grid xs:grid-cols-2 gap-4 mt-4">
|
||||||
{
|
{
|
||||||
otherProjects.length > 0 &&
|
otherProjects.length > 0 &&
|
||||||
otherProjects.map((project) => <SmallCard post={project} />)
|
otherProjects.map((project) => <SmallCard post={project} />)
|
||||||
|
@ -22,6 +22,14 @@ export default defineConfig({
|
|||||||
"md": "20px",
|
"md": "20px",
|
||||||
"diag-md": "0px var(--border-radius-md) 0px var(--border-radius-md)"
|
"diag-md": "0px var(--border-radius-md) 0px var(--border-radius-md)"
|
||||||
},
|
},
|
||||||
|
screens: {
|
||||||
|
xs: "375px",
|
||||||
|
sm: "640px",
|
||||||
|
md: "768px",
|
||||||
|
lg: "1024px",
|
||||||
|
xl: "1280px",
|
||||||
|
"2xl": "1536px",
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
neutral: {
|
neutral: {
|
||||||
"000": "var(--neutral-000)",
|
"000": "var(--neutral-000)",
|
||||||
|
Loading…
Reference in New Issue
Block a user