This proof-of-concept demonstrates a physically legible shield system where impacts deform the shield envelope at the point of contact rather than subtracting abstract hit points. Incoming hits create localized dents and traveling ripples across the shield surface, conveying both direction and force without UI text. Shield strength passively recharges over time, visually smoothing the envelope back to equilibrium. Color and thickness communicate shield stress and integrity, allowing players to read damage state, impact direction, and recovery intuitively through motion and geometry alone.


Code for Above

<div style="max-width:1200px;margin:0 auto;">
  <iframe
    title="Shield Envelope Deformation — Directional Impact & Recharge 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 lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Shield Envelope Deformation POC</title>
<style>
  :root { color-scheme: dark; }
  html, body { margin:0; padding:0; width:100%; height:100%; overflow:hidden; background:#000; }
  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 canvas = document.getElementById("c");
  const ctx = canvas.getContext("2d");

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

  function lerp(a,b,t){ return a+(b-a)*t; }
  function clamp01(v){ return Math.max(0, Math.min(1, v)); }
  function dist(x1,y1,x2,y2){ return Math.hypot(x1-x2, y1-y2); }

  // Stress color: cold->amber->red (state-driven, not decorative)
  function stressColor(s){
    let r, g, b;
    if (s < 0.6) {
      const t = s / 0.6;
      r = lerp(80, 255, t);
      g = lerp(220, 180, t);
      b = lerp(255, 40, t);
    } else {
      const t = (s - 0.6) / 0.4;
      r = 255;
      g = lerp(180, 40, t);
      b = 40;
    }
    return `rgb(${r|0},${g|0},${b|0})`;
  }

  const ship = { x: 600, y: 700, r: 26 };
  const shield = {
    baseR: 120,
    strength: 1,        // 0..1
    rechargeRate: 0.12, // strength/sec
    hits: []            // impact dents/ripples
  };

  const TUNE = {
    hitCooldown: 0.08,
    dentAmp: 22,
    dentFalloff: 1.6,
    rippleAmp: 10,
    rippleFreq: 12,
    rippleDecay: 2.4,
    dentDecay: 3.0,
    shieldDamage: 0.12
  };

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

  function addHit(worldX, worldY, impact=1){
    const ang = Math.atan2(worldY - ship.y, worldX - ship.x);
    shield.hits.push({
      ang,
      dent: TUNE.dentAmp * impact,
      ripple: TUNE.rippleAmp * impact,
      t: 0
    });
    shield.strength = clamp01(shield.strength - TUNE.shieldDamage * impact);
  }

  function update(dt){
    // recharge
    shield.strength = clamp01(shield.strength + shield.rechargeRate * dt);

    // simulate hits while mouse held
    hitTimer -= dt;
    if (mouse.down && hitTimer <= 0) {
      const d = dist(mouse.x, mouse.y, ship.x, ship.y);
      const impact = clamp01(1.2 - d / (shield.baseR * 1.2));
      addHit(mouse.x, mouse.y, Math.max(0.25, impact));
      hitTimer = TUNE.hitCooldown;
    }

    // decay hit effects
    for (let i = shield.hits.length - 1; i >= 0; i--) {
      const h = shield.hits[i];
      h.t += dt;
      h.dent *= Math.exp(-TUNE.dentDecay * dt);
      h.ripple *= Math.exp(-TUNE.rippleDecay * dt);
      if (h.dent < 0.15 && h.ripple < 0.15) shield.hits.splice(i, 1);
    }
  }

  function computeRadiusAtAngle(theta, now){
    // weak shields contract slightly
    let r = shield.baseR * lerp(0.92, 1.02, shield.strength);

    for (const h of shield.hits) {
      let d = Math.abs(theta - h.ang);
      d = Math.min(d, Math.PI*2 - d);

      // Dent (localized inward)
      const dentLocal = h.dent * Math.exp(-TUNE.dentFalloff * d*d);
      r -= dentLocal;

      // Ripple (travels around ring)
      const phase = (now * 0.006) + theta * (TUNE.rippleFreq / (Math.PI*2));
      const rippleLocal = h.ripple * Math.exp(-0.9 * d*d) * Math.sin(phase * Math.PI*2);
      r += rippleLocal;
    }
    return r;
  }

  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.globalAlpha = 0.85;
    ctx.beginPath();
    ctx.arc(0, 6, 6, 0, Math.PI*2);
    ctx.fill();
    ctx.restore();
  }

  function drawShield(now){
    let activity = 0;
    for (const h of shield.hits) activity += (h.dent + h.ripple);
    activity = clamp01(activity / 40);

    const stress = clamp01((1 - shield.strength) * 0.9 + activity * 0.6);
    const col = stressColor(stress);

    const steps = 160;

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

    const alpha = lerp(0.12, 0.55, clamp01(shield.strength * 0.8 + activity * 0.8));
    ctx.globalAlpha = alpha;
    ctx.strokeStyle = col;
    ctx.lineWidth = lerp(2.0, 6.0, stress);

    ctx.beginPath();
    for (let i = 0; i <= steps; i++) {
      const theta = (i / steps) * Math.PI * 2;
      const r = computeRadiusAtAngle(theta, now);
      const x = Math.cos(theta) * r;
      const y = Math.sin(theta) * r;
      if (i === 0) ctx.moveTo(x, y);
      else ctx.lineTo(x, y);
    }
    ctx.stroke();

    // inner faint ring for depth
    ctx.globalAlpha = alpha * 0.45;
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.arc(0, 0, shield.baseR * lerp(0.72, 0.9, shield.strength), 0, Math.PI*2);
    ctx.stroke();

    ctx.restore();
  }

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

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

    update(dt);

    ctx.clearRect(0,0,canvas.width,canvas.height);
    drawShield(now);
    drawShip();
    drawCursor();

    requestAnimationFrame(loop);
  }

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