-
-
Save shricodev/ce71d67d8d6a26464f497d5430dc9984 to your computer and use it in GitHub Desktop.
Black Hole Animation - Claude Opus 4 - Blog Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Black Hole Visualization</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background: radial-gradient(ellipse at center, #0a0a1a 0%, #000002 70%); | |
| color: #e0e0ff; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| #info { | |
| position: absolute; | |
| top: 20px; | |
| width: 100%; | |
| text-align: center; | |
| color: rgba(220, 220, 255, 0.9); | |
| font-size: 18px; | |
| letter-spacing: 0.5px; | |
| pointer-events: none; | |
| z-index: 100; | |
| text-shadow: 0 1px 5px rgba(0, 0, 0, 0.7); | |
| transition: opacity 2s ease-in-out 1s; | |
| } | |
| .ui-panel { | |
| position: absolute; | |
| background-image: linear-gradient(145deg, rgba(20, 25, 45, 0.85), rgba(10, 15, 30, 0.9)); | |
| backdrop-filter: blur(10px) saturate(160%); | |
| -webkit-backdrop-filter: blur(10px) saturate(160%); | |
| padding: 15px 20px; | |
| border-radius: 10px; | |
| border: 1px solid rgba(180, 180, 220, 0.15); | |
| color: rgba(225, 225, 255, 0.9); | |
| font-size: 14px; | |
| user-select: none; | |
| z-index: 50; | |
| transition: opacity 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease; | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(180,180,220,0.07) inset; | |
| box-sizing: border-box; | |
| opacity: 0; | |
| transform: translateY(15px); | |
| animation: panelFadeIn 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.2s forwards; | |
| } | |
| @keyframes panelFadeIn { to { opacity: 1; transform: translateY(0); } } | |
| .ui-panel:hover { | |
| box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(180,180,220,0.09) inset; | |
| } | |
| #controls { bottom: 20px; right: 20px; } | |
| #autoRotateToggle { | |
| cursor: pointer; padding: 8px 5px; display: flex; align-items: center; | |
| gap: 8px; color: inherit; font-size: inherit; transition: color 0.2s ease; | |
| } | |
| #autoRotateToggle:hover { color: #fff; } | |
| #autoRotateToggle span { vertical-align: middle; } | |
| .rotate-icon { | |
| width: 1.1em; height: 1.1em; stroke: currentColor; stroke-width: 1.8; | |
| fill: none; stroke-linecap: round; stroke-linejoin: round; vertical-align: middle; | |
| } | |
| .ui-slider { margin: 10px 0; display: flex; align-items: center; gap: 8px; } | |
| .ui-slider input[type="range"] { flex: 1; } | |
| .ui-color { margin: 10px 0; display: flex; align-items: center; gap: 8px; } | |
| .ui-color input[type="color"] { width: 30px; height: 30px; padding: 0; border: none; } | |
| @media (max-width: 640px) { | |
| .ui-panel { padding: 10px 12px; border-radius: 8px; } | |
| #controls { max-width: 150px; } | |
| #info { font-size: 16px; top: 15px; } | |
| #info span { font-size: 12px; } | |
| } | |
| </style> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js", | |
| "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <div id="info"> | |
| Black Hole<br> | |
| <span style="font-size: 14px; opacity: 0.8;">Click and drag to rotate view</span> | |
| </div> | |
| <div id="controls" class="ui-panel"> | |
| <div id="autoRotateToggle" title="Toggle automatic rotation"></div> | |
| <div class="ui-slider"> | |
| <label for="bloomSlider">Bloom:</label> | |
| <input type="range" id="bloomSlider" min="0" max="2" step="0.1" value="0.8"> | |
| </div> | |
| <div class="ui-color"> | |
| <label for="hotColorPicker">Hot Color:</label> | |
| <input type="color" id="hotColorPicker" value="#ffffff"> | |
| </div> | |
| <div class="ui-color"> | |
| <label for="outerColorPicker">Outer Color:</label> | |
| <input type="color" id="outerColorPicker" value="#4477ff"> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; | |
| import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; | |
| import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; | |
| import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; | |
| const BLACK_HOLE_RADIUS = 1.3; | |
| const DISK_INNER_RADIUS = BLACK_HOLE_RADIUS + 0.2; | |
| const DISK_OUTER_RADIUS = 8.0; | |
| const DISK_TILT_ANGLE = Math.PI / 3.0; | |
| const scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2(0x020104, 0.025); | |
| const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 4000); | |
| camera.position.set(-6.5, 5.0, 6.5); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); | |
| renderer.outputColorSpace = THREE.SRGBColorSpace; | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| renderer.toneMappingExposure = 1.2; | |
| document.body.appendChild(renderer.domElement); | |
| const composer = new EffectComposer(renderer); | |
| composer.addPass(new RenderPass(scene, camera)); | |
| const bloomPass = new UnrealBloomPass( | |
| new THREE.Vector2(window.innerWidth, window.innerHeight), | |
| 0.8, 0.7, 0.8 | |
| ); | |
| composer.addPass(bloomPass); | |
| const lensingShader = { | |
| uniforms: { | |
| "tDiffuse": { value: null }, | |
| "blackHoleScreenPos": { value: new THREE.Vector2(0.5, 0.5) }, | |
| "lensingStrength": { value: 0.12 }, | |
| "lensingRadius": { value: 0.3 }, | |
| "aspectRatio": { value: window.innerWidth / window.innerHeight }, | |
| "chromaticAberration": { value: 0.005 } | |
| }, | |
| vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, | |
| fragmentShader: ` | |
| uniform sampler2D tDiffuse; | |
| uniform vec2 blackHoleScreenPos; | |
| uniform float lensingStrength; | |
| uniform float lensingRadius; | |
| uniform float aspectRatio; | |
| uniform float chromaticAberration; | |
| varying vec2 vUv; | |
| void main() { | |
| vec2 screenPos = vUv; | |
| vec2 toCenter = screenPos - blackHoleScreenPos; | |
| toCenter.x *= aspectRatio; | |
| float dist = length(toCenter); | |
| float distortionAmount = lensingStrength / (dist * dist + 0.003); | |
| distortionAmount = clamp(distortionAmount, 0.0, 0.7); | |
| float falloff = smoothstep(lensingRadius, lensingRadius * 0.3, dist); | |
| distortionAmount *= falloff; | |
| vec2 offset = normalize(toCenter) * distortionAmount; | |
| offset.x /= aspectRatio; | |
| vec2 distortedUvR = screenPos - offset * (1.0 + chromaticAberration); | |
| vec2 distortedUvG = screenPos - offset; | |
| vec2 distortedUvB = screenPos - offset * (1.0 - chromaticAberration); | |
| float r = texture2D(tDiffuse, distortedUvR).r; | |
| float g = texture2D(tDiffuse, distortedUvG).g; | |
| float b = texture2D(tDiffuse, distortedUvB).b; | |
| gl_FragColor = vec4(r, g, b, 1.0); | |
| }` | |
| }; | |
| const lensingPass = new ShaderPass(lensingShader); | |
| composer.addPass(lensingPass); | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; controls.dampingFactor = 0.035; | |
| controls.rotateSpeed = 0.4; controls.autoRotate = false; | |
| controls.autoRotateSpeed = 0.1; | |
| controls.target.set(0, 0, 0); | |
| controls.minDistance = 2.5; | |
| controls.maxDistance = 100; | |
| controls.enablePan = false; | |
| controls.update(); | |
| let autoRotateEnabled = true; | |
| const autoRotateToggle = document.getElementById('autoRotateToggle'); | |
| const rotateIconSVG = `<svg class="rotate-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`; | |
| updateAutoRotateText(); | |
| autoRotateToggle.addEventListener('click', () => { | |
| autoRotateEnabled = !autoRotateEnabled; controls.autoRotate = autoRotateEnabled; updateAutoRotateText(); | |
| }); | |
| function updateAutoRotateText() { | |
| autoRotateToggle.innerHTML = rotateIconSVG + `<span>Auto-Rotate: ${autoRotateEnabled ? "ON" : "OFF"}</span>`; | |
| } | |
| const starGeometry = new THREE.BufferGeometry(); | |
| const starCount = 150000; | |
| const starPositions = new Float32Array(starCount * 3); | |
| const starColors = new Float32Array(starCount * 3); | |
| const starSizes = new Float32Array(starCount); | |
| const starTwinkle = new Float32Array(starCount); | |
| const starFieldRadius = 2000; | |
| const starPalette = [ | |
| new THREE.Color(0x88aaff), new THREE.Color(0xffaaff), new THREE.Color(0xaaffff), | |
| new THREE.Color(0xffddaa), new THREE.Color(0xffeecc), new THREE.Color(0xffffff), | |
| new THREE.Color(0xff8888), new THREE.Color(0x88ff88), new THREE.Color(0xffff88), | |
| new THREE.Color(0x88ffff) | |
| ]; | |
| for (let i = 0; i < starCount; i++) { | |
| const i3 = i * 3; | |
| const phi = Math.acos(-1 + (2 * i) / starCount); | |
| const theta = Math.sqrt(starCount * Math.PI) * phi; | |
| const radius = Math.cbrt(Math.random()) * starFieldRadius + 100; | |
| starPositions[i3] = radius * Math.sin(phi) * Math.cos(theta); | |
| starPositions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta); | |
| starPositions[i3 + 2] = radius * Math.cos(phi); | |
| const starColor = starPalette[Math.floor(Math.random() * starPalette.length)].clone(); | |
| starColor.multiplyScalar(Math.random() * 0.7 + 0.3); | |
| starColors[i3] = starColor.r; starColors[i3 + 1] = starColor.g; starColors[i3 + 2] = starColor.b; | |
| starSizes[i] = THREE.MathUtils.randFloat(0.6, 3.0); | |
| starTwinkle[i] = Math.random() * Math.PI * 2; | |
| } | |
| starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3)); | |
| starGeometry.setAttribute('color', new THREE.BufferAttribute(starColors, 3)); | |
| starGeometry.setAttribute('size', new THREE.BufferAttribute(starSizes, 1)); | |
| starGeometry.setAttribute('twinkle', new THREE.BufferAttribute(starTwinkle, 1)); | |
| const starMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| uTime: { value: 0 }, | |
| uPixelRatio: { value: renderer.getPixelRatio() } | |
| }, | |
| vertexShader: ` | |
| uniform float uTime; | |
| uniform float uPixelRatio; | |
| attribute float size; | |
| attribute float twinkle; | |
| varying vec3 vColor; | |
| varying float vTwinkle; | |
| void main() { | |
| vColor = color; | |
| vTwinkle = sin(uTime * 2.5 + twinkle) * 0.5 + 0.5; | |
| vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); | |
| gl_PointSize = size * uPixelRatio * (300.0 / -mvPosition.z); | |
| gl_Position = projectionMatrix * mvPosition; | |
| } | |
| `, | |
| fragmentShader: ` | |
| varying vec3 vColor; | |
| varying float vTwinkle; | |
| void main() { | |
| float dist = distance(gl_PointCoord, vec2(0.5)); | |
| if (dist > 0.5) discard; | |
| float alpha = 1.0 - smoothstep(0.0, 0.5, dist); | |
| alpha *= (0.2 + vTwinkle * 0.8); | |
| gl_FragColor = vec4(vColor, alpha); | |
| } | |
| `, | |
| transparent: true, | |
| vertexColors: true, | |
| blending: THREE.AdditiveBlending, | |
| depthWrite: false | |
| }); | |
| const stars = new THREE.Points(starGeometry, starMaterial); | |
| scene.add(stars); | |
| const eventHorizonGeom = new THREE.SphereGeometry(BLACK_HOLE_RADIUS * 1.05, 128, 64); | |
| const eventHorizonMat = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| uTime: { value: 0 }, | |
| uCameraPosition: { value: camera.position } | |
| }, | |
| vertexShader: ` | |
| varying vec3 vNormal; | |
| varying vec3 vPosition; | |
| void main() { | |
| vNormal = normalize(normalMatrix * normal); | |
| vPosition = position; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform float uTime; | |
| uniform vec3 uCameraPosition; | |
| varying vec3 vNormal; | |
| varying vec3 vPosition; | |
| void main() { | |
| vec3 viewDirection = normalize(uCameraPosition - vPosition); | |
| float fresnel = 1.0 - abs(dot(vNormal, viewDirection)); | |
| fresnel = pow(fresnel, 2.5); | |
| vec3 glowColor = vec3(1.0, 0.4, 0.1); | |
| float pulse = sin(uTime * 2.5) * 0.15 + 0.85; | |
| gl_FragColor = vec4(glowColor * fresnel * pulse, fresnel * 0.4); | |
| } | |
| `, | |
| transparent: true, | |
| blending: THREE.AdditiveBlending, | |
| side: THREE.BackSide | |
| }); | |
| const eventHorizon = new THREE.Mesh(eventHorizonGeom, eventHorizonMat); | |
| scene.add(eventHorizon); | |
| const blackHoleGeom = new THREE.SphereGeometry(BLACK_HOLE_RADIUS, 128, 64); | |
| const blackHoleMat = new THREE.MeshBasicMaterial({ color: 0x000000 }); | |
| const blackHoleMesh = new THREE.Mesh(blackHoleGeom, blackHoleMat); | |
| blackHoleMesh.renderOrder = 0; | |
| scene.add(blackHoleMesh); | |
| const diskGeometry = new THREE.RingGeometry(DISK_INNER_RADIUS, DISK_OUTER_RADIUS, 256, 128); | |
| const diskMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| uTime: { value: 0.0 }, | |
| uColorHot: { value: new THREE.Color(0xffffff) }, | |
| uColorMid1: { value: new THREE.Color(0xff7733) }, | |
| uColorMid2: { value: new THREE.Color(0xff4477) }, | |
| uColorMid3: { value: new THREE.Color(0x7744ff) }, | |
| uColorOuter: { value: new THREE.Color(0x4477ff) }, | |
| uNoiseScale: { value: 2.5 }, | |
| uFlowSpeed: { value: 0.22 }, | |
| uDensity: { value: 1.3 } | |
| }, | |
| vertexShader: ` | |
| varying vec2 vUv; | |
| varying float vRadius; | |
| varying float vAngle; | |
| void main() { | |
| vUv = uv; | |
| vRadius = length(position.xy); | |
| vAngle = atan(position.y, position.x); | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform float uTime; | |
| uniform vec3 uColorHot; | |
| uniform vec3 uColorMid1; | |
| uniform vec3 uColorMid2; | |
| uniform vec3 uColorMid3; | |
| uniform vec3 uColorOuter; | |
| uniform float uNoiseScale; | |
| uniform float uFlowSpeed; | |
| uniform float uDensity; | |
| varying vec2 vUv; | |
| varying float vRadius; | |
| varying float vAngle; | |
| vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } | |
| vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } | |
| vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); } | |
| vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; } | |
| float snoise(vec3 v) { | |
| const vec2 C = vec2(1.0/6.0, 1.0/3.0); | |
| const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); | |
| vec3 i = floor(v + dot(v, C.yyy) ); | |
| vec3 x0 = v - i + dot(i, C.xxx) ; | |
| vec3 g = step(x0.yzx, x0.xyz); | |
| vec3 l = 1.0 - g; | |
| vec3 i1 = min( g.xyz, l.zxy ); | |
| vec3 i2 = max( g.xyz, l.zxy ); | |
| vec3 x1 = x0 - i1 + C.xxx; | |
| vec3 x2 = x0 - i2 + C.yyy; | |
| vec3 x3 = x0 - D.yyy; | |
| i = mod289(i); | |
| 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 )); | |
| float n_ = 0.142857142857; | |
| vec3 ns = n_ * D.wyz - D.xzx; | |
| vec4 j = p - 49.0 * floor(p * ns.z * ns.z); | |
| vec4 x_ = floor(j * ns.z); | |
| vec4 y_ = floor(j - 7.0 * x_ ); | |
| vec4 x = x_ *ns.x + ns.yyyy; | |
| vec4 y = y_ *ns.x + ns.yyyy; | |
| vec4 h = 1.0 - abs(x) - abs(y); | |
| vec4 b0 = vec4( x.xy, y.xy ); | |
| vec4 b1 = vec4( x.zw, y.zw ); | |
| vec4 s0 = floor(b0)*2.0 + 1.0; | |
| vec4 s1 = floor(b1)*2.0 + 1.0; | |
| vec4 sh = -step(h, vec4(0.0)); | |
| vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; | |
| vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; | |
| 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); | |
| vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3))); | |
| p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w; | |
| vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); | |
| m = m * m; | |
| return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3) ) ); | |
| } | |
| void main() { | |
| float normalizedRadius = smoothstep(${DISK_INNER_RADIUS.toFixed(2)}, ${DISK_OUTER_RADIUS.toFixed(2)}, vRadius); | |
| float spiral = vAngle * 3.0 - (1.0 / (normalizedRadius + 0.1)) * 2.0; | |
| vec2 noiseUv = vec2(vUv.x + uTime * uFlowSpeed * (2.0 / (vRadius * 0.3 + 1.0)) + sin(spiral) * 0.1, vUv.y * 0.8 + cos(spiral) * 0.1); | |
| float noiseVal1 = snoise(vec3(noiseUv * uNoiseScale, uTime * 0.15)); | |
| float noiseVal2 = snoise(vec3(noiseUv * uNoiseScale * 3.0 + 0.8, uTime * 0.22)); | |
| float noiseVal3 = snoise(vec3(noiseUv * uNoiseScale * 6.0 + 1.5, uTime * 0.3)); | |
| float noiseVal = (noiseVal1 * 0.45 + noiseVal2 * 0.35 + noiseVal3 * 0.2); | |
| noiseVal = (noiseVal + 1.0) * 0.5; | |
| vec3 color = uColorOuter; | |
| color = mix(color, uColorMid3, smoothstep(0.0, 0.25, normalizedRadius)); | |
| color = mix(color, uColorMid2, smoothstep(0.2, 0.55, normalizedRadius)); | |
| color = mix(color, uColorMid1, smoothstep(0.5, 0.75, normalizedRadius)); | |
| color = mix(color, uColorHot, smoothstep(0.7, 0.95, normalizedRadius)); | |
| color *= (0.5 + noiseVal * 1.0); | |
| float brightness = pow(1.0 - normalizedRadius, 1.0) * 3.5 + 0.5; | |
| brightness *= (0.3 + noiseVal * 2.2); | |
| float pulse = sin(uTime * 1.8 + normalizedRadius * 12.0 + vAngle * 2.0) * 0.15 + 0.85; | |
| brightness *= pulse; | |
| float alpha = uDensity * (0.2 + noiseVal * 0.9); | |
| alpha *= smoothstep(0.0, 0.15, normalizedRadius); | |
| alpha *= (1.0 - smoothstep(0.85, 1.0, normalizedRadius)); | |
| alpha = clamp(alpha, 0.0, 1.0); | |
| gl_FragColor = vec4(color * brightness, alpha); | |
| } | |
| `, | |
| transparent: true, | |
| side: THREE.DoubleSide, | |
| depthWrite: false, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| const accretionDisk = new THREE.Mesh(diskGeometry, diskMaterial); | |
| accretionDisk.rotation.x = DISK_TILT_ANGLE; | |
| accretionDisk.renderOrder = 1; | |
| scene.add(accretionDisk); | |
| // Planet 1: Blue planet with atmosphere | |
| const planet1Geom = new THREE.SphereGeometry(0.8, 64, 64); | |
| const planet1Mat = new THREE.MeshLambertMaterial({ color: 0x0033ff, emissive: 0x001144 }); | |
| const planet1 = new THREE.Mesh(planet1Geom, planet1Mat); | |
| const atmosGeom = new THREE.SphereGeometry(0.85, 64, 64); | |
| const atmosMat = new THREE.MeshBasicMaterial({ color: 0x3399ff, transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending }); | |
| const atmosphere = new THREE.Mesh(atmosGeom, atmosMat); | |
| planet1.add(atmosphere); | |
| planet1.position.set(12, 0, 0); | |
| scene.add(planet1); | |
| // Planet 2: Red planet with rings | |
| const planet2Geom = new THREE.SphereGeometry(0.6, 64, 64); | |
| const planet2Mat = new THREE.MeshLambertMaterial({ color: 0xff3300, emissive: 0x441100 }); | |
| const planet2 = new THREE.Mesh(planet2Geom, planet2Mat); | |
| const ringGeom = new THREE.RingGeometry(0.9, 1.5, 128); | |
| const ringMat = new THREE.MeshBasicMaterial({ color: 0xffaa77, side: THREE.DoubleSide, transparent: true, opacity: 0.7 }); | |
| const ring = new THREE.Mesh(ringGeom, ringMat); | |
| ring.rotation.x = Math.PI / 2; | |
| planet2.add(ring); | |
| planet2.position.set(-15, 0, 0); | |
| scene.add(planet2); | |
| const ambientLight = new THREE.AmbientLight(0x404040, 0.5); | |
| scene.add(ambientLight); | |
| setTimeout(() => { const info = document.getElementById('info'); if (info) info.style.opacity = '0'; }, 5000); | |
| let resizeTimeout; | |
| window.addEventListener('resize', () => { | |
| clearTimeout(resizeTimeout); | |
| resizeTimeout = setTimeout(() => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| composer.setSize(window.innerWidth, window.innerHeight); | |
| bloomPass.resolution.set(window.innerWidth, window.innerHeight); | |
| lensingPass.uniforms.aspectRatio.value = window.innerWidth / window.innerHeight; | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); | |
| }, 150); | |
| }); | |
| const clock = new THREE.Clock(); | |
| const blackHoleScreenPosVec3 = new THREE.Vector3(); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const elapsedTime = clock.getElapsedTime(); | |
| const deltaTime = clock.getDelta(); | |
| diskMaterial.uniforms.uTime.value = elapsedTime; | |
| starMaterial.uniforms.uTime.value = elapsedTime; | |
| eventHorizonMat.uniforms.uTime.value = elapsedTime; | |
| eventHorizonMat.uniforms.uCameraPosition.value.copy(camera.position); | |
| blackHoleScreenPosVec3.copy(blackHoleMesh.position).project(camera); | |
| lensingPass.uniforms.blackHoleScreenPos.value.set( | |
| (blackHoleScreenPosVec3.x + 1) / 2, | |
| (blackHoleScreenPosVec3.y + 1) / 2 | |
| ); | |
| controls.update(); | |
| stars.rotation.y += deltaTime * 0.003; | |
| stars.rotation.x += deltaTime * 0.001; | |
| accretionDisk.rotation.z += deltaTime * 0.005; | |
| planet1.position.set(12 * Math.cos(elapsedTime * 0.03), 3 * Math.sin(elapsedTime * 0.05), 12 * Math.sin(elapsedTime * 0.03)); | |
| planet1.rotation.y += deltaTime * 0.2; | |
| planet2.position.set(-15 * Math.cos(elapsedTime * 0.02), 2 * Math.sin(elapsedTime * 0.04), -15 * Math.sin(elapsedTime * 0.02)); | |
| planet2.rotation.y += deltaTime * 0.3; | |
| composer.render(deltaTime); | |
| } | |
| animate(); | |
| const bloomSlider = document.getElementById('bloomSlider'); | |
| bloomSlider.addEventListener('input', (e) => { | |
| bloomPass.strength = parseFloat(e.target.value); | |
| }); | |
| const hotColorPicker = document.getElementById('hotColorPicker'); | |
| hotColorPicker.addEventListener('input', (e) => { | |
| diskMaterial.uniforms.uColorHot.value.set(e.target.value); | |
| }); | |
| const outerColorPicker = document.getElementById('outerColorPicker'); | |
| outerColorPicker.addEventListener('input', (e) => { | |
| diskMaterial.uniforms.uColorOuter.value.set(e.target.value); | |
| }); | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment