PageSourceSearch

https://aymuos15.github.io/js/gallery.js

js aymuos15.github.io collected 2026-10-03 08:41:03 UTC 7,484 bytes, 251 lines download raw bytes

1// Free-floating image cloud
2const photos = [
3  {
4    alt: "Bhavith",
5    rot: -2.5,
6    src: "gallery/bhavith.jpeg",
7    x: -155,
8    y: -80,
9    z: 2,
10  },
11  {
12    alt: "CAI4CAI workshop",
13    rot: 1.8,
14    src: "gallery/cai4cai.jpg",
15    x: 30,
16    y: -30,
17    z: 5,
18  },
19  {
20    alt: "ISBI 2024",
21    rot: -1.5,
22    src: "gallery/isbi.jpeg",
23    x: 150,
24    y: -75,
25    z: 3,
26  },
27  { alt: "Pooja", rot: 2.2, src: "gallery/pooja.jpeg", x: -160, y: 72, z: 2 },
28  {
29    alt: "Parents",
30    rot: -2.0,
31    src: "gallery/parents.jpeg",
32    x: -35,
33    y: 20,
34    z: 4,
35  },
36  {
37    alt: "Pupil study",
38    rot: 0.9,
39    src: "gallery/pupil.jpeg",
40    x: 155,
41    y: 62,
42    z: 1,
43  },
44  { alt: "", rot: -1.2, src: "gallery/wa1.jpeg", x: -85, y: -145, z: 3 },
45  { alt: "", rot: 2.8, src: "gallery/wa2.jpeg", x: 100, y: -140, z: 2 },
46  { alt: "", rot: 1.5, src: "gallery/wa3.jpeg", x: 230, y: -15, z: 2 },
47  { alt: "", rot: -2.3, src: "gallery/wa4.jpeg", x: -230, y: 10, z: 2 },
48  { alt: "", rot: -2.0, src: "gallery/wa5.jpeg", x: 115, y: 145, z: 3 },
49  { alt: "", rot: 3.1, src: "gallery/wa6.jpeg", x: -50, y: 148, z: 1 },
50  { alt: "", rot: 2.4, src: "gallery/wa7.jpeg", x: -270, y: -110, z: 1 },
51  { alt: "", rot: -1.7, src: "gallery/wa8.jpeg", x: 265, y: 130, z: 2 },
52  { alt: "", rot: 1.1, src: "gallery/wa9.jpeg", x: -10, y: -200, z: 1 },
53  { alt: "", rot: -3.0, src: "gallery/wa10.jpeg", x: 205, y: -150, z: 1 },
54  { alt: "", rot: 2.0, src: "gallery/wa11.jpeg", x: -160, y: -195, z: 6 },
55];
56
57const cloud = document.querySelector(".image-cloud");
58const universe = document.createElement("div");
59universe.className = "cloud-universe";
60cloud.appendChild(universe);
61
62// Size the canvas so it always contains every photo (which is positioned from
63// the centre) plus margin — drop a new photo at any coordinate and the canvas
64// grows to include it; existing photos never move. Stays at least viewport-ish
65// so there is always room to pan.
66const margin = 260;
67const spanX = 2 * (Math.max(...photos.map((p) => Math.abs(p.x))) + margin);
68const spanY = 2 * (Math.max(...photos.map((p) => Math.abs(p.y))) + margin);
69universe.style.width = `max(${spanX}px, 140vw)`;
70universe.style.height = `max(${spanY}px, 140vh)`;
71
72let focused = null;
73
74// Centroid of the photo cluster, relative to the canvas centre (where each
75// photo is positioned from). Load-independent — uses fixed coordinates, not
76// rendered image sizes.
77const clusterX =
78  (Math.min(...photos.map((p) => p.x)) + Math.max(...photos.map((p) => p.x))) /
79  2;
80const clusterY =
81  (Math.min(...photos.map((p) => p.y)) + Math.max(...photos.map((p) => p.y))) /
82  2;
83
84// Scroll so the photo cluster sits at the true centre of the screen (both axes),
85// accounting for the cloud's header/footer inset.
86function centreCanvas(behavior) {
87  if (!cloud.clientWidth) {
88    return;
89  }
90  const rect = cloud.getBoundingClientRect();
91  const contentX = universe.offsetWidth / 2 + clusterX;
92  const contentY = universe.offsetHeight / 2 + clusterY;
93  cloud.scrollTo({
94    behavior: behavior || "auto",
95    left: contentX - (window.innerWidth / 2 - rect.left),
96    top: contentY - (window.innerHeight / 2 - rect.top),
97  });
98}
99for (const photo of photos) {
100  const el = document.createElement("div");
101  el.className = "cloud-photo";
102  el.style.setProperty("--cx", `${photo.x}px`);
103  el.style.setProperty("--cy", `${photo.y}px`);
104  el.style.setProperty("--rot", `${photo.rot}deg`);
105  el.style.setProperty("--z", photo.z);
106  // Unique drift per photo: small random-ish offsets based on index
107  const i = photos.indexOf(photo);
108  const dx1 = `${((i * 7 + 3) % 11) - 5}px`;
109  const dy1 = `${((i * 5 + 2) % 9) - 4}px`;
110  const dx2 = `${((i * 3 + 8) % 11) - 5}px`;
111  const dy2 = `${((i * 9 + 1) % 9) - 4}px`;
112  el.style.setProperty("--dx1", dx1);
113  el.style.setProperty("--dy1", dy1);
114  el.style.setProperty("--dx2", dx2);
115  el.style.setProperty("--dy2", dy2);
116  el.style.setProperty("--dur", `${(7 + ((i * 1.3) % 5)).toFixed(1)}s`);
117  el.style.setProperty("--delay", `${(-(i * 1.7) % 6).toFixed(1)}s`);
118  el.setAttribute("role", "button");
119  el.setAttribute("tabindex", "0");
120  el.setAttribute("aria-label", `View photo: ${photo.alt}`);
121
122  const img = document.createElement("img");
123  img.src = photo.src;
124  img.alt = photo.alt;
125  img.loading = "lazy";
126  img.decoding = "async";
127
128  el.appendChild(img);
129  universe.appendChild(el);
130
131  el.addEventListener("click", (e) => {
132    e.stopPropagation();
133    // Ignore the click synthesised at the end of a drag-to-pan.
134    if (wasDragging) {
135      return;
136    }
137    if (focused === el) {
138      el.classList.remove("focused");
139      cloud.classList.remove("has-focus");
140      focused = null;
141    } else {
142      if (focused) {
143        focused.classList.remove("focused");
144      }
145      el.classList.add("focused");
146      cloud.classList.add("has-focus");
147      focused = el;
148      // Bring the centred/zoomed photo into view from any pan offset.
149      centreCanvas("smooth");
150    }
151  });
152
153  el.addEventListener("keydown", (e) => {
154    if (e.key === "Enter" || e.key === " ") {
155      e.preventDefault();
156      el.click();
157    }
158  });
159}
160
161// Dismiss on click outside or Escape
162cloud.addEventListener("click", () => {
163  if (focused) {
164    focused.classList.remove("focused");
165    cloud.classList.remove("has-focus");
166    focused = null;
167  }
168});
169
170document.addEventListener("keydown", (e) => {
171  if (e.key === "Escape" && focused) {
172    focused.classList.remove("focused");
173    cloud.classList.remove("has-focus");
174    focused = null;
175  }
176});
177
178// Mouse drag-to-pan. Touch/trackpad/wheel use native overflow scrolling.
179// Panning only begins once the pointer moves past a threshold, so a plain
180// click still reaches the photo (capturing the pointer up front would swallow
181// the click and break click-to-highlight).
182let wasDragging = false;
183let pointerDown = false;
184let panning = false;
185let startX = 0,
186  startY = 0,
187  startLeft = 0,
188  startTop = 0;
189
190cloud.addEventListener("pointerdown", (e) => {
191  if (e.pointerType !== "mouse" || e.button !== 0) {
192    return;
193  }
194  pointerDown = true;
195  panning = false;
196  wasDragging = false;
197  startX = e.clientX;
198  startY = e.clientY;
199  startLeft = cloud.scrollLeft;
200  startTop = cloud.scrollTop;
201});
202
203cloud.addEventListener("pointermove", (e) => {
204  if (!pointerDown) {
205    return;
206  }
207  const dx = e.clientX - startX;
208  const dy = e.clientY - startY;
209  if (!panning) {
210    if (Math.abs(dx) <= 5 && Math.abs(dy) <= 5) {
211      return;
212    }
213    panning = true;
214    wasDragging = true;
215    cloud.classList.add("is-panning");
216    cloud.setPointerCapture(e.pointerId);
217  }
218  cloud.scrollLeft = startLeft - dx;
219  cloud.scrollTop = startTop - dy;
220});
221
222function endPan(e) {
223  if (!pointerDown) {
224    return;
225  }
226  pointerDown = false;
227  if (panning) {
228    panning = false;
229    cloud.classList.remove("is-panning");
230    if (cloud.hasPointerCapture(e.pointerId)) {
231      cloud.releasePointerCapture(e.pointerId);
232    }
233    // Clear the drag guard after the click event has fired.
234    setTimeout(() => {
235      wasDragging = false;
236    }, 0);
237  }
238}
239
240cloud.addEventListener("pointerup", endPan);
241cloud.addEventListener("pointercancel", endPan);
242
243// Frame the photo cluster whenever the gallery becomes visible. The section is
244// display:none until its tab is opened, so we can't centre until it has size.
245new IntersectionObserver((entries) => {
246  for (const entry of entries) {
247    if (entry.isIntersecting && cloud.clientWidth > 0) {
248      centreCanvas();
249    }
250  }
251}).observe(cloud);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.