HLRTest/render/index.js

523 lines
16 KiB
JavaScript
Raw Normal View History

2023-11-28 18:32:40 +01:00
"use strict";
const fieldFuncDefault = (value) => {
return [Text(value)];
};
function makeId(d) {
return `${d.test}/${d.channel}`;
}
function rowAttribs(d) {
2024-02-12 15:00:30 +01:00
return d.fail ? {"class": "fail"} : null;
2023-11-28 18:32:40 +01:00
}
function sectionLink(value, d) {
const id = makeId(d);
const a = Tag('a', {href: '#'+id}, value);
2024-02-12 15:00:30 +01:00
//a.addEventListener('click', () => { $(id).setAttribute('open', true); });
2023-11-28 18:32:40 +01:00
return [a];
}
// fonts: Twemoji, Noto, OpenMoji
const emojiFontReplacementTable = {
"🌑":"Twemoji",
"☀":"Noto",
"🧲":"Noto",
"☢":"Noto",
"🔦":"OpenMoji",
"🎆":"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;
}
2024-02-13 13:50:32 +01:00
function channelWithEmoji(value) {
const replacements = {
"full": "🖼",
"basecolor": "🎨",
"emissive": "☢",
"nshade": "🥬", //🏔🌄
2024-02-13 13:50:32 +01:00
"ngeom": "🥒", //⛰
"lighting": "☀",//🗿
"direct": "🔦",//🔦🎇☄
"direct_diffuse": "🔦🎆",
"direct_specular": "🔦💎",
2024-02-13 13:50:32 +01:00
"diffuse": "🎆",
"specular": "💎",
"material": "🖌",
"indirect": "🏀",//🏀🏓🥏
"indirect_specular": "🏀💎",
"indirect_diffuse": "🏀🎆"
2024-02-13 13:50:32 +01:00
}
const emoji = replacements[value];
if (emoji) {
return [
...emojiToTag(emoji),
Tag("span", {"class": "text-left-margin"}, value)
];
2024-02-13 13:50:32 +01:00
} 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) {
const font = value ? emojiFont("❌") : emojiFont("✔");
return [
Tag("span", {"class": "emoji" + font},
value ? "❌" : "✅" // ✔✅
)
];
2024-02-13 13:50:32 +01:00
}
2023-11-28 18:32:40 +01:00
function makeTable(fields, data, attrs_func) {
const table = Tag('table', null, null, [
Tag('tr', null, null, (() => {
let tds = [];
for (const f of fields) {
2024-02-12 15:00:30 +01:00
tds.push(Tag('th', {"class": "table-sticky"}, null, [Text(f.label)]));
2023-11-28 18:32:40 +01:00
}
return tds;
})()),
]);
2024-02-13 13:50:32 +01:00
2023-11-28 18:32:40 +01:00
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) {
2024-02-14 18:36:01 +01:00
const value = d[f.field];
2023-11-28 18:32:40 +01:00
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})`};
// }
2024-02-13 00:10:45 +01:00
ret.push(Tag('td', null, null, tags));
2023-11-28 18:32:40 +01:00
}
return ret;
})()));
}
return table;
}
function buildTestResultsTable(data) {
const fields = [
{label: 'Test', field: 'test', tags_func: sectionLink},
2024-02-13 13:50:32 +01:00
{label: 'Channel', field: 'channel', tags_func: channelWithEmoji},
{label: 'Δ, %', field: 'diff_pct', tags_func: colorPct},
{label: 'Passed', field: 'fail', tags_func: isPassed},
2023-11-28 18:32:40 +01:00
];
return [makeTable(fields, data, rowAttribs)];
}
// Filter out all success
function filterData(data) {
return data
2024-02-12 15:00:30 +01:00
.filter((d) => { return d.diff_pct !== 0; })
.sort((l, r) => {
return l.diff_pct < r.diff_pct ? 1 : -1; // RTFM
});
2023-11-28 18:32:40 +01:00
}
function buildSummary(data) {
const total = data.length;
2024-02-12 15:00:30 +01:00
const fails = data.filter((d) => { return !!d.fail; }).length;
2023-11-28 18:32:40 +01:00
const fails_pct = fails * 100.0 / total;
return [Text(`Tests failed: ${fails} / ${total} (${fails_pct.toFixed(3)}%)`)];
}
function buildTestResultImages(data) {
return data.flatMap((d) => {
2024-02-12 15:00:30 +01:00
//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}`)
]),
2024-02-12 15:00:30 +01:00
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}),
2023-11-28 18:32:40 +01:00
]);
});
}
2024-02-12 15:00:30 +01:00
function buildData(table, images, data, sort) {
if (sort) {
data = filterData(data);
}
table.replaceChildren(...buildTestResultsTable(data));
images.replaceChildren(...buildTestResultImages(data));
2024-02-13 00:10:45 +01:00
for (let block of images.querySelectorAll(".image-container")) {
block.addEventListener("click", function() {
let diffElement = this.querySelector(".block-diff");
let goldElement = this.querySelector(".block-gold");
2024-02-13 00:10:45 +01:00
diffElement.classList.toggle("show_diff");
goldElement.classList.toggle("anim_force_off");
2024-02-13 00:10:45 +01:00
});
};
2024-02-13 13:50:32 +01:00
2024-02-13 00:10:45 +01:00
for (let tr of document.querySelectorAll("table tr")) {
tr.addEventListener("click", function() {
this.querySelector("a").click();
});
}
saveToLocalStorage("rendertest_tablesort", sort);
2024-02-12 15:00:30 +01:00
}
2024-02-13 00:10:45 +01:00
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
}
2023-11-28 18:32:40 +01:00
window.onload = () => {
2024-02-12 15:00:30 +01:00
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);
2024-02-12 15:00:30 +01:00
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);
}),
...emojiTags
2024-02-12 15:00:30 +01:00
])
)
};
2024-02-13 00:10:45 +01:00
2024-02-12 15:00:30 +01:00
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);
}
2024-02-13 00:10:45 +01:00
2024-02-12 15:00:30 +01:00
let isPaused = false;
let gridContainer, sidebarOptions, numberInput, rangeInput, table, images;
2024-02-13 00:10:45 +01:00
const tableSort = !!loadFromLocalStorage("rendertest_tablesort");
const sidebarPos = loadFromLocalStorage("rendertest_sidebarpos");
2024-02-13 13:50:32 +01:00
2024-02-13 00:10:45 +01:00
function uglyChecked(value) { // fast hack
return value ? {"checked": "checked"} : {};
}
// TODO: simplification and bindings
2024-02-12 15:00:30 +01:00
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"}, "🤼"),
2024-02-14 18:36:01 +01:00
Text(" Image compare mode")
2024-02-12 15:00:30 +01:00
]),
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;
})
]),
2024-02-13 00:10:45 +01:00
/* future
Tag("details", null, null, [
Tag("summary", null, "Advanced options"),
]),
*/
2024-02-12 15:00:30 +01:00
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 Noto"}, "🧲"),
2024-02-12 15:00:30 +01:00
Text(" position"),
Tag("label", null, null, [
2024-02-13 00:10:45 +01:00
Tag("input", {"type": "radio", "name": "sidebar_pos", "value": "left", ...uglyChecked(sidebarPos == "left")}, null, null, "input", (e) => {
2024-02-12 15:00:30 +01:00
gridContainer.classList.toggle("reversed");
2024-02-13 00:10:45 +01:00
saveToLocalStorage("rendertest_sidebarpos", e.target.value);
2024-02-12 15:00:30 +01:00
}),
Text(" left")
]),
Tag("label", null, null, [
2024-02-13 00:10:45 +01:00
Tag("input", {"type": "radio", "name": "sidebar_pos", "value": "right", ...uglyChecked(sidebarPos == "right")}, null, null, "input", (e) => {
2024-02-12 15:00:30 +01:00
gridContainer.classList.toggle("reversed");
2024-02-13 00:10:45 +01:00
saveToLocalStorage("rendertest_sidebarpos", e.target.value);
2024-02-12 15:00:30 +01:00
}),
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"}),
2024-02-13 00:10:45 +01:00
Text(" center ")
2024-02-12 15:00:30 +01:00
]),
Tag("b", null, null, [
Tag("span", {"class": "emoji Noto"}, "🖼"),
2024-02-12 15:00:30 +01:00
Tag("label", null, null, [
2024-02-13 00:10:45 +01:00
Text(" vertical padding"),
2024-02-12 15:00:30 +01:00
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, [
2024-02-13 00:10:45 +01:00
Tag("input", {"type": "radio", "name": "sort_table", "value": "yes", ...uglyChecked(tableSort)}, null, null, "input", (e) => {
2024-02-12 15:00:30 +01:00
buildData(table, images, data, true);
}),
Text(" yes")
]),
Tag("label", null, null, [
2024-02-13 00:10:45 +01:00
Tag("input", {"type": "radio", "name": "sort_table", "value": "no", ...uglyChecked(!tableSort)}, null, null, "input", (e) => {
2024-02-12 15:00:30 +01:00
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);
2024-02-13 00:10:45 +01:00
buildData(table, images, data, tableSort);
2024-02-12 15:00:30 +01:00
2024-02-13 00:10:45 +01:00
// TODO: remove this
if (sidebarPos == "right") {
gridContainer.classList.toggle("reversed");
2024-02-12 15:00:30 +01:00
}
2023-11-28 18:32:40 +01:00
// 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");
}
}
}
const observer = new IntersectionObserver(handleIntersection, {
root: null,
threshold: 0.1
});
for (let element of document.querySelectorAll(".block-gold")) {
observer.observe(element);
}
2023-11-28 18:32:40 +01:00
}