anycoder-37d1a261 / index.html
idgmatrix's picture
Upload folder using huggingface_hub
9e1cdc2 verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGPU Black Hole Simulation</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Noto+Sans+KR:wght@300;500;700&display=swap');
:root {
--primary-color: #00d2ff;
--secondary-color: #ff0055;
--bg-color: #050505;
--panel-bg: rgba(20, 20, 30, 0.75);
--text-color: #ffffff;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Noto Sans KR', sans-serif;
overflow: hidden;
width: 100vw;
height: 100vh;
}
/* Header / Branding */
header {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
pointer-events: none; /* click through to canvas */
}
.brand {
font-family: 'Orbitron', sans-serif;
font-weight: 700;
font-size: 1.5rem;
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(0, 210, 255, 0.5);
pointer-events: auto;
}
.anycoder-link {
font-family: 'Orbitron', sans-serif;
font-size: 0.9rem;
color: var(--text-color);
text-decoration: none;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 8px 16px;
border-radius: 20px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
transition: all 0.3s ease;
pointer-events: auto;
}
.anycoder-link:hover {
background: var(--primary-color);
color: #000;
box-shadow: 0 0 15px var(--primary-color);
border-color: var(--primary-color);
}
/* Canvas */
canvas {
display: block;
width: 100%;
height: 100%;
}
/* UI Overlay */
.controls {
position: absolute;
bottom: 2rem;
left: 2rem;
width: 300px;
background: var(--panel-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 1.5rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
z-index: 100;
transition: opacity 0.3s ease;
}
.controls h2 {
font-family: 'Orbitron', sans-serif;
font-size: 1.1rem;
margin-bottom: 1rem;
color: var(--primary-color);
display: flex;
align-items: center;
gap: 10px;
}
.control-group {
margin-bottom: 1rem;
}
.control-group label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: #ccc;
}
.control-group input[type="range"] {
width: 100%;
-webkit-appearance: none;
background: transparent;
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.1);
outline: none;
}
.control-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
box-shadow: 0 0 10px var(--primary-color);
margin-top: -5px;
transition: transform 0.1s;
}
.control-group input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.control-group input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 6px;
cursor: pointer;
}
.status {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
z-index: 200;
background: rgba(0,0,0,0.9);
padding: 2rem;
border-radius: 10px;
border: 1px solid var(--secondary-color);
display: none; /* Hidden by default */
}
.status.error {
display: block;
color: var(--secondary-color);
}
.status h3 {
margin-bottom: 1rem;
font-family: 'Orbitron', sans-serif;
}
/* Loading Spinner */
.loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50px;
height: 50px;
border: 3px solid rgba(255,255,255,0.1);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
z-index: 150;
}
@keyframes spin {
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.hidden {
display: none !important;
}
.instructions {
position: absolute;
bottom: 2rem;
right: 2rem;
text-align: right;
font-size: 0.8rem;
color: rgba(255,255,255,0.5);
pointer-events: none;
}
</style>
</head>
<body>
<header>
<div class="brand">GARGANTUA <span style="color:var(--primary-color); font-size: 0.8em;">WebGPU</span></div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
</header>
<div id="loader" class="loader"></div>
<div id="error-modal" class="status">
<h3>WebGPU ์ง€์› ๋ถˆ๊ฐ€</h3>
<p>์ด ๋ธŒ๋ผ์šฐ์ €๋Š” WebGPU๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.<br>Chrome ์ตœ์‹  ๋ฒ„์ „์ด๋‚˜ Edge๋ฅผ ์‚ฌ์šฉํ•ด ์ฃผ์„ธ์š”.</p>
</div>
<canvas id="gpuCanvas"></canvas>
<div class="controls">
<h2>
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/></svg>
์ œ์–ด ํŒจ๋„
</h2>
<div class="control-group">
<label>๋ธ”๋ž™ํ™€ ์งˆ๋Ÿ‰ (Mass) <span id="val-mass">1.0</span></label>
<input type="range" id="mass" min="0.1" max="3.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>์›๋ฐ˜ ๋ฐ๊ธฐ (Intensity) <span id="val-intensity">1.5</span></label>
<input type="range" id="intensity" min="0.1" max="5.0" step="0.1" value="1.5">
</div>
<div class="control-group">
<label>๊ฐ€์Šค ์˜จ๋„ (Temperature) <span id="val-temp">0.5</span></label>
<input type="range" id="temp" min="0.0" max="1.0" step="0.01" value="0.5">
</div>
<div class="control-group">
<label>์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์†๋„ (Speed) <span id="val-speed">1.0</span></label>
<input type="range" id="speed" min="0.0" max="5.0" step="0.1" value="1.0">
</div>
</div>
<div class="instructions">
๋“œ๋ž˜๊ทธํ•˜์—ฌ ํšŒ์ „ / ํœ ๋กœ ์คŒ
</div>
<script type="module">
// WebGPU Setup and Logic
const canvas = document.getElementById('gpuCanvas');
const loader = document.getElementById('loader');
const errorModal = document.getElementById('error-modal');
// UI Elements
const ui = {
mass: document.getElementById('mass'),
intensity: document.getElementById('intensity'),
temp: document.getElementById('temp'),
speed: document.getElementById('speed'),
valMass: document.getElementById('val-mass'),
valIntensity: document.getElementById('val-intensity'),
valTemp: document.getElementById('val-temp'),
valSpeed: document.getElementById('val-speed'),
};
// State
const state = {
mass: 1.0,
intensity: 1.5,
temperature: 0.5,
speed: 1.0,
time: 0,
camRadius: 8.0,
camTheta: 0.0, // Horizontal
camPhi: 0.3, // Vertical
mouseDown: false,
lastMouseX: 0,
lastMouseY: 0
};
// Update UI Labels
const updateLabels = () => {
ui.valMass.innerText = state.mass.toFixed(1);
ui.valIntensity.innerText = state.intensity.toFixed(1);
ui.valTemp.innerText = state.temperature.toFixed(2);
ui.valSpeed.innerText = state.speed.toFixed(1);
};
// Event Listeners for UI
ui.mass.addEventListener('input', (e) => { state.mass = parseFloat(e.target.value); updateLabels(); });
ui.intensity.addEventListener('input', (e) => { state.intensity = parseFloat(e.target.value); updateLabels(); });
ui.temp.addEventListener('input', (e) => { state.temperature = parseFloat(e.target.value); updateLabels(); });
ui.speed.addEventListener('input', (e) => { state.speed = parseFloat(e.target.value); updateLabels(); });
// Mouse Controls
canvas.addEventListener('mousedown', (e) => {
state.mouseDown = true;
state.lastMouseX = e.clientX;
state.lastMouseY = e.clientY;
});
window.addEventListener('mouseup', () => state.mouseDown = false);
window.addEventListener('mousemove', (e) => {
if (!state.mouseDown) return;
const dx = e.clientX - state.lastMouseX;
const dy = e.clientY - state.lastMouseY;
state.lastMouseX = e.clientX;
state.lastMouseY = e.clientY;
state.camTheta -= dx * 0.005;
state.camPhi += dy * 0.005;
// Clamp vertical angle to avoid flipping
state.camPhi = Math.max(-1.5, Math.min(1.5, state.camPhi));
});
canvas.addEventListener('wheel', (e) => {
state.camRadius += e.deltaY * 0.01;
state.camRadius = Math.max(3.0, Math.min(20.0, state.camRadius));
});
// WGSL Shader Code
const shaderCode = `
struct Uniforms {
resolution: vec2f,
time: f32,
mass: f32,
intensity: f32,
temperature: f32,
camPos: vec3f,
camTarget: vec3f,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
};
@vertex
fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
var pos = 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(pos[vertexIndex], 0.0, 1.0);
output.uv = pos[vertexIndex]; // -1 to 1
return output;
}
// --- Noise Functions for Accretion Disk ---
fn hash(p: vec2f) -> f32 {
var p3 = fract(vec3f(p.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
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 st = p;
for (var i = 0; i < 5; i++) {
value += amplitude * noise(st);
st *= 2.0;
amplitude *= 0.5;
}
return value;
}
// --- Physics & Rendering ---
// Rotate vector
fn rotateY(v: vec3f, angle: f32) -> vec3f {
let c = cos(angle);
let s = sin(angle);
return vec3f(c * v.x + s * v.z, v.y, -s * v.x + c * v.z);
}
// Blackbody-ish color ramp
fn blackBodyColor(t: f32) -> vec3f {
// Map t (0 to 1) to colors: Red -> Orange -> White -> Blue
let r = smoothstep(0.0, 0.5, t) + smoothstep(0.3, 1.0, t);
let g = smoothstep(0.2, 0.7, t) + smoothstep(0.8, 1.0, t) * 0.5;
let b = smoothstep(0.5, 0.9, t) + smoothstep(0.9, 1.0, t) * 2.0;
return vec3f(r, g, b) * u.intensity;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
// 1. Setup Camera
let uv = in.uv * vec2f(u.resolution.x / u.resolution.y, 1.0);
let ro = u.camPos;
let ta = u.camTarget;
let ww = normalize(ta - ro);
let uu = normalize(cross(ww, vec3f(0.0, 1.0, 0.0)));
let vv = normalize(cross(uu, ww));
var rd = normalize(uv.x * uu + uv.y * vv + 1.5 * ww);
// 2. Ray Marching Simulation (Curved Space-Time approximation)
var pos = ro;
var color = vec3f(0.0);
var glow = 0.0;
let dt = 0.05; // Step size
let maxSteps = 300;
// Accretion disk parameters
let innerRadius = 2.0 * u.mass; // Schwarzschild radius * scaling
let diskInner = 2.6 * u.mass;
let diskOuter = 8.0 * u.mass;
var hitHorizon = false;
for(var i = 0; i < maxSteps; i++) {
let distSq = dot(pos, pos);
let dist = sqrt(distSq);
// Event Horizon Check
if (dist < innerRadius) {
hitHorizon = true;
break;
}
// Escape Check
if (dist > 25.0) {
break;
}
// Gravity Bending (Newtonian approx for visual flair)
// Acceleration towards center
let force = (1.5 * u.mass) / (distSq * 1.5); // Tweaked for visuals
let acc = -normalize(pos) * force;
// Update Velocity (Ray direction)
rd += acc * dt;
rd = normalize(rd);
// Update Position
let prevPos = pos;
pos += rd * dt;
// --- Accretion Disk Rendering ---
// Check if we crossed the Y=0 plane
if (prevPos.y * pos.y < 0.0 || abs(pos.y) < 0.1) {
let midPoint = (prevPos + pos) * 0.5;
let r = length(midPoint.xz);
if (r > diskInner && r < diskOuter) {
// Calculate polar coordinates for noise
let angle = atan2(midPoint.z, midPoint.x);
// Rotating texture
let uvDisk = vec2f(r - diskInner, angle + u.time * 0.5 + 10.0/r);
// Noise generation
var density = fbm(uvDisk * vec2f(1.0, 3.0));
// Radial fade out
let fade = smoothstep(diskOuter, diskOuter - 1.0, r) * smoothstep(diskInner, diskInner + 0.5, r);
density *= fade;
// Doppler Beaming Approximation
// Matter on left moves towards us (brighter), right moves away (dimmer)
// Assuming camera is somewhat aligned with z-axis logic
let velDir = normalize(vec3f(-midPoint.z, 0.0, midPoint.x)); // Tangential velocity
let viewDir = normalize(ro - midPoint);
let doppler = 1.0 + dot(velDir, viewDir) * 0.5;
// Color mapping
let temp = density * doppler;
let diskCol = blackBodyColor(temp * (0.5 + u.temperature));
// Accumulate (Volumetric-ish addition)
color += diskCol * density * 0.4 * u.intensity;
}
}
// Accumulate Glow around the black hole
glow += 0.01 / (dist * dist * 0.1 + 0.01);
}
// Stars / Background
if (!hitHorizon) {
let starDir = rd;
let starVal = pow(hash(starDir.xy * 50.0 + starDir.zz * 50.0), 50.0) * 2.0;
color += vec3f(starVal);
}
// Add Glow
color += vec3f(0.1, 0.3, 0.5) * glow * 0.5;
// Tone mapping
color = color / (color + vec3f(1.0));
color = pow(color, vec3f(0.4545)); // Gamma correction
return vec4f(color, 1.0);
}
`;
async function init() {
if (!navigator.gpu) {
loader.classList.add('hidden');
errorModal.classList.add('error');
return;
}
try {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("No adapter found");
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: format,
alphaMode: 'opaque',
});
// Create Shader Module
const shaderModule = device.createShaderModule({
label: 'Black Hole Shader',
code: shaderCode
});
// Create Pipeline
const pipeline = device.createRenderPipeline({
label: 'Render Pipeline',
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [{ format: format }],
},
primitive: {
topology: 'triangle-list',
}
});
// Uniform Buffer Setup
// Struct: resolution(vec2), time(f32), mass(f32), intensity(f32), temperature(f32), camPos(vec3), camTarget(vec3)
// Padding rules: vec3 takes 16 bytes (4 floats) alignment.
// Layout:
// 0: res.x, res.y, time, mass
// 16: intensity, temperature, padding, padding
// 32: camPos.x, camPos.y, camPos.z, padding
// 48: camTarget.x, camTarget.y, camTarget.z, padding
// Total: 64 bytes
const uniformBufferSize = 64;
const uniformBuffer = device.createBuffer({
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}]
});
const uniformValues = new Float32Array(uniformBufferSize / 4);
loader.classList.add('hidden');
function render(timestamp) {
state.time += 0.01 * state.speed;
// Update Canvas Size
const width = canvas.clientWidth;
const height = canvas.clientHeight;
canvas.width = width;
canvas.height = height;
// Update Camera Position (Spherical coords)
const camX = state.camRadius * Math.sin(state.camTheta) * Math.cos(state.camPhi);
const camY = state.camRadius * Math.sin(state.camPhi);
const camZ = state.camRadius * Math.cos(state.camTheta) * Math.cos(state.camPhi);
// Update Uniforms
// 0: res.x, res.y, time, mass
uniformValues[0] = width;
uniformValues[1] = height;
uniformValues[2] = state.time;
uniformValues[3] = state.mass;
// 4: intensity, temp, padding, padding
uniformValues[4] = state.intensity;
uniformValues[5] = state.temperature;
uniformValues[6] = 0; // padding
uniformValues[7] = 0; // padding
// 8: camPos (vec3 + padding)
uniformValues[8] = camX;
uniformValues[9] = camY;
uniformValues[10] = camZ;
uniformValues[11] = 0;
// 12: camTarget (vec3 + padding)
uniformValues[12] = 0;
uniformValues[13] = 0;
uniformValues[14] = 0;
uniformValues[15] = 0;
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPassDescriptor = {
colorAttachments: [{
view: textureView,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
}]
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(6); // Draw 6 vertices (2 triangles = 1 quad)
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
} catch (e) {
console.error(e);
loader.classList.add('hidden');
errorModal.innerHTML = `<h3>์˜ค๋ฅ˜ ๋ฐœ์ƒ</h3><p>${e.message}</p>`;
errorModal.classList.add('error');
}
}
init();
</script>
</body>
</html>