Move a dot around a large world; the camera smoothly follows and the viewport stays 1500×1500.
Code for Above
<div style="max-width:1500px;margin:0 auto;">
<iframe
title="POC — Camera Follow (Soft)"
scrolling="no"
style="width:1500px;height:1500px;border:0;display:block;background:#111;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.5);"
sandbox="allow-scripts allow-same-origin"
srcdoc='
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>POC — Camera Follow (Soft)</title>
<style>
html, body { margin:0; width:100%; height:100%; overflow:hidden; background:#111; }
canvas { display:block; }
.hint { position:absolute; left:12px; bottom:12px; color:#aaa; font:14px system-ui; }
</style>
</head>
<body>
<canvas id="c" width="1500" height="1500"></canvas>
<div class="hint">Arrows: move • Camera follows with smoothing</div>
<script>
const c = document.getElementById("c");
const x = c.getContext("2d");
const VW = 1500, VH = 1500; // viewport (canvas)
const WW = 4200, WH = 4200; // world size (bigger than viewport)
// player
const p = { x: WW/2, y: WH/2, vx: 0, vy: 0 };
// camera (top-left of viewport in world coords)
const cam = { x: p.x - VW/2, y: p.y - VH/2 };
// tunables
const ACCEL = 0.45;
const DRAG = 0.92;
const MAX = 12;
const CAM_LERP = 0.08; // lower = more lag, higher = snappier
// input
const k = {};
addEventListener("keydown", e => k[e.key] = true);
addEventListener("keyup", e => k[e.key] = false);
function clamp(v, a, b){ return Math.max(a, Math.min(b, v)); }
function lerp(a,b,t){ return a + (b-a)*t; }
function update(){
if (k["ArrowUp"]) p.vy -= ACCEL;
if (k["ArrowDown"]) p.vy += ACCEL;
if (k["ArrowLeft"]) p.vx -= ACCEL;
if (k["ArrowRight"]) p.vx += ACCEL;
p.vx *= DRAG; p.vy *= DRAG;
const sp = Math.hypot(p.vx, p.vy);
if (sp > MAX) { p.vx = (p.vx/sp)*MAX; p.vy = (p.vy/sp)*MAX; }
p.x = clamp(p.x + p.vx, 0, WW);
p.y = clamp(p.y + p.vy, 0, WH);
// desired camera center on player
const targetCamX = p.x - VW/2;
const targetCamY = p.y - VH/2;
// soft follow
cam.x = lerp(cam.x, targetCamX, CAM_LERP);
cam.y = lerp(cam.y, targetCamY, CAM_LERP);
// clamp camera to world bounds
cam.x = clamp(cam.x, 0, WW - VW);
cam.y = clamp(cam.y, 0, WH - VH);
}
function drawGrid(){
x.strokeStyle = "rgba(255,255,255,0.07)";
x.lineWidth = 1;
const step = 150;
const startX = Math.floor(cam.x / step) * step;
const startY = Math.floor(cam.y / step) * step;
for (let gx = startX; gx <= cam.x + VW; gx += step){
const sx = gx - cam.x;
x.beginPath();
x.moveTo(sx, 0);
x.lineTo(sx, VH);
x.stroke();
}
for (let gy = startY; gy <= cam.y + VH; gy += step){
const sy = gy - cam.y;
x.beginPath();
x.moveTo(0, sy);
x.lineTo(VW, sy);
x.stroke();
}
// world border
x.strokeStyle = "rgba(255,255,255,0.18)";
x.strokeRect(-cam.x, -cam.y, WW, WH);
}
function draw(){
x.clearRect(0,0,VW,VH);
drawGrid();
// player in screen coords
const sx = p.x - cam.x;
const sy = p.y - cam.y;
x.fillStyle = "#fff";
x.beginPath();
x.arc(sx, sy, 10, 0, Math.PI*2);
x.fill();
// debug text
x.fillStyle = "rgba(255,255,255,0.5)";
x.font = "14px system-ui";
x.fillText(`player: (${p.x|0}, ${p.y|0}) cam: (${cam.x|0}, ${cam.y|0})`, 12, 24);
}
(function loop(){
update();
draw();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
'></iframe>
</div>