🔥 Three.js Solar Core Animation – Complete Explanation
This document explains the entire structure of the HTML, CSS, and JavaScript used in your interactive 3D solar-core project.
Each section is explained separately — HTML, CSS, and JavaScript.
🟧 HTML Explanation
The HTML defines the UI and loads external modules.
1. Page Setup
- The document uses a minimal HTML structure.
- It sets the title to Solaris Core and prepares the page for rendering a full-screen 3D canvas.
2. Overlay Information
- A small text overlay is displayed at the top-left.
- It shows the project name and basic instructions like “Drag to orbit” and “Scroll to zoom”.
- This overlay does not interfere with mouse events on the canvas.
3. Theme Switch Button
- A button is placed at the top-right.
- It cycles through themes like Solaris, Nebula, and Supernova.
- This button updates the shader colors dynamically.
4. Import Map
- Import maps are used to load Three.js modules (like
OrbitControls). - This avoids long relative paths and makes imports cleaner.
🟦 CSS Explanation
The CSS makes the project visually clean and fully immersive.
1. Reset & Fullscreen Layout
- All margins and paddings are cleared.
htmlandbodyare set to fullscreen and given a black background.- Overflow is hidden to prevent unwanted scrolling.
2. Canvas Styling
- The 3D canvas is set as a fixed background layer.
- It always covers 100% width and height of the screen.
3. Overlay Text Styling
- Positioned at the top-left.
- Light opacity so it doesn’t distract from the 3D animation.
- White color for maximum readability against the dark background.
4. Theme Button Styling
- The button has modern UI styling using:
- Glassmorphism effects (
backdrop-filter: blur) - Rounded edges
- Light backgrounds
- Glassmorphism effects (
- The active theme button has a highlighted background.
🟩 JavaScript Explanation
The JavaScript builds the entire 3D environment using Three.js and shaders.
1. Setting Up Renderer, Scene, and Camera
- A WebGL renderer is created with anti-aliasing.
- A 3D scene object holds all mesh and particle elements.
- A PerspectiveCamera is used to give a realistic depth look.
2. Orbit Controls
- Allows the user to rotate around the star.
- Smooth damping creates natural camera movement.
- Zooming is enabled with scroll.
3. Theme System
- A theme object stores color combinations for:
- Core glow
- Accretion disk
- Particle embers
- Outer shell
- Pressing the theme button cycles through these color schemes.
4. Core Star Shader
- A custom ShaderMaterial is used.
- Noise-based vertex displacement simulates a pulsating star.
- A Fresnel effect adds a thin glowing outline.
- Colors change dynamically based on the selected theme.
5. Accretion Disk Particles
- 30,000 small particles rotate around the core.
- They simulate a sci-fi glowing energy disk.
- Particles slightly vibrate and animate over time for realism.
6. Ember Particles
- Small glowing particles shoot outward.
- Each ember has:
- Random start position
- Random direction
- A lifespan cycle
- Creates a dynamic "energy burst" effect around the star.
7. Animation Loop
- Updates controls, shaders, and particles.
- Renders using post-processing.
8. Post-Processing Effects
The animation uses:
- UnrealBloomPass for glow
- FXAA for antialiasing
- AfterimagePass for subtle motion trails
These effects enhance the cinematic sci-fi look.
1<!DOCTYPE html>
2<html lang="en">
3
4 <head>
5 <meta charset="UTF-8">
6 <title>Solaris Core</title>
7
8
9 </head>
10
11 <body>
12 <style>
13 * { margin:0; padding:0; box-sizing: border-box; }
14 html, body {
15 height:100%;
16 background:#000;
17 overflow:hidden;
18 font-family: 'Inter', sans-serif;
19 }
20 canvas {
21 width:100%;
22 height:100%;
23 display:block;
24 position:fixed;
25 top:0;
26 left:0;
27 }
28 .overlay {
29 position: fixed;
30 top: 1rem;
31 left: 1rem;
32 color: white;
33 text-shadow: 0 0 5px black;
34 pointer-events: none;
35 opacity: 0.7;
36 }
37 .overlay h1 {
38 font-size: 1.5rem;
39 font-weight: 500;
40 }
41 .overlay p {
42 font-size: 0.9rem;
43 font-weight: 300;
44 }
45 .theme-switcher {
46 position: fixed;
47 top: 1rem;
48 right: 1rem;
49 display: flex;
50 gap: 0.5rem;
51 z-index: 10;
52 }
53 .theme-button {
54 padding: 0.5rem 1rem;
55 font-size: 0.9rem;
56 font-family: 'Inter', sans-serif;
57 color: white;
58 background: rgba(255, 255, 255, 0.1);
59 border: 1px solid rgba(255, 255, 255, 0.2);
60 border-radius: 12px;
61 backdrop-filter: blur(10px) saturate(180%);
62 -webkit-backdrop-filter: blur(10px) saturate(180%);
63 cursor: pointer;
64 transition: background 0.3s ease, border-color 0.3s ease;
65 }
66 .theme-button:hover {
67 background: rgba(255, 255, 255, 0.2);
68 }
69 .theme-button.active {
70 background: rgba(255, 255, 255, 0.25);
71 border-color: rgba(255, 255, 255, 0.4);
72 font-weight: 500;
73 }
74</style>
75<div class="overlay">
76 <h1 id="theme-title">Solaris</h1>
77 <p>Drag to orbit. Scroll to zoom.</p>
78</div>
79<div class="theme-switcher">
80 <button class="theme-button" id="theme-cycle-button">Solaris</button>
81</div>
82<script type="importmap">
83{
84 "imports": {
85 "three": "https://cdn.jsdelivr.net/npm/three@0.162.0/build/three.module.js",
86 "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/"
87 }
88}
89</script>
90<script type="module">
91import * as THREE from 'three';
92import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
93import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
94import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
95import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
96import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
97import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';
98import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';
99
100const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
101const DPR = Math.min(window.devicePixelRatio || 1, 2);
102renderer.setPixelRatio(DPR);
103renderer.setSize(window.innerWidth, window.innerHeight);
104renderer.setClearColor(0x000000, 1);
105renderer.outputColorSpace = THREE.SRGBColorSpace;
106renderer.toneMapping = THREE.ACESFilmicToneMapping;
107renderer.toneMappingExposure = 1.08;
108document.body.appendChild(renderer.domElement);
109const scene = new THREE.Scene();
110const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 3000);
111camera.position.set(0, 8, 35);
112const controls = new OrbitControls(camera, renderer.domElement);
113controls.enableDamping = true;
114controls.dampingFactor = 0.05;
115controls.minDistance = 5;
116controls.maxDistance = 80;
117
118const THEMES = {
119 solaris: {
120 name: "Solaris",
121 colors: {
122 core: new THREE.Color(1.00, 0.90, 0.70),
123 shell: new THREE.Color(1.00, 0.55, 0.10),
124 diskA: new THREE.Color(1.00, 0.65, 0.10),
125 diskB: new THREE.Color(0.98, 0.25, 0.08),
126 emberA: new THREE.Color(1.00, 0.95, 0.80),
127 emberB: new THREE.Color(1.00, 0.55, 0.05),
128 prominence: new THREE.Color(1.00, 0.65, 0.10)
129 }
130 },
131 nebula: {
132 name: "Nebula",
133 colors: {
134 core: new THREE.Color(0.8, 0.9, 1.0),
135 shell: new THREE.Color(0.1, 0.4, 1.0),
136 diskA: new THREE.Color(0.2, 0.5, 1.0),
137 diskB: new THREE.Color(0.6, 0.2, 0.9),
138 emberA: new THREE.Color(0.9, 0.95, 1.0),
139 emberB: new THREE.Color(0.3, 0.5, 1.0),
140 prominence: new THREE.Color(0.5, 0.3, 1.0)
141 }
142 },
143 supernova: {
144 name: "Supernova",
145 colors: {
146 core: new THREE.Color(1.0, 0.9, 0.95),
147 shell: new THREE.Color(1.0, 0.1, 0.2),
148 diskA: new THREE.Color(1.0, 0.2, 0.4),
149 diskB: new THREE.Color(0.9, 0.1, 0.8),
150 emberA: new THREE.Color(1.0, 0.95, 0.95),
151 emberB: new THREE.Color(1.0, 0.3, 0.3),
152 prominence: new THREE.Color(1.0, 0.2, 0.5)
153 }
154 }
155};
156
157const noiseFunctions = `
158 vec3 mod289(vec3 x){return x - floor(x*(1.0/289.0))*289.0;}
159 vec4 mod289(vec4 x){return x - floor(x*(1.0/289.0))*289.0;}
160 vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}
161 vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
162 float snoise(vec3 v){
163 const vec2 C=vec2(1.0/6.0,1.0/3.0);
164 const vec4 D=vec4(0.0,0.5,1.0,2.0);
165 vec3 i=floor(v+dot(v,C.yyy));
166 vec3 x0=v-i+dot(i,C.xxx);
167 vec3 g=step(x0.yzx,x0.xyz);
168 vec3 l=1.0-g;
169 vec3 i1=min(g.xyz,l.zxy);
170 vec3 i2=max(g.xyz,l.zxy);
171 vec3 x1=x0-i1+C.xxx;
172 vec3 x2=x0-i2+C.yyy;
173 vec3 x3=x0-D.yyy;
174 i=mod289(i);
175 vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));
176 float n_=0.142857142857;
177 vec3 ns=n_*D.wyz-D.xzx;
178 vec4 j=p-49.0*floor(p*ns.z*ns.z);
179 vec4 x_=floor(j*ns.z);
180 vec4 y_=floor(j-7.0*x_);
181 vec4 x=x_*ns.x+ns.yyyy;
182 vec4 y=y_*ns.x+ns.yyyy;
183 vec4 h=1.0-abs(x)-abs(y);
184 vec4 b0=vec4(x.xy,y.xy); vec4 b1=vec4(x.zw,y.zw);
185 vec4 s0=floor(b0)*2.0+1.0; vec4 s1=floor(b1)*2.0+1.0;
186 vec4 sh=-step(h,vec4(0.0));
187 vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy; vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
188 vec3 p0=vec3(a0.xy,h.x); vec3 p1=vec3(a0.zw,h.y); vec3 p2=vec3(a1.xy,h.z); vec3 p3=vec3(a1.zw,h.w);
189 vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));
190 p0*=norm.x; p1*=norm.y; p2*=norm.z; p3*=norm.w;
191 vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);
192 m=m*m;
193 return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
194 }
195`;
196
197const coreGroup = new THREE.Group();
198scene.add(coreGroup);
199
200const starGeometry = new THREE.IcosahedronGeometry(4, 5);
201const starMaterial = new THREE.ShaderMaterial({
202 uniforms: { time: { value: 0 }, uCore: { value: THEMES.solaris.colors.core.clone() } },
203 vertexShader: `
204 uniform float time;
205 varying vec3 vN;
206 ${noiseFunctions}
207 void main(){
208 vN = normalize(normal);
209 float displacement = snoise(normal * 4.0 + time * 0.8) * 0.45;
210 vec3 newPosition = position + normal * displacement;
211 gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
212 }
213 `,
214 fragmentShader: `
215 uniform float time;
216 uniform vec3 uCore;
217 varying vec3 vN;
218 ${noiseFunctions}
219 void main(){
220 float pulse = pow(0.5 + 0.5*sin(time*2.15), 1.7);
221 float fres = pow(1.0 - abs(dot(vN, vec3(0,0,1))), 3.0);
222 float surfaceNoise = snoise(vN * 8.0 + time * 1.2);
223 vec3 col = uCore * (0.4 + 1.5*fres) * (0.4 + 1.0*pulse) * (1.0 + 0.2 * surfaceNoise);
224 col = clamp(col, 0.0, 0.95);
225 gl_FragColor = vec4(col, 1.0);
226 }
227 `,
228 blending: THREE.AdditiveBlending, depthWrite: false
229});
230coreGroup.add(new THREE.Mesh(starGeometry, starMaterial));
231
232const shellGeometry = new THREE.IcosahedronGeometry(8, 5);
233const shellMaterial = new THREE.ShaderMaterial({
234 uniforms: { time: { value: 0 }, uShell: { value: THEMES.solaris.colors.shell.clone() } },
235 vertexShader: `
236 uniform float time;
237 varying vec3 vN;
238 varying vec2 vUv;
239 ${noiseFunctions}
240 void main(){
241 vN = normalize(normal);
242 vUv = uv;
243 float displacement = snoise(position * 2.0 + time * 0.5) * 1.2;
244 vec3 newPosition = position + normal * displacement;
245 gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition,1.0);
246 }
247 `,
248 fragmentShader: `
249 uniform float time;
250 uniform vec3 uShell;
251 varying vec3 vN;
252 varying vec2 vUv;
253 ${noiseFunctions}
254 void main(){
255 float fres = pow(1.0 - abs(dot(vN, vec3(0,0,1))), 0.6);
256 float n = snoise(vec3(vUv*8.0 + vec2(time*0.3, 0.0), time*0.3));
257 float fil = smoothstep(0.55, 0.82, n) * pow(abs(vUv.y*2.0 - 1.0), 14.0);
258 vec3 color = uShell * (0.1 + 2.0*fil + 0.8*fres);
259 float alpha = clamp(0.1 + 0.6*fres + 0.7*fil, 0.0, 1.0);
260 gl_FragColor = vec4(color, alpha);
261 }
262 `,
263 transparent: true, blending: THREE.AdditiveBlending, depthWrite: false
264});
265coreGroup.add(new THREE.Mesh(shellGeometry, shellMaterial));
266
267const particleCount = 30000;
268const diskPositions = new Float32Array(particleCount * 3);
269const diskSeeds = new Float32Array(particleCount);
270const diskBands = new Float32Array(particleCount);
271for (let i = 0; i < particleCount; i++) {
272 const r = 8 + Math.random()*20;
273 const theta = Math.random()*Math.PI*2;
274 diskPositions[i*3] = Math.cos(theta)*r;
275 diskPositions[i*3 + 1] = (Math.random() - 0.5) * 4.0;
276 diskPositions[i*3 + 2] = Math.sin(theta)*r;
277 diskSeeds[i] = Math.random()*1000.0;
278 diskBands[i] = (r - 8.0) / 20.0;
279}
280const diskGeom = new THREE.BufferGeometry();
281diskGeom.setAttribute('position', new THREE.BufferAttribute(diskPositions, 3));
282diskGeom.setAttribute('aSeed', new THREE.BufferAttribute(diskSeeds, 1));
283diskGeom.setAttribute('aBand', new THREE.BufferAttribute(diskBands, 1));
284const diskMat = new THREE.ShaderMaterial({
285 uniforms: { uColorA: { value: THEMES.solaris.colors.diskA.clone() }, uColorB: { value: THEMES.solaris.colors.diskB.clone() }, time: { value: 0 } },
286 vertexShader: `
287 uniform float time;
288 attribute float aSeed;
289 attribute float aBand;
290 varying float vMix;
291 varying float vAlpha;
292 vec2 rot(vec2 p, float a){ float c=cos(a), s=sin(a); return vec2(c*p.x - s*p.y, s*p.x + c*p.y); }
293 void main(){
294 vec3 p = position;
295 float r = length(p.xz);
296 float speed = (14.5 / max(16.0, r*r));
297 float angle = -time * speed;
298 vec2 xz = rot(p.xz, angle);
299 float breathe = 1.0 + 0.011*sin(time*0.8 + aSeed);
300 p.xz = xz * breathe;
301 p.y *= (1.0 + 0.2*sin(time*1.4 + aSeed*2.0 + r*0.2));
302 vec4 mvp = modelViewMatrix * vec4(p, 1.0);
303 gl_Position = projectionMatrix * mvp;
304 gl_PointSize = (65.0 / -mvp.z) * (1.0 - aBand);
305 vMix = aBand;
306 vAlpha = 0.4 + 0.4 * sin(time*3.0 + aSeed);
307 }
308 `,
309 fragmentShader: `
310 uniform vec3 uColorA;
311 uniform vec3 uColorB;
312 varying float vMix;
313 varying float vAlpha;
314 void main(){
315 if (length(gl_PointCoord - vec2(0.5)) > 0.5) discard;
316 vec3 col = mix(uColorA, uColorB, vMix);
317 gl_FragColor = vec4(col * 1.2, vAlpha);
318 }
319 `,
320 transparent: true, blending: THREE.AdditiveBlending, depthWrite: false
321});
322scene.add(new THREE.Points(diskGeom, diskMat));
323
324const emberCount = 5000;
325const emberPos = new Float32Array(emberCount * 3);
326const emberSeeds = new Float32Array(emberCount * 4);
327for (let i = 0; i < emberCount; i++) {
328 emberPos.set([0,0,0], i*3);
329 emberSeeds.set([Math.random(), 0.1 + Math.random()*0.9, Math.random()*10, 0.5 + Math.random()], i*4);
330}
331const emberGeom = new THREE.BufferGeometry();
332emberGeom.setAttribute('position', new THREE.BufferAttribute(emberPos, 3));
333emberGeom.setAttribute('aSeed', new THREE.BufferAttribute(emberSeeds, 4));
334const emberMat = new THREE.ShaderMaterial({
335 uniforms: { uEmberA: { value: THEMES.solaris.colors.emberA.clone() }, uEmberB: { value: THEMES.solaris.colors.emberB.clone() }, time: { value: 0 }},
336 vertexShader: `
337 uniform float time;
338 attribute vec4 aSeed;
339 varying float vLife;
340 void main() {
341 float life = mod(time * aSeed.y * 0.3 + aSeed.x, 1.0);
342 vec3 p = normalize(vec3(
343 sin(aSeed.z * 1.2),
344 cos(aSeed.z * 1.7),
345 sin(aSeed.z * 1.1)
346 )) * (8.0 + life * 60.0);
347 vec4 mvPosition = modelViewMatrix * vec4(p, 1.0);
348 gl_PointSize = (150.0 / -mvPosition.z) * (1.0 - life) * aSeed.w;
349 gl_Position = projectionMatrix * mvPosition;
350 vLife = life;
351 }
352 `,
353 fragmentShader: `
354 uniform vec3 uEmberA;
355 uniform vec3 uEmberB;
356 varying float vLife;
357 void main() {
358 if (length(gl_PointCoord - vec2(0.5)) > 0.5) discard;
359 float opacity = pow(1.0 - vLife, 2.0);
360 vec3 col = mix(uEmberA, uEmberB, vLife);
361 gl_FragColor = vec4(col * 1.4, opacity);
362 }
363 `,
364 transparent: true, blending: THREE.AdditiveBlending, depthWrite: false
365});
366scene.add(new THREE.Points(emberGeom, emberMat));
367
368const prominenceCount = 1000;
369const prominencePos = new Float32Array(prominenceCount * 3);
370const prominenceSeeds = new Float32Array(prominenceCount * 4);
371const q = new THREE.Quaternion();
372const v = new THREE.Vector3();
373for (let i = 0; i < prominenceCount; i++) {
374 prominencePos.set([0, 0, 0], i * 3);
375 prominenceSeeds.set([
376 Math.random(),
377 0.1 + Math.random() * 0.4,
378 5.0 + Math.random() * 25.0,
379 0.5 + Math.random() * 1.5
380 ], i * 4);
381}
382const prominenceGeom = new THREE.BufferGeometry();
383prominenceGeom.setAttribute('position', new THREE.BufferAttribute(prominencePos, 3));
384prominenceGeom.setAttribute('aSeed', new THREE.BufferAttribute(prominenceSeeds, 4));
385const prominenceMat = new THREE.ShaderMaterial({
386 uniforms: {
387 uColor: { value: THEMES.solaris.colors.prominence.clone() },
388 time: { value: 0 }
389 },
390 vertexShader: `
391 uniform float time;
392 attribute vec4 aSeed;
393 varying float vLife;
394
395 vec4 quat_from_axis_angle(vec3 axis, float angle) {
396 vec4 qr;
397 float half_angle = (angle * 0.5);
398 qr.x = axis.x * sin(half_angle);
399 qr.y = axis.y * sin(half_angle);
400 qr.z = axis.z * sin(half_angle);
401 qr.w = cos(half_angle);
402 return qr;
403 }
404
405 vec3 rotate_vertex_position(vec3 position, vec4 q) {
406 return position + 2.0 * cross(q.xyz, cross(q.xyz, position) + q.w * position);
407 }
408
409 void main() {
410 float life = mod(time * aSeed.y + aSeed.x, 1.0);
411 vLife = life;
412
413 float arc = sin(life * 3.14159);
414 vec3 p = vec3(0.0, 0.0, 0.0);
415 p.y = arc * aSeed.z;
416 p.x = (life - 0.5) * 16.0;
417
418 vec3 axis = normalize(vec3(aSeed.x - 0.5, aSeed.y - 0.5, aSeed.z - 0.5));
419 float angle = aSeed.x * 6.28318;
420 vec4 q = quat_from_axis_angle(axis, angle);
421 p = rotate_vertex_position(p, q);
422
423 p += normalize(p) * 8.0;
424
425 vec4 mvPosition = modelViewMatrix * vec4(p, 1.0);
426 gl_PointSize = (250.0 / -mvPosition.z) * arc * (1.0 - life) * aSeed.w;
427 gl_Position = projectionMatrix * mvPosition;
428 }
429 `,
430 fragmentShader: `
431 uniform vec3 uColor;
432 varying float vLife;
433 void main() {
434 if (length(gl_PointCoord - vec2(0.5)) > 0.5) discard;
435 float opacity = pow(sin(vLife * 3.14159), 1.5) * 0.8;
436 gl_FragColor = vec4(uColor * 1.5, opacity);
437 }
438 `,
439 transparent: true,
440 blending: THREE.AdditiveBlending,
441 depthWrite: false
442});
443coreGroup.add(new THREE.Points(prominenceGeom, prominenceMat));
444
445{
446 const count = 2400;
447 const pos = new Float32Array(count * 3);
448 for (let i=0;i<count;i++){
449 const r = THREE.MathUtils.randFloat(250, 1000);
450 const th = Math.random()*Math.PI*2;
451 const ph = Math.acos(THREE.MathUtils.randFloatSpread(2));
452 pos[i*3] = r * Math.sin(ph) * Math.cos(th);
453 pos[i*3+1] = r * Math.cos(ph);
454 pos[i*3+2] = r * Math.sin(ph) * Math.sin(th);
455 }
456 const g = new THREE.BufferGeometry();
457 g.setAttribute('position', new THREE.BufferAttribute(pos,3));
458 const m = new THREE.PointsMaterial({ color: 0xffffff, size: 0.8, sizeAttenuation: true, transparent:true, opacity:0.5, depthWrite:false });
459 scene.add(new THREE.Points(g,m));
460}
461
462const composer = new EffectComposer(renderer);
463composer.addPass(new RenderPass(scene, camera));
464composer.addPass(new AfterimagePass(0.92));
465const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.2, 0.7, 0.92);
466composer.addPass(bloomPass);
467const fxaaPass = new ShaderPass(FXAAShader);
468fxaaPass.material.uniforms['resolution'].value.x = 1 / (window.innerWidth * DPR);
469fxaaPass.material.uniforms['resolution'].value.y = 1 / (window.innerHeight * DPR);
470composer.addPass(fxaaPass);
471
472const themeTitleElement = document.getElementById('theme-title');
473const cycleButton = document.getElementById('theme-cycle-button');
474const themeOrder = ['solaris', 'nebula', 'supernova'];
475let currentThemeIndex = 0;
476
477function applyTheme(themeName) {
478 const theme = THEMES[themeName];
479 if (!theme) return;
480
481 const colors = theme.colors;
482 starMaterial.uniforms.uCore.value.set(colors.core);
483 shellMaterial.uniforms.uShell.value.set(colors.shell);
484 diskMat.uniforms.uColorA.value.set(colors.diskA);
485 diskMat.uniforms.uColorB.value.set(colors.diskB);
486 emberMat.uniforms.uEmberA.value.set(colors.emberA);
487 emberMat.uniforms.uEmberB.value.set(colors.emberB);
488 prominenceMat.uniforms.uColor.value.set(colors.prominence);
489
490 themeTitleElement.textContent = theme.name;
491 cycleButton.textContent = theme.name;
492}
493
494cycleButton.addEventListener('click', () => {
495 currentThemeIndex = (currentThemeIndex + 1) % themeOrder.length;
496 const nextThemeName = themeOrder[currentThemeIndex];
497 applyTheme(nextThemeName);
498});
499
500window.addEventListener('resize', () => {
501 camera.aspect = window.innerWidth / window.innerHeight;
502 camera.updateProjectionMatrix();
503 renderer.setSize(window.innerWidth, window.innerHeight);
504 composer.setSize(window.innerWidth, window.innerHeight);
505 fxaaPass.material.uniforms['resolution'].value.x = 1 / (window.innerWidth * renderer.getPixelRatio());
506 fxaaPass.material.uniforms['resolution'].value.y = 1 / (window.innerHeight * renderer.getPixelRatio());
507});
508
509const clock = new THREE.Clock();
510function animate() {
511 requestAnimationFrame(animate);
512 const delta = clock.getDelta();
513 const time = clock.getElapsedTime();
514 starMaterial.uniforms.time.value = time;
515 shellMaterial.uniforms.time.value = time;
516 diskMat.uniforms.time.value = time;
517 emberMat.uniforms.time.value = time;
518 prominenceMat.uniforms.time.value = time;
519
520 const pulse = 0.5 + 0.5 * Math.sin(time * 2.15);
521 bloomPass.strength = 0.9 + 0.4 * pulse;
522
523 coreGroup.rotation.y += delta * 0.05;
524 controls.update();
525 composer.render();
526}
527
528animate();
529</script>
530
531 </body>
532
533</html>
534