232 lines
6.8 KiB
JavaScript
232 lines
6.8 KiB
JavaScript
// Load manifest and populate galleries with intelligent masonry layout
|
|
(async () => {
|
|
const manifest = await fetch("manifest.json").then((r) => r.json());
|
|
const imageTemplate = document.getElementById("media-template");
|
|
const videoTemplate = document.getElementById("video-template");
|
|
|
|
let currentIsMobile = window.innerWidth < 768;
|
|
|
|
// Intersection Observer for scroll-triggered animations
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add("loaded");
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{
|
|
rootMargin: "0px 0px -16.67% 0px",
|
|
},
|
|
);
|
|
|
|
// Function to get image dimensions
|
|
const getImageDimensions = (src) => {
|
|
return new Promise((resolve) => {
|
|
const img = new Image();
|
|
img.onload = () => resolve({ width: img.width, height: img.height });
|
|
img.onerror = () => resolve({ width: 1, height: 1 }); // Default to square
|
|
img.src = src;
|
|
});
|
|
};
|
|
|
|
// Function to pack items into rows
|
|
const packIntoRows = (items, isMobile) => {
|
|
if (isMobile) {
|
|
// Mobile: each item gets its own row, full width, no sizing classes
|
|
return items.map((item) => [item]);
|
|
}
|
|
|
|
const rows = [];
|
|
let i = 0;
|
|
|
|
while (i < items.length) {
|
|
const current = items[i];
|
|
|
|
// Try to fill a row intelligently
|
|
if (current.isLandscape) {
|
|
// Check if next item is also landscape
|
|
if (i + 1 < items.length && items[i + 1].isLandscape) {
|
|
// Two landscapes
|
|
rows.push([
|
|
{ ...current, className: "landscape-half" },
|
|
{ ...items[i + 1], className: "landscape-half" },
|
|
]);
|
|
i += 2;
|
|
} else if (i + 1 < items.length && !items[i + 1].isLandscape) {
|
|
// One landscape + one portrait
|
|
rows.push([
|
|
{ ...current, className: "landscape-with-portrait" },
|
|
{ ...items[i + 1], className: "portrait-with-landscape" },
|
|
]);
|
|
i += 2;
|
|
} else {
|
|
// Single landscape - centered
|
|
rows.push([{ ...current, className: "single-landscape" }]);
|
|
i += 1;
|
|
}
|
|
} else {
|
|
// Portrait - try to group optimally
|
|
const portraits = [];
|
|
let j = i;
|
|
|
|
// Count how many portraits are available
|
|
while (j < items.length && !items[j].isLandscape) {
|
|
j++;
|
|
}
|
|
const portraitCount = j - i;
|
|
|
|
// Smart grouping to avoid single portraits
|
|
if (portraitCount >= 3) {
|
|
// Group 3 together
|
|
portraits.push({ ...items[i], className: "portrait-third" });
|
|
portraits.push({ ...items[i + 1], className: "portrait-third" });
|
|
portraits.push({ ...items[i + 2], className: "portrait-third" });
|
|
i += 3;
|
|
} else if (portraitCount === 2) {
|
|
// Group 2 together
|
|
portraits.push({ ...items[i], className: "portrait-double" });
|
|
portraits.push({ ...items[i + 1], className: "portrait-double" });
|
|
i += 2;
|
|
} else {
|
|
// Single portrait - centered with constrained width
|
|
portraits.push({ ...items[i], className: "single-portrait" });
|
|
i += 1;
|
|
}
|
|
|
|
rows.push(portraits);
|
|
}
|
|
}
|
|
|
|
return rows;
|
|
};
|
|
|
|
// Store processed items for each section
|
|
const sectionItems = new Map();
|
|
|
|
// Function to render a section
|
|
const renderSection = (section, items, isMobile) => {
|
|
const grid = section.querySelector(".media-grid");
|
|
|
|
// Clear existing content
|
|
grid.innerHTML = "";
|
|
|
|
// Pack items into rows
|
|
const rows = packIntoRows(items, isMobile);
|
|
|
|
// Render rows
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
rows.forEach((row) => {
|
|
const rowDiv = document.createElement("div");
|
|
rowDiv.className = "media-row";
|
|
|
|
row.forEach((item) => {
|
|
const template = item.isVideo ? videoTemplate : imageTemplate;
|
|
const clone = template.content.cloneNode(true);
|
|
|
|
if (item.isVideo) {
|
|
// Videos are not wrapped in links
|
|
const container = clone.querySelector(".media-item");
|
|
const video = clone.querySelector("video");
|
|
|
|
video.src = item.src;
|
|
|
|
// Try to use a poster image if it exists
|
|
const posterPath = item.src.replace(
|
|
/\.(mp4|mov|avi|webm|mkv)$/i,
|
|
".jpg",
|
|
);
|
|
video.poster = posterPath;
|
|
|
|
if (item.className) {
|
|
container.classList.add(item.className);
|
|
}
|
|
} else {
|
|
// Images wrapped in download links
|
|
const link = clone.querySelector("a");
|
|
const media = clone.querySelector("img");
|
|
|
|
link.href = item.src;
|
|
link.download = item.file.name;
|
|
media.src = item.src;
|
|
media.alt = `Foto - ${item.file.name}`;
|
|
|
|
if (item.className) {
|
|
link.classList.add(item.className);
|
|
}
|
|
}
|
|
|
|
rowDiv.appendChild(clone);
|
|
});
|
|
|
|
fragment.appendChild(rowDiv);
|
|
});
|
|
|
|
grid.appendChild(fragment);
|
|
|
|
// Observe all media items for scroll animation
|
|
grid
|
|
.querySelectorAll(".media-item")
|
|
.forEach((item) => observer.observe(item));
|
|
};
|
|
|
|
// Process and render all sections
|
|
for (const section of document.querySelectorAll("section[data-dir]")) {
|
|
const dir = section.dataset.dir;
|
|
const files = manifest[dir];
|
|
|
|
if (!files || files.length === 0) continue;
|
|
|
|
// Preload and classify all items
|
|
const items = await Promise.all(
|
|
files.map(async (file) => {
|
|
const isVideo = /\.(mp4|mov|avi|webm|mkv)$/i.test(file.name);
|
|
const src = `conteudo/${dir}/${file.name}`;
|
|
|
|
let isLandscape = true;
|
|
if (!isVideo) {
|
|
const dims = await getImageDimensions(src);
|
|
isLandscape = dims.width > dims.height;
|
|
}
|
|
|
|
return {
|
|
file,
|
|
isVideo,
|
|
isLandscape,
|
|
src,
|
|
dir,
|
|
};
|
|
}),
|
|
);
|
|
|
|
// Store items for this section
|
|
sectionItems.set(section, items);
|
|
|
|
// Initial render
|
|
renderSection(section, items, currentIsMobile);
|
|
}
|
|
|
|
// Handle window resize
|
|
let resizeTimeout;
|
|
window.addEventListener("resize", () => {
|
|
clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(() => {
|
|
const newIsMobile = window.innerWidth < 768;
|
|
|
|
// Only re-render if we crossed the breakpoint
|
|
if (newIsMobile !== currentIsMobile) {
|
|
currentIsMobile = newIsMobile;
|
|
|
|
// Re-render all sections
|
|
document.querySelectorAll("section[data-dir]").forEach((section) => {
|
|
const items = sectionItems.get(section);
|
|
if (items) {
|
|
renderSection(section, items, currentIsMobile);
|
|
}
|
|
});
|
|
}
|
|
}, 250); // Debounce resize events
|
|
});
|
|
})();
|