Skip to content

Commit b0bcbad

Browse files
committed
Implemented Background Animation
1 parent 9a490bb commit b0bcbad

8 files changed

Lines changed: 452 additions & 2 deletions

File tree

public/scripts/global-canvas.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Neural Network / Particle Network Animation
3+
* Creates an interactive canvas background with floating particles
4+
* that connect to each other and to the mouse cursor
5+
*/
6+
7+
class Particle {
8+
constructor(canvas) {
9+
this.canvas = canvas;
10+
this.x = Math.random() * canvas.width;
11+
this.y = Math.random() * canvas.height;
12+
this.vx = (Math.random() - 0.5) * 0.5;
13+
this.vy = (Math.random() - 0.5) * 0.5;
14+
this.radius = Math.random() * 2 + 1;
15+
}
16+
17+
update() {
18+
this.x += this.vx;
19+
this.y += this.vy;
20+
21+
// Bounce off edges
22+
if (this.x < 0 || this.x > this.canvas.width) this.vx *= -1;
23+
if (this.y < 0 || this.y > this.canvas.height) this.vy *= -1;
24+
25+
// Keep within bounds
26+
this.x = Math.max(0, Math.min(this.canvas.width, this.x));
27+
this.y = Math.max(0, Math.min(this.canvas.height, this.y));
28+
}
29+
30+
draw(ctx, theme) {
31+
ctx.beginPath();
32+
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
33+
ctx.fillStyle = theme === 'dark'
34+
? 'rgba(192, 202, 245, 0.6)' // Indigo-ish for dark theme
35+
: 'rgba(100, 116, 139, 0.5)'; // Slate for light theme
36+
ctx.fill();
37+
}
38+
}
39+
40+
class ParticleNetwork {
41+
constructor() {
42+
this.canvas = document.getElementById('globalCanvas');
43+
if (!this.canvas) return;
44+
45+
this.ctx = this.canvas.getContext('2d');
46+
this.particles = [];
47+
this.mouse = { x: null, y: null };
48+
this.particleCount = 80;
49+
this.maxDistance = 150;
50+
this.theme = document.documentElement.getAttribute('data-theme') || 'light';
51+
52+
this.init();
53+
this.setupEventListeners();
54+
this.animate();
55+
}
56+
57+
init() {
58+
this.resize();
59+
this.createParticles();
60+
}
61+
62+
resize() {
63+
this.canvas.width = window.innerWidth;
64+
this.canvas.height = window.innerHeight;
65+
}
66+
67+
createParticles() {
68+
this.particles = [];
69+
for (let i = 0; i < this.particleCount; i++) {
70+
this.particles.push(new Particle(this.canvas));
71+
}
72+
}
73+
74+
setupEventListeners() {
75+
window.addEventListener('resize', () => {
76+
this.resize();
77+
this.createParticles();
78+
});
79+
80+
window.addEventListener('mousemove', (e) => {
81+
this.mouse.x = e.clientX;
82+
this.mouse.y = e.clientY;
83+
});
84+
85+
window.addEventListener('mouseout', () => {
86+
this.mouse.x = null;
87+
this.mouse.y = null;
88+
});
89+
90+
// Listen for theme changes
91+
const observer = new MutationObserver((mutations) => {
92+
mutations.forEach((mutation) => {
93+
if (mutation.attributeName === 'data-theme') {
94+
this.theme = document.documentElement.getAttribute('data-theme') || 'light';
95+
}
96+
});
97+
});
98+
99+
observer.observe(document.documentElement, {
100+
attributes: true,
101+
attributeFilter: ['data-theme']
102+
});
103+
}
104+
105+
drawConnections() {
106+
const lineColor = this.theme === 'dark'
107+
? 'rgba(192, 202, 245, 0.15)' // Indigo for dark theme
108+
: 'rgba(100, 116, 139, 0.12)'; // Slate for light theme
109+
110+
// Connect particles to each other
111+
for (let i = 0; i < this.particles.length; i++) {
112+
for (let j = i + 1; j < this.particles.length; j++) {
113+
const dx = this.particles[i].x - this.particles[j].x;
114+
const dy = this.particles[i].y - this.particles[j].y;
115+
const distance = Math.sqrt(dx * dx + dy * dy);
116+
117+
if (distance < this.maxDistance) {
118+
const opacity = 1 - distance / this.maxDistance;
119+
this.ctx.beginPath();
120+
this.ctx.strokeStyle = this.theme === 'dark'
121+
? `rgba(192, 202, 245, ${opacity * 0.15})`
122+
: `rgba(100, 116, 139, ${opacity * 0.12})`;
123+
this.ctx.lineWidth = 1;
124+
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
125+
this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
126+
this.ctx.stroke();
127+
}
128+
}
129+
130+
// Connect particles to mouse
131+
if (this.mouse.x !== null && this.mouse.y !== null) {
132+
const dx = this.particles[i].x - this.mouse.x;
133+
const dy = this.particles[i].y - this.mouse.y;
134+
const distance = Math.sqrt(dx * dx + dy * dy);
135+
136+
if (distance < this.maxDistance * 1.5) {
137+
const opacity = 1 - distance / (this.maxDistance * 1.5);
138+
this.ctx.beginPath();
139+
this.ctx.strokeStyle = this.theme === 'dark'
140+
? `rgba(255, 158, 100, ${opacity * 0.4})` // Warm orange for dark theme
141+
: `rgba(15, 23, 42, ${opacity * 0.3})`; // Dark slate for light theme
142+
this.ctx.lineWidth = 1.5;
143+
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
144+
this.ctx.lineTo(this.mouse.x, this.mouse.y);
145+
this.ctx.stroke();
146+
}
147+
}
148+
}
149+
}
150+
151+
animate() {
152+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
153+
154+
// Update and draw particles
155+
this.particles.forEach(particle => {
156+
particle.update();
157+
particle.draw(this.ctx, this.theme);
158+
});
159+
160+
// Draw connections
161+
this.drawConnections();
162+
163+
requestAnimationFrame(() => this.animate());
164+
}
165+
}
166+
167+
// Initialize when DOM is ready
168+
if (document.readyState === 'loading') {
169+
document.addEventListener('DOMContentLoaded', () => {
170+
new ParticleNetwork();
171+
});
172+
} else {
173+
new ParticleNetwork();
174+
}

