anycoder-19619425 / index.html
idgmatrix's picture
Upload folder using huggingface_hub
cd6e336 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Black Hole with Accretion Disk - WebGPU Simulator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #0a0a0a 100%);
min-height: 100vh;
color: #fff;
overflow-x: hidden;
}
header {
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.logo {
font-size: 1.5rem;
font-weight: bold;
background: linear-gradient(90deg, #ff6b35, #f7c59f);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.built-with {
font-size: 0.9rem;
color: #888;
}
.built-with a {
color: #ff6b35;
text-decoration: none;
transition: color 0.3s ease;
}
.built-with a:hover {
color: #f7c59f;
}
main {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
gap: 2rem;
}
.canvas-container {
position: relative;
width: 100%;
max-width: 900px;
aspect-ratio: 16/9;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 0 60px rgba(255, 107, 53, 0.3),
0 0 100px rgba(255, 107, 53, 0.1);
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
width: 100%;
max-width: 900px;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-group label {
font-size: 0.9rem;
color: #ccc;
display: flex;
justify-content: space-between;
align-items: center;
}
.control-group label span {
color: #ff6b35;
font-weight: bold;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 8px;
border-radius: 4px;
background: linear-gradient(90deg, #333, #555);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #ff6b35, #f7c59f);
cursor: pointer;
box-shadow: 0 0 10px rgba(255, 107, 53, 0.5);
transition: transform 0.2s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #ff6b35, #f7c59f);
cursor: pointer;
border: none;
box-shadow: 0 0 10px rgba(255, 107, 53, 0.5);
}
.info-panel {
width: 100%;
max-width: 900px;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.info-panel h2 {
font-size: 1.2rem;
margin-bottom: 1rem;
color: #ff6b35;
}
.info-panel p {
font-size: 0.9rem;
line-height: 1.6;
color: #aaa;
}
.error-message {
padding: 2rem;
background: rgba(255, 0, 0, 0.1);
border: 1px solid rgba(255, 0, 0, 0.3);
border-radius: 16px;
text-align: center;
max-width: 600px;
}
.error-message h2 {
color: #ff4444;
margin-bottom: 1rem;
}
.error-message p {
color: #ccc;
line-height: 1.6;
}
.stats {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-label {
font-size: 0.8rem;
color: #888;
}
.stat-value {
font-size: 1.1rem;
color: #ff6b35;
font-weight: bold;
}
@media (max-width: 768px) {
header {
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
main {
padding: 1rem;
}
.controls {
grid-template-columns: 1fr;
}
.stats {
justify-content: center;
}
}
</style>
</head>
<body>
<header>
<div class="logo">🌌 Black Hole Simulator</div>
<div class="built-with">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder"
target="_blank">anycoder</a></div>
</header>
<main>
<div class="canvas-container">
<canvas id="webgpu-canvas"></canvas>
</div>
<div class="controls">
<div class="control-group">
<label>Black Hole Mass <span id="mass-value">1.0</span></label>
<input type="range" id="mass" min="0.5" max="3.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>Disk Inner Radius <span id="inner-radius-value">3.0</span></label>
<input type="range" id="inner-radius" min="2.0" max="5.0" step="0.1" value="3.0">
</div>
<div class="control-group">
<label>Disk Outer Radius <span id="outer-radius-value">12.0</span></label>
<input type="range" id="outer-radius" min="8.0" max="20.0" step="0.5" value="12.0">
</div>
<div class="control-group">
<label>Disk Temperature <span id="temperature-value">1.0</span></label>
<input type="range" id="temperature" min="0.3" max="2.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>Camera Distance <span id="camera-dist-value">20.0</span></label>
<input type="range" id="camera-dist" min="10.0" max="40.0" step="1.0" value="20.0">
</div>
<div class="control-group">
<label>View Angle <span id="view-angle-value">75</span>°</label>
<input type="range" id="view-angle" min="10" max="90" step="1" value="75">
</div>
</div>
<div class="info-panel">
<h2>About This Simulation</h2>
<p>
This is a real-time ray-traced simulation of a Schwarzschild black hole with an accretion disk,
powered by WebGPU. The simulation uses general relativistic ray tracing to accurately depict
gravitational lensing, photon sphere effects, and the characteristic appearance of matter
spiraling into the event horizon. The accretion disk glows due to friction-heated matter,
with colors ranging from red (cooler outer regions) to white-hot (near the inner edge).
</p>
<div class="stats">
<div class="stat">
<span class="stat-label">Schwarzschild Radius</span>
<span class="stat-value" id="schwarzschild-radius">2.0 Rs</span>
</div>
<div class="stat">
<span class="stat-label">Photon Sphere</span>
<span class="stat-value" id="photon-sphere">3.0 Rs</span>
</div>
<div class="stat">
<span class="stat-label">ISCO</span>
<span class="stat-value" id="isco">6.0 Rs</span>
</div>
<div class="stat">
<span class="stat-label">FPS</span>
<span class="stat-value" id="fps">0</span>
</div>
</div>
</div>
</main>
<script>
const shaderCode = `
struct Uniforms {
resolution: vec2f,
time: f32,
mass: f32,
innerRadius: f32,
outerRadius: f32,
temperature: f32,
cameraDist: f32,
viewAngle: f32,
padding: f32,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
}
@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
var positions = array<vec2f, 6>(
vec2f(-1.0, -1.0),
vec2f(1.0, -1.0),
vec2f(-1.0, 1.0),
vec2f(-1.0, 1.0),
vec2f(1.0, -1.0),
vec2f(1.0, 1.0)
);
var output: VertexOutput;
output.position = vec4f(positions[vertexIndex], 0.0, 1.0);
output.uv = positions[vertexIndex] * 0.5 + 0.5;
return output;
}
const PI: f32 = 3.14159265359;
const MAX_STEPS: i32 = 300;
const STEP_SIZE: f32 = 0.15;
fn rotateX(p: vec3f, angle: f32) -> vec3f {
let c = cos(angle);
let s = sin(angle);
return vec3f(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
}
fn rotateY(p: vec3f, angle: f32) -> vec3f {
let c = cos(angle);
let s = sin(angle);
return vec3f(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
}
fn hash(p: vec2f) -> f32 {
let h = dot(p, vec2f(127.1, 311.7));
return fract(sin(h) * 43758.5453123);
}
fn noise(p: vec2f) -> f32 {
let i = floor(p);
let f = fract(p);
let u = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i + vec2f(0.0, 0.0)), hash(i + vec2f(1.0, 0.0)), u.x),
mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), u.x),
u.y
);
}
fn fbm(p: vec2f) -> f32 {
var value: f32 = 0.0;
var amplitude: f32 = 0.5;
var frequency: f32 = 1.0;
var pp = p;
for (var i: i32 = 0; i < 5; i++) {
value += amplitude * noise(pp * frequency);
amplitude *= 0.5;
frequency *= 2.0;
}
return value;
}
fn blackbodyColor(temp: f32) -> vec3f {
let t = temp * 6500.0;
var color: vec3f;
if (t < 6600.0) {
color.r = 1.0;
color.g = saturate(0.39 * log(t / 100.0) - 0.634);
if (t < 2000.0) {
color.b = 0.0;
} else {
color.b = saturate(0.543 * log(t / 100.0 - 10.0) - 1.185);
}
} else {
color.r = saturate(1.29 * pow(t / 100.0 - 60.0, -0.1332));
color.g = saturate(1.13 * pow(t / 100.0 - 60.0, -0.0755));
color.b = 1.0;
}
return color;
}
fn diskColor(r: f32, phi: f32, time: f32) -> vec3f {
let innerR = uniforms.innerRadius * uniforms.mass;
let outerR = uniforms.outerRadius * uniforms.mass;
let normalizedR = (r - innerR) / (outerR - innerR);
let temp = uniforms.temperature * pow(1.0 - normalizedR, 0.75) * (0.8 + 0.4 * fbm(vec2f(phi * 3.0, r * 0.5 + time * 0.1)));
var color = blackbodyColor(temp);
let spiral = sin(phi * 4.0 - r * 0.5 + time * 2.0) * 0.5 + 0.5;
let turbulence = fbm(vec2f(phi * 5.0 + time * 0.5, r * 2.0)) * 0.3;
color *= (0.7 + 0.3 * spiral + turbulence);
let falloff = smoothstep(innerR, innerR + 0.5, r) * smoothstep(outerR, outerR - 2.0, r);
color *= falloff;
let glow = exp(-normalizedR * 2.0) * 2.0;
color += vec3f(1.0, 0.5, 0.2) * glow * 0.3;
return color;
}
fn rayMarchBlackHole(ro: vec3f, rd: vec3f) -> vec3f {
let rs = 2.0 * uniforms.mass;
let photonSphere = 1.5 * rs;
let innerR = uniforms.innerRadius * uniforms.mass;
let outerR = uniforms.outerRadius * uniforms.mass;
var pos = ro;
var dir = rd;
var color = vec3f(0.0);
var accumulated = vec3f(0.0);
var transmittance: f32 = 1.0;
for (var i: i32 = 0; i < MAX_STEPS; i++) {
let r = length(pos);
if (r < rs * 1.01) {
return accumulated;
}
if (r > 100.0) {
let starField = pow(hash(dir.xy * 1000.0), 20.0) * 0.8;
let nebula = fbm(dir.xy * 3.0 + dir.z) * 0.1;
accumulated += transmittance * (vec3f(starField) + vec3f(0.1, 0.05, 0.15) * nebula);
break;
}
let diskY = pos.y;
let diskR = length(pos.xz);
if (abs(diskY) < 0.3 && diskR > innerR && diskR < outerR) {
let phi = atan2(pos.z, pos.x) + PI;
let diskCol = diskColor(diskR, phi, uniforms.time);
let density = (1.0 - abs(diskY) / 0.3) * 0.5;
accumulated += transmittance * diskCol * density;
transmittance *= (1.0 - density * 0.3);
if (transmittance < 0.01) {
break;
}
}
let schwarzschildFactor = 1.0 - rs / r;
let bendStrength = rs / (r * r) * 1.5;
let toCenter = -normalize(pos);
let perpComponent = dir - toCenter * dot(dir, toCenter);
dir = normalize(dir + toCenter * bendStrength * STEP_SIZE);
let adaptiveStep = STEP_SIZE * (1.0 + r * 0.05);
pos += dir * adaptiveStep;
}
return accumulated;
}
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
let uv = (input.uv * 2.0 - 1.0) * vec2f(uniforms.resolution.x / uniforms.resolution.y, 1.0);
let viewAngleRad = uniforms.viewAngle * PI / 180.0;
let camDist = uniforms.cameraDist * uniforms.mass;
var camPos = vec3f(0.0, 0.0, camDist);
camPos = rotateX(camPos, PI / 2.0 - viewAngleRad);
let fov: f32 = 1.5;
var rayDir = normalize(vec3f(uv.x * fov, uv.y * fov, -1.0));
rayDir = rotateX(rayDir, PI / 2.0 - viewAngleRad);
var color = rayMarchBlackHole(camPos, rayDir);
color = pow(color, vec3f(0.85));
color = color / (color + vec3f(1.0));
let vignette = 1.0 - length(input.uv - 0.5) * 0.5;
color *= vignette;
color = pow(color, vec3f(1.0 / 2.2));
return vec4f(color, 1.0);
}
`;
async function initWebGPU() {
const canvas = document.getElementById('webgpu-canvas');
if (!navigator.gpu) {
showError();
return null;
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
showError();
return null;
}
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: format,
alphaMode: 'premultiplied',
});
return { device, context, format, canvas };
}
function showError() {
const container = document.querySelector('.canvas-container');
container.innerHTML = `
<div class="error-message">
<h2>WebGPU Not Supported</h2>
<p>Your browser doesn't support WebGPU. Please try using Chrome 113+ or Edge 113+ with WebGPU enabled.</p>
</div>
`;
}
async function main() {
const gpu = await initWebGPU();
if (!gpu) return;
const { device, context, format, canvas } = gpu;
const shaderModule = device.createShaderModule({
code: shaderCode
});
const uniformBuffer = device.createBuffer({
size: 48,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const bindGroupLayout = device.createBindGroupLayout({
entries: [{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
buffer: { type: 'uniform' }
}]
});
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}]
});
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout]
});
const pipeline = device.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module: shaderModule,
entryPoint: 'vertexMain',
},
fragment: {
module: shaderModule,
entryPoint: 'fragmentMain',
targets: [{ format: format }],
},
primitive: {
topology: 'triangle-list',
},
});
const params = {
mass: 1.0,
innerRadius: 3.0,
outerRadius: 12.0,
temperature: 1.0,
cameraDist: 20.0,
viewAngle: 75.0,
};
function setupControls() {
const controls = ['mass', 'inner-radius', 'outer-radius', 'temperature', 'camera-dist', 'view-angle'];
const paramNames = ['mass', 'innerRadius', 'outerRadius', 'temperature', 'cameraDist', 'viewAngle'];
controls.forEach((id, index) => {
const input = document.getElementById(id);
const valueSpan = document.getElementById(`${id}-value`);
input.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
params[paramNames[index]] = value;
valueSpan.textContent = value.toFixed(1);
updateStats();
});
});
}
function updateStats() {
const rs = 2.0 * params.mass;
document.getElementById('schwarzschild-radius').textContent = `${rs.toFixed(1)} Rs`;
document.getElementById('photon-sphere').textContent = `${(1.5 * rs).toFixed(1)} Rs`;
document.getElementById('isco').textContent = `${(3.0 * rs).toFixed(1)} Rs`;
}
setupControls();
updateStats();
let lastTime = performance.now();
let frameCount = 0;
let fps = 0;
function resizeCanvas() {
const container = canvas.parentElement;
const rect = container.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio, 2);
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
function render(time) {
frameCount++;
const now = performance.now();
if (now - lastTime >= 1000) {
fps = frameCount;
frameCount = 0;
lastTime = now;
document.getElementById('fps').textContent = fps;
}
resizeCanvas();
const uniformData = new Float32Array([
canvas.width, canvas.height,
time * 0.001,
params.mass,
params.innerRadius,
params.outerRadius,
params.temperature,
params.cameraDist,
params.viewAngle,
0.0,
]);
device.queue.writeBuffer(uniformBuffer, 0, uniformData);
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [{
view: textureView,
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store',
}]
});
renderPass.setPipeline(pipeline);
renderPass.setBindGroup(0, bindGroup);
renderPass.draw(6, 1, 0, 0);
renderPass.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>
</body>
</html>