Home

31. Hopf · 1950

Burgers shock

Colors and knobs

These rewrite the live file. Copy HTML picks up whatever you set.

About

u_t + u u_x = 0 with u₀ = −amp sin(x). The most negative slope is −amp, so the classical solution ends at T* = 1/amp. Each particle keeps its speed; the x–t plot is those lines, and they cross at T*. ‖∂x u‖∞ = 1/(T*−t) until it leaves the top of the chart. After T* the parametric wave is multi-valued; the vertical mark is the entropy shock. This one actually blows up. Pointer sets the phase.

Move the pointer left or right to shift the phase of the sine.

Browser APIs

  • Canvas 2D
  • Pointer Events
  • requestAnimationFrame
  • matchMedia

If WebGL is missing, the demo draws a message on the canvas instead of a blank frame. prefers-reduced-motion freezes the first still.

Source

(function () {
  const PARAMS = {
    color: "#ff6a2a",
    amp: 1,
    speed: 1
  };
  function hexToRgb(hex) {
    const n = parseInt(String(hex).replace("#", ""), 16);
    if (isNaN(n)) return [255, 106, 42];
    return [n >> 16 & 255, n >> 8 & 255, n & 255];
  }
  function css(rgb, a) {
    return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + a + ")";
  }

  const canvas = document.getElementById("c");
  const fallback = document.getElementById("fallback");
  const ctx = canvas.getContext("2d");
  if (!ctx) {
    fallback.hidden = false;
    fallback.textContent = "Canvas 2D is not available in this browser.";
    return;
  }

  const N = 280;
  const PI = Math.PI;
  let phase = 0, look = 0, tClock = 0, last = 0;
  const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  canvas.addEventListener("pointermove", function (ev) {
    const r = canvas.getBoundingClientRect();
    look = (ev.clientX - r.left) / r.width - 0.5;
  });

  function u0(xi, amp) {
    return -amp * Math.sin(xi + phase);
  }
  function du0(xi, amp) {
    return -amp * Math.cos(xi + phase);
  }

  // Left/right states of the stationary entropy shock: η = (t amp) sin(η).
  function entropyShockU(t, amp) {
    const lambda = t * amp;
    if (lambda <= 1) return 0;
    let lo = 1e-8, hi = Math.PI - 1e-8;
    for (let k = 0; k < 20; k++) {
      const mid = 0.5 * (lo + hi);
      if (mid > lambda * Math.sin(mid)) hi = mid;
      else lo = mid;
    }
    return amp * Math.sin(0.5 * (lo + hi));
  }

  function mapX(x, box) {
    return box.x + ((x + PI) / (2 * PI)) * box.w;
  }
  function mapU(u, amp, box) {
    const m = amp * 1.25;
    return box.y + box.h * 0.5 - (u / m) * (box.h * 0.5);
  }

  function drawProfile(box, t, Tstar, amp, rgb) {
    ctx.save();
    ctx.beginPath();
    ctx.rect(box.x, box.y, box.w, box.h);
    ctx.clip();
    ctx.fillStyle = "#0b0c0e";
    ctx.fillRect(box.x, box.y, box.w, box.h);
    ctx.strokeStyle = "rgba(200,210,220,0.18)";
    ctx.lineWidth = 1;
    ctx.beginPath();
    const u0y = mapU(0, amp, box);
    ctx.moveTo(box.x, u0y);
    ctx.lineTo(box.x + box.w, u0y);
    ctx.stroke();

    const broken = t > Tstar + 1e-4;
    ctx.beginPath();
    for (let i = 0; i <= N; i++) {
      const xi = -PI + (2 * PI * i) / N;
      const u = u0(xi, amp);
      const x = xi + t * u;
      const px = mapX(x, box);
      const py = mapU(u, amp, box);
      if (i === 0) ctx.moveTo(px, py);
      else ctx.lineTo(px, py);
    }
    ctx.strokeStyle = css(rgb, broken ? 0.35 : 0.95);
    ctx.lineWidth = broken ? 1.2 : 2;
    ctx.stroke();

    if (broken) {
      const xs = mapX(0 - phase, box);
      const us = entropyShockU(t, amp);
      const ulo = mapU(us, amp, box);
      const uhi = mapU(-us, amp, box);
      ctx.strokeStyle = css(rgb, 0.95);
      ctx.lineWidth = 2.4;
      ctx.beginPath();
      ctx.moveTo(xs, ulo);
      ctx.lineTo(xs, uhi);
      ctx.stroke();
      ctx.fillStyle = css(rgb, 0.9);
      ctx.font = Math.max(10, (box.h * 0.06) | 0) + "px ui-monospace, monospace";
      ctx.fillText("shock  (entropy)", xs + 8, box.y + 14);
    }
    ctx.restore();
  }

  function drawSpaceTime(box, t, Tstar, amp, rgb, tMax) {
    ctx.save();
    ctx.beginPath();
    ctx.rect(box.x, box.y, box.w, box.h);
    ctx.clip();
    ctx.fillStyle = "#0b0c0e";
    ctx.fillRect(box.x, box.y, box.w, box.h);
    function mx(x) { return box.x + ((x + PI) / (2 * PI)) * box.w; }
    function my(tt) { return box.y + box.h - (tt / tMax) * box.h; }
    ctx.strokeStyle = "rgba(200,210,220,0.22)";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(box.x, my(Tstar));
    ctx.lineTo(box.x + box.w, my(Tstar));
    ctx.stroke();
    ctx.strokeStyle = css(rgb, 0.35);
    ctx.lineWidth = 1;
    const nChar = 22;
    for (let i = 0; i <= nChar; i++) {
      const xi = -PI + (2 * PI * i) / nChar;
      const u = u0(xi, amp);
      ctx.beginPath();
      ctx.moveTo(mx(xi), my(0));
      ctx.lineTo(mx(xi + tMax * u), my(tMax));
      ctx.stroke();
    }
    ctx.strokeStyle = css(rgb, 0.95);
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(box.x, my(t));
    ctx.lineTo(box.x + box.w, my(t));
    ctx.stroke();
    ctx.restore();
  }

  function hud(w, h, t, Tstar, amp, rgb) {
    const remain = Math.max(0, Tstar - t);
    const slope = remain < 1e-4 ? Infinity : 1 / remain;
    const fs = Math.max(11, (h * 0.028) | 0);
    ctx.font = fs + "px ui-monospace, 'IBM Plex Mono', monospace";
    ctx.textBaseline = "top";
    ctx.fillStyle = "rgba(7,6,10,0.55)";
    ctx.fillRect(8, 8, w * 0.52, fs * 4.4);
    ctx.fillStyle = "#c8d2dc";
    ctx.fillText("u_t + u u_x = 0", 14, 12);
    ctx.fillText("T* = 1/amp = " + Tstar.toFixed(2), 14, 12 + fs * 1.15);
    ctx.fillStyle = css(rgb, 1);
    const slopeTxt = !isFinite(slope) ? "∞  classical ends" : "‖∂x u‖∞ = " + slope.toFixed(2);
    ctx.fillText(slopeTxt, 14, 12 + fs * 2.3);
  }

  function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
  }

  function frame(now) {
    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);
    last = now;
    phase = look * PI;
    const amp = Math.max(0.2, PARAMS.amp);
    const Tstar = 1 / amp;
    if (!reduced) tClock += dt * 0.42 * PARAMS.speed;
    const cycle = Tstar * 1.35;
    const t = reduced ? Tstar * 0.82 : tClock % cycle;

    resize();
    const w = canvas.width, h = canvas.height;
    ctx.fillStyle = "#07060a";
    ctx.fillRect(0, 0, w, h);
    const rgb = hexToRgb(PARAMS.color);
    const pad = 10;
    const hudH = h * 0.16;
    const stH = h * 0.28;
    const prof = { x: pad, y: hudH, w: w - pad * 2, h: h - hudH - stH - pad * 2 };
    const st = { x: pad, y: h - stH - pad, w: w - pad * 2, h: stH };
    hud(w, h, t, Tstar, amp, rgb);
    drawProfile(prof, t, Tstar, amp, rgb);
    drawSpaceTime(st, t, Tstar, amp, rgb, cycle);
    const fs = Math.max(10, (h * 0.022) | 0);
    ctx.font = fs + "px ui-monospace, monospace";
    ctx.fillStyle = "rgba(200,210,220,0.7)";
    ctx.textBaseline = "bottom";
    ctx.fillText("x–t  characteristics cross at T*", st.x + 6, st.y - 4);
    if (!reduced) requestAnimationFrame(frame);
  }

  if (reduced) { resize(); frame(0); }
  else requestAnimationFrame(frame);
})();