Skip to content

Commit fef6edb

Browse files
committed
feat: add toggle for animated background with performance optimizations
- Add user preference to disable animated background - Implement reduced motion detection for accessibility - Optimize animation performance (reduce particles, throttle mouse events, 30fps) - Add floating toggle button to enable/disable background - Persist user preference in localStorage Addresses GPU strain and battery drain issues reported by users
1 parent 433e2fc commit fef6edb

File tree

44 files changed

+1750
-11
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1750
-11
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
"use client";
2+
"use strict";
3+
Object.defineProperty(exports, "__esModule", { value: true });
4+
exports.AnimatedBackground = AnimatedBackground;
5+
var react_1 = require("react");
6+
function AnimatedBackground() {
7+
var canvasRef = (0, react_1.useRef)(null);
8+
var _a = (0, react_1.useState)(true), isEnabled = _a[0], setIsEnabled = _a[1];
9+
var _b = (0, react_1.useState)(false), prefersReducedMotion = _b[0], setPrefersReducedMotion = _b[1];
10+
(0, react_1.useEffect)(function () {
11+
// Check for user preference in localStorage
12+
var savedPreference = localStorage.getItem('animatedBackgroundEnabled');
13+
if (savedPreference !== null) {
14+
setIsEnabled(savedPreference === 'true');
15+
}
16+
// Check for reduced motion preference
17+
var mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
18+
setPrefersReducedMotion(mediaQuery.matches);
19+
var handleMotionPreferenceChange = function (e) {
20+
setPrefersReducedMotion(e.matches);
21+
};
22+
mediaQuery.addEventListener('change', handleMotionPreferenceChange);
23+
return function () {
24+
mediaQuery.removeEventListener('change', handleMotionPreferenceChange);
25+
};
26+
}, []);
27+
(0, react_1.useEffect)(function () {
28+
// Don't render animation if disabled or user prefers reduced motion
29+
if (!isEnabled || prefersReducedMotion)
30+
return;
31+
var canvas = canvasRef.current;
32+
if (!canvas)
33+
return;
34+
var ctx = canvas.getContext("2d", { alpha: false });
35+
if (!ctx)
36+
return;
37+
// Grid settings - exact values from roocode.com
38+
var gridSize = 50;
39+
var gridOpacity = 0.15;
40+
// Initialize gradient points for lighting effects - exact colors and positions
41+
var gradientPoints = [
42+
{
43+
x: canvas.width * 0.2,
44+
y: canvas.height * 0.3,
45+
radius: canvas.width * 0.4,
46+
color: "rgba(0, 100, 255, 0.15)",
47+
},
48+
{
49+
x: canvas.width * 0.8,
50+
y: canvas.height * 0.7,
51+
radius: canvas.width * 0.5,
52+
color: "rgba(100, 0, 255, 0.1)",
53+
},
54+
];
55+
// Particle system - reduced particle count for better performance
56+
var particles = [];
57+
var particleCount = Math.min(30, Math.floor(window.innerWidth / 60));
58+
// Set canvas dimensions
59+
var resizeCanvas = function () {
60+
var width = window.innerWidth;
61+
var height = window.innerHeight;
62+
canvas.width = width;
63+
canvas.height = height;
64+
// Update gradient points when canvas is resized
65+
gradientPoints = [
66+
{
67+
x: canvas.width * 0.2,
68+
y: canvas.height * 0.3,
69+
radius: canvas.width * 0.4,
70+
color: "rgba(0, 100, 255, 0.15)",
71+
},
72+
{
73+
x: canvas.width * 0.8,
74+
y: canvas.height * 0.7,
75+
radius: canvas.width * 0.5,
76+
color: "rgba(100, 0, 255, 0.1)",
77+
},
78+
];
79+
};
80+
// Initial resize and setup
81+
resizeCanvas();
82+
// Add resize listener
83+
window.addEventListener("resize", resizeCanvas);
84+
// Ensure canvas is properly initialized
85+
setTimeout(function () {
86+
resizeCanvas();
87+
drawGrid();
88+
}, 0);
89+
// Draw grid with perspective effect - exact implementation
90+
function drawGrid() {
91+
if (!ctx) {
92+
throw new Error("Context is null (not initialized?)");
93+
}
94+
if (!canvas) {
95+
throw new Error("Canvas is null (not initialized?)");
96+
}
97+
ctx.clearRect(0, 0, canvas.width, canvas.height);
98+
// Draw gradient lighting effects
99+
gradientPoints.forEach(function (point) {
100+
var gradient = ctx.createRadialGradient(point.x, point.y, 0, point.x, point.y, point.radius);
101+
gradient.addColorStop(0, point.color);
102+
gradient.addColorStop(1, "rgba(0, 0, 0, 0)");
103+
ctx.fillStyle = gradient;
104+
ctx.fillRect(0, 0, canvas.width, canvas.height);
105+
});
106+
// Draw grid lines with perspective effect
107+
ctx.strokeStyle = "rgba(50, 50, 70, ".concat(gridOpacity, ")");
108+
ctx.lineWidth = 0.5;
109+
// Horizontal lines with perspective
110+
var horizonY = canvas.height * 0.7; // Horizon point
111+
var vanishingPointX = canvas.width * 0.5; // Center vanishing point
112+
// Vertical lines
113+
for (var x = 0; x <= canvas.width; x += gridSize) {
114+
var normalizedX = x / canvas.width - 0.5; // -0.5 to 0.5
115+
ctx.beginPath();
116+
ctx.moveTo(x, 0);
117+
// Calculate curve based on distance from center
118+
var curveStrength = 50 * Math.abs(normalizedX);
119+
var controlPointY = horizonY - curveStrength;
120+
// Create curved line toward vanishing point
121+
ctx.quadraticCurveTo(x + (vanishingPointX - x) * 0.3, controlPointY, vanishingPointX + (x - vanishingPointX) * 0.2, horizonY);
122+
ctx.stroke();
123+
}
124+
// Horizontal lines
125+
for (var y = 0; y <= horizonY; y += gridSize) {
126+
var normalizedY = y / horizonY; // 0 to 1
127+
var lineWidth = gridSize * (1 + normalizedY * 5); // lines get wider as they get closer
128+
ctx.beginPath();
129+
ctx.moveTo(vanishingPointX - lineWidth, y);
130+
ctx.lineTo(vanishingPointX + lineWidth, y);
131+
ctx.stroke();
132+
}
133+
updateParticles();
134+
}
135+
var Particle = /** @class */ (function () {
136+
function Particle() {
137+
if (!canvas) {
138+
throw new Error("Canvas is null (not initialized?)");
139+
}
140+
this.x = Math.random() * canvas.width;
141+
this.y = Math.random() * (canvas.height * 0.7); // Keep particles above horizon
142+
this.size = Math.random() * 2 + 1;
143+
this.speedX = (Math.random() - 0.5) * 0.8;
144+
this.speedY = (Math.random() - 0.5) * 0.8;
145+
this.color = "rgba(100, 150, 255, ";
146+
this.opacity = Math.random() * 0.5 + 0.2;
147+
}
148+
Particle.prototype.update = function () {
149+
if (!canvas) {
150+
throw new Error("Canvas is null (not initialized?)");
151+
}
152+
this.x += this.speedX;
153+
this.y += this.speedY;
154+
// Boundary check
155+
if (this.x > canvas.width)
156+
this.x = 0;
157+
else if (this.x < 0)
158+
this.x = canvas.width;
159+
if (this.y > canvas.height * 0.7)
160+
this.y = 0;
161+
else if (this.y < 0)
162+
this.y = canvas.height * 0.7;
163+
// Pulsate opacity
164+
this.opacity += Math.sin(Date.now() * 0.001) * 0.01;
165+
this.opacity = Math.max(0.1, Math.min(0.7, this.opacity));
166+
};
167+
Particle.prototype.draw = function () {
168+
if (!ctx) {
169+
throw new Error("Context is null (not initialized?)");
170+
}
171+
ctx.fillStyle = "".concat(this.color).concat(this.opacity, ")");
172+
ctx.beginPath();
173+
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
174+
ctx.fill();
175+
};
176+
return Particle;
177+
}());
178+
// Initialize particles
179+
for (var i = 0; i < particleCount; i++) {
180+
particles.push(new Particle());
181+
}
182+
// Connect particles with lines
183+
function connectParticles() {
184+
if (!ctx) {
185+
throw new Error("Context is null (not initialized?)");
186+
}
187+
var maxDistance = 150;
188+
for (var a = 0; a < particles.length; a++) {
189+
for (var b = a; b < particles.length; b++) {
190+
var dx = particles[a].x - particles[b].x;
191+
var dy = particles[a].y - particles[b].y;
192+
var distance = Math.sqrt(dx * dx + dy * dy);
193+
if (distance < maxDistance) {
194+
var opacity = (1 - distance / maxDistance) * 0.5;
195+
ctx.strokeStyle = "rgba(100, 150, 255, ".concat(opacity, ")");
196+
ctx.lineWidth = 0.5;
197+
ctx.beginPath();
198+
ctx.moveTo(particles[a].x, particles[a].y);
199+
ctx.lineTo(particles[b].x, particles[b].y);
200+
ctx.stroke();
201+
}
202+
}
203+
}
204+
}
205+
function updateParticles() {
206+
particles.forEach(function (particle) {
207+
particle.update();
208+
particle.draw();
209+
});
210+
connectParticles();
211+
}
212+
// Animation loop
213+
var animationId;
214+
var frameCount = 0;
215+
// Target position for smooth following
216+
var targetX = canvas.width * 0.2;
217+
var targetY = canvas.height * 0.3;
218+
var moveSpeed = 0.05; // Exact speed from roocode.com
219+
// Move gradient points with mouse - throttled for performance
220+
var mouseThrottle = false;
221+
var handleMouseMove = function (e) {
222+
if (!mouseThrottle) {
223+
targetX = e.clientX;
224+
targetY = e.clientY;
225+
mouseThrottle = true;
226+
setTimeout(function () { mouseThrottle = false; }, 50); // Throttle to 20fps
227+
}
228+
};
229+
// Update gradient point position in animation loop
230+
function updateGradientPosition() {
231+
if (!canvas)
232+
throw new Error("Canvas is null (not initialized?)");
233+
// Calculate direction vector
234+
var dx = targetX - gradientPoints[0].x;
235+
var dy = targetY - gradientPoints[0].y;
236+
// Smooth movement using linear interpolation
237+
gradientPoints[0].x += dx * moveSpeed;
238+
gradientPoints[0].y += dy * moveSpeed;
239+
// Adjust radius based on distance to target
240+
var distanceToTarget = Math.sqrt(dx * dx + dy * dy);
241+
gradientPoints[0].radius = Math.max(canvas.width * 0.2, Math.min(canvas.width * 0.4, canvas.width * 0.3 + distanceToTarget * 0.1));
242+
}
243+
function animate() {
244+
animationId = requestAnimationFrame(animate);
245+
frameCount++;
246+
// Skip every other frame for better performance (30fps instead of 60fps)
247+
if (frameCount % 2 === 0) {
248+
updateGradientPosition();
249+
drawGrid();
250+
}
251+
}
252+
animate();
253+
window.addEventListener("mousemove", handleMouseMove);
254+
return function () {
255+
window.removeEventListener("resize", resizeCanvas);
256+
window.removeEventListener("mousemove", handleMouseMove);
257+
cancelAnimationFrame(animationId);
258+
};
259+
}, [isEnabled, prefersReducedMotion]);
260+
// Don't render canvas if animation is disabled
261+
if (!isEnabled || prefersReducedMotion) {
262+
return null;
263+
}
264+
return <canvas ref={canvasRef} style={{
265+
position: 'fixed',
266+
top: 0,
267+
left: 0,
268+
width: '100%',
269+
height: '100%',
270+
zIndex: -1,
271+
pointerEvents: 'none',
272+
willChange: 'transform' // Hint for GPU optimization
273+
}}/>;
274+
}

