Skip to content

Commit c3a1a19

Browse files
Update fix.js
1 parent 8413431 commit c3a1a19

1 file changed

Lines changed: 154 additions & 6 deletions

File tree

web-app/js/fix.js

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,33 +48,181 @@
4848
}, 800);
4949
})();
5050

51-
// ── 5. Sidebar: hidden during hero+timeline, appears at projects section
51+
// ── 5. Timeline: one continuous S-snake dotted line, card border to card border ──
52+
(function () {
53+
function buildTimelinePath() {
54+
const container = document.querySelector('.timeline-container');
55+
const grid = document.querySelector('.timeline-grid');
56+
if (!container || !grid) return;
57+
58+
const old = container.querySelector('.timeline-svg');
59+
if (old) old.remove();
60+
61+
const items = Array.from(grid.querySelectorAll('.timeline-item'));
62+
if (items.length < 2) return;
63+
64+
const cRect = container.getBoundingClientRect();
65+
const svgW = cRect.width;
66+
const svgH = cRect.height;
67+
68+
// Collect the anchor point on the card border facing the centre for each item.
69+
// Odd items (i=0,2,4): card is on LEFT → anchor = right-centre border of card
70+
// Even items (i=1,3,5): card is on RIGHT → anchor = left-centre border of card
71+
const anchors = items.map((item, i) => {
72+
const cardEl = item.querySelector('.timeline-content');
73+
if (!cardEl) return null;
74+
const cr = cardEl.getBoundingClientRect();
75+
const isLeft = (i % 2 === 0);
76+
return {
77+
x: isLeft ? (cr.right - cRect.left) : (cr.left - cRect.left),
78+
y: cr.top - cRect.top + cr.height / 2
79+
};
80+
}).filter(Boolean);
81+
82+
if (anchors.length < 2) return;
83+
84+
// Build ONE continuous cubic Bézier path through all anchors.
85+
// The trick for a smooth S with no breaks:
86+
// At every interior anchor the tangent must be continuous.
87+
// We achieve this by using the same horizontal pull direction and
88+
// making control points symmetric around each anchor.
89+
//
90+
// For segment i → i+1:
91+
// a = anchors[i], b = anchors[i+1]
92+
// The path travels horizontally out of a (CP1 at same Y as a),
93+
// crosses over, then arrives horizontally into b (CP2 at same Y as b).
94+
// Pull distance = half the horizontal span between a and b.
95+
//
96+
// To keep the path CONTINUOUS and SMOOTH at every junction we use
97+
// a Catmull-Rom → cubic Bézier conversion approach:
98+
// each segment's control points are tangent-matched to neighbours.
99+
100+
// Compute tangent at each anchor using Catmull-Rom rule
101+
// (for the endpoints we mirror the first/last interior tangent)
102+
function catmullTangent(prev, curr, next) {
103+
return {
104+
tx: (next.x - prev.x) * 0.5,
105+
ty: (next.y - prev.y) * 0.5
106+
};
107+
}
108+
109+
const n = anchors.length;
110+
const tan = anchors.map((_, i) => {
111+
const prev = anchors[Math.max(i - 1, 0)];
112+
const next = anchors[Math.min(i + 1, n - 1)];
113+
return catmullTangent(prev, anchors[i], next);
114+
});
115+
116+
// Override tangents to be PURELY HORIZONTAL at each card border.
117+
// This ensures the line exits/enters every card border at 90° (clean T-junction).
118+
// We keep the sign (direction) from Catmull-Rom but zero out the Y component.
119+
const horizontalTan = anchors.map((a, i) => {
120+
const raw = tan[i];
121+
// Magnitude = half the horizontal gap to the nearest neighbour
122+
const prev = anchors[Math.max(i - 1, 0)];
123+
const next = anchors[Math.min(i + 1, n - 1)];
124+
const mag = Math.abs(next.x - prev.x) * 1;
125+
const sign = raw.tx >= 0 ? 1 : -1;
126+
return { tx: sign * mag, ty: 0 };
127+
});
128+
129+
let d = `M ${anchors[0].x.toFixed(2)} ${anchors[0].y.toFixed(2)}`;
130+
131+
for (let i = 0; i < n - 1; i++) {
132+
const a = anchors[i];
133+
const b = anchors[i + 1];
134+
const t0 = horizontalTan[i];
135+
const t1 = horizontalTan[i + 1];
136+
137+
// Cubic Bézier from Catmull-Rom:
138+
// CP1 = a + t0/3, CP2 = b - t1/3
139+
const cp1x = a.x + t0.tx;
140+
const cp1y = a.y + t0.ty;
141+
const cp2x = b.x - t1.tx;
142+
const cp2y = b.y - t1.ty;
143+
144+
d += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${b.x.toFixed(2)} ${b.y.toFixed(2)}`;
145+
}
146+
147+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
148+
svg.setAttribute('class', 'timeline-svg');
149+
svg.setAttribute('viewBox', `0 0 ${svgW} ${svgH}`);
150+
svg.setAttribute('aria-hidden', 'true');
151+
svg.style.cssText = `position:absolute;left:0;top:0;width:100%;height:${svgH}px;pointer-events:none;z-index:2;overflow:visible;`;
152+
153+
// Dim ghost track
154+
const track = document.createElementNS('http://www.w3.org/2000/svg', 'path');
155+
track.setAttribute('class', 'timeline-svg-track');
156+
track.setAttribute('d', d);
157+
track.setAttribute('fill', 'none');
158+
159+
// Glowing accent fill — animated draw-in
160+
const fill = document.createElementNS('http://www.w3.org/2000/svg', 'path');
161+
fill.setAttribute('class', 'timeline-svg-fill');
162+
fill.setAttribute('d', d);
163+
fill.setAttribute('fill', 'none');
164+
165+
svg.appendChild(track);
166+
svg.appendChild(fill);
167+
container.insertBefore(svg, container.firstChild);
168+
169+
// Scroll-triggered draw-in animation
170+
requestAnimationFrame(() => {
171+
const totalLen = fill.getTotalLength ? fill.getTotalLength() : 5000;
172+
fill.style.strokeDasharray = '5 16';
173+
fill.style.strokeDashoffset = String(totalLen);
174+
fill.style.transition = 'none';
175+
176+
const section = document.getElementById('timelineSection');
177+
const io2 = new IntersectionObserver(entries => {
178+
if (entries[0].isIntersecting) {
179+
requestAnimationFrame(() => {
180+
fill.style.transition = 'stroke-dashoffset 2.4s cubic-bezier(0.16,1,0.3,1)';
181+
fill.style.strokeDashoffset = '0';
182+
});
183+
io2.disconnect();
184+
}
185+
}, { threshold: 0.05 });
186+
if (section) io2.observe(section);
187+
});
188+
}
189+
190+
if (document.readyState === 'complete') {
191+
buildTimelinePath();
192+
} else {
193+
window.addEventListener('load', buildTimelinePath);
194+
}
195+
196+
let resizeTimer;
197+
window.addEventListener('resize', () => {
198+
clearTimeout(resizeTimer);
199+
resizeTimer = setTimeout(buildTimelinePath, 150);
200+
});
201+
})();
202+
203+
// ── 6. Sidebar: hidden during hero+timeline, appears at projects section
52204
(function () {
53205
const projectsSection = document.getElementById('projectsSection');
54206

55-
// Not the index page (sub-pages have sidebar-active on <body> already)
56207
if (!projectsSection) {
57208
document.body.classList.add('sidebar-active');
58209
return;
59210
}
60211

61-
// Index page: start with sidebar hidden
62212
document.body.classList.remove('sidebar-active');
63213

64214
const THRESHOLD = 0.05;
65215

66216
const io = new IntersectionObserver(entries => {
67217
entries.forEach(entry => {
68218
if (entry.isIntersecting) {
69-
// Projects section in view → show sidebar
70219
document.body.classList.add('sidebar-active');
71220
} else {
72-
// Scrolled back above projects → hide sidebar
73221
const rect = projectsSection.getBoundingClientRect();
74222
if (rect.top > 0) document.body.classList.remove('sidebar-active');
75223
}
76224
});
77225
}, { threshold: THRESHOLD });
78226

79227
io.observe(projectsSection);
80-
})();
228+
})();

0 commit comments

Comments
 (0)