summaryrefslogtreecommitdiffstats
path: root/src/scripts/lib
diff options
context:
space:
mode:
authorVolpeon <git@volpeon.ink>2026-07-22 22:32:09 +0200
committerVolpeon <git@volpeon.ink>2026-07-22 22:32:09 +0200
commit656b606c3ed21c64823cd5a91dfc9ca01fa7b1fb (patch)
tree0e93a2cc828f6d5167dfae6ec93a5c96ee8980d8 /src/scripts/lib
parentUse online release of Corvos (diff)
downloadcorvos-website-656b606c3ed21c64823cd5a91dfc9ca01fa7b1fb.tar.gz
corvos-website-656b606c3ed21c64823cd5a91dfc9ca01fa7b1fb.tar.bz2
corvos-website-656b606c3ed21c64823cd5a91dfc9ca01fa7b1fb.zip
Package cleanup
Diffstat (limited to 'src/scripts/lib')
-rw-r--r--src/scripts/lib/lightbox.ts450
1 files changed, 0 insertions, 450 deletions
diff --git a/src/scripts/lib/lightbox.ts b/src/scripts/lib/lightbox.ts
deleted file mode 100644
index ac5b581..0000000
--- a/src/scripts/lib/lightbox.ts
+++ /dev/null
@@ -1,450 +0,0 @@
1import Alpine from "alpinejs";
2
3export interface LightboxItem {
4 thumbnail?: string;
5 src: string;
6 srcset?: string;
7 alt: string;
8}
9
10function touchParams(e: TouchEvent) {
11 const [t0, t1] = e.touches;
12
13 if (e.touches.length > 2 || !t0) {
14 return;
15 }
16
17 let x = t0.clientX;
18 let y = t0.clientY;
19 let dist = 0;
20
21 if (t1) {
22 dist = Math.hypot(x - t1.clientX, y - t1.clientY);
23 x = (x + t1.clientX) / 2;
24 y = (y + t1.clientY) / 2;
25 }
26
27 return { x, y, dist };
28}
29
30export function mountLightbox() {
31 const minZoom = 0.9;
32
33 Alpine.data("lightbox", () => ({
34 _zoom: minZoom,
35 _offsetX: 0,
36 _offsetY: 0,
37
38 loading: false,
39 inertia: false,
40 items: <LightboxItem[]>[],
41 current: 0,
42 dragging: false,
43 touchStart: {
44 x: 0,
45 y: 0,
46 distance: 0,
47 transformOrigin: { x: 0, y: 0 },
48 zoom: 1,
49 offsetX: 0,
50 offsetY: 0,
51 timestamp: 0,
52 },
53 velocity: {
54 measure: false,
55 prevOffsetX: 0,
56 prevOffsetY: 0,
57 timestamp: 0,
58 x: 0,
59 y: 0,
60 },
61
62 get zoom() {
63 return this._zoom;
64 },
65 set zoom(value) {
66 this._zoom = Math.min(this.maxZoom, Math.max(minZoom, value));
67 /* oxlint-disable-next-line eslint/no-self-assign */ // deno-lint-ignore no-self-assign
68 this.offsetX = this.offsetX;
69 /* oxlint-disable-next-line eslint/no-self-assign */ // deno-lint-ignore no-self-assign
70 this.offsetY = this.offsetY;
71 },
72
73 get offsetX() {
74 return this._offsetX;
75 },
76 set offsetX(value) {
77 const limit = this.offsetLimit;
78 this._offsetX = Math.min(limit.x, Math.max(-limit.x, value));
79 },
80
81 get offsetY() {
82 return this._offsetY;
83 },
84 set offsetY(value) {
85 const limit = this.offsetLimit;
86 this._offsetY = Math.min(limit.y, Math.max(-limit.y, value));
87 },
88
89 get offsetLimit(): { x: number; y: number } {
90 if (!this.$refs.img) {
91 return { x: 0, y: 0 };
92 }
93
94 const th = this.$refs.thumbnails?.clientHeight ?? 0;
95 const cw = this.$root.clientWidth;
96 const ch = this.$root.clientHeight - th;
97 const x = 0.5 * Math.max(0, this.$refs.img.clientWidth * this.zoom - cw);
98 const y = 0.5 * Math.max(0, this.$refs.img.clientHeight * this.zoom - ch);
99
100 return { x, y };
101 },
102
103 get open(): boolean {
104 return !!this.$root.hasAttribute("open");
105 },
106
107 get currentItem(): LightboxItem {
108 return this.items[this.current] ?? { src: "", alt: "" };
109 },
110
111 get transformOrigin(): { x: number; y: number } {
112 const img = this.$refs.img;
113
114 if (!img) {
115 return { x: this.offsetX, y: this.offsetY };
116 }
117
118 return {
119 x: img.offsetLeft + img.clientWidth / 2 + this.offsetX,
120 y: img.offsetTop + img.clientHeight / 2 + this.offsetY,
121 };
122 },
123
124 get naturalZoom() {
125 const img = <HTMLImageElement>this.$refs.img;
126 return img ? img.naturalWidth / img.clientWidth : 1;
127 },
128
129 get maxZoom() {
130 return Math.max(this.naturalZoom, 5);
131 },
132
133 get zoomedFull() {
134 return Math.abs(this.zoom - this.naturalZoom) < 0.1;
135 },
136
137 init() {
138 const triggerEls = document.querySelectorAll<HTMLAnchorElement>("a[data-lightbox]");
139 const galleries = new Map<string, LightboxItem[]>();
140
141 for (const triggerEl of triggerEls) {
142 const triggerImgEl = triggerEl.querySelector("img");
143
144 const key = triggerEl.dataset.lightbox ?? "";
145 const gallery = galleries.get(key);
146 const item: LightboxItem = {
147 thumbnail: triggerImgEl?.src,
148 src: triggerEl.href,
149 srcset: triggerEl.dataset.lightboxSrcset,
150 alt: "",
151 };
152
153 if (gallery) {
154 gallery.push(item);
155 } else {
156 galleries.set(key, [item]);
157 }
158
159 triggerEl.style.cursor = "zoom-in";
160 triggerEl.addEventListener("click", (e) => {
161 e.preventDefault();
162 this.show(galleries.get(key) ?? [], item.src);
163 });
164 }
165
166 this.$watch("currentItem.src", () => {
167 this.loading = true;
168 this.reset();
169 });
170 },
171
172 show(items: LightboxItem[], src?: string) {
173 const current = items.findIndex((item) => item.src === src);
174 this.items = items;
175 this.current = current === -1 ? 0 : current;
176 this.reset();
177
178 (<HTMLDialogElement>this.$root).showModal();
179 },
180
181 close() {
182 (<HTMLDialogElement>this.$root).close();
183 },
184
185 reset() {
186 this.stopVelocity();
187 this.stopInertia();
188 this.dragging = false;
189 this._zoom = minZoom;
190 this._offsetX = 0;
191 this._offsetY = 0;
192 },
193
194 prev() {
195 this.current = this.current > 0 ? this.current - 1 : this.items.length - 1;
196 },
197
198 next() {
199 this.current = this.current < this.items.length - 1 ? this.current + 1 : 0;
200 },
201
202 toggleZoom(x?: number, y?: number) {
203 const zoom = this.zoomedFull ? minZoom : this.naturalZoom;
204 const deltaZoom = zoom / this.zoom - 1;
205
206 this.zoom = zoom;
207
208 if (x !== undefined && y !== undefined) {
209 const transformOrigin = this.transformOrigin;
210 const offsetX = transformOrigin.x - x;
211 const offsetY = transformOrigin.y - y;
212
213 this.offsetX += offsetX * deltaZoom;
214 this.offsetY += offsetY * deltaZoom;
215 }
216 },
217
218 measureVelocity(loop?: boolean) {
219 if (!loop) {
220 this.inertia = false;
221
222 if (this.velocity.measure) {
223 return;
224 } else {
225 this.velocity.prevOffsetX = this.offsetX;
226 this.velocity.prevOffsetY = this.offsetY;
227 this.velocity.x = 0;
228 this.velocity.y = 0;
229 this.velocity.measure = true;
230 }
231 }
232 if (!this.velocity.measure) {
233 return;
234 }
235
236 const deltaX = this.offsetX - this.velocity.prevOffsetX;
237 const deltaY = this.offsetY - this.velocity.prevOffsetY;
238 let window = 1;
239
240 if (
241 Math.sqrt(deltaX ** 2 + deltaY ** 2) <
242 Math.sqrt(this.velocity.x ** 2 + this.velocity.y ** 2)
243 ) {
244 window = 6;
245 }
246
247 this.velocity.x -= this.velocity.x / window;
248 this.velocity.x += deltaX / window;
249 this.velocity.y -= this.velocity.y / window;
250 this.velocity.y += deltaY / window;
251
252 this.velocity.prevOffsetX = this.offsetX;
253 this.velocity.prevOffsetY = this.offsetY;
254
255 requestAnimationFrame(this.measureVelocity.bind(this, true));
256 },
257
258 stopVelocity() {
259 this.velocity.measure = false;
260 },
261
262 runInertia(loop?: boolean, timestamp: DOMHighResTimeStamp = performance.now()) {
263 const len = Math.sqrt(this.velocity.x ** 2 + this.velocity.y ** 2);
264
265 if (!loop) {
266 this.stopVelocity();
267
268 if (this.inertia) {
269 return;
270 } else if (len > 5) {
271 this.inertia = true;
272 this.velocity.timestamp = timestamp;
273 }
274 }
275
276 if (!this.inertia) {
277 this.stopInertia();
278 return;
279 }
280
281 if (loop) {
282 const norm = { x: this.velocity.x / len, y: this.velocity.y / len };
283 const elapsed = timestamp - this.velocity.timestamp;
284 const friction = 30 * (elapsed / 1000);
285 const decel = { x: norm.x * friction, y: norm.y * friction };
286 const decelLen = Math.sqrt(decel.x ** 2 + decel.y ** 2);
287
288 if (len < decelLen) {
289 this.stopInertia();
290 return;
291 }
292
293 this.offsetX += this.velocity.x;
294 this.offsetY += this.velocity.y;
295 this.velocity.x -= decel.x;
296 this.velocity.y -= decel.y;
297 this.velocity.timestamp = timestamp;
298 }
299
300 requestAnimationFrame(this.runInertia.bind(this, true));
301 },
302
303 stopInertia() {
304 this.inertia = false;
305 },
306
307 handleClose() {
308 this.stopVelocity();
309 this.stopInertia();
310 },
311
312 handleScroll(e: WheelEvent) {
313 this.stopInertia();
314
315 const direction = e.deltaY > 0 ? -1 : 1;
316
317 const transformOrigin = this.transformOrigin;
318 const offsetX = transformOrigin.x - e.clientX;
319 const offsetY = transformOrigin.y - e.clientY;
320
321 const zoom = Math.min(this.maxZoom, Math.max(minZoom, this.zoom * (1 + direction * 0.1)));
322 const deltaZoom = zoom / this.zoom - 1;
323
324 this.zoom = zoom;
325 this.offsetX += offsetX * deltaZoom;
326 this.offsetY += offsetY * deltaZoom;
327 },
328
329 handleMouseDown(e: MouseEvent) {
330 this.measureVelocity();
331
332 if (e.buttons === 1) {
333 this.dragging = true;
334 }
335 },
336
337 handleMouseMove(e: MouseEvent) {
338 if (!this.dragging) {
339 return;
340 }
341
342 this.offsetX += e.movementX;
343 this.offsetY += e.movementY;
344 },
345
346 handleMouseUp() {
347 this.dragging = false;
348 this.runInertia();
349 },
350
351 handleDoubleClick(e: MouseEvent) {
352 this.toggleZoom(e.clientX, e.clientY);
353 },
354
355 handleTouchStart(e: TouchEvent) {
356 const params = touchParams(e);
357
358 if (!params || (e.touches.length === 1 && e.target !== this.$refs.img)) {
359 return;
360 }
361
362 e.preventDefault();
363
364 const now = Date.now();
365
366 if (e.touches.length === 1) {
367 if (
368 now - this.touchStart.timestamp < 200 &&
369 Math.sqrt((params.x - this.touchStart.x) ** 2 + (params.y - this.touchStart.y) ** 2) < 20
370 ) {
371 this.toggleZoom(params.x, params.y);
372 }
373 }
374
375 this.touchStart.x = params.x;
376 this.touchStart.y = params.y;
377 this.touchStart.distance = params.dist;
378 this.touchStart.transformOrigin = this.transformOrigin;
379 this.touchStart.zoom = this.zoom;
380 this.touchStart.offsetX = this.offsetX;
381 this.touchStart.offsetY = this.offsetY;
382 this.touchStart.timestamp = now;
383
384 this.dragging = false;
385
386 if (e.touches.length === 1) {
387 this.measureVelocity();
388 } else {
389 this.stopVelocity();
390 }
391 },
392
393 handleTouchMove(e: TouchEvent) {
394 const params = touchParams(e);
395
396 if (!params || (e.touches.length === 1 && e.target !== this.$refs.img)) {
397 return;
398 }
399
400 e.preventDefault();
401
402 const deltaX = params.x - this.touchStart.x;
403 const deltaY = params.y - this.touchStart.y;
404 const deltaDist = this.touchStart.distance ? params.dist / this.touchStart.distance : 1;
405
406 const offsetX = this.touchStart.transformOrigin.x - this.touchStart.x;
407 const offsetY = this.touchStart.transformOrigin.y - this.touchStart.y;
408
409 const zoom = Math.min(this.maxZoom, Math.max(minZoom, this.touchStart.zoom * deltaDist));
410 const deltaZoom = zoom / this.touchStart.zoom - 1;
411
412 this.zoom = zoom;
413 this.offsetX = this.touchStart.offsetX + deltaX + offsetX * deltaZoom;
414 this.offsetY = this.touchStart.offsetY + deltaY + offsetY * deltaZoom;
415 },
416
417 handleTouchEnd(e: TouchEvent) {
418 this.handleTouchStart(e);
419
420 if (!e.touches.length) {
421 this.runInertia();
422 }
423 },
424
425 handleKeyDown(e: KeyboardEvent) {
426 if (!this.open || e.defaultPrevented) {
427 return;
428 }
429
430 this.stopInertia();
431
432 switch (e.key) {
433 case "Left":
434 case "ArrowLeft":
435 this.prev();
436 break;
437
438 case "Right":
439 case "ArrowRight":
440 this.next();
441 break;
442
443 default:
444 return;
445 }
446
447 e.preventDefault();
448 },
449 }));
450}