// 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 }); })();