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 • <b>WASD</b> Move • <b>Q/E</b> Rotate</div>
<div class="score-row" id="scoreDisplay">SHOTS: 0 | 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} | 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
