preview
WASD • QE • Click & Drag
🔥 slow (GPU melter) 📱 RIP mobile users
source
<!DOCTYPE html>
<html>
<head>
    <title>SDF Golf: Reset on Fall</title>
    <style>
        body { margin: 0; overflow: hidden; background: #000; font-family: sans-serif; user-select: none; }
        #ui { 
            position: absolute; top: 20px; width: 100%; pointer-events: none; 
            text-align: center; color: white; text-shadow: 0 1px 2px black; 
            display: flex; flex-direction: column; gap: 10px;
        }
        .hud-row { font-size: 14px; opacity: 0.8; }
        .score-row { font-size: 24px; font-weight: bold; letter-spacing: 1px; }
        
        /* Victory Modal Styles */
        #modal {
            display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
            background: rgba(255, 255, 255, 0.95); color: #222; padding: 30px; border-radius: 12px;
            text-align: center; box-shadow: 0 10px 40px rgba(0,0,0,0.5); pointer-events: auto; min-width: 250px;
        }
        #modal h1 { margin: 0 0 10px 0; color: #d35400; text-transform: uppercase; }
        #modal p { font-size: 18px; margin: 5px 0; }
        #modal .big-score { font-size: 42px; font-weight: 800; margin: 15px 0; color: #2c3e50; }
        button {
            background: #27ae60; color: white; border: none; padding: 10px 20px; font-size: 16px;
            border-radius: 6px; cursor: pointer; margin-top: 15px; transition: background 0.2s;
        }
        button:hover { background: #219150; }
    </style>
</head>
<body>
    <div id="ui">
        <div class="hud-row"><b>Left Click+Drag</b> Shoot &bull; <b>WASD</b> Move &bull; <b>Q/E</b> Rotate</div>
        <div class="score-row" id="scoreDisplay">SHOTS: 0 &nbsp;|&nbsp; PAR: 4</div>
    </div>

    <div id="modal">
        <h1>Hole Complete!</h1>
        <p id="msgResult">Par</p>
        <div class="big-score" id="finalScore">4</div>
        <p>Total Shots</p>
        <button onclick="resetGame()">Play Again</button>
    </div>

    <canvas id="glcanvas"></canvas>

<script>
const SDF_LOGIC = `
// Rotation helper (JS/GLSL compatible)
vec3 rotX(vec3 p, float a) {
    float s = sin(a), c = cos(a);
    return vec3(p.x, p.y*c - p.z*s, p.y*s + p.z*c);
}

// 1. NEW PRIMITIVE: Capped Cylinder
// r = radius, h = height (half-height)
float sdCyl(vec3 p, float r, float h) {
    float xz = length(vec3(p.x, 0.0, p.z)) - r;
    float y = abs(p.y) - h;
    // Interior distance (negative) + Exterior distance (positive)
    return min(max(xz, y), 0.0) + length(vec3(max(xz, 0.0), max(y, 0.0), 0.0));
}

float map(vec3 p) {
    // --- FLOOR ---
    float ground = sdBox(vSub(p, vec3(0.0,-1.0,0.0)), vec3(30.0, 1.0, 30.0));
    // Hole at x=10, z=0
    float holeVal = length(vSub(vec3(p.x, 0.0, p.z), vec3(10.0, 0.0, 0.0))) - 1.25;
    float terrain = max(ground, -holeVal);

    // --- OBSTACLES ---
    // Wall
    float wall = sdBox(vSub(p, vec3(4.0, 0.5, 0.0)), vec3(0.5, 1.5, 30.0));
    wall = min(wall, sdBox(vSub(p, vec3(0.0, 0.5, -30.0)), vec3(30.0, 1.5, 1.0)));
    wall = min(wall, sdBox(vSub(p, vec3(0.0, 0.5, 30.0)), vec3(30.0, 1.5, 1.0)));
    wall = min(wall, sdBox(vSub(p, vec3(0.0, 0.5, -10.0)), vec3(5.0, 6.0, 1.0)));
    wall = min(wall, sdBox(vSub(p, vec3(0.0, 0.5, -4.0)), vec3(3.0, 4.0, 1.0)));
    wall = min(wall, sdBox(vSub(p, vec3(0.0, 0.5, -7.0)), vec3(5.0, 1.5, 3.0)));
    
    // Existing Cylinders (Now using the new function)
    float c1 = sdCyl(vSub(p, vec3(8.0, 0.5, 4.0)), 0.8, 1.5);
    float c2 = sdCyl(vSub(p, vec3(8.0, 0.5, -4.0)), 0.8, 1.5);
    
    // --- NEW: SCATTERED CYLINDERS ---
    // 1. Create a local coordinate system shifted behind the hole (x=14)
    vec3 forestP = vSub(p, vec3(14.0, 0.5, 0.0));
    
    // 2. Modulo Repetition: Repeat coordinate space every 2.5 units on Z
    vec3 repP = vec3(forestP.x, forestP.y, mod(forestP.z + 1.25, 2.5) - 1.25);
    
    // 3. Draw infinite cylinders in that repeated space
    float forest = sdCyl(repP, 0.4, 2.0);


    
    // 4. Bounding Box: Intersection to limit the infinite cylinders to a specific zone
    float bounds = sdBox(forestP, vec3(2.0, 5.0, 30.0));
    float scattered = max(forest, bounds);
    wall = min(wall, scattered);

        // again
    forestP = vSub(p, vec3(-20.0, 0.5, 0.0));
    repP = vec3(forestP.x, forestP.y, mod(forestP.z + 1.25, 2.5) - 1.25);
    forest = sdCyl(repP, 0.4, 2.0);
    bounds = sdBox(forestP, vec3(2.0, 5.0, 30.0));
    scattered = max(forest, bounds);
    wall = min(wall, scattered);

    // RAMP
    vec3 rampP = vSub(p, vec3(-5.0, 0.5, 0.0));
    rampP = rotX(rampP, -0.4);
    float ramp = sdBox(rampP, vec3(3.0, 0.2, 4.0));

    // Combine everything
    float obs = min(wall, min(c1, min(c2, min(ramp, scattered))));

    // Smooth Blend
    float k = 0.1; 
    float h = clamp( 0.5 + 0.5 * (obs - terrain) / k, 0.0, 1.0 );
    return mix( obs, terrain, h ) - k * h * (1.0 - h);
}
`;
</script>

<script id="vs" type="x-shader/x-vertex">
    attribute vec2 position;
    void main() { gl_Position = vec4(position, 0.0, 1.0); }
</script>

<script id="fs" type="x-shader/x-fragment">
    precision highp float;
    uniform vec2 u_res;
    uniform vec3 u_camPos, u_camTgt, u_ball, u_as, u_ae;
    uniform float u_sa;

    // --- GLSL SHIMS ---
    vec3 vSub(vec3 a, vec3 b) { return a - b; }
    
    float sdSphere(vec3 p, float s) { return length(p) - s; }
    float sdBox(vec3 p, vec3 b) {
        vec3 q = abs(p) - b;
        return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
    }
    float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
        vec3 pa = p - a, ba = b - a;
        float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
        return length( pa - ba*h ) - r;
    }

    // INJECT LOGIC
    {{SDF_LOGIC}}

    vec2 mapRender(vec3 p) {
        float world = map(p); 
        float ball = sdSphere(p - u_ball, 0.4);
        float arrow = u_sa > 0.5 ? sdCapsule(p, u_as, u_ae, 0.1) : 100.0;
        
        if(ball < world && ball < arrow) return vec2(ball, 1.0);
        if(arrow < world) return vec2(arrow, 2.0);
        return vec2(world, 0.0);
    }

    vec3 calcNormal(vec3 p) {
        const float eps = 0.001; vec2 e = vec2(eps, 0);
        return normalize(vec3(mapRender(p+e.xyy).x - mapRender(p-e.xyy).x, mapRender(p+e.yxy).x - mapRender(p-e.yxy).x, mapRender(p+e.yyx).x - mapRender(p-e.yyx).x));
    }

    float softshadow(vec3 ro, vec3 rd, float mint, float maxt, float k) {
        float res = 1.0;
        float t = mint;
        for(int i=0; i<16; i++) {
            vec3 p = ro + rd*t;
            float h = min(map(p), sdSphere(p - u_ball, 0.4));
            if(h < 0.001) return 0.0;
            res = min(res, k*h/t);
            t += h;
            if(t > maxt) break;
        }
        return res;
    }

    void main() {
        vec2 uv = (gl_FragCoord.xy - 0.5 * u_res.xy) / u_res.y;
        vec3 ro = u_camPos, ta = u_camTgt;
        vec3 cw = normalize(ta - ro), cp = vec3(0,1,0), cu = normalize(cross(cw,cp)), cv = normalize(cross(cu,cw));
        vec3 rd = normalize(uv.x * cu + uv.y * cv + 1.5 * cw);
        
        float t=0.0, id=-1.0;
        for(int i=0; i<80; i++) {
            vec3 p = ro + rd*t;
            vec2 res = mapRender(p);
            if(res.x < 0.001) { id = res.y; break; }
            if(t>80.0) break;
            t += res.x;
        }

        vec3 col = vec3(0.05, 0.08, 0.1); 
        if(id > -0.5) {
            vec3 p = ro + rd*t;
            vec3 n = calcNormal(p);
            vec3 lig = normalize(vec3(-0.8, 0.7, -0.6));
            float amb = 0.5 + 0.5*n.y;
            float diff = max(dot(n, lig), 0.0);
            float sha = softshadow(p, lig, 0.02, 10.0, 16.0);
            vec3 ref = reflect(-lig, n);
            float spec = pow(max(dot(ref, normalize(ro-p)), 0.0), 16.0);

            vec3 mate = vec3(0.18); 
            if(id == 1.0) { mate = vec3(0.9, 0.9, 0.9); spec*=2.0; } 
            else if(id == 2.0) { mate = vec3(1.0, 0.2, 0.1); sha=1.0; } 
            else { 
                float f = mod(floor(p.x*2.0) + floor(p.z*2.0), 2.0);
                mate = vec3(0.2, 0.7, 0.3) * (0.8 + 0.2 * f);
                if(p.y > 0.1) mate = vec3(0.7, 0.4, 0.2); 
            }

            col = mate * amb * 0.3 + mate * diff * sha * 1.5 + vec3(1.0) * spec * sha * 0.5;
            col = mix(col, vec3(0.05, 0.08, 0.1), 1.0 - exp(-0.02*t));
        }
        col = pow(col, vec3(0.4545));
        gl_FragColor = vec4(col, 1.0);
    }
