2025-10-02 16:01:12 -06:00

114 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ───────── js/main.js ─────────
Dynamically builds the project grid + injects about/contact/crew copy.
-----------------------------------------------------------------*/
// Page loader overlay
(function(){
const overlay=document.createElement('div');
overlay.id='loader-overlay';
overlay.innerHTML='<div class="spinner"></div>';
document.addEventListener('DOMContentLoaded',()=>document.body.appendChild(overlay));
window.addEventListener('load',()=>{
overlay.classList.add('hide');
setTimeout(()=>overlay.remove(),700);
});
})();
// Mobile 100vh fix
function setVh(){document.documentElement.style.setProperty('--vh',window.innerHeight*0.01);}setVh();window.addEventListener('resize',setVh);
// Header height fix
function setHeaderOffset(){
const headerEl = document.querySelector('header.nav');
if(!headerEl) return; // Header not in DOM yet abort and wait
const h = headerEl.offsetHeight;
document.documentElement.style.setProperty('--header-h', `${h}px`);
}
setHeaderOffset();
// Run once more after a short delay to catch late-injected nav fragments
setTimeout(setHeaderOffset, 500);
window.addEventListener('resize', setHeaderOffset);
// Fadein intersection observer
const reveal = new IntersectionObserver((entries,o)=>{
entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('loaded');o.unobserve(e.target);}});
},{threshold:.3});
// Grab the grid once so every scope has it
const grid = document.getElementById('projects');
// Only run project-grid logic on pages that actually have the grid element
if(grid){
// Build card element
function card({id,title,thumbnail,size='small'}){
const a = document.createElement('a');
a.href = `project.html?id=${encodeURIComponent(id)}`;
a.className = 'item';
a.dataset.span = size; // <— MAGIC LINE
a.innerHTML = `
<h2>${title}</h2>
<div class="thumb" style="background-image:url('${thumbnail}')"></div>`;
reveal.observe(a);
return a;
}
// Load manifest
fetch('projects/projects.json')
.then(r=>{if(!r.ok)throw new Error('manifest missing');return r.json();})
.then(list=>{
if(!Array.isArray(list))throw new Error('manifest must be an array');
list.forEach(p=>grid.appendChild(card(p)));
})
.catch(err=>{
console.error('Manifest issue:',err.message);
grid.innerHTML=`<p style="grid-column:1/-1">No projects manifest found.</p>`;
});
}
// Inject About & Contact copy
['about','contact'].forEach(key=>{
const el=document.getElementById(key);
if(!el)return;
fetch(`written/${key}_us.txt`)
.then(r=>r.ok?r.text():Promise.reject())
.then(txt=>{
el.innerHTML=`<h2 style="text-transform:uppercase;margin-bottom:1em">${key}</h2><p>${txt.replace(/\n/g,'<br>')}</p>`;
})
.catch(()=>{});
});
// Inject Crew copy
['crew'].forEach(key=>{
const el=document.getElementById(key);
if(!el)return;
fetch(`written/${key}.txt`)
.then(r=>r.ok?r.text():Promise.reject())
.then(txt=>{
el.innerHTML=`<p>${txt.replace(/\n/g,'<br>')}</p>`;
})
.catch(()=>{});
});
// ───────── SIMPLE PARALLAX HANDLER ─────────
// Applies translateY based on scroll position to any element with a data-speed attribute.
document.addEventListener('DOMContentLoaded', () => {
const layers = Array.from(document.querySelectorAll('[data-speed]'));
if(!layers.length) return;
const update = () => {
const scrollY = window.scrollY;
layers.forEach(el => {
const speed = parseFloat(el.dataset.speed) || 0;
// Negative so layers move opposite to scroll for depth illusion
el.style.transform = `translateY(${ -scrollY * speed }px)`;
});
};
update();
window.addEventListener('scroll', update, { passive: true });
window.addEventListener('resize', update, { passive: true });
});