src/components/BaseHead.astro

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,17 @@ const ogImageUrl = image
6262
<meta property="twitter:title" content={title} />
6363
<meta property="twitter:description" content={description} />
6464
<meta property="twitter:image" content={ogImageUrl} />
65+
66+
<!-- Global Canvas Animation Styles -->
67+
<style is:global>
68+
#globalCanvas {
69+
position: fixed;
70+
top: 0;
71+
left: 0;
72+
width: 100%;
73+
height: 100%;
74+
z-index: -1;
75+
pointer-events: none;
76+
}
77+
</style>
78+

src/components/GlobalCanvas.astro

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<!-- Global Canvas Animation Background -->
2+
<canvas id="globalCanvas"></canvas>
3+
4+
<script>
5+
/**
6+
* Neural Network / Particle Network Animation
7+
* Creates an interactive canvas background with floating particles
8+
* that connect to each other and to the mouse cursor
9+
*/
10+
11+
class Particle {
12+
constructor(canvas) {
13+
this.canvas = canvas;
14+
this.x = Math.random() * canvas.width;
15+
this.y = Math.random() * canvas.height;
16+
this.vx = (Math.random() - 0.5) * 0.5;
17+
this.vy = (Math.random() - 0.5) * 0.5;
18+
this.radius = Math.random() * 2 + 1;
19+
}
20+
21+
update() {
22+
this.x += this.vx;
23+
this.y += this.vy;
24+
25+
// Bounce off edges
26+
if (this.x < 0 || this.x > this.canvas.width) this.vx *= -1;
27+
if (this.y < 0 || this.y > this.canvas.height) this.vy *= -1;
28+
29+
// Keep within bounds
30+
this.x = Math.max(0, Math.min(this.canvas.width, this.x));
31+
this.y = Math.max(0, Math.min(this.canvas.height, this.y));
32+
}
33+
34+
draw(ctx, theme) {
35+
ctx.beginPath();
36+
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
37+
ctx.fillStyle = theme === 'dark'
38+
? 'rgba(192, 202, 245, 0.6)' // Indigo-ish for dark theme
39+
: 'rgba(100, 116, 139, 0.5)'; // Slate for light theme
40+
ctx.fill();
41+
}
42+
}
43+
44+
class ParticleNetwork {
45+
constructor() {
46+
this.canvas = document.getElementById('globalCanvas');
47+
if (!this.canvas) return;
48+
49+
this.ctx = this.canvas.getContext('2d');
50+
this.particles = [];
51+
this.mouse = { x: null, y: null };
52+
this.particleCount = 80;
53+
this.maxDistance = 150;
54+
this.theme = document.documentElement.getAttribute('data-theme') || 'light';
55+
56+
this.init();
57+
this.setupEventListeners();
58+
this.animate();
59+
}
60+
61+
init() {
62+
this.resize();
63+
this.createParticles();
64+
}
65+
66+
resize() {
67+
this.canvas.width = window.innerWidth;
68+
this.canvas.height = window.innerHeight;
69+
}
70+
71+
createParticles() {
72+
this.particles = [];
73+
for (let i = 0; i < this.particleCount; i++) {
74+
this.particles.push(new Particle(this.canvas));
75+
}
76+
}
77+
78+
setupEventListeners() {
79+
window.addEventListener('resize', () => {
80+
this.resize();
81+
this.createParticles();
82+
});
83+
84+
window.addEventListener('mousemove', (e) => {
85+
this.mouse.x = e.clientX;
86+
this.mouse.y = e.clientY;
87+
});
88+
89+
window.addEventListener('mouseout', () => {
90+
this.mouse.x = null;
91+
this.mouse.y = null;
92+
});
93+
94+
// Listen for theme changes
95+
const observer = new MutationObserver((mutations) => {
96+
mutations.forEach((mutation) => {
97+
if (mutation.attributeName === 'data-theme') {
98+
this.theme = document.documentElement.getAttribute('data-theme') || 'light';
99+
}
100+
});
101+
});
102+
103+
observer.observe(document.documentElement, {
104+
attributes: true,
105+
attributeFilter: ['data-theme']
106+
});
107+
}
108+
109+
drawConnections() {
110+
// Connect particles to each other
111+
for (let i = 0; i < this.particles.length; i++) {
112+
for (let j = i + 1; j < this.particles.length; j++) {
113+
const dx = this.particles[i].x - this.particles[j].x;
114+
const dy = this.particles[i].y - this.particles[j].y;
115+
const distance = Math.sqrt(dx * dx + dy * dy);
116+
117+
if (distance < this.maxDistance) {
118+
const opacity = 1 - distance / this.maxDistance;
119+
this.ctx.beginPath();
120+
this.ctx.strokeStyle = this.theme === 'dark'
121+
? `rgba(192, 202, 245, ${opacity * 0.15})`
122+
: `rgba(100, 116, 139, ${opacity * 0.12})`;
123+
this.ctx.lineWidth = 1;
124+
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
125+
this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
126+
this.ctx.stroke();
127+
}
128+
}
129+
130+
// Connect particles to mouse
131+
if (this.mouse.x !== null && this.mouse.y !== null) {
132+
const dx = this.particles[i].x - this.mouse.x;
133+
const dy = this.particles[i].y - this.mouse.y;
134+
const distance = Math.sqrt(dx * dx + dy * dy);
135+
136+
if (distance < this.maxDistance * 1.5) {
137+
const opacity = 1 - distance / (this.maxDistance * 1.5);
138+
this.ctx.beginPath();
139+
this.ctx.strokeStyle = this.theme === 'dark'
140+
? `rgba(255, 158, 100, ${opacity * 0.4})` // Warm orange for dark theme
141+
: `rgba(15, 23, 42, ${opacity * 0.3})`; // Dark slate for light theme
142+
this.ctx.lineWidth = 1.5;
143+
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
144+
this.ctx.lineTo(this.mouse.x, this.mouse.y);
145+
this.ctx.stroke();
146+
}
147+
}
148+
}
149+
}
150+
151+
animate() {
152+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
153+
154+
// Update and draw particles
155+
this.particles.forEach(particle => {
156+
particle.update();
157+
particle.draw(this.ctx, this.theme);
158+
});
159+
160+
// Draw connections
161+
this.drawConnections();
162+
163+
requestAnimationFrame(() => this.animate());
164+
}
165+
}
166+
167+
// Initialize when DOM is ready
168+
if (document.readyState === 'loading') {
169+
document.addEventListener('DOMContentLoaded', () => {
170+
new ParticleNetwork();
171+
});
172+
} else {
173+
new ParticleNetwork();
174+
}
175+
</script>

0 commit comments

Comments
 (0)