src/components/AnimatedBackground.tsx

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,42 @@
11
"use client"
22

3-
import { useEffect, useRef } from "react"
3+
import { useEffect, useRef, useState } from "react"
44

55
export function AnimatedBackground() {
66
const canvasRef = useRef<HTMLCanvasElement>(null)
7+
const [isEnabled, setIsEnabled] = useState(true)
8+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
79

810
useEffect(() => {
11+
// Check for user preference in localStorage
12+
const savedPreference = localStorage.getItem('animatedBackgroundEnabled')
13+
if (savedPreference !== null) {
14+
setIsEnabled(savedPreference === 'true')
15+
}
16+
17+
// Check for reduced motion preference
18+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
19+
setPrefersReducedMotion(mediaQuery.matches)
20+
21+
const handleMotionPreferenceChange = (e: MediaQueryListEvent) => {
22+
setPrefersReducedMotion(e.matches)
23+
}
24+
25+
mediaQuery.addEventListener('change', handleMotionPreferenceChange)
26+
27+
return () => {
28+
mediaQuery.removeEventListener('change', handleMotionPreferenceChange)
29+
}
30+
}, [])
31+
32+
useEffect(() => {
33+
// Don't render animation if disabled or user prefers reduced motion
34+
if (!isEnabled || prefersReducedMotion) return
35+
936
const canvas = canvasRef.current
1037
if (!canvas) return
1138

12-
const ctx = canvas.getContext("2d")
39+
const ctx = canvas.getContext("2d", { alpha: false })
1340
if (!ctx) return
1441

1542
// Grid settings - exact values from roocode.com
@@ -32,9 +59,9 @@ export function AnimatedBackground() {
3259
},
3360
]
3461

35-
// Particle system - exact configuration
62+
// Particle system - reduced particle count for better performance
3663
const particles: Particle[] = []
37-
const particleCount = Math.min(50, Math.floor(window.innerWidth / 40))
64+
const particleCount = Math.min(30, Math.floor(window.innerWidth / 60))
3865

3966
// Set canvas dimensions
4067
const resizeCanvas = () => {
@@ -236,16 +263,22 @@ export function AnimatedBackground() {
236263

237264
// Animation loop
238265
let animationId: number
266+
let frameCount = 0
239267

240268
// Target position for smooth following
241269
let targetX = canvas.width * 0.2
242270
let targetY = canvas.height * 0.3
243271
const moveSpeed = 0.05 // Exact speed from roocode.com
244272

245-
// Move gradient points with mouse
273+
// Move gradient points with mouse - throttled for performance
274+
let mouseThrottle = false
246275
const handleMouseMove = (e: MouseEvent) => {
247-
targetX = e.clientX
248-
targetY = e.clientY
276+
if (!mouseThrottle) {
277+
targetX = e.clientX
278+
targetY = e.clientY
279+
mouseThrottle = true
280+
setTimeout(() => { mouseThrottle = false }, 50) // Throttle to 20fps
281+
}
249282
}
250283

251284
// Update gradient point position in animation loop
@@ -270,8 +303,13 @@ export function AnimatedBackground() {
270303

271304
function animate() {
272305
animationId = requestAnimationFrame(animate)
273-
updateGradientPosition()
274-
drawGrid()
306+
frameCount++
307+
308+
// Skip every other frame for better performance (30fps instead of 60fps)
309+
if (frameCount % 2 === 0) {
310+
updateGradientPosition()
311+
drawGrid()
312+
}
275313
}
276314

277315
animate()
@@ -283,7 +321,12 @@ export function AnimatedBackground() {
283321
window.removeEventListener("mousemove", handleMouseMove)
284322
cancelAnimationFrame(animationId)
285323
}
286-
}, [])
324+
}, [isEnabled, prefersReducedMotion])
325+
326+
// Don't render canvas if animation is disabled
327+
if (!isEnabled || prefersReducedMotion) {
328+
return null
329+
}
287330

288331
return <canvas
289332
ref={canvasRef}
@@ -294,7 +337,8 @@ export function AnimatedBackground() {
294337
width: '100%',
295338
height: '100%',
296339
zIndex: -1,
297-
pointerEvents: 'none'
340+
pointerEvents: 'none',
341+
willChange: 'transform' // Hint for GPU optimization
298342
}}
299343
/>
300344
}

0 commit comments

Comments
 (0)