Skip to content
LAM
Read Home Blog
Make Projects HTML Tools Games
Touch grass Notes Resume Links
Home Blog HTML Projects
Tools Games Notes Resume Links
Back ORBITAL_COMMAND // GRAVITY_WELL Physics
Download Open
Show description 351 chars · Physics

ORBITAL_COMMAND // GRAVITY_WELL

ORBITAL_COMMAND // GRAVITY_WELL





GRAVITY_WELL // v1.0.4

PHYSICS: ACTIVE








DRAG & RELEASE TO LAUNCH SATELLITE
SCROLL TO ZOOM







Time Scale (Warp) 1.0x





Planet Mass (G) 1.0x





Prediction Steps 500





OBJECTS: 0

HIGHEST VEL: 0.0 km/s

AVG ALTITUDE: 0.0 km





Clear Debris
Reset System

ORBITAL_COMMAND // GRAVITY_WELL

20,222 bytes · HTML source
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ORBITAL_COMMAND // GRAVITY_WELL</title>
    <style>
        :root {
            --bg-deep: #050505;
            --bg-panel: rgba(15, 15, 20, 0.85);
            --text-main: #e0e0e0;
            --text-dim: #555;
            --neon-blue: #00f3ff;
            --neon-green: #00ff9d;
            --neon-warn: #ffcc00;
            --neon-danger: #ff0055;
            --font-mono: 'Courier New', Courier, monospace;
        }

        * { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }

        body {
            background-color: var(--bg-deep);
            color: var(--text-main);
            font-family: var(--font-mono);
            height: 100vh;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }

        /* --- HEADER --- */
        header {
            height: 50px;
            border-bottom: 1px solid rgba(255,255,255,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 0 1.5rem;
            background: rgba(0,0,0,0.8);
            z-index: 10;
        }

        h1 {
            font-size: 1rem;
            letter-spacing: 2px;
            color: var(--neon-blue);
            text-shadow: 0 0 10px rgba(0, 243, 255, 0.3);
        }

        .status-badge {
            font-size: 0.7rem;
            padding: 4px 8px;
            border: 1px solid var(--neon-green);
            color: var(--neon-green);
            border-radius: 4px;
        }

        /* --- MAIN LAYOUT --- */
        main {
            flex: 1;
            position: relative;
            display: flex;
        }

        /* CANVAS LAYER */
        .viewport {
            flex: 1;
            position: relative;
            cursor: crosshair;
            background: radial-gradient(circle at center, #0a0a10 0%, #000 80%);
            overflow: hidden;
        }

        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }

        /* SIDEBAR CONTROLS */
        aside {
            width: 300px;
            background: var(--bg-panel);
            border-left: 1px solid rgba(255,255,255,0.1);
            backdrop-filter: blur(10px);
            padding: 1.5rem;
            display: flex;
            flex-direction: column;
            gap: 1.5rem;
            z-index: 20;
        }

        .control-group {
            display: flex;
            flex-direction: column;
            gap: 0.5rem;
        }

        label {
            font-size: 0.7rem;
            text-transform: uppercase;
            color: var(--text-dim);
            letter-spacing: 1px;
            display: flex;
            justify-content: space-between;
        }

        input[type="range"] {
            -webkit-appearance: none;
            width: 100%;
            height: 4px;
            background: #333;
            outline: none;
            border-radius: 2px;
        }

        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 12px;
            height: 12px;
            background: var(--neon-blue);
            cursor: pointer;
            box-shadow: 0 0 10px var(--neon-blue);
        }

        button {
            background: transparent;
            border: 1px solid var(--neon-blue);
            color: var(--neon-blue);
            padding: 10px;
            font-family: var(--font-mono);
            font-size: 0.8rem;
            cursor: pointer;
            text-transform: uppercase;
            transition: 0.2s;
        }

        button:hover {
            background: var(--neon-blue);
            color: #000;
        }

        button.danger {
            border-color: var(--neon-danger);
            color: var(--neon-danger);
        }
        button.danger:hover {
            background: var(--neon-danger);
            color: #fff;
        }

        .data-readout {
            font-size: 0.8rem;
            line-height: 1.6;
            color: #888;
            border-top: 1px dashed #333;
            padding-top: 1rem;
        }
        
        .val-highlight { color: #fff; }

        /* OVERLAY INSTRUCTIONS */
        .overlay-text {
            position: absolute;
            bottom: 20px;
            left: 20px;
            color: rgba(255,255,255,0.3);
            font-size: 0.8rem;
            pointer-events: none;
        }

        @media (max-width: 800px) {
            main { flex-direction: column; }
            aside { width: 100%; height: 300px; overflow-y: auto; border-left: none; border-top: 1px solid #333; }
        }
    </style>
</head>
<body>

    <header>
        <h1>GRAVITY_WELL <span style="font-size:0.8em; color:#666;">// v1.0.4</span></h1>
        <div class="status-badge">PHYSICS: ACTIVE</div>
    </header>

    <main>
        <div class="viewport" id="viewport">
            <canvas id="spaceCanvas"></canvas>
            <div class="overlay-text">
                DRAG & RELEASE TO LAUNCH SATELLITE<br>
                SCROLL TO ZOOM
            </div>
        </div>

        <aside>
            <div class="control-group">
                <label>Time Scale (Warp) <span class="val-highlight" id="val-time">1.0x</span></label>
                <input type="range" id="timeScale" min="0.1" max="5.0" step="0.1" value="1.0">
            </div>

            <div class="control-group">
                <label>Planet Mass (G) <span class="val-highlight" id="val-mass">1.0x</span></label>
                <input type="range" id="planetMass" min="0.5" max="3.0" step="0.1" value="1.0">
            </div>

            <div class="control-group">
                <label>Prediction Steps <span class="val-highlight" id="val-pred">500</span></label>
                <input type="range" id="predSteps" min="100" max="2000" step="100" value="800">
            </div>

            <div class="data-readout">
                <div>OBJECTS: <span class="val-highlight" id="objCount">0</span></div>
                <div>HIGHEST VEL: <span class="val-highlight" id="maxVel">0.0</span> km/s</div>
                <div>AVG ALTITUDE: <span class="val-highlight" id="avgAlt">0.0</span> km</div>
            </div>

            <!-- Removed inline onclick handlers, added IDs for JS binding -->
            <button id="btnClear" class="danger">Clear Debris</button>
            <button id="btnReset">Reset System</button>
        </aside>
    </main>

    <script>
        // IIFE (Immediately Invoked Function Expression) to prevent global scope pollution
        // and "Identifier has already been declared" errors on re-run.
        (function() {

            // --- PHYSICS CONFIG ---
            const G = 0.5; // Gravitational Constant (Tweaked for pixel space)
            let BASE_MASS = 2000;
            let TIME_STEP = 1.0;
            let PREDICTION_STEPS = 800;

            // --- ENGINE STATE ---
            const canvas = document.getElementById('spaceCanvas');
            const ctx = canvas.getContext('2d');
            let width, height;
            let cx, cy; // Center X, Y
            
            let satellites = [];
            let particles = []; // For explosions/trails
            
            // Input State
            let isDragging = false;
            let dragStart = {x:0, y:0};
            let dragCurrent = {x:0, y:0};
            let zoom = 1.0;
            let animationFrameId;

            // --- CLASSES ---

            class Satellite {
                constructor(x, y, vx, vy) {
                    this.x = x;
                    this.y = y;
                    this.vx = vx;
                    this.vy = vy;
                    this.color = `hsl(${Math.random()*60 + 180}, 100%, 70%)`; // Cyans/Blues
                    this.trail = [];
                    this.crashed = false;
                }

                update(dt) {
                    if (this.crashed) return;

                    // Physics: F = G*M*m / r^2
                    // Vector Math
                    const dx = cx - this.x;
                    const dy = cy - this.y;
                    const distSq = dx*dx + dy*dy;
                    const dist = Math.sqrt(distSq);

                    // Collision Detection with Planet (Radius 40)
                    if (dist < 40) {
                        this.crashed = true;
                        spawnExplosion(this.x, this.y, this.color);
                        return;
                    }

                    // Gravity Force
                    const force = (G * BASE_MASS) / distSq;
                    
                    // F = ma (assume m=1 for satellite) -> a = F
                    const ax = (dx / dist) * force;
                    const ay = (dy / dist) * force;

                    // Symplectic Euler / Semi-Implicit
                    this.vx += ax * dt;
                    this.vy += ay * dt;
                    
                    this.x += this.vx * dt;
                    this.y += this.vy * dt;

                    // Trail Logic
                    if (frame % 5 === 0) {
                        this.trail.push({x: this.x, y: this.y});
                        if (this.trail.length > 50) this.trail.shift();
                    }
                }

                draw() {
                    if (this.crashed) return;

                    // Draw Trail
                    ctx.beginPath();
                    ctx.strokeStyle = this.color;
                    ctx.lineWidth = 1;
                    for (let i = 0; i < this.trail.length - 1; i++) {
                        // Fade trail opacity
                        ctx.globalAlpha = i / this.trail.length;
                        ctx.moveTo(this.trail[i].x, this.trail[i].y);
                        ctx.lineTo(this.trail[i+1].x, this.trail[i+1].y);
                    }
                    ctx.stroke();
                    ctx.globalAlpha = 1.0;

                    // Draw Body
                    ctx.fillStyle = "#fff";
                    ctx.beginPath();
                    ctx.arc(this.x, this.y, 3, 0, Math.PI*2);
                    ctx.fill();
                }
            }

            class Particle {
                constructor(x, y, color) {
                    this.x = x;
                    this.y = y;
                    this.vx = (Math.random() - 0.5) * 4;
                    this.vy = (Math.random() - 0.5) * 4;
                    this.life = 1.0;
                    this.color = color;
                }
                update() {
                    this.x += this.vx;
                    this.y += this.vy;
                    this.life -= 0.02;
                }
                draw() {
                    ctx.globalAlpha = this.life;
                    ctx.fillStyle = this.color;
                    ctx.fillRect(this.x, this.y, 2, 2);
                    ctx.globalAlpha = 1.0;
                }
            }

            // --- CORE FUNCTIONS ---

            function init() {
                resize();
                window.addEventListener('resize', resize);
                // Cancel previous loop if running
                if (window.__orbital_anim_id) {
                    cancelAnimationFrame(window.__orbital_anim_id);
                }
                animate();
            }

            function resize() {
                const container = document.getElementById('viewport');
                if (!container) return;
                width = container.clientWidth;
                height = container.clientHeight;
                canvas.width = width;
                canvas.height = height;
                cx = width / 2;
                cy = height / 2;
            }

            function spawnExplosion(x, y, color) {
                for(let i=0; i<20; i++) {
                    particles.push(new Particle(x, y, color));
                }
            }

            // --- PREDICTION ENGINE ---
            function drawPrediction(startX, startY, velX, velY) {
                let px = startX;
                let py = startY;
                let pvx = velX;
                let pvy = velY;

                ctx.beginPath();
                ctx.strokeStyle = "rgba(255, 255, 255, 0.4)";
                ctx.setLineDash([5, 5]);

                ctx.moveTo(px, py);

                for(let i=0; i<PREDICTION_STEPS; i++) {
                    const dx = cx - px;
                    const dy = cy - py;
                    const distSq = dx*dx + dy*dy;
                    const dist = Math.sqrt(distSq);

                    if (dist < 40) break; // Crash prediction

                    const force = (G * BASE_MASS) / distSq;
                    const ax = (dx / dist) * force;
                    const ay = (dy / dist) * force;

                    pvx += ax; 
                    pvy += ay;
                    px += pvx;
                    py += pvy;

                    if(i % 5 === 0) ctx.lineTo(px, py);
                }
                ctx.stroke();
                ctx.setLineDash([]);
            }

            // --- MAIN LOOP ---
            let frame = 0;
            function animate() {
                frame++;
                
                // Clear Background
                ctx.fillStyle = "rgba(5, 5, 8, 0.4)"; // Trails effect
                ctx.fillRect(0, 0, width, height);

                // Draw Grid
                drawGrid();

                // Draw Planet
                ctx.shadowBlur = 30;
                ctx.shadowColor = "#00f3ff";
                ctx.fillStyle = "#000";
                ctx.beginPath();
                ctx.arc(cx, cy, 30, 0, Math.PI*2);
                ctx.fill();
                
                ctx.strokeStyle = "#00f3ff";
                ctx.lineWidth = 2;
                ctx.stroke();
                ctx.shadowBlur = 0;

                // Draw Drag Line / Prediction
                if (isDragging) {
                    const vx = (dragStart.x - dragCurrent.x) * 0.05;
                    const vy = (dragStart.y - dragCurrent.y) * 0.05;
                    
                    // Draw Launch Vector
                    ctx.beginPath();
                    ctx.strokeStyle = "#ffcc00";
                    ctx.moveTo(dragStart.x, dragStart.y);
                    ctx.lineTo(dragCurrent.x, dragCurrent.y);
                    ctx.stroke();

                    // Draw Future Path
                    drawPrediction(dragStart.x, dragStart.y, vx, vy);
                }

                // Update & Draw Satellites
                satellites.forEach((sat, index) => {
                    if(sat.crashed) {
                        satellites.splice(index, 1);
                    } else {
                        sat.update(TIME_STEP);
                        sat.draw();
                    }
                });

                // Update Particles
                for(let i=particles.length-1; i>=0; i--) {
                    particles[i].update();
                    particles[i].draw();
                    if(particles[i].life <= 0) particles.splice(i, 1);
                }

                updateUI();
                window.__orbital_anim_id = requestAnimationFrame(animate);
            }

            function drawGrid() {
                ctx.strokeStyle = "rgba(255,255,255,0.03)";
                ctx.lineWidth = 1;
                const gridSize = 50 * zoom;
                
                ctx.beginPath();
                for(let x=0; x<width; x+=gridSize) {
                    ctx.moveTo(x, 0); ctx.lineTo(x, height);
                }
                for(let y=0; y<height; y+=gridSize) {
                    ctx.moveTo(0, y); ctx.lineTo(width, y);
                }
                ctx.stroke();
            }

            function updateUI() {
                document.getElementById('objCount').innerText = satellites.length;
                
                let maxV = 0;
                let totalAlt = 0;
                
                satellites.forEach(s => {
                    const v = Math.sqrt(s.vx*s.vx + s.vy*s.vy);
                    if(v > maxV) maxV = v;
                    
                    const dist = Math.sqrt((s.x-cx)*(s.x-cx) + (s.y-cy)*(s.y-cy));
                    totalAlt += dist;
                });

                document.getElementById('maxVel').innerText = maxV.toFixed(2);
                document.getElementById('avgAlt').innerText = satellites.length ? (totalAlt / satellites.length).toFixed(0) : 0;
            }

            // --- CONTROLS ---

            const view = document.getElementById('viewport');

            view.addEventListener('mousedown', e => {
                const rect = view.getBoundingClientRect();
                isDragging = true;
                dragStart = {
                    x: e.clientX - rect.left,
                    y: e.clientY - rect.top
                };
                dragCurrent = { ...dragStart };
            });

            window.addEventListener('mousemove', e => {
                if (isDragging) {
                    const rect = view.getBoundingClientRect();
                    dragCurrent = {
                        x: e.clientX - rect.left,
                        y: e.clientY - rect.top
                    };
                }
            });

            window.addEventListener('mouseup', e => {
                if (isDragging) {
                    isDragging = false;
                    const vx = (dragStart.x - dragCurrent.x) * 0.05;
                    const vy = (dragStart.y - dragCurrent.y) * 0.05;
                    
                    if (Math.abs(vx) > 0.1 || Math.abs(vy) > 0.1) {
                        satellites.push(new Satellite(dragStart.x, dragStart.y, vx, vy));
                    }
                }
            });

            // Sliders
            document.getElementById('timeScale').addEventListener('input', e => {
                TIME_STEP = parseFloat(e.target.value);
                document.getElementById('val-time').innerText = TIME_STEP.toFixed(1) + 'x';
            });

            document.getElementById('planetMass').addEventListener('input', e => {
                BASE_MASS = 2000 * parseFloat(e.target.value);
                document.getElementById('val-mass').innerText = e.target.value + 'x';
            });

            document.getElementById('predSteps').addEventListener('input', e => {
                PREDICTION_STEPS = parseInt(e.target.value);
                document.getElementById('val-pred').innerText = PREDICTION_STEPS;
            });

            function clearDebris() {
                satellites = [];
                particles = [];
            }

            function resetSim() {
                clearDebris();
                document.getElementById('timeScale').value = 1.0;
                document.getElementById('planetMass').value = 1.0;
                TIME_STEP = 1.0;
                BASE_MASS = 2000;
            }

            // Bind Buttons via JS (since clearDebris/resetSim are now local scoped)
            document.getElementById('btnClear').addEventListener('click', clearDebris);
            document.getElementById('btnReset').addEventListener('click', resetSim);

            // Start Logic
            if (document.readyState === 'complete' || document.readyState === 'interactive') {
                init();
            } else {
                window.addEventListener('load', init);
            }

        })();
    </script>
</body>
</html>