</script>

<script>
// --- VECTOR MATH LIBRARY ---
const vec3 = (x,y,z) => ({x,y,z});
const vSub = (a,b) => ({x:a.x-b.x, y:a.y-b.y, z:a.z-b.z});
const vAdd = (a,b) => ({x:a.x+b.x, y:a.y+b.y, z:a.z+b.z});
const vScale = (v,s) => ({x:v.x*s, y:v.y*s, z:v.z*s});
const vCross = (a,b) => ({ x: a.y*b.z - a.z*b.y, y: a.z*b.x - a.x*b.z, z: a.x*b.y - a.y*b.x });
const vNorm = (v) => { let l = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z); return l===0 ? v : {x:v.x/l, y:v.y/l, z:v.z/l}; };
const vDot = (a,b) => a.x*b.x + a.y*b.y + a.z*b.z;
const length = (v) => Math.sqrt(v.x*v.x + (v.y*v.y || 0) + (v.z*v.z || 0)); 
const max = Math.max; const min = Math.min;

// *** ADDED MOD FUNCTION FOR JS PHYSICS ***
const mod = (x, y) => x - y * Math.floor(x/y);

const abs = (v) => typeof v === 'number' ? Math.abs(v) : {x:Math.abs(v.x), y:Math.abs(v.y), z:Math.abs(v.z)};
const clamp = (v, mn, mx) => Math.min(Math.max(v, mn), mx);
const mix = (a, b, t) => a * (1-t) + b * t;
const sin = Math.sin; const cos = Math.cos;

