HLRTest/render/index.js

469 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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];
}
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 [Tag('div', {"class": "emoji"}, emoji, [
Tag("span", {"class": "text-left-margin"}, value)
])];
} else {
return [];
}
}
function colorPct(value) {
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}, value.toFixed(3))];
}
function isPassed(value) {
return [Tag("span", {"class": "emoji"}, 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) {
let 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 [Text(`Tests failed: ${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('h2', 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('img', {src: d.image_flip}),
]);
});
}
function buildData(table, images, data, sort) {
if (sort) {
data = filterData(data);
}
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_off");
});
};
for (let tr of document.querySelectorAll("table tr")) {
tr.addEventListener("click", function() {
this.querySelector("a").click();
});
}
saveToLocalStorage("rendertest_tablesort", sort);
}
const saveToLocalStorage = debounce((key, value) => {
localStorage.setItem(key, value);
});
const loadFromLocalStorage = (key) => {
const value = localStorage.getItem(key);
if (isNaN(value)) {
switch(value) { // ugly textual localStorage
case "true":
return true;
case "false":
return false;
case "null":
return null;
default:
return value;
}
}
return parseFloat(value); // fix !!"0" = true
}
window.onload = () => {
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 checked = !linkElement.rel.includes("alternate") ? {"checked": "checked"} : {};
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);
}),
Text(linkElement.title)
])
)
};
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 isPaused = false;
let gridContainer, sidebarOptions, numberInput, rangeInput, table, images;
const tableSort = !!loadFromLocalStorage("rendertest_tablesort");
const sidebarPos = loadFromLocalStorage("rendertest_sidebarpos");
function uglyChecked(value) { // fast hack
return value ? {"checked": "checked"} : {};
}
// 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", "checked": "checked"}),
Tag("span", {"class": "emoji"}, "🖱"),
Text(" Click (LMB)")
]),
Tag("label", null, null, [
Tag("input", {"type": "radio", "name": "diff_mode", "value": "separate", "disabled": "disabled"}),
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", "checked": "checked"}),
Tag("span", {"class": "emoji"}, "🎞"),
Text(" Switch images")
]),
Tag("label", {"title": "under construction"}, null, [
Tag("input", {"type": "radio", "name": "compare_mode", "value": "split", "disabled": "disabled"}),
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": "700"}, null, null, "input", syncSliderValues),
numberInput = Tag("input", {"type": "number", "size": "5", "min": "10", "max": "1000", "value": "700"}, null, null, "input", syncSliderValues),
Text(" ms "),
Tag("button", null, "pause", null, "click", (e) => {
//e.target.textContent = isPaused ? "❄ pause" : "🔥 resume";
e.target.textContent = isPaused ? "pause" : "resume";
changeAnimationDuration(isPaused ? numberInput.value : 0);
isPaused = !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", "checked": "checked"}, null, null, "input", (e) => {
sidebarOptions.classList.toggle("sticky")
}),
Text(" auto minimize")
]),
Tag("label", null, null, [
Tag("input", {"type": "radio", "name": "sidebar-mode", "value": "always"}, null, null, "input", (e) => {
sidebarOptions.classList.toggle("sticky")
}),
Text(" show always")
]),
Tag("ul", {"class": "list"}, null, [
Tag("li", null, null, [
Tag("span", {"class": "emoji"}, "🧲"),
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": "always", "checked": "checked"}, null, null, "input", (e) => {
document.body.classList.toggle("min");
}),
Text(" default")
]),
Tag("label", null, null, [
Tag("input", {"type": "radio", "name": "sidebar-size", "value": "minimize"}, null, null, "input", (e) => {
document.body.classList.toggle("min");
}),
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", "disabled": "disabled"}),
Text(" relative")
]),
Tag("label", null, null, [
Tag("input", {"type": "radio", "name": "image_position", "value": "center", "checked": "checked"}),
Text(" center ")
]),
Tag("b", null, null, [
Tag("span", {"class": "emoji"}, "🖼"),
Tag("label", null, null, [
Text(" vertical padding"),
Tag("input", {"type": "checkbox", "name": "all_diff", "checked": "checked"}, null, null, "input", (e) => {
images.classList.toggle("padding");
})
])
])
]),
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", "disabled": "disabled"})
])
])
]),
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"),
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"),
images = Tag("div", {"id": "fail_images", "class": "padding"})
])
])
document.body.appendChild(gridContainer);
buildData(table, images, data, tableSort);
// TODO: remove this
if (sidebarPos == "right") {
gridContainer.classList.toggle("reversed");
}
// stop animation when not in 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");
}
}
}
const observer = new IntersectionObserver(handleIntersection, {
root: null,
threshold: 0.1
});
for (let element of document.querySelectorAll(".block-gold")) {
observer.observe(element);
}
}