1/* Jerick Shi â site interactions (v2) 2 WebGL nebula background (2D starfield fallback), custom cursor, decode/scramble 3 text, magnetic buttons, spotlight cards, page transitions, scroll spy, hero 4 parallax, research map (force-directed graph), command palette, lightbox, 5 carousel, tilt cards, filters, counters, easter eggs. No dependencies. */ 6 7(function () { 8 "use strict"; 9 10 var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 11 var finePointer = window.matchMedia("(pointer: fine)").matches; 12 var mouse = { x: -9999, y: -9999, active: false }; 13 14 document.documentElement.classList.add("js"); 15 16 window.addEventListener("pointermove", function (e) { 17 mouse.x = e.clientX; 18 mouse.y = e.clientY; 19 mouse.active = true; 20 }, { passive: true }); 21 document.addEventListener("pointerleave", function () { mouse.active = false; }); 22 23 function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } 24 function lerp(a, b, t) { return a + (b - a) * t; } 25 26 /* ====================================================================== 27 BACKGROUND â WebGL nebula + procedural stars, mouse warp, scroll drift 28 ====================================================================== */ 29 var WARP = { level: 0, target: 0 }; // easter egg hook 30 31 var VERT = "attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}"; 32 33 var FRAG = [ 34 "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif", 35 "uniform vec2 u_res;uniform float u_time;uniform vec2 u_mouse;uniform float u_scroll;uniform float u_warp;", 36 "float hash(vec2 p){p=fract(p*vec2(123.34,456.21));p+=dot(p,p+45.32);return fract(p.x*p.y);}", 37 "float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.-2.*f);", 38 " float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));", 39 " return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}", 40 "float fbm(vec2 p){float v=0.,a=.5;mat2 m=mat2(1.6,1.2,-1.2,1.6);", 41 " for(int i=0;i<5;i++){v+=a*noise(p);p=m*p;a*=.5;}return v;}", 42 "float stars(vec2 uv,float scale,float t,float bright){", 43 " vec2 g=uv*scale;vec2 id=floor(g);vec2 f=fract(g)-.5;", 44 " float h=hash(id);if(h<.93)return 0.;", 45 " vec2 off=vec2(hash(id+7.1),hash(id+3.7))-.5;f-=off*.7;", 46 " float d=length(f);float tw=.6+.4*sin(t*(1.+h*2.)+h*40.);", 47 " float s=smoothstep(.06,0.,d)*tw*bright;", 48 " s+=smoothstep(.25,0.,d)*.06*tw;return s;}", 49 "void main(){", 50 " vec2 uv=(gl_FragCoord.xy-.5*u_res)/u_res.y;", 51 " vec2 m=(u_mouse-.5*u_res)/u_res.y;", 52 " float t=u_time*.045;", 53 " vec2 d=uv-m;float dist=length(d);", 54 " float infl=exp(-dist*dist*9.);", 55 " vec2 wuv=uv+normalize(d+1e-4)*infl*.07;", 56 " vec2 p=wuv*1.5+vec2(t*.35,-t*.22)+vec2(0.,u_scroll*.00035);", 57 " float q=fbm(p+t*.3);", 58 " float n=fbm(p+q*.9+vec2(1.7,9.2));", 59 " vec3 c1=vec3(.36,.55,1.);vec3 c2=vec3(.62,.42,1.);vec3 c3=vec3(.27,.88,1.);", 60 " vec3 col=mix(c1,c2,smoothstep(.25,.85,n));", 61 " col=mix(col,c3,smoothstep(.6,.95,q)*.6);", 62 " float a=smoothstep(.38,.92,n)*.30;", 63 " vec3 bg=vec3(.016,.024,.05);", 64 " vec3 o=bg+col*a;", 65 " o+=c3*infl*.12;", 66 " float st=0.;", 67 " vec2 suv=uv+vec2(t*.15,u_scroll*.00025);", 68 " st+=stars(suv,18.,u_time,1.);", 69 " st+=stars(suv*1.7+3.1,26.,u_time*1.3,.6);", 70 " st+=stars(uv*.6+vec2(t*.05,u_scroll*.0001),13.,u_time*.7,1.1);", 71 " o+=vec3(.7,.78,1.)*st;", 72 " if(u_warp>0.){", 73 " float ang=atan(uv.y,uv.x);float r=length(uv);", 74 " float streak=pow(noise(vec2(ang*40.,r*3.-u_time*8.)),8.)*u_warp*smoothstep(.05,.6,r)*3.;", 75 " o+=vec3(.5,.7,1.)*streak;o+=c3*u_warp*.06;}", 76 " o+=(hash(gl_FragCoord.xy+u_time)-.5)*.012;", 77 " gl_FragColor=vec4(o,1.);}" 78 ].join("\n"); 79
80 function initBackgroundGL(canvas) { 81 var gl = canvas.getContext("webgl", { antialias: false, alpha: false, powerPreference: "low-power" }); 82 if (!gl) return false; 83 84 function shader(type, src) { 85 var s = gl.createShader(type); 86 gl.shaderSource(s, src); 87 gl.compileShader(s); 88 if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.warn(gl.getShaderInfoLog(s)); return null; } 89 return s; 90 } 91 var vs = shader(gl.VERTEX_SHADER, VERT), fs = shader(gl.FRAGMENT_SHADER, FRAG); 92 if (!vs || !fs) return false; 93 var prog = gl.createProgram(); 94 gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); 95 if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return false; 96 gl.useProgram(prog); 97 98 var buf = gl.createBuffer(); 99 gl.bindBuffer(gl.ARRAY_BUFFER, buf); 100 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); 101 var loc = gl.getAttribLocation(prog, "p"); 102 gl.enableVertexAttribArray(loc); 103 gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); 104 105 var U = { 106 res: gl.getUniformLocation(prog, "u_res"), 107 time: gl.getUniformLocation(prog, "u_time"), 108 mouse: gl.getUniformLocation(prog, "u_mouse"), 109 scroll: gl.getUniformLocation(prog, "u_scroll"), 110 warp: gl.getUniformLocation(prog, "u_warp") 111 }; 112 113 var scale = 0.6; // render at reduced resolution, CSS upscales â cheap on fill rate 114 var w, h, smx = -9999, smy = -9999; 115 116 function resize() { 117 var dpr = Math.min(window.devicePixelRatio || 1, 1.5); 118 w = Math.floor(window.innerWidth * dpr * scale); 119 h = Math.floor(window.innerHeight * dpr * scale); 120 canvas.width = w; canvas.height = h; 121 gl.viewport(0, 0, w, h); 122 } 123 124 var running = true, start = performance.now(); 125 function frame(now) { 126 if (!running) return; 127 var t = (now - start) / 1000; 128 // smooth mouse 129 var tx = mouse.active ? mouse.x / window.innerWidth * w : -9999; 130 var ty = mouse.active ? (1 - mouse.y / window.innerHeight) * h : -9999; 131 smx = tx === -9999 ? -9999 : (smx === -9999 ? tx : lerp(smx, tx, 0.08)); 132 smy = ty === -9999 ? -9999 : (smy === -9999 ? ty : lerp(smy, ty, 0.08)); 133 WARP.level = lerp(WARP.level, WARP.target, 0.05); 134 gl.uniform2f(U.res, w, h); 135 gl.uniform1f(U.time, t * (1 + WARP.level * 6)); 136 gl.uniform2f(U.mouse, smx, smy); 137 gl.uniform1f(U.scroll, window.scrollY); 138 gl.uniform1f(U.warp, WARP.level); 139 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 140 if (!reducedMotion) requestAnimationFrame(frame); 141 } 142 143 window.addEventListener("resize", resize); 144 document.addEventListener("visibilitychange", function () { 145 running = !document.hidden; 146 if (running && !reducedMotion) requestAnimationFrame(frame); 147 }); 148 resize(); 149 requestAnimationFrame(frame); 150 return true; 151 } 152
153 function initBackground2D(canvas) { 154 var ctx = canvas.getContext("2d"); 155 var stars = [], w, h, dpr; 156 function resize() { 157 dpr = Math.min(window.devicePixelRatio || 1, 2); 158 w = window.innerWidth; h = window.innerHeight; 159 canvas.width = w * dpr; canvas.height = h * dpr; 160 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 161 var count = Math.min(160, Math.floor((w * h) / 9000)); 162 stars = []; 163 for (var i = 0; i < count; i++) { 164 stars.push({ x: Math.random() * w, y: Math.random() * h, r: Math.random() * 1.4 + 0.3, 165 vx: (Math.random() - 0.5) * 0.12, vy: (Math.random() - 0.5) * 0.12, tw: Math.random() * Math.PI * 2 }); 166 } 167 } 168 function frame(t) { 169 ctx.clearRect(0, 0, w, h); 170 for (var i = 0; i < stars.length; i++) { 171 var s = stars[i]; 172 s.x += s.vx; s.y += s.vy; 173 if (s.x < -10) s.x = w + 10; if (s.x > w + 10) s.x = -10; 174 if (s.y < -10) s.y = h + 10; if (s.y > h + 10) s.y = -10; 175 var twinkle = 0.45 + 0.4 * Math.sin(t / 900 + s.tw); 176 ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); 177 ctx.fillStyle = "rgba(155,175,255," + twinkle * 0.7 + ")"; ctx.fill(); 178 var dx = s.x - mouse.x, dy = s.y - mouse.y, d = Math.sqrt(dx * dx + dy * dy); 179 if (d < 110) { 180 ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(mouse.x, mouse.y); 181 ctx.strokeStyle = "rgba(91,140,255," + (1 - d / 110) * 0.25 + ")"; ctx.lineWidth = 0.6; ctx.stroke(); 182 } 183 } 184 if (!reducedMotion) requestAnimationFrame(frame); 185 } 186 window.addEventListener("resize", resize); 187 resize(); 188 requestAnimationFrame(frame); 189 } 190
191 function initBackground() { 192 var canvas = document.getElementById("starfield"); 193 if (!canvas) return; 194 var ok = false; 195 try { ok = initBackgroundGL(canvas); } catch (e) { ok = false; } 196 if (!ok) initBackground2D(canvas); 197 if (!reducedMotion) { 198 var grain = document.createElement("div"); 199 grain.id = "grain"; 200 document.body.appendChild(grain); 201 } 202 } 203 204 /* ====================================================================== 205 CURSOR â dot + lagging ring (fine pointers only) 206 ====================================================================== */ 207 function initCursor() { 208 if (!finePointer || reducedMotion) return; 209 var dot = document.createElement("div"), ring = document.createElement("div"); 210 dot.id = "cur-dot"; ring.id = "cur-ring"; 211 document.body.appendChild(dot); document.body.appendChild(ring); 212 document.body.classList.add("has-cursor", "cur-off"); 213 var rx = 0, ry = 0, shown = false; 214 215 function tick() { 216 rx = lerp(rx, mouse.x, 0.18); ry = lerp(ry, mouse.y, 0.18); 217 dot.style.transform = "translate(" + mouse.x + "px," + mouse.y + "px) translate(-50%,-50%)"; 218 ring.style.transform = "translate(" + rx + "px," + ry + "px) translate(-50%,-50%)"; 219 requestAnimationFrame(tick); 220 } 221 requestAnimationFrame(tick); 222 223 window.addEventListener("pointermove", function (e) { 224 if (!shown) { shown = true; rx = e.clientX; ry = e.clientY; document.body.classList.remove("cur-off"); } 225 var el = e.target; 226 var interactive = el.closest && el.closest("a, button, .card, .gallery-img, .dot, .rmap canvas, input, label"); 227 document.body.classList.toggle("cur-hover", !!interactive); 228 var isInput = el.closest && el.closest("input, textarea"); 229 document.body.classList.toggle("cur-off", !!isInput); 230 }, { passive: true }); 231 document.addEventListener("pointerdown", function () { document.body.classList.add("cur-down"); }); 232 document.addEventListener("pointerup", function () { document.body.classList.remove("cur-down"); }); 233 document.addEventListener("mouseleave", function () { document.body.classList.add("cur-off"); }); 234 document.addEventListener("mouseenter", function () { document.body.classList.remove("cur-off"); }); 235 } 236 237 /* ====================================================================== 238 SCRAMBLE / DECODE TEXT 239 ====================================================================== */ 240 var GLYPHS = "!<>-_\\/[]{}â=+*^?#01"; 241 242 function scramble(el, opts) { 243 opts = opts || {}; 244 if (reducedMotion) return; 245 var nodes = []; 246 (function walk(n) { 247 for (var i = 0; i < n.childNodes.length; i++) { 248 var c = n.childNodes[i]; 249 if (c.nodeType === 3 && c.textContent.trim()) nodes.push(c); 250 else if (c.nodeType === 1) walk(c); 251 } 252 })(el); 253 var targets = nodes.map(function (n) { 254 var span = document.createElement("span"); 255 n.parentNode.replaceChild(span, n); 256 return { span: span, text: n.textContent }; 257 }); 258 var dur = opts.duration || 900, start = null; 259 function frame(ts) { 260 if (!start) start = ts; 261 var p = Math.min((ts - start) / dur, 1); 262 targets.forEach(function (t) { 263 var out = "", len = t.text.length; 264 for (var i = 0; i < len; i++) { 265 var ch = t.text[i]; 266 var reveal = (i / len) * 0.75 + 0.1; // left-to-right reveal 267 if (ch === " " || p >= reveal) out += ch; 268 else out += '<span class="sc-glyph">' + GLYPHS[Math.floor(Math.random() * GLYPHS.length)] + "</span>"; 269 } 270 t.span.innerHTML = out; 271 }); 272 if (p < 1) requestAnimationFrame(frame); 273 else targets.forEach(function (t) { t.span.textContent = t.text; }); 274 } 275 requestAnimationFrame(frame); 276 } 277 278 function initScramble() { 279 var els = document.querySelectorAll("[data-scramble]"); 280 if (!els.length || reducedMotion) return; 281 var io = new IntersectionObserver(function (entries) { 282 entries.forEach(function (en) { 283 if (en.isIntersecting) { 284 scramble(en.target, { duration: +en.target.getAttribute("data-scramble") || 900 }); 285 io.unobserve(en.target); 286 } 287 }); 288 }, { threshold: 0.3 });
289 els.forEach(function (el) { io.observe(el); }); 290 } 291 292 /* ====================================================================== 293 MAGNETIC BUTTONS + SPOTLIGHT CARDS + HERO PARALLAX 294 ====================================================================== */ 295 function initMagnetic() { 296 if (!finePointer || reducedMotion) return; 297 document.querySelectorAll(".btn, .cmdk-btn, .filter-btn").forEach(function (btn) { 298 btn.addEventListener("pointermove", function (e) { 299 var r = btn.getBoundingClientRect(); 300 var dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2); 301 btn.style.transform = "translate(" + dx * 0.22 + "px," + dy * 0.28 + "px)"; 302 btn.style.transition = "transform 0.1s ease-out, box-shadow 0.18s, border-color 0.18s, background 0.18s"; 303 }); 304 btn.addEventListener("pointerleave", function () { 305 btn.style.transform = ""; 306 btn.style.transition = "transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.18s, border-color 0.18s, background 0.18s"; 307 }); 308 }); 309 } 310 311 function initSpotlight() { 312 if (!finePointer) return; 313 document.addEventListener("pointermove", function (e) { 314 var card = e.target.closest && e.target.closest(".card"); 315 if (!card) return; 316 var r = card.getBoundingClientRect(); 317 card.style.setProperty("--mx", (e.clientX - r.left) + "px"); 318 card.style.setProperty("--my", (e.clientY - r.top) + "px"); 319 }, { passive: true }); 320 } 321 322 function initHeroParallax() { 323 if (!finePointer || reducedMotion) return; 324 var hero = document.querySelector(".hero"); 325 if (!hero) return; 326 var layers = hero.querySelectorAll("[data-depth]"); 327 if (!layers.length) return; 328 hero.addEventListener("pointermove", function (e) { 329 var r = hero.getBoundingClientRect(); 330 var px = (e.clientX - r.left) / r.width - 0.5, py = (e.clientY - r.top) / r.height - 0.5; 331 layers.forEach(function (l) { 332 var d = parseFloat(l.getAttribute("data-depth")); 333 l.style.transform = "translate(" + (-px * d * 30) + "px," + (-py * d * 30) + "px)"; 334 }); 335 }); 336 hero.addEventListener("pointerleave", function () { 337 layers.forEach(function (l) { l.style.transform = ""; }); 338 }); 339 } 340 341 /* ====================================================================== 342 PAGE TRANSITIONS 343 ====================================================================== */ 344 function initTransitions() { 345 var curtain = document.createElement("div"); 346 curtain.id = "curtain"; 347 document.body.appendChild(curtain); 348 if (reducedMotion) return; 349 350 document.addEventListener("click", function (e) { 351 var a = e.target.closest && e.target.closest("a[href]"); 352 if (!a) return; 353 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; 354 if (a.target === "_blank" || a.hasAttribute("download")) return; 355 var href = a.getAttribute("href"); 356 if (!href || href[0] === "#" || /^(mailto|tel|javascript):/.test(href)) return; 357 var url; 358 try { url = new URL(a.href, location.href); } catch (err) { return; } 359 if (url.origin !== location.origin) return; 360 if (url.pathname === location.pathname && url.hash) return; 361 if (/\.(pdf|jpg|jpeg|png|zip)$/i.test(url.pathname)) return; 362 e.preventDefault(); 363 document.body.classList.add("leaving"); 364 setTimeout(function () { location.href = url.href; }, 300); 365 }); 366 367 window.addEventListener("pageshow", function (e) { 368 if (e.persisted) document.body.classList.remove("leaving"); 369 }); 370 } 371 372 /* ====================================================================== 373 NAV â scrolled state, hide on scroll down, active link, burger 374 ====================================================================== */ 375 function initNav() { 376 var nav = document.querySelector(".nav"); 377 var burger = document.querySelector(".nav-burger"); 378 var links = document.querySelector(".nav-links"); 379 if (burger && links) burger.addEventListener("click", function () { links.classList.toggle("open"); }); 380 381 var path = location.pathname.replace(/index\.html$/, ""); 382 document.querySelectorAll(".nav-links a").forEach(function (a) { 383 var href = a.getAttribute("href"); 384 if (href === "/" ? path === "/" : path.indexOf(href.replace(/\/$/, "")) === 0 && href !== "/") a.classList.add("active"); 385 }); 386 387 if (!nav) return; 388 var last = window.scrollY, ticking = false;
389 function onScroll() { 390 var y = window.scrollY; 391 nav.classList.toggle("scrolled", y > 24); 392 if (y > 320 && y > last + 4) nav.classList.add("hidden"); 393 else if (y < last - 4 || y < 320) nav.classList.remove("hidden"); 394 last = y; ticking = false; 395 } 396 window.addEventListener("scroll", function () { 397 if (!ticking) { requestAnimationFrame(onScroll); ticking = true; } 398 }, { passive: true }); 399 onScroll(); 400 } 401 402 /* ====================================================================== 403 SCROLL SPY (homepage sections) 404 ====================================================================== */ 405 function initSpy() { 406 var sections = document.querySelectorAll("section.block[id]"); 407 if (sections.length < 3) return; 408 var spy = document.createElement("nav"); 409 spy.id = "spy"; 410 spy.setAttribute("aria-label", "Sections"); 411 var items = []; 412 sections.forEach(function (s, i) { 413 var label = (s.querySelector(".eyebrow") || {}).textContent || s.id; 414 label = label.replace(/^\s*\d+\s*/, "").trim(); 415 var a = document.createElement("a"); 416 a.href = "#" + s.id; 417 a.innerHTML = '<span class="lbl">' + label + "</span><i></i>"; 418 spy.appendChild(a); 419 items.push(a); 420 }); 421 document.body.appendChild(spy); 422 var io = new IntersectionObserver(function (entries) { 423 entries.forEach(function (en) { 424 if (en.isIntersecting) { 425 items.forEach(function (a, i) { a.classList.toggle("active", sections[i] === en.target); }); 426 } 427 }); 428 }, { rootMargin: "-40% 0px -50% 0px" }); 429 sections.forEach(function (s) { io.observe(s); }); 430 } 431 432 /* ====================================================================== 433 SCROLL PROGRESS / REVEAL / TYPING / COUNTERS / SKILL BARS / TILT 434 ====================================================================== */ 435 function initProgress() { 436 var bar = document.getElementById("progress"); 437 if (!bar) return; 438 function update() { 439 var max = document.documentElement.scrollHeight - window.innerHeight; 440 bar.style.width = (max > 0 ? (window.scrollY / max) * 100 : 0) + "%"; 441 } 442 window.addEventListener("scroll", update, { passive: true }); 443 update(); 444 } 445 446 function initReveal() { 447 var els = document.querySelectorAll(".reveal"); 448 if (!els.length) return; 449 if (!("IntersectionObserver" in window) || reducedMotion) { 450 els.forEach(function (el) { el.classList.add("visible"); }); 451 return; 452 } 453 var io = new IntersectionObserver(function (entries) { 454 entries.forEach(function (en) { 455 if (en.isIntersecting) { en.target.classList.add("visible"); io.unobserve(en.target); } 456 }); 457 }, { threshold: 0.12 }); 458 els.forEach(function (el) { io.observe(el); }); 459 } 460 461 function initTyping() { 462 var el = document.querySelector("[data-type]"); 463 if (!el) return; 464 var phrases; 465 try { phrases = JSON.parse(el.getAttribute("data-type")); } catch (e) { return; } 466 var target = el.querySelector(".typed"); 467 if (!target || !phrases.length) return; 468 if (reducedMotion) { target.textContent = phrases[0]; return; } 469 var pi = 0, ci = 0, deleting = false; 470 function tick() { 471 var phrase = phrases[pi]; 472 if (!deleting) { 473 ci++; 474 target.textContent = phrase.slice(0, ci); 475 if (ci === phrase.length) { deleting = true; setTimeout(tick, 2100); return; } 476 setTimeout(tick, 38 + Math.random() * 40); 477 } else { 478 ci--; 479 target.textContent = phrase.slice(0, ci); 480 if (ci === 0) { deleting = false; pi = (pi + 1) % phrases.length; setTimeout(tick, 350); return; } 481 setTimeout(tick, 18); 482 } 483 } 484 setTimeout(tick, 1100); 485 } 486 487 function initCounters() { 488 var nums = document.querySelectorAll("[data-count]"); 489 if (!nums.length) return; 490 function animate(el) { 491 var end = parseFloat(el.getAttribute("data-count")); 492 var suffix = el.getAttribute("data-suffix") || ""; 493 var dec = parseInt(el.getAttribute("data-decimals") || "0", 10); 494 function fmt(v) { return (dec ? v.toFixed(dec) : String(Math.round(v))) + suffix; } 495 if (reducedMotion) { el.textContent = fmt(end); return; } 496 var dur = 1400, start = null; 497 function step(ts) { 498 if (!start) start = ts; 499 var p = Math.min((ts - start) / dur, 1); 500 el.textContent = fmt(end * (1 - Math.pow(1 - p, 3))); 501 if (p < 1) requestAnimationFrame(step); 502 } 503 requestAnimationFrame(step); 504 } 505 var io = new IntersectionObserver(function (entries) {
506 entries.forEach(function (en) { if (en.isIntersecting) { animate(en.target); io.unobserve(en.target); } }); 507 }, { threshold: 0.4 }); 508 nums.forEach(function (el) { io.observe(el); }); 509 } 510 511 function initSkillBars() { 512 var fills = document.querySelectorAll(".skill .fill"); 513 if (!fills.length) return; 514 var io = new IntersectionObserver(function (entries) { 515 entries.forEach(function (en) { 516 if (en.isIntersecting) { en.target.style.width = en.target.getAttribute("data-w") + "%"; io.unobserve(en.target); } 517 }); 518 }, { threshold: 0.4 }); 519 fills.forEach(function (el) { io.observe(el); }); 520 } 521 522 function initTilt() { 523 if (reducedMotion || !finePointer) return; 524 document.querySelectorAll(".card.tilt, [data-tilt]").forEach(function (card) { 525 card.addEventListener("pointermove", function (e) { 526 var r = card.getBoundingClientRect(); 527 var px = (e.clientX - r.left) / r.width - 0.5, py = (e.clientY - r.top) / r.height - 0.5; 528 card.style.transform = "perspective(800px) rotateY(" + px * 6 + "deg) rotateX(" + -py * 6 + "deg) translateY(-4px)"; 529 }); 530 card.addEventListener("pointerleave", function () { card.style.transform = ""; }); 531 }); 532 } 533 534 /* ====================================================================== 535 SMALL WIDGETS â abstracts, copy, filters, lightbox, carousel, read time, 536 footer clock, toast 537 ====================================================================== */ 538 function initAbstracts() { 539 document.querySelectorAll("[data-toggle]").forEach(function (btn) { 540 btn.addEventListener("click", function () { 541 var target = document.getElementById(btn.getAttribute("data-toggle")); 542 if (!target) return; 543 btn.textContent = target.classList.toggle("open") ? "Hide abstract" : "Abstract"; 544 }); 545 }); 546 } 547 548 function initCopy() { 549 document.querySelectorAll("[data-copy]").forEach(function (btn) { 550 btn.addEventListener("click", function () { 551 var src = document.getElementById(btn.getAttribute("data-copy")); 552 if (!src) return; 553 navigator.clipboard.writeText(src.textContent.trim()).then(function () { 554 var old = btn.textContent; 555 btn.textContent = "Copied â"; 556 toast("BibTeX copied to clipboard"); 557 setTimeout(function () { btn.textContent = old; }, 1600); 558 }); 559 }); 560 }); 561 } 562 563 function initFilters() { 564 var bar = document.querySelector(".filter-bar"); 565 if (!bar) return; 566 bar.addEventListener("click", function (e) { 567 var btn = e.target.closest(".filter-btn"); 568 if (!btn) return; 569 bar.querySelectorAll(".filter-btn").forEach(function (b) { b.classList.remove("active"); }); 570 btn.classList.add("active"); 571 var f = btn.getAttribute("data-filter"); 572 document.querySelectorAll("[data-tags]").forEach(function (item, i) { 573 var show = f === "all" || item.getAttribute("data-tags").split(" ").indexOf(f) !== -1; 574 item.style.display = show ? "" : "none"; 575 if (show && !reducedMotion) { 576 item.style.animation = "none"; 577 void item.offsetWidth; 578 item.style.animation = "fade-in 0.4s ease " + (i * 0.04) + "s both"; 579 } 580 }); 581 }); 582 } 583 584 function initLightbox() { 585 var imgs = document.querySelectorAll(".gallery-img"); 586 if (!imgs.length) return; 587 var box = document.createElement("div"); 588 box.id = "lightbox"; 589 box.innerHTML = "<img alt=''>"; 590 document.body.appendChild(box); 591 var boxImg = box.querySelector("img"); 592 imgs.forEach(function (img) { 593 img.addEventListener("click", function () { 594 boxImg.src = img.getAttribute("data-full") || img.src; 595 box.classList.add("open"); 596 }); 597 }); 598 box.addEventListener("click", function () { box.classList.remove("open"); }); 599 document.addEventListener("keydown", function (e) { if (e.key === "Escape") box.classList.remove("open"); }); 600 } 601 602 function initCarousels() { 603 document.querySelectorAll(".carousel").forEach(function (car) { 604 var imgs; 605 try { imgs = JSON.parse(car.getAttribute("data-images")); } catch (e) { return; } 606 var frameImg = car.querySelector(".frame img");
607 var dotsWrap = car.querySelector(".dots"); 608 var idx = 0, timer; 609 imgs.forEach(function (_, i) { 610 var d = document.createElement("button"); 611 d.className = "dot" + (i === 0 ? " active" : ""); 612 d.setAttribute("aria-label", "Photo " + (i + 1)); 613 d.addEventListener("click", function () { go(i); restart(); }); 614 dotsWrap.appendChild(d); 615 }); 616 function go(i) { 617 idx = (i + imgs.length) % imgs.length; 618 frameImg.style.opacity = "0"; 619 setTimeout(function () { frameImg.src = imgs[idx]; frameImg.style.opacity = "1"; }, 280); 620 dotsWrap.querySelectorAll(".dot").forEach(function (d, j) { d.classList.toggle("active", j === idx); }); 621 } 622 function restart() { 623 clearInterval(timer); 624 if (!reducedMotion) timer = setInterval(function () { go(idx + 1); }, 5000); 625 } 626 car.querySelector(".c-prev").addEventListener("click", function () { go(idx - 1); restart(); }); 627 car.querySelector(".c-next").addEventListener("click", function () { go(idx + 1); restart(); }); 628 car.addEventListener("pointerenter", function () { clearInterval(timer); }); 629 car.addEventListener("pointerleave", restart); 630 restart(); 631 }); 632 } 633 634 function initReadTime() { 635 var body = document.querySelector(".article-body"); 636 var eyebrow = document.querySelector(".article header .eyebrow"); 637 if (!body || !eyebrow) return; 638 var words = body.textContent.trim().split(/\s+/).length; 639 var mins = Math.max(1, Math.round(words / 220)); 640 var span = document.createElement("span"); 641 span.className = "readtime"; 642 span.textContent = mins + " min read"; 643 eyebrow.appendChild(span); 644 } 645 646 /* highlights the current section in the article TOC rail. Uses viewport-relative 647 rects rather than offsetTop: .article is transformed by the page-slide animation, 648 which moves the offsetParent out from under the headings. */ 649 function initArticleToc() { 650 var toc = document.querySelector(".article-toc"); 651 if (!toc) return; 652 var links = [].slice.call(toc.querySelectorAll("a")); 653 var heads = links.map(function (a) { 654 var id = a.getAttribute("href").slice(1); 655 try { return document.getElementById(decodeURIComponent(id)); } catch (e) { return document.getElementById(id); } 656 }); 657 if (!links.length || heads.indexOf(null) !== -1) { toc.style.display = "none"; return; } 658 659 var ticking = false; 660 function update() { 661 ticking = false; 662 var idx = 0; 663 heads.forEach(function (h, i) { if (h.getBoundingClientRect().top <= 140) idx = i; }); 664 links.forEach(function (a, i) { a.classList.toggle("active", i === idx); }); 665 } 666 update(); 667 window.addEventListener("scroll", function () { 668 if (!ticking) { ticking = true; requestAnimationFrame(update); } 669 }, { passive: true }); 670 } 671 672 function initClock() { 673 var note = document.querySelector(".foot-note"); 674 if (!note) return; 675 var up = document.createElement("span"); up.className = "up"; up.title = "systems nominal"; 676 var clock = document.createElement("span"); clock.className = "clock"; 677 note.appendChild(up); note.appendChild(clock); 678 function tick() { 679 try { 680 clock.textContent = "BERKELEY " + new Date().toLocaleTimeString("en-US", { timeZone: "America/Los_Angeles", hour12: false }); 681 } catch (e) { clock.textContent = new Date().toLocaleTimeString(); } 682 } 683 tick(); setInterval(tick, 1000); 684 } 685 686 var toastEl, toastTimer; 687 function toast(msg) { 688 if (!toastEl) { toastEl = document.createElement("div"); toastEl.id = "toast"; document.body.appendChild(toastEl); } 689 toastEl.textContent = msg; 690 toastEl.classList.add("show"); 691 clearTimeout(toastTimer); 692 toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200); 693 } 694 695 /* ====================================================================== 696 EASTER EGGS â Konami hyperdrive 697 ====================================================================== */ 698 function initEggs() { 699 var seq = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"]; 700 var pos = 0; 701 document.addEventListener("keydown", function (e) { 702 var k = e.key.length === 1 ? e.key.toLowerCase() : e.key; 703 pos = k === seq[pos] ? pos + 1 : (k === seq[0] ? 1 : 0); 704 if (pos === seq.length) { 705 pos = 0; 706 WARP.target = 1; 707 document.body.classList.add("warp"); 708 toast("â¡ hyperdrive engaged"); 709 setTimeout(function () { WARP.target = 0; document.body.classList.remove("warp"); }, 4500); 710 } 711 }); 712 } 713 714 /* ====================================================================== 715 RESEARCH MAP â force-directed graph of papers, ideas, people, venues 716 ====================================================================== */ 717 var RMAP_DATA = { 718 nodes: [ 719 { id: "me", type: "self", label: "Jerick Shi", sub: "multi-agent LLM systems · AI safety", href: "/" }, 720 721 { id: "lie", type: "paper", label: "When Agents Lie", sub: "ICML '26 NExT-Game workshop · Best Paper Award", star: true, 722 href: "https://openreview.net/pdf?id=v8nYIkYjY0", ext: true, tip: "Premeditation, persistence, and exploitation in repeated games" }, 723 { id: "silence", type: "paper", label: "Strategic Silence", sub: "ICML '26 Failure Modes of Agentic AI
723workshop", 724 href: "https://openreview.net/pdf?id=ZOdCsExYgi", ext: true, tip: "What game-theoretic benchmarks miss in multi-agent LLMs" }, 725 { id: "cheap", type: "paper", label: "Cheap Talk, Empty Promise", sub: "ICLR '26 AI for Mechanism Design workshop", 726 href: "https://arxiv.org/abs/2604.04782", ext: true, tip: "Frontier LLMs break public promises for self-interest" }, 727 { id: "tax", type: "paper", label: "Hallucination â Scheming", sub: "ICLR '26 Agents in the Wild workshop", 728 href: "https://arxiv.org/abs/2604.04788", ext: true, tip: "A unified taxonomy and benchmark analysis for LLM deception" }, 729 { id: "market", type: "paper", label: "Market-Dependent Communication", sub: "NeurIPS '25 GenAI in Finance workshop", 730 href: "https://doi.org/10.48550/arXiv.2511.13614", ext: true, tip: "Communication in multi-agent alpha generation" }, 731 { id: "thesis", type: "paper", label: "Master's Thesis", sub: "CMU-CS-26-105 · The Structure of Deception", 732 href: "/projects/masters-thesis/", tip: "How LLM agents lie, break promises, and exploit trust" }, 733 { id: "senior", type: "paper", label: "Senior Thesis", sub: "Multi-agent conversations & LLM forecasting", 734 href: "/projects/senior-thesis/", tip: "Why multi-agent conversations cannot fix LLM forecasting" }, 735 { id: "mom", type: "paper", label: "LLMs in Financial Markets", sub: "Meeting of the Minds · First Place", 736 href: "/projects/meeting-minds/", tip: "Predictive power of LLMs on the Fed's Beige Book" }, 737 738 { id: "c-decep", type: "concept", label: "deception" }, 739 { id: "c-repeat", type: "concept", label: "repeated games" }, 740 { id: "c-commit", type: "concept", label: "commitment" }, 741 { id: "c-cheap", type: "concept", label: "cheap talk" }, 742 { id: "c-bench", type: "concept", label: "benchmarks" }, 743 { id: "c-tax", type: "concept", label: "taxonomy" }, 744 { id: "c-multi", type: "concept", label: "multi-agent" }, 745 { id: "c-game", type: "concept", label: "game theory" }, 746 { id: "c-safety", type: "concept", label: "AI safety" }, 747 { id: "c-fin", type: "concept", label: "finance" }, 748 { id: "c-comm", type: "concept", label: "communication" }, 749 { id: "c-forecast", type: "concept", label: "forecasting" }, 750 { id: "c-trust", type: "concept", label: "trust & exploitation" }, 751 752 { id: "p-conitzer", type: "person", label: "Vincent Conitzer", sub: "CMU · advisor", href: "https://www.cs.cmu.edu/~conitzer/", ext: true }, 753 { id: "p-jin", type: "person", label: "Zhijing Jin", sub: "U Toronto · advisor", href: "https://zhijing-jin.com/", ext: true }, 754 { id: "p-zhang", type: "person", label: "Terry Zhang", sub: "coauthor" }, 755 { id: "p-schoelkopf", type: "person", label: "Bernhard Schölkopf", sub: "MPI-IS · coauthor" }, 756 { id: "p-hollifield", type: "person", label: "Burton Hollifield", sub: "CMU Tepper · coauthor" }, 757 { id: "p-raghunathan", type: "person", label: "Aditi Raghunathan", sub: "CMU · thesis committee" }, 758 759 { id: "v-icml", type: "venue", label: "ICML 2026" }, 760 { id: "v-iclr", type: "venue", label: "ICLR 2026" }, 761 { id: "v-neurips", type: "venue", label: "NeurIPS 2025" }, 762 { id: "v-cmu", type: "venue", label: "CMU" } 763 ], 764 links: [ 765 ["me", "lie"], ["me", "silence"], ["me", "cheap"], ["me", "tax"], ["me", "market"], ["me", "thesis"], ["me", "senior"], ["me", "mom"], 766 767 ["lie", "c-decep"], ["lie", "c-repeat"], ["lie", "c-trust"], ["lie", "c-multi"], 768 ["silence", "c-bench"], ["silence", "c-comm"], ["silence", "c-game"], ["silence", "c-multi"], 769 ["cheap", "c-commit"], ["cheap", "c-cheap"], ["cheap", "c-decep"], ["cheap", "c-game"], 770 ["tax", "c-tax"], ["tax", "c-bench"], ["tax", "c-decep"], ["tax", "c-safety"], 771 ["market", "c-fin"], ["market", "c-comm"], ["market", "c-multi"], 772 ["thesis", "c-decep"], ["thesis", "c-repeat"], ["thesis", "c-commit"], ["thesis", "c-trust"], ["thesis", "c-safety"], 773 ["senior", "c-forecast"], ["senior", "c-multi"], ["senior", "c-comm"], 774 ["mom", "c-fin"], ["mom", "c-forecast"], 775 776 ["lie", "p-zhang"], ["lie", "p-schoelkopf"], ["lie", "p-conitzer"], ["lie", "p-jin"], 777 ["silence", "p-zhang"], ["silence", "p-conitzer"], ["silence", "p-jin"], 778 ["cheap", "p-zhang"], ["cheap", "p-jin"], ["cheap", "p-conitzer"], 779 ["tax", "p-zhang"], ["tax", "p-jin"], ["tax", "p-conitzer"], 780 ["market", "p-hollifield"], 781 ["thesis", "p-conitzer"], ["thesis", "p-jin"], ["thesis", "p-raghunathan"], 782 783 ["lie", "v-icml"], ["silence", "v-icml"], ["cheap", "v-iclr"], ["tax", "v-iclr"], ["market", "v-neurips"], 784 ["thesis", "v-cmu"], ["senior", "v-cmu"], ["mom", "v-cmu"] 785 ] 786 }; 787 788 var RMAP_STYLE = { 789 self: { color: "#ffffff", glow: "rgba(70,224,255,0.9)", r: 15, label: "always" }, 790 paper: { color: "#9d6bff", glow: "rgba(157,107,255,0.9)", r: 9, label: "always" }, 791 concept: { color: "#46e0ff", glow: "rgba(70,224,255,0.8)", r: 5.5, label: "wide" }, 792 person: { color: "#5b8cff", glow: "rgba(91,140,255,0.8)", r: 6.5, label: "wide" }, 793 venue: { color: "#ffd479", glow: "rgba(255,212,121,0.8)", r: 6, label: "wide" } 794 }; 795 796 function createResearchMap(host) { 797 var compact = host.classList.contains("compact"); 798 var canvas = document.createElement("canvas"); 799 host.appendChild(canvas); 800 var ctx = canvas.getContext("2d"); 801 802 // legend 803 var legend = document.createElement("div"); 804 legend.className = "rmap-legend"; 805 var hidden = {}; 806 ["paper", "concept", "person", "venue"].forEach(function (t) { 807 var b = document.createElement("button"); 808 b.innerHTML = "<i style=\"--c:" + RMAP_STYLE[t].color + "\"></i>" + (t === "person" ? "people" : t + "s"); 809 b.addEventListener("click", function () { 810 hidden[t] = !hidden[t]; 811 b.classList.toggle("off", hidden[t]); 812 alpha = Math.max(alpha, 0.5); 813 }); 814 legend.appendChild(b); 815 }); 816 host.appendChild(legend); 817 818 var hint = document.createElement("div"); 819 hint.className = "rmap-hint"; 820 hint.textContent = finePointer ? "drag nodes · hover to explore · click to open" : "drag nodes · tap to open"; 821 host.appendChild(hint); 822 823 var tip = document.createElement("div"); 824 tip.className = "rmap-tip"; 825 host.appendChild(tip); 826 827 // graph state 828 var nodes = RMAP_DATA.nodes.map(function (n) { 829 return Object.assign({ x: 0, y: 0, vx: 0, vy: 0, phase: Math.random() * Math.PI * 2 }, n, { style: RMAP_STYLE[n.type] }); 830 }); 831 var byId = {};
832 nodes.forEach(function (n) { byId[n.id] = n; }); 833 var links = RMAP_DATA.links.map(function (l) { return { a: byId[l[0]], b: byId[l[1]] }; }); 834 var adj = {}; 835 links.forEach(function (l) { 836 (adj[l.a.id] = adj[l.a.id] || []).push(l.b.id); 837 (adj[l.b.id] = adj[l.b.id] || []).push(l.a.id); 838 }); 839 840 var W = 0, H = 0, dpr = 1, alpha = 1, hover = null, drag = null, pinned = null, externalHighlight = null; 841 842 function visible(n) { return !hidden[n.type]; } 843 844 function resize() { 845 dpr = Math.min(window.devicePixelRatio || 1, 2); 846 var r = host.getBoundingClientRect(); 847 W = r.width; H = r.height; 848 canvas.width = W * dpr; canvas.height = H * dpr; 849 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 850 alpha = Math.max(alpha, 0.4); 851 } 852 853 function seed() { 854 var cx = W / 2, cy = H / 2; 855 nodes.forEach(function (n, i) { 856 var ang = (i / nodes.length) * Math.PI * 2, rad = n.type === "self" ? 0 : n.type === "paper" ? Math.min(W, H) * 0.18 : Math.min(W, H) * 0.36; 857 n.x = cx + Math.cos(ang) * rad + (Math.random() - 0.5) * 20; 858 n.y = cy + Math.sin(ang) * rad + (Math.random() - 0.5) * 20; 859 }); 860 } 861 862 function restLength(l) { 863 var t = l.a.type + "-" + l.b.type; 864 var base = Math.min(W, H); 865 if (t === "self-paper") return base * (compact ? 0.30 : 0.26); 866 if (l.a.type === "paper" && l.b.type === "concept") return base * (compact ? 0.22 : 0.19); 867 return base * (compact ? 0.24 : 0.21); 868 } 869 870 function step() { 871 var i, j, n, m, dx, dy, d2, d, f; 872 var k = Math.min(W, H) * (compact ? 9 : 11); 873 for (i = 0; i < nodes.length; i++) { 874 n = nodes[i]; 875 if (!visible(n)) continue; 876 for (j = i + 1; j < nodes.length; j++) { 877 m = nodes[j]; 878 if (!visible(m)) continue; 879 dx = m.x - n.x; dy = m.y - n.y; 880 d2 = dx * dx + dy * dy + 0.01; 881 if (d2 > 250000) continue; 882 f = (k / d2) * alpha; 883 d = Math.sqrt(d2); 884 dx /= d; dy /= d; 885 n.vx -= dx * f; n.vy -= dy * f; 886 m.vx += dx * f; m.vy += dy * f; 887 } 888 } 889 links.forEach(function (l) { 890 if (!visible(l.a) || !visible(l.b)) return; 891 dx = l.b.x - l.a.x; dy = l.b.y - l.a.y; 892 d = Math.sqrt(dx * dx + dy * dy) + 0.01; 893 f = (d - restLength(l)) * 0.012 * alpha; 894 dx /= d; dy /= d; 895 l.a.vx += dx * f; l.a.vy += dy * f; 896 l.b.vx -= dx * f; l.b.vy -= dy * f; 897 }); 898 var cx = W / 2, cy = H / 2, t = performance.now() / 1000; 899 nodes.forEach(function (n) { 900 if (!visible(n)) return; 901 if (n === drag) { n.vx = n.vy = 0; return; } 902 // gravity is weaker along the wider axis so the graph fills the box 903 var gx = n.type === "self" ? 0.03 : (W >= H ? 0.0012 : 0.003); 904 var gy = n.type === "self" ? 0.03 : (W >= H ? 0.003 : 0.0012); 905 n.vx += (cx - n.x) * gx * alpha; 906 n.vy += (cy - n.y) * gy * alpha; 907 // gentle idle drift so the map never looks frozen 908 if (!reducedMotion) { n.vx += Math.cos(t * 0.6 + n.phase) * 0.012; n.vy += Math.sin(t * 0.5 + n.phase) * 0.012; } 909 n.vx *= 0.82; n.vy *= 0.82; 910 n.x += n.vx; n.y += n.vy; 911 n.x = clamp(n.x, 30, W - 30); n.y = clamp(n.y, 40, H - 44); 912 }); 913 alpha = Math.max(alpha * 0.985, 0.06); 914 } 915 916 function focusSet() { 917 var f = hover || pinned || externalHighlight; 918 if (!f) return null; 919 var set = {}; set[f.id] = true; 920 (adj[f.id] || []).forEach(function (id) { set[id] = true; }); 921 return set; 922 } 923 924 function draw() { 925 ctx.clearRect(0, 0, W, H); 926 var focus = focusSet(); 927 var showAllLabels = W > 700; 928 929 // links 930 links.forEach(function (l) { 931 if (!visible(l.a) || !visible(l.b)) return; 932 var lit = focus && focus[l.a.id] && focus[l.b.id] && (l.a === (hover || pinned || externalHighlight) || l.b === (hover || pinned || externalHighlight)); 933 ctx.beginPath(); 934 ctx.moveTo(l.a.x, l.a.y); ctx.lineTo(l.b.x, l.b.y); 935 if (lit) { 936 var g = ctx.createLinearGradient(l.a.x, l.a.y, l.b.x, l.b.y); 937 g.addColorStop(0, l.a.style.color); g.addColorStop(1, l.b.style.color); 938 ctx.strokeStyle = g; ctx.lineWidth = 1.4; ctx.globalAlpha = 0.9; 939 } else { 940 ctx.strokeStyle = "rgba(139,156,255,1)"; ctx.lineWidth = 0.7;
940 ctx.globalAlpha = focus ? 0.06 : 0.16; 941 } 942 ctx.stroke(); 943 }); 944 ctx.globalAlpha = 1; 945 946 // nodes 947 nodes.forEach(function (n) { 948 if (!visible(n)) return; 949 var dim = focus && !focus[n.id]; 950 var isFocus = n === hover || n === pinned || n === externalHighlight; 951 var r = n.style.r * (isFocus ? 1.35 : 1) * (n.star ? 1.15 : 1); 952 ctx.globalAlpha = dim ? 0.18 : 1; 953 // glow 954 var g = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, r * 3.2); 955 g.addColorStop(0, n.style.glow); g.addColorStop(1, "rgba(0,0,0,0)"); 956 ctx.fillStyle = g; ctx.globalAlpha = (dim ? 0.05 : isFocus ? 0.6 : 0.28); 957 ctx.beginPath(); ctx.arc(n.x, n.y, r * 3.2, 0, Math.PI * 2); ctx.fill(); 958 ctx.globalAlpha = dim ? 0.2 : 1; 959 // body 960 ctx.beginPath(); ctx.arc(n.x, n.y, r, 0, Math.PI * 2); 961 ctx.fillStyle = n.type === "self" ? "#0a0f1e" : n.style.color; 962 ctx.fill(); 963 ctx.lineWidth = n.type === "self" ? 2 : 1; 964 ctx.strokeStyle = n.type === "self" ? "#46e0ff" : "rgba(255,255,255,0.35)"; 965 ctx.stroke(); 966 if (n.star) { 967 ctx.fillStyle = "#ffd479"; ctx.font = "9px " + "JetBrains Mono, monospace"; 968 ctx.textAlign = "center"; ctx.textBaseline = "middle"; 969 ctx.fillText("â ", n.x, n.y + 0.5); 970 } 971 // label 972 var wantLabel = n.style.label === "always" || showAllLabels || isFocus || (focus && focus[n.id]); 973 if (wantLabel) { 974 var big = n.type === "self" || n.type === "paper"; 975 ctx.font = (big ? "600 " : "") + (big ? 12 : 10.5) + "px " + (big ? "Space Grotesk, Helvetica Neue, sans-serif" : "JetBrains Mono, monospace"); 976 ctx.textAlign = "center"; ctx.textBaseline = "top"; 977 var ty = n.y + r + 5; 978 var half = ctx.measureText(n.label).width / 2 + 4; 979 var lx = clamp(n.x, half, W - half); 980 ctx.lineWidth = 3; ctx.strokeStyle = "rgba(4,6,13,0.85)"; ctx.lineJoin = "round"; 981 ctx.strokeText(n.label, lx, ty); 982 ctx.fillStyle = big ? "#e8ecf8" : (dim ? "#6b7490" : "#9aa3bc"); 983 if (isFocus) ctx.fillStyle = n.style.color === "#ffffff" ? "#46e0ff" : n.style.color; 984 ctx.fillText(n.label, lx, ty); 985 } 986 ctx.globalAlpha = 1; 987 }); 988 } 989 990 var running = false; 991 function loop() { 992 if (!running) return; 993 step(); draw(); 994 requestAnimationFrame(loop); 995 } 996 997 // interaction 998 function pick(x, y) { 999 var best = null, bd = 1e9; 1000 nodes.forEach(function (n) { 1001 if (!visible(n)) return; 1002 var dx = n.x - x, dy = n.y - y, d = dx * dx + dy * dy, rr = (n.style.r + 10); 1003 if (d < rr * rr && d < bd) { bd = d; best = n; } 1004 }); 1005 return best; 1006 } 1007 1008 function local(e) { 1009 var r = canvas.getBoundingClientRect(); 1010 return { x: e.clientX - r.left, y: e.clientY - r.top }; 1011 } 1012 1013 function showTip(n, x, y) { 1014 if (!n || n.type === "concept" || n.type === "venue") { tip.classList.remove("show"); return; } 1015 var kind = n.type === "self" ? "you are here" : n.type; 1016 tip.innerHTML = '<span class="k">' + kind + (n.star ? " · â best paper" : "") + "</span><b>" + n.label + "</b>" + 1017 (n.tip ? '<div style="font-size:0.8rem;">' + n.tip + "</div>" : "") + 1018 (n.sub ? '<div style="font-size:0.74rem; color:var(--faint); margin-top:0.25rem;">' + n.sub + "</div>" : "") + 1019 (n.href && n.type !== "self" ? '<span class="go">click to open â</span>' : ""); 1020 var tw = 280; 1021 var left = clamp(x + 18, 8, W - tw - 8), top = y + 18; 1022 if (top > H - 140) top = y - 120; 1023 tip.style.left = left + "px"; tip.style.top = top + "px"; 1024 tip.classList.add("show"); 1025 } 1026 1027 var downAt = null, moved = false; 1028 canvas.addEventListener("pointerdown", function (e) { 1029 var p = local(e); 1030 var n = pick(p.x, p.y); 1031 downAt = p; moved = false; 1032 if (n) { drag = n; canvas.classList.add("dragging"); canvas.setPointerCapture(e.pointerId); alpha = Math.max(alpha, 0.3); } 1033 }); 1034 canvas.addEventListener("pointermove", function (e) { 1035 var p = local(e); 1036 if (drag) { 1037 drag.x = clamp(p.x, 30, W - 30); drag.y = clamp(p.y, 40, H - 44); 1038 if (downAt && Math.hypot(p.x - downAt.x, p.y - downAt.y) > 4) moved = true; 1039 alpha = Math.max(alpha, 0.25); 1040 showTip(drag, p.x, p.y); 1041 return; 1042 } 1043 var n = pick(p.x, p.y); 1044 if (n !== hover) { hover = n; alpha = Math.max(alpha, 0.12); } 1045 canvas.style.cursor = n ? "pointer" : ""; 1046 showTip(n, p.x, p.y); 1047 }); 1048 function release(e) { 1049 if (drag) { 1050 var n = drag; 1051 drag = null; canvas.classList.remove("dragging"); 1052 try { canvas.releasePointerCapture(e.pointerId); } catch (err) {} 1053 if (!moved) { 1054 if (n.href && n.type !== "self") { 1055 if (n.ext) window.open(n.href, "_blank", "noopener"); 1056 else { document.body.classList.add("leaving"); setTimeout(function () { location.href = n.href; }, reducedMotion ? 0 : 300); } 1057 } else { 1058 pinned = pinned === n ? null : n; 1059 } 1060 } 1061 } 1062 downAt = null; 1063 } 1064 canvas.addEventListener("pointerup", release); 1065 canvas.addEventListener("pointercancel", release); 1066 canvas.addEventListener("pointerleave", function () { hover = null; tip.classList.remove("show"); }); 1067 1068 // start when visible 1069 var started = false;
1070 var io = new IntersectionObserver(function (entries) { 1071 entries.forEach(function (en) { 1072 if (en.isIntersecting) { 1073 if (!started) { 1074 started = true; 1075 resize(); seed(); 1076 alpha = 1; 1077 for (var i = 0; i < 260; i++) step(); // settle before first paint 1078 alpha = 0.25; 1079 } 1080 if (!running) { running = true; requestAnimationFrame(loop); } 1081 } else { 1082 running = false; 1083 } 1084 }); 1085 }, { threshold: 0.05 }); 1086 io.observe(host); 1087 window.addEventListener("resize", function () { if (started) resize(); }); 1088 1089 return { 1090 highlight: function (id) { externalHighlight = id ? byId[id] || null : null; alpha = Math.max(alpha, 0.1); } 1091 }; 1092 } 1093 1094 function initResearchMaps() { 1095 var maps = []; 1096 document.querySelectorAll("[data-research-map]").forEach(function (host) { 1097 host.classList.add("rmap"); 1098 if (host.getAttribute("data-research-map") === "compact") host.classList.add("compact"); 1099 maps.push(createResearchMap(host)); 1100 }); 1101 if (!maps.length) return; 1102 // paper cards can highlight their node on hover 1103 document.querySelectorAll("[data-node]").forEach(function (el) { 1104 el.addEventListener("pointerenter", function () { maps.forEach(function (m) { m.highlight(el.getAttribute("data-node")); }); }); 1105 el.addEventListener("pointerleave", function () { maps.forEach(function (m) { m.highlight(null); }); }); 1106 }); 1107 } 1108 1109 /* ====================================================================== 1110 COMMAND PALETTE 1111 ====================================================================== */ 1112 var CMD_ITEMS = [ 1113 { label: "Home / About", href: "/", k: "go" }, 1114 { label: "Research & Publications", href: "/publications/", k: "go" }, 1115 { label: "Research Map (interactive)", href: "/publications/#map", k: "go" }, 1116 { label: "Projects", href: "/projects/", k: "go" }, 1117 { label: "Teaching", href: "/teaching/", k: "go" }, 1118 { label: "Blog", href: "/blog/", k: "go" }, 1119 { label: "Hobbies", href: "/hobbies/", k: "go" }, 1120 { label: "Bookshelf", href: "/books/", k: "go" }, 1121 { label: "CV", href: "/cv/", k: "go" }, 1122 { label: "Download CV (PDF)", href: "/assets/pdf/Shi_Jerick_CV.pdf", k: "pdf" }, 1123 { label: "Master's Thesis (PDF)", href: "/assets/pdf/masters_thesis.pdf", k: "pdf" }, 1124 { label: "When Agents Lie â Best Paper, ICML '26 WS (PDF)", href: "https://openreview.net/pdf?id=v8nYIkYjY0", k: "pdf" }, 1125 { label: "Strategic Silence in Multi-Agent LLMs (PDF)", href: "https://openreview.net/pdf?id=ZOdCsExYgi", k: "pdf" }, 1126 { label: "Thesis Defense (YouTube)", href: "https://youtu.be/Z3Q9AkriPxg", k: "link" }, 1127 { label: "GitHub â Jerick-1380", href: "https://github.com/Jerick-1380", k: "link" }, 1128 { label: "LinkedIn", href: "https://www.linkedin.com/in/jerick-shi-293773216", k: "link" }, 1129 { label: "Google Scholar", href: "https://scholar.google.com/citations?user=6wj2mTQAAAAJ", k: "link" }, 1130 { label: "Flickr Portfolio", href: "https://flickr.com/photos/203834484@N07/", k: "link" }, 1131 { label: "Email â [email protected]", href: "mailto:[email protected]", k: "link" }, 1132 { label: "Quant Engine â join waitlist", href: "https://quant-engine.com/", k: "link" }, 1133 { label: "Engage hyperdrive", href: "#warp", k: "fun" } 1134 ]; 1135 1136 /* ====================================================================== 1137 BOOKSHELF â cover wall with genre filter, search, and FLIP reordering. 1138 Ratings and genres are Jerick's own; covers and years come from Open 1139 Library. Add a book here and the page, stats and filters all follow. 1140 ====================================================================== */ 1141 var BOOK_GENRES = { 1142 productivity: "Productivity", 1143 finance: "Finance", 1144 psychology: "Psychology", 1145 cs: "Computer Science", 1146 freediving: "Freediving" 1147 }; 1148 1149 var BOOKS = [ 1150 { t: "Atomic Habits", a: "James Clear", y: 2018, g: "productivity", r: 5, c: "atomic-habits" }, 1151 { t: "Deep Work", a: "Cal Newport", y: 2016, g: "productivity", r: 5, c: "deep-work" }, 1152 { t: "The Defining Decade", a: "Meg Jay", y: 2012, g: "productivity", r: 5, c: "the-defining-decade" }, 1153 { t: "Make Time", a: "Jake Knapp & John Zeratsky", y: 2018, g: "productivity", r: 4, c: "make-time" }, 1154 { t: "How to Be a Gentleman", a: "John Bridges", y: 2001, g: "productivity", r: 4, c: "how-to-be-a-gentleman" }, 1155 { t: "The 12 Week Year", a: "Brian P. Moran", y: 2013, g: "productivity", r: 3, c: "the-12-week-year" }, 1156 { t: "Feel-Good Productivity", a: "Ali Abdaal", y: 2023, g: "productivity", r: 3, c: "feel-good-productivity" }, 1157 { t: "Indistractable", a: "Nir Eyal", y: 2019, g: "productivity", r: 3, c: "indistractable" }, 1158 { t: "No More Mr. Nice Guy", a: "Robert Glover", y: 2001, g: "psychology", r: 5, c: "no-more-mr-nice-guy" }, 1159 { t: "Surrounded by Idiots", a: "Thomas Erikson", y: 2014, g: "psychology", r: 4, c: "surrounded-by-idiots" }, 1160 { t: "How to Win Friends and Influence People", a: "Dale Carnegie", y: 1936, g: "psychology", r: 4, c: "how-to-win-friends-and-influence-people" }, 1161 { t: "Million Dollar Weekend", a: "Noah Kagan", y: 2024, g: "finance", r: 4, c: "million-dollar-weekend" }, 1162 { t: "The Millionaire Fastlane", a: "MJ DeMarco", y: 2011, g: "finance", r: 4, c: "the-millionaire-fastlane" }, 1163 { t: "The Complete TurtleTrader", a: "Michael W. Covel", y: 2007, g: "finance", r: 4, c: "the-complete-turtletrader" }, 1164 { t: "The 4-Hour Workweek", a: "Timothy Ferriss", y: 2007, g: "finance", r: 3, c: "the-4-hour-workweek" }, 1165 { t: "The Psychology of Money", a: "Morgan Housel", y: 2020, g: "finance", r: 3, c: "the-psychology-of-money" }, 1166 { t: "Life 3.0", a: "Max Tegmark", y: 2017, g: "cs", r: 4, c: "life-3-0" }, 1167 { t: "The Precipice", a: "Toby Ord", y: 2020, g: "cs", r: 3, c: "the-precipice" }, 1168 { t: "Deep", a: "James Nestor", y: 2014, g: "freediving", r: 4, c: "deep" }, 1169 { t: "One Breath", a: "Adam Skolnick", y: 2016, g: "freediving", r: 3, c: "one-breath" } 1170 ]; 1171 1172 function bookStars(n) { 1173 var s = ""; 1174 for (var i = 1; i <= 5; i++) s += i <= n ? "â " : "â"; 1175 return s; 1176 } 1177 1178 function esc(s) { 1179 return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); 1180 } 1181 1182 /* FLIP: measure, mutate, then animate each survivor from its old box */ 1183 function flipReorder(items, mutate) { 1184 if (reducedMotion || !items.length || !items[0].el.animate) { mutate(); return; } 1185 var before = [];
1186 items.forEach(function (it) { 1187 before.push(it.el.style.display === "none" ? null : it.el.getBoundingClientRect()); 1188 }); 1189 mutate(); 1190 items.forEach(function (it, i) { 1191 if (it.el.style.display === "none") return; 1192 var a = before[i]; 1193 if (!a) { 1194 it.el.animate([{ opacity: 0, transform: "scale(0.94)" }, { opacity: 1, transform: "none" }], 1195 { duration: 300, easing: "ease-out" }); 1196 return; 1197 } 1198 var b = it.el.getBoundingClientRect(); 1199 var dx = a.left - b.left, dy = a.top - b.top; 1200 if (!dx && !dy) return; 1201 it.el.animate([{ transform: "translate(" + dx + "px," + dy + "px)" }, { transform: "none" }], 1202 { duration: 420, easing: "cubic-bezier(0.22, 1, 0.36, 1)" }); 1203 }); 1204 } 1205 1206 function initBookshelf() { 1207 var host = document.querySelector("[data-bookshelf]"); 1208 if (!host) return; 1209 1210 var total = BOOKS.length; 1211 var genreKeys = Object.keys(BOOK_GENRES); 1212 var sum = 0, top = 0; 1213 BOOKS.forEach(function (b) { sum += b.r; if (b.r === 5) top++; }); 1214 var avg = (sum / total).toFixed(1); 1215 1216 host.innerHTML = 1217 '<div class="stats cols-4 reveal">' + 1218 '<div class="stat"><div class="num" data-count="' + total + '">0</div><div class="lbl">books</div></div>' + 1219 '<div class="stat"><div class="num" data-count="' + genreKeys.length + '">0</div><div class="lbl">genres</div></div>' + 1220 '<div class="stat"><div class="num" data-count="' + avg + '" data-decimals="1">0</div><div class="lbl">average rating</div></div>' + 1221 '<div class="stat"><div class="num" data-count="' + top + '">0</div><div class="lbl">five-star reads</div></div>' + 1222 '</div>' + 1223 '<div class="shelf-controls">' + 1224 '<div class="filter-bar shelf-filters">' + 1225 '<button class="filter-btn active" data-genre="all">All</button>' + 1226 genreKeys.map(function (k) { 1227 return '<button class="filter-btn" data-genre="' + k + '">' + BOOK_GENRES[k] + '</button>'; 1228 }).join("") + 1229 '</div>' + 1230 '<input class="shelf-search" type="search" placeholder="Search title or authorâ¦" aria-label="Search books">' + 1231 '</div>' + 1232 '<div class="shelf-count mono"></div>' + 1233 '<div class="shelf-grid"></div>' + 1234 '<p class="shelf-empty mono">// no books match that</p>'; 1235 1236 var grid = host.querySelector(".shelf-grid"); 1237 var countEl = host.querySelector(".shelf-count"); 1238 var emptyEl = host.querySelector(".shelf-empty"); 1239 var search = host.querySelector(".shelf-search"); 1240 1241 var items = BOOKS.map(function (b) { 1242 var el = document.createElement("div"); 1243 el.className = "book"; 1244 el.setAttribute("data-tilt", ""); 1245 el.setAttribute("tabindex", "0"); 1246 el.setAttribute("role", "button"); 1247 el.setAttribute("aria-label", b.t + " by " + b.a + ", rated " + b.r + " of 5"); 1248 el.innerHTML = 1249 '<img src="/assets/img/book_covers/' + b.c + '.jpg" alt="' + esc(b.t) + ' cover" loading="lazy">' + 1250 '<div class="book-ov">' + 1251 '<div class="t">' + esc(b.t) + '</div>' + 1252 '<div class="a">' + esc(b.a) + '</div>' + 1253 '<div class="r"><span class="on">' + bookStars(b.r).slice(0, b.r) + '</span>' + 1254 bookStars(b.r).slice(b.r) + '</div>' + 1255 '</div>'; 1256 grid.appendChild(el); 1257 return { el: el, b: b, q: (b.t + " " + b.a + " " + BOOK_GENRES[b.g]).toLowerCase() }; 1258 }); 1259 1260 /* detail panel â appended to <body>, never inside <main>, which is 1261 transformed by page-slide and would capture position:fixed */ 1262 var modal = document.createElement("div"); 1263 modal.className = "book-modal"; 1264 modal.innerHTML = '<div class="book-modal-panel" role="dialog" aria-modal="true"></div>'; 1265 document.body.appendChild(modal); 1266 var panel = modal.querySelector(".book-modal-panel"); 1267 1268 function openBook(b) { 1269 panel.innerHTML = 1270 '<button class="book-close" aria-label="Close">â</button>' + 1271 '<img src="/assets/img/book_covers/' + b.c + '.jpg" alt="' + esc(b.t) + ' cover">' + 1272 '<div class="book-meta">' + 1273 '<span class="chip purple">' + BOOK_GENRES[b.g] + '</span>' + 1274 '<h3>' + esc(b.t) + '</h3>' + 1275 '<p class="muted">' + esc(b.a) + ' · ' + b.y + '</p>' + 1276 '<div class="book-rating"><span class="on">' + bookStars(b.r).slice(0, b.r) + '</span>' + 1277 bookStars(b.r).slice(b.r) + '<span class="n">' + b.r + '/5</span></div>' + 1278 '<a class="btn btn-sm btn-ghost" target="_blank" rel="noopener" href="https://openlibrary.org/search?q=' + 1279 encodeURIComponent(b.t + " " + b.a) + '">Find the book</a>' + 1280 '</div>'; 1281 modal.classList.add("open"); 1282 } 1283 function closeBook() { modal.classList.remove("open"); } 1284 1285 modal.addEventListener("click", function (e) { 1286 if (e.target === modal || e.target.closest(".book-close")) closeBook(); 1287 }); 1288 document.addEventListener("keydown", function (e) { 1289 if (e.key === "Escape") closeBook(); 1290 });
1291 items.forEach(function (it) { 1292 it.el.addEventListener("click", function () { openBook(it.b); }); 1293 it.el.addEventListener("keydown", function (e) { 1294 if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openBook(it.b); } 1295 }); 1296 }); 1297 1298 /* anything on the page can open a book's panel: data-open-book="<cover-slug>" */ 1299 document.querySelectorAll("[data-open-book]").forEach(function (el) { 1300 var slug = el.getAttribute("data-open-book"), match = null; 1301 BOOKS.forEach(function (b) { if (b.c === slug) match = b; }); 1302 if (!match) return; 1303 el.addEventListener("click", function (e) { e.preventDefault(); openBook(match); }); 1304 }); 1305 1306 var genre = "all", query = ""; 1307 function apply() { 1308 var shown = 0; 1309 flipReorder(items, function () { 1310 items.forEach(function (it) { 1311 var ok = (genre === "all" || it.b.g === genre) && (!query || it.q.indexOf(query) !== -1); 1312 it.el.style.display = ok ? "" : "none"; 1313 if (ok) shown++; 1314 }); 1315 }); 1316 countEl.textContent = shown === total 1317 ? "// showing all " + total + " books" 1318 : "// showing " + shown + " of " + total; 1319 emptyEl.style.display = shown ? "none" : "block"; 1320 } 1321 1322 host.querySelector(".shelf-filters").addEventListener("click", function (e) { 1323 var btn = e.target.closest(".filter-btn"); 1324 if (!btn) return; 1325 host.querySelectorAll(".shelf-filters .filter-btn").forEach(function (b) { b.classList.remove("active"); }); 1326 btn.classList.add("active"); 1327 genre = btn.getAttribute("data-genre"); 1328 apply(); 1329 }); 1330 search.addEventListener("input", function () { 1331 query = search.value.toLowerCase().trim(); 1332 apply(); 1333 }); 1334 1335 apply(); 1336 } 1337 1338 function initCmdk() { 1339 var overlay = document.createElement("div"); 1340 overlay.id = "cmdk"; 1341 overlay.innerHTML = 1342 '<div class="cmdk-panel" role="dialog" aria-label="Command palette">' + 1343 '<input type="text" placeholder="Type to search â pages, papers, linksâ¦" aria-label="Search">' + 1344 '<div class="cmdk-list"></div></div>'; 1345 document.body.appendChild(overlay); 1346 1347 var input = overlay.querySelector("input"); 1348 var list = overlay.querySelector(".cmdk-list"); 1349 var sel = 0, filtered = CMD_ITEMS; 1350 1351 function render() { 1352 if (!filtered.length) { list.innerHTML = '<div class="cmdk-empty">no results_</div>'; return; } 1353 list.innerHTML = filtered.map(function (it, i) { 1354 return '<div class="cmdk-item' + (i === sel ? " sel" : "") + '" data-i="' + i + '">' + 1355 '<span class="k">' + it.k + "</span><span>" + it.label + "</span></div>"; 1356 }).join(""); 1357 var s = list.querySelector(".sel"); 1358 if (s && s.scrollIntoView) s.scrollIntoView({ block: "nearest" }); 1359 } 1360 function open() { overlay.classList.add("open"); input.value = ""; filtered = CMD_ITEMS; sel = 0; render(); input.focus(); } 1361 function close() { overlay.classList.remove("open"); } 1362 function go(item) { 1363 close(); 1364 if (item.href === "#warp") { 1365 WARP.target = 1; document.body.classList.add("warp"); toast("â¡ hyperdrive engaged"); 1366 setTimeout(function () { WARP.target = 0; document.body.classList.remove("warp"); }, 4500); 1367 return; 1368 } 1369 if (/^https?:|^mailto:/.test(item.href)) window.open(item.href, item.href.indexOf("mailto:") === 0 ? "_self" : "_blank"); 1370 else if (reducedMotion) location.href = item.href; 1371 else { document.body.classList.add("leaving"); setTimeout(function () { location.href = item.href; }, 300); } 1372 } 1373 1374 input.addEventListener("input", function () { 1375 var q = input.value.toLowerCase().trim(); 1376 filtered = CMD_ITEMS.filter(function (it) { return it.label.toLowerCase().indexOf(q) !== -1; }); 1377 sel = 0; render(); 1378 }); 1379 input.addEventListener("keydown", function (e) { 1380 if (e.key === "ArrowDown") { sel = Math.min(sel + 1, filtered.length - 1); render(); e.preventDefault(); } 1381 else if (e.key === "ArrowUp") { sel = Math.max(sel - 1, 0); render(); e.preventDefault(); } 1382 else if (e.key === "Enter" && filtered[sel]) go(filtered[sel]); 1383 }); 1384 list.addEventListener("click", function (e) { 1385 var item = e.target.closest(".cmdk-item"); 1386 if (item) go(filtered[+item.getAttribute("data-i")]); 1387 }); 1388 overlay.addEventListener("click", function (e) { if (e.target === overlay) close(); }); 1389 document.addEventListener("keydown", function (e) { 1390 var typing = /input|textarea/i.test(document.activeElement && document.activeElement.tagName); 1391 if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); overlay.classList.contains("open") ? close() : open(); } 1392 else if (e.key === "/" && !typing) { e.preventDefault(); open(); } 1393 else if (e.key === "Escape") close(); 1394 }); 1395 document.querySelectorAll(".cmdk-btn").forEach(function (b) { b.addEventListener("click", open); }); 1396 } 1397 1398 /* ====================================================================== 1399 BOOT 1400 ====================================================================== */ 1401 document.addEventListener("DOMContentLoaded", function () {
1402 initBackground(); 1403 initTransitions(); 1404 initCursor(); 1405 initNav(); 1406 initProgress(); 1407 initBookshelf(); // renders before the reveal/counter/tilt observers attach 1408 initReveal(); 1409 initScramble(); 1410 initTyping(); 1411 initCounters(); 1412 initSkillBars(); 1413 initTilt(); 1414 initMagnetic(); 1415 initSpotlight(); 1416 initHeroParallax(); 1417 initSpy(); 1418 initAbstracts(); 1419 initCopy(); 1420 initFilters(); 1421 initLightbox(); 1422 initCarousels(); 1423 initReadTime(); 1424 initArticleToc(); 1425 initClock(); 1426 initResearchMaps(); 1427 initCmdk(); 1428 initEggs(); 1429 }); 1430})();
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.