const sdBox = (p, b) => {
    let qx = Math.abs(p.x)-b.x, qy = Math.abs(p.y)-b.y, qz = Math.abs(p.z)-b.z;
    let outLen = Math.sqrt(Math.max(qx,0)**2 + Math.max(qy,0)**2 + Math.max(qz,0)**2);
    return outLen + Math.min(Math.max(qx, Math.max(qy, qz)), 0.0);
};

// 4. POLYGLOT COMPILER
const JS_SOURCE = SDF_LOGIC
    .replace(/(float|vec2|vec3|int)\s/g, 'let ') 
    .replace(/let\s+map\s*\(\s*let\s+p\s*\)/, 'function map(p)')
    .replace(/let\s+sdCyl\s*\(\s*let\s+p,\s*let\s+r,\s*let\s+h\s*\)/, 'function sdCyl(p, r, h)')
    .replace(/let\s+rotX\s*\(\s*let\s+p,\s*let\s+a\s*\)/, 'function rotX(p, a)');

const header = `
function rotX(p, a) { let s = Math.sin(a), c = Math.cos(a); return {x:p.x, y:p.y*c - p.z*s, z:p.y*s + p.z*c}; }
`;

// Clean up function definitions from source so we don't double define in the eval
let cleanSource = JS_SOURCE.replace(/float\s+rotX[\s\S]*?}/, "");
const map = new Function('p', header + cleanSource + ' return map(p);');

