Created
May 7, 2026 05:50
-
-
Save EncodeTheCode/22279788d111650fb2fa20f856a4b79f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" /> | |
| <title>Syphon-Style Canvas FPS - Exact Impact Decals</title> | |
| <style> | |
| html, body { | |
| margin: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| background: #000; | |
| font-family: system-ui, sans-serif; | |
| user-select: none; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100vw; | |
| height: 100vh; | |
| image-rendering: auto; | |
| cursor: crosshair; | |
| outline: none; | |
| } | |
| .hud { | |
| position: fixed; | |
| top: 10px; | |
| left: 10px; | |
| color: rgba(255,255,255,.92); | |
| font-size: 13px; | |
| line-height: 1.35; | |
| text-shadow: 0 1px 2px rgba(0,0,0,.85); | |
| pointer-events: none; | |
| background: rgba(0,0,0,.22); | |
| padding: 8px 10px; | |
| border: 1px solid rgba(255,255,255,.12); | |
| border-radius: 10px; | |
| backdrop-filter: blur(2px); | |
| } | |
| .hud strong { font-weight: 700; } | |
| .lockHint { | |
| position: fixed; | |
| inset: 0; | |
| display: grid; | |
| place-items: center; | |
| pointer-events: none; | |
| color: rgba(255,255,255,.9); | |
| text-shadow: 0 1px 3px black; | |
| font-size: 16px; | |
| transition: opacity .15s ease; | |
| } | |
| .lockHint > div { | |
| background: rgba(0,0,0,.45); | |
| padding: 12px 16px; | |
| border-radius: 12px; | |
| border: 1px solid rgba(255,255,255,.12); | |
| } | |
| .hidden { opacity: 0; } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="game" tabindex="1"></canvas> | |
| <div class="hud"> | |
| <strong>WASD</strong> move • mouse aim • click to lock • left click to fire • ESC unlock<br> | |
| Exact wall-face bullet impacts • anti-wall-stuck collision | |
| </div> | |
| <div id="lockHint" class="lockHint"> | |
| <div>Click the game to lock mouse and play.</div> | |
| </div> | |
| <script> | |
| (() => { | |
| const canvas = document.getElementById("game"); | |
| const ctx = canvas.getContext("2d", { alpha: false }); | |
| const lockHint = document.getElementById("lockHint"); | |
| if (!ctx) { | |
| document.body.innerHTML = '<p style="color:white;padding:20px">Canvas unsupported.</p>'; | |
| return; | |
| } | |
| const DPR = Math.min(2, window.devicePixelRatio || 1); | |
| let W = 0; | |
| let H = 0; | |
| function resize() { | |
| W = Math.floor(innerWidth); | |
| H = Math.floor(innerHeight); | |
| canvas.width = Math.floor(W * DPR); | |
| canvas.height = Math.floor(H * DPR); | |
| canvas.style.width = W + "px"; | |
| canvas.style.height = H + "px"; | |
| ctx.setTransform(DPR, 0, 0, DPR, 0, 0); | |
| ctx.imageSmoothingEnabled = false; | |
| } | |
| addEventListener("resize", resize); | |
| resize(); | |
| const map = [ | |
| [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], | |
| [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], | |
| [1,0,1,1,1,1,0,1,1,1,1,0,1,1,1,0,1,1,1,1,1,0,0,1], | |
| [1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,0,1,0,0,0,1,0,0,1], | |
| [1,0,1,0,0,1,0,1,0,0,1,0,1,0,1,1,1,0,1,0,1,1,0,1], | |
| [1,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,1,0,1], | |
| [1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,0,1,0,1], | |
| [1,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,1,0,0,0,0,1,0,1], | |
| [1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,0,1,0,1,1,1,1,0,1], | |
| [1,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,1], | |
| [1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,0,1], | |
| [1,0,1,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1], | |
| [1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,0,1,0,1], | |
| [1,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1], | |
| [1,0,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1,0,0,1,1,1,0,1], | |
| [1,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,1,0,1], | |
| [1,0,1,0,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,0,1,0,1], | |
| [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1], | |
| [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1], | |
| [1,0,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,1,1,1,0,1], | |
| [1,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1], | |
| [1,0,1,1,1,1,0,1,0,1,0,1,1,1,1,1,0,1,1,1,0,1,0,1], | |
| [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], | |
| [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] | |
| ]; | |
| const MAP_W = map[0].length; | |
| const MAP_H = map.length; | |
| const player = { | |
| x: 1.5, | |
| y: 1.5, | |
| a: 0.25, | |
| pitch: 0, | |
| moveSpeed: 3.2, | |
| radius: 0.16 | |
| }; | |
| const keys = Object.create(null); | |
| let pointerLocked = false; | |
| let mouseDown = false; | |
| let shootCooldown = 0; | |
| const bullets = []; | |
| const MAX_BULLETS = 48; | |
| const bulletHoles = []; | |
| const MAX_HOLES = 150; | |
| function isWallTile(tx, ty) { | |
| return tx < 0 || ty < 0 || tx >= MAP_W || ty >= MAP_H || map[ty][tx] === 1; | |
| } | |
| function collides(x, y) { | |
| const r = player.radius; | |
| return ( | |
| isWallTile(Math.floor(x - r), Math.floor(y - r)) || | |
| isWallTile(Math.floor(x + r), Math.floor(y - r)) || | |
| isWallTile(Math.floor(x - r), Math.floor(y + r)) || | |
| isWallTile(Math.floor(x + r), Math.floor(y + r)) | |
| ); | |
| } | |
| function moveWithSlide(dx, dy) { | |
| const steps = Math.ceil(Math.max(Math.abs(dx), Math.abs(dy)) / 0.05); | |
| const sx = dx / steps; | |
| const sy = dy / steps; | |
| for (let i = 0; i < steps; i++) { | |
| const nx = player.x + sx; | |
| if (!collides(nx, player.y)) player.x = nx; | |
| const ny = player.y + sy; | |
| if (!collides(player.x, ny)) player.y = ny; | |
| } | |
| } | |
| function normalizeAngle(a) { | |
| while (a > Math.PI) a -= Math.PI * 2; | |
| while (a < -Math.PI) a += Math.PI * 2; | |
| return a; | |
| } | |
| function addBulletHole(hitX, hitY, side) { | |
| const faceFrac = side === 0 | |
| ? hitY - Math.floor(hitY) | |
| : hitX - Math.floor(hitX); | |
| bulletHoles.push({ | |
| x: hitX, | |
| y: hitY, | |
| side, | |
| faceFrac, | |
| rot: Math.random() * Math.PI * 2, | |
| size: 7 + Math.random() * 5 | |
| }); | |
| if (bulletHoles.length > MAX_HOLES) { | |
| bulletHoles.shift(); | |
| } | |
| } | |
| function castRayFrom(startX, startY, rayAngle, maxDistance = Infinity) { | |
| const rayDirX = Math.cos(rayAngle); | |
| const rayDirY = Math.sin(rayAngle); | |
| let mapX = Math.floor(startX); | |
| let mapY = Math.floor(startY); | |
| const deltaDistX = Math.abs(1 / (rayDirX || 1e-9)); | |
| const deltaDistY = Math.abs(1 / (rayDirY || 1e-9)); | |
| let stepX, stepY; | |
| let sideDistX, sideDistY; | |
| if (rayDirX < 0) { | |
| stepX = -1; | |
| sideDistX = (startX - mapX) * deltaDistX; | |
| } else { | |
| stepX = 1; | |
| sideDistX = (mapX + 1 - startX) * deltaDistX; | |
| } | |
| if (rayDirY < 0) { | |
| stepY = -1; | |
| sideDistY = (startY - mapY) * deltaDistY; | |
| } else { | |
| stepY = 1; | |
| sideDistY = (mapY + 1 - startY) * deltaDistY; | |
| } | |
| let hit = false; | |
| let side = 0; | |
| let maxSteps = 128; | |
| while (!hit && maxSteps-- > 0) { | |
| if (sideDistX < sideDistY) { | |
| sideDistX += deltaDistX; | |
| mapX += stepX; | |
| side = 0; | |
| } else { | |
| sideDistY += deltaDistY; | |
| mapY += stepY; | |
| side = 1; | |
| } | |
| if (isWallTile(mapX, mapY)) { | |
| hit = true; | |
| } | |
| } | |
| if (!hit) return null; | |
| let dist; | |
| if (side === 0) { | |
| dist = (mapX - startX + (1 - stepX) / 2) / (rayDirX || 1e-9); | |
| } else { | |
| dist = (mapY - startY + (1 - stepY) / 2) / (rayDirY || 1e-9); | |
| } | |
| dist = Math.max(0.0001, Math.abs(dist)); | |
| if (dist > maxDistance) return null; | |
| return { | |
| dist, | |
| side, | |
| mapX, | |
| mapY, | |
| stepX, | |
| stepY, | |
| hitX: startX + rayDirX * dist, | |
| hitY: startY + rayDirY * dist | |
| }; | |
| } | |
| function fireBullet() { | |
| if (shootCooldown > 0) return; | |
| shootCooldown = 0.14; | |
| const dirX = Math.cos(player.a); | |
| const dirY = Math.sin(player.a); | |
| bullets.push({ | |
| x: player.x + dirX * 0.28, | |
| y: player.y + dirY * 0.28, | |
| dx: dirX, | |
| dy: dirY, | |
| speed: 9.5, | |
| life: 2.0 | |
| }); | |
| if (bullets.length > MAX_BULLETS) bullets.shift(); | |
| } | |
| function updateBullets(dt) { | |
| for (let i = bullets.length - 1; i >= 0; i--) { | |
| const b = bullets[i]; | |
| const step = b.speed * dt; | |
| const angle = Math.atan2(b.dy, b.dx); | |
| const hit = castRayFrom(b.x, b.y, angle, step + 1e-6); | |
| if (hit) { | |
| addBulletHole(hit.hitX, hit.hitY, hit.side); | |
| bullets.splice(i, 1); | |
| continue; | |
| } | |
| b.x += b.dx * step; | |
| b.y += b.dy * step; | |
| b.life -= dt; | |
| if (b.life <= 0 || isWallTile(Math.floor(b.x), Math.floor(b.y))) { | |
| const impact = castRayFrom(b.x - b.dx * 0.02, b.y - b.dy * 0.02, angle, 0.05); | |
| if (impact) addBulletHole(impact.hitX, impact.hitY, impact.side); | |
| bullets.splice(i, 1); | |
| } | |
| } | |
| } | |
| addEventListener("keydown", e => { | |
| keys[e.code] = true; | |
| if (e.code === "Escape" && document.pointerLockElement === canvas) { | |
| document.exitPointerLock(); | |
| } | |
| }); | |
| addEventListener("keyup", e => { | |
| keys[e.code] = false; | |
| }); | |
| canvas.addEventListener("click", () => { | |
| if (!pointerLocked) { | |
| canvas.requestPointerLock(); | |
| canvas.focus(); | |
| } | |
| }); | |
| canvas.addEventListener("mousedown", e => { | |
| if (e.button === 0) { | |
| mouseDown = true; | |
| if (pointerLocked) fireBullet(); | |
| } | |
| }); | |
| canvas.addEventListener("mouseup", e => { | |
| if (e.button === 0) mouseDown = false; | |
| }); | |
| document.addEventListener("pointerlockchange", () => { | |
| pointerLocked = document.pointerLockElement === canvas; | |
| lockHint.classList.toggle("hidden", pointerLocked); | |
| canvas.style.cursor = pointerLocked ? "none" : "crosshair"; | |
| }); | |
| canvas.addEventListener("mousemove", e => { | |
| if (!pointerLocked) return; | |
| player.a += e.movementX * 0.0024; | |
| player.pitch += e.movementY * 0.7; | |
| const limit = H * 0.35; | |
| if (player.pitch > limit) player.pitch = limit; | |
| if (player.pitch < -limit) player.pitch = -limit; | |
| }); | |
| function update(dt) { | |
| if (shootCooldown > 0) shootCooldown = Math.max(0, shootCooldown - dt); | |
| if (pointerLocked && mouseDown) fireBullet(); | |
| let forward = 0; | |
| let strafe = 0; | |
| if (keys.KeyW) forward += 1; | |
| if (keys.KeyS) forward -= 1; | |
| if (keys.KeyD) strafe += 1; | |
| if (keys.KeyA) strafe -= 1; | |
| const mag = Math.hypot(forward, strafe) || 1; | |
| forward /= mag; | |
| strafe /= mag; | |
| const sin = Math.sin(player.a); | |
| const cos = Math.cos(player.a); | |
| const dx = (cos * forward - sin * strafe) * player.moveSpeed * dt; | |
| const dy = (sin * forward + cos * strafe) * player.moveSpeed * dt; | |
| moveWithSlide(dx, dy); | |
| updateBullets(dt); | |
| } | |
| function castRay(rayAngle) { | |
| return castRayFrom(player.x, player.y, rayAngle, Infinity); | |
| } | |
| function drawBackground() { | |
| const horizon = H * 0.5 - player.pitch; | |
| const sky = ctx.createLinearGradient(0, 0, 0, horizon); | |
| sky.addColorStop(0, "#1c2230"); | |
| sky.addColorStop(1, "#0b0f15"); | |
| ctx.fillStyle = sky; | |
| ctx.fillRect(0, 0, W, horizon); | |
| const floor = ctx.createLinearGradient(0, horizon, 0, H); | |
| floor.addColorStop(0, "#222"); | |
| floor.addColorStop(1, "#090909"); | |
| ctx.fillStyle = floor; | |
| ctx.fillRect(0, horizon, W, H - horizon); | |
| } | |
| function drawBulletHoles(fov, projPlane) { | |
| for (const hole of bulletHoles) { | |
| const dx = hole.x - player.x; | |
| const dy = hole.y - player.y; | |
| const dist = Math.hypot(dx, dy); | |
| const ang = Math.atan2(dy, dx); | |
| const rel = normalizeAngle(ang - player.a); | |
| if (Math.abs(rel) > fov * 0.5 + 0.2) continue; | |
| const wallHit = castRay(ang); | |
| if (!wallHit) continue; | |
| if (dist > wallHit.dist + 0.03) continue; | |
| const corrected = Math.max(0.0001, dist * Math.cos(rel)); | |
| const screenX = ((rel + fov * 0.5) / fov) * W; | |
| const wallH = Math.min(H * 2, projPlane / corrected); | |
| const wallTop = (H - wallH) * 0.5 - player.pitch; | |
| const screenY = wallTop + hole.faceFrac * wallH; | |
| const size = Math.max(4, Math.min(18, hole.size * (projPlane / (corrected * 70)) + 3)); | |
| ctx.save(); | |
| ctx.translate(screenX, screenY); | |
| ctx.rotate(hole.rot); | |
| ctx.fillStyle = "rgba(16,16,16,0.92)"; | |
| ctx.fillRect(-size * 0.5, -size * 0.5, size, size); | |
| ctx.strokeStyle = "rgba(255,255,255,0.06)"; | |
| ctx.lineWidth = 1; | |
| ctx.strokeRect(-size * 0.5, -size * 0.5, size, size); | |
| ctx.fillStyle = "rgba(255,255,255,0.05)"; | |
| ctx.fillRect(-size * 0.18, -size * 0.18, size * 0.36, size * 0.36); | |
| ctx.restore(); | |
| } | |
| } | |
| function drawScene() { | |
| drawBackground(); | |
| const fov = Math.PI / 3; | |
| const columnCount = Math.max(220, Math.floor(W / 2)); | |
| const sliceW = W / columnCount; | |
| const projPlane = (W * 0.5) / Math.tan(fov * 0.5); | |
| for (let i = 0; i < columnCount; i++) { | |
| const t = i / (columnCount - 1); | |
| const rayAngle = player.a - fov * 0.5 + t * fov; | |
| const hit = castRay(rayAngle); | |
| if (!hit) continue; | |
| const corrected = Math.max(0.0001, hit.dist * Math.cos(rayAngle - player.a)); | |
| const wallH = Math.min(H * 2, projPlane / corrected); | |
| const y = (H - wallH) * 0.5 - player.pitch; | |
| const x = i * sliceW; | |
| let shade = 255 - corrected * 26; | |
| if (shade < 0) shade = 0; | |
| if (shade > 255) shade = 255; | |
| if (hit.side === 1) shade *= 0.78; | |
| ctx.fillStyle = `rgb(${shade|0},${shade|0},${shade|0})`; | |
| ctx.fillRect(x, y, sliceW + 1, wallH); | |
| ctx.fillStyle = "rgba(0,0,0,0.12)"; | |
| ctx.fillRect(x, y, 1, wallH); | |
| } | |
| drawBulletHoles(fov, projPlane); | |
| drawCrosshair(); | |
| drawMinimap(); | |
| drawBulletsOnMinimap(); | |
| } | |
| function drawCrosshair() { | |
| ctx.save(); | |
| ctx.translate(W / 2, H / 2); | |
| ctx.strokeStyle = "rgba(255,255,255,0.8)"; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(-10, 0); ctx.lineTo(-3, 0); | |
| ctx.moveTo(3, 0); ctx.lineTo(10, 0); | |
| ctx.moveTo(0, -10); ctx.lineTo(0, -3); | |
| ctx.moveTo(0, 3); ctx.lineTo(0, 10); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| function drawNorthArrow(cx, cy) { | |
| const g = ctx.createLinearGradient(cx, cy - 18, cx, cy + 18); | |
| g.addColorStop(0, "rgba(255,255,255,1)"); | |
| g.addColorStop(0.5, "rgba(245,245,245,0.95)"); | |
| g.addColorStop(1, "rgba(190,190,190,0.72)"); | |
| ctx.save(); | |
| ctx.translate(cx, cy); | |
| ctx.fillStyle = g; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -13); | |
| ctx.lineTo(13, 9); | |
| ctx.lineTo(5, 7); | |
| ctx.lineTo(5, 18); | |
| ctx.lineTo(-5, 18); | |
| ctx.lineTo(-5, 7); | |
| ctx.lineTo(-13, 9); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function drawMinimap() { | |
| const boxSize = Math.min(184, Math.floor(Math.min(W, H) * 0.23)); | |
| const x0 = 18; | |
| const y0 = H - boxSize - 26; | |
| const scale = boxSize / MAP_W; | |
| ctx.save(); | |
| ctx.fillStyle = "rgba(255,255,255,0.045)"; | |
| ctx.fillRect(x0, y0, boxSize, boxSize); | |
| ctx.strokeStyle = "rgba(255,255,255,0.24)"; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(x0, y0, boxSize, boxSize); | |
| ctx.strokeStyle = "rgba(255,255,255,0.05)"; | |
| ctx.lineWidth = 1; | |
| for (let i = 1; i < MAP_W; i++) { | |
| const gx = x0 + i * scale; | |
| ctx.beginPath(); | |
| ctx.moveTo(gx, y0); | |
| ctx.lineTo(gx, y0 + boxSize); | |
| ctx.stroke(); | |
| } | |
| for (let j = 1; j < MAP_H; j++) { | |
| const gy = y0 + j * scale; | |
| ctx.beginPath(); | |
| ctx.moveTo(x0, gy); | |
| ctx.lineTo(x0 + boxSize, gy); | |
| ctx.stroke(); | |
| } | |
| for (let y = 0; y < MAP_H; y++) { | |
| for (let x = 0; x < MAP_W; x++) { | |
| if (!map[y][x]) continue; | |
| ctx.fillStyle = "rgba(165,165,165,0.86)"; | |
| ctx.fillRect(x0 + x * scale + 0.5, y0 + y * scale + 0.5, scale - 1, scale - 1); | |
| } | |
| } | |
| drawNorthArrow(x0 + boxSize * 0.5, y0 + 16); | |
| const px = x0 + player.x * scale; | |
| const py = y0 + player.y * scale; | |
| ctx.strokeStyle = "rgba(255,255,255,0.45)"; | |
| ctx.lineWidth = 1.25; | |
| ctx.beginPath(); | |
| ctx.moveTo(px, py); | |
| ctx.lineTo(px + Math.cos(player.a - 0.28) * 15, py + Math.sin(player.a - 0.28) * 15); | |
| ctx.moveTo(px, py); | |
| ctx.lineTo(px + Math.cos(player.a + 0.28) * 15, py + Math.sin(player.a + 0.28) * 15); | |
| ctx.stroke(); | |
| ctx.fillStyle = "rgba(255,255,255,0.95)"; | |
| ctx.beginPath(); | |
| ctx.arc(px, py, 3.3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = "rgba(0,0,0,0.55)"; | |
| ctx.beginPath(); | |
| ctx.arc(px, py, 1.1, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function drawBulletsOnMinimap() { | |
| if (!bullets.length) return; | |
| const boxSize = Math.min(184, Math.floor(Math.min(W, H) * 0.23)); | |
| const x0 = 18; | |
| const y0 = H - boxSize - 26; | |
| const scale = boxSize / MAP_W; | |
| ctx.save(); | |
| ctx.fillStyle = "rgba(255,220,120,0.92)"; | |
| for (const b of bullets) { | |
| const x = x0 + b.x * scale; | |
| const y = y0 + b.y * scale; | |
| ctx.fillRect(x - 1, y - 1, 2, 2); | |
| } | |
| ctx.restore(); | |
| } | |
| let last = performance.now(); | |
| let accumulator = 0; | |
| const fixedStep = 1 / 60; | |
| const renderInterval = 1000 / 30; | |
| let lastRender = 0; | |
| function loop(now) { | |
| requestAnimationFrame(loop); | |
| const dt = Math.min(0.05, (now - last) / 1000); | |
| last = now; | |
| accumulator += dt; | |
| while (accumulator >= fixedStep) { | |
| update(fixedStep); | |
| accumulator -= fixedStep; | |
| } | |
| if (now - lastRender < renderInterval) return; | |
| lastRender = now; | |
| drawScene(); | |
| } | |
| requestAnimationFrame(loop); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment