Files
imagesbyolofsson-se/src/components/ThreeDScroll.astro
T
Linus OlofssonandClaude Opus 5.5 1e921dfe37 fix(images): stop publishing EXIF/GPS originals, responsive images, crawlable gallery
- 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
2026-10-27 19:13:24 +01:00

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>