// --- WEBGL BOILERPLATE ---
const cvs = document.getElementById('glcanvas');
const gl = cvs.getContext('webgl');
const pid = gl.createProgram();
const vs = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vs, document.getElementById('vs').text); gl.compileShader(vs);
const fsSrc = document.getElementById('fs').text.replace('{{SDF_LOGIC}}', SDF_LOGIC);
const fs = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fs, fsSrc); gl.compileShader(fs);
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) console.error(gl.getShaderInfoLog(fs));
gl.attachShader(pid, vs); gl.attachShader(pid, fs); gl.linkProgram(pid); gl.useProgram(pid);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

const loc = { res: gl.getUniformLocation(pid, "u_res"), cam: gl.getUniformLocation(pid, "u_camPos"), tgt: gl.getUniformLocation(pid, "u_camTgt"), ball: gl.getUniformLocation(pid, "u_ball"), as: gl.getUniformLocation(pid, "u_as"), ae: gl.getUniformLocation(pid, "u_ae"), sa: gl.getUniformLocation(pid, "u_sa") };

// --- VERLET PHYSICS STATE ---
let ball = { 
    x:-25, y:0.5, z:-25,       
    px:-25, py:0.5, pz:-25,    
    r: 0.4                 
};
const cam = { x:-35, z:-25, angle: -Math.PI/2, dist: 25, pitch: 0.6 };
const keys = {};
let mouse = { x:0, y:0, wx:0, wz:0 }, drag = { on:false, sx:0, sz:0 };

// --- GAME LOGIC STATE ---
let shots = 0;
const PAR = 4;
let gameState = "PLAYING"; // "PLAYING" | "WON"

function updateUI() {
    document.getElementById('scoreDisplay').innerHTML = `SHOTS: ${shots} &nbsp;|&nbsp; PAR: ${PAR}`;
}

window.onkeydown = e => keys[e.key.toLowerCase()] = true;
window.onkeyup = e => keys[e.key.toLowerCase()] = false;

