HLRTest/render/index.js
NightFox a09c4968c9 fix emoji rendering (force font emoji for everything)
use hybrid emoji (different fonts for better UX)
add missing Twemoji.Mozilla license
2024-02-14 20:28:25 +03:00

521 lines
15 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];
}
// 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;
}
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 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 ? "❌" : "✅" // ✔✅
)
];
}
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_force_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 emojiTags = emojiToTag(linkElement.title);
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
])
)
};
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 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": "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 Noto"}, "🖼"),
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 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);
}
}