1{ 2 const cv = document.getElementById("tetra"); 3 const AX = [ 4 { name: "Neural graphics", c: [226, 59, 64] }, 5 { name: "Real-time rendering", c: [40, 160, 70] }, 6 { name: "Programming systems", c: [40, 100, 225] }, 7 { name: "Appearance modeling", c: [235, 180, 20] }, 8 ]; 9 const PAPERS = [ 10 { name: "RCGP", title: "RCGP: Resource Contracts for Graphics Programming", href: "rcgp/", w: [0, 0.25, 0.75, 0] }, 11 { name: "Glow", title: "Modeling and Rendering Glow Discharge", href: "glow/", w: [0, 0.3, 0, 0.7] }, 12 { name: "GFS", title: "Geometry Field Splatting with Gaussian Surfels", href: "https://arxiv.org/abs/2411.17067", w: [0.6, 0.2, 0, 0.2] }, 13 { name: "NGF", title: "Neural Geometry Fields for Meshes", href: "ngf/", w: [0.75, 0.25, 0, 0] }, 14 { name: "ReSTIR DR", title: "Parameter-space ReSTIR for Differentiable and Inverse Rendering", href: "https://weschang.com/publications/restir-dr/", w: [0, 0.5, 0, 0.5] }, 15 ]; 16 const r = Math.sqrt(8) / 3; 17 const V = [[0, 1, 0], ...[90, 210, 330].map((d) => [r * Math.cos((d * Math.PI) / 180), -1 / 3, r * Math.sin((d * Math.PI) / 180)])]; 18 const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; 19 const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; 20 const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; 21 const F = [[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]].map((f) => { 22 const n = cross(sub(V[f[1]], V[f[0]]), sub(V[f[2]], V[f[0]])); 23 const m = [0, 1, 2].map((k) => (V[f[0]][k] + V[f[1]][k] + V[f[2]][k]) / 3); 24 return dot(n, m) < 0 ? { v: [f[0], f[2], f[1]], n: n.map((x) =>
24 -x) } : { v: f, n }; 25 }); 26 const E = [[0, 1], [0, 2], [0, 3], [1, 2], [2, 3], [3, 1]]; 27 const pos = PAPERS.map((p) => { 28 const t = p.w.reduce((a, b) => a + b, 0); 29 const w = p.w.map((x) => x / t); 30 return { 31 ...p, 32 p: [0, 1, 2].map((k) => w.reduce((s, x, i) => s + x * V[i][k], 0)), 33 c: [0, 1, 2].map((k) => Math.round(w.reduce((s, x, i) => s + x * AX[i].c[k], 0))), 34 }; 35 }); 36 const still = matchMedia("(prefers-reduced-motion: reduce)"); 37 const off = document.createElement("canvas"), octx = off.getContext("2d"); 38 const ctx = cv.getContext("2d"); 39 let th = 0.5, ph = 0.35, W = 0, H = 0, S = 0, dpr = 1, live = false, drag = null, hover = -1, hoverT = 0, last = 0, shown = []; 40 41 const rot = (p) => { 42 const c = Math.cos(th), s = Math.sin(th), cp = Math.cos(ph), sp = Math.sin(ph); 43 const x = p[0] * c + p[2] * s, z = -p[0] * s + p[2] * c; 44 return [x, p[1] * cp - z * sp, p[1] * sp + z * cp]; 45 }; 46 const scr = (q) => [W / 2 + q[0] * S, H / 2 - q[1] * S, q[2]]; 47 48 function size() { 49 dpr = Math.min(2, devicePixelRatio || 1); 50 W = cv.clientWidth; 51 const B = Math.min(W, 432); 52 H = Math.round(B * 0.5 + 72); 53 cv.style.height = H + "px"; 54 cv.width = W * dpr; 55 cv.height = H * dpr; 56 off.width = W; 57 off.height = H; 58 S = B * 0.25; 59 } 60 61 function faces(P, front) { 62 const img = octx.createImageData(W, H), d = img.data; 63 const order = F.map((f, i) => ({ f, i, z: (P[f.v[0]][2] + P[f.v[1]][2] + P[f.v[2]][2]) / 3 })).sort((a, b) => a.z - b.z); 64 for (const { f, i } of order) { 65 const [a, b, c] = f.v.map((k) => P[k]); 66 const A = front[i] ? 0.24 : 0.12; 67 const den = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1]); 68 const x0 = Math.max(0, Math.floor(Math.min(a[0], b[0], c[0]))), x1 = Math.min(W - 1, Math.ceil(Math.max(a[0], b[0], c[0]))); 69 const y0 = Math.max(0, Math.floor(Math.min(a[1], b[1], c[1]))), y1 = Math.min(H - 1, Math.ceil(Math.max(a[1], b[1], c[1]))); 70 const ca = AX[f.v[0]].c, cb = AX[f.v[1]].c, cc = AX[f.v[2]].c; 71 for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { 72 const px = x + 0.5, py = y + 0.5; 73 const l0 = ((b[1] - c[1]) * (px - c[0]) + (c[0] - b[0]) * (py - c[1])) / den; 74 const l1 = ((c[1] - a[1]) * (px - c[0]) + (a[0] - c[0]) * (py - c[1])) / den; 75 const l2 = 1 - l0 - l1; 76 if (l0 < 0 || l1 < 0 || l2 < 0) continue; 77 const o = (y * W + x) * 4, da = d[o + 3] / 255, na = A + da * (1 - A); 78 for (let k = 0; k < 3; k++) d[o + k] = ((l0 * ca[k] + l1 * cb[k] + l2 * cc[k]) * A + d[o + k] * da * (1 - A)) / na; 79 d[o + 3] = na * 255; 80 } 81 } 82 octx.putImageData(img, 0, 0); 83 } 84 85 function draw() { 86 const P = V.map((v) => scr(rot(v))); 87 const front = F.map((f) => rot(f.n)[2] > 0); 88 faces(P, front); 89 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 90 ctx.clearRect(0, 0, W, H); 91 ctx.drawImage(off, 0, 0, W, H); 92 for (const [i, j] of E) { 93 const vis = F.some((f, k) => front[k] && f.v.includes(i) && f.v.includes(j)); 94 ctx.beginPath(); 95 ctx.moveTo(P[i][0], P[i][1]); 96 ctx.lineTo(P[j][0], P[j][1]); 97 ctx.setLineDash(vis ? [] : [3, 4]); 98 ctx.strokeStyle = vis ? "rgba(28,27,44,0.55)" : "rgba(28,27,44,0.3)"; 99 ctx.lineWidth = 1.2; 100 ctx.stroke(); 101 } 102 ctx.setLineDash([]); 103 ctx.font = "700 13px 'Fira Sans Condensed', sans-serif"; 104 ctx.textBaseline = "middle"; 105 ctx.globalAlpha = hover >= 0 ? 0.3 : 1; 106 AX.forEach((a, i) => { 107 const dx = P[i][0] - W / 2, dy = P[i][1] - H / 2, l = Math.hypot(dx, dy) || 1; 108 const x = P[i][0] + (dx / l) * 16, y = P[i][1] + (dy / l) * 14; 109 ctx.textAlign = Math.abs(dx / l) < 0.35 ? "center" : dx > 0 ? "left" : "right"; 110 ctx.fillStyle = `rgb(${a.c})`; 111 ctx.beginPath(); 112 ctx.arc(P[i][0], P[i][1], 3.5, 0, 7); 113 ctx.fill(); 114 ctx.lineWidth = 4; 115 ctx.strokeStyle = "rgba(246,239,224,0.9)"; 116 ctx.lineJoin = "round"; 117 const lines = a.name.split(" "), y0 = y - ((lines.length - 1) * 14) / 2 + (dy > l * 0.35 ? 7 : dy < -l * 0.35 ? -7 : 0);
118 lines.forEach((ln, k) => ctx.strokeText(ln, x, y0 + k * 14)); 119 ctx.fillStyle = "#1c1b2c"; 120 lines.forEach((ln, k) => ctx.fillText(ln, x, y0 + k * 14)); 121 }); 122 ctx.globalAlpha = 1; 123 shown = pos.map((p, i) => ({ i, q: scr(rot(p.p)) })).sort((a, b) => a.q[2] - b.q[2]); 124 for (const { i, q } of shown) { 125 const p = pos[i], R = hover === i ? 7.5 : 6; 126 ctx.globalAlpha = 0.55 + 0.45 * Math.min(1, Math.max(0, (q[2] + 1) / 2)); 127 ctx.beginPath(); 128 ctx.arc(q[0], q[1], R, 0, 7); 129 ctx.fillStyle = `rgb(${p.c})`; 130 ctx.fill(); 131 ctx.lineWidth = 2; 132 ctx.strokeStyle = "#f6efe0"; 133 ctx.stroke(); 134 } 135 ctx.globalAlpha = 1; 136 if (hover >= 0) { 137 const p = pos[hover], q = shown.find((x) => x.i === hover).q; 138 let fs = 14; 139 const font = () => (ctx.font = `700 ${fs}px 'Fira Sans Condensed', sans-serif`); 140 font(); 141 let tw = ctx.measureText(p.title).width; 142 const d = 16, R0 = 8, gap = R0 * 0.7 + d + 6; 143 const roomR = W - q[0] - gap, roomL = q[0] - gap; 144 let right = tw <= roomR && (tw > roomL || roomR >= roomL) ? true : tw <= roomL ? false : roomR >= roomL; 145 const room = right ? roomR : roomL; 146 let lines = [p.title]; 147 if (tw > room) { 148 fs = Math.max(12, Math.floor((fs * room) / tw)); 149 font(); 150 tw = ctx.measureText(p.title).width; 151 if (tw > room) { 152 const words = p.title.split(" "); 153 let best = null; 154 for (let i = 1; i < words.length; i++) { 155 const ls = [words.slice(0, i).join(" "), words.slice(i).join(" ")], w = Math.max(...ls.map((l) => ctx.measureText(l).width)); 156 if (!best || w < best.w) best = { ls, w }; 157 } 158 lines = best.ls; 159 tw = best.w; 160 } 161 } 162 const k = still.matches ? 1 : Math.max(0, Math.min(1, (performance.now() - hoverT) / 420)), e = 1 - (1 - k) ** 3; 163 const lh = fs * 1.2, th2 = lh * lines.length; 164 const up = q[1] - d - th2 - 6 > 0, sx = right ? 1 : -1, sy = up ? -1 : 1; 165 const x0 = q[0] + sx * R0 * 0.7, y0 = q[1] + sy * R0 * 0.7, x1 = x0 + sx * d, y1 = y0 + sy * d; 166 const L = tw + 4, a1 = Math.min(1, e / 0.3), a2 = Math.max(0, (e - 0.3) / 0.7); 167 ctx.beginPath(); 168 ctx.moveTo(x0, y0); 169 ctx.lineTo(x0 + sx * d * a1, y0 + sy * d * a1); 170 if (a2 > 0) ctx.lineTo(x1 + sx * L * a2, y1); 171 ctx.lineWidth = 3.5; 172 ctx.lineJoin = "round"; 173 ctx.strokeStyle = "rgba(246,239,224,0.85)"; 174 ctx.stroke(); 175 ctx.lineWidth = 1.2; 176 ctx.strokeStyle = "#1c1b2c"; 177 ctx.stroke(); 178 if (a2 > 0) { 179 ctx.save(); 180 ctx.beginPath(); 181 ctx.rect(right ? x1 : x1 - L * a2, y1 - th2 - 6, L * a2, th2 + 6); 182 ctx.clip(); 183 ctx.textAlign = right ? "left" : "right"; 184 ctx.textBaseline = "alphabetic"; 185 const tx = x1 + sx * 2, ty = y1 - 4; 186 ctx.lineWidth = 4; 187 ctx.strokeStyle = "rgba(246,239,224,0.85)"; 188 lines.forEach((ln, i) => ctx.strokeText(ln, tx, ty - (lines.length - 1 - i) * lh)); 189 ctx.fillStyle = "#1c1b2c"; 190 lines.forEach((ln, i) => ctx.fillText(ln, tx, ty - (lines.length - 1 - i) * lh)); 191 ctx.restore(); 192 } 193 } 194 } 195 196 function tick(t) { 197 if (!live) return; 198 const dt = last ? Math.min(0.05, (t - last) / 1000) : 0; 199 last = t; 200 if (!drag && !still.matches && hover < 0) th += dt * 0.25; 201 draw(); 202 requestAnimationFrame(tick); 203 } 204 205 const hit = (e) => { 206 const b = cv.getBoundingClientRect(), x = e.clientX - b.left, y = e.clientY - b.top; 207 for (let k = shown.length - 1; k >= 0; k--) if (Math.hypot(shown[k].q[0] - x, shown[k].q[1] - y) < 11) return shown[k].i; 208 return -1; 209 }; 210 cv.addEventListener("pointerdown", (e) => { 211 drag = { x: e.clientX, y: e.clientY, th, ph, moved: false }; 212 cv.setPointerCapture(e.pointerId); 213 }); 214 cv.addEventListener("pointermove", (e) => { 215 if (drag) { 216 const dx = e.clientX - drag.x, dy = e.clientY - drag.y; 217 if (Math.hypot(dx, dy) > 3) drag.moved = true; 218 th = drag.th + dx * 0.012; 219 ph = Math.max(-1.2, Math.min(1.2, drag.ph + dy * 0.012)); 220 if (still.matches) draw(); 221 return; 222 } 223 const h = hit(e); 224 if (h !== hover) { 225 hover = h; 226 hoverT = performance.now(); 227 cv.style.cursor = h < 0 ? "grab" : "pointer"; 228 if (still.matches) draw(); 229 } 230 }); 231 cv.addEventListener("pointerup", (e) => { 232 const moved = drag && drag.moved; 233 drag = null; 234 if (!moved) { 235 const h = hit(e); 236 if (h >= 0) location.href = pos[h].href; 237 } 238 }); 239 cv.addEventListener("pointerleave", () => { 240 hover = -1; 241 }); 242 new IntersectionObserver(([e]) => { 243 const was = live; 244 live = e.isIntersecting && cv.clientWidth > 0; 245 if (live && !was) { 246 last = 0; 247 requestAnimationFrame(tick); 248 } 249 }).observe(cv); 250 addEventListener("resize", () => { 251 size(); 252 draw(); 253 }); 254 size(); 255 document.fonts.ready.then(() => { 256 size(); 257 draw(); 258 }); 259}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.