Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save EncodeTheCode/22279788d111650fb2fa20f856a4b79f to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/22279788d111650fb2fa20f856a4b79f to your computer and use it in GitHub Desktop.
<!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