Skip to content

Commit ccc8c44

Browse files
committed
feat: Implement device detection and performance optimization for 3D background; add loading spinner
1 parent 890b250 commit ccc8c44

5 files changed

Lines changed: 328 additions & 29 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<script>
2+
export let size = 'medium'; // 'small', 'medium', 'large'
3+
export let message = 'Loading...';
4+
</script>
5+
6+
<div class="loading-container" aria-live="polite" role="status">
7+
<div class="spinner {size}">
8+
<div class="spinner-ring"></div>
9+
<div class="spinner-ring"></div>
10+
<div class="spinner-ring"></div>
11+
</div>
12+
{#if message}
13+
<p class="loading-message">{message}</p>
14+
{/if}
15+
</div>
16+
17+
<style>
18+
.loading-container {
19+
display: flex;
20+
flex-direction: column;
21+
align-items: center;
22+
justify-content: center;
23+
gap: 1rem;
24+
}
25+
26+
.spinner {
27+
position: relative;
28+
display: inline-block;
29+
}
30+
31+
.spinner.small {
32+
width: 24px;
33+
height: 24px;
34+
}
35+
36+
.spinner.medium {
37+
width: 40px;
38+
height: 40px;
39+
}
40+
41+
.spinner.large {
42+
width: 64px;
43+
height: 64px;
44+
}
45+
46+
.spinner-ring {
47+
position: absolute;
48+
inset: 0;
49+
border: 2px solid transparent;
50+
border-top: 2px solid rgb(37, 99, 235);
51+
border-radius: 50%;
52+
animation: spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
53+
}
54+
55+
.spinner-ring:nth-child(1) {
56+
animation-delay: -0.45s;
57+
}
58+
59+
.spinner-ring:nth-child(2) {
60+
animation-delay: -0.3s;
61+
}
62+
63+
.spinner-ring:nth-child(3) {
64+
animation-delay: -0.15s;
65+
}
66+
67+
.loading-message {
68+
font-size: 0.875rem;
69+
color: rgb(107, 114, 128);
70+
text-align: center;
71+
margin: 0;
72+
}
73+
74+
:global(html.dark) .loading-message {
75+
color: rgb(156, 163, 175);
76+
}
77+
78+
@keyframes spin {
79+
0% {
80+
transform: rotate(0deg);
81+
}
82+
100% {
83+
transform: rotate(360deg);
84+
}
85+
}
86+
87+
/* Respect reduced motion */
88+
@media (prefers-reduced-motion: reduce) {
89+
.spinner-ring {
90+
animation: none !important;
91+
border: 2px solid rgb(37, 99, 235);
92+
border-radius: 50%;
93+
}
94+
}
95+
</style>

src/lib/components/ThreeBackground.svelte

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { onMount, onDestroy } from 'svelte';
33
import { browser } from '$app/environment';
44
import ThreeBackgroundFallback from './ThreeBackgroundFallback.svelte';
5+
import LoadingSpinner from './LoadingSpinner.svelte';
6+
import { getDeviceInfo, getPerformanceRecommendations } from '../utils/deviceDetection.js';
57
68
let canvas;
79
let scene, camera, renderer;
@@ -11,6 +13,13 @@
1113
let mouseX = 0, mouseY = 0;
1214
let threeFailed = false;
1315
let showFallback = false;
16+
let isLoading = false;
17+
let isReady = false;
18+
let performanceSettings = {
19+
particles: 1000,
20+
quality: 'high',
21+
enableAnimations: true
22+
};
1423
1524
onMount(async () => {
1625
if (!browser) return;
@@ -23,13 +32,25 @@
2332
}
2433
2534
try {
35+
isLoading = true;
36+
37+
// Get device info and performance recommendations
38+
const deviceInfo = getDeviceInfo();
39+
const recommendations = getPerformanceRecommendations(deviceInfo);
40+
performanceSettings = recommendations;
41+
42+
// Skip 3D on very low-end devices
43+
if (deviceInfo.isLowEnd && !deviceInfo.supportsWebGL) {
44+
throw new Error('Device not suitable for 3D');
45+
}
46+
2647
// Check for WebGL support
27-
if (!window.WebGLRenderingContext) {
48+
if (!window.WebGLRenderingContext || !deviceInfo.supportsWebGL) {
2849
throw new Error('WebGL not supported');
2950
}
3051
31-
const canvas = document.createElement('canvas');
32-
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
52+
const testCanvas = document.createElement('canvas');
53+
const gl = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl');
3354
if (!gl) {
3455
throw new Error('WebGL context not available');
3556
}
@@ -39,8 +60,15 @@
3960
animate();
4061
window.addEventListener('resize', handleResize);
4162
window.addEventListener('mousemove', handleMouseMove);
63+
64+
// Add a small delay to ensure smooth loading
65+
setTimeout(() => {
66+
isLoading = false;
67+
isReady = true;
68+
}, 500);
4269
} catch (error) {
4370
console.warn('3D background failed to initialize, using fallback:', error);
71+
isLoading = false;
4472
threeFailed = true;
4573
showFallback = true;
4674
}
@@ -89,29 +117,31 @@
89117
mesh = new THREE.Mesh(geometry, material);
90118
scene.add(mesh);
91119
92-
// Add particles
93-
particleGeometry = new THREE.BufferGeometry();
94-
const particleCount = 1000;
95-
const positions = new Float32Array(particleCount * 3);
96-
97-
for (let i = 0; i < particleCount * 3; i += 3) {
98-
positions[i] = (Math.random() - 0.5) * 10;
99-
positions[i + 1] = (Math.random() - 0.5) * 10;
100-
positions[i + 2] = (Math.random() - 0.5) * 10;
120+
// Add particles based on performance settings
121+
if (performanceSettings.enableAnimations) {
122+
particleGeometry = new THREE.BufferGeometry();
123+
const particleCount = performanceSettings.particles || 1000;
124+
const positions = new Float32Array(particleCount * 3);
125+
126+
for (let i = 0; i < particleCount * 3; i += 3) {
127+
positions[i] = (Math.random() - 0.5) * 10;
128+
positions[i + 1] = (Math.random() - 0.5) * 10;
129+
positions[i + 2] = (Math.random() - 0.5) * 10;
130+
}
131+
132+
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
133+
134+
particleMaterial = new THREE.PointsMaterial({
135+
color: 0x3b82f6,
136+
size: performanceSettings.quality === 'high' ? 0.02 : 0.01,
137+
transparent: true,
138+
opacity: performanceSettings.quality === 'low' ? 0.3 : 0.6,
139+
blending: THREE.AdditiveBlending
140+
});
141+
142+
particles = new THREE.Points(particleGeometry, particleMaterial);
143+
scene.add(particles);
101144
}
102-
103-
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
104-
105-
particleMaterial = new THREE.PointsMaterial({
106-
color: 0x3b82f6,
107-
size: 0.02,
108-
transparent: true,
109-
opacity: 0.6,
110-
blending: THREE.AdditiveBlending
111-
});
112-
113-
particles = new THREE.Points(particleGeometry, particleMaterial);
114-
scene.add(particles);
115145
116146
// Add lights
117147
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
@@ -159,8 +189,12 @@
159189

160190
{#if showFallback}
161191
<ThreeBackgroundFallback />
192+
{:else if isLoading}
193+
<div class="three-loading">
194+
<LoadingSpinner size="medium" message="Initializing 3D background..." />
195+
</div>
162196
{:else}
163-
<canvas bind:this={canvas} class="three-canvas" aria-hidden="true"></canvas>
197+
<canvas bind:this={canvas} class="three-canvas" aria-hidden="true" class:ready={isReady}></canvas>
164198
{/if}
165199

166200
<style>
@@ -172,5 +206,30 @@
172206
height: 100%;
173207
pointer-events: none;
174208
z-index: 0;
209+
opacity: 0;
210+
transition: opacity 0.5s ease;
211+
}
212+
213+
.three-canvas.ready {
214+
opacity: 1;
215+
}
216+
217+
.three-loading {
218+
position: fixed;
219+
top: 0;
220+
left: 0;
221+
width: 100%;
222+
height: 100%;
223+
display: flex;
224+
align-items: center;
225+
justify-content: center;
226+
pointer-events: none;
227+
z-index: 0;
228+
background: rgba(0, 0, 0, 0.02);
229+
backdrop-filter: blur(1px);
230+
}
231+
232+
:global(html.dark) .three-loading {
233+
background: rgba(0, 0, 0, 0.1);
175234
}
176235
</style>

0 commit comments

Comments
 (0)