new design for rendertest

This commit is contained in:
NightFox 2024-02-12 17:00:30 +03:00
parent d20af78fa7
commit 8b075645e7
7 changed files with 703 additions and 38 deletions

307
render/base.css Normal file
View File

@ -0,0 +1,307 @@
html, body {
box-sizing: border-box;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-family: system-ui;
/*font-variant: all-petite-caps;*/
}
*, *:before, *:after {
box-sizing: inherit;
}
h1,h2,h3,h4,h5 {
margin-top: 0;
}
.emoji:after {
font-variant-emoji: emoji; /* experimental and only in firefox :( */
content: '\FE0F'; /* fuck off, give me emoji // https://codepoints.net/U+FE0F */
}
footer {
text-align: center;
color: #888;
margin: .5em;
}
.autocomplete {
position: relative;
display: inline-block;
}
.autocomplete-items {
z-index: 99;
position: absolute;
top: 100%;
left: 0;
right: 0;
}
.autocomplete-item {
background-color: #fefefe;
cursor: pointer;
border: 1px solid #eee;
border-top: none;
}
.autocomplete-item:hover {
background-color: #e9e9e9;
}
.item-inactive {
padding: 5px;
}
.item-inactive .group {
color: #777;
}
.item-active {
padding: 5px;
background-color: DodgerBlue !important;
color: #fff;
}
.item-active .group {
color: #ccc;
}
/*
table, tr, td, th {
border: 1px solid #888;
}
*/
:root {
--caption-size: 2rem;
--outline-width: 2px;
--outline-width-: calc(0px - var(--outline-width));
--options-height: 18.6em; /* FIXME */
--options-height-min: 17em;
--image-height: 800px;
}
.grid-container {
display: grid;
width: 100%;
grid-template-columns: auto 1fr;
grid-template-rows: auto;
grid-template-areas: "sidebar content";
column-gap: 10px;
height: 100vh;
}
.grid-container.reversed {
grid-template-columns: 1fr auto;
grid-template-areas: "content sidebar";
}
.sidebar {
grid-area: sidebar;
/*position: fixed;*/
height: 100vh;
overflow-y: auto;
white-space: nowrap; /* ugly firefox */
overflow-block: scroll; /* ugly firefox */
display: grid;
grid-gap: 2px;
align-items: stretch;
}
#options {
height: var(--options-height);
}
#table { }
#fail_images {
display: grid;
justify-content: center;
}
.panel {
display: block;
padding: 10px;
}
.panel > div {
line-height: 30px;
}
.sticky {
top: 0;
position: sticky;
z-index: 10;
}
.content {
grid-area: content;
overflow: auto;
justify-content: center;
}
body.min { /* split feature */
width: 75%; margin: 0 auto; /* FIXME */
}
.min .sidebar {
font-size: 12px;
}
.min .sidebar .panel > div {
line-height: 20px;
}
.min .sidebar #options.panel {
height: var(--options-height-min);
}
@keyframes flash {
0% {
z-index: 1;
}
49% {
z-index: 1;
}
50% {
z-index: -1;
}
100% {
z-index: -1;
}
}
.block {
position: relative;
display: block;
max-height: var(--image-height);
}
.block:after {
display: block;
position: absolute;
bottom: 20px;
left: 25px;
font-family: Impact;
font-size: var(--caption-size);
/*-webkit-text-stroke: 1px black;*/ /*ugly*/
text-shadow:
var(--outline-width) var(--outline-width) 0 #000,
var(--outline-width-) var(--outline-width) 0 #000,
var(--outline-width-) var(--outline-width-) 0 #000,
var(--outline-width) var(--outline-width-) 0 #000;
}
.block-test:after {
/*content: "TEST 🧪\FE0F";*/
content: "TEST";
color: white;
}
.block-gold:after {
/*content: "GOLD 🏆\FE0F";*/
content: "GOLD";
color: gold;
}
/*
.block:before {
display: block;
position: absolute;
bottom: 20px;
left: 20px;
}
.block-test:before {
content: "🧪\FE0F";
color: white;
}
.block-gold:before {
content: "🏆\FE0F";
color: gold;
}
*/
.block-gold {
position: absolute;
animation: flash var(--animation-duration) infinite;
}
:root {
--animation-duration: 0.7s;
--height-image: calc((100vh - 800px) / 2);
}
.image-container,
.meta-block {
position: relative;
}
.meta-block h2 {
/*display: none;*/
font-family: monospace;
/*font-family: "Gill Sans", sans-serif;*/
padding-top: 10px;
margin-top: revert;
}
.meta-block h2:before {
content: "🎯\FE0F"; /* for hedging her bets; see .emoji for details */
}
#fail_images.padding .meta-block {
/*margin-block: 100vh;*/
padding-top: calc(var(--height-image));
/*padding-bottom: calc(50% - var(--height-image) );*/
}
#fail_images.padding .meta-block h2, .meta-block:first-child h2 {
border-top: none;
}
.block-diff {
display: none;
/*visibility: collapse;*/
position: absolute;
top: 0px;
/*z-index: 10;*/
}
.block-diff.show_diff {
display: block;
z-index: 2;
}
.image-container, img, .block {
max-width: 100%;
}
table {
position: relative;
width: 100%;
text-align: left;
border-collapse: collapse;
line-height: normal;
}
th, td {
padding: 0.15rem;
border: none;
}
th {
}
th.table-sticky {
position: sticky;
top: 0;
}
#options.sticky + #table th.table-sticky {
top: calc(var(--options-height) + 0.07em); /* FIXME */
}
.min .sidebar #options.sticky + #table th.table-sticky {
top: calc(var(--options-height-min) + 0.07em); /* FIXME */
}
input[type=range] { vertical-align: middle; }
a, a:visited {
}
ul.list {
margin: 0;
}
ul.list li { display: inline-block; margin-left: 10px; } /* FIXME: flex or grid */
ul.list li:first-child { margin-left: 0;}

