Skip to content

Commit d181b12

Browse files
Merge pull request steam-bell-92#878 from C4aZy/fix/track-route
fix: sidebar overlap on timeline, project cards, track-route scroll r…
2 parents 546e498 + 6d2367e commit d181b12

3 files changed

Lines changed: 246 additions & 8 deletions

File tree

web-app/css/styles.css

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,21 +1875,23 @@ body.sidebar-active .hero-controls {
18751875
}
18761876

18771877
.timeline-svg-track {
1878-
stroke: var(--border);
1879-
stroke-width: 6px;
1878+
stroke: var(--text-tertiary);
1879+
stroke-width: 3px;
18801880
stroke-linecap: round;
1881-
stroke-dasharray: 0 16;
1882-
opacity: 0.8;
1881+
/* dot · gap: tiny dot, generous gap → sparse dotted look */
1882+
stroke-dasharray: 3 20;
1883+
opacity: 0.35;
18831884
transition: stroke var(--duration-base) ease;
18841885
}
18851886

18861887
.timeline-svg-fill {
18871888
stroke: var(--accent);
1888-
stroke-width: 6px;
1889+
stroke-width: 3px;
18891890
stroke-linecap: round;
1890-
stroke-dasharray: 0 16;
1891+
stroke-dasharray: 3 20;
1892+
/* dashoffset animated by JS; starts at full length (path not drawn) */
18911893
transition: stroke var(--duration-base) ease;
1892-
filter: drop-shadow(0 0 8px var(--accent));
1894+
filter: drop-shadow(0 0 6px var(--accent-glow));
18931895
}
18941896

18951897
/* Ensure timeline grid elements overlap properly */
@@ -6218,3 +6220,9 @@ html[data-theme="dark"] body {
62186220

62196221
transition: width 0.08s linear;
62206222
}
6223+
/* Smooth sidebar enter/exit on index page scroll */
6224+
.sidebar-dock {
6225+
transition:
6226+
transform var(--duration-slow, 500ms) cubic-bezier(0.16, 1, 0.3, 1),
6227+
opacity var(--duration-slow, 500ms) cubic-bezier(0.16, 1, 0.3, 1);
6228+
}

web-app/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<link href="css/styles.css" rel="stylesheet"/>
1919
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
2020
</head>
21-
<body>
21+
<body class="sidebar-active">
2222

2323
<!-- Scroll Progress Bar -->
2424
<div id="scrollProgressContainer" aria-label="Page scroll progress">
@@ -712,6 +712,8 @@ <h3>Legal</h3>
712712
<script src="js/main.js"></script>
713713
<script src="js/hero-canvas.js"></script>
714714
<script>lucide.createIcons();</script>
715+
<script>lucide.createIcons();</script>
716+
<script src="js/fix.js"></script>
715717

716718
<div class="search-kbd-hint" id="searchKbdHint" aria-hidden="true">
717719
Press <kbd>Ctrl</kbd><span>+</span><kbd>K</kbd> to focus search

web-app/js/fix.js

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// ── 1. Project cards: populate grid from <template> if still empty ─────
2+
(function () {
3+
const grid = document.getElementById('projectsGrid');
4+
if (!grid || grid.children.length > 0) return;
5+
const tmpl = document.getElementById('projectsTemplate');
6+
if (!tmpl) return;
7+
const tmplGrid = tmpl.content.querySelector('.projects-grid');
8+
if (tmplGrid) grid.innerHTML = tmplGrid.innerHTML;
9+
})();
10+
11+
// ── 2. Timeline items: reveal on scroll ───────────────────────────────
12+
(function () {
13+
const items = document.querySelectorAll('.timeline-item[data-reveal]');
14+
if (!items.length) return;
15+
const io = new IntersectionObserver(entries => {
16+
entries.forEach(e => {
17+
if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); }
18+
});
19+
}, { threshold: 0.15 });
20+
items.forEach(el => io.observe(el));
21+
})();
22+
23+
// ── 3. Track-Route badge: hidden until timeline scrolls into view ──────
24+
(function () {
25+
const badge = document.querySelector('.timeline-route-badge');
26+
const section = document.getElementById('timelineSection');
27+
if (!badge || !section) return;
28+
Object.assign(badge.style, {
29+
opacity: '0', transform: 'translateY(-16px)',
30+
transition: 'opacity .55s cubic-bezier(.22,1,.36,1), transform .55s cubic-bezier(.22,1,.36,1)'
31+
});
32+
const io = new IntersectionObserver(entries => {
33+
if (entries[0].isIntersecting) {
34+
badge.style.opacity = '1'; badge.style.transform = 'translateY(0)';
35+
io.disconnect();
36+
}
37+
}, { threshold: 0.05 });
38+
io.observe(section);
39+
})();
40+
41+
// ── 4. Hero content: guarantee visibility if animations stall ──────────
42+
(function () {
43+
const shell = document.querySelector('.hero-shell');
44+
if (!shell) return;
45+
setTimeout(() => {
46+
shell.querySelectorAll('.hero-logo-header,.hero-badge-row,.hero-title-wrapper,.hero-subtitle,.hero-cta-row,.hero-meta')
47+
.forEach(el => { el.style.opacity = '1'; el.style.transform = 'translateY(0)'; });
48+
}, 800);
49+
})();
50+
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
204+
(function () {
205+
const projectsSection = document.getElementById('projectsSection');
206+
207+
if (!projectsSection) {
208+
document.body.classList.add('sidebar-active');
209+
return;
210+
}
211+
212+
document.body.classList.remove('sidebar-active');
213+
214+
const THRESHOLD = 0.05;
215+
216+
const io = new IntersectionObserver(entries => {
217+
entries.forEach(entry => {
218+
if (entry.isIntersecting) {
219+
document.body.classList.add('sidebar-active');
220+
} else {
221+
const rect = projectsSection.getBoundingClientRect();
222+
if (rect.top > 0) document.body.classList.remove('sidebar-active');
223+
}
224+
});
225+
}, { threshold: THRESHOLD });
226+
227+
io.observe(projectsSection);
228+
})();

0 commit comments

Comments
 (0)