{"slug":"teapot","number":"17","title":"Utah teapot","era":"Newell · 1975","section":"historical","provenance":"documented","reference":{"label":"M. Newell's teapot data set, University of Utah, 1975, as archived at cs.utah.edu/~natevm/newell_teaset and described in F. Crow, \"The Origins of the Teapot\", IEEE CG&A 7(1), 1987","url":"https://www.cs.utah.edu/~natevm/newell_teaset/newell_teaset.zip"},"summary":"The real Utah teapot: Newell's 32 Bézier patches, lit per pixel. Move the pointer to drag the lights.","description":"Martin Newell's 1975 teapot, built from his original 306 control points and 32 bicubic Bézier patches rather than approximated. The patches are evaluated in JavaScript at load, with analytic normals, in Newell's own proportions (the 4/3 vertical squash some renderers apply is not used). Lighting is per-pixel Phong with two point lights that sit in the scene as little orbs. Move the pointer: the key light follows, the fill stays opposite. Ceramic, lamp, and fill colours are on the tweak row.","apis":["WebGL 1","GLSL ES 1.0","Pointer Events","requestAnimationFrame","matchMedia"],"tags":["webgl","3d"],"hasGlsl":true,"interaction":"Move the pointer to drag the two lights around the teapot.","url":"https://3d-retro.com/experiments/teapot","example_url":"https://3d-retro.com/examples/teapot.html","api_url":"https://3d-retro.com/api/v1/experiments/teapot","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=\"The Utah teapot from Newell's 32 Bézier patches, lit per pixel. Move the pointer to drag the lights.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/teapot\" />\n  <title>Utah teapot · 3d retro graphics</title>\n  <style>\n    html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #0b0c0e; 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: #0b0c0e; }\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: \"#d8c4a8\",\n    light: \"#fff1c8\",\n    fill: \"#6ee7b7\"\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [0.85, 0.77, 0.66];\n    return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];\n  }\n\n  // The Utah teapot. Martin Newell modelled it at the University of Utah in\n  // 1975 as 28 bicubic Bézier patches; the four patches that close the bottom\n  // were added later and ship with the data set. These are the 306 control\n  // points and 32 patches of the \"teapot\" file in the University of Utah\n  // archive of Newell's tea set (https://www.cs.utah.edu/~natevm/newell_teaset/),\n  // the same data Frank Crow described in \"The Origins of the Teapot\", IEEE\n  // Computer Graphics and Applications 7(1), 1987. The points are in Newell's\n  // original units, z up, handle toward +x and spout toward -x; they are not\n  // squashed by the 4/3 factor that some renderers apply. Each patch row lists\n  // its sixteen control-point indices, zero-based.\n  const TEAPOT_VERTS = [\n    1.4,0,2.4,1.4,-.784,2.4,.784,-1.4,2.4,0,-1.4,2.4,1.3375,0,2.5313,1.3375,-.749,2.5313,.749,-1.3375,2.5313,0,-1.3375,2.5313,\n    1.4375,0,2.5313,1.4375,-.805,2.5313,.805,-1.4375,2.5313,0,-1.4375,2.5313,1.5,0,2.4,1.5,-.84,2.4,.84,-1.5,2.4,0,-1.5,2.4,\n    -.784,-1.4,2.4,-1.4,-.784,2.4,-1.4,0,2.4,-.749,-1.3375,2.5313,-1.3375,-.749,2.5313,-1.3375,0,2.5313,-.805,-1.4375,2.5313,-1.4375,-.805,2.5313,\n    -1.4375,0,2.5313,-.84,-1.5,2.4,-1.5,-.84,2.4,-1.5,0,2.4,-1.4,.784,2.4,-.784,1.4,2.4,0,1.4,2.4,-1.3375,.749,2.5313,\n    -.749,1.3375,2.5313,0,1.3375,2.5313,-1.4375,.805,2.5313,-.805,1.4375,2.5313,0,1.4375,2.5313,-1.5,.84,2.4,-.84,1.5,2.4,0,1.5,2.4,\n    .784,1.4,2.4,1.4,.784,2.4,.749,1.3375,2.5313,1.3375,.749,2.5313,.805,1.4375,2.5313,1.4375,.805,2.5313,.84,1.5,2.4,1.5,.84,2.4,\n    1.75,0,1.875,1.75,-.98,1.875,.98,-1.75,1.875,0,-1.75,1.875,2,0,1.35,2,-1.12,1.35,1.12,-2,1.35,0,-2,1.35,\n    2,0,.9,2,-1.12,.9,1.12,-2,.9,0,-2,.9,-.98,-1.75,1.875,-1.75,-.98,1.875,-1.75,0,1.875,-1.12,-2,1.35,\n    -2,-1.12,1.35,-2,0,1.35,-1.12,-2,.9,-2,-1.12,.9,-2,0,.9,-1.75,.98,1.875,-.98,1.75,1.875,0,1.75,1.875,\n    -2,1.12,1.35,-1.12,2,1.35,0,2,1.35,-2,1.12,.9,-1.12,2,.9,0,2,.9,.98,1.75,1.875,1.75,.98,1.875,\n    1.12,2,1.35,2,1.12,1.35,1.12,2,.9,2,1.12,.9,2,0,.45,2,-1.12,.45,1.12,-2,.45,0,-2,.45,\n    1.5,0,.225,1.5,-.84,.225,.84,-1.5,.225,0,-1.5,.225,1.5,0,.15,1.5,-.84,.15,.84,-1.5,.15,0,-1.5,.15,\n    -1.12,-2,.45,-2,-1.12,.45,-2,0,.45,-.84,-1.5,.225,-1.5,-.84,.225,-1.5,0,.225,-.84,-1.5,.15,-1.5,-.84,.15,\n    -1.5,0,.15,-2,1.12,.45,-1.12,2,.45,0,2,.45,-1.5,.84,.225,-.84,1.5,.225,0,1.5,.225,-1.5,.84,.15,\n    -.84,1.5,.15,0,1.5,.15,1.12,2,.45,2,1.12,.45,.84,1.5,.225,1.5,.84,.225,.84,1.5,.15,1.5,.84,.15,\n    -1.6,0,2.025,-1.6,-.3,2.025,-1.5,-.3,2.25,-1.5,0,2.25,-2.3,0,2.025,-2.3,-.3,2.025,-2.5,-.3,2.25,-2.5,0,2.25,\n    -2.7,0,2.025,-2.7,-.3,2.025,-3,-.3,2.25,-3,0,2.25,-2.7,0,1.8,-2.7,-.3,1.8,-3,-.3,1.8,-3,0,1.8,\n    -1.5,.3,2.25,-1.6,.3,2.025,-2.5,.3,2.25,-2.3,.3,2.025,-3,.3,2.25,-2.7,.3,2.025,-3,.3,1.8,-2.7,.3,1.8,\n    -2.7,0,1.575,-2.7,-.3,1.575,-3,-.3,1.35,-3,0,1.35,-2.5,0,1.125,-2.5,-.3,1.125,-2.65,-.3,.9375,-2.65,0,.9375,\n    -2,-.3,.9,-1.9,-.3,.6,-1.9,0,.6,-3,.3,1.35,-2.7,.3,1.575,-2.65,.3,.9375,-2.5,.3,1.125,-1.9,.3,.6,\n    -2,.3,.9,1.7,0,1.425,1.7,-.66,1.425,1.7,-.66,.6,1.7,0,.6,2.6,0,1.425,2.6,-.66,1.425,3.1,-.66,.825,\n    3.1,0,.825,2.3,0,2.1,2.3,-.25,2.1,2.4,-.25,2.025,2.4,0,2.025,2.7,0,2.4,2.7,-.25,2.4,3.3,-.25,2.4,\n    3.3,0,2.4,1.7,.66,.6,1.7,.66,1.425,3.1,.66,.825,2.6,.66,1.425,2.4,.25,2.025,2.3,.25,2.1,3.3,.25,2.4,\n    2.7,.25,2.4,2.8,0,2.475,2.8,-.25,2.475,3.525,-.25,2.4937,3.525,0,2.4937,2.9,0,2.475,2.9,-.15,2.475,3.45,-.15,2.5125,\n    3.45,0,2.5125,2.8,0,2.4,2.8,-.15,2.4,3.2,-.15,2.4,3.2,0,2.4,3.525,.25,2.4937,2.8,.25,2.475,3.45,.15,2.5125,\n    2.9,.15,2.475,3.2,.15,2.4,2.8,.15,2.4,0,0,3.15,0,-.002,3.15,.002,0,3.15,.8,0,3.15,.8,-.45,3.15,\n    .45,-.8,3.15,0,-.8,3.15,0,0,2.85,.2,0,2.7,.2,-.112,2.7,.112,-.2,2.7,0,-.2,2.7,-.002,0,3.15,\n    -.45,-.8,3.15,-.8,-.45,3.15,-.8,0,3.15,-.112,-.2,2.7,-.2,-.112,2.7,-.2,0,2.7,0,.002,3.15,-.8,.45,3.15,\n    -.45,.8,3.15,0,.8,3.15,-.2,.112,2.7,-.112,.2,2.7,0,.2,2.7,.45,.8,3.15,.8,.45,3.15,.112,.2,2.7,\n    .2,.112,2.7,.4,0,2.55,.4,-.224,2.55,.224,-.4,2.55,0,-.4,2.55,1.3,0,2.55,1.3,-.728,2.55,.728,-1.3,2.55,\n    0,-1.3,2.55,1.3,0,2.4,1.3,-.728,2.4,.728,-1.3,2.4,0,-1.3,2.4,-.224,-.4,2.55,-.4,-.224,2.55,-.4,0,2.55,\n    -.728,-1.3,2.55,-1.3,-.728,2.55,-1.3,0,2.55,-.728,-1.3,2.4,-1.3,-.728,2.4,-1.3,0,2.4,-.4,.224,2.55,-.224,.4,2.55,\n    0,.4,2.55,-1.3,.728,2.55,-.728,1.3,2.55,0,1.3,2.55,-1.3,.728,2.4,-.728,1.3,2.4,0,1.3,2.4,.224,.4,2.55,\n    .4,.224,2.55,.728,1.3,2.55,1.3,.728,2.55,.728,1.3,2.4,1.3,.728,2.4,0,0,0,1.5,0,.15,1.5,.84,.15,\n    .84,1.5,.15,0,1.5,.15,1.5,0,.075,1.5,.84,.075,.84,1.5,.075,0,1.5,.075,1.425,0,0,1.425,.798,0,\n    .798,1.425,0,0,1.425,0,-.84,1.5,.15,-1.5,.84,.15,-1.5,0,.15,-.84,1.5,.075,-1.5,.84,.075,-1.5,0,.075,\n    -.798,1.425,0,-1.425,.798,0,-1.425,0,0,-1.5,-.84,.15,-.84,-1.5,.15,0,-1.5,.15,-1.5,-.84,.075,-.84,-1.5,.075,\n    0,-1.5,.075,-1.425,-.798,0,-.798,-1.425,0,0,-1.425,0,.84,-1.5,.15,1.5,-.84,.15,.84,-1.5,.075,1.5,-.84,.075,\n    .798,-1.425,0,1.425,-.798,0\n  ];\n  const TEAPOT_PATCHES = [\n    0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,\n    3,16,17,18,7,19,20,21,11,22,23,24,15,25,26,27,\n    18,28,29,30,21,31,32,33,24,34,35,36,27,37,38,39,\n    30,40,41,0,33,42,43,4,36,44,45,8,39,46,47,12,\n    12,13,14,15,48,49,50,51,52,53,54,55,56,57,58,59,\n    15,25,26,27,51,60,61,62,55,63,64,65,59,66,67,68,\n    27,37,38,39,62,69,70,71,65,72,73,74,68,75,76,77,\n    39,46,47,12,71,78,79,48,74,80,81,52,77,82,83,56,\n    56,57,58,59,84,85,86,87,88,89,90,91,92,93,94,95,\n    59,66,67,68,87,96,97,98,91,99,100,101,95,102,103,104,\n    68,75,76,77,98,105,106,107,101,108,109,110,104,111,112,113,\n    77,82,83,56,107,114,115,84,110,116,117,88,113,118,119,92,\n    120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,\n    123,136,137,120,127,138,139,124,131,140,141,128,135,142,143,132,\n    132,133,134,135,144,145,146,147,148,149,150,151,68,152,153,154,\n    135,142,143,132,147,155,156,144,151,157,158,148,154,159,160,68,\n    161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,\n    164,177,178,161,168,179,180,165,172,181,182,169,176,183,184,173,\n    173,174,175,176,185,186,187,188,189,190,191,192,193,194,195,196,\n    176,183,184,173,188,197,198,185,192,199,200,189,196,201,202,193,\n    203,203,203,203,206,207,208,209,210,210,210,210,211,212,213,214,\n    203,203,203,203,209,216,217,218,210,210,210,210,214,219,220,221,\n    203,203,203,203,218,223,224,225,210,210,210,210,221,226,227,228,\n    203,203,203,203,225,229,230,206,210,210,210,210,228,231,232,211,\n    211,212,213,214,233,234,235,236,237,238,239,240,241,242,243,244,\n    214,219,220,221,236,245,246,247,240,248,249,250,244,251,252,253,\n    221,226,227,228,247,254,255,256,250,257,258,259,253,260,261,262,\n    228,231,232,211,256,263,264,233,259,265,266,237,262,267,268,241,\n    269,269,269,269,278,279,280,281,274,275,276,277,270,271,272,273,\n    269,269,269,269,281,288,289,290,277,285,286,287,273,282,283,284,\n    269,269,269,269,290,297,298,299,287,294,295,296,284,291,292,293,\n    269,269,269,269,299,304,305,278,296,302,303,274,293,300,301,270\n  ];\n\n  function buildTeapot(segments) {\n    const pos = [], nrm = [], idx = [];\n    function B(t) { const s = 1 - t; return [s * s * s, 3 * s * s * t, 3 * s * t * t, t * t * t]; }\n    function dB(t) { const s = 1 - t; return [-3 * s * s, 3 * s * s - 6 * s * t, 6 * s * t - 3 * t * t, 3 * t * t]; }\n    function tangents(patch, u, v) {\n      const bu = B(u), bv = B(v), du = dB(u), dv = dB(v);\n      const tu = [0, 0, 0], tv = [0, 0, 0];\n      for (let i = 0; i < 4; i++) {\n        for (let j = 0; j < 4; j++) {\n          const k = TEAPOT_PATCHES[patch * 16 + i * 4 + j] * 3;\n          for (let c = 0; c < 3; c++) {\n            tu[c] += du[i] * bv[j] * TEAPOT_VERTS[k + c];\n            tv[c] += bu[i] * dv[j] * TEAPOT_VERTS[k + c];\n          }\n        }\n      }\n      return [tu, tv];\n    }\n    function normal(patch, u, v) {\n      // Pv x Pu: Newell's parameterisation runs clockwise seen from outside,\n      // so Pu x Pv points into the pot. Where a patch edge collapses to a\n      // point (lid tip, bottom centre) one tangent vanishes; nudge inward.\n      let t = tangents(patch, u, v);\n      let n = [t[1][1] * t[0][2] - t[1][2] * t[0][1], t[1][2] * t[0][0] - t[1][0] * t[0][2], t[1][0] * t[0][1] - t[1][1] * t[0][0]];\n      let len = Math.hypot(n[0], n[1], n[2]);\n      if (len < 1e-6) {\n        const e = 1e-3;\n        t = tangents(patch, u < 0.5 ? u + e : u - e, v < 0.5 ? v + e : v - e);\n        n = [t[1][1] * t[0][2] - t[1][2] * t[0][1], t[1][2] * t[0][0] - t[1][0] * t[0][2], t[1][0] * t[0][1] - t[1][1] * t[0][0]];\n        len = Math.hypot(n[0], n[1], n[2]) || 1;\n      }\n      return [n[0] / len, n[1] / len, n[2] / len];\n    }\n    for (let p = 0; p < 32; p++) {\n      const base = pos.length / 3;\n      for (let i = 0; i <= segments; i++) {\n        const u = i / segments, bu = B(u);\n        for (let j = 0; j <= segments; j++) {\n          const v = j / segments, bv = B(v);\n          let x = 0, y = 0, z = 0;\n          for (let a = 0; a < 4; a++) {\n            for (let b = 0; b < 4; b++) {\n              const k = TEAPOT_PATCHES[p * 16 + a * 4 + b] * 3, w = bu[a] * bv[b];\n              x += w * TEAPOT_VERTS[k]; y += w * TEAPOT_VERTS[k + 1]; z += w * TEAPOT_VERTS[k + 2];\n            }\n          }\n          const n = normal(p, u, v);\n          // Newell's z is up. Turn the pot upright for a y-up scene.\n          pos.push(x, z, -y);\n          nrm.push(n[0], n[2], -n[1]);\n        }\n      }\n      for (let i = 0; i < segments; i++) {\n        for (let j = 0; j < segments; j++) {\n          const a = base + i * (segments + 1) + j, b = a + segments + 1;\n          idx.push(a, a + 1, b, a + 1, b + 1, b);\n        }\n      }\n    }\n    return { pos: new Float32Array(pos), nrm: new Float32Array(nrm), idx: new Uint16Array(idx) };\n  }\n\n  function sphere(r, segs) {\n    const pos = [], nrm = [];\n    function pt(a, b) {\n      const x = Math.sin(a) * Math.cos(b), y = Math.cos(a), z = Math.sin(a) * Math.sin(b);\n      return [x * r, y * r, z * r];\n    }\n    for (let i = 0; i < segs; i++) {\n      const a0 = (i / segs) * Math.PI, a1 = ((i + 1) / segs) * Math.PI;\n      for (let j = 0; j < segs * 2; j++) {\n        const b0 = (j / (segs * 2)) * Math.PI * 2, b1 = ((j + 1) / (segs * 2)) * Math.PI * 2;\n        const p00 = pt(a0, b0), p10 = pt(a1, b0), p11 = pt(a1, b1), p01 = pt(a0, b1);\n        // Counter-clockwise from outside: ring i+1 to ring i runs the other way.\n        for (const q of [p00, p11, p10, p00, p01, p11]) { pos.push(q[0], q[1], q[2]); nrm.push(q[0] / r, q[1] / r, q[2] / r); }\n      }\n    }\n    return { pos: new Float32Array(pos), nrm: new Float32Array(nrm), count: pos.length / 3 };\n  }\n\n  const canvas = document.getElementById(\"c\");\n  const fallback = document.getElementById(\"fallback\");\n  function fail(message) {\n    fallback.hidden = false;\n    fallback.textContent = message;\n  }\n  const gl = canvas.getContext(\"webgl\", { alpha: false, antialias: true });\n  if (!gl) {\n    fail(\"WebGL is not available in this browser. Try a current Firefox, Chrome, Safari, or Edge.\");\n    return;\n  }\n\n  const VERT = `attribute vec3 a_pos;\nattribute vec3 a_nrm;\nuniform mat4 u_mvp;\nuniform mat4 u_model;\nvarying vec3 v_nrm;\nvarying vec3 v_pos;\nvoid main() {\n  vec4 world = u_model * vec4(a_pos, 1.0);\n  v_pos = world.xyz;\n  v_nrm = mat3(u_model) * a_nrm;\n  gl_Position = u_mvp * vec4(a_pos, 1.0);\n}`;\n  const FRAG = `precision mediump float;\nvarying vec3 v_nrm;\nvarying vec3 v_pos;\nuniform vec3 u_cam;\nuniform vec3 u_color;\nuniform vec3 u_lightA;\nuniform vec3 u_lightB;\nuniform vec3 u_colA;\nuniform vec3 u_colB;\nvoid main() {\n  vec3 n = normalize(v_nrm);\n  vec3 v = normalize(u_cam - v_pos);\n  vec3 la = normalize(u_lightA - v_pos);\n  vec3 lb = normalize(u_lightB - v_pos);\n  float d1 = max(dot(n, la), 0.0);\n  float d2 = max(dot(n, lb), 0.0);\n  float s1 = pow(max(dot(reflect(-la, n), v), 0.0), 48.0);\n  float s2 = pow(max(dot(reflect(-lb, n), v), 0.0), 24.0);\n  vec3 col = u_color * (0.12 + 0.72 * d1 + 0.38 * d2);\n  col += u_colA * s1 * 0.85;\n  col += u_colB * s2 * 0.45;\n  gl_FragColor = vec4(col, 1.0);\n}`;\n  const BALL_FRAG = `precision mediump float;\nuniform vec3 u_colA;\nvoid main() { gl_FragColor = vec4(u_colA, 1.0); }`;\n\n  function compile(type, src) {\n    const sh = gl.createShader(type);\n    gl.shaderSource(sh, src);\n    gl.compileShader(sh);\n    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh) || \"shader\");\n    return sh;\n  }\n  function program(vs, fs) {\n    const p = gl.createProgram();\n    gl.attachShader(p, compile(gl.VERTEX_SHADER, vs));\n    gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fs));\n    gl.linkProgram(p);\n    if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p) || \"link\");\n    return p;\n  }\n  function mul(a, b) {\n    const o = new Float32Array(16);\n    for (let c = 0; c < 4; c++) {\n      for (let r = 0; r < 4; r++) {\n        o[c * 4 + r] = a[r]*b[c*4] + a[4+r]*b[c*4+1] + a[8+r]*b[c*4+2] + a[12+r]*b[c*4+3];\n      }\n    }\n    return o;\n  }\n  function perspective(fovy, aspect, near, far) {\n    const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);\n    const o = new Float32Array(16);\n    o[0] = f / aspect; o[5] = f; o[10] = (far + near) * nf; o[11] = -1; o[14] = 2 * far * near * nf;\n    return o;\n  }\n  function translate(x, y, z) {\n    const o = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);\n    o[12] = x; o[13] = y; o[14] = z;\n    return o;\n  }\n  function scale(s) {\n    return new Float32Array([s,0,0,0, 0,s,0,0, 0,0,s,0, 0,0,0,1]);\n  }\n  function rotateY(a) {\n    const c = Math.cos(a), s = Math.sin(a);\n    return new Float32Array([c,0,-s,0, 0,1,0,0, s,0,c,0, 0,0,0,1]);\n  }\n\n  const mesh = buildTeapot(10);\n  const ball = sphere(1, 8);\n\n  const meshProg = program(VERT, FRAG);\n  const ballProg = program(VERT, BALL_FRAG);\n  function makeBuf(target, data) {\n    const b = gl.createBuffer();\n    gl.bindBuffer(target, b);\n    gl.bufferData(target, data, gl.STATIC_DRAW);\n    return b;\n  }\n  const posBuf = makeBuf(gl.ARRAY_BUFFER, mesh.pos);\n  const nrmBuf = makeBuf(gl.ARRAY_BUFFER, mesh.nrm);\n  const idxBuf = makeBuf(gl.ELEMENT_ARRAY_BUFFER, mesh.idx);\n  const ballP = makeBuf(gl.ARRAY_BUFFER, ball.pos);\n  const ballN = makeBuf(gl.ARRAY_BUFFER, ball.nrm);\n\n  function bind(prog, name, buf, size) {\n    const loc = gl.getAttribLocation(prog, name);\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.enableVertexAttribArray(loc);\n    gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0);\n  }\n\n  gl.enable(gl.DEPTH_TEST);\n  gl.enable(gl.CULL_FACE);\n  gl.clearColor(0.043, 0.047, 0.055, 1);\n\n  let az = 0.55, el = 0.7, yaw = 0.95, 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 (e) {\n    const r = canvas.getBoundingClientRect();\n    az = ((e.clientX - r.left) / r.width - 0.5) * Math.PI * 1.4;\n    el = 0.25 + (1 - (e.clientY - r.top) / r.height) * 1.15;\n    wake();\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) {\n      canvas.width = w;\n      canvas.height = h;\n    }\n    gl.viewport(0, 0, canvas.width, canvas.height);\n  }\n\n  function lightPos(a, e, rad) {\n    return [Math.cos(e) * Math.sin(a) * rad, Math.sin(e) * rad, Math.cos(e) * Math.cos(a) * rad];\n  }\n\n  // Newell's pot is 3.15 units tall and spans x from -3 (spout tip) to 3.525\n  // (handle). Half size, centred on the handle-to-spout span, base at y = 0.\n  const POT_SCALE = 0.5;\n  const POT_X = -0.26;\n  const CAM = [0, 0.85, 5.4];\n\n  function frame(now) {\n    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);\n    last = now;\n    if (!reduced) yaw += dt * 0.35;\n    resize();\n    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n    const aspect = canvas.width / canvas.height;\n    const proj = perspective(Math.PI / 4.2, aspect, 0.1, 40);\n    const view = translate(-CAM[0], -CAM[1], -CAM[2]);\n    const model = mul(rotateY(yaw), mul(translate(POT_X, 0, 0), scale(POT_SCALE)));\n    const mvp = mul(mul(proj, view), model);\n    const L1 = lightPos(az, el, 3.4);\n    const L2 = lightPos(az + 2.3, el * 0.55 + 0.2, 3.2);\n\n    gl.useProgram(meshProg);\n    gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, \"u_mvp\"), false, mvp);\n    gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, \"u_model\"), false, model);\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_cam\"), CAM);\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_color\"), hexToRgb(PARAMS.color));\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_lightA\"), L1);\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_lightB\"), L2);\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_colA\"), hexToRgb(PARAMS.light));\n    gl.uniform3fv(gl.getUniformLocation(meshProg, \"u_colB\"), hexToRgb(PARAMS.fill));\n    bind(meshProg, \"a_pos\", posBuf, 3);\n    bind(meshProg, \"a_nrm\", nrmBuf, 3);\n    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idxBuf);\n    gl.drawElements(gl.TRIANGLES, mesh.idx.length, gl.UNSIGNED_SHORT, 0);\n\n    function drawBall(L, col) {\n      const m = mul(translate(L[0], L[1], L[2]), scale(0.09));\n      const bmvp = mul(mul(proj, view), m);\n      gl.useProgram(ballProg);\n      gl.uniformMatrix4fv(gl.getUniformLocation(ballProg, \"u_mvp\"), false, bmvp);\n      gl.uniformMatrix4fv(gl.getUniformLocation(ballProg, \"u_model\"), false, m);\n      gl.uniform3fv(gl.getUniformLocation(ballProg, \"u_colA\"), col);\n      bind(ballProg, \"a_pos\", ballP, 3);\n      bind(ballProg, \"a_nrm\", ballN, 3);\n      gl.drawArrays(gl.TRIANGLES, 0, ball.count);\n    }\n    drawBall(L1, hexToRgb(PARAMS.light));\n    drawBall(L2, hexToRgb(PARAMS.fill));\n\n    if (!reduced) raf = requestAnimationFrame(frame);\n  }\n\n  canvas.addEventListener(\"webglcontextlost\", function (e) {\n    e.preventDefault();\n    cancelAnimationFrame(raf);\n    fail(\"WebGL context was lost. Reload the page.\");\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  window.__teapotTest = { build: buildTeapot, verts: TEAPOT_VERTS, patches: TEAPOT_PATCHES };\n\n  if (reduced) frame(performance.now());\n  else raf = requestAnimationFrame(frame);\n  readMotionHash();\n})();\n</script>\n</body>\n</html>\n"}