-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
231 lines (195 loc) · 7.36 KB
/
script.js
File metadata and controls
231 lines (195 loc) · 7.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Detect system theme preference and set initial theme
function initTheme() {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const html = document.documentElement;
if (prefersDark) {
html.classList.add("dark");
} else {
html.classList.remove("dark");
}
updateLogoSrc();
}
// Initialize theme on page load
initTheme();
// Listen for theme preference changes
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
const html = document.documentElement;
if (e.matches) {
html.classList.add("dark");
} else {
html.classList.remove("dark");
}
updateLogoSrc();
});
// Update logo src based on theme
function updateLogoSrc() {
const logoImage = document.querySelector("header .logo .logo-image");
if (!logoImage) return;
const isDark = document.documentElement.classList.contains("dark");
const darkSrc = logoImage.getAttribute("data-dark");
const lightSrc = logoImage.getAttribute("data-light");
logoImage.src = isDark ? darkSrc : lightSrc;
const demoLogoImage = document.querySelector(
"section.hero#top .visual .demo-image",
);
if (!demoLogoImage) return;
const isDemoDark = document.documentElement.classList.contains("dark");
const demoDarkSrc = demoLogoImage.getAttribute("data-dark");
const demoLightSrc = demoLogoImage.getAttribute("data-light");
demoLogoImage.src = isDemoDark ? demoDarkSrc : lightSrc;
}
// Build spacer height to match content
const content = document.querySelector(".smooth-content");
const spacer = document.getElementById("spacer");
function updateSpacer() {
// total height equals content height from smooth-content
const contentHeight = content.offsetHeight;
spacer.style.height = contentHeight + "px";
}
window.addEventListener("resize", updateSpacer);
updateSpacer();
// Smooth/slow scroll: interpolate transform towards window.scrollY
const smoothEl = document.querySelector(".smooth-content");
let current = 0;
let target = 0;
const ease = 0.06; // lower -> slower, tighter lag
// Track parallax offset for visual elements (cards move faster)
let parallaxCurrent = 0;
const parallaxEase = 0.08; // slightly higher ease = faster movement
function raf() {
target = window.scrollY || window.pageYOffset;
current += (target - current) * ease;
parallaxCurrent += (target - parallaxCurrent) * parallaxEase;
// round to avoid subpixel jitter affecting text clarity
const rounded = Math.round(current * 100) / 100;
smoothEl.style.transform = `translateY(${-rounded}px)`;
// Apply parallax offset to visual elements
const visualEls = document.querySelectorAll(".visual");
visualEls.forEach((el) => {
const parallaxOffset = parallaxCurrent - current;
el.style.transform = `translateY(${parallaxOffset * 0.15}px)`;
});
// Apply parallax depth to floating symbols
const floatingSymbols = document.querySelectorAll(".floating-symbol");
floatingSymbols.forEach((symbol) => {
const depth = parseFloat(symbol.getAttribute("data-depth")) || 0.5;
const parallaxOffset = parallaxCurrent - current;
symbol.style.transform = `translateY(${parallaxOffset * depth}px)`;
});
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
// Reveal elements when near viewport (works with transformed container)
const revealEls = document.querySelectorAll(".reveal");
function checkReveal() {
const viewportTop = current; // use smoothed current for subtle timing
const vh = window.innerHeight;
revealEls.forEach((el) => {
const rect = el.getBoundingClientRect();
const top = rect.top + current; // element's top relative to document
const threshold = vh * 0.7; // reveal when 70% into view
if (top < viewportTop + threshold) {
el.classList.add("in-view");
}
});
}
// run check periodically alongside raf
function rafReveal() {
checkReveal();
requestAnimationFrame(rafReveal);
}
requestAnimationFrame(rafReveal);
// Update scroll indicator based on scroll position and remaining content
function updateScrollIndicator() {
const scrollIndicator = document.getElementById("scrollIndicator");
const scrollY = window.scrollY || window.pageYOffset;
const docHeight = document.documentElement.scrollHeight;
const windowHeight = window.innerHeight;
const maxScroll = docHeight - windowHeight;
const scrollPercentage = maxScroll > 0 ? (scrollY / maxScroll) * 100 : 0;
// Only show scroll down at the start, hide once user reaches near the end
if (scrollPercentage < 10) {
// At top: show scroll down
scrollIndicator.textContent = `Scroll down ↓`;
scrollIndicator.classList.remove("hidden");
} else {
// Anywhere else: hide indicator
scrollIndicator.classList.add("hidden");
}
}
window.addEventListener("scroll", updateScrollIndicator);
window.addEventListener("resize", updateScrollIndicator);
updateScrollIndicator();
// Optional: smooth wheel damping (prevent very large jumps on fast wheels)
// We leave native scroll but the slow interpolation creates the desired effect.
// Improve performance: reduce motion for users who prefer reduced motion
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (prefersReduced) {
// disable transforms and smooth effect
smoothEl.style.transform = "none";
// remove RAF loops by not starting them (they've already started — simple fallback: set ease to 1)
}
// Typewriter effect for code snippets
function typewriterEffect(element, speed = 30) {
const fullText = element.getAttribute("data-code");
if (!fullText) return;
element.textContent = "";
let index = 0;
function type() {
if (index < fullText.length) {
element.textContent += fullText.charAt(index);
index++;
setTimeout(type, speed);
}
}
type();
}
// Initialize typewriter effect when code snippets come into view
const codeSnippets = document.querySelectorAll(".code-snippet[data-code]");
const snippetObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !entry.target.classList.contains("typed")) {
entry.target.classList.add("typed");
typewriterEffect(entry.target);
}
});
},
{ threshold: 0.5 },
);
codeSnippets.forEach((snippet) => {
snippetObserver.observe(snippet);
});
// Logo animation: show in navbar when scrolling past first section (desktop only)
const logoImage = document.querySelector("header .logo .logo-image");
function updateLogoVisibility() {
const scrollY = window.scrollY || window.pageYOffset;
const firstSectionHeight = window.innerHeight;
const isDesktop = window.matchMedia("(min-width: 901px)").matches;
if (scrollY > firstSectionHeight * 0.5 && isDesktop) {
logoImage.classList.add("visible");
} else {
logoImage.classList.remove("visible");
}
}
window.addEventListener("scroll", updateLogoVisibility);
window.addEventListener("resize", updateLogoVisibility);
updateLogoVisibility();
// Basic link passive handlers for mobile
document.querySelectorAll('a[href^="#"]').forEach((a) => {
a.addEventListener("click", (e) => {
e.preventDefault();
const id = a.getAttribute("href").slice(1);
const el = document.getElementById(id);
if (!el) return;
// scroll instantly to native position then allow smooth lag to catch up
window.scrollTo({
top: el.offsetTop,
behavior: "instant" in HTMLDivElement.prototype ? "instant" : "auto",
});
});
});