1/* Site behaviour: news, publications, and the photo playground. No dependencies. */ 2(function () { 3 "use strict"; 4 5 var S = window.SITE; 6 var root = document.documentElement; 7 var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 8 var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 9 10 function $(id) { return document.getElementById(id); } 11 function el(tag, attrs, children) { 12 var n = document.createElement(tag); 13 if (attrs) for (var k in attrs) { 14 if (k === "class") n.className = attrs[k]; 15 else if (k === "html") n.innerHTML = attrs[k]; 16 else if (k === "text") n.textContent = attrs[k]; 17 else n.setAttribute(k, attrs[k]); 18 } 19 (children || []).forEach(function (c) { if (c) n.appendChild(typeof c === "string" ? document.createTextNode(c) : c); }); 20 return n; 21 } 22 function setAccent(h, c) { 23 root.style.setProperty("--acc-h", h); 24 root.style.setProperty("--acc-c", c); 25 } 26 27 /* Radio-style groups built from buttons: click + arrow keys. */ 28 function radioGroup(container, onPick) { 29 container.addEventListener("click", function (e) { 30 var b = e.target.closest("[role=radio]"); 31 if (b && container.contains(b)) pick(b); 32 }); 33 container.addEventListener("keydown", function (e) { 34 var items = [].slice.call(container.querySelectorAll("[role=radio]")); 35 var i = items.indexOf(document.activeElement); 36 if (i < 0) return; 37 var d = (e.key === "ArrowRight" || e.key === "ArrowDown") ? 1 : (e.key === "ArrowLeft" || e.key === "ArrowUp") ? -1 : 0; 38 if (!d) return; 39 e.preventDefault(); 40 var next = items[(i + d + items.length) % items.length]; 41 next.focus(); 42 pick(next); 43 }); 44 function pick(b) { 45 container.querySelectorAll("[role=radio]").forEach(function (x) { 46 var on = x === b; 47 x.setAttribute("aria-checked", on ? "true" : "false"); 48 x.tabIndex = on ? 0 : -1; 49 }); 50 onPick(b); 51 } 52 container.querySelectorAll("[role=radio]").forEach(function (x) { 53 x.tabIndex = x.getAttribute("aria-checked") === "true" ? 0 : -1; 54 }); 55 return { pick: pick }; 56 } 57 58 /* ---------------------------------------------------------------- 59 News 60 ---------------------------------------------------------------- */ 61 (function news() { 62 var list = $("news-list"), more = $("news-more"); 63 var SHOW = 6; 64 S.news.forEach(function (n, i) { 65 var p = n.date.split("-"); 66 var li = el("li", { class: i >= SHOW ? "is-extra" : "" }, [ 67 el("time", { datetime: n.date, text: MONTHS[+p[1] - 1] + " " + p[0] }), 68 el("div", { class: "text", html: n.html }) 69 ]); 70 list.appendChild(li); 71 }); 72 if (S.news.length > SHOW) { 73 more.hidden = false; 74 var label = function (open) { more.textContent = open ? "Show fewer" : "Show all " + S.news.length + " items"; }; 75 label(false); 76 more.addEventListener("click", function () { 77 var open = list.classList.toggle("open"); 78 more.setAttribute("aria-expanded", open ? "true" : "false"); 79 label(open); 80 }); 81 } 82 })(); 83 84 /* ---------------------------------------------------------------- 85 Publications 86 ---------------------------------------------------------------- */ 87 (function pubs() { 88 var box = $("pubs"); 89 var canHover = window.matchMedia("(hover: hover)").matches; 90 var videos = []; 91 92 function name(a) { 93 var p = a.split(", "); 94 return p.length === 2 ? p[1] + " " + p[0] : a; 95 } 96 97 var years = []; 98 S.papers.forEach(function (p) { if (years.indexOf(p.year) < 0) years.push(p.year); }); 99 100 years.forEach(function (y) { 101 var group = el("div", { class: "year-group" }, [el("p", { class: "year", text: String(y) })]); 102 S.papers.filter(function (p) { return p.year === y; }).forEach(function (p) { 103 var first = p.authors[0] === S.me; 104 var href = p.links.project || p.links.paper || p.links.arXiv; 105 106 var thumb = el("div", { class: "thumb" }); 107 if (p.teaser && /\.mp4$/.test(p.teaser)) { 108 var v = el("video", { muted: "", loop: "", playsinline: "", preload: "none", poster: p.poster || "", "aria-hidden": "true" }); 109 v.muted = true; 110 v.appendChild(el("source", { src: p.teaser, type: "video/mp4" })); 111 thumb.appendChild(v); 112 thumb.appendChild(el("span", { class: "play-dot", "aria-hidden": "true" })); 113 videos.push({ v: v, t: thumb }); 114 if (canHover) { 115 thumb.addEventListener("pointerenter", function () { playV(v, thumb); }); 116 thumb.addEventListener("pointerleave", function () { pauseV(v, thumb); }); 117 } 118 } else if (p.teaser) { 119 thumb.appendChild(el("img", { src: p.teaser, alt: "", loading: "lazy" })); 120 } else { 121 thumb.classList.add("thumb-empty"); 122 thumb.appendChild(el("span", { text: p.venue })); 123 } 124 125 var authors = el("p", { class: "authors" });
126 p.authors.forEach(function (a, i) { 127 if (i) authors.appendChild(document.createTextNode(", ")); 128 authors.appendChild(a === S.me ? el("span", { class: "me", text: name(a) }) : document.createTextNode(name(a))); 129 }); 130 131 var links = el("ul", { class: "links" }); 132 Object.keys(p.links).forEach(function (k) { 133 links.appendChild(el("li", {}, [el("a", { href: p.links[k], text: k })])); 134 }); 135 136 var art = el("article", { class: "pub", "data-first": first ? "1" : "0" }, [ 137 thumb, 138 el("div", { class: "pub-body" }, [ 139 el("h3", {}, [href ? el("a", { href: href, text: p.title }) : p.title]), 140 authors, 141 el("p", { class: "venue" }, [ 142 p.short ? el("span", { class: "short", text: p.short + " · " }) : null, 143 p.venue 144 ]), 145 links 146 ]) 147 ]); 148 group.appendChild(art); 149 }); 150 box.appendChild(group); 151 }); 152 153 function playV(v, t) { 154 if (reduceMotion) return; 155 var pr = v.play(); 156 if (pr && pr.catch) pr.catch(function () {}); 157 t.classList.add("playing"); 158 } 159 function pauseV(v, t) { v.pause(); t.classList.remove("playing"); } 160 161 // Touch screens: play teasers while they are on screen. 162 if (!canHover && "IntersectionObserver" in window && !reduceMotion) { 163 var io = new IntersectionObserver(function (entries) { 164 entries.forEach(function (e) { 165 var item = videos.filter(function (x) { return x.t === e.target; })[0]; 166 if (!item) return; 167 if (e.isIntersecting && e.intersectionRatio > 0.6) playV(item.v, item.t); 168 else pauseV(item.v, item.t); 169 }); 170 }, { threshold: [0, 0.6, 1] }); 171 videos.forEach(function (x) { io.observe(x.t); }); 172 } 173 174 // Filter 175 var filter = $("pub-filter"); 176 var total = S.papers.length; 177 var firsts = S.papers.filter(function (p) { return p.authors[0] === S.me; }).length; 178 filter.querySelector('[data-f="all"]').innerHTML = 'All <span class="n">' + total + "</span>"; 179 filter.querySelector('[data-f="first"]').innerHTML = 'First author <span class="n">' + firsts + "</span>"; 180 radioGroup(filter, function (b) { 181 var onlyFirst = b.getAttribute("data-f") === "first"; 182 box.querySelectorAll(".year-group").forEach(function (g) { 183 var shown = 0; 184 g.querySelectorAll(".pub").forEach(function (a) { 185 var show = !onlyFirst || a.getAttribute("data-first") === "1"; 186 a.hidden = !show; 187 if (show) shown++; 188 }); 189 g.hidden = shown === 0; 190 }); 191 }); 192 })(); 193 194 /* ---------------------------------------------------------------- 195 Playground 196 ---------------------------------------------------------------- */ 197 var H = S.hero; 198 var play = $("play"), stage = $("stage"); 199 var frameA = $("frame-a"), original = $("original"); 200 var standin = $("standin"); 201 var mode = "nc"; 202 203 var capText = $("cap-text"), capLink = $("cap-link"); 204 function caption(cfg) { 205 capText.innerHTML = "<b>" + H.caption.replace(/^(Fig\. \d+\.)/, "$1</b>") + " " + cfg.caption; 206 if (cfg.link) { capLink.href = cfg.link; capLink.hidden = false; } else capLink.hidden = true; 207 } 208 209 /* Hold to compare with the original ------------------------------ */ 210 (function hold() { 211 var btn = $("hold"); 212 function on(e) { if (e) e.preventDefault(); stage.classList.add("comparing"); } 213 function off() { stage.classList.remove("comparing"); } 214 btn.addEventListener("pointerdown", function (e) { btn.setPointerCapture && btn.setPointerCapture(e.pointerId); on(e); }); 215 ["pointerup", "pointercancel", "lostpointercapture"].forEach(function (t) { btn.addEventListener(t, off); }); 216 btn.addEventListener("keydown", function (e) { if (e.key === " " || e.key === "Enter") on(e); }); 217 btn.addEventListener("keyup", off); 218 btn.addEventListener("blur", off); 219 btn.addEventListener("contextmenu", function (e) { e.preventDefault(); }); 220 })(); 221
222 /* Precomputed frames (SyncLight, UniSlider) ---------------------- 223 Frames are drawn on one canvas as a weighted blend of the nearest 224 renders, so the sliders move smoothly between them. */ 225 var fx = $("fx-canvas"), fctx = fx.getContext("2d"); 226 var cache = {}; 227 function frame(url) { 228 var im = cache[url]; 229 if (!im) { 230 im = cache[url] = new Image(); 231 im.onload = function () { im.ok = true; redraw(); }; 232 im.src = url; 233 } 234 return im.ok ? im : null; 235 } 236 function preload(urls) { 237 var i = 0; 238 (function next() { 239 for (var n = 0; n < 4 && i < urls.length; n++, i++) frame(urls[i]); 240 if (i < urls.length) setTimeout(next, 60); 241 })(); 242 } 243 // parts: [[url, weight], ...]. Weighted average via successive "over" blends. 244 function blend(parts) { 245 var ready = []; 246 parts.forEach(function (p) { if (p[1] > 0.001) { var im = frame(p[0]); if (im) ready.push([im, p[1]]); } }); 247 if (!ready.length) return; 248 var acc = 0; 249 ready.forEach(function (p, i) { 250 acc += p[1]; 251 fctx.globalAlpha = i === 0 ? 1 : p[1] / acc; 252 fctx.drawImage(p[0], 0, 0, fx.width, fx.height); 253 }); 254 fctx.globalAlpha = 1; 255 fx.classList.add("ready"); 256 } 257 function redraw() { 258 if (mode === "sl") renderSL(); 259 else if (mode === "us") renderUS(); 260 } 261 function hexMix(a, b, f) { 262 var pa = [1, 3, 5].map(function (i) { return parseInt(a.substr(i, 2), 16); }); 263 var pb = [1, 3, 5].map(function (i) { return parseInt(b.substr(i, 2), 16); }); 264 return "rgb(" + pa.map(function (v, i) { return Math.round(v + (pb[i] - v) * f); }).join(",") + ")"; 265 } 266 267 /* SyncLight: chromaticity x intensity ---------------------------- */ 268 var SL = H.synclight; 269 var slC = $("sl-c"), slI = $("sl-i"); 270 var colorIdx = SL.colors.map(function (c) { return c.id; }); 271 function slUrl(ci, ii) { 272 if (SL.intensities[ii] === 0 && SL.off) return SL.off; // lamps off: one shared frame 273 return "assets/hero/synclight/" + SL.colors[ci].id + "_" + ii + ".jpg"; 274 } 275 // Both sliders snap: one color and one intensity level at a time, one image shown. 276 slC.max = SL.colors.length - 1; 277 slC.value = Math.max(0, colorIdx.indexOf(SL.startColor)); 278 slI.max = SL.intensities.length - 1; 279 slI.value = Math.max(0, SL.intensities.indexOf(SL.startIntensity)); 280 // Track drawn as one block per stop, each block centred on the thumb's stop position. 281 function segments(colors) { 282 var n = colors.length, half = 9, at = function (f) { return "calc(" + half + "px + (100% - " + 2 * half + "px) * " + f.toFixed(4) + ")"; }; 283 return "linear-gradient(90deg," + colors.map(function (c, i) { 284 return c + " " + (i === 0 ? "0px" : at((i - 0.5) / (n - 1))) + " " + (i === n - 1 ? "100%" : at((i + 0.5) / (n - 1))); 285 }).join(",") + ")"; 286 } 287 slC.style.setProperty("--track", segments(SL.colors.map(function (c) { return c.swatch; })));
288 function renderSL() { 289 var ci = Math.round(+slC.value), ii = Math.round(+slI.value), C = SL.colors[ci]; 290 blend([[slUrl(ci, ii), 1]]); 291 $("sl-dot").style.background = C.swatch; 292 $("sl-c-name").textContent = C.label; 293 slI.style.setProperty("--track", segments(SL.intensities.map(function (v) { return hexMix("#1b1c1e", C.swatch, v); }))); 294 $("sl-i-out").textContent = Math.round(SL.intensities[ii] * 100) + "%"; 295 setAccent(C.hue, C.chroma); 296 } 297 function slAll() { 298 // nearest frames first, then the rest 299 var c = Math.round(+slC.value), urls = []; 300 for (var ii = 0; ii < SL.intensities.length; ii++) urls.push(slUrl(c, ii)); 301 for (var ci = 0; ci < SL.colors.length; ci++) if (ci !== c) for (ii = 0; ii < SL.intensities.length; ii++) urls.push(slUrl(ci, ii)); 302 return urls; 303 } 304 slC.addEventListener("input", renderSL); 305 slI.addEventListener("input", renderSL); 306 307 /* UniSlider ------------------------------------------------------ */ 308 var US = H.unislider, usPrompt = US.start; 309 var range = $("us-s"); 310 var promptText = $("us-prompt-text"), typing = 0; 311 function usUrl(id, k) { return "assets/hero/unislider/" + id + "/" + k + ".jpg"; } 312 function usCfg(id) { return US.prompts.filter(function (p) { return p.id === id; })[0]; } 313 function usSteps(id) { return usCfg(id).steps || US.steps; } 314 function usFrames(id) { var a = []; for (var k = 0; k <= usSteps(id); k++) a.push(usUrl(id, k)); return a; } 315 (function buildPrompts() { 316 var box = $("prompts");
317 US.prompts.forEach(function (P) { 318 box.appendChild(el("button", { 319 type: "button", role: "radio", class: "prompt", 320 "aria-checked": P.id === US.start ? "true" : "false", 321 "data-id": P.id, text: P.label || P.text, title: P.text 322 })); 323 }); 324 radioGroup(box, function (b) { setPrompt(b.getAttribute("data-id"), true); }); 325 })(); 326 // Show the instruction; typed out when the visitor picks a new one. 327 function showPrompt(text, animate) { 328 var token = ++typing; 329 if (!animate || reduceMotion) { promptText.textContent = text; promptText.classList.remove("typing"); return; } 330 var i = 0; 331 promptText.classList.add("typing"); 332 (function tick() { 333 if (token !== typing) return; 334 promptText.textContent = text.slice(0, ++i); 335 if (i < text.length) setTimeout(tick, 28); 336 else promptText.classList.remove("typing"); 337 })(); 338 } 339 function setPrompt(id, animate) { 340 var changed = id !== usPrompt; 341 usPrompt = id; 342 range.max = usSteps(id); 343 preload(usFrames(id)); 344 showPrompt(usCfg(id).text, animate && changed); 345 renderUS(); 346 } 347 // The slider snaps to the rendered frames (equal perceptual steps), one image at a time. 348 function renderUS() { 349 var P = usCfg(usPrompt), n = usSteps(usPrompt); 350 var k = Math.max(0, Math.min(n, Math.round(+range.value))); 351 blend([[usUrl(usPrompt, k), 1]]); 352 setAccent(P.hue, P.chroma); 353 } 354 range.max = usSteps(usPrompt); 355 range.value = US.startStep != null ? US.startStep : Math.round(usSteps(usPrompt) / 2); 356 range.addEventListener("input", renderUS); 357 358 /* Mode switching ------------------------------------------------- */ 359 var MODE = { unislider: "us", synclight: "sl", namedcurves: "nc" }; 360 var order = (H.order || ["unislider", "synclight", "namedcurves"]).map(function (k) { return MODE[k]; }); 361 var tablist = play.querySelector("[role=tablist]"); 362 order.forEach(function (m) { tablist.appendChild(tablist.querySelector('[data-mode="' + m + '"]')); }); 363 var tabs = [].slice.call(tablist.querySelectorAll("[role=tab]")); 364 function setMode(m, focus) { 365 mode = m; 366 play.setAttribute("data-mode", m);
367 tabs.forEach(function (t) { 368 var on = t.getAttribute("data-mode") === m; 369 t.setAttribute("aria-selected", on ? "true" : "false"); 370 t.tabIndex = on ? 0 : -1; 371 if (on && focus) t.focus(); 372 }); 373 ["nc", "sl", "us"].forEach(function (k) { 374 var pn = $("panel-" + k), off = k !== m; 375 pn.classList.toggle("is-off", off); 376 if (off) pn.setAttribute("inert", ""); else pn.removeAttribute("inert"); 377 }); 378 fx.classList.remove("ready"); 379 if (m === "nc") NC.activate(); 380 else { 381 NC.deactivate(); 382 if (m === "sl") { renderSL(); preload(slAll()); } 383 if (m === "us") setPrompt(usPrompt, false); 384 } 385 var cfg = m === "sl" ? SL : m === "us" ? US : H.namedcurves; 386 standin.hidden = !cfg.placeholder; 387 caption(cfg); 388 } 389 tabs.forEach(function (t) { 390 t.addEventListener("click", function () { setMode(t.getAttribute("data-mode")); }); 391 t.addEventListener("keydown", function (e) { 392 var i = tabs.indexOf(t), d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0; 393 if (!d) return; 394 e.preventDefault(); 395 setMode(tabs[(i + d + tabs.length) % tabs.length].getAttribute("data-mode"), true); 396 }); 397 }); 398 399 /* ---------------------------------------------------------------- 400 NamedCurves, live 401 Colour naming: van de Weijer et al. (2009), 11 names grouped into 402 the 6 NamedCurves categories. Each category has one Bezier tone curve 403 per RGB channel; the output is the curve responses blended by the 404 per-pixel colour-name probabilities. 405 ---------------------------------------------------------------- */ 406 var NC = (function () { 407 var CATS = [ 408 { name: "orange · brown · yellow", sw: "linear-gradient(90deg,#e38b2c 0 33%,#8b5a2b 33% 66%,#f2d03b 66%)", hue: 62, chroma: 0.12 }, 409 { name: "achromatic", sw: "linear-gradient(90deg,#161616 0 33%,#8a8a8a 33% 66%,#fbfbfb 66%)", hue: 250, chroma: 0 }, 410 { name: "pink · purple", sw: "linear-gradient(90deg,#f29bbf 0 50%,#8b4fb0 50%)", hue: 330, chroma: 0.13 }, 411 { name: "red", sw: "#d62f2d", hue: 27, chroma: 0.15 }, 412 { name: "green", sw: "#3f9b46", hue: 145, chroma: 0.13 }, 413 { name: "blue", sw: "#2f5fd0", hue: 262, chroma: 0.14 } 414 ]; 415 var NPTS = 5; 416 var ID = [0, 0.25, 0.5, 0.75, 1]; 417 var BIN = [1, 4, 6, 4, 1]; 418 419 // curves[cat][channel] = control-point heights; x positions are uniform. 420 var curves = CATS.map(function () { return [ID.slice(), ID.slice(), ID.slice()]; }); 421 // Start with a small warm lift on orange-brown-yellow so the edit is visible at rest. 422 curves[0] = [[0, 0.31, 0.6, 0.82, 1], [0, 0.28, 0.55, 0.79, 1], [0, 0.22, 0.46, 0.72, 1]]; 423 424 var cat = 0, chan = "rgb"; 425 var lut = null; // Uint8Array(32768 * 6) 426 var canvas = $("nc-canvas"), ctx = canvas.getContext("2d", { willReadFrequently: true }); 427 var src = null, outImg = null, probs = null, N = 0; 428 var ownPhoto = false, pending = false, showMap = false, ready = false; 429 430 function decodeLut() { 431 if (lut || !window.W2C6) return; 432 var bin = atob(window.W2C6); 433 lut = new Uint8Array(bin.length); 434 for (var i = 0; i < bin.length; i++) lut[i] = bin.charCodeAt(i); 435 } 436 437 function curveDelta(ys) { 438 var d = new Float32Array(256); 439 for (var v = 0; v < 256; v++) { 440 var t = v / 255, u = 1 - t, y = 0; 441 for (var j = 0; j < NPTS; j++) y += BIN[j] * Math.pow(t, j) * Math.pow(u, NPTS - 1 - j) * ys[j]; 442 d[v] = Math.max(0, Math.min(255, y * 255)) - v; 443 } 444 return d; 445 } 446 function isIdentity(ys) { for (var j = 0; j < NPTS; j++) if (Math.abs(ys[j] - ID[j]) > 1e-4) return false; return true; } 447 function catEdited(k) { return !(isIdentity(curves[k][0]) && isIdentity(curves[k][1]) && isIdentity(curves[k][2])); } 448 449 function loadImage(img) { 450 decodeLut(); 451 var maxW = 720, w = img.naturalWidth || img.width, h = img.naturalHeight || img.height; 452 if (w > maxW) { h = Math.round(h * maxW / w); w = maxW; } 453 canvas.width = w; canvas.height = h; 454 ctx.drawImage(img, 0, 0, w, h); 455 src = ctx.getImageData(0, 0, w, h); 456 outImg = ctx.createImageData(w, h); 457 N = w * h; 458 probs = new Uint8Array(N * 6); 459 var d = src.data; 460 for (var i = 0, p = 0, q = 0; i < N; i++, p += 4, q += 6) { 461 var b = ((d[p] >> 3) + ((d[p + 1] >> 3) << 5) + ((d[p + 2] >> 3) << 10)) * 6; 462 probs[q] = lut[b]; probs[q + 1] = lut[b + 1]; probs[q + 2] = lut[b + 2]; 463 probs[q + 3] = lut[b + 3]; probs[q + 4] = lut[b + 4]; probs[q + 5] = lut[b + 5]; 464 } 465 ready = true; 466 render(); 467 canvas.classList.add("ready"); 468 } 469 470 function render() { 471 pending = false;
472 if (!ready) return; 473 var active = []; 474 for (var k = 0; k < 6; k++) if (catEdited(k)) active.push({ k: k, r: curveDelta(curves[k][0]), g: curveDelta(curves[k][1]), b: curveDelta(curves[k][2]) }); 475 var d = src.data, o = outImg.data, na = active.length; 476 var bg = getComputedStyle(root).getPropertyValue("color-scheme").indexOf("dark") >= 0 ? 34 : 236; 477 for (var i = 0, p = 0, q = 0; i < N; i++, p += 4, q += 6) { 478 var r = d[p], g = d[p + 1], b = d[p + 2], dr = 0, dg = 0, db = 0; 479 for (var a = 0; a < na; a++) { 480 var A = active[a], w = probs[q + A.k]; 481 if (w) { dr += w * A.r[r]; dg += w * A.g[g]; db += w * A.b[b]; } 482 } 483 r += dr / 255; g += dg / 255; b += db / 255; 484 if (showMap) { 485 var m = probs[q + cat] / 255, y = (0.3 * r + 0.59 * g + 0.11 * b) * 0.3 + bg * 0.7; 486 r = r * m + y * (1 - m); g = g * m + y * (1 - m); b = b * m + y * (1 - m); 487 } 488 o[p] = r; o[p + 1] = g; o[p + 2] = b; o[p + 3] = 255; 489 } 490 ctx.putImageData(outImg, 0, 0); 491 } 492 function schedule() { if (!pending) { pending = true; requestAnimationFrame(render); } } 493 494 /* Color-name chips */ 495 var catBox = $("cats"), catName = $("cat-name"); 496 CATS.forEach(function (c, k) { 497 catBox.appendChild(el("button", { 498 type: "button", role: "radio", class: "cat", "data-k": k, 499 "aria-checked": k === cat ? "true" : "false", 500 "aria-label": c.name, title: c.name, 501 style: "background:" + c.sw 502 }, [el("span", { class: "edited", "aria-hidden": "true" })])); 503 }); 504 function showName() { catName.innerHTML = CATS[cat].name + (catEdited(cat) ? " <span>· edited</span>" : ""); } 505 function markEdited() { 506 catBox.querySelectorAll(".cat").forEach(function (b) { b.classList.toggle("is-edited", catEdited(+b.getAttribute("data-k"))); }); 507 showName(); 508 } 509 radioGroup(catBox, function (b) { 510 cat = +b.getAttribute("data-k"); 511 setAccent(CATS[cat].hue, CATS[cat].chroma); 512 drawCurve(); 513 if (showMap) schedule(); 514 }); 515 radioGroup($("chan"), function (b) { chan = b.getAttribute("data-ch"); drawCurve(); }); 516 517 /* Curve editor */ 518 var cv = $("curve"), cctx = cv.getContext("2d"); 519 var sel = 2, dragging = false, hasFocus = false; 520 function sizeCurve() { 521 var css = cv.getBoundingClientRect().width || 224, dpr = window.devicePixelRatio || 1; 522 cv.width = Math.round(css * dpr); cv.height = Math.round(css * dpr); 523 cctx.setTransform(dpr, 0, 0, dpr, 0, 0); 524 drawCurve(); 525 } 526 function tok(name) { return getComputedStyle(root).getPropertyValue(name).trim(); } 527 function shownYs() { 528 var c = curves[cat]; 529 if (chan !== "rgb") return c[+chan]; 530 return ID.map(function (_, j) { return (c[0][j] + c[1][j] + c[2][j]) / 3; }); 531 } 532 function drawCurve() { 533 var s = cv.getBoundingClientRect().width || 224, pad = 8, W = s - 2 * pad; 534 var X = function (x) { return pad + x * W; }, Y = function (y) { return pad + (1 - y) * W; }; 535 cctx.clearRect(0, 0, s, s); 536 cctx.fillStyle = tok("--surface"); cctx.fillRect(0, 0, s, s); 537 cctx.strokeStyle = tok("--rule"); cctx.lineWidth = 1; 538 for (var i = 0; i <= 4; i++) { 539 cctx.beginPath(); cctx.moveTo(X(i / 4), Y(0)); cctx.lineTo(X(i / 4), Y(1)); cctx.stroke(); 540 cctx.beginPath(); cctx.moveTo(X(0), Y(i / 4)); cctx.lineTo(X(1), Y(i / 4)); cctx.stroke(); 541 } 542 cctx.setLineDash([3, 3]); cctx.strokeStyle = tok("--rule-2"); 543 cctx.beginPath(); cctx.moveTo(X(0), Y(0)); cctx.lineTo(X(1), Y(1)); cctx.stroke(); 544 cctx.setLineDash([]); 545 546 var c = curves[cat]; 547 var same = isIdentity(c[0].map(function (v, j) { return v - c[1][j] + ID[j]; })) && isIdentity(c[0].map(function (v, j) { return v - c[2][j] + ID[j]; })); 548 function bez(ys, color, width) { 549 cctx.strokeStyle = color; cctx.lineWidth = width; cctx.beginPath(); 550 for (var v = 0; v <= 64; v++) { 551 var t = v / 64, u = 1 - t, y = 0; 552 for (var j = 0; j < NPTS; j++) y += BIN[j] * Math.pow(t, j) * Math.pow(u, NPTS - 1 - j) * ys[j]; 553 v ? cctx.lineTo(X(t), Y(y)) : cctx.moveTo(X(t), Y(y)); 554 } 555 cctx.stroke(); 556 } 557 var CH = ["#d8453b", "#3a9a4a", "#3d6bd6"]; 558 if (chan === "rgb") { 559 if (same) bez(c[0], tok("--ink"), 2); 560 else [0, 1, 2].forEach(function (k) { bez(c[k], CH[k], 1.75); }); 561 } else { 562 [0, 1, 2].forEach(function (k) { if (k !== +chan) bez(c[k], tok("--rule-2"), 1.25); }); 563 bez(c[+chan], CH[+chan], 2); 564 } 565 // control polygon + points 566 var ys = shownYs(); 567 cctx.setLineDash([2, 3]); cctx.strokeStyle = tok("--ink-3"); cctx.lineWidth = 1; 568 cctx.beginPath(); ys.forEach(function (y, j) { j ? cctx.lineTo(X(ID[j]), Y(y)) : cctx.moveTo(X(ID[j]), Y(y)); }); cctx.stroke(); 569 cctx.setLineDash([]); 570 ys.forEach(function (y, j) { 571 var active = (dragging || hasFocus) && j === sel; 572 cctx.beginPath(); cctx.arc(X(ID[j]), Y(y), active ? 6 : 4.5, 0, Math.PI * 2); 573 cctx.fillStyle = active ? tok("--ink") : tok("--surface"); cctx.fill(); 574 cctx.strokeStyle = tok("--ink"); cctx.lineWidth = 1.5; cctx.stroke(); 575 }); 576 markEdited(); 577 } 578 function setPoint(j, y) { 579 y = Math.max(0, Math.min(1, y)); 580 var c = curves[cat]; 581 if (chan === "rgb") { 582 var dy = y - shownYs()[j]; 583 for (var k = 0; k < 3; k++) c[k][j] = Math.max(0, Math.min(1, c[k][j] + dy)); 584 } else c[+chan][j] = y; 585 drawCurve(); schedule(); 586 } 587 function local(e) { 588 var r = cv.getBoundingClientRect(), pad = 8, W = r.width - 2 * pad; 589 return { x: (e.clientX - r.left - pad) / W, y: 1 - (e.clientY - r.top - pad) / W }; 590 } 591 cv.addEventListener("pointerdown", function (e) { 592 var p = local(e); 593 sel = Math.max(0, Math.min(NPTS - 1, Math.round(p.x * (NPTS - 1)))); 594 dragging = true; 595 cv.setPointerCapture(e.pointerId); 596 setPoint(sel, p.y); 597 e.preventDefault(); 598 }); 599 cv.addEventListener("pointermove", function (e) { if (dragging) setPoint(sel, local(e).y); }); 600 ["pointerup", "pointercancel"].forEach(function (t) { cv.addEventListener(t, function () { dragging = false; drawCurve(); }); }); 601 cv.addEventListener("focus", function () { hasFocus = true; drawCurve(); }); 602 cv.addEventListener("blur", function () { hasFocus = false; drawCurve(); }); 603 cv.addEventListener("keydown", function (e) { 604 var step = e.shiftKey ? 0.1 : 0.02; 605 if (e.key === "ArrowLeft") sel = Math.max(0, sel - 1); 606 else if (e.key === "ArrowRight") sel = Math.min(NPTS - 1, sel + 1); 607 else if (e.key === "ArrowUp") { setPoint(sel, shownYs()[sel] + step); } 608 else if (e.key === "ArrowDown") { setPoint(sel, shownYs()[sel] - step); } 609 else return; 610 e.preventDefault(); drawCurve(); 611 }); 612 613 $("nc-reset").addEventListener("click", function () { 614 curves = CATS.map(function () { return [ID.slice(), ID.slice(), ID.slice()]; }); 615 drawCurve(); schedule(); 616 }); 617 $("show-map").addEventListener("change", function (e) { showMap = e.target.checked; schedule(); }); 618 619 /* Own photo */ 620 var mine = $("my-photo"); 621 $("own-photo").addEventListener("change", function (e) { 622 var f = e.target.files && e.target.files[0]; 623 if (!f) return; 624 var url = URL.createObjectURL(f), img = new Image(); 625 img.onload = function () { 626 ownPhoto = true; 627 stage.classList.add("own"); 628 original.src = url; 629 loadImage(img); 630 mine.hidden = false;
631 }; 632 img.src = url; 633 e.target.value = ""; 634 }); 635 function backToMine() { 636 ownPhoto = false; 637 stage.classList.remove("own"); 638 original.src = H.photo; 639 mine.hidden = true; 640 loadPhoto(); 641 } 642 mine.addEventListener("click", backToMine); 643 644 var photoImg = null; 645 function loadPhoto() { 646 if (photoImg && photoImg.complete && photoImg.naturalWidth) { loadImage(photoImg); return; } 647 photoImg = new Image(); 648 photoImg.onload = function () { if (!ownPhoto) loadImage(photoImg); }; 649 photoImg.src = H.photo; 650 } 651 652 // Re-render the curve (and the map background) when the theme flips. 653 var mq = window.matchMedia("(prefers-color-scheme: dark)"); 654 (mq.addEventListener ? mq.addEventListener.bind(mq, "change") : mq.addListener.bind(mq))(function () { drawCurve(); if (showMap) schedule(); }); 655 new MutationObserver(function () { drawCurve(); if (showMap) schedule(); }).observe(root, { attributes: true, attributeFilter: ["data-theme"] }); 656 window.addEventListener("resize", sizeCurve); 657 658 sizeCurve(); 659 loadPhoto(); 660 661 return { 662 activate: function () { setAccent(CATS[cat].hue, CATS[cat].chroma); }, 663 deactivate: function () { if (ownPhoto) backToMine(); } 664 }; 665 })(); 666 667 setMode(order[0]); 668 669 /* Header shows the name only once the big one has scrolled away. */ 670 (function headerName() { 671 var bar = $("topbar"), h1 = $("name"); 672 if (!("IntersectionObserver" in window) || !h1) return; 673 new IntersectionObserver(function (e) { 674 bar.classList.toggle("name-off", e[0].isIntersecting); 675 }, { rootMargin: "-60px 0px 0px 0px" }).observe(h1); 676 })(); 677})();
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.