View File

@ -9,13 +9,13 @@ function makeId(d) {
} }
function rowAttribs(d) { function rowAttribs(d) {
return d.fail ? {style: `background-color: rgb(128, 0, 0)`} : null; return d.fail ? {"class": "fail"} : null;
} }
function sectionLink(value, d) { function sectionLink(value, d) {
const id = makeId(d); const id = makeId(d);
const a = Tag('a', {href: '#'+id}, value); const a = Tag('a', {href: '#'+id}, value);
a.addEventListener('click', () => { $(id).setAttribute('open', true); }); //a.addEventListener('click', () => { $(id).setAttribute('open', true); });
return [a]; return [a];
} }
@ -24,7 +24,7 @@ function makeTable(fields, data, attrs_func) {
Tag('tr', null, null, (() => { Tag('tr', null, null, (() => {
let tds = []; let tds = [];
for (const f of fields) { for (const f of fields) {
tds.push(Tag('td', null, null, [Text(f.label)])); tds.push(Tag('th', {"class": "table-sticky"}, null, [Text(f.label)]));
} }
return tds; return tds;
})()), })()),
@ -36,12 +36,17 @@ function makeTable(fields, data, attrs_func) {
table.appendChild(Tag('tr', attrs, null, (() => { table.appendChild(Tag('tr', attrs, null, (() => {
let ret = []; let ret = [];
for (const f of fields) { for (const f of fields) {
const value = d[f.field]; let value = d[f.field];
if (value === undefined) { if (value === undefined) {
ret.push(Tag('td')); ret.push(Tag('td'));
continue; continue;
} }
// TODO: remove ugly hack
if (f.field == "fail") {
value = value ? "❌" : "✅";
}
const field_func = f.tags_func ? f.tags_func : fieldFuncDefault; const field_func = f.tags_func ? f.tags_func : fieldFuncDefault;
const tags = field_func(value, d); const tags = field_func(value, d);
let attrs = null; let attrs = null;
@ -78,8 +83,10 @@ function buildTestResultsTable(data) {
// Filter out all success // Filter out all success
function filterData(data) { function filterData(data) {
return data return data
.filter((d) => {return d.diff_pct != 0;}) .filter((d) => { return d.diff_pct !== 0; })
.sort((l, r) => {return l.diff_pct < r.diff_pct;}); .sort((l, r) => {
return l.diff_pct < r.diff_pct ? 1 : -1; // RTFM
});
} }
function buildSummary(data) { function buildSummary(data) {
@ -91,20 +98,286 @@ function buildSummary(data) {
function buildTestResultImages(data) { function buildTestResultImages(data) {
return data.flatMap((d) => { return data.flatMap((d) => {
return Tag('details', {id: makeId(d)}, null, [ //return Tag('details', {id: makeId(d)}, null, [
Tag('summary', null, `${d.test}/${d.channel} δ=${d.diff_pct}`), return Tag('div', {id: makeId(d), "class": "meta-block"}, null, [
Tag('img', {src: d.image_diff}), //Tag('summary', null, `${d.test}/${d.channel} δ=${d.diff_pct}`),
Tag('img', {src: d.image_flip}), 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}),
]); ]);
}); });
} }
window.onload = () => {
$('summary').replaceChildren(...buildSummary(data));
const filtered_data = filterData(data);
$('fail_table').replaceChildren(...buildTestResultsTable(filtered_data)); function buildData(table, images, data, sort) {
$('fail_images').replaceChildren(...buildTestResultImages(filtered_data)); if (sort) {
data = filterData(data);
}
table.replaceChildren(...buildTestResultsTable(data));
images.replaceChildren(...buildTestResultImages(data));
}
window.onload = () => {
const linkElements = document.querySelectorAll('link[rel^="stylesheet"][data-theme]');
const themeOptionsDiv = document.querySelector(".theme-options");
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";
}
}
// themes
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');
}
const saveToLocalStorage = debounce((key, value) => {
localStorage.setItem(key, value);
});
const loadFromLocalStorage = localStorage.getItem;
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;
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;
})
]),
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", "checked": "checked"}, null, null, "input", (e) => {
console.log(e);
gridContainer.classList.toggle("reversed");
}),
Text(" left")
]),
Tag("label", null, null, [
Tag("input", {"type": "radio", "name": "sidebar_pos", "value": "right"}, null, null, "input", (e) => {
console.log(e);
gridContainer.classList.toggle("reversed");
}),
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(" Image 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", "checked": "checked"}, null, null, "input", (e) => {
buildData(table, images, data, true);
}),
Text(" yes")
]),
Tag("label", null, null, [
Tag("input", {"type": "radio", "name": "sort_table", "value": "no"}, 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, true);
for (let block of document.querySelectorAll(".image-container")) {
block.addEventListener("click", function() {
let diffElement = this.querySelector(".block-diff");
diffElement.classList.toggle('show_diff');
});
};
function handleCheckboxChange(checkboxId, key) {
var checkbox = document.getElementById(checkboxId);
saveToLocalStorage(key, checkbox.checked);
}
function handleRadioChange(radioGroupId, key) {
var radios = document.getElementsByName(radioGroupId);
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
saveToLocalStorage(key, radios[i].value);
break;
}
}
}
} }

View File

@ -11,6 +11,8 @@ import subprocess
ROOT = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(os.path.abspath(__file__))
imagecompare = f'{ROOT}/imagecompare' imagecompare = f'{ROOT}/imagecompare'
#convert = f'convert' # set path for imagemagick convert if need
convert = f'A:/imagemagick-q8/convert'
WORKDIR = f'{ROOT}/work' WORKDIR = f'{ROOT}/work'
REPORT_ROOT = f'{ROOT}' # FIXME should be workdir? REPORT_ROOT = f'{ROOT}' # FIXME should be workdir?
@ -150,21 +152,25 @@ def compare():
mkdir_p(WORKDIR) mkdir_p(WORKDIR)
screenshot_base = f'{args.xash_dir}/valve/rendertest' screenshot_base = f'{args.xash_dir}/valve/rendertest'
diffs = [] diffs = []
command_png()
print(f'Compare...')
with concurrent.futures.ThreadPoolExecutor() as executor: with concurrent.futures.ThreadPoolExecutor() as executor:
for test in args.tests: for test in args.tests:
for channel, _ in channels.items(): for channel, _ in channels.items():
image_base = f'{test}_{channel}' image_base = f'{test}_{channel}'
image_test = f'{screenshot_base}/{image_base}.tga' #image_test = f'{screenshot_base}/{image_base}.tga'
image_test = f'{ROOT}/work/gold/{image_base}.png'
image_gold = f'{ROOT}/gold/{image_base}.png' image_gold = f'{ROOT}/gold/{image_base}.png'
image_diff = f'{WORKDIR}/{image_base}_diff.png' image_diff = f'{WORKDIR}/{image_base}_diff.png'
image_flip = f'{WORKDIR}/{image_base}_flip.gif' image_flip = f'{WORKDIR}/{image_base}_flip.gif'
diffs.append(executor.submit(compare_one, test, channel, image_base, image_gold, image_test, image_diff, image_flip)) diffs.append(executor.submit(compare_one, test, channel, image_base, image_gold, image_test, image_diff, image_flip))
executor.submit(subprocess.run, ['convert', # legacy
'(', image_gold, '-bordercolor', 'gold', '-border', '2x2', '-gravity', 'SouthWest', '-font', 'Impact', '-pointsize', '24', '-fill', 'gold', '-stroke', 'black', '-annotate', '0', 'GOLD', ')', #executor.submit(subprocess.run, [convert,
'(', image_test, '-bordercolor', 'white', '-border', '2x2', '-fill', 'white', '-annotate', '0', 'TEST', ')', # '(', image_gold, '-bordercolor', 'gold', '-border', '2x2', '-gravity', 'SouthWest', '-font', 'Impact', '-pointsize', '24', '-fill', 'gold', '-stroke', 'black', '-annotate', '0', 'GOLD', ')',
'-loop', '0', '-set', 'delay', '100', image_flip], check=True) # '(', image_test, '-bordercolor', 'white', '-border', '2x2', '-fill', 'white', '-annotate', '0', 'TEST', ')',
# '-loop', '0', '-set', 'delay', '100', image_flip], check=True)
results = [diff.result() for diff in diffs] results = [diff.result() for diff in diffs]
# json.dump(results, open(f'{WORKDIR}/data.json', 'w')) # json.dump(results, open(f'{WORKDIR}/data.json', 'w'))
@ -187,8 +193,8 @@ def command_png():
image_test = f'{screenshot_base}/{image_base}.tga' image_test = f'{screenshot_base}/{image_base}.tga'
image_new_gold = f'{new_gold_base}/{image_base}.png' image_new_gold = f'{new_gold_base}/{image_base}.png'
print(f'{image_new_gold}') print(f'convert to {image_new_gold}')
executor.submit(subprocess.run, ['convert', image_test, image_new_gold], check=True) executor.submit(subprocess.run, [convert, "-auto-orient", image_test, image_new_gold], check=True)
def command_run(): def command_run():
compile() compile()
@ -216,4 +222,3 @@ match args.command:
# TODO: # TODO:
# - settings object to pass as an argument # - settings object to pass as an argument
# - HTML report

View File

@ -3,22 +3,15 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Rendertest report</title> <title>Rendertest report</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" type="text/css" href="base.css">
<link rel="stylesheet" type="text/css" href="theme-dark.css"; data-theme="theme-dark" title="🌑 Dark theme">
<link rel="stylesheet alternate" type="text/css" href="theme-light.css" disabled="true" data-theme="theme-light" title="☀️ Light theme">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script src="utils.js"></script> <script src="utils.js"></script>
<script src="work/data.js"></script> <script src="work/data.js"></script>
<script src="index.js"></script> <script src="index.js"></script>
</head> </head>
<body> <body>
<!-- TODO have a date and build number or whatever --> <!-- TODO: have a date and build number or whatever -->
<h1>Rendertest report</h1>
<h2>Summary</h2>
<div id="summary"></div>
<h2>List of things that are not perfect</h2>
<div id="fail_table"></div>
<h2>Images of things that are not perfect</h2>
<div id="fail_images"></div>
</body> </body>
</html> </html>

45
render/theme-dark.css Normal file
View File

@ -0,0 +1,45 @@
body {
background: black;
color: #EEE;
scrollbar-color: #67005c #222;
}
.panel {
background: #222;
border: #333 1px solid;
border-radius: 10px;
color: cornflowerblue;
}
.meta-block h2 {
color: gold;
border-top: 3px gray solid;
}
table {
color: #eb4300;
border: #e100ca 1px solid;
border-radius: 6px;
}
table tr:hover {
background: oklch(76.47% 0.279 334.17);
background: #512649;
}
th {
/*background: oklch(62.5% 0.291 308.12);*/
background: linear-gradient(0deg,#b540ff,cornflowerblue);
box-shadow: 3px 3px 3px 3px rgba(0, 0, 0, 0.5);
text-shadow: 2px 1px 3px #000A;
}
a, a:visited {
color: #e100ca;
color: oklch(62.5% 0.291 334.06);
}
:root {
/*--font-gradient: linear-gradient(90deg,#7209d4,#2832d4 33%,#00a5b2);*/
}
.content {
scrollbar-color: #e100ca #222;
}

35
render/theme-light.css Normal file
View File

@ -0,0 +1,35 @@
body {
background: white;
color: black;
}
.panel {
background: #FFF;
border: #EEE 1px solid;
border-radius: 10px;
color: cornflowerblue;
}
.meta-block h2 {
color: goldenrod;
border-top: 3px lightgray solid;
}
table {
color: #eb4300;
border: #e100ca 1px solid;
border-radius: 6px;
}
th {
background: oklch(62.5% 0.291 308.12);
background: linear-gradient(0deg,#b540ff,cornflowerblue);
/* box-shadow: 3px 3px 3px 3px rgba(0, 0, 0, 0.5);*/
}
a, a:visited {
color: #e100ca;
color: oklch(62.5% 0.291 334.06);
}
:root {
/*--font-gradient: linear-gradient(90deg,#7209d4,#2832d4 33%,#00a5b2);*/
}

View File

@ -2,10 +2,12 @@
function $(name) { return document.getElementById(name); } function $(name) { return document.getElementById(name); }
function Tag(name, attrs, body, children) { function Tag(name, attrs, body, children, eventName, eventHandler) {
let elem = document.createElement(name); let elem = document.createElement(name);
if (body) { if (body) {
elem.innerHTML = body; // elem.innerHTML = body; // innerHTML is ugly (slow, overwrite childNodes)
let textNode = document.createTextNode(body);
elem.appendChild(textNode);
} }
for (let k in attrs) { for (let k in attrs) {
elem.setAttribute(k, attrs[k]); elem.setAttribute(k, attrs[k]);
@ -13,6 +15,11 @@ function Tag(name, attrs, body, children) {
for (let i in children) { for (let i in children) {
elem.appendChild(children[i]); elem.appendChild(children[i]);
} }
if (eventName && eventHandler) {
elem.addEventListener(eventName, eventHandler);
}
return elem; return elem;
} }
@ -20,7 +27,7 @@ function Text(text) {
return document.createTextNode(text); return document.createTextNode(text);
} }
function debounce(func, cancelFunc, timeout = 500) { function debounce(func, cancelFunc = ()=>{}, timeout = 500) {
let timer; let timer;
return (...args) => { return (...args) => {
cancelFunc(); cancelFunc();