Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created July 15, 2025 18:06
Show Gist options
  • Save shricodev/2a87d241dff2a7630f31e1d504b32cd8 to your computer and use it in GitHub Desktop.
Save shricodev/2a87d241dff2a7630f31e1d504b32cd8 to your computer and use it in GitHub Desktop.
Black Hole Animation - Gemini 2.5 Pro - Blog Demo
<!DOCTYPE html>
<html lang="en">
<head>
<title>Black Hole Horizon</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<style>
body {
margin: 0;
background-color: #000000;
color: #fff;
font-family: Monospace;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<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';
let scene, camera, renderer, composer, controls;
let accretionDisk, blackHole, plasmaBeam, stars;
// Shaders
const gravitationalLensingShader = {
uniforms: {
tDiffuse: { value: null },
'strength': { value: 0.05 }, // Gravitational lensing strength
'center': { value: new THREE.Vector2(0.5, 0.5) }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float strength;
uniform vec2 center;
varying vec2 vUv;
void main() {
vec2 toCenter = center - vUv;
float dist = length(toCenter);
vec2 distortedUv = vUv + toCenter * (1.0/dist) * strength * pow(max(0.0, 1.0-dist*2.0), 2.0);
gl_FragColor = texture2D(tDiffuse, distortedUv);
}
`
};
const accretionDiskShader = {
uniforms: {
'time': { value: 1.0 },
'color1': { value: new THREE.Color(0xff8c00) }, // Orange
'color2': { value: new THREE.Color(0x00bfff) } // Blue
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform vec3 color1;
uniform vec3 color2;
varying vec2 vUv;
float noise(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
float angle = atan(vUv.y - 0.5, vUv.x - 0.5);
float radius = length(vUv - 0.5);
float strength = (sin(angle * 10.0 + time * 2.0) * 0.1 + 0.9);
vec3 color = mix(color1, color2, (angle + 3.1415) / (2.0*3.1415));
float n = noise(vUv * 20.0 + time * 0.1);
gl_FragColor = vec4(color * strength * (1.0 - radius * 0.1) * (1.0 - n * 0.2), 1.0);
}
`
};
const plasmaBeamShader = {
uniforms: {
'time': { value: 1.0 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
varying vec2 vUv;
void main() {
float t = time * 2.0;
float plasma = sin(vUv.y * 10.0 + t) + sin(vUv.x * 5.0 - t) * 0.5;
vec3 color = mix(vec3(0.0, 0.5, 1.0), vec3(1.0, 0.5, 0.0), smoothstep(0.0, 1.0, vUv.x));
float intensity = pow(0.05 / abs(vUv.y - 0.5), 1.2);
gl_FragColor = vec4(color * intensity * (sin(vUv.x * 20.0 + time * 5.0) * 0.2 + 0.8), 1.0);
}
`
};
init();
animate();
function init() {
// Scene
scene = new THREE.Scene();
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 10;
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 3;
controls.maxDistance = 50;
// Starfield
const starVertices = [];
for (let i = 0; i < 10000; i++) {
const x = THREE.MathUtils.randFloatSpread(2000);
const y = THREE.MathUtils.randFloatSpread(2000);
const z = THREE.MathUtils.randFloatSpread(2000);
starVertices.push(x, y, z);
}
const starGeometry = new THREE.BufferGeometry();
starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3));
const starMaterial = new THREE.PointsMaterial({ color: 0x888888 });
stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
// Black Hole Sphere
const blackHoleGeometry = new THREE.SphereGeometry(1, 32, 32);
const blackHoleMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
blackHole = new THREE.Mesh(blackHoleGeometry, blackHoleMaterial);
scene.add(blackHole);
// Accretion Disk
const diskGeometry = new THREE.RingGeometry(1.2, 4, 64);
const diskMaterial = new THREE.ShaderMaterial({
uniforms: accretionDiskShader.uniforms,
vertexShader: accretionDiskShader.vertexShader,
fragmentShader: accretionDiskShader.fragmentShader,
side: THREE.DoubleSide,
transparent: true,
});
accretionDisk = new THREE.Mesh(diskGeometry, diskMaterial);
accretionDisk.rotation.x = Math.PI * 0.5;
scene.add(accretionDisk);
// Plasma Beam
const beamGeometry = new THREE.PlaneGeometry(20, 0.2);
const beamMaterial = new THREE.ShaderMaterial({
uniforms: plasmaBeamShader.uniforms,
vertexShader: plasmaBeamShader.vertexShader,
fragmentShader: plasmaBeamShader.fragmentShader,
side: THREE.DoubleSide,
transparent: true,
blending: THREE.AdditiveBlending,
});
plasmaBeam = new THREE.Mesh(beamGeometry, beamMaterial);
plasmaBeam.rotation.z = Math.PI / 4; // Diagonal
scene.add(plasmaBeam);
// Post-processing
composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// Gravitational Lensing Pass
const lensingPass = new ShaderPass(gravitationalLensingShader);
composer.addPass(lensingPass);
// Bloom Pass
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
bloomPass.threshold = 0;
bloomPass.strength = 1.2;
bloomPass.radius = 0.5;
composer.addPass(bloomPass);
window.addEventListener('resize', onWindowResize, false);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const time = performance.now() * 0.001;
accretionDisk.material.uniforms['time'].value = time;
plasmaBeam.material.uniforms['time'].value = time;
controls.update();
composer.render();
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment