Press Space to emit a shockwave that expands and physically pushes nearby dots outward as it passes


Code for Above

<div style="max-width:1500px;margin:0 auto;">
<iframe
  title="POC — Shockwave Pushback"
  scrolling="no"
  style="width:1500px;height:1500px;border:0;display:block;background:#000;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 — Shockwave Pushback</title>
<style>
  html, body { margin:0; width:100%; height:100%; overflow:hidden; background:#000; }
  canvas { display:block; outline:none; }
  .hint { position:absolute; left:12px; bottom:12px; color:#aaa; font:14px system-ui; user-select:none; }
</style>
</head>
<body>
<canvas id="c" width="1500" height="1500" tabindex="0"></canvas>
<div class="hint">CLICK canvas once • Arrows move • Space = shockwave push</div>

<script>
const c = document.getElementById("c");
const x = c.getContext("2d");
const W = 1500, H = 1500;

function focusCanvas(){ try { c.focus(); } catch(_){} }
focusCanvas();
c.addEventListener("pointerdown", focusCanvas);

// Key state inside iframe
const k = Object.create(null);
function isBlocked(code, key){
  return code === "ArrowUp" || code === "ArrowDown" || code === "ArrowLeft" || code === "ArrowRight" ||
         code === "Space" || key === " ";
}
document.addEventListener("keydown", (e) => {
  if (isBlocked(e.code, e.key)) e.preventDefault();
  if (e.code.startsWith("Arrow")) k[e.code] = true;
  if (e.code === "Space" || e.key === " "){
    if (!k.__spaceDown){
      k.__spaceDown = true;
      emitShock();
    }
  }
}, {passive:false});

document.addEventListener("keyup", (e) => {
  if (isBlocked(e.code, e.key)) e.preventDefault();
  if (e.code.startsWith("Arrow")) k[e.code] = false;
  if (e.code === "Space" || e.key === " ") k.__spaceDown = false;
}, {passive:false});

// Player
const p = { x: W/2, y: H/2, vx:0, vy:0 };
const ACCEL = 0.55, DRAG = 0.92, MAX = 14;

// Particles/dots that get pushed
const dots = Array.from({length: 110}, () => ({
  x: Math.random()*W,
  y: Math.random()*H,
  vx: (Math.random()*2-1)*0.4,
  vy: (Math.random()*2-1)*0.4,
  r: 3 + Math.random()*8
}));

// Shockwaves
const shocks = [];
const SHOCK_SPEED = 20;     // radius growth/frame
const SHOCK_BAND  = 30;     // thickness where force applies
const SHOCK_FORCE = 2.6;    // impulse strength
const DOT_DAMP    = 0.985;  // motion damping

function clamp(v,a,b){ return Math.max(a, Math.min(b,v)); }

function emitShock(){
  shocks.push({ x:p.x, y:p.y, r:0, life:1 });
}

function update(){
  // player movement
  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, W);
  p.y = clamp(p.y + p.vy, 0, H);

  // advance shocks
  for (const s of shocks){
    s.r += SHOCK_SPEED;
    s.life *= 0.986;
  }
  for (let i=shocks.length-1;i>=0;i--){
    if (shocks[i].r > 2600 || shocks[i].life < 0.06) shocks.splice(i,1);
  }

  // update dots
  for (const d of dots){
    // apply shock impulse if within band
    for (const s of shocks){
      const dx = d.x - s.x;
      const dy = d.y - s.y;
      const dist = Math.hypot(dx,dy) || 0.0001;
      const band = Math.abs(dist - s.r);
      if (band <= SHOCK_BAND){
        const nx = dx / dist;
        const ny = dy / dist;
        const t = 1 - (band / SHOCK_BAND); // 1 at center of band, 0 at edge
        const impulse = SHOCK_FORCE * t * (0.35 + s.life);
        d.vx += nx * impulse;
        d.vy += ny * impulse;
      }
    }

    // integrate + damp
    d.vx *= DOT_DAMP;
    d.vy *= DOT_DAMP;
    d.x += d.vx;
    d.y += d.vy;

    // soft bounds bounce
    if (d.x < d.r){ d.x = d.r; d.vx *= -0.35; }
    if (d.x > W-d.r){ d.x = W-d.r; d.vx *= -0.35; }
    if (d.y < d.r){ d.y = d.r; d.vy *= -0.35; }
    if (d.y > H-d.r){ d.y = H-d.r; d.vy *= -0.35; }
  }
}

function draw(){
  x.clearRect(0,0,W,H);

  // grid
  x.strokeStyle = "rgba(255,255,255,0.05)";
  for (let i=0;i<=W;i+=150){ x.beginPath(); x.moveTo(i,0); x.lineTo(i,H); x.stroke(); }
  for (let j=0;j<=H;j+=150){ x.beginPath(); x.moveTo(0,j); x.lineTo(W,j); x.stroke(); }

  // dots
  x.fillStyle = "rgba(255,255,255,0.65)";
  for (const d of dots){
    x.beginPath();
    x.arc(d.x, d.y, d.r, 0, Math.PI*2);
    x.fill();
  }

  // shock rings
  for (const s of shocks){
    x.strokeStyle = `rgba(255,255,255,${0.18 + s.life*0.35})`;
    x.lineWidth = 3;
    x.beginPath();
    x.arc(s.x, s.y, s.r, 0, Math.PI*2);
    x.stroke();
  }

  // player
  x.fillStyle = "#fff";
  x.beginPath();
  x.arc(p.x, p.y, 10, 0, Math.PI*2);
  x.fill();

  // HUD
  x.fillStyle = "rgba(255,255,255,0.6)";
  x.font = "14px system-ui";
  x.fillText(`shocks=${shocks.length}  dots=${dots.length}`, 12, 24);
}

function loop(){
  update();
  draw();
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
'></iframe>
</div>