{"slug":"burgers","number":"31","title":"Burgers shock","era":"Hopf · 1950 · 2026 illustration","section":"modern","provenance":"documented","reference":{"label":"E. Hopf, \"The partial differential equation u_t + uu_x = μu_xx\", Communications on Pure and Applied Mathematics 3, 1950","url":"https://doi.org/10.1002/cpa.3160030302"},"summary":"Inviscid Burgers. Characteristics cross at a predicted T*.","description":"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.","apis":["Canvas 2D","Pointer Events","requestAnimationFrame","matchMedia"],"tags":["pde","shock"],"hasGlsl":false,"interaction":"Move the pointer left or right to shift the phase of the sine.","url":"https://3d-retro.com/experiments/burgers","example_url":"https://3d-retro.com/examples/burgers.html","api_url":"https://3d-retro.com/api/v1/experiments/burgers","html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <meta name=\"description\" content=\"Inviscid Burgers. Characteristics cross at a predicted T*. The classical solution ends.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/burgers\" />\n  <title>Burgers shock · 3d retro graphics</title>\n  <style>\n    html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #07060a; color: #e8e6e1; position: absolute; inset: 0; }\n    canvas { display: block; position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }\n    .fallback { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; font: 14px/1.5 ui-monospace, \"IBM Plex Mono\", monospace; text-align: center; color: #e8e6e1; background: #07060a; }\n    .fallback[hidden] { display: none; }\n  </style>\n</head>\n<body>\n<canvas id=\"c\"></canvas>\n<div id=\"fallback\" class=\"fallback\" hidden></div>\n<script>\n(function () {\n  const PARAMS = {\n    color: \"#ff6a2a\",\n    amp: 1,\n    speed: 1\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [255, 106, 42];\n    return [n >> 16 & 255, n >> 8 & 255, n & 255];\n  }\n  function css(rgb, a) {\n    return \"rgba(\" + rgb[0] + \",\" + rgb[1] + \",\" + rgb[2] + \",\" + a + \")\";\n  }\n\n  const canvas = document.getElementById(\"c\");\n  const fallback = document.getElementById(\"fallback\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    fallback.hidden = false;\n    fallback.textContent = \"Canvas 2D is not available in this browser.\";\n    return;\n  }\n\n  const N = 280;\n  const PI = Math.PI;\n  let phase = 0, look = 0, tClock = 0, last = 0;\n  let raf = 0;\n  const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  let reduced = motionQuery.matches;\n\n  canvas.addEventListener(\"pointermove\", function (ev) {\n    const r = canvas.getBoundingClientRect();\n    look = (ev.clientX - r.left) / r.width - 0.5;\n    wake();\n  });\n\n  function u0(xi, amp) {\n    return -amp * Math.sin(xi + phase);\n  }\n  function du0(xi, amp) {\n    return -amp * Math.cos(xi + phase);\n  }\n\n  // Left/right states of the stationary entropy shock: η = (t amp) sin(η).\n  function entropyShockU(t, amp) {\n    const lambda = t * amp;\n    if (lambda <= 1) return 0;\n    let lo = 1e-8, hi = Math.PI - 1e-8;\n    for (let k = 0; k < 20; k++) {\n      const mid = 0.5 * (lo + hi);\n      if (mid > lambda * Math.sin(mid)) hi = mid;\n      else lo = mid;\n    }\n    return amp * Math.sin(0.5 * (lo + hi));\n  }\n\n  function mapX(x, box) {\n    return box.x + ((x + PI) / (2 * PI)) * box.w;\n  }\n  function mapU(u, amp, box) {\n    const m = amp * 1.25;\n    return box.y + box.h * 0.5 - (u / m) * (box.h * 0.5);\n  }\n\n  function drawProfile(box, t, Tstar, amp, rgb) {\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(box.x, box.y, box.w, box.h);\n    ctx.clip();\n    ctx.fillStyle = \"#0b0c0e\";\n    ctx.fillRect(box.x, box.y, box.w, box.h);\n    ctx.strokeStyle = \"rgba(200,210,220,0.18)\";\n    ctx.lineWidth = 1;\n    ctx.beginPath();\n    const u0y = mapU(0, amp, box);\n    ctx.moveTo(box.x, u0y);\n    ctx.lineTo(box.x + box.w, u0y);\n    ctx.stroke();\n\n    const broken = t > Tstar + 1e-4;\n    ctx.beginPath();\n    for (let i = 0; i <= N; i++) {\n      const xi = -PI + (2 * PI * i) / N;\n      const u = u0(xi, amp);\n      const x = xi + t * u;\n      const px = mapX(x, box);\n      const py = mapU(u, amp, box);\n      if (i === 0) ctx.moveTo(px, py);\n      else ctx.lineTo(px, py);\n    }\n    ctx.strokeStyle = css(rgb, broken ? 0.35 : 0.95);\n    ctx.lineWidth = broken ? 1.2 : 2;\n    ctx.stroke();\n\n    if (broken) {\n      const xs = mapX(0 - phase, box);\n      const us = entropyShockU(t, amp);\n      const ulo = mapU(us, amp, box);\n      const uhi = mapU(-us, amp, box);\n      ctx.strokeStyle = css(rgb, 0.95);\n      ctx.lineWidth = 2.4;\n      ctx.beginPath();\n      ctx.moveTo(xs, ulo);\n      ctx.lineTo(xs, uhi);\n      ctx.stroke();\n      ctx.fillStyle = css(rgb, 0.9);\n      ctx.font = Math.max(10, (box.h * 0.06) | 0) + \"px ui-monospace, monospace\";\n      ctx.fillText(\"shock  (entropy)\", xs + 8, box.y + 14);\n    }\n    ctx.restore();\n  }\n\n  function drawSpaceTime(box, t, Tstar, amp, rgb, tMax) {\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(box.x, box.y, box.w, box.h);\n    ctx.clip();\n    ctx.fillStyle = \"#0b0c0e\";\n    ctx.fillRect(box.x, box.y, box.w, box.h);\n    function mx(x) { return box.x + ((x + PI) / (2 * PI)) * box.w; }\n    function my(tt) { return box.y + box.h - (tt / tMax) * box.h; }\n    ctx.strokeStyle = \"rgba(200,210,220,0.22)\";\n    ctx.lineWidth = 1;\n    ctx.beginPath();\n    ctx.moveTo(box.x, my(Tstar));\n    ctx.lineTo(box.x + box.w, my(Tstar));\n    ctx.stroke();\n    ctx.strokeStyle = css(rgb, 0.35);\n    ctx.lineWidth = 1;\n    const nChar = 22;\n    for (let i = 0; i <= nChar; i++) {\n      const xi = -PI + (2 * PI * i) / nChar;\n      const u = u0(xi, amp);\n      ctx.beginPath();\n      ctx.moveTo(mx(xi), my(0));\n      ctx.lineTo(mx(xi + tMax * u), my(tMax));\n      ctx.stroke();\n    }\n    ctx.strokeStyle = css(rgb, 0.95);\n    ctx.lineWidth = 1.5;\n    ctx.beginPath();\n    ctx.moveTo(box.x, my(t));\n    ctx.lineTo(box.x + box.w, my(t));\n    ctx.stroke();\n    ctx.restore();\n  }\n\n  function hud(w, h, t, Tstar, amp, rgb) {\n    const remain = Math.max(0, Tstar - t);\n    const slope = remain < 1e-4 ? Infinity : 1 / remain;\n    const fs = Math.max(11, (h * 0.028) | 0);\n    ctx.font = fs + \"px ui-monospace, 'IBM Plex Mono', monospace\";\n    ctx.textBaseline = \"top\";\n    ctx.fillStyle = \"rgba(7,6,10,0.55)\";\n    ctx.fillRect(8, 8, w * 0.52, fs * 4.4);\n    ctx.fillStyle = \"#c8d2dc\";\n    ctx.fillText(\"u_t + u u_x = 0\", 14, 12);\n    ctx.fillText(\"T* = 1/amp = \" + Tstar.toFixed(2), 14, 12 + fs * 1.15);\n    ctx.fillStyle = css(rgb, 1);\n    const slopeTxt = !isFinite(slope) ? \"∞  classical ends\" : \"‖∂x u‖∞ = \" + slope.toFixed(2);\n    ctx.fillText(slopeTxt, 14, 12 + fs * 2.3);\n  }\n\n  function resize() {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }\n  }\n\n  function frame(now) {\n    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);\n    last = now;\n    phase = look * PI;\n    const amp = Math.max(0.2, PARAMS.amp);\n    const Tstar = 1 / amp;\n    if (!reduced) tClock += dt * 0.42 * PARAMS.speed;\n    const cycle = Tstar * 1.35;\n    const t = reduced ? Tstar * 0.82 : tClock % cycle;\n\n    resize();\n    const w = canvas.width, h = canvas.height;\n    ctx.fillStyle = \"#07060a\";\n    ctx.fillRect(0, 0, w, h);\n    const rgb = hexToRgb(PARAMS.color);\n    const pad = 10;\n    const hudH = h * 0.16;\n    const stH = h * 0.28;\n    const prof = { x: pad, y: hudH, w: w - pad * 2, h: h - hudH - stH - pad * 2 };\n    const st = { x: pad, y: h - stH - pad, w: w - pad * 2, h: stH };\n    hud(w, h, t, Tstar, amp, rgb);\n    drawProfile(prof, t, Tstar, amp, rgb);\n    drawSpaceTime(st, t, Tstar, amp, rgb, cycle);\n    const fs = Math.max(10, (h * 0.022) | 0);\n    ctx.font = fs + \"px ui-monospace, monospace\";\n    ctx.fillStyle = \"rgba(200,210,220,0.7)\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(\"x–t  characteristics cross at T*\", st.x + 6, st.y - 4);\n    if (!reduced) raf = requestAnimationFrame(frame);\n  }\n\n  // Motion. The system's reduced-motion preference decides by default. A\n  // standalone copy animates anyway with \"#motion=play\" in its URL (or holds a\n  // frame with \"#motion=still\"); an embedding page can post {motion: \"play\"}.\n  let motionMode = \"auto\";\n  function applyMotion() {\n    const next = motionMode === \"still\" || (motionMode === \"auto\" && motionQuery.matches);\n    if (next === reduced) return;\n    reduced = next;\n    last = 0;\n    if (reduced) { cancelAnimationFrame(raf); raf = 0; frame(performance.now()); }\n    else if (!raf) raf = requestAnimationFrame(frame);\n  }\n  function setMotion(mode) {\n    if (mode === \"play\" || mode === \"still\" || mode === \"auto\") { motionMode = mode; applyMotion(); }\n  }\n  function readMotionHash() {\n    const m = /motion=(play|still|auto)/.exec(location.hash);\n    let mode = m ? m[1] : \"auto\";\n    // Embedded in the gallery, the page sets window.__demoMotion before any frame loads.\n    try { if (!m && window.parent !== window && typeof window.parent.__demoMotion === \"string\") mode = window.parent.__demoMotion; } catch (e) { /* cross-origin parent */ }\n    setMotion(mode);\n  }\n  function wake() { if (reduced) frame(performance.now()); }\n  if (motionQuery.addEventListener) motionQuery.addEventListener(\"change\", applyMotion);\n  else if (motionQuery.addListener) motionQuery.addListener(applyMotion);\n  window.addEventListener(\"hashchange\", readMotionHash);\n  window.addEventListener(\"message\", function (e) { if (e.data && typeof e.data.motion === \"string\") setMotion(e.data.motion); });\n  window.addEventListener(\"resize\", wake);\n\n  if (reduced) frame(performance.now());\n  else raf = requestAnimationFrame(frame);\n  readMotionHash();\n})();\n</script>\n</body>\n</html>\n"}