feat: some shit
This commit is contained in:
391
src/components/ImageGallery.svelte
Normal file
391
src/components/ImageGallery.svelte
Normal file
@ -0,0 +1,391 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import normalizeWheel from "@helpers/normalizeWheel";
|
||||
let images = [];
|
||||
let progress = [];
|
||||
let currentIndex = -1;
|
||||
const maxZoom = 5;
|
||||
import { swipe } from "svelte-gestures";
|
||||
|
||||
const mod = (a: number, b: number) => ((a % b) + b) % b;
|
||||
|
||||
const lerp = (a: number, b: number, t: number) => a * t + b * (1 - t);
|
||||
|
||||
const addIndex = (offset: number) => {
|
||||
return setIndex(mod(currentIndex + offset, images.length));
|
||||
};
|
||||
|
||||
let mx = 0,
|
||||
my = 0,
|
||||
_mx = 0,
|
||||
_my = 0,
|
||||
scale = 1;
|
||||
|
||||
const setIndex = (index: number) => {
|
||||
mx = window.innerWidth / 2;
|
||||
my = window.innerHeight / 2;
|
||||
scale = 1;
|
||||
if (index < 0) {
|
||||
document.body.style.overflowY = "auto";
|
||||
} else {
|
||||
document.body.style.overflowY = "hidden";
|
||||
}
|
||||
currentIndex = index;
|
||||
};
|
||||
|
||||
const handleKeyDown = ({ key }) => {
|
||||
if (currentIndex < 0) return;
|
||||
if (key === "Escape" && currentIndex > -1) setIndex(-1);
|
||||
if (key === "ArrowLeft") addIndex(-1);
|
||||
if (key === "ArrowRight") addIndex(+1);
|
||||
};
|
||||
|
||||
const handleOriginalLoading = async (image) => {
|
||||
if (!image.startedLoading) {
|
||||
image.startedLoading = true;
|
||||
let cIndex = currentIndex;
|
||||
if (image.original) {
|
||||
const response = await fetch(image.original, {
|
||||
headers: {
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Content-Type":
|
||||
"image/avif,image/webp,image/apng,image/svg+xml,image/jpeg,image/png,image/*,*/*;q=0.8",
|
||||
},
|
||||
});
|
||||
const total = Number(response.headers.get("content-length"));
|
||||
const reader = response.body.getReader();
|
||||
let bytesReceived = 0;
|
||||
let chunks = [];
|
||||
console.log("[SLIDER] started loading " + image.original);
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
console.log("[SLIDER] Image complete");
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
if (total) {
|
||||
bytesReceived += value.length;
|
||||
progress[cIndex] = bytesReceived / total;
|
||||
console.log(
|
||||
"[SLIDER] " + Math.floor(progress[cIndex] * 1000) / 10 + "%",
|
||||
);
|
||||
progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
const b = new Blob(chunks);
|
||||
images[cIndex].loaded = URL.createObjectURL(b);
|
||||
images = images;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleSwipe(ev: any) {
|
||||
if (currentIndex === -1) return;
|
||||
if (ev.detail.direction === "right") addIndex(-1);
|
||||
if (ev.detail.direction === "left") addIndex(+1);
|
||||
}
|
||||
|
||||
const handleScroll = (ev: WheelEvent) => {
|
||||
const { pixelY } = normalizeWheel(ev);
|
||||
scale = Math.min(Math.max(scale - pixelY / 100, 1), maxZoom);
|
||||
if (scale > 3) {
|
||||
handleOriginalLoading(images[currentIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
let isUpdating = false;
|
||||
const update = () => {
|
||||
isUpdating && requestAnimationFrame(update);
|
||||
|
||||
let dist = Math.abs(mx - _mx) + Math.abs(my - _my);
|
||||
|
||||
if (dist < 0.1 || scale == 1) {
|
||||
mx = _mx;
|
||||
my = _my;
|
||||
isUpdating = false;
|
||||
} else {
|
||||
let s = (0.2 + (1 - scale / maxZoom) * 0.8) * 0.3;
|
||||
mx = lerp(mx, _mx, 1 - s);
|
||||
my = lerp(my, _my, 1 - s);
|
||||
}
|
||||
};
|
||||
|
||||
const startUpdate = () => {
|
||||
if (!isUpdating && scale !== 1) {
|
||||
isUpdating = true;
|
||||
update();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (ev: MouseEvent) => {
|
||||
_mx = ev.clientX;
|
||||
_my = ev.clientY;
|
||||
startUpdate();
|
||||
};
|
||||
|
||||
const handlePointerMove = () => {
|
||||
// console.log(ev);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const wrappers = Array.prototype.slice.call(
|
||||
document.querySelectorAll("[data-image-component]"),
|
||||
);
|
||||
console.log({ wrappers });
|
||||
|
||||
images = wrappers.map((image, i: number) => {
|
||||
console.log({ image });
|
||||
image.classList.add("image-active");
|
||||
image.addEventListener("click", () => setIndex(i));
|
||||
image.addEventListener("touch", () => setIndex(i));
|
||||
image.style.cursor = "pointer";
|
||||
|
||||
image.addEventListener("error", () => {
|
||||
console.log("Error loading", image);
|
||||
});
|
||||
|
||||
let exif = null;
|
||||
|
||||
try {
|
||||
let rawExif = image.getAttribute("data-exif");
|
||||
exif = JSON.parse(rawExif);
|
||||
} catch (error) {
|
||||
// No biggie
|
||||
}
|
||||
|
||||
return {
|
||||
exif,
|
||||
// preview: preview.getAttribute("src"),
|
||||
src: image.getAttribute("srcset"),
|
||||
alt: image.getAttribute("alt"),
|
||||
sizes: image.getAttribute("sizes"),
|
||||
original: image.getAttribute("data-original"),
|
||||
originalLoaded: false,
|
||||
};
|
||||
});
|
||||
|
||||
return () => {
|
||||
wrappers.forEach(({ children: [_, image] }, i: number) => {
|
||||
image.removeEventListener("click", () => setIndex(i));
|
||||
});
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
<div class="image-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="gallery-wrapper" class:visible={currentIndex > -1}>
|
||||
{#if images.length > 1}
|
||||
<div class="controls">
|
||||
{#each images as _, i}
|
||||
<button
|
||||
class:active={currentIndex === i}
|
||||
on:click={() => {
|
||||
currentIndex = i;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button class="left" on:click={() => addIndex(-1)}><</button>
|
||||
<button class="right" on:click={() => addIndex(+1)}>></button>
|
||||
<button class="close" on:click={() => setIndex(-1)}>X</button>
|
||||
|
||||
{#if currentIndex > -1}
|
||||
<div
|
||||
class="image"
|
||||
use:swipe
|
||||
on:swipe={handleSwipe}
|
||||
on:wheel|passive={handleScroll}
|
||||
on:mousemove={handleMouseMove}
|
||||
on:pointermove={handlePointerMove}
|
||||
>
|
||||
{#if progress[currentIndex] && progress[currentIndex] < 0.99}
|
||||
<div
|
||||
transition:fade
|
||||
id="progress"
|
||||
style={`transform: scaleX(${progress[currentIndex]});`}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<img
|
||||
class="background"
|
||||
src={images[currentIndex].preview}
|
||||
alt="background blur"
|
||||
/>
|
||||
|
||||
<span>
|
||||
<img
|
||||
style={`transform: scale(${scale}); transform-origin: ${
|
||||
window.innerWidth - mx
|
||||
}px ${window.innerHeight - my}px`}
|
||||
srcset={images[currentIndex].loaded ? "" : images[currentIndex].src}
|
||||
src={images[currentIndex].loaded}
|
||||
alt={images[currentIndex].alt}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{#if images[currentIndex].exif}
|
||||
{@const exif = images[currentIndex].exif}
|
||||
<div class="exif" on:click={() => console.log(exif)}>
|
||||
{#if "FocalLength" in exif}
|
||||
{exif.FocalLength}mm |
|
||||
{/if}
|
||||
|
||||
{#if "FNumber" in exif}
|
||||
<i>f</i>{exif.FNumber} |
|
||||
{/if}
|
||||
|
||||
{#if "ExposureTime" in exif}
|
||||
{exif.ExposureTime.replace(" s", "s")} |
|
||||
{/if}
|
||||
|
||||
{#if "Date" in exif}
|
||||
{exif.Date}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#progress {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
height: 10px;
|
||||
z-index: 99;
|
||||
left: 47px;
|
||||
background-color: black;
|
||||
width: calc(100% - 94px);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.gallery-wrapper {
|
||||
position: fixed;
|
||||
z-index: 199;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: rgba(24, 24, 24, 0.99);
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.gallery-wrapper.visible {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image > span {
|
||||
height: calc(100vh - 70px);
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.image img {
|
||||
position: relative;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 98;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
transform-origin 0.1s linear;
|
||||
}
|
||||
|
||||
.image > .background {
|
||||
filter: brightness(0.2);
|
||||
position: absolute;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
z-index: 98;
|
||||
height: 100%;
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: fit-content;
|
||||
left: 50vw;
|
||||
transform: translateX(-50%);
|
||||
background: black;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.controls > button {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-style: none;
|
||||
background: black;
|
||||
border: solid 1px white;
|
||||
cursor: pointer;
|
||||
margin: 2px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.controls > button.active {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.exif {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 50vw;
|
||||
transform: translateX(-50%);
|
||||
z-index: 99;
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding: 5px;
|
||||
font-size: 0.8em;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.gallery-wrapper > button {
|
||||
background: black;
|
||||
color: white;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
position: fixed;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.close {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user