114 lines
3.8 KiB
JavaScript
114 lines
3.8 KiB
JavaScript
/* ───────── 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 100‑vh 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);
|
||
|
||
// Fade‑in 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 });
|
||
});
|
||
|