"use strict"; const fieldFuncDefault = (value) => { return [Text(value)]; }; function makeId(d) { return `${d.test}/${d.channel}`; } function rowAttribs(d) { return d.fail ? {"class": "fail"} : null; } function sectionLink(value, d) { const id = makeId(d); const a = Tag('a', {href: '#'+id}, value); //a.addEventListener('click', () => { $(id).setAttribute('open', true); }); return [a]; } // fonts: Twemoji, Noto, OpenMoji const emojiFontReplacementTable = { "๐ŸŒ‘":"Twemoji", "โ˜€":"Noto", "๐Ÿงฒ":"Noto", "โ˜ข":"Noto", "๐Ÿ”ฆ":"OpenMoji", "๐ŸŽ†":"Noto", "๐Ÿ’Ž":"Noto", "๐Ÿ€":"Noto", "๐Ÿ–Œ":"Noto", //"โŒ":"Noto", "๐Ÿ":"Noto", "๐Ÿ›‘":"Noto", "โ›”":"Noto", } function emojiFont(emoji) { let emojiFontReplacement = emojiFontReplacementTable[emoji]; emojiFontReplacement = emojiFontReplacement ? " " + emojiFontReplacement : ""; return emojiFontReplacement; } function emojiDetect(string) { const regex_emoji = /\p{Extended_Pictographic}/gu; // it's not complete but it's enough for case const matches = [...string.matchAll(regex_emoji)]; return matches; } function emojiToTag(string) { const results = emojiDetect(string); let lastIndex = 0; const tagArray = []; for (let result of results) { let emoji = result[0]; if (result.index > lastIndex) { tagArray.push(Text(string.slice(lastIndex, result.index))); } tagArray.push(Tag('span', {"class": "emoji" + emojiFont(emoji)}, emoji)); lastIndex = result.index + emoji.length; } if (lastIndex < string.length) { tagArray.push(Text(string.slice(lastIndex))); } return tagArray; } function channelWithEmoji(value) { const replacements = { "full": "๐Ÿ–ผ", "basecolor": "๐ŸŽจ", "emissive": "โ˜ข", "nshade": "๐Ÿฅฌ", //๐Ÿ”๐ŸŒ„ "ngeom": "๐Ÿฅ’", //โ›ฐ "lighting": "โ˜€",//๐Ÿ—ฟ "direct": "๐Ÿ”ฆ",//๐Ÿ”ฆ๐ŸŽ‡โ˜„ "direct_diffuse": "๐Ÿ”ฆ๐ŸŽ†", "direct_specular": "๐Ÿ”ฆ๐Ÿ’Ž", "diffuse": "๐ŸŽ†", "specular": "๐Ÿ’Ž", "material": "๐Ÿ–Œ", "indirect": "๐Ÿ€",//๐Ÿ€๐Ÿ“๐Ÿฅ "indirect_specular": "๐Ÿ€๐Ÿ’Ž", "indirect_diffuse": "๐Ÿ€๐ŸŽ†" } const emoji = replacements[value]; if (emoji) { return [ ...emojiToTag(emoji), Tag("span", {"class": "text-left-margin"}, value) ]; } else { return []; } } function colorPct(value) { let fixedPercent = value.toFixed(3); //fixedPercent = (fixedPercent < 10 ? '0' : '') + fixedPercent; let css_class = ""; if (value < 1) { css_class = "green"; } else if (value < 5) { css_class = "yellow"; } else { css_class = "red"; } return [Tag("span", {"class": css_class}, fixedPercent)]; } function isPassed(value) { const font = value ? emojiFont("โŒ") : emojiFont("โœ”"); return [ Tag("span", {"class": "emoji" + font}, value ? "โŒ" : "โœ…" // โœ”โœ… ) ]; } function makeTable(fields, data, attrs_func) { const table = Tag('table', null, null, [ Tag('tr', null, null, (() => { let tds = []; for (const f of fields) { tds.push(Tag('th', {"class": "table-sticky"}, null, [Text(f.label)])); } return tds; })()), ]); for (const di in data) { let d = data[di]; const attrs = attrs_func ? attrs_func(d) : null; table.appendChild(Tag('tr', attrs, null, (() => { let ret = []; for (const f of fields) { const value = d[f.field]; if (value === undefined) { ret.push(Tag('td')); continue; } const field_func = f.tags_func ? f.tags_func : fieldFuncDefault; const tags = field_func(value, d); // if (f.good && f.bad) { // const rating = linearStep(value, f.good, f.bad); // const c = { // r: 224 + 31 * clamp(2 - rating * 2, 0, 1), // g: 224 + 31 * clamp(rating * 2, 0, 1), // b: 224, // }; // attrs = {style: `background-color: rgb(${c.r}, ${c.g}, ${c.b})`}; // } ret.push(Tag('td', null, null, tags)); } return ret; })())); } return table; } function buildTestResultsTable(data) { const fields = [ {label: 'Test', field: 'test', tags_func: sectionLink}, {label: 'Channel', field: 'channel', tags_func: channelWithEmoji}, {label: 'ฮ”, %', field: 'diff_pct', tags_func: colorPct}, {label: 'Passed', field: 'fail', tags_func: isPassed}, ]; return [makeTable(fields, data, rowAttribs)]; } // Filter out all success function filterData(data) { return data .filter((d) => { return d.diff_pct !== 0; }) .sort((l, r) => { return l.diff_pct < r.diff_pct ? 1 : -1; // RTFM }); } function buildSummary(data) { const total = data.length; const fails = data.filter((d) => { return !!d.fail; }).length; const fails_pct = fails * 100.0 / total; return emojiToTag(`๐Ÿงช Tests ๐Ÿ’ฅ: ${fails}/${total}๐Ÿ (โš ${fails_pct.toFixed(3)}%)`); } function buildTestResultImages(data) { return data.flatMap((d) => { //return Tag('details', {id: makeId(d)}, null, [ return Tag('div', {id: makeId(d), "class": "meta-block"}, null, [ //Tag('summary', null, `${d.test}/${d.channel} ฮด=${d.diff_pct}`), Tag("a", {"class": "text-link", "href": `#${d.test}/${d.channel}`}, null, [ Tag('h3', null, `${d.test}/${d.channel} ฮด=${d.diff_pct}`) ]), Tag('div', {"class": "image-container"}, null, [ Tag('div', {"class": "block block-gold"}, null, [Tag('img', {src: d.image_gold, loading: "lazy", "class": "gold"})]), Tag('div', {"class": "block block-test"}, null, [Tag('img', {src: d.image_test, loading: "lazy", "class": "test"})]), Tag('div', {"class": "block block-diff"}, null, [Tag('img', {src: d.image_diff, loading: "lazy", "class": "diff"})]), Tag('div', {"class": "separator"}, null, null), ]) //Tag('img', {src: d.image_flip}), ]); }); } function buildData(table, images, data, sort, filter) { if (sort) { data = filterData(data); } if (filter) { data = data.filter((d) => { const test = d.test.includes(filter); const channel = d.channel.includes(filter); return test || channel; }) } table.replaceChildren(...buildTestResultsTable(data)); images.replaceChildren(...buildTestResultImages(data)); for (let block of images.querySelectorAll(".image-container")) { block.addEventListener("click", function() { let diffElement = this.querySelector(".block-diff"); let goldElement = this.querySelector(".block-gold"); diffElement.classList.toggle("show_diff"); goldElement.classList.toggle("anim_force_off"); }); }; for (let tr of document.querySelectorAll("table tr")) { tr.addEventListener("click", function() { this.querySelector("a").click(); }); } saveToLocalStorage("rendertest_tablesort", sort); } const buildDataSlowMode = debounce((table, images, data, sort, filter) => { buildData(table, images, data, sort, filter); }, ()=>{}, 250); const saveToLocalStorage = debounce((key, value) => { localStorage.setItem(key, value); }, ()=>{}, 250); const loadFromLocalStorage = (key, default_option) => { const value = localStorage.getItem(key); if (isNaN(value)) { switch(value) { // ugly textual localStorage case "true": return true; case "false": return false; case "null": return default_option ? default_option : null; default: return value; } } else if (value === null) { return default_option ? default_option : null; } return parseFloat(value); // fix !!"0" = true } function uglyChecked(value) { // fast hack return value ? {"checked": "checked"} : {}; } window.onload = () => { const theme = loadFromLocalStorage("rendertest_theme", ""); const linkElements = document.querySelectorAll('link[rel^="stylesheet"][data-theme]'); function updateLinkRel(linkElements, id) { for (const link of linkElements) { const themeName = link.getAttribute("data-theme"); link.disabled = themeName !== id; link.rel = themeName === id && link.rel.includes("alternate") ? link.rel.replace(" alternate", "") : link.rel + " alternate"; } } let themes = []; for (const linkElement of linkElements) { const themeName = linkElement.getAttribute("data-theme"); const emojiTags = emojiToTag(linkElement.title); let checked = uglyChecked(!linkElement.rel.includes("alternate")); if (theme) { checked = uglyChecked(theme === themeName); } themes.push( Tag("label", null, null, [ Tag("input", {"type": "radio", "class": "theme-button", "id": themeName, "name": "theme", ...checked}, null, null, "click", (e) => { updateLinkRel(linkElements, e.target.id); saveToLocalStorage("rendertest_theme", e.target.id); }), ...emojiTags ]) ) }; function changeAnimationDuration(newDuration) { document.documentElement.style.setProperty('--animation-duration', newDuration + 'ms'); } function syncSliderValues(event) { changeAnimationDuration(event.target.value); const value = event.target.value; const input = event.target; const otherInput = input === rangeInput ? numberInput : rangeInput; otherInput.value = value; saveToLocalStorage("rendertest_switchfrequency", value); } let gridContainer, sidebarOptions, numberInput, rangeInput, table, images, vpaddin_input; const switchFrequency = loadFromLocalStorage("rendertest_switchfrequency", 700); const tableSort = loadFromLocalStorage("rendertest_tablesort", false); const sidebarPos = loadFromLocalStorage("rendertest_sidebarpos", "left"); const sidebarMode = loadFromLocalStorage("rendertest_sidebarmode", "minimize"); const sidebarSize = loadFromLocalStorage("rendertest_sidebarsize", "default"); const vpadding = loadFromLocalStorage("rendertest_vpadding", true); const all_diff = loadFromLocalStorage("rendertest_all_diff", false); const diffMode = loadFromLocalStorage("rendertest_diffmode", "click"); const imageCompareMode = loadFromLocalStorage("rendertest_imagecomparemode", "switch"); const imagePosition = loadFromLocalStorage("rendertest_imageposition", "center"); let isPaused = loadFromLocalStorage("rendertest_paused", false); if (theme) { updateLinkRel(linkElements, theme); } // TODO: simplification and bindings gridContainer = Tag("div", {"class": "grid-container"}, null, [ Tag("div", {"class": "sidebar"}, null, [ sidebarOptions = Tag("div", {"class": "panel", "id": "options"}, null, [ Tag("div", {"class": "theme-options"}, null, [ Tag("b", null, null, [ Tag("span", {"class": "emoji"}, "๐ŸŽจ"), Text(" Theme") ]), ...themes ]), Tag("div", null, null, [ Tag("b", null, null, [ Tag("span", {"class": "emoji"}, "๐ŸŒˆ"), Text(" Show diff mode on") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "diff_mode", "value": "click", ...uglyChecked(diffMode === "click")}, null, null, "input", (e) => { images.classList.toggle("diff_separate"); saveToLocalStorage("rendertest_diffmode", e.target.value); }), Tag("span", {"class": "emoji"}, "๐Ÿ–ฑ"), Text(" Click (LMB)") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "diff_mode", "value": "separate", ...uglyChecked(diffMode === "separate")}, null, null, "input", (e) => { images.classList.toggle("diff_separate"); saveToLocalStorage("rendertest_diffmode", e.target.value); }), Tag("span", {"class": "emoji"}, "๐ŸŽ"), Text(" Separate") ]) ]), Tag("div", null, null, [ Tag("b", null, null, [ Tag("span", {"class": "emoji"}, "๐Ÿคผ"), Text(" Image compare mode") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "compare_mode", "value": "switch", ...uglyChecked(imageCompareMode === "switch")}, null, null, "input", (e) => { images.classList.toggle("split_compare"); saveToLocalStorage("rendertest_imagecomparemode", e.target.value); for (let blocks of images.querySelectorAll(".block-gold")) { blocks.removeAttribute("style"); // FIXME } for (let seps of images.querySelectorAll(".separator")) { seps.removeAttribute("style"); // FIXME } }), Tag("span", {"class": "emoji"}, "๐ŸŽž"), Text(" Switch images") ]), Tag("label", {"title": "under construction"}, null, [ Tag("input", {"type": "radio", "name": "compare_mode", "value": "split", ...uglyChecked(imageCompareMode === "split")}, null, null, "input", (e) => { images.classList.toggle("split_compare"); saveToLocalStorage("rendertest_imagecomparemode", e.target.value); }), Tag("span", {"class": "emoji"}, "๐Ÿช“"), Text(" Split images") ]) ]), Tag("div", null, null, [ Tag("b", null, null, [ Tag("span", {"class": "emoji"}, "๐ŸŽข"), Text(" Speed switch image") ]), rangeInput = Tag("input", {"type": "range", "style": "width: 140px;", "min": "10", "max": "1000", "value": switchFrequency}, null, null, "input", syncSliderValues), numberInput = Tag("input", {"type": "number", "size": "5", "min": "10", "max": "1000", "value": switchFrequency}, null, null, "input", syncSliderValues), Text(" ms "), Tag("button", null, isPaused ? "resume" : "pause", null, "click", (e) => { //e.target.textContent = isPaused ? "โ„ pause" : "๐Ÿ”ฅ resume"; e.target.textContent = isPaused ? "pause" : "resume"; changeAnimationDuration(isPaused ? numberInput.value : 0); isPaused = !isPaused; saveToLocalStorage("rendertest_paused", isPaused); }) ]), /* future Tag("details", null, null, [ Tag("summary", null, "Advanced options"), ]), */ Tag("div", null, null, [ Tag("b", null, null, [ Tag("span", {"class": "emoji"}, "๐Ÿงป"), Text(" Sidebar") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "sidebar-mode", "value": "minimize", ...uglyChecked(sidebarMode === "minimize")}, null, null, "input", (e) => { sidebarOptions.classList.toggle("sticky"); saveToLocalStorage("rendertest_sidebarmode", e.target.value); }), Text(" auto minimize") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "sidebar-mode", "value": "always", ...uglyChecked(sidebarMode === "always")}, null, null, "input", (e) => { sidebarOptions.classList.toggle("sticky"); saveToLocalStorage("rendertest_sidebarmode", e.target.value); }), Text(" show always") ]), Tag("ul", {"class": "list"}, null, [ Tag("li", null, null, [ Tag("span", {"class": "emoji Noto"}, "๐Ÿงฒ"), Text(" position"), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "sidebar_pos", "value": "left", ...uglyChecked(sidebarPos == "left")}, null, null, "input", (e) => { gridContainer.classList.toggle("reversed"); saveToLocalStorage("rendertest_sidebarpos", e.target.value); }), Text(" left") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "sidebar_pos", "value": "right", ...uglyChecked(sidebarPos == "right")}, null, null, "input", (e) => { gridContainer.classList.toggle("reversed"); saveToLocalStorage("rendertest_sidebarpos", e.target.value); }), Text(" right") ]), ]), Tag("li", null, null, [ Tag("span", {"class": "emoji"}, "๐Ÿ“"), Text(" size"), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "sidebar-size", "value": "default", ...uglyChecked(sidebarSize == "default")}, null, null, "input", (e) => { document.body.classList.toggle("mini"); saveToLocalStorage("rendertest_sidebarsize", e.target.value); }), Text(" default") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "sidebar-size", "value": "mini", ...uglyChecked(sidebarSize == "mini")}, null, null, "input", (e) => { document.body.classList.toggle("mini"); saveToLocalStorage("rendertest_sidebarsize", e.target.value); }), Text(" mini") ]) ]), ]), ]), Tag("div", null, null, [ Tag("b", null, null, [ Tag("span", {"class": "emoji"}, "โš–"), Text(" Image position") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "image_position", "value": "relative", ...uglyChecked(imagePosition === "relative")}, null, null, "input", (e) => { images.classList.toggle("center"); saveToLocalStorage("rendertest_imageposition", e.target.value); }), Text(" relative") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "image_position", "value": "center", ...uglyChecked(imagePosition === "center")}, null, null, "input", (e) => { images.classList.toggle("center"); saveToLocalStorage("rendertest_imageposition", e.target.value); }), Text(" center ") ]), Tag("b", null, null, [ Tag("span", {"class": "emoji Noto"}, "๐Ÿ–ผ"), Tag("label", null, null, [ Text(" vertical padding"), vpaddin_input = Tag("input", {"type": "checkbox", "name": "vpadding", ...uglyChecked(vpadding)}, null, null, "change", (e) => { images.classList.toggle("vpadding"); saveToLocalStorage("rendertest_vpadding", e.target.checked); }) ]) ]) ]), Tag("div", null, null, [ Tag("b", null, null, [ Tag("span", {"class": "emoji"}, "๐Ÿ“ท"), Tag("label", null, null, [ Text(" Toggle diff for all"), Tag("input", {"type": "checkbox", "name": "all_diff", ...uglyChecked(all_diff)}, null, null, "change", (e) => { images.classList.toggle("all_diff"); //vpaddin_input.disabled = !vpaddin_input.disabled; //images.classList.toggle("vpadding"); saveToLocalStorage("rendertest_all_diff", e.target.checked); }) ]) ]) ]), Tag("div", null, null, [ Tag("b", null, null, [ Tag("span", {"class": "emoji"}, "๐Ÿ“Š"), Text(" Sort table by Failed") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "sort_table", "value": "yes", ...uglyChecked(tableSort)}, null, null, "input", (e) => { buildData(table, images, data, true); }), Text(" yes") ]), Tag("label", null, null, [ Tag("input", {"type": "radio", "name": "sort_table", "value": "no", ...uglyChecked(!tableSort)}, null, null, "input", (e) => { buildData(table, images, data, false); }), Text(" no") ]) ]) ]), Tag("div", {"class": "panel", "id": "table"}, null, [ Tag("h2", null, "List of things that are not perfect"), Tag("label", {"class": "filter sticky"}, "Filter", [ Tag("input", {"type": "input", "name": "filter", "value": ""}, null, null, "input", (e) => { buildDataSlowMode(table, images, data, tableSort, e.target.value); }), ]), table = Tag("div", {"id": "fail_table"}) ]) ]), Tag("div", {"class": "content"}, null, [ Tag("h1", null, "Rendertest report"), Tag("h2", null, "Summary"), Tag("div", {"id": "summary"}, null, [...buildSummary(data)]), Tag("h2", null, "Images of things that are not perfect ", [ Tag("span", {"class": "emoji"}, "โคต") ]), images = Tag("div", {"id": "fail_images", "class": "vpadding"}) ]) ]) document.body.appendChild(gridContainer); buildData(table, images, data, tableSort); // TODO: remove this if (sidebarPos === "right") { gridContainer.classList.toggle("reversed"); } if (sidebarMode === "always") { sidebarOptions.classList.toggle("sticky"); } if (sidebarSize === "mini") { document.body.classList.toggle("mini"); } if (imagePosition === "center") { images.classList.toggle("center"); } if (!vpadding) { images.classList.toggle("vpadding"); } if (all_diff) { // TODO: better UX images.classList.toggle("all_diff"); //vpaddin_input.disabled = !vpaddin_input.disabled; //images.classList.toggle("vpadding"); } if (diffMode === "separate") { images.classList.toggle("diff_separate"); } if (imageCompareMode === "split") { images.classList.toggle("split_compare"); } changeAnimationDuration(isPaused ? 0 : numberInput.value); // stop animation when not in the viewport function handleIntersection(entries, observer) { for (let entry of entries) { if (!entry.isIntersecting) { entry.target.classList.add("anim_off"); } else { entry.target.classList.remove("anim_off"); } } } // TODO: auto subs&unsubs const observer = new IntersectionObserver(handleIntersection, { root: null, threshold: 0.1 }); for (let element of document.querySelectorAll(".block-gold")) { observer.observe(element); } function getPointerPositionPercentage(container, event) { const rect = container.getBoundingClientRect(); let x; switch (event.type) { case "mousemove": x = event.clientX - rect.left; break; case "touchmove": // TODO: testing x = event.touches[0].clientX - rect.left; break; } const percent = (x / rect.width) * 100; return percent; } images.addEventListener("mousemove", (event) => { if (images.classList.contains("split_compare")) { const target = event.target.closest(".image-container"); if (target) { event.preventDefault(); const percent = getPointerPositionPercentage(target, event); const separator = target.querySelector(".separator"); const compare = target.querySelector(".block-gold"); compare.style.width = `${percent}%`; separator.style.left = `${percent}%`; } } }); }