function getNormal(x,y,z) {
    let e = 0.001;
    return {
        x: map({x:x+e, y, z}) - map({x:x-e, y, z}),
        y: map({x, y:y+e, z}) - map({x, y:y-e, z}),
        z: map({x, y, z:z+e}) - map({x, y, z:z-e})
    };
}

function getHit(mx, my) {
    let uvx = (mx - cvs.width * 0.5) / cvs.height;
    let uvy = -(my - cvs.height * 0.5) / cvs.height;
    let cx = cam.x + Math.sin(cam.angle) * cam.dist;
    let cy = cam.dist * cam.pitch;
    let cz = cam.z + Math.cos(cam.angle) * cam.dist;
    let ro = {x:cx, y:cy, z:cz}, ta = {x:cam.x, y:0, z:cam.z};
    let cw = vNorm(vSub(ta, ro)), cp = {x:0, y:1, z:0}, cu = vNorm(vCross(cw, cp)), cv = vNorm(vCross(cu, cw));
    let rd = vNorm(vAdd(vAdd(vScale(cu, uvx), vScale(cv, uvy)), vScale(cw, 1.5)));
    if(Math.abs(rd.y) < 0.001) return null;
    let t = -ro.y / rd.y;
    return t < 0 ? null : { x: ro.x + rd.x * t, z: ro.z + rd.z * t };
}

cvs.onmousedown = e => { 
    if(gameState !== "PLAYING") return;
    if(e.button===0) { 
        let h = getHit(e.clientX, e.clientY); 
        // Only allow drag if mouse is near ball
        if(h && Math.sqrt((h.x-ball.x)**2+(h.z-ball.z)**2) < 4) { 
            drag.on=true; drag.sx=ball.x; drag.sz=ball.z; 
        } 
    } 
};
cvs.onmousemove = e => { let h = getHit(e.clientX, e.clientY); if(h) { mouse.wx=h.x; mouse.wz=h.z; } };
window.onmouseup = () => {
    if(drag.on && gameState === "PLAYING") {
        let dx = drag.sx - mouse.wx, dz = drag.sz - mouse.wz;
        let len = Math.sqrt(dx*dx+dz*dz);
        let p = Math.min(len, 15) * 0.1; 
        // Only counting shot if enough power was applied
        if(len > 0.2) {
            shots++;
            updateUI();
            ball.px = ball.x - (dx/len) * p;
            ball.pz = ball.z - (dz/len) * p;
        }
    }
    drag.on = false;
};

// Check if ball is in the hole (Defined at x:10, z:0 in SDF)
function checkWin() {
    if(gameState === "WON") return;
    
    let distToHole = Math.sqrt((ball.x - 10.0)**2 + (ball.z - 0.0)**2);
    
    // If inside hole radius (1.25) and fallen below floor level (0.0)
    if(distToHole < 1.0 && ball.y < -0.5) {
        gameState = "WON";
        let diff = shots - PAR;
        let msg = "Par";
        if(diff <= -2) msg = "Eagle!";
        else if(diff === -1) msg = "Birdie!";
        else if(diff === 0) msg = "Par";
        else if(diff === 1) msg = "Bogey";
        else if(diff === 2) msg = "Double Bogey";
        else msg = "+" + diff;

        if(shots === 1) msg = "HOLE IN ONE!";

        document.getElementById('msgResult').innerText = msg;
        document.getElementById('finalScore').innerText = shots;
        document.getElementById('modal').style.display = 'block';
    }
}

function resetGame() {
    ball.x=-25; ball.y=0.5; ball.z=-25;
    ball.px=-25; ball.py=0.5; ball.pz=-25;
    shots = 0;
    gameState = "PLAYING";
    updateUI();
    document.getElementById('modal').style.display = 'none';
}

function loop() {
    if(cvs.width !== window.innerWidth) { cvs.width = window.innerWidth; cvs.height = window.innerHeight; gl.viewport(0,0,cvs.width,cvs.height); gl.uniform2f(loc.res, cvs.width, cvs.height); }

    let s = Math.sin(cam.angle), c = Math.cos(cam.angle), spd = 0.3;
    if(keys.w) { cam.x -= s*spd; cam.z -= c*spd; }
    if(keys.s) { cam.x += s*spd; cam.z += c*spd; }
    if(keys.d) { cam.x += c*spd; cam.z -= s*spd; }
    if(keys.a) { cam.x -= c*spd; cam.z += s*spd; }
    if(keys.q) cam.angle -= 0.04;
    if(keys.e) cam.angle += 0.04;

    const dt = 1.0; 
    const friction = 0.99;
    const gravity = 0.015;
    const elasticity = 0.9; 

    // Physics only runs normally if not won (or if falling into hole)
    let vx = (ball.x - ball.px) * friction;
    let vy = (ball.y - ball.py) * friction;
    let vz = (ball.z - ball.pz) * friction;

    vy -= gravity;

    ball.px = ball.x;
    ball.py = ball.y;
    ball.pz = ball.z;

    ball.x += vx;
    ball.y += vy;
    ball.z += vz;

    for(let i=0; i<4; i++) {
        let d = map({x:ball.x, y:ball.y, z:ball.z});
        if(d < ball.r) {
            let pen = ball.r - d;
            let n = getNormal(ball.x, ball.y, ball.z);
            let len = Math.sqrt(n.x*n.x + n.y*n.y + n.z*n.z);
            n.x/=len; n.y/=len; n.z/=len;

            ball.x += n.x * pen;
            ball.y += n.y * pen;
            ball.z += n.z * pen;

            let curVx = ball.x - ball.px;
            let curVy = ball.y - ball.py;
            let curVz = ball.z - ball.pz;

            let dot = curVx*n.x + curVy*n.y + curVz*n.z;

            if(dot < 0) {
                let vNormalX = n.x * dot;
                let vNormalY = n.y * dot;
                let vNormalZ = n.z * dot;

                let vTangentX = curVx - vNormalX;
                let vTangentY = curVy - vNormalY;
                let vTangentZ = curVz - vNormalZ;

                let e = Math.abs(dot) < 0.05 ? 0.0 : elasticity;
                let f = 0.98;

                let newVx = vTangentX * f - vNormalX * e;
                let newVy = vTangentY * f - vNormalY * e;
                let newVz = vTangentZ * f - vNormalZ * e;

                ball.px = ball.x - newVx;
                ball.py = ball.y - newVy;
                ball.pz = ball.z - newVz;
            }
        }
    }

    if(ball.y < -15) { 
        // Reset Ball
        ball.x=-25; ball.y=1.0; ball.z=-25; 
        ball.px=-25; ball.py=1.0; ball.pz=-25; 
        // Reset Score
        shots = 0;
        updateUI();
    }
    
    // Check Win Condition
    checkWin();

    let cx = cam.x + Math.sin(cam.angle) * cam.dist;
    let cy = cam.dist * cam.pitch;
    let cz = cam.z + Math.cos(cam.angle) * cam.dist;
    gl.uniform3f(loc.cam, cx, cy, cz);
    gl.uniform3f(loc.tgt, cam.x, 0, cam.z);
    gl.uniform3f(loc.ball, ball.x, ball.y, ball.z);

    if(drag.on) {
        gl.uniform1f(loc.sa, 1.0);
        gl.uniform3f(loc.as, ball.x, ball.y, ball.z);
        let dx = ball.x - mouse.wx, dz = ball.z - mouse.wz;
        let len = Math.sqrt(dx*dx+dz*dz), sc = Math.min(len, 10) * 0.5;
        if(len > 0.01) gl.uniform3f(loc.ae, ball.x+(dx/len)*sc, ball.y, ball.z+(dz/len)*sc);
    } else gl.uniform1f(loc.sa, 0.0);

    gl.drawArrays(gl.TRIANGLES, 0, 6);
    requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>

remixes

no remixes yet... email to remix@oortic.net