Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <title>Mini Wolfenstein-style Raycaster — "Unscreenshottable" HUD Demo</title>
- <style>
- html,body{height:100%;margin:0;background:#111;color:#ddd;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
- #game{display:block;margin:0 auto;background:#000;image-rendering:pixelated}
- .info{max-width:900px;margin:12px auto;padding:8px;text-align:center}
- a{color:#7fd}
- </style>
- </head>
- <body>
- <div class="info">
- <strong>Mini raycaster demo</strong> — arrow keys / WASD to move, mouse to look, <kbd>H</kbd> toggles the "unscreenshottable" HUD. Open the file in a modern browser.
- </div>
- <canvas id="game" width="960" height="480"></canvas>
- <script>
- // Simple 2D raycaster + unscreenshottable dynamic-HUD overlay.
- // Single-file demo. Performance-minded but intentionally simple.
- const canvas = document.getElementById('game');
- const ctx = canvas.getContext('2d');
- const W = canvas.width, H = canvas.height;
- // ----- MAP -----
- // 1 = wall, 0 = empty
- const map = [
- 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,1,
- 1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,
- 1,0,0,0,1,1,1,0,0,0,1,0,0,0,0,1,
- 1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,
- 1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,
- 1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,
- 1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,
- 1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,
- 1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,
- 1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,
- 1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,1,
- 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
- 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
- 1,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,
- ];
- const MAPW = 16, MAPH = 16;
- // ----- PLAYER -----
- let px = 3.5, py = 3.5; // position
- let pa = 0; // angle radians
- let moveSpeed = 2.5; // units / sec
- let rotSpeed = 2.5; // rad / sec
- // ----- INPUT -----
- const keys = {};
- document.addEventListener('keydown', e=>{ keys[e.key.toLowerCase()] = true; if(e.key==='h' || e.key==='H') { hudEnabled = !hudEnabled; } });
- document.addEventListener('keyup', e=>{ keys[e.key.toLowerCase()] = false; });
- // Lock pointer for mouse-look
- canvas.addEventListener('click', ()=>{ canvas.requestPointerLock?.(); });
- let mouseDX = 0;
- function onPointerMove(e){ mouseDX += e.movementX || 0; }
- document.addEventListener('pointermove', onPointerMove);
- // ----- RAYCAST SETTINGS -----
- const FOV = Math.PI/3; // 60deg
- const NUM_RAYS = W/2 | 0; // columns (reduce for perf)
- const DIST_PROJ_PLANE = (W/2) / Math.tan(FOV/2);
- // ----- TEXTURES (simple color bands) -----
- function wallColor(id, side){
- if(id===1) return side? '#8888cc' : '#6666aa';
- if(id===2) return side? '#88cc88' : '#66aa66';
- return '#444';
- }
- // ----- OFFSCREEN CANVASES FOR NOISE EFFECT -----
- const staticNoise = document.createElement('canvas');
- staticNoise.width = W; staticNoise.height = H;
- const sCtx = staticNoise.getContext('2d');
- const dynamicNoise = document.createElement('canvas');
- dynamicNoise.width = W; dynamicNoise.height = H;
- const dCtx = dynamicNoise.getContext('2d');
- // Mask canvas holds the HUD shape (text) as opaque pixels
- const maskCanvas = document.createElement('canvas');
- maskCanvas.width = W; maskCanvas.height = H;
- const mCtx = maskCanvas.getContext('2d');
- let hudEnabled = true;
- function generateStaticNoise(){
- // draw a static noise field once (background)
- const img = sCtx.createImageData(W,H);
- for(let i=0;i<img.data.length;i+=4){
- const v = (Math.random()*255)|0;
- img.data[i]=img.data[i+1]=img.data[i+2]=v;
- img.data[i+3]=255;
- }
- sCtx.putImageData(img,0,0);
- }
- function resizeMask(){
- mCtx.clearRect(0,0,W,H);
- // Draw some HUD text and a little minimap box
- mCtx.font = 'bold 48px monospace';
- mCtx.textAlign = 'center';
- mCtx.fillStyle = '#fff';
- mCtx.fillText('SECRET', W/2, 80);
- // minimap rectangle (we'll use this area as part of the mask too)
- mCtx.fillRect(12,12,150,110);
- }
- generateStaticNoise();
- resizeMask();
- // Precompute mask image data
- let maskData = mCtx.getImageData(0,0,W,H);
- function updateMaskData(){ maskData = mCtx.getImageData(0,0,W,H); }
- updateMaskData();
- // Dynamic noise state (will change each frame inside mask)
- function generateDynamicNoiseFrame(tick){
- const img = dCtx.createImageData(W,H);
- // create motion: vertical scroll of noise + small temporal variation
- for(let y=0;y<H;y++){
- for(let x=0;x<W;x++){
- const idx = (y*W + x)*4;
- // Per-pixel value depends on x,y and tick so it moves
- const v = ( ((x*37 + y*17 + tick*3) & 255) ^ ((x*y + tick*13)&255) );
- img.data[idx]=img.data[idx+1]=img.data[idx+2]=v;
- img.data[idx+3]=255;
- }
- }
- dCtx.putImageData(img,0,0);
- }
- // Composite function: show static noise where mask is transparent, dynamic where mask opaque
- function compositeNoise(){
- // We'll read both images and the mask and draw into main ctx as an overlay
- const sImg = sCtx.getImageData(0,0,W,H).data;
- const dImg = dCtx.getImageData(0,0,W,H).data;
- const m = maskData.data;
- const out = ctx.createImageData(W,H);
- for(let i=0;i<sImg.length;i+=4){
- const alpha = m[i+3]; // mask alpha
- if(alpha>16){ // mask present -> show dynamic noise
- out.data[i]=dImg[i]; out.data[i+1]=dImg[i+1]; out.data[i+2]=dImg[i+2]; out.data[i+3]=255;
- } else {
- out.data[i]=sImg[i]; out.data[i+1]=sImg[i+1]; out.data[i+2]=sImg[i+2]; out.data[i+3]=255;
- }
- }
- // draw overlay with some transparency so world still visible under it
- ctx.save();
- ctx.globalAlpha = 0.85;
- ctx.putImageData(out,0,0);
- ctx.restore();
- }
- // ----- RAYCASTING -----
- function castRays(){
- // clear sky/floor
- ctx.fillStyle = '#6aa'; ctx.fillRect(0,0,W,H/2); // sky
- ctx.fillStyle = '#444'; ctx.fillRect(0,H/2,W,H/2); // floor
- for(let i=0;i<NUM_RAYS;i++){
- const rayScreenPos = (i/NUM_RAYS) - 0.5;
- const rayAngle = pa + rayScreenPos * FOV;
- // unit vector for ray
- const rx = Math.cos(rayAngle), ry = Math.sin(rayAngle);
- // DDA
- let mapX = Math.floor(px), mapY = Math.floor(py);
- const deltaDistX = Math.abs(1 / rx);
- const deltaDistY = Math.abs(1 / ry);
- let stepX, stepY, sideDistX, sideDistY;
- if(rx < 0){ stepX = -1; sideDistX = (px - mapX) * deltaDistX; }
- else { stepX = 1; sideDistX = (mapX + 1 - px) * deltaDistX; }
- if(ry < 0){ stepY = -1; sideDistY = (py - mapY) * deltaDistY; }
- else { stepY = 1; sideDistY = (mapY + 1 - py) * deltaDistY; }
- let hit = false, side = 0; // side: 0=x, 1=y
- let cell;
- let perpWallDist = 0;
- for(let depth=0; depth<64 && !hit; depth++){
- if(sideDistX < sideDistY){
- sideDistX += deltaDistX; mapX += stepX; side=0;
- } else {
- sideDistY += deltaDistY; mapY += stepY; side=1;
- }
- if(mapX>=0 && mapX<MAPW && mapY>=0 && mapY<MAPH){
- cell = map[mapY*MAPW + mapX];
- if(cell>0) hit = true;
- } else { hit = true; cell=1; }
- }
- if(side===0) perpWallDist = (mapX - px + (1-stepX)/2) / rx;
- else perpWallDist = (mapY - py + (1-stepY)/2) / ry;
- if(perpWallDist<=0) perpWallDist = 0.0001;
- // correct fisheye
- const correctedDist = perpWallDist * Math.cos(rayAngle-pa);
- const lineHeight = (DIST_PROJ_PLANE / correctedDist) | 0;
- const drawStart = Math.max(0, -lineHeight/2 + H/2);
- const drawEnd = Math.min(H-1, lineHeight/2 + H/2);
- ctx.fillStyle = wallColor(cell, side);
- const columnX = (i * (W / NUM_RAYS)) | 0;
- const columnW = Math.ceil(W / NUM_RAYS) + 0.5;
- ctx.fillRect(columnX, drawStart, columnW, drawEnd - drawStart);
- }
- }
- // ----- SIMPLE MINIMAP -----
- function drawMiniMap(){
- const size = 140; const sx = 12, sy = 12;
- ctx.save();
- ctx.globalAlpha = 0.9;
- ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(sx-4,sy-4,size+8, size+8);
- for(let y=0;y<MAPH;y++){
- for(let x=0;x<MAPW;x++){
- const v = map[y*MAPW+x];
- if(v===0) ctx.fillStyle = '#222'; else ctx.fillStyle = '#ddd';
- const cx = sx + (x * (size/MAPW));
- const cy = sy + (y * (size/MAPH));
- ctx.fillRect(cx, cy, Math.ceil(size/MAPW), Math.ceil(size/MAPH));
- }
- }
- // player
- ctx.fillStyle = 'red'; ctx.fillRect(sx + px*(size/MAPW)-2, sy + py*(size/MAPH)-2,4,4);
- // facing line
- ctx.strokeStyle = 'red'; ctx.beginPath(); ctx.moveTo(sx+px*(size/MAPW), sy+py*(size/MAPH)); ctx.lineTo(sx + (px + Math.cos(pa)*0.8)*(size/MAPW), sy + (py + Math.sin(pa)*0.8)*(size/MAPH)); ctx.stroke();
- ctx.restore();
- }
- // ----- GAME LOOP -----
- let last = performance.now();
- let tick = 0;
- function frame(now){
- const dt = Math.min(0.05, (now - last)/1000);
- last = now;
- tick++;
- // movement
- let forward = (keys['w']||keys['arrowup'])?1:0;
- let back = (keys['s']||keys['arrowdown'])?1:0;
- let left = (keys['a']||keys['arrowleft'])?1:0;
- let right = (keys['d']||keys['arrowright'])?1:0;
- const speed = moveSpeed * dt;
- const rot = rotSpeed * dt;
- // rotate by mouse
- pa += (mouseDX * 0.0025);
- mouseDX = 0;
- if(forward) { px += Math.cos(pa)*speed; py += Math.sin(pa)*speed; }
- if(back) { px -= Math.cos(pa)*speed; py -= Math.sin(pa)*speed; }
- if(left) { pa -= rot; }
- if(right) { pa += rot; }
- // simple collision: keep inside world and not inside walls
- if(px<1) px=1; if(px>MAPW-1) px=MAPW-1; if(py<1) py=1; if(py>MAPH-1) py=MAPH-1;
- const pcell = map[Math.floor(py)*MAPW + Math.floor(px)]; if(pcell>0){ px = Math.floor(px)+0.5; py = Math.floor(py)+0.5; }
- // draw world
- castRays();
- // minimap
- drawMiniMap();
- // HUD overlay (unscreenshottable effect)
- if(hudEnabled){
- generateDynamicNoiseFrame(tick);
- compositeNoise();
- }
- requestAnimationFrame(frame);
- }
- requestAnimationFrame(frame);
- // Resize handling (optional)
- window.addEventListener('resize', ()=>{});
- // Helpful note: user can toggle HUD with H
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment