This proof-of-concept demonstrates directional damage feedback using screen-space responses instead of UI indicators. Incoming impacts are translated into biased edge vignettes, subtle camera shake away from the hit vector, and a brief impact flash, allowing players to instantly perceive attack direction and intensity. The system decays naturally over time, preserving clarity without clutter, and communicates spatial threat, damage orientation, and urgency through motion, color, and screen response alone.


Code for Above

<div style="max-width:1200px;margin:0 auto;">
<iframe
  title="Damage Directional Feedback — Angular Wedge POC"
  scrolling="no"
  style="display:block;margin:0 auto;border:0;width:1200px;height:1200px;overflow:hidden;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.55);background:#000;"
  sandbox="allow-scripts allow-same-origin"
  srcdoc='<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
html,body{margin:0;background:#000;overflow:hidden}
canvas{display:block;width:1200px;height:1200px;background:radial-gradient(#0a0a0a,#000);cursor:none}
</style>
</head>
<body>
<canvas id="c" width="1200" height="1200"></canvas>

<script>
const c = document.getElementById("c");
const ctx = c.getContext("2d");

const ship = { x:600, y:650 };
const mouse = { x:600, y:600, down:false };

c.addEventListener("mousemove",e=>{
  const r=c.getBoundingClientRect();
  mouse.x=e.clientX-r.left;
  mouse.y=e.clientY-r.top;
});
c.addEventListener("mousedown",()=>mouse.down=true);
c.addEventListener("mouseup",()=>mouse.down=false);

const impacts = []; // { ang, power, age }

const TUNE = {
  wedgeWidth: Math.PI/6,   // ~30°
  decay: 1.8,              // seconds⁻¹
  noiseAmp: 0.08,
  maxAlpha: 0.55,
  hitCooldown: 0.08
};

let last = performance.now();
let hitTimer = 0;

function clamp01(v){return Math.max(0,Math.min(1,v));}
function lerp(a,b,t){return a+(b-a)*t;}

function impactColor(t){
  // white-hot → red → amber
  let r,g,b;
  if (t < 0.5){
    const k=t/0.5;
    r=255; g=lerp(255,120,k); b=lerp(255,40,k);
  } else {
    const k=(t-0.5)/0.5;
    r=255; g=lerp(120,40,k); b=40;
  }
  return `rgb(${r|0},${g|0},${b|0})`;
}

function addImpact(x,y){
  const ang = Math.atan2(y-ship.y,x-ship.x);
  impacts.push({ ang, power:1, age:0 });
}

function update(dt){
  hitTimer -= dt;
  if(mouse.down && hitTimer<=0){
    addImpact(mouse.x,mouse.y);
    hitTimer=TUNE.hitCooldown;
  }

  for(let i=impacts.length-1;i>=0;i--){
    const im=impacts[i];
    im.age+=dt;
    im.power*=Math.exp(-TUNE.decay*dt);
    if(im.power<0.02) impacts.splice(i,1);
  }
}

function drawShip(){
  ctx.save();
  ctx.translate(ship.x,ship.y);
  ctx.fillStyle="#fff";
  ctx.beginPath();
  ctx.moveTo(0,-18);
  ctx.lineTo(14,16);
  ctx.lineTo(-14,16);
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

function drawWedges(now){
  for(const im of impacts){
    const t = clamp01(1-im.age*0.9);
    const col = impactColor(t);
    const alpha = TUNE.maxAlpha * im.power;

    ctx.save();
    ctx.translate(ship.x,ship.y);

    const noise = Math.sin(now*0.004 + im.ang*9)*TUNE.noiseAmp;

    ctx.beginPath();
    ctx.moveTo(0,0);
    ctx.arc(
      0,0,
      900,
      im.ang - TUNE.wedgeWidth*(1+noise),
      im.ang + TUNE.wedgeWidth*(1+noise)
    );
    ctx.closePath();

    const grad = ctx.createRadialGradient(0,0,200,0,0,900);
    grad.addColorStop(0, `rgba(0,0,0,0)`);
    grad.addColorStop(1, col.replace("rgb","rgba").replace(")",`,${alpha})`));

    ctx.fillStyle = grad;
    ctx.fill();
    ctx.restore();
  }
}

function drawCursor(){
  ctx.save();
  ctx.translate(mouse.x,mouse.y);
  ctx.strokeStyle="rgba(255,255,255,0.7)";
  ctx.lineWidth=2;
  ctx.beginPath();
  ctx.arc(0,0,10,0,Math.PI*2);
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(0,0,2.5,0,Math.PI*2);
  ctx.fillStyle="#fff";
  ctx.fill();
  ctx.restore();
}

function loop(now){
  const dt=Math.min(0.033,(now-last)/1000);
  last=now;

  update(dt);
  ctx.clearRect(0,0,c.width,c.height);

  drawShip();
  drawWedges(now);
  drawCursor();

  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>'>
</iframe>
</div>