-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
167 lines (140 loc) · 4.95 KB
/
script.js
File metadata and controls
167 lines (140 loc) · 4.95 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
(() => {
const reduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
// Year
const y = document.getElementById("year");
if (y) y.textContent = new Date().getFullYear();
// Reveal sections on scroll
const revealEls = document.querySelectorAll(".reveal");
if ("IntersectionObserver" in window && !reduced) {
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) {
e.target.classList.add("is-visible");
io.unobserve(e.target);
}
}
},
{ threshold: 0.12, rootMargin: "0px 0px -8% 0px" }
);
revealEls.forEach((el) => io.observe(el));
} else {
revealEls.forEach((el) => el.classList.add("is-visible"));
}
// Dot field animation
const canvases = document.querySelectorAll("canvas.dots");
if (!canvases.length) return;
// Shared lightweight field. Each canvas renders its own slice but with
// unique phase so left and right do not move in sync.
const fields = [];
function buildField(canvas) {
const side = canvas.dataset.side === "right" ? 1 : -1;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
// Measure the parent hero so we always cover the full vertical span,
// even if the canvas itself is reporting a stale height.
const host = canvas.parentElement;
const hostRect = host.getBoundingClientRect();
const cRect = canvas.getBoundingClientRect();
const w = Math.max(1, Math.floor(cRect.width));
const h = Math.max(1, Math.floor(hostRect.height));
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
const ctx = canvas.getContext("2d", { alpha: true });
ctx.scale(dpr, dpr);
// Grid spacing — denser on larger screens, capped for perf.
const spacing = w < 200 ? 22 : w < 320 ? 26 : 30;
const cols = Math.ceil(w / spacing) + 1;
const rows = Math.ceil(h / spacing) + 1;
const points = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = c * spacing + (r % 2 === 0 ? 0 : spacing / 2);
const y = r * spacing;
// Phase seed so each dot animates uniquely.
const seed = (c * 13.37 + r * 7.91 + (side === 1 ? 100 : 0)) * 0.13;
points.push({ x, y, seed });
}
}
return { canvas, ctx, w, h, points, side, spacing };
}
function rebuild() {
fields.length = 0;
canvases.forEach((c) => fields.push(buildField(c)));
}
rebuild();
// Rebuild after fonts/layout settle in case initial heights were stale.
window.addEventListener("load", () => requestAnimationFrame(rebuild), {
once: true,
});
// Track hero size changes (viewport resize, address bar collapse, etc).
const hero = document.querySelector(".hero");
if (hero && "ResizeObserver" in window) {
let roRaf;
const ro = new ResizeObserver(() => {
cancelAnimationFrame(roRaf);
roRaf = requestAnimationFrame(rebuild);
});
ro.observe(hero);
}
let resizeRaf;
window.addEventListener(
"resize",
() => {
cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(rebuild);
},
{ passive: true }
);
if (reduced) {
// Render a single static frame and stop.
for (const f of fields) drawFrame(f, 0);
return;
}
function drawFrame(f, t) {
const { ctx, w, h, points, side, spacing } = f;
ctx.clearRect(0, 0, w, h);
// Slow horizontal drift opposite on each side.
const drift = Math.sin(t * 0.00008) * 6 * side;
// Wave parameters
const k = 0.012; // spatial frequency
const speed = 0.00045; // temporal frequency
for (let i = 0; i < points.length; i++) {
const p = points[i];
// Distance from outer edge (left for left side, right for right side).
const edgeDist = side === -1 ? p.x : w - p.x;
// Outer edge is brightest, fades inward.
const edgeFade = Math.max(0, 1 - edgeDist / w);
// Slow wave-like opacity shift.
const wave =
Math.sin(p.x * k + p.y * k * 0.6 + t * speed + p.seed) * 0.5 + 0.5;
// Combine: base + wave, modulated by edge fade.
const alpha = (0.05 + wave * 0.55) * (0.25 + edgeFade * 0.85);
// Very subtle radius pulse on a few points.
const radius = 0.9 + (wave > 0.85 ? 0.6 : 0);
ctx.beginPath();
ctx.arc(p.x + drift, p.y, radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${alpha.toFixed(3)})`;
ctx.fill();
}
}
let running = true;
document.addEventListener("visibilitychange", () => {
running = !document.hidden;
if (running) loop(performance.now());
});
let lastFrame = 0;
const minDelta = 1000 / 30; // cap at ~30fps; animation is slow
function loop(now) {
if (!running) return;
if (now - lastFrame >= minDelta) {
lastFrame = now;
for (const f of fields) drawFrame(f, now);
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();