- Homepage images: <Picture> from src/assets (AVIF/WebP/JPG srcset, width/ height, first image eager + fetchpriority high, rest lazy), Swedish alt texts. (The duplicate EXIF-laden copies in public/3d-scroll were deleted in 30e385e; README links now point at src/assets.) - Portfolio: originals are no longer referenced (no .src access), so Astro does not copy them to dist/. Small/Medium/Large downloads and the lightbox srcset are EXIF-free JPGs from getImage. Old -s/-m files are no longer imported. - Alt texts: Swedish fallback from date (EXIF or filename), never camera model or "undefined"; portfolio-meta.ts overrides per image. - Gallery items are <a href> to the large image (lightbox on click); download options are buttons; no empty H3, no H4 for the password box. - Stats beacon unchanged; item keys computed at build time (same format). - Script moved inside the layout so it is no longer rendered after </html>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
122 lines
4.2 KiB
Plaintext
122 lines
4.2 KiB
Plaintext
---
|
|
// src/components/ThreeDScroll.astro
|
|
// Bilderna ligger i src/assets så att Astro skalar om dem (AVIF/WebP/JPG, srcset)
|
|
// och rensar bort EXIF (GPS, serienummer m.m.). Originalen publiceras inte.
|
|
import { Picture } from 'astro:assets';
|
|
import img1 from '../assets/images/3d-scroll/img1.jpg';
|
|
import img2 from '../assets/images/3d-scroll/img2.jpg';
|
|
import img3 from '../assets/images/3d-scroll/img3.jpg';
|
|
import img4 from '../assets/images/3d-scroll/img4.jpg';
|
|
import img5 from '../assets/images/3d-scroll/img5.jpg';
|
|
|
|
const images = [
|
|
{ src: img1, alt: 'Närbild av en gammal läderinbunden bok med slitna pappersark i varmt motljus' },
|
|
{ src: img2, alt: 'Veteranbil med tända strålkastare på en stadsgata i kvällsljus' },
|
|
{ src: img3, alt: 'Överblommade tistlar bland höga grässtrån i dämpade bruna toner' },
|
|
{ src: img4, alt: 'Järnvägsspår som slingrar sig mellan träd i motljus, i sepiaton' },
|
|
{ src: img5, alt: 'Svartvit bild av en man som bär ett litet barn bland såpbubblor i motljus' },
|
|
];
|
|
const last = images.length - 1;
|
|
---
|
|
|
|
<div id="scroll-container" class="relative h-[400vh] w-full">
|
|
<div class="sticky top-0 h-screen w-full overflow-hidden perspective-container bg-transparent">
|
|
|
|
<div id="scene" class="absolute inset-0 w-full h-full transform-style-3d flex items-center justify-center">
|
|
|
|
{images.map((img, index) => (
|
|
<div
|
|
class="gallery-item absolute transform-style-3d will-change-transform flex items-center justify-center"
|
|
data-index={index}
|
|
>
|
|
{/* Första bilden: eager + hög prioritet. Den sista ligger överst innan man
|
|
scrollat (alla bilder står på samma plats tills scriptet flyttar dem),
|
|
så den laddas också direkt. Övriga laddas lat. */}
|
|
<Picture
|
|
src={img.src}
|
|
alt={img.alt}
|
|
widths={[640, 1024, 1600, 2048]}
|
|
sizes="80vw"
|
|
formats={['avif', 'webp']}
|
|
fallbackFormat="jpg"
|
|
quality={78}
|
|
loading={index === 0 || index === last ? 'eager' : 'lazy'}
|
|
fetchpriority={index === 0 ? 'high' : 'auto'}
|
|
decoding="async"
|
|
class="floating-animation max-w-[80vw] max-h-[70vh] w-auto h-auto object-contain rounded-lg shadow-[0_20px_60px_-15px_rgba(0,0,0,0.8)] border border-white/5 grayscale hover:grayscale-0 transition-all duration-700 ease-out"
|
|
/>
|
|
</div>
|
|
))}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.perspective-container {
|
|
perspective: 1000px;
|
|
}
|
|
|
|
.transform-style-3d {
|
|
transform-style: preserve-3d;
|
|
}
|
|
|
|
/* Svävande animation */
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0px); }
|
|
50% { transform: translateY(-5px); }
|
|
}
|
|
|
|
/* Vi sätter olika delay på animationen så alla bilder inte svävar i synk */
|
|
.floating-animation {
|
|
animation: float 6s ease-in-out infinite;
|
|
}
|
|
.gallery-item:nth-child(odd) .floating-animation {
|
|
animation-duration: 7s;
|
|
animation-delay: 1s;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
const container = document.getElementById('scroll-container');
|
|
const scene = document.getElementById('scene');
|
|
const items = document.querySelectorAll('.gallery-item');
|
|
|
|
const Z_SPACING = 1500;
|
|
const START_Z = -500;
|
|
|
|
function updateScroll() {
|
|
if (!container || !scene) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const scrollProgress = -rect.top;
|
|
|
|
// Om vi inte scrollat in än, nollställ
|
|
if (scrollProgress < 0) return;
|
|
|
|
items.forEach((item, index) => {
|
|
let zPos = (scrollProgress * 1.5) - (index * Z_SPACING) + START_Z;
|
|
|
|
let opacity = 1;
|
|
|
|
if (zPos > 500) {
|
|
opacity = 1 - ((zPos - 500) / 400);
|
|
}
|
|
if (zPos < -2000) {
|
|
opacity = 1 - (Math.abs(zPos + 2000) / 2000);
|
|
}
|
|
if (opacity < 0) opacity = 0;
|
|
|
|
(item as HTMLElement).style.transform = `translateZ(${zPos}px)`;
|
|
(item as HTMLElement).style.opacity = opacity.toString();
|
|
});
|
|
|
|
requestAnimationFrame(updateScroll);
|
|
}
|
|
|
|
window.addEventListener('scroll', () => {
|
|
requestAnimationFrame(updateScroll);
|
|
});
|
|
|
|
updateScroll();
|
|
</script> |