17. Newell · 1975 · Retro techniques
Utah teapot
About
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.
Move the pointer to drag the two lights around the teapot.
Reference
Browser APIs
- WebGL 1
- GLSL ES 1.0
- Pointer Events
- requestAnimationFrame
- matchMedia
If WebGL is missing, the demo draws a message on the canvas instead of a blank frame. By default a demo holds a still frame when your system asks for reduced motion; the Motion buttons override that, and a standalone copy animates with #motion=play on its URL.
Source
(function () {
const PARAMS = {
color: "#d8c4a8",
light: "#fff1c8",
fill: "#6ee7b7"
};
function hexToRgb(hex) {
const n = parseInt(String(hex).replace("#", ""), 16);
if (isNaN(n)) return [0.85, 0.77, 0.66];
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
}
// The Utah teapot. Martin Newell modelled it at the University of Utah in
// 1975 as 28 bicubic Bézier patches; the four patches that close the bottom
// were added later and ship with the data set. These are the 306 control
// points and 32 patches of the "teapot" file in the University of Utah
// archive of Newell's tea set (https://www.cs.utah.edu/~natevm/newell_teaset/),
// the same data Frank Crow described in "The Origins of the Teapot", IEEE
// Computer Graphics and Applications 7(1), 1987. The points are in Newell's
// original units, z up, handle toward +x and spout toward -x; they are not
// squashed by the 4/3 factor that some renderers apply. Each patch row lists
// its sixteen control-point indices, zero-based.
const TEAPOT_VERTS = [
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,
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,
-.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,
-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,
-.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,
.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,
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,
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,
-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,
-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,
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,
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,
-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,
-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,
-.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,
-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,
-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,
-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,
-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,
-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,
-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,
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,
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,
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,
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,
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,
.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,
-.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,
-.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,
.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,
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,
-.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,
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,
.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,
.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,
.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,
-.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,
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,
.798,-1.425,0,1.425,-.798,0
];
const TEAPOT_PATCHES = [
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
3,16,17,18,7,19,20,21,11,22,23,24,15,25,26,27,
18,28,29,30,21,31,32,33,24,34,35,36,27,37,38,39,
30,40,41,0,33,42,43,4,36,44,45,8,39,46,47,12,
12,13,14,15,48,49,50,51,52,53,54,55,56,57,58,59,
15,25,26,27,51,60,61,62,55,63,64,65,59,66,67,68,
27,37,38,39,62,69,70,71,65,72,73,74,68,75,76,77,
39,46,47,12,71,78,79,48,74,80,81,52,77,82,83,56,
56,57,58,59,84,85,86,87,88,89,90,91,92,93,94,95,
59,66,67,68,87,96,97,98,91,99,100,101,95,102,103,104,
68,75,76,77,98,105,106,107,101,108,109,110,104,111,112,113,
77,82,83,56,107,114,115,84,110,116,117,88,113,118,119,92,
120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,
123,136,137,120,127,138,139,124,131,140,141,128,135,142,143,132,
132,133,134,135,144,145,146,147,148,149,150,151,68,152,153,154,
135,142,143,132,147,155,156,144,151,157,158,148,154,159,160,68,
161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,
164,177,178,161,168,179,180,165,172,181,182,169,176,183,184,173,
173,174,175,176,185,186,187,188,189,190,191,192,193,194,195,196,
176,183,184,173,188,197,198,185,192,199,200,189,196,201,202,193,
203,203,203,203,206,207,208,209,210,210,210,210,211,212,213,214,
203,203,203,203,209,216,217,218,210,210,210,210,214,219,220,221,
203,203,203,203,218,223,224,225,210,210,210,210,221,226,227,228,
203,203,203,203,225,229,230,206,210,210,210,210,228,231,232,211,
211,212,213,214,233,234,235,236,237,238,239,240,241,242,243,244,
214,219,220,221,236,245,246,247,240,248,249,250,244,251,252,253,
221,226,227,228,247,254,255,256,250,257,258,259,253,260,261,262,
228,231,232,211,256,263,264,233,259,265,266,237,262,267,268,241,
269,269,269,269,278,279,280,281,274,275,276,277,270,271,272,273,
269,269,269,269,281,288,289,290,277,285,286,287,273,282,283,284,
269,269,269,269,290,297,298,299,287,294,295,296,284,291,292,293,
269,269,269,269,299,304,305,278,296,302,303,274,293,300,301,270
];
function buildTeapot(segments) {
const pos = [], nrm = [], idx = [];
function B(t) { const s = 1 - t; return [s * s * s, 3 * s * s * t, 3 * s * t * t, t * t * t]; }
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]; }
function tangents(patch, u, v) {
const bu = B(u), bv = B(v), du = dB(u), dv = dB(v);
const tu = [0, 0, 0], tv = [0, 0, 0];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const k = TEAPOT_PATCHES[patch * 16 + i * 4 + j] * 3;
for (let c = 0; c < 3; c++) {
tu[c] += du[i] * bv[j] * TEAPOT_VERTS[k + c];
tv[c] += bu[i] * dv[j] * TEAPOT_VERTS[k + c];
}
}
}
return [tu, tv];
}
function normal(patch, u, v) {
// Pv x Pu: Newell's parameterisation runs clockwise seen from outside,
// so Pu x Pv points into the pot. Where a patch edge collapses to a
// point (lid tip, bottom centre) one tangent vanishes; nudge inward.
let t = tangents(patch, u, v);
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]];
let len = Math.hypot(n[0], n[1], n[2]);
if (len < 1e-6) {
const e = 1e-3;
t = tangents(patch, u < 0.5 ? u + e : u - e, v < 0.5 ? v + e : v - e);
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]];
len = Math.hypot(n[0], n[1], n[2]) || 1;
}
return [n[0] / len, n[1] / len, n[2] / len];
}
for (let p = 0; p < 32; p++) {
const base = pos.length / 3;
for (let i = 0; i <= segments; i++) {
const u = i / segments, bu = B(u);
for (let j = 0; j <= segments; j++) {
const v = j / segments, bv = B(v);
let x = 0, y = 0, z = 0;
for (let a = 0; a < 4; a++) {
for (let b = 0; b < 4; b++) {
const k = TEAPOT_PATCHES[p * 16 + a * 4 + b] * 3, w = bu[a] * bv[b];
x += w * TEAPOT_VERTS[k]; y += w * TEAPOT_VERTS[k + 1]; z += w * TEAPOT_VERTS[k + 2];
}
}
const n = normal(p, u, v);
// Newell's z is up. Turn the pot upright for a y-up scene.
pos.push(x, z, -y);
nrm.push(n[0], n[2], -n[1]);
}
}
for (let i = 0; i < segments; i++) {
for (let j = 0; j < segments; j++) {
const a = base + i * (segments + 1) + j, b = a + segments + 1;
idx.push(a, a + 1, b, a + 1, b + 1, b);
}
}
}
return { pos: new Float32Array(pos), nrm: new Float32Array(nrm), idx: new Uint16Array(idx) };
}
function sphere(r, segs) {
const pos = [], nrm = [];
function pt(a, b) {
const x = Math.sin(a) * Math.cos(b), y = Math.cos(a), z = Math.sin(a) * Math.sin(b);
return [x * r, y * r, z * r];
}
for (let i = 0; i < segs; i++) {
const a0 = (i / segs) * Math.PI, a1 = ((i + 1) / segs) * Math.PI;
for (let j = 0; j < segs * 2; j++) {
const b0 = (j / (segs * 2)) * Math.PI * 2, b1 = ((j + 1) / (segs * 2)) * Math.PI * 2;
const p00 = pt(a0, b0), p10 = pt(a1, b0), p11 = pt(a1, b1), p01 = pt(a0, b1);
// Counter-clockwise from outside: ring i+1 to ring i runs the other way.
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); }
}
}
return { pos: new Float32Array(pos), nrm: new Float32Array(nrm), count: pos.length / 3 };
}
const canvas = document.getElementById("c");
const fallback = document.getElementById("fallback");
function fail(message) {
fallback.hidden = false;
fallback.textContent = message;
}
const gl = canvas.getContext("webgl", { alpha: false, antialias: true });
if (!gl) {
fail("WebGL is not available in this browser. Try a current Firefox, Chrome, Safari, or Edge.");
return;
}
const VERT = `attribute vec3 a_pos;
attribute vec3 a_nrm;
uniform mat4 u_mvp;
uniform mat4 u_model;
varying vec3 v_nrm;
varying vec3 v_pos;
void main() {
vec4 world = u_model * vec4(a_pos, 1.0);
v_pos = world.xyz;
v_nrm = mat3(u_model) * a_nrm;
gl_Position = u_mvp * vec4(a_pos, 1.0);
}`;
const FRAG = `precision mediump float;
varying vec3 v_nrm;
varying vec3 v_pos;
uniform vec3 u_cam;
uniform vec3 u_color;
uniform vec3 u_lightA;
uniform vec3 u_lightB;
uniform vec3 u_colA;
uniform vec3 u_colB;
void main() {
vec3 n = normalize(v_nrm);
vec3 v = normalize(u_cam - v_pos);
vec3 la = normalize(u_lightA - v_pos);
vec3 lb = normalize(u_lightB - v_pos);
float d1 = max(dot(n, la), 0.0);
float d2 = max(dot(n, lb), 0.0);
float s1 = pow(max(dot(reflect(-la, n), v), 0.0), 48.0);
float s2 = pow(max(dot(reflect(-lb, n), v), 0.0), 24.0);
vec3 col = u_color * (0.12 + 0.72 * d1 + 0.38 * d2);
col += u_colA * s1 * 0.85;
col += u_colB * s2 * 0.45;
gl_FragColor = vec4(col, 1.0);
}`;
const BALL_FRAG = `precision mediump float;
uniform vec3 u_colA;
void main() { gl_FragColor = vec4(u_colA, 1.0); }`;
function compile(type, src) {
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh) || "shader");
return sh;
}
function program(vs, fs) {
const p = gl.createProgram();
gl.attachShader(p, compile(gl.VERTEX_SHADER, vs));
gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fs));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p) || "link");
return p;
}
function mul(a, b) {
const o = new Float32Array(16);
for (let c = 0; c < 4; c++) {
for (let r = 0; r < 4; r++) {
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];
}
}
return o;
}
function perspective(fovy, aspect, near, far) {
const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);
const o = new Float32Array(16);
o[0] = f / aspect; o[5] = f; o[10] = (far + near) * nf; o[11] = -1; o[14] = 2 * far * near * nf;
return o;
}
function translate(x, y, z) {
const o = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
o[12] = x; o[13] = y; o[14] = z;
return o;
}
function scale(s) {
return new Float32Array([s,0,0,0, 0,s,0,0, 0,0,s,0, 0,0,0,1]);
}
function rotateY(a) {
const c = Math.cos(a), s = Math.sin(a);
return new Float32Array([c,0,-s,0, 0,1,0,0, s,0,c,0, 0,0,0,1]);
}
const mesh = buildTeapot(10);
const ball = sphere(1, 8);
const meshProg = program(VERT, FRAG);
const ballProg = program(VERT, BALL_FRAG);
function makeBuf(target, data) {
const b = gl.createBuffer();
gl.bindBuffer(target, b);
gl.bufferData(target, data, gl.STATIC_DRAW);
return b;
}
const posBuf = makeBuf(gl.ARRAY_BUFFER, mesh.pos);
const nrmBuf = makeBuf(gl.ARRAY_BUFFER, mesh.nrm);
const idxBuf = makeBuf(gl.ELEMENT_ARRAY_BUFFER, mesh.idx);
const ballP = makeBuf(gl.ARRAY_BUFFER, ball.pos);
const ballN = makeBuf(gl.ARRAY_BUFFER, ball.nrm);
function bind(prog, name, buf, size) {
const loc = gl.getAttribLocation(prog, name);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0);
}
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clearColor(0.043, 0.047, 0.055, 1);
let az = 0.55, el = 0.7, yaw = 0.95, last = 0;
let raf = 0;
const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
let reduced = motionQuery.matches;
canvas.addEventListener("pointermove", function (e) {
const r = canvas.getBoundingClientRect();
az = ((e.clientX - r.left) / r.width - 0.5) * Math.PI * 1.4;
el = 0.25 + (1 - (e.clientY - r.top) / r.height) * 1.15;
wake();
});
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;
}
gl.viewport(0, 0, canvas.width, canvas.height);
}
function lightPos(a, e, rad) {
return [Math.cos(e) * Math.sin(a) * rad, Math.sin(e) * rad, Math.cos(e) * Math.cos(a) * rad];
}
// Newell's pot is 3.15 units tall and spans x from -3 (spout tip) to 3.525
// (handle). Half size, centred on the handle-to-spout span, base at y = 0.
const POT_SCALE = 0.5;
const POT_X = -0.26;
const CAM = [0, 0.85, 5.4];
function frame(now) {
const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);
last = now;
if (!reduced) yaw += dt * 0.35;
resize();
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const aspect = canvas.width / canvas.height;
const proj = perspective(Math.PI / 4.2, aspect, 0.1, 40);
const view = translate(-CAM[0], -CAM[1], -CAM[2]);
const model = mul(rotateY(yaw), mul(translate(POT_X, 0, 0), scale(POT_SCALE)));
const mvp = mul(mul(proj, view), model);
const L1 = lightPos(az, el, 3.4);
const L2 = lightPos(az + 2.3, el * 0.55 + 0.2, 3.2);
gl.useProgram(meshProg);
gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, "u_mvp"), false, mvp);
gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, "u_model"), false, model);
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_cam"), CAM);
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_color"), hexToRgb(PARAMS.color));
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_lightA"), L1);
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_lightB"), L2);
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_colA"), hexToRgb(PARAMS.light));
gl.uniform3fv(gl.getUniformLocation(meshProg, "u_colB"), hexToRgb(PARAMS.fill));
bind(meshProg, "a_pos", posBuf, 3);
bind(meshProg, "a_nrm", nrmBuf, 3);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idxBuf);
gl.drawElements(gl.TRIANGLES, mesh.idx.length, gl.UNSIGNED_SHORT, 0);
function drawBall(L, col) {
const m = mul(translate(L[0], L[1], L[2]), scale(0.09));
const bmvp = mul(mul(proj, view), m);
gl.useProgram(ballProg);
gl.uniformMatrix4fv(gl.getUniformLocation(ballProg, "u_mvp"), false, bmvp);
gl.uniformMatrix4fv(gl.getUniformLocation(ballProg, "u_model"), false, m);
gl.uniform3fv(gl.getUniformLocation(ballProg, "u_colA"), col);
bind(ballProg, "a_pos", ballP, 3);
bind(ballProg, "a_nrm", ballN, 3);
gl.drawArrays(gl.TRIANGLES, 0, ball.count);
}
drawBall(L1, hexToRgb(PARAMS.light));
drawBall(L2, hexToRgb(PARAMS.fill));
if (!reduced) raf = requestAnimationFrame(frame);
}
canvas.addEventListener("webglcontextlost", function (e) {
e.preventDefault();
cancelAnimationFrame(raf);
fail("WebGL context was lost. Reload the page.");
});
// Motion. The system's reduced-motion preference decides by default. A
// standalone copy animates anyway with "#motion=play" in its URL (or holds a
// frame with "#motion=still"); an embedding page can post {motion: "play"}.
let motionMode = "auto";
function applyMotion() {
const next = motionMode === "still" || (motionMode === "auto" && motionQuery.matches);
if (next === reduced) return;
reduced = next;
last = 0;
if (reduced) { cancelAnimationFrame(raf); raf = 0; frame(performance.now()); }
else if (!raf) raf = requestAnimationFrame(frame);
}
function setMotion(mode) {
if (mode === "play" || mode === "still" || mode === "auto") { motionMode = mode; applyMotion(); }
}
function readMotionHash() {
const m = /motion=(play|still|auto)/.exec(location.hash);
let mode = m ? m[1] : "auto";
// Embedded in the gallery, the page sets window.__demoMotion before any frame loads.
try { if (!m && window.parent !== window && typeof window.parent.__demoMotion === "string") mode = window.parent.__demoMotion; } catch (e) { /* cross-origin parent */ }
setMotion(mode);
}
function wake() { if (reduced) frame(performance.now()); }
if (motionQuery.addEventListener) motionQuery.addEventListener("change", applyMotion);
else if (motionQuery.addListener) motionQuery.addListener(applyMotion);
window.addEventListener("hashchange", readMotionHash);
window.addEventListener("message", function (e) { if (e.data && typeof e.data.motion === "string") setMotion(e.data.motion); });
window.addEventListener("resize", wake);
window.__teapotTest = { build: buildTeapot, verts: TEAPOT_VERTS, patches: TEAPOT_PATCHES };
if (reduced) frame(performance.now());
else raf = requestAnimationFrame(frame);
readMotionHash();
})();