641 lines
26 KiB
JavaScript
641 lines
26 KiB
JavaScript
gsap.registerPlugin(ScrollTrigger);
|
||
|
||
/* ─────────────────────────────────────────
|
||
UTILITIES
|
||
───────────────────────────────────────── */
|
||
|
||
/* ── Dark mode: invert near-black strokes/fills to light grey ──
|
||
Scans every SVG element after load. Elements whose computed stroke
|
||
or fill is "dark" (luminance < 0.25) get stored originals and a
|
||
light replacement. Runs again whenever Congo toggles .dark on <html>.
|
||
── */
|
||
const DARK_STROKE = '#d4d4d4';
|
||
const DARK_FILL = '#c8c8c8';
|
||
|
||
function luminance(r, g, b) {
|
||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||
}
|
||
function parseRgb(str) {
|
||
const m = str && str.match(/rgba?\((\d+)[, ]+(\d+)[, ]+(\d+)/);
|
||
return m ? [+m[1], +m[2], +m[3]] : null;
|
||
}
|
||
function adaptSvg(svgEl) {
|
||
const dark = document.documentElement.classList.contains('dark');
|
||
svgEl.querySelectorAll('path, line, rect, circle, ellipse, polyline, polygon').forEach(el => {
|
||
const cs = window.getComputedStyle(el);
|
||
// stroke
|
||
const sc = parseRgb(cs.stroke);
|
||
if (sc && luminance(...sc) < 0.25) {
|
||
if (!el.dataset.origStrokeColor) el.dataset.origStrokeColor = cs.stroke;
|
||
el.style.stroke = dark ? DARK_STROKE : el.dataset.origStrokeColor;
|
||
} else if (el.dataset.origStrokeColor && !dark) {
|
||
el.style.stroke = el.dataset.origStrokeColor;
|
||
}
|
||
// fill (non-transparent only)
|
||
if (cs.fill !== 'none' && cs.fill !== 'rgba(0, 0, 0, 0)') {
|
||
const fc = parseRgb(cs.fill);
|
||
if (fc && luminance(...fc) < 0.25) {
|
||
if (!el.dataset.origFillColor) el.dataset.origFillColor = cs.fill;
|
||
el.style.fill = dark ? DARK_FILL : el.dataset.origFillColor;
|
||
} else if (el.dataset.origFillColor && !dark) {
|
||
el.style.fill = el.dataset.origFillColor;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Registry of all loaded SVGs – re-adapt when dark mode toggles
|
||
const _allSvgEls = [];
|
||
new MutationObserver(() => _allSvgEls.forEach(svg => adaptSvg(svg)))
|
||
.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||
|
||
function registerAndAdapt(svgEl) {
|
||
_allSvgEls.push(svgEl);
|
||
adaptSvg(svgEl);
|
||
}
|
||
|
||
function prepareDraw(svg) {
|
||
const els = svg.querySelectorAll('path, line, rect, circle, polyline, polygon, ellipse');
|
||
els.forEach(el => {
|
||
let len; try { len = el.getTotalLength(); } catch(e) { len = 1000; }
|
||
el.style.strokeDasharray = len;
|
||
el.style.strokeDashoffset = len;
|
||
const fill = getComputedStyle(el).fill;
|
||
if (fill && fill !== 'none' && fill !== 'rgba(0, 0, 0, 0)') { el.style.fillOpacity = 0; el.dataset.hasFill = '1'; }
|
||
});
|
||
return els;
|
||
}
|
||
|
||
function prepareDrawSubset(els) {
|
||
els.forEach(el => {
|
||
let len; try { len = el.getTotalLength(); } catch(e) { len = 1000; }
|
||
el.style.strokeDasharray = len;
|
||
el.style.strokeDashoffset = len;
|
||
const fill = getComputedStyle(el).fill;
|
||
if (fill && fill !== 'none' && fill !== 'rgba(0, 0, 0, 0)') { el.style.fillOpacity = 0; el.dataset.hasFill = '1'; }
|
||
});
|
||
}
|
||
|
||
function svgToScreen(svgEl, vbW, vbH, box) {
|
||
const r = svgEl.getBoundingClientRect();
|
||
return {
|
||
left: r.left + box.x * (r.width / vbW),
|
||
top: r.top + box.y * (r.height / vbH),
|
||
width: box.w * (r.width / vbW),
|
||
height: box.h * (r.height / vbH)
|
||
};
|
||
}
|
||
|
||
/* ─────────────────────────────────────────
|
||
STATE
|
||
───────────────────────────────────────── */
|
||
let heroSvg = null;
|
||
let heroCirclesGroup = null;
|
||
let fig2GreenGroup = null;
|
||
let fig2OrangeGroup = null;
|
||
let f2TextEls = [];
|
||
|
||
let fig2Flyer = null;
|
||
let fig2FlyerSrc = null;
|
||
|
||
let arrowClipRect = null, arrowClipH = 0;
|
||
let fig4DrawEls = [], fig4DrawDone = false;
|
||
|
||
let _createArrowTrigger = null;
|
||
let _createFig5Trigger = null;
|
||
|
||
const F4_VB = { w: 864.15, h: 479.55 };
|
||
const F4_GREEN = { x: 1, y: 123, w: 54, h: 52 };
|
||
const F4_ORANGE = { x: 1, y: 187, w: 54, h: 52 };
|
||
const F4_RED = { x: 773, y: 122, w: 90, h: 104 };
|
||
const F4_F2AREA = { x: 1, y: 100, w: 340, h: 160 };
|
||
|
||
|
||
/* ═══════════════════════════════════════
|
||
1) HERO – Figure1 draw
|
||
═══════════════════════════════════════ */
|
||
gsap.set('#heroText', { opacity: 0, y: 40 });
|
||
|
||
fetch('/Figure1.svg').then(r => r.text()).then(svgText => {
|
||
const cnt = document.getElementById('heroFigure');
|
||
cnt.innerHTML = svgText;
|
||
heroSvg = cnt.querySelector('svg');
|
||
heroSvg.style.cssText = 'width:100%;height:auto;overflow:visible';
|
||
registerAndAdapt(heroSvg);
|
||
|
||
const labGroup = heroSvg.querySelector('[data-name="Ebene 2"]');
|
||
heroCirclesGroup = heroSvg.querySelector('[data-name="Ebene 3"]');
|
||
const labEls = labGroup ? labGroup.querySelectorAll('path, line, rect, circle') : [];
|
||
const circleEls = heroCirclesGroup ? heroCirclesGroup.querySelectorAll('path') : [];
|
||
|
||
[...labEls, ...circleEls].forEach(el => {
|
||
let len; try { len = el.getTotalLength(); } catch(e) { len = 1000; }
|
||
el.style.strokeDasharray = len; el.style.strokeDashoffset = len;
|
||
const fill = getComputedStyle(el).fill;
|
||
if (fill && fill !== 'none' && fill !== 'rgba(0, 0, 0, 0)') { el.style.fillOpacity = 0; el.dataset.hasFill = '1'; }
|
||
});
|
||
|
||
circleEls.forEach(el => {
|
||
try {
|
||
const bb = el.getBBox(), cx = bb.x + bb.width/2, cy = bb.y + bb.height/2;
|
||
if (cx > 850 && cy < 200) { el.dataset.flyAway = '1'; el.dataset.flyColor = 'green'; }
|
||
else if (cx > 690 && cx < 850 && cy > 100 && cy < 250) { el.dataset.flyAway = '1'; el.dataset.flyColor = 'orange'; }
|
||
} catch(e) {}
|
||
});
|
||
|
||
const tl = gsap.timeline({ delay: 0.3 });
|
||
tl.to('#heroText', { opacity: 1, y: 0, duration: 0.9, ease: 'power3.out' }, 0);
|
||
tl.to(labEls, { strokeDashoffset: 0, duration: 3, stagger: { amount: 2.5, from: 'start' }, ease: 'power1.inOut' }, 0.2);
|
||
tl.to(circleEls, { strokeDashoffset: 0, duration: 2, stagger: { amount: 1.0, from: 'start' }, ease: 'power1.inOut' }, 1.5);
|
||
const cf = Array.from(circleEls).filter(e => e.dataset.hasFill === '1');
|
||
if (cf.length) tl.to(cf, { fillOpacity: 1, duration: 0.8, stagger: 0.1, ease: 'power1.inOut' }, '-=0.5');
|
||
|
||
const onEarlyScroll = () => { if (tl.progress() < 1) tl.progress(1, false); window.removeEventListener('scroll', onEarlyScroll); };
|
||
window.addEventListener('scroll', onEarlyScroll, { passive: true });
|
||
|
||
tl.call(() => {
|
||
[...labEls, ...circleEls].forEach(el => { el.style.strokeDashoffset = '0'; });
|
||
gsap.to('.hero-figure', { y: -80, scrollTrigger: { trigger: '.hero', start: 'top top', end: 'bottom top', scrub: true } });
|
||
gsap.to('#heroText', { opacity: 0, y: -40, scrollTrigger: { trigger: '.hero', start: '30% top', end: '80% top', scrub: 1 } });
|
||
initCircleFadeIn();
|
||
});
|
||
});
|
||
|
||
|
||
/* ═══════════════════════════════════════
|
||
2) FLYING CIRCLES: Figure1 → Figure2
|
||
═══════════════════════════════════════ */
|
||
function initCircleFadeIn() {
|
||
const poll = () => {
|
||
const f2svg = document.querySelector('#fig2Wrap svg');
|
||
if (f2svg && heroSvg && heroCirclesGroup) {
|
||
fig2GreenGroup = f2svg.querySelector('[data-circle-group="green"]');
|
||
fig2OrangeGroup = f2svg.querySelector('[data-circle-group="orange"]');
|
||
buildFlyingCircles(f2svg);
|
||
} else requestAnimationFrame(poll);
|
||
};
|
||
poll();
|
||
}
|
||
|
||
function buildFlyingCircles(f2svg) {
|
||
const ns = 'http://www.w3.org/2000/svg';
|
||
|
||
const overlay = document.createElementNS(ns, 'svg');
|
||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:60;pointer-events:none;overflow:visible;display:none;';
|
||
document.body.appendChild(overlay);
|
||
const wrapG = document.createElementNS(ns, 'g');
|
||
const wrapO = document.createElementNS(ns, 'g');
|
||
overlay.appendChild(wrapG);
|
||
overlay.appendChild(wrapO);
|
||
|
||
const greenEls = heroSvg.querySelectorAll('[data-fly-color="green"]');
|
||
const orangeEls = heroSvg.querySelectorAll('[data-fly-color="orange"]');
|
||
|
||
function populateWrap(wrapGroup, els) {
|
||
wrapGroup.innerHTML = '';
|
||
els.forEach(el => {
|
||
let bb, sr;
|
||
try { bb = el.getBBox(); sr = el.getBoundingClientRect(); } catch(e) { return; }
|
||
if (!bb || !sr || bb.width === 0 || sr.width === 0) return;
|
||
const sx = sr.width / bb.width;
|
||
const sy = sr.height / bb.height;
|
||
const tx = sr.left - bb.x * sx;
|
||
const ty = sr.top - bb.y * sy;
|
||
const clone = el.cloneNode(true);
|
||
clone.style.fillOpacity = '1';
|
||
clone.style.opacity = '1';
|
||
clone.removeAttribute('data-fly-color');
|
||
clone.removeAttribute('data-fly-away');
|
||
const g = document.createElementNS(ns, 'g');
|
||
g.setAttribute('transform', `matrix(${sx},0,0,${sy},${tx},${ty})`);
|
||
g.appendChild(clone);
|
||
wrapGroup.appendChild(g);
|
||
});
|
||
}
|
||
|
||
function screenRect(els) {
|
||
let l=Infinity, t=Infinity, r=-Infinity, b=-Infinity;
|
||
els.forEach(el => {
|
||
try {
|
||
const rect = el.getBoundingClientRect();
|
||
l=Math.min(l,rect.left); t=Math.min(t,rect.top);
|
||
r=Math.max(r,rect.right); b=Math.max(b,rect.bottom);
|
||
} catch(e) {}
|
||
});
|
||
return { left:l, top:t, width:r-l, height:b-t };
|
||
}
|
||
|
||
const F2 = { w:500.49, h:653.39 };
|
||
const F2G = { x:1, y:222, w:96, h:93 };
|
||
const F2O = { x:1, y:338, w:96, h:94 };
|
||
const lerp = gsap.utils.interpolate;
|
||
let ep = 0;
|
||
|
||
function applyTransform(wrap, src, tgt) {
|
||
if (!src || !isFinite(src.left)) return;
|
||
const sx = lerp(1, tgt.width / src.width, ep);
|
||
const sy = lerp(1, tgt.height / src.height, ep);
|
||
const tx = lerp(0, tgt.left - src.left * (tgt.width / src.width), ep);
|
||
const ty = lerp(0, tgt.top - src.top * (tgt.height / src.height), ep);
|
||
wrap.setAttribute('transform', `translate(${tx},${ty}) scale(${sx},${sy})`);
|
||
}
|
||
|
||
ScrollTrigger.create({
|
||
trigger: '#middleWrapper', start: 'top 100%', end: 'top 30%', scrub: 0.4,
|
||
onUpdate: (self) => {
|
||
const p = self.progress;
|
||
const flyEls = heroSvg ? heroSvg.querySelectorAll('[data-fly-away="1"]') : [];
|
||
if (p <= 0) {
|
||
overlay.style.display = 'none';
|
||
flyEls.forEach(el => el.style.opacity = '1');
|
||
if (fig2GreenGroup) fig2GreenGroup.style.opacity = 0;
|
||
if (fig2OrangeGroup) fig2OrangeGroup.style.opacity = 0;
|
||
return;
|
||
}
|
||
const srcG = screenRect(greenEls);
|
||
const srcO = screenRect(orangeEls);
|
||
populateWrap(wrapG, greenEls);
|
||
populateWrap(wrapO, orangeEls);
|
||
overlay.style.display = 'block';
|
||
flyEls.forEach(el => el.style.opacity = '0');
|
||
const tgtG = svgToScreen(f2svg, F2.w, F2.h, F2G);
|
||
const tgtO = svgToScreen(f2svg, F2.w, F2.h, F2O);
|
||
ep = gsap.parseEase('power2.inOut')(p);
|
||
applyTransform(wrapG, srcG, tgtG);
|
||
applyTransform(wrapO, srcO, tgtO);
|
||
const flyOp = p < 0.08 ? p/0.08 : p > 0.80 ? (1-p)/0.20 : 1;
|
||
overlay.style.opacity = flyOp;
|
||
const f2o = p > 0.88 ? Math.min(1,(p-0.88)/0.12) : 0;
|
||
if (fig2GreenGroup) fig2GreenGroup.style.opacity = f2o;
|
||
if (fig2OrangeGroup) fig2OrangeGroup.style.opacity = f2o;
|
||
if (heroCirclesGroup) heroCirclesGroup.style.opacity = 1 - p * 0.7;
|
||
},
|
||
onLeaveBack: () => {
|
||
overlay.style.display = 'none';
|
||
wrapG.innerHTML = ''; wrapO.innerHTML = '';
|
||
if (heroCirclesGroup) heroCirclesGroup.style.opacity = 1;
|
||
heroSvg?.querySelectorAll('[data-fly-away="1"]').forEach(el => el.style.opacity = '1');
|
||
if (fig2GreenGroup) fig2GreenGroup.style.opacity = 0;
|
||
if (fig2OrangeGroup) fig2OrangeGroup.style.opacity = 0;
|
||
},
|
||
onLeave: () => {
|
||
overlay.style.display = 'none';
|
||
wrapG.innerHTML = ''; wrapO.innerHTML = '';
|
||
if (fig2GreenGroup) fig2GreenGroup.style.opacity = 1;
|
||
if (fig2OrangeGroup) fig2OrangeGroup.style.opacity = 1;
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
/* ═══════════════════════════════════════
|
||
3) FIGURE 2 – load, draw on scroll
|
||
═══════════════════════════════════════ */
|
||
fetch('/Figure2.svg').then(r => r.text()).then(svgText => {
|
||
const wrap = document.getElementById('fig2Wrap');
|
||
wrap.innerHTML = svgText.replace(/cls-/g, 'f2-');
|
||
const svg = wrap.querySelector('svg');
|
||
svg.style.width = '100%'; svg.style.height = 'auto';
|
||
registerAndAdapt(svg);
|
||
|
||
svg.querySelectorAll(':scope > g > g > g').forEach(g => {
|
||
g.querySelectorAll('path').forEach(p => {
|
||
const cls = p.getAttribute('class') || '';
|
||
if (cls.includes('f2-4')||cls.includes('f2-5')) { g.style.opacity='0'; g.dataset.circleGroup='green'; }
|
||
if (cls.includes('f2-7')||cls.includes('f2-6')) { g.style.opacity='0'; g.dataset.circleGroup='orange'; }
|
||
});
|
||
});
|
||
|
||
f2TextEls = Array.from(svg.querySelectorAll('text'));
|
||
|
||
const els = prepareDraw(svg);
|
||
ScrollTrigger.create({
|
||
trigger: '#rdocPanel', start: 'top 60%', once: true,
|
||
onEnter: () => {
|
||
const tl = gsap.timeline();
|
||
tl.to(els, { strokeDashoffset: 0, duration: 1.2, stagger: { amount: 0.6 }, ease: 'power1.inOut' }, 0);
|
||
const fills = Array.from(els).filter(e => e.dataset.hasFill==='1');
|
||
if (fills.length) tl.to(fills, { fillOpacity: 1, duration: 0.6, stagger: { amount: 0.2 }, ease: 'power1.inOut' }, '-=0.4');
|
||
}
|
||
});
|
||
_svgReady();
|
||
});
|
||
|
||
|
||
/* ═══════════════════════════════════════
|
||
4) FIGURE 4 – load into fig4Wrap
|
||
═══════════════════════════════════════ */
|
||
|
||
function buildFig2Flyer() {
|
||
if (fig2Flyer) return;
|
||
const f2svg = document.querySelector('#fig2Wrap svg');
|
||
if (!f2svg) return;
|
||
|
||
const div = document.createElement('div');
|
||
div.id = 'fig2Flyer';
|
||
div.style.cssText = 'position:fixed;z-index:55;pointer-events:none;opacity:0;display:none;will-change:transform;overflow:visible;';
|
||
|
||
const ns = 'http://www.w3.org/2000/svg';
|
||
const s = document.createElementNS(ns, 'svg');
|
||
const vb = f2svg.viewBox.baseVal;
|
||
s.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.width} ${vb.height}`);
|
||
s.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||
s.style.cssText = 'width:100%;height:100%;display:block;overflow:visible;';
|
||
|
||
Array.from(f2svg.children).forEach(child => {
|
||
const clone = child.cloneNode(true);
|
||
clone.querySelectorAll('text, tspan').forEach(t => t.remove());
|
||
clone.querySelectorAll('*').forEach(el => {
|
||
el.style.strokeDasharray = 'none';
|
||
el.style.strokeDashoffset = '0';
|
||
el.style.fillOpacity = '1';
|
||
el.style.opacity = '1';
|
||
});
|
||
s.appendChild(clone);
|
||
});
|
||
|
||
div.appendChild(s);
|
||
document.body.appendChild(div);
|
||
fig2Flyer = div;
|
||
}
|
||
|
||
fetch('/Figure4.svg').then(r => r.text()).then(svgText => {
|
||
const wrap = document.getElementById('fig4Wrap');
|
||
wrap.innerHTML = svgText.replace(/cls-/g, 'f4-');
|
||
const svg = wrap.querySelector('svg');
|
||
svg.style.cssText = 'width:100%;height:auto;overflow:visible';
|
||
registerAndAdapt(svg);
|
||
|
||
const arrowEls = Array.from(svg.querySelectorAll('path')).filter(p => {
|
||
const cls=p.getAttribute('class')||'', d=p.getAttribute('d')||'';
|
||
return cls.includes('f4-13') && d.startsWith('m602');
|
||
});
|
||
if (arrowEls.length) {
|
||
let x0=Infinity,y0=Infinity,x1=-Infinity,y1=-Infinity;
|
||
arrowEls.forEach(el => { try { const b=el.getBBox(); x0=Math.min(x0,b.x); y0=Math.min(y0,b.y); x1=Math.max(x1,b.x+b.width); y1=Math.max(y1,b.y+b.height); } catch(e){} });
|
||
const pad=4, cW=x1-x0+pad*2;
|
||
arrowClipH = y1-y0+pad*2;
|
||
const ns = 'http://www.w3.org/2000/svg';
|
||
const defs = svg.querySelector('defs') || svg.insertBefore(document.createElementNS(ns,'defs'), svg.firstChild);
|
||
const clip = document.createElementNS(ns,'clipPath'); clip.setAttribute('id','f4ArrowClip');
|
||
arrowClipRect = document.createElementNS(ns,'rect');
|
||
arrowClipRect.setAttribute('x',x0-pad); arrowClipRect.setAttribute('y',y0-pad);
|
||
arrowClipRect.setAttribute('width',cW); arrowClipRect.setAttribute('height',0);
|
||
clip.appendChild(arrowClipRect); defs.appendChild(clip);
|
||
const ag = document.createElementNS(ns,'g'); ag.setAttribute('clip-path','url(#f4ArrowClip)');
|
||
arrowEls[0].parentNode.insertBefore(ag, arrowEls[0]);
|
||
arrowEls.forEach(el => ag.appendChild(el));
|
||
}
|
||
|
||
const arrowSet = new Set(arrowEls);
|
||
fig4DrawEls = Array.from(svg.querySelectorAll('rect, circle, ellipse, path, line'))
|
||
.filter(el => !arrowSet.has(el));
|
||
prepareDrawSubset(fig4DrawEls);
|
||
|
||
const lerp = gsap.utils.interpolate;
|
||
|
||
ScrollTrigger.create({
|
||
trigger: '#multiPanel',
|
||
start: 'center 80%',
|
||
end: 'center 20%',
|
||
scrub: 0.6,
|
||
invalidateOnRefresh: true,
|
||
onUpdate: (self) => {
|
||
const p = self.progress;
|
||
const f2wrap = document.getElementById('fig2Wrap');
|
||
const f4svg = svg;
|
||
|
||
buildFig2Flyer();
|
||
|
||
if (!fig2FlyerSrc && fig2Flyer) {
|
||
const r = f2wrap.getBoundingClientRect();
|
||
fig2FlyerSrc = {
|
||
left: r.left, top: r.top,
|
||
width: r.width, height: r.height,
|
||
aspect: r.height / r.width
|
||
};
|
||
}
|
||
|
||
f2TextEls.forEach(el => el.style.opacity = '0');
|
||
f2wrap.style.opacity = 0;
|
||
|
||
if (fig2Flyer && fig2FlyerSrc) {
|
||
const src = fig2FlyerSrc;
|
||
const tgtArea = svgToScreen(f4svg, F4_VB.w, F4_VB.h, F4_F2AREA);
|
||
const tgtW = tgtArea.width;
|
||
const tgtH = tgtW * src.aspect;
|
||
const tgtL = tgtArea.left;
|
||
const tgtT = tgtArea.top + (tgtArea.height - tgtH) / 2;
|
||
const ep = gsap.parseEase('power2.inOut')(p);
|
||
const flyOp = p > 0.92 ? Math.max(0,(1-p)/0.08) : 1;
|
||
|
||
fig2Flyer.style.display = 'block';
|
||
fig2Flyer.style.opacity = flyOp;
|
||
fig2Flyer.style.left = lerp(src.left, tgtL, ep) + 'px';
|
||
fig2Flyer.style.top = lerp(src.top, tgtT, ep) + 'px';
|
||
fig2Flyer.style.width = lerp(src.width, tgtW, ep) + 'px';
|
||
fig2Flyer.style.height = lerp(src.height, tgtH, ep) + 'px';
|
||
}
|
||
|
||
wrap.style.opacity = p < 0.50 ? 0 : Math.min(1, (p - 0.50) / 0.40);
|
||
|
||
if (p > 0.52 && !fig4DrawDone) {
|
||
fig4DrawDone = true;
|
||
const tl = gsap.timeline();
|
||
tl.to(fig4DrawEls, { strokeDashoffset: 0, duration: 1.4, stagger: { amount: 0.8 }, ease: 'power1.inOut' }, 0);
|
||
const fills = fig4DrawEls.filter(e => e.dataset.hasFill==='1');
|
||
if (fills.length) tl.to(fills, { fillOpacity: 1, duration: 0.7, stagger: { amount: 0.3 }, ease: 'power1.inOut' }, '-=0.5');
|
||
}
|
||
},
|
||
onLeaveBack: () => {
|
||
const f2wrap = document.getElementById('fig2Wrap');
|
||
f2wrap.style.opacity = 1;
|
||
f2TextEls.forEach(el => el.style.opacity = '1');
|
||
if (fig2Flyer) { fig2Flyer.style.display = 'none'; fig2Flyer.style.opacity = 0; }
|
||
fig2FlyerSrc = null;
|
||
wrap.style.opacity = 0;
|
||
fig4DrawDone = false;
|
||
fig4DrawEls.forEach(el => {
|
||
let len; try { len = el.getTotalLength(); } catch(e) { len = 1000; }
|
||
el.style.strokeDasharray = len; el.style.strokeDashoffset = len;
|
||
if (el.dataset.hasFill==='1') el.style.fillOpacity = 0;
|
||
});
|
||
}
|
||
});
|
||
|
||
_createArrowTrigger = () => {
|
||
ScrollTrigger.create({
|
||
trigger: '#adolPanel',
|
||
start: 'center 65%',
|
||
end: 'center 50%',
|
||
scrub: 0.6,
|
||
onUpdate: (self) => {
|
||
if (arrowClipRect) arrowClipRect.setAttribute('height', self.progress * arrowClipH);
|
||
},
|
||
onLeaveBack: () => {
|
||
if (arrowClipRect) arrowClipRect.setAttribute('height', 0);
|
||
}
|
||
});
|
||
};
|
||
_svgReady();
|
||
});
|
||
|
||
|
||
/* ═══════════════════════════════════════
|
||
5) SCROLL PROGRESS BAR
|
||
═══════════════════════════════════════ */
|
||
window.addEventListener('scroll', () => {
|
||
const p = document.documentElement.scrollTop / (document.documentElement.scrollHeight - window.innerHeight) * 100;
|
||
document.getElementById('scrollProgress').style.width = p + '%';
|
||
}, { passive: true });
|
||
|
||
|
||
/* ═══════════════════════════════════════
|
||
6) FIGURE 5 – Venn, circles fly from Figure4
|
||
═══════════════════════════════════════ */
|
||
fetch('/Figure5.svg').then(r => r.text()).then(svgText => {
|
||
const cnt = document.getElementById('vennFigure');
|
||
cnt.innerHTML = svgText.replace(/cls-/g, 'f5-');
|
||
const svg = cnt.querySelector('svg');
|
||
svg.style.width = '100%'; svg.style.height = 'auto';
|
||
registerAndAdapt(svg);
|
||
|
||
const greyCircles = svg.querySelectorAll('.f5-1');
|
||
const allGroups = svg.querySelectorAll('#Ebene_3 > g > g');
|
||
const f5O = allGroups[0]||null, f5R = allGroups[1]||null, f5G = allGroups[2]||null;
|
||
|
||
greyCircles.forEach(c => c.style.opacity = 0);
|
||
[f5O,f5R,f5G].filter(Boolean).forEach(g => g.style.opacity = 0);
|
||
|
||
const F5 = { w:432.94, h:462.7 };
|
||
const F5G = { x:206, y:3, w:225, h:222 };
|
||
const F5O = { x:2, y:123, w:226, h:222 };
|
||
const F5R = { x:206, y:237, w:225, h:224 };
|
||
|
||
function emptyFlyer(id) {
|
||
const d=document.createElement('div'); d.id=id;
|
||
d.style.cssText='position:fixed;z-index:60;pointer-events:none;opacity:0;display:none;will-change:transform;';
|
||
document.body.appendChild(d); return d;
|
||
}
|
||
|
||
function buildF4Flyer(div, f4svg, srcBox) {
|
||
if (div.querySelector('svg')) return;
|
||
const ns='http://www.w3.org/2000/svg', s=document.createElementNS(ns,'svg');
|
||
s.setAttribute('viewBox',`${srcBox.x} ${srcBox.y} ${srcBox.w} ${srcBox.h}`);
|
||
s.style.cssText='width:100%;height:100%;overflow:visible';
|
||
f4svg.querySelectorAll('path, circle, ellipse, rect').forEach(el => {
|
||
try {
|
||
const bb=el.getBBox(), cx=bb.x+bb.width/2, cy=bb.y+bb.height/2;
|
||
if (cx>=srcBox.x-2&&cx<=srcBox.x+srcBox.w+2&&cy>=srcBox.y-2&&cy<=srcBox.y+srcBox.h+2) {
|
||
const cs=parseFloat(window.getComputedStyle(el).getPropertyValue('stroke-width'))||2.5;
|
||
const c=el.cloneNode(true); c.dataset.origStroke=cs;
|
||
c.style.setProperty('stroke-width',cs.toString());
|
||
c.style.setProperty('stroke-dasharray','none');
|
||
c.style.setProperty('stroke-dashoffset','0');
|
||
c.style.setProperty('fill-opacity','1');
|
||
c.style.setProperty('opacity','1');
|
||
s.appendChild(c);
|
||
}
|
||
} catch(e) {}
|
||
});
|
||
div.appendChild(s);
|
||
}
|
||
|
||
const fG=emptyFlyer('fly5G'), fO=emptyFlyer('fly5O'), fR=emptyFlyer('fly5R');
|
||
let f4svg=null, f4SP=null, f5SI=false;
|
||
const lerp=gsap.utils.interpolate;
|
||
|
||
_createFig5Trigger = () => {
|
||
ScrollTrigger.create({
|
||
trigger: '#researchAreas', start: 'top 80%', end: 'bottom 80%', scrub: 0.4,
|
||
onUpdate: (self) => {
|
||
const p=self.progress;
|
||
if (!f4svg) f4svg=document.querySelector('#fig4Wrap svg');
|
||
if (p<=0||!f4svg) { [fG,fO,fR].forEach(f=>f.style.display='none'); f4SP=null; return; }
|
||
buildF4Flyer(fG,f4svg,F4_GREEN); buildF4Flyer(fO,f4svg,F4_ORANGE); buildF4Flyer(fR,f4svg,F4_RED);
|
||
if (!f4SP) {
|
||
f4SP={ G:svgToScreen(f4svg,F4_VB.w,F4_VB.h,F4_GREEN),
|
||
O:svgToScreen(f4svg,F4_VB.w,F4_VB.h,F4_ORANGE),
|
||
R:svgToScreen(f4svg,F4_VB.w,F4_VB.h,F4_RED) };
|
||
}
|
||
[fG,fO,fR].forEach(f=>f.style.display='block');
|
||
const ep=gsap.parseEase('power2.inOut')(p);
|
||
const f4Scale=f4svg.getBoundingClientRect().width/F4_VB.w;
|
||
|
||
function pos(div,srcBox,f5Box,start) {
|
||
const end=svgToScreen(svg,F5.w,F5.h,f5Box);
|
||
const cW=lerp(start.width,end.width,ep);
|
||
div.style.left=lerp(start.left,end.left,ep)+'px'; div.style.top=lerp(start.top,end.top,ep)+'px';
|
||
div.style.width=cW+'px'; div.style.height=lerp(start.height,end.height,ep)+'px';
|
||
const cs=cW/srcBox.w;
|
||
if (cs>0) { const sf=f4Scale/cs; div.querySelectorAll('path,circle,ellipse,rect').forEach(el=>{ const os=parseFloat(el.dataset.origStroke)||2.5; el.style.setProperty('stroke-width',(os*sf).toString()); }); }
|
||
}
|
||
pos(fG,F4_GREEN, F5G,f4SP.G);
|
||
pos(fO,F4_ORANGE,F5O,f4SP.O);
|
||
pos(fR,F4_RED, F5R,f4SP.R);
|
||
|
||
const fOp=p<0.08?p/0.08:p>0.80?(1-p)/0.20:1;
|
||
fG.style.opacity=fO.style.opacity=fR.style.opacity=fOp;
|
||
|
||
if (!f5SI) {
|
||
f5SI=true;
|
||
const f5Sc=svg.getBoundingClientRect().width/F5.w;
|
||
const se=fG.querySelector('path,circle'); const os=se?(parseFloat(se.dataset.origStroke)||2.5):2.5;
|
||
const ts=(os*f4Scale)/f5Sc*2.0;
|
||
[f5G,f5O,f5R].filter(Boolean).forEach(g=>g.querySelectorAll('path,circle,ellipse').forEach(el=>el.style.setProperty('stroke-width',ts.toString())));
|
||
}
|
||
|
||
const f5o=p>0.75?Math.min(1,(p-0.75)/0.15):0;
|
||
[f5G,f5O,f5R].filter(Boolean).forEach(g=>g.style.opacity=f5o);
|
||
greyCircles.forEach(c=>c.style.opacity=f5o);
|
||
|
||
['vennLabelA','vennLabelB','vennLabelC','vennLabelZ'].forEach((id,i)=>{
|
||
const delay=0.50+i*0.05, lp=Math.max(0,Math.min((p-delay)/0.15,1));
|
||
const el=document.getElementById(id); el.style.opacity=lp; el.style.transform=`translateY(${(1-lp)*20}px)`;
|
||
});
|
||
const cs=document.getElementById('vennConnectors');
|
||
if (cs) cs.style.opacity=Math.max(0,Math.min((p-0.50)/0.15,1));
|
||
const fp=Math.max(0,Math.min((p-0.70)/0.15,1));
|
||
const vf=document.getElementById('vennFooter'); vf.style.opacity=fp; vf.style.transform=`translateY(${(1-fp)*15}px)`;
|
||
},
|
||
onLeaveBack: () => {
|
||
f4SP=null; f5SI=false;
|
||
[fG,fO,fR].forEach(f=>{f.style.display='none';f.style.opacity=0;});
|
||
[f5G,f5O,f5R].filter(Boolean).forEach(g=>g.style.opacity=0);
|
||
greyCircles.forEach(c=>c.style.opacity=0);
|
||
['vennLabelA','vennLabelB','vennLabelC','vennLabelZ'].forEach(id=>{ const el=document.getElementById(id); el.style.opacity=0; el.style.transform='translateY(20px)'; });
|
||
const cs=document.getElementById('vennConnectors'); if(cs)cs.style.opacity=0;
|
||
document.getElementById('vennFooter').style.opacity=0;
|
||
},
|
||
onLeave: () => {
|
||
[fG,fO,fR].forEach(f=>{f.style.display='none';f.style.opacity=0;});
|
||
[f5G,f5O,f5R].filter(Boolean).forEach(g=>g.style.opacity=1);
|
||
greyCircles.forEach(c=>c.style.opacity=1);
|
||
}
|
||
});
|
||
};
|
||
_svgReady();
|
||
});
|
||
|
||
|
||
/* ═══════════════════════════════════════
|
||
7) PANEL SNAP / DWELL
|
||
═══════════════════════════════════════ */
|
||
let _svgCount = 0;
|
||
function _svgReady() {
|
||
if (++_svgCount < 3) return;
|
||
const DWELL = Math.round(window.innerHeight * 0.20);
|
||
gsap.utils.toArray('.text-panel').forEach(panel => {
|
||
ScrollTrigger.create({
|
||
trigger: panel,
|
||
start: 'center center',
|
||
end: `+=${DWELL}`,
|
||
pin: true,
|
||
pinSpacing: true,
|
||
anticipatePin: 1,
|
||
});
|
||
});
|
||
ScrollTrigger.refresh();
|
||
_createArrowTrigger?.();
|
||
_createFig5Trigger?.();
|
||
} |