Create Magnetosphere Three.js Demo
Messages
The messages exchanged in this conversation.
- Created
A new turn starts now. Fetch the current task details and execute the steps in the enclosed instructions.
- Created activity call _getTaskDetails
- Created
Let me look at the current state. The task is "Wake up" - wake up and do as I please. The last execution was about creating a Magnetosphere Three.js demo that was interrupted. Let me check the current state of the demos folder and the /tmp/mag/ directory.
Let me install shell tools first and explore.
- Created
The task is "Wake up and do as you please." — this is a fresh turn. The last execution was creating a Magnetosphere Three.js demo (#034) that was interrupted. A natural thing to do: check the state of /space/demos/ and see whether demo 034 exists there. If not, I could finish it or create a new demo. Let me first check the current state.
- Created activity call execute_shell_command
- Created
So the demos folder has 033 demos (up to 033-gossamer). The magnetosphere demo (034) was never deployed — /tmp/mag/ is gone (fresh environment), and /space/demos/034-magnetosphere doesn't exist.
The task is "Wake up and do as you please." So I should continue/finish what was started: create the Magnetosphere demo #034, since that was the last unfinished work. That fits my purpose: create cool Three.js demos.
Let me check the index.html to see the format used for listing demos.
- Created activity call execute_shell_command
- Created
Now I understand the structure. The task is "Wake up and do as you please." — I should finish the interrupted work: create demo #034 Magnetosphere, deploy it to /space/demos/034-magnetosphere/, and update the index.
Let me check a previous demo to see the general style/structure used (e.g., 033-gossamer) so I can match conventions (import maps, CDN versions).
- Created activity call execute_shell_command
- Created
The convention: self-contained single HTML file, three.js r0.170.0 from unpkg with import maps. Good.
Now I'll build the 034-magnetosphere demo from scratch (the /tmp/mag/ draft is gone). I need to write a complete, high-quality, self-contained demo. This will be a large file (~1500+ lines). Let me plan the demo:
034 — Magnetosphere: Interactive simulation of Earth's magnetosphere:
- Procedural starfield skybox with Milky Way (GLSL shader)
- Sun with corona billboard (shader), directional light
- Earth: procedural shader planet (continents via noise, oceans with specular, ice caps, night city lights, cloud layer, atmosphere limb glow)
- Tilted dipole magnetic field lines (instanced tubes or lines), with flow pulses
- Magnetopause + bow shock translucent shells (shaders) that compress during storms
- Solar wind particle system flowing from the Sun, deflected around the magnetosphere
- Precipitating particles funneling through cusps → auroras
- Van Allen belts: trapped particles bouncing between mirror points + drifting around
- Auroral ovals with animated shader curtains
- CME event: launch a coronal mass ejection that travels and slams into the magnetosphere, triggering a geomagnetic storm (brighter auroras, compressed magnetopause, audio rumble)
- UI: sliders (solar wind speed, storm level / Kp), buttons (launch CME, pause, mute, quality), HUD with stats
- WebAudio: ambient solar wind hiss, storm rumble, whistler chorus, CME whoosh/impact
- Post-processing: UnrealBloomPass + vignette/grading pass
- OrbitControls, responsive resize
This is a big build. I'll write it carefully in one go, in chunks using the file write tool (heredoc via execute_shell_command might be easier for large content, but the read_write_file tool works too; content length limits — I'll split into a few appended chunks using shell heredocs with quoted delimiters to avoid interpolation issues).
Actually, best approach: write the file in several parts using
cat >>with heredocs quoted ('EOF') so nothing gets interpolated. Each part maybe 300-400 lines. Let me carefully design the code first.Key technical design decisions:
-
Import map: three@0.170.0, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, ShaderPass from examples/jsm.
-
Scene layout:
- Sun at position (-140, 30, 0) roughly — actually let's put Sun far away along -X (solar wind blows +X direction toward Earth at origin). Sun at (-160, 0, 0)? Give slight offset for aesthetics: (-150, 25, -40), and wind direction from sun to earth normalized. Simpler: wind flows along +X. Sun at (-160, 18, 0). Light from sun direction.
- Earth at origin, radius R_E = 6 (scene units). Distances in "Earth radii" feel: magnetopause subsolar ~10 R_E... but for visuals, scale down: Earth radius 6 units, magnetopause subsolar distance ~ 5*R? Real: 10 R_E sunward, tail extends hundreds. For a pleasing view: Earth radius 6, magnetopause nose at ~24 units (4 R_E... let's use ~3.2 R_E scaled). Bow shock a bit further. Field lines out to ~30 units. Solar wind streams in a box of ~±140 units.
Actually let me define RE = 6. Magnetopause shape: use the classic empirical model — subsolar standoff distance Rmp ≈ 10.7·RE real, but compressed for the scene: nose at 3.6 RE ≈ 21.6 units; flaring: r = R0 * (2/(1+cosθ))^α approximately (Shue-like). I'll build a custom parametric surface: for angle θ from +X axis (sunward... actually wind comes FROM -X, so subsolar point faces -X; let me define wind direction as +X, so the sunward/nose direction is -X).
Let me define: windDir = (1,0,0) (flowing toward +X). Sun sits at -X. Subsolar nose of magnetopause points toward -X.
Magnetopause shape (simplified Shue): in polar coordinates around the X axis with θ measured from the -X axis: r(θ) = R0 * (2/(1+cos θ))^α, with R0 = nose distance, α ≈ 0.6 (flaring). For θ→π (down-tail), r → R0·2^α... that formula closes the tail though; Shue's model gives finite radius at θ=π (tail radius). Fine — that yields a paraboloid-like teardrop closed-ish at the tail; good enough visually with fresnel shader.
I'll generate a BufferGeometry of revolution: sample θ in [0, π], φ in [0, 2π]. Vertex: axis = x = -r cosθ... wait θ measured from -X axis: direction = (-cosθ, sinθ cosφ, sinθ sinφ). Point = r(θ)·direction. Radius of cross-section at θ: r·sinθ.
Bow shock: same shape scaled ~1.25 with nose slightly further out.
-
Dipole field: Earth's dipole tilted ~11° from rotation axis; rotate the dipole group over time (Earth rotation) slowly. Field lines: L shells from L=1.5 to L=8: r = L·RE·cos²λ (λ magnetic latitude). Generate line points in dipole frame, then orient the whole group. Use LineBasicMaterial with additive blending and low opacity, colored teal/cyan. Flow pulses: small glowing points traveling along field lines — use THREE.Points with per-point position computed each frame from parametric line traversal. Simpler: create N pulses, each assigned to a field line with a phase t; position = pointOnDipoleLine(L, λ(t)); color intensity varies. Use a single Points object with a custom shader (size attenuation, soft round sprite).
-
Solar wind particles: ~6000 particles in a streaming volume: x ∈ [-160, 120], cross-section radius ~70. Each particle: velocity +X (speed slider), plus deflection around magnetosphere: analytic deflection — approximate the magnetosphere as an obstacle: use the magnetopause SDF-ish function; when near/inside, push velocity tangentially. Cheaper approach: precompute deflection as function of (impact parameter, x): v = windDir·v0; then add lateral push based on gradient of a potential: treat shape r_mp(θ) as a boundary; compute "penetration" d = particle's implied θ...
Simpler robust approach: model the obstacle as a smooth potential U(x) = strength / (1 + (s/σ)^4) where s = distance from particle to the magnetopause surface estimated by: compute particle spherical coords relative to -X axis: θp, rp; boundary radius Rb(θp) = R0(2/(1+cosθp))^α scaled by bowshock factor; s = rp - Rb(θp) (positive outside). If s < shell thickness, apply outward normal push ≈ along radial direction (from Earth) plus tangential slide: v += radialDir * push * dt, and damp the inward radial component. This makes particles flow around it. Particles that get within capture zone near cusps (θ near 90°±? cusps at ~75° from pole...). Keep it visual, not rigorous.
Also remove particles that hit Earth (r < RE) → respawn upstream; those near poles can become "precipitation" but I'll manage precipitation as its own system for control.
-
Precipitation particles: spawn at cusp regions (ring around magnetic poles at ~72° magnetic latitude, radius ~ up to 2 RE high), accelerate along field lines down into the atmosphere, fade at r < RE·1.05. Their glow contributes near aurora. ~800 particles.
-
Van Allen belts: two toroidal-ish populations in dipole frame: inner belt L∈[1.3,2.2], outer L∈[3,6]. Each particle has: L, phase along field line (mirror bounce: λ oscillates between ±λm), azimuthal drift angle ψ that increases over time (grad-curvature drift). Position: r = L·RE·cos²λ; in dipole frame: (r·cosλ·cosψ? ) — parametrize dipole field line in meridian plane then rotate by ψ around dipole axis. Colors: inner = warm (protons, orange/red), outer = cool (electrons, cyan/blue). ~1400 particles. Shader points with pulsing.
-
Aurora ovals: geometry: rings/crowns around both magnetic poles at r = RE·1.02, built as ribbons: N segments around a circle (magnetic latitude ~70°, oval radius ~20°); each segment a vertical "curtain" quad strip extending upward ~0.35 RE with a shader: green (557.7nm) base → red/purple top, animated ray structure with noise + time, intensity from storm level and local precipitation. Double-sided, additive, depthWrite off. Build as a single BufferGeometry with uv.y = height, custom ShaderMaterial sampling noise for rays.
-
Earth shader: procedural — fragment shader with 3D noise (fbm) for continents: use triplanar-ish 3D value noise on the sphere normal. Land: green/brown by latitude (deserts near tropics), oceans: deep blue with specular sun glint (Blinn-ish), ice caps by latitude+noise, night side: city lights (noise-thresholded on land) + terminator softening. Cloud layer: separate slightly larger sphere with fbm noise alpha, casts subtle darkening? Keep simple: white clouds with alpha, rotating slowly. Atmosphere: back-side fresnel shell additive blue.
Earth axis tilt: 23.4°. Rotate Earth slowly.
-
Sky: big sphere, shader: hash-based stars (multiple layers, twinkle), milky way band via fbm along a great-circle plane, subtle nebula tint.
-
Sun: sphere with noisy granulation shader + corona: billboard plane with radial falloff + streaks (noise rays), additive. Plus a strong PointLight? Use DirectionalLight from sun direction for earth lighting handled in custom shader — Earth shader is custom, so compute lighting manually using uniform sunDir. For any standard materials (none much), fine.
-
CME: state machine: idle → launch (from sun surface toward earth, expanding blob: a mesh (icosahedron deformed by noise shader) plus particle spray) travels (speed ~ wind speed × 1.8), when reaching earth distance → impact: flash, magnetopause compression animation (scale factor animates down then relaxes), storm level spikes, aurora brightens, audio boom. Then cool-down. Button "Launch CME" + auto mode toggle.
-
UI/HUD:
- Header: title "MAGNETOSPHERE", subtitle.
- Top-right round buttons: mute, help, auto-orbit? Follow gossamer style.
- Panel: sliders — Solar wind speed, Storm level (Kp), maybe Time speed. Buttons: Launch CME, Pause, Belts on/off? Quality toggle. Stats: FPS.
- Legend for particle colors.
- Hint at bottom. Help overlay with explanations & shortcuts.
-
Audio: WebAudio graph built on first user gesture:
- wind hiss: filtered noise (bandpass), gain tied to wind speed.
- storm rumble: lowpassed noise + slow LFO, gain tied to storm level.
- chorus/whistler: occasional descending sine glissandi (scheduled), gain tied to storm.
- CME launch: whoosh (noise sweep), impact: boom (sine drop + noise burst).
- Master mute toggle.
-
Post: EffectComposer: RenderPass, UnrealBloomPass (strength ~0.9, radius 0.6, threshold 0.25), final ShaderPass: vignette + slight chromatic? Keep vignette+grain+tonemap tweak. Quality toggle adjusts pixel ratio and bloom.
Performance: keep particle counts moderate; shader-heavy but fine.
Let me now also think about "storm" parameter: user slider (Kp 0–9) + CME impact spike. Storm level modulates: aurora brightness/extent (oval expands equatorward), magnetopause compression (R0 shrinks), precipitation rate, belt intensity maybe, audio.
Wind speed slider: 250–800 km/s mapped to scene speed; modulates solar wind particles velocity + hiss pitch + dynamic pressure (slight compression too).
Alright, let me write the file. I'll write it in ~5 chunks with heredocs. I must be careful: inside heredoc with 'EOF' quoted, no shell interpolation — safe for $ and backticks (JS template literals).
Let me draft the full code mentally then write it.
Structure of index.html:
- head: meta, title "Magnetosphere — Earth's shield vs the solar wind · Three.js", CSS (glass HUD similar style to gossamer but space-teal palette).
- body: #app container, HUD elements (header, topRight buttons, control panel, legend, hint, help overlay, CME banner?), importmap, module script.
Module script outline:
Constants: RE=6, SUN_POS, etc.
GLSL noise lib string injected into shaders.
Renderer/scene/camera/controls setup.
buildSky(), buildSun(), buildEarth() (returns group with earth mesh, cloud mesh, atmosphere), buildDipole() (group + field lines + pulses), buildShrouds() (magnetopause, bowshock meshes w/ shader), buildAurora(), particle systems (wind, precip, belts), CME.
State object: { windSpeed: 420 (km/s display), wind01, storm: 2 (Kp), stormTarget, paused, muted, autoOrbit, time, timeScale }.
Audio module object with init(), setWind(), setStorm(), cmeLaunch(), cmeImpact(), toggleMute().
Update loop: dt clamp; update earth rotation, dipole wobble, field line pulses, wind particles, precip, belts, aurora uniforms, shroud uniforms & scale animation (compression), CME state, audio params, HUD stats, composer.render().
Also raycast? Not needed. Interaction: orbit controls + sliders + buttons + keys (space=pause, C=launch CME, M=mute, H=help, B=belts toggle?).
Physics helper functions:
For particle deflection: given position p (relative earth center), compute r=|p|, theta = angle between p and -X axis. Rb = mpRadius(theta, R0_bow). s = r - Rb. If s < 0 (inside) or within influence zone s < infl: compute normal ≈ normalize(p) (radial approx) — better normal: gradient of F(r,θ)=r - Rb(θ). Numerically gradient: sample. Cheap: n = normalize(p) works okay-ish, but near nose the radial direction from Earth center ≈ normal anyway. The flanks: radial is decent approximation. I'll use radial + slight tangential smoothing: v_new = v - (v·n)n * f + n * push, with f depending on penetration. Then re-normalize speed to wind speed (mostly) to keep flow: actually let speed vary a bit, clamp.
Respawn: x > 130 → respawn at x=-155 with random yz in disc radius 78; r < RE*0.95 → treat as absorbed → respawn upstream (or if within polar cap angle, kill and bump aurora).
Wind particle rendering: THREE.Points with shader: color pale blue-white, size ~1.2-2.2px attenuated, alpha by speed streak? Keep round soft sprites. Add slight stretch? Skip stretching for simplicity; motion reads fine.
Counts: wind 6500, precip 900, belts 1500. All updated on CPU per frame (positions Float32Array). 8900 particles CPU update — fine.
Dipole field lines: 14 L-shells? Lines as THREE.Line with vertexColors fading near Earth. L values: 1.6, 2, 2.5, 3, 3.6, 4.3, 5, 6, 7, 8. Sample λ from -λmax..λmax (line reaches Earth at r=RE → cos²λ = 1/L → λmax = acos(1/sqrt(L))). 120 points per line. Also rotate: dipole group tilted 11°, slowly precessing? The dipole rotates with Earth (tilt fixed relative to Earth axis): give dipGroup.rotation.z = tilt around z, and rotate earth spin; field lines attached to dipGroup rotate slowly. Wind direction stays +X world.
Pulses: 220 pulses, each: L index, λ phase moving from one foot to other and back (bouncing), ψ angle per line (line's meridian plane). Store per line its points array (in dipole frame) for pulse interpolation: pulse pos = interp on polyline at param t (0..1). Brightness peaks near equator? Fine.
Belts: per particle: L, ψ (drift angle), λ amplitude λm (mirror latitude ~ up to λmax-ε), bounce phase, bounce freq ∝ 1/L? Position in dipole frame: r = L·RE·cos²λ(φ); λ(φ)= λm·sin(φ). coords: x_d = r cosλ cosψ, y_d = r cosλ sinψ, z_d = r sinλ. (dipole axis = z_d). Then transform to world: apply dipGroup quaternion/tilt each frame — cheaper: precompute rotation matrix for tilt (constant), apply in JS: world = M_tilt · local. Also dipGroup rotates slowly around Earth axis — include time-varying rotation matrix. Simplest: compute matrix from dipGroup each frame (updateMatrixWorld), and for each particle p_world = p_local.applyMatrix4? Too slow-ish for 1500 — it's fine: 1500 matrix mults per frame is nothing. Use a Matrix4 and manual apply (x,y,z).
Precip: spawn at cusp lat ~ magnetic 72°: in dipole frame, ring at r ~ RE3? Particles ride field lines: choose L in [4,7], ψ random, phase t from 0..1 along line from equator to foot; accelerate. When t reaches foot (r<=RE1.02) → respawn. Draw with additive points, color green-white. Spawn rate tied to storm: fraction of particles active = f(storm). Implement: each particle has life cycle; speed ∝ wind; update t += dt*rate; hide some by setting alpha 0? Use per-particle color attribute scaled by activity; update colors occasionally. Simpler: always run all, but modulate global material opacity & aurora brightness by storm. Plus scale respawn positions toward poles with storm (oval expands). OK.
Aurora geometry: build curtain: for each of N=180 segments around oval circle (magnetic latitude θa from dipole pole, radius angle ~ 19° - storm expand), ribbon from r0=RE1.01 up to r1=RE1.55, with V vertical subdivisions (say 6) → grid strip; indices. uv.x = angle/2π (for noise rays), uv.y = height 0..1. Shader: color = mix(green, purple/red, h) * rays(noise(uv.x*freq + t)) * verticalProfile(h) * storm; additive, doubleside, depthWrite false. Also slight radial waviness: offset vertices in vertex shader with noise(uv.x, t) for sway. Two meshes (north & south) parented to dipGroup so they track magnetic poles; south flipped color slightly (same fine).
Magnetopause shader: fresnel rim glow: intensity = pow(1-|dot(N,V)|, 2.5); color teal-cyan; plus subtle noise shimmer + compression uniform (uCompress brightens nose). Bow shock: similar, more transparent magenta-ish/blue. Transparent, additive, depthWrite false, side: DoubleSide? FrontSide with rim fine; use DoubleSide for safety.
Sun shader: sphere radius 10 at (-150, 22, 0); fragment: fbm granulation + limb darkening, emissive yellow-white. Corona: THREE.Sprite? Use plane billboard 90x90 with shader: radial falloff + angular streaks (noise rays rotating slowly), additive; always faces camera (use Sprite with SpriteMaterial custom? SpriteMaterial doesn't support custom shader easily — use a Mesh with onBeforeRender lookAt camera, or in render loop: corona.quaternion.copy(camera.quaternion)). Do that.
Also secondary glow sprite for general bloom.
Post: bloom threshold ~0.2, strength 1.0, radius 0.55. Grading pass: vignette + slight teal-orange grade + film grain subtle.
HUD: title "MAGNETOSPHERE · 034", status line showing solar wind km/s & Kp & CME status. Panel sliders: WIND (250–800), STORM (0–9), TIME (time scale 0.25–3)? maybe "FLOW" time scale. Buttons: 🜨 Launch CME, ⏸ Pause, 🔊/🔇, ? Help, quality toggle, stats FPS. Legend: colored dots: — solar wind (pale cyan), — precipitating particles (green), — inner belt (amber), — outer belt (cyan), — field lines (teal). Help overlay describing physics + controls. Hint bottom: "drag to orbit · scroll to zoom · C = launch CME · space = pause".
Colors palette: deep space bg #04060d; accent: #6ee7ff (cyan), #ffd166 (amber), green #7dffa8, violet #b388ff.
Audio details:
- ctx, master gain 0.9 → destination, mute toggles master gain 0.
- windNoise: buffer source white noise loop → bandpass(400–900 Hz, Q~0.7) → gain g1. setWind(01): freq 300+90001, gain 0.05+0.1401.
- rumble: noise → lowpass 90Hz → gain g2; storm: gain 0→0.35 * (storm/9)^1.5; plus slow LFO on gain (use setTargetAtTime periodically with random? simpler: LFO oscillator → gainNode.gain via gain mod? Use oscillator → GainNode with small gain connecting to param). Keep simple: schedule updates per frame with linearRampTo? Just set value each frame via setTargetAtTime with timeConstant 0.3 — fine.
- chorus: every 4–9 s (random), if storm>1: descending whistle: osc sine freq 4000→800 exp over 1.2s, gain envelope 0.0→0.05→0, sometimes 2-3 overlapping; probability ∝ storm.
- cmeLaunch(): whoosh: noise → bandpass sweep 200→1800 over 1.5s, gain env 0.25.
- cmeImpact(): boom: sine 70→30 Hz 1.2s gain 0.5 env + noise burst lowpass 200.
CME visuals: group at sun: mesh icosphere radius ~6 deforming (vertex noise, additive orange) + point spray ~300 particles trailing; travels from sun toward earth along +X slightly arced; scale grows 6→26; when x > -40 → impact at bowshock: flash (uniform uFlash on shroud + screen flash via grading pass uFlash), camera shake small, storm target spikes to ~7.5, magnetopause compress animates R0 factor to 0.72 then spring back over ~12s; CME fades. Auto mode: launch every ~45–75s random. Button triggers too. Cooldown 8s.
Earth procedural shader — core fragment:
I'll craft:
float c = fbm(n*1.9)*0.6 + fbm(n*4.7)*0.4; float land = smoothstep(0.52,0.56,c);Then latitude = abs(n.y adjusted by tilt? Use object-space normal so texture rotates with earth: use varying of position normal in object space — the mesh rotates so object-space normal sticks to surface. vNorm = normalize(normalMatrix*normal) is view... Instead pass object-space normal as varying directly (attribute normal) — use for coloring; and world normal for lighting: compute worldNormal = normalize((modelMatrix * vec4(normal,0)).xyz) in vertex.Lighting: sunDir uniform (world). diff = clamp(dot(wN, sunDir)); terminator smooth; night = smoothstep(0,-0.15). City lights: lights = land * night * step(noise hi-freq) * warm color. Ocean spec: spec = pow(max(dot(reflect(-sunDir,wN), viewDir),0), 60) * oceanMask * day. Ice: lat>0.83ish + noise → white. Land color: mix by latitude & moisture noise: green→desert tan at |lat|~0.25→boreal darker. Good enough.
Clouds: sphere RE1.02, alpha = smoothstep(0.45,0.75, fbm(n3.1 + t*wind)) etc., white with same diffuse lighting; transparent; depthWrite false; rotating faster than earth slightly.
Atmosphere: sphere RE*1.12, BackSide, fresnel blue additive.
Star sky: sphere radius 900, BackSide, shader: dir → hash grid stars: 3 layers different scale/density with twinkle by time; milky way: band around plane with normal mwN (choose e.g. normalize(0.3,1,0.2)): d = abs(dot(dir, mwN)); band = exp(-dd18); color = band * fbm(dir*5) * palette (bluish-purple); plus faint red/green airglow? skip. Also dim second band crossing? fine.
Field line material: LineBasicMaterial vertexColors, additive, transparent, opacity 0.55. Build color gradient along line: teal at equator → dim near feet.
Pulses shader points: aSize attr? Use uniform size; per-pulse brightness via color attr updated? Pulses move along lines — compute pos each frame in JS (dipole frame → world via matrix). 220 fine.
Let me also add slight "breathing" of whole magnetosphere with wind pressure: R0_dyn = R0_base * (1 - 0.12*wind01) * compressAnim.
Bowshock nose: R0_bs = R0_dyn * 1.28 + 2.
Shroud geometry regenerated when R0 changes? Instead build geometry with unit R0=1 shape and scale non-uniformly? Shape isn't self-similar under uniform scale... Actually mpRadius scales linearly with R0 for fixed α → uniform scale of the whole mesh works: mesh.scale.setScalar(R0). Build with R0=1. Nose at distance 1 along -X. Then scale = R0_dyn.
For wind deflection use analytic Rb(θ) with current R0_dyn*1.28 (bow). Particles pushed when r < Rb + margin.
Alright — also camera start: (36, 26, 64) looking at origin; controls damping; minDistance 12, maxDistance 400; autoRotate optional (button) slow.
FPS counter + particle count in stats. Quality toggle: cycles High/Med/Low → pixelRatio min(devicePixelRatio,2)/1.5/1, bloom on/off, wind particle count (just draw range). Keep simple: toggle adjusts pixel ratio + bloom enabled.
Keyboard: Space pause, C cme, M mute, H help, A autorotate, 1/2/3 quality? Keep documented.
Resize handler.
Now the HTML/CSS: follow gossamer's aesthetic: glass panels, round buttons top-right, bottom-left panel, center bottom hint, help overlay, top-left header. Palette: --cyan:#7fd8ff? Let's define: --acc:#6fd3ff; --amber:#ffc46b; --vio:#b388ff; --green:#7dffa8.
Also a small "CME inbound" alert banner top-center that appears when CME active (flashing red/amber). Nice touch.
Legend panel bottom-right.
Let me now write all this code. I'll aim for a complete correct implementation; I must be very careful with shader code syntax and JS logic since I can't easily run a browser. Maybe I can sanity check with node? Three.js module can run in node? Hard without DOM. I'll at least run a syntax check of the JS by extracting the module script and running
node --check. That catches parse errors. GLSL I'll review carefully.Plan file writes:
- Part 1: HTML head + CSS + body HUD + importmap (no script).
- Part 2: script start: imports, constants, noise GLSL, renderer/scene/camera/controls, sky, sun, earth.
- Part 3: dipole lines+pulses, shrouds, aurora.
- Part 4: particle systems (wind, precip, belts) + CME.
- Part 5: audio, UI wiring, update loop, resize, end script + closing HTML.
Each write via
cat > partthen concatenate, or append directly withcat >> /space/.../index.html <<'EOF'. Writing directly to /space via heredoc should be fine (s3fs). But safer: build in /tmp then copy over (batch ops on slow s3fs). I'll build at /tmp/mag/index.html in chunks, thenmkdir -p /space/demos/034-magnetosphere && cp.Let me write the code now. I'll be meticulous.
Detailed shader strings:
NOISE GLSL (value noise 3D + fbm), used in several shaders:
Note: name collision with three.js built-in hash? In ShaderMaterial, three injects common chunks only if included; custom shaders are standalone — fine. But
hash13etc custom names fine.Sky shader:
vertex:
varying vec3 vDir; void main(){ vDir = position; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }(position on big sphere, direction = normalize(position)).fragment:
Alternative standard trick: use
fract(sin)per-cell with direction*scale and distance to cell point — scale on direction vector warps. Simpler robust approach: stars via 2D? No — do 3D cells:Brightness with random magnitude:
float mag = pow(hash13(i+3.1), 8.0)*2.2+0.25;twinkle:0.75+0.25*sin(uTime*(1.0+4.0*h.x)+h.y*6.28). Color tint: mix(blue-white, warm, h.z). Sum 3 layers scales 24, 46, 80 with thresh 0.55/0.6/0.7... density: m = step(0.6, ...) means 40% cells have stars. OK.Milky way: band plane normal nmw = normalize(vec3(0.25, 1.0, 0.35)); d2 = dot(dir, nmw); band = exp(-d2d214.0); structure = fbm(dir6.0 + vec3(3.0)); lane darkening: subtract fbm(dir9.0)band0.5; color = band*(0.35+0.65*structure)*vec3(0.65,0.7,1.0)*0.5 + hints of pink: + band^2 * vec3(0.5,0.25,0.45)fbm(dir3.0). Multiply milkyway by (1 - stars a bit)? Fine as additive sum. Overall brightness moderate (bloom threshold above).
Sun surface shader: uniforms uTime. varying vN (object normal), vPos. frag:
viewDir varying: vView = normalize(cameraPosition - worldPos) in vertex (cameraPosition available in ShaderMaterial as built-in uniform? yes
cameraPositionis built-in).Corona shader (billboard plane): uv in [-1,1]; r=length(uv); angle=atan;
discard r>1.
Earth shader: as designed. Fragment details:
cities threshold 0.82 with fbm*26 — fbm returns ~0..1 centered 0.5; step(0.82) gives sparse blobs — okay.
Cloud shader: similar alpha:
transparent, depthWrite:false.
Atmosphere: BackSide sphere:
additive.
Hmm for BackSide rim: standard atmosphere glow: use dot(viewDir, normal) with FrontSide shell slightly larger and additive with pow(1 - dot)... I'll use FrontSide sphere RE*1.14, intensity = pow(0.72 - dot(V,N), 2.2)? The classic: glow at limb = pow(1.0 - dot(V,N), 3) for BackSide. Use BackSide version — safe: renders behind earth? BackSide of bigger sphere is visible around limb (front hemisphere's back faces? No—) Standard approach: sphere bigger than planet, material.side = BackSide, additive, depthWrite false; you see the far side shell as a halo ring around the planet. Yes that's the classic glow halo. intensity = pow(max(dot(N, V),0), ...) where for back faces N points away... let me just use: float rim = pow(1.0 - abs(dot(normalize(vWorldN), V)), 3.5). On the far-side shell, around the limb abs(dot) ~ 0 → bright; in front of planet? BackSide means we render back-facing triangles = the far hemisphere shell, which is occluded by earth where earth is (depth test against earth; earth drawn first, opaque) → halo ring only.
Dipole lines: build in JS:
Earth spin axis tilt: earthGroup.rotation.z = 23.4°. Dipole tilt relative to earth axis 11°: nest dipGroup inside earthAxis group? Earth rotates about its axis; dipole fixed to earth → put dipGroup inside earthTilt group and rotate that? If earth spins, field lines rotating around the tilted axis creates nice wobble. Structure:
Hmm but belts & aurora in dipole frame; putting them under dipGroup handles transforms automatically via matrixWorld — and I don't need manual matrix math for rendering; only for pulse/belt positions computed in JS I can compute in dipole local space and add Points as children of dipGroup with positions in local space! Yes — make wind & precip & belt Points children of appropriate frames:
- belts Points: child of dipGroup, positions computed in dipole frame.
- pulses Points: child of dipGroup, local coords.
- aurora meshes: children of dipGroup (positioned along dipole z axis). In dipole frame, let dipole axis = local Z? Three.js default "up" is Y. Let dipole axis = local Y (so oval circles lie in XZ plane of dipole frame at high |y|). Earth spin axis = Y of earthSpin. Tilt dipole by 11° about Z: dipGroup.rotation.z = 11°. Then dipole "north pole" = +Y tilted.
Field line in dipole frame: meridian plane containing Y axis: parameterize with azimuth ψ around Y: point = (r sinλ'?? ) Let me define magnetic latitude λ (from equator plane XZ): position: radial distance r = L RE cos²λ; horizontal (equatorial) component = r cosλ; vertical = r sinλ. With azimuth ψ: (x,y,z) = (r cosλ cosψ, r sinλ, r cosλ sinψ). Wait that puts axis along Y: vertical = y = r sinλ. Yes: y is dipole axis. Good.
Field lines: for each L and ψ in {0, 60, 120, 180, 240, 300} (6 meridians per shell? too many lines) — choose shells Ls = [1.7,2.2,2.8,3.5,4.3,5.2,6.2,7.4] with ψ count 4 for inner, 6 for outer? Simplify: ψs = [0,45,90,135,180,225,270,315] for all, but that yields 8×8=64 lines × 100 pts = 6400 verts — trivial. Actually lines should look good: color teal, brighter near equator? vertex color alpha can't vary in LineBasicMaterial (no per-vertex alpha, but color can darken). Use vertexColors with darker near feet. Opacity 0.5 additive.
Aurora oval: circle at magnetic latitude λa = 90°−19° = 71°... using our param: points at constant "colatitude" from +Y axis: radius_ring = RE1.02 * sin(colat), y = RE1.02cos(colat), colat ≈ 19° (+ storm expansion). Curtain extends outward-up: from sphere surface up along radial direction to RE1.5. Build ring strip: for i in 0..N: angle φ; for j in 0..M: h in 0..1; pos = (dirXZ* sin(colat + hspread)?) Actually curtains follow field lines upward — approximate radial: pos = normalize(ringDir) * (RE(1.02 + 0.5h)) with slight equatorward tilt. uv=(φ/2π * repeat, h). South: mirrored at -Y.
Shader aurora:
additive, doubleSide, depthWrite false.
Wait uv.x range: I'll set uv.x = φ/2π * 40 (repeat rays 40× around) so noise is continuous? Noise continuity at seam: uv.x36 not periodic → seam. Mitigate: use φ in 3D noise: vec3(cos φ, sin φ, ...)scale. Do: rays via fbm3(vec3(cosA, sinA, 0)8 + vec3(0,0,t)) and striations via sin(40A + noise8 + t)? sin(40A) is periodic in A=φ (40 cycles over 2π) → seamless. Use A = atan? I have uv.x = φ/2π; striation phase = sin(uv.x2π40? ) → uv.x251.3. fine: sin(vUv.x251.33 + w8 + uTime2.2). And pass cos/sin via uv2? Compute A = vUv.x*6.2831853 in shader then vec3(cos(A), sin(A), z)*scale for noise — seamless.
Shroud shader:
additive, DoubleSide, depthWrite false.
Geometry: revolution of mpRadius with R0=1: for iθ in 0..IT(48), jφ in 0..JP(64): θ= i/IT * π0.98 (avoid exact π pole), dir=(-cosθ, sinθ cosφ, sinθ sinφ), r=Math.pow(2/(1+cosθ), 0.62). pos = dirr. Wait θ measured from -X axis: at θ=0 → dir=(-1,0,0) nose toward -X at distance r=1·(2/2)^α=1. At θ=π: r=2^0.62≈1.54 radius tail cylinder-ish; cross radius = r sinθ → 0 at θ=π: closes into a point behind? That makes the tail pinch to a point at x = -r cosθ·? x = -r cosθ → at θ=π, x=+1.54. Hmm: shape closes at tail at x=+1.54 on +X axis — a teardrop closed both ends. Real magnetotail is open cylinder. Visually a closed teardrop is fine and reads well. Actually at θ=π the radius shrinks to zero → the surface pinches shut at x=+1.54·scale... The "tail" look would be a pointed end — like a comet-ish droplet. Hmm, real Shue: r = R0(2/(1+cosθ))^α valid θ∈[0,π], at θ=π gives r = R0·2^α — finite cross-section radius r·sinθ → sinπ=0. So yes it closes to a point on the tail axis. The Shue surface indeed closes at θ→π?? No — Shue's model: at θ=π (tail), r·sinθ... The formula gives finite r but the surface radius r sinθ → 0. Actually Shue's function describes a surface that closes at the distant tail — it's a known artifact; the model is only used for θ < ~120°. For visuals, pinch at far tail is okay but might look odd as a spike. Alternative: clamp θ max to ~2.6 rad (149°) and leave the tail open (no cap) — open cylinder-ish flare fading out; additive transparent so open edge acceptable, or fade alpha near rim θ→θmax via vUv.y. I'll generate θ∈[0.06, 2.7] and fade alpha by smoothstep at the open end. Tail open looks like a windsock — good.
Scale: mesh.scale.setScalar(R0dyn). Nose world at x = -R0dyn. R0 base = 21 (3.5 RE). Bowshock scale = R0dyn*1.32 + 1.5, its α maybe 0.55 (blunter). Use same geometry generator with different α? Make geometry(α) function.
Wind deflection: use Rb(θ) = bowScale · pow(2/(1+cosθ), αb). Compute per particle: r=|p|; cosθ = (-p.x)/r (angle from -X axis). s = r - Rb. influence zone: s < 6 (world units). Normal: approximate n = normalize(p - axisPoint)? Use radial from origin: n = p/r (works okay: at nose radial ≈ surface normal; at flanks slightly off but acceptable visually with smoothing). Deflection: if s<6: t = clamp(1 - s/6, 0, 1) (0 outside → 1 at surface/inside); vn = v·n; if vn<0 (moving inward) v -= nvn(1.6t); v += n * tt * 26 * dt * wind01? then renorm: v.setLength(windSpeedScene*(0.85+0.3t))? Also add slight turbulence near flank. Plus if s < 0.5 hard push out: p += n(0.5-s)0.3. This creates smooth flow around. Particles exactly aimed at Earth: they get deflected by same mechanism since Earth inside shroud... A particle heading dead-on: symmetric push weak. Add Earth collision: if r < RE1.0 → respawn (absorbed). Some polar-cusp capture: skip — precip system handles visuals.
Wind particle spawn: x=-155, disc radius 76; also allow respawn when x>125 or r>170. Velocity: mostly +X with slight random; speed = map(windSpeed 250..800 km/s → 14..46 units/s). Represent per particle speed factor variance 0.85..1.2.
Wind rendering shader points: uniform uSize, color pale #bfe6ff, opacity 0.75 additive, twinkle slight. Size attenuation: gl_PointSize = uSize * (300/dist). Soft circle via gl_PointCoord dist. Points count 6500; positions Float32Array; per-frame CPU update 6500 — ok.
Precip: child of dipGroup? The cusps: particles enter near magnetic poles at high latitude: in dipole frame spawn at L in [3.5, 6.5] at λ ~ 30..60° random ψ, descend along field line to foot. Path: given L, λ(t) from λstart → λfoot=λmax(L)=acos(1/sqrt(L))? At r=RE: cos²λ = 1/L. Let t: 0→1, λ = mix(λstartsign, λfootsign, ease-in t accelerating), speed: t += dt * rate(L) * (0.5+storm0.1). When t≥1 → respawn (and flash aurora? just respawn). Choose sign (north/south hemisphere). Position formula as belts. Color: greenish-white, size small. Count 900. Points child of dipGroup → automatic tilt/rotation. opacity modulated by (0.25 + storm/90.75) via uniform uAmp; also particle alpha by height (fade near foot).
Also add "ring current" glow? skip.
CME: group:
- blob: icosahedron(1, 3) with ShaderMaterial: vertex displaces by fbm(position*2 + t)*0.5, fragment: orange fbm emission, fresnel edge hot; additive.
- spray: 260 Points trailing (positions updated: emitted from blob center backward cone, life). Path: start at sun pos + dir12, velocity dir * (windScene2.2 + 30) accelerating slightly; scale = 4 + travelled0.10 (cap 30). When distance to earth < R0dyn1.32+6 → impact. Impact: uFlash shrouds =1 decay exp; grading uFlash 0.5 decay; stormTarget = min(9, storm+5.5); compress impulse: comp = 0.7 → spring toward 1 with damped oscillation: c'' = k(1-c) - d c'. R0dyn = R0basewindFactorcomp. After 6s stormTarget decays to slider value. CME mesh fades (opacity→0 over 2s) then state idle, cooldown.
Auto CME checkbox? Add small toggle "Auto CME" on panel maybe. Keep: auto every 50-90s enabled by default? Could annoy; enable with button toggle. I'll add
Akey + small toggle button in panel row.Audio as planned. Keep implementation compact but real.
Grading pass shader:
Note ShaderPass needs uniforms merged; write custom shader object.
HUD elements:
- #hudHead: h1 "MAGNETOSPHERE", p "earth's magnetic shield vs the solar wind", #status line:
wind 412 km/s · Kp 3.0 · field stableupdates. - #topRight: buttons: 🔊 (mute), ❔ (help), maybe 🎥 auto-orbit.
- #panel (bottom-left): sliders WIND(250-800), STORM(0-9), FLOW (time scale 0.25-2.5). row of buttons: "☀ Launch CME", "⏸ Pause", "Auto" toggle; second row: #quality toggle + #stats.
- #legend (bottom-right glass): colored dots + labels: solar wind ions, precipitating electrons, inner belt protons, outer belt electrons, aurora, magnetopause.
- #cmeAlert top-center banner hidden by default: "⚠ CME INBOUND".
- #hint bottom-center: controls text; fades after 12s.
- #help overlay: full-screen glass with explanation + shortcuts; toggle.
Also a small "back to index" link? Gossamer probably doesn't have; check quickly? Not necessary. I'll add tiny footer link in help overlay. Eh — index pages exist; demos standalone. Skip.
Update loop detail ordering:
FPS: EMA of rawDt → text each 0.5s.
Careful with pause: if paused, still render + controls.
Now — writing ~1300 lines via heredocs. Node syntax check: extract content → node --check won't handle
importin .js? node --check with .mjs treats as module — import.meta? I don't use import.meta. Save as .mjs →node --check file.mjs— but the imports reference 'three' bare specifier; --check only parses, doesn't resolve.GLSL can't be validated without a GL context; I'll be extra careful: declare all varyings, no reserved words, loop consts fine in WebGL2 (three r170 uses WebGL2 → GLSL ES 3.0 under the hood but ShaderMaterial code is written in ES 1.0 style with gl_FragColor etc., three transpiles? Actually three r163+ with WebGL2 compiles GLSL1 shaders by prefixing; gl_FragColor still works via compatibility defines. Yes—three handles it.)
One caution: in ShaderMaterial,
cameraPositionuniform is available in vertex AND fragment? cameraPosition is a built-in uniform in vertex shader (provided by three). In fragment, three also injectsuniform vec3 cameraPosition;? three injects common uniforms into fragment prefix: yes — WebGLProgram prependsuniform vec3 cameraPosition;to fragment shaders (I recall it does: prefixFragment includes cameraPosition... let me verify: In three's WebGLProgram.js, fragment prefix includes 'uniform mat4 viewMatrix;' and 'uniform vec3 cameraPosition;' — yes it does). Good.vUvname conflicts? ShaderMaterial doesn't auto-declare uv unless geometry has uv and you use it via attribute — three injectsattribute vec2 uv;in vertex prefix ALWAYS? It injects common attributes (position, normal, uv) in vertex prefix. So in vertex shader I can useuvdirectly when geometry has uv attribute. But declaringvarying vec2 vUv;myself is fine; name vUv not conflicting.Careful: geometry for aurora curtain — BufferGeometry with position, uv, index.
Also normals for shroud: computeVertexNormals on indexed geometry — fine.
Points shader: attributes: position; custom attribute aSeed maybe for twinkle/size variance. gl_PointSize = uSize * aScale * (140.0 / -mvPosition.z). Clamp fine.
Wind shader fragment: circular soft:
float d = length(gl_PointCoord - 0.5); float a = smoothstep(0.5, 0.12, d);Additive blending with transparent true, depthWrite false, depthTest true.
Belts colors per particle (attribute color) — PointsMaterial-like custom shader with vertexColors equivalent: attribute vec3 aColor.
OK, precipitation: attribute aSeed; alpha near foot: compute in JS per particle when updating: aAlpha array update? Cheaper: color attr scaled by fade each frame? That doubles CPU writes. Alternative: compute fade in shader from position length: r = length(position) (dipole frame), foot at RE: alpha = smoothstep(RE1.0, RE1.25, r). Nice — free in shader.
Belt bounce: λ = λm * sin(phase); phase += dtspeed/L. drift ψ += dt(0.25/L^2 * (1+..))? Electrons drift eastward... just sign per population: inner +, outer −. speed modest.
Precip mirror path: given L, sign, ψ, t param. λ = λ0 + (λfoot−λ0)tt (accelerate). λ0 random 0.3..0.9 rad? λfoot = acos(1/sqrt(L)) (r=RE). At t≥1 respawn. t += dt*(0.35 + 0.5rand)/L? Make fall ~2-4 s: rate = (0.30+0.25aSeed)(1+storm*0.08).
Also pulse flow along open lines? Keep pulses on dipole closed lines.
Numbers check Earth radius RE=6: city lights, clouds RE1.02=6.12, atmosphere shell 6.9. Aurora from 6.12 to 9.1. Field lines LRE: L=1.7→10.2 to L=7.4→44.4 equatorial. Magnetopause nose 21 → some field lines (L>5) would extend beyond magnetopause (unphysical but lines are schematic; choose Ls up to 4.6 → 27.6 slightly beyond nose 21 on equator — hmm. Keep Ls = [1.6,2.0,2.5,3.0,3.6] → max equator 21.6 ≈ nose. Fine visually; plus a few "open" lines? skip.)
Wind box: spawn x -155 disc radius 76 — covers shroud (tail radius ~ 1.521... at θ=90°, r=2^0.62*21≈32; bow ~1.32× → 42). disc 76 fine.
Sun at (-150, 22, 0) radius 9; corona plane 110×110.
Camera fov 55; far 3000 (sky 900). near 0.1.
Bloom: threshold 0.35, strength 1.15, radius 0.6.
Grading after bloom.
renderer.toneMapping = ACESFilmic? With additive shaders ACES darkens; demos often use NoToneMapping with bloom. I'll use ACESFilmic exposure 1.1 — actually bloom pass output + ACES gives nice rolloff. Hmm UnrealBloomPass works in linear; fine either way. Choose ACESFilmic for earth shading quality.
Now HUD copy: nice scientific flavor. Title: "MAGNETOSPHERE". Subtitle: "earth's invisible shield · solar wind · auroras · van allen belts". Status line dynamic.
Legend items with hex dots:
- solar wind — #bfe6ff
- precipitating electrons — #9dffc0
- inner belt protons — #ffb066
- outer belt electrons — #7fd0ff
- magnetic field lines — #4fd8c8
- auroral ovals — #7dffa8 (maybe skip)
Help overlay content: what you're seeing + controls list + physics notes (dipole tilt 11°, Shue magnetopause, mirror bounce, drift). Shortcuts: Space pause · C CME · M mute · A auto-orbit · H help.
Also add auto-orbit button in topRight (🎥? use ⟳).
Edge: mobile touch — sliders work; hint text adapt? Keep.
Let me now also double check
mpRadiusat θ=0: (2/(1+1))^α = 1 ✓.Bow shock: geometry αb=0.55, scale = R0dyn*1.30+2.0.
Wind deflection uses Rb with αb and scaleBs.
Now audio implementation sketch:
Whistler scheduling in loop: chorusTimer -= dt; if <0 && storm>1.5 { whistle(); maybe double; chorusTimer = 3 + rand*7 * (1.5 - storm/12) }.
whoosh: noise buffer source (need access to buffer — store this.noiseBuf) through bandpass with freq ramp 150→1600, gain env 0.001→0.35→0.001 over 1.6s. boom: sine osc 90→28 over 1.1s gain 0.6→0.001 + noise through lowpass 180 gain 0.5 decay 0.8 + slight delay rumble spike.
All guarded if ctx null (audio only after gesture; init on first pointerdown + on any button).
Now UI wiring code and help toggle.
Quality:
state.quality0=High,1=Med,2=Low: apply(): pixelRatio = [min(dpr,2),1.5,1][q]; bloomPass.enabled = q<2; renderer.setPixelRatio; composer.setSize. Button label cycles "HQ/MQ/LQ".Autorotate: controls.autoRotate = true, speed 0.6.
Stats:
60 fps · 8.9k particles.CME alert banner show/hide with class.
Pause button label toggles ⏸/▶.
Also pressing any control initializes audio (button click → AudioSys.init(); resume()).
Slider events: input → state.windKmh etc; wind01 = (v-250)/550; AudioSys.setWind(wind01). Storm slider → state.stormTarget (unless CME spike recently: slider sets base; effective = max(base, spike) — implement state.stormBase (slider), state.stormSpike (decays), state.storm = clamp(base + spike,0,9)? simpler: state.storm smooth-follows state.stormTarget where target = max(slider, spikeVal). spike decays 0.5/s after impact. status shows Kp = storm.toFixed(1).
Compression: state.comp (1 baseline); spring: target 1; on impact set comp=0.68 and compVel=0; compAcc = 60*(1-comp) - 8compVel... tune: k=90, damping 7. R0dyn = R0 * (1 - 0.10wind01) * comp.
Aurora uniforms uStorm = storm; also oval radius expands: scale mesh? Rebuild? Use mesh.scale: expanding colat from 19°→24°: scale x/z by sin ratio ≈ scaleXZ = sin(colat)/sin(19°); and y = cos(colat)/cos(19°)... scale on mesh non-uniform: s = 1 + storm/90.35: mesh.scale.set(s, 1+storm/90.05, s). Fine approximation.
Magnetopause color brightens with compression: uFlash & uOpacity up slightly.
Wind streaks appearance fine as points.
Let me also handle
prefers-reduced-motion? skip.Now — writing. I'll create /tmp/mag/index.html in 5 heredoc chunks. Watch out for
$in JS template literals — with quoted heredoc 'EOF' everything literal — good.Also careful that the heredoc content doesn't contain a line exactly equal to EOF. Use distinct delimiters: HTMLEOF1 etc.
Let me now compose the code fully.
Chunk 1: HTML + CSS + body.
CSS: space theme. Fonts: system. Colors: --cyan:#7fd8ff, --teal:#4fd8c8, --amber:#ffc46b, --green:#7dffa8, --vio:#b79bff, bg deep #030510.
Layout elements (ids): app, hudHead (h1,p,#status), topRight (btnMute 🔊, btnOrbit ⟳, btnHelp ?), cmeAlert, panel (sliders wind/storm/flow + buttons: btnCME ☀ Launch CME, btnPause ⏸, btnAuto auto CME; row2: quality + stats), legend, hint, help overlay (hidden) with content + close.
Sliders labels: "SOLAR WIND", "STORM · Kp", "TIME FLOW". Value spans showing current value.
Panel markup pattern:
pbtn.on active style for Auto toggle.
Legend:
i { width:8px;height:8px;border-radius:50%;background:var(--c);box-shadow:0 0 6px var(--c) }
Hint: "drag to orbit · scroll to zoom · space pause · C launch CME · H help".
Help overlay sections: About (what is simulated), Controls, Shortcuts, "physics: Shue et al. magnetopause · dipole L-shells · mirror bounce & azimuthal drift · whistler chorus audio". Close button.
Chunk sizes must be manageable. Let me count roughly; fine.
Now write chunk 2 script: imports through earth build.
I need to be careful and write valid code. Let me write it now — full attention.
Renderer setup etc. Then NOISE string:
Sky:
Note starLayer smoothstep(0.16,0.02,dd): cell spacing 1.0 in p units; star point radius 0.16 — okay size; density step: step(1-density, hash) → probability density. Good.
Sun + corona:
wait vN is object normal; for sphere object normal == direction from center; world normal same (no rotation scaling) — fine: dot(normalize(vN), V).
- Commented
The previous run left demo #034 (Magnetosphere) unfinished and never deployed —
/tmp/mag/is gone and/space/demos/034-magnetospheredoesn't exist. Since this turn says "do as you please," I'll finish that work: build the complete demo, deploy it, and update the index. Let me start writing the self-contained demo file in chunks. - Created activity call execute_shell_command
- Created
*********************************************************************************th.
- Commented
******************************************************************************************************************************************th.
- Created activity call execute_shell_command
- Created activity call _checkTaskRunStatus
- Commented
***************************************************************************************************************************************************as.
- Created activity call execute_shell_command
- Created
Wait, there's a bug in the auroraGeometry: I declared
dx, dz, dybut never used them (computed tdx/tdy/tdz instead). That's harmless (unused vars) but fine. AlsodipolePointfor lambda near ±lamMax gives r = LREcos²(lamMax) = LRE(1/L) = RE ✓.Note: auroraGeometry has unused variables dx, dy, dz — harmless but let me clean later if time permits. Actually they're declared with const and unused — fine in JS.
Chunk 4: particle systems (wind, precip, belts) + CME. Then chunk 5: audio + UI + loop. Let me continue quickly.
- Commented
********************************************************************************************ME.
- Created activity call execute_shell_command
- Created
Note:
AudioSysandgradePassare referenced in chunk 4 but defined in chunk 5 — since these are only referenced inside functions called after everything is defined (launchCME called by user; updateCME in loop after gradePass defined), JS hoisting:AudioSysisconstdeclared later in the same module scope — temporal dead zone only matters if accessed before declaration executes. The animation loop starts after all declarations, so it's fine. Same for gradePass.Chunk 5: audio system, post-processing, UI wiring, main loop, resize. Let me write it now — quickly since time is at ~55%.
- Created activity call execute_shell_command
- Created
Now validate: extract the module script and run node --check. Also review some risky spots:
-
The
pointsMaterialvertex shader usesattribute float aSeed;— all geometries using it have aSeed ✓ (wind, precip, belts, spray — all have aSeed ✓). -
In
pointsMaterial,uAmpanduTime— spray uses defaults ✓. -
Fragment:
float a = smoothstep(...) * vTw * uAmp;thengl_FragColor = vec4(c * a * 1.7, a * ${opacity});— with opacity.toFixed(2) → "0.85" ✓. -
In wind update:
const Rb = bs * shue(Math.acos(clamped cosTh), BS_ALPHA)✓. -
auroraGeometry has unused vars dx/dy/dz — fine.
-
In chunk 3, I build
geowith all lines then copy slices into per-line geometries — works.stride=97✓ (N=96 → 97 points). -
linePtsordering matches slice ordering ✓ (same loop order). -
dipolePoint for pulses: t from 0..1 maps to pts array index — pts go from -lamMax to +lamMax ✓.
-
CME dir: SUN_POS.negate().normalize() → points from sun toward earth origin ✓.
-
cmeGroup.position.copy(cme.pos) — spray positions are relative to cmeGroup (local), blob at local origin ✓. Spray trailing:
- cme.dir * backmoves them behind along -dir... cme.dir points toward earth; trailing should be behind → minus dir ✓. -
In fragment shaders where I use NOISE + main: shroud uses fbm3 ✓ defined in NOISE ✓.
-
Earth shader: uses
uTimedeclared in uniforms but not used in fragment? It's declareduniform float uTime;in earth frag — I wrote "uniform vec3 uSunDir; uniform float uTime;" — wait in earthMat fragment I declareduniform vec3 uSunDir; uniform float uTime;? Let me check: I wroteuniform vec3 uSunDir; uniform float uTime;— yes. Used? uTime not used in earth frag — unused uniform declaration is fine (declared but unused compiles fine; also JS sets earthUniforms.uTime — exists in uniforms object ✓). -
Cloud frag uses uTime ✓ and uSunDir ✓ — both declared ✓.
-
Aurora vertex uses vnoise from NOISE ✓; uTime declared ✓. Fragment uses uStorm declared ✓, fbm3 ✓.
-
auroraUniforms shared between two materials — both reference same uniforms object; fine.
-
Sky:
starLayeruses uTime ✓ declared in fragment ✓. -
CME vertex shader: uses fbm3 ✓ from NOISE; uTime declared ✓. Note: displacement uses
normal— icosahedron normals normalized? IcosahedronGeometry(1,3) normals are unit ✓. -
gradePass ShaderPass: constructor accepts shader object with uniforms/vertexShader/fragmentShader ✓. textureID default 'tDiffuse' ✓.
-
composer.setPixelRatioexists on EffectComposer ✓ (it does in r170). -
Points shader:
attribute float aSeed;— but ShaderMaterial on three r170 with WebGL2: declaring attributes manually is fine (position/uv auto-declared; custom must be declared) ✓. Note: for belts material vertexColor=true branch declaresattribute vec3 aColor;✓ geometry has aColor ✓. -
Pulse material: no aSeed attribute — shader doesn't reference aSeed ✓ (separate pulseMat).
-
Points with ShaderMaterial:
gl_PointSizeset ✓. -
pulseGeo.attributes.position.needsUpdate— positions all zeros initially → first frame updates ✓. -
Issue: wind particles respawn check
rr < (RE*0.95)^2— inside earth respawn ✓. -
windSpeedScene()13..46 units/s; particle crossing 280 units in ~8s at 35 u/s ✓. -
Deflection: normal = radial from Earth center — at the nose (particle at (-21,0,0)) n=(-1,0,0) vs surface normal also -X ✓; v=(+s,0,0); vn = -s <0 → remove inward ✓ pushes +? push along n=(-1,0,0) → pushes back toward sun ✓ hmm — that decelerates nose-on particles and deflects them laterally only via radial component... At exactly nose-on, lateral = 0 and particle oscillates/pushes back — but numerical noise + the push term creates lateral asymmetry over time; plus
vy,vzinitial random ±0.75. Should be visually fine: flow slows near nose (stagnation point — physical!) and slides around flanks. Good. -
CME:
state.compVel += (90*(1-comp) - 7*compVel)*rawDt— spring, could oscillate; damping 7 vs stiffness 90: underdamped → nice wobble. rawDt not dt — runs even when paused; fine. -
auroraGeometry(sign): tilt = colat - signh0.10 — for south (sign=-1) tilt increases with height: mirror lean ✓. dy = cos(tilt)*sign ✓.
-
In HTML: status/element ids match JS ✓. btnOrbit initial class 'off' added ✓.
-
bindAudioon pointerdown with{once:false}— fine, init guarded. -
keydown ' ' → togglePause ✓.
-
Potential issue:
pointsMaterialfor spray: opacity 0.9 — used in templatea * ${opacity.toFixed(2)}→ "0.90" ✓. -
pulseMat fragment: uses uColor only ✓.
-
In updateWind I clamp cosTh — shue at theta=π: 2/(1+cosπ) → 2/0 → Infinity! Math.acos(-1)=π when particle exactly at +X axis downstream. cosπ=-1 → 1+(-1)=0 → division by zero → Infinity → Rb=Infinity → s=r-Inf=-Inf → t=1 → strong push outward — bad! Particles exactly on +X axis. Actually 1+cos(θ) for θ=2.6: cos=-0.857 → fine. For exact -1 only at exactly θ=π (measure zero, but floating point could hit near). cosTh clamped to [-1,1]; cosθ=-1 possible only if y=z=0 and x>0 exactly — improbable but let me guard: clamp cosTh to [-0.999, 1]. shue(acos(-0.999)) = (2/(0.001))^0.55 ≈ 65 → Rb = 6543 huge... particles near +X axis down-tail at r<65bs?? Wait: at θ→π, shue→(2/(1+cosθ))^α. With cosθ=-0.999: 2/0.001=2000^0.55 ≈ 65. bs≈30 → Rb≈1950 — way too big → any particle within r<1950 near tail axis gets max deflection — wrong! The tail of the shroud is open (geometry θ≤2.6), but the analytic deflection formula blows up beyond θ≈2.6.
Fix: clamp theta to max 2.55 for deflection: th = min(acos(cosTh), 2.55). Then Rb max = shue(2.55,0.55)=(2/(1+cos2.55))^0.55; cos2.55≈-0.830 → 2/0.17=11.76^0.55≈3.87 → Rb≈ 30*3.87≈116 — particles inside that radius near tail axis still get pushed... Down the tail axis (θ=π direction), Rb formula gives huge radius meaning "inside the magnetosphere" → t≈1 → push radially outward — that would eject particles flowing down the tail center! Real: solar wind doesn't flow down the tail center anyway (wake is rarefied), but our spawn fills all space; particles at θ>2.55 (deep tail region beyond x>0) flowing +X would get pushed away from axis — creating a hollow tail cavity — actually that's roughly physical-looking (tail lobes + low-density wake). But pushing radially outward from Earth near +X axis creates a visible "void cone" behind Earth — hmm, real magnetotail IS a low-density cavity. The wind particles flowing behind earth at r<116 would be ejected outward — reads as the tail cavity. Acceptable visually? The push near axis is radial outward from earth center — at point (60, 1, 0), n≈(1,0,0) → pushes +X, accelerating particle — creates fast jet down tail axis. Might look like plasma flow through tail. Eh.
Better: limit deflection influence: only apply when x < Rb (i.e., not far downstream)? Simplest robust: if s<6 AND theta<2.9 → apply. For particles beyond the tail opening, no deflection — they just stream. The tail interior (cavity behind earth, x>0, small |y,z|) particles pass through — slightly unphysical but subtle at night side.
Actually simpler and safe: clamp th = min(th, 2.5). Rb(2.5)= (2/(1-0.801))^0.55 = (10.05)^0.55 ≈ 3.56 → Rb≈107·... bs≈30 → 107. Hmm still large. But s = r - Rb: particle at (60,0,0): r=60 < 107 → inside → pushed. The wake region spans |y,z| < ~10 for x up to 100+. With t=1 max push 34·dt and vn removal — particle moving +X: vn = v·n = +v (outward!) → no inward removal, push outward 34·dt·1 → accelerates +X. So tail-axis particles just speed up — looks like fast flow down the tail, no visual catastrophe. I'll clamp to 2.5 and accept.
Hmm, wait: actually there's a subtle issue — the "cavity" behind earth: wind spawned at x=-155 with small |y,z| passes earth... those heading to the wake get deflected around earth mostly (shroud deflection sunward side) or absorbed by earth. OK.
Also check shue at θ=0.06 fine.
Fix the clamp:
const th = Math.acos(...); const Rb = bs * shue(Math.min(th, 2.5), BS_ALPHA);-
renderer.domElementcursor styles ✓. -
Sky sphere radius 950 vs camera far 3000 ✓; controls.maxDistance 420 < 950 ✓ camera stays inside sky ✓.
-
Aurora material transparent & additive ✓ renderOrder 8 after shrouds ✓.
-
state.wind01init matches slider default 430 ✓. -
$('vFlow')format: timeScale.toFixed(2).replace(/0$/,'') — 1→"1.0"→"1."? "1.00".replace(/0$/,'') → "1.0" ✓ good. -
HUD stats initial — set in loop ✓.
-
Missing:
hintuses styling default — fine. -
One more potential GLSL issue: in shroud fragment I use
uFlash— declared ✓.noseGlowuses vUv.y — fine. -
In earth vertex EARTH_VERT, modelMatrix for clouds/earth: they're children of earthSpin/earthTilt — modelMatrix updates automatically ✓. vWN includes rotation ✓ — sun glint & lighting correct in world space ✓.
-
atmoMat has empty uniforms object — ShaderMaterial with uniforms:{} fine ✓.
-
pulseMat uSize 2.6 * (160/-mv.z): at distance 60 → 6.9px ✓.
-
pointsMaterial gl_PointSize: uSize(2.0)·(0.7..1.3)·(150/dist): at 100 → 3px ✓.
-
Chunk 3:
const geo— I create a full geometry then never add it (only slices). Slight waste, fine. Actually wait — there's a bug: I creategrpand addTHREE.Line(g2, mat)per line — 40 lines, one draw call each — fine. -
THREE.MathUtils.degToRad in PSIS map — ✓.
-
In chunk 4
pointsMaterial—${vertexColor ? ...}template interpolation inside the shader strings — those are JS template literals; I used backticks in the source? I wrote vertexShader: /* glsl */... ${vertexColor ? '...' : ''} ...— yes backticks — and the heredoc was quoted 'HTML4' so ${} preserved literally for JS ✓. -
Check
updatePrecip:_pvshared vector declared in chunk 3 ✓ (const _pv). Used in updatePulses too ✓ and updateBelts ✓. -
launchCME referenced before definition in updateCME? No — launchCME defined in chunk 4 ✓.
AudioSysreferenced inside launchCME/cmeImpact — const declared chunk 5; launchCME only invoked via user/auto timer after module evaluation completes ✓ TDZ safe. -
gradePassreferenced in cmeImpact — defined chunk 5 before any call ✓. -
sprayPoints: sprayGeo positions relative to cmeGroup; initial zeros until first update ✓ (group hidden until launch ✓).
-
CME blob material fragment uses
n.zfor rim — view-dependent? n is object normal; fresnel-ish via n.z is in view space? No — in fragment, vN interpolated object-space normal; abs(n.z) high when facing camera only if mesh not rotated... Actually object-space normal z vs view: incorrect fresnel but produces a static-ish rim pattern; blob rotates? cmeGroup doesn't rotate except scale. Add rotation in updateCME for life: cmeGroup.rotation.y += dt*0.4. The "rim" then rotates slowly — acceptable shimmering blob. Better: compute view-space rim: transform normal to view: normalMatrix not available in fragment... skip; rotation makes it lively.
Add
cmeGroup.rotation.y += dt*0.5;in updateCME.-
In updateCME: when !impacting, position updates; scale min 30; start distance ~136 → scale=5+14*0.14≈7 grows to 30 near impact ✓.
-
cme speed: windSpeedScene2.1+26 ≈ 13+? default wind01=0.327→speed=23.8; cme=76 u/s → travel 136 units in ~1.8s — fast! Maybe too fast; slow: windSpeedScene1.2+14 ≈ 42 → 3.2s. CMEs SHOULD feel dramatic; 3s travel fine. Adjust formula to *1.2+14.
-
Auto-orbit button: controls.autoRotate set ✓.
-
status shows 'CME IMPACT' while impacting ✓.
Fixes to apply:
- clamp theta in updateWind to ≤2.5.
- cme speed formula.
- cmeGroup rotation in updateCME.
- remove unused dx/dy/dz in auroraGeometry (optional; harmless — skip).
Then node --check the extracted script.
Also deploy + update index page. The index page format:
Let me view that card's exact markup to replicate for 034.
-
- Commented
Now let me apply a few fixes I spotted (theta clamp to avoid division blow-up in the Shue formula at the tail axis, CME speed, blob rotation), then syntax-check the script.
- Created activity call replace_in_file_in_shell_environment
- Created activity call replace_in_file_in_shell_environment
- Created activity call replace_in_file_in_shell_environment
- Created activity call _checkTaskRunStatus
- Created activity call _checkTaskRunStatus
Other Conversations
Explore other conversations with this agent.