new design for rendertest (#8)

Что сделано:
* Удалёны gif, тесты заметно ускорились из-за этого, даже не смотря что делаются png из tga форсировано теперь.
* Добавлены темы, но светлой уделялось мало внимания. С костылём на автотёмную тему не знаю как будет тёмная тема работать, не проверял.
* Изменён дизайн, теперь таблица-список картинок вынесена в отдельную боковую панель.
* Появились разные режимы и настройки (лень перечислять там и так всё видно).
* Исправлена ошибка сортировки которая проявлялась на chrome-подобных браузерах.
* Исправлена ошибка с ориентацией картинок (поймал у себя что они оказались перевёрнутые).
* Произошла некоторая реорганизация кодовой базы, некоторые функции были расширены, а что-то исправлено по мелочи.

Reviewed-on: #8
Co-authored-by: NightFox <nightfox@dev.null>
Co-committed-by: NightFox <nightfox@dev.null>
This commit is contained in:
NightFox 2024-02-15 23:35:40 +01:00 committed by provod
parent d20af78fa7
commit 25faf8b0e1
14 changed files with 1790 additions and 43 deletions

2
.gitattributes vendored
View File

@ -1,3 +1,5 @@
*.sav filter=lfs diff=lfs merge=lfs -text *.sav filter=lfs diff=lfs merge=lfs -text
*.bsp filter=lfs diff=lfs merge=lfs -text *.bsp filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.woff2 filter=lfs diff=lfs merge=lfs -text

514
render/base.css Normal file
View File

@ -0,0 +1,514 @@
@font-face {
font-family: "Noto Color Emoji";
src: url("./fonts/NotoColorEmoji/Noto-COLRv1-noflags.ttf");
}
@font-face {
font-family: "OpenMojiDemoFont";
src: url("./fonts/OpenMoji/OpenMoji-color-colr1_svg.woff2") format("woff2");
}
@font-face {
font-family: "Twemoji Mozilla Built";
src: url("./fonts/Mozilla/Twemoji.Mozilla.ttf");
}
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:first-child,h2:first-child,h3:first-child,h4:first-child,h5:first-child {
margin-top: 0;
}
.emoji {
font-family: "Noto Color Emoji", sans-serif;
font-family: "OpenMojiDemoFont", sans-serif;
font-family: "Twemoji Mozilla Built", sans-serif;
font-size: 16px;
font-weight: 400;
font-style: normal;
position: relative;
/*top: 1px; /* Twemoji */
}
.emoji:after {
font-variant-emoji: emoji; /* experimental feature and only in firefox :( */
content: '\FE0F'; /* fuck off, give me emoji // https://codepoints.net/U+FE0F */
}
.emoji.Twemoji {
font-family: "Twemoji Mozilla Built", sans-serif;
font-size: 16px;
top: 2px;
}
.emoji.Noto {
font-family: "Noto Color Emoji", sans-serif;
font-size: 14px;
/*left: -1px;*/
top: 0px;
}
.emoji.OpenMoji {
font-family: "OpenMojiDemoFont", sans-serif;
font-size: 17px;
left: -2px;
top: 0px;
color: #888; /* fix outline */
}
.emoji.OpenMoji + .text-left-margin {
margin-left: 0;
}
.emoji.Noto + .text-left-margin {
margin-left: 5px;
}
.sidebar .emoji.Noto + .emoji.Noto {
margin-left: 4px;
}
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;
align-content: baseline;
}
.sidebar h2 {
font-size: 17px;
}
#options {
height: var(--options-height);
}
#table { }
#fail_images {
display: grid;
justify-content: left;
}
#fail_images.center,
.grid-container.reversed #fail_images.center{
justify-content: center;
}
.grid-container.reversed #fail_images {
justify-content: right;
}
.panel {
display: block;
padding: 10px;
}
.panel > div {
line-height: 30px;
}
.sticky {
top: 0;
position: sticky;
z-index: 10;
}
.content {
grid-area: content;
overflow: auto;
padding: 0px 10px;
}
body.mini { /* split feature */
width: 75%; margin: 0 auto; /* FIXME */
}
.mini .sidebar {
font-size: 12px;
/*font-weight: bold;*/
}
.mini .sidebar .panel > div {
line-height: 20px;
}
.mini .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;
}
.block-gold.anim_off,
.block-gold.anim_force_off,
.all_diff .block-gold,
.split_compare .block-gold{
animation: none;
}
.split_compare .image-container {
cursor: col-resize;
}
.split_compare .block-gold {
overflow: hidden;
width: 50%;
position: absolute;
z-index: 10;
}
.split_compare .separator {
width: 1px;
height: 100%;
cursor: col-resize;
position: absolute;
top: 0;
left: 50%;
z-index: 10;
backdrop-filter: invert(1);
}
.split_compare .block-test:after {
left: auto;
right: 25px;
}
:root {
--animation-duration: 0.7s;
--height-image: calc((100vh - 800px) / 2);
}
.image-container,
.meta-block {
position: relative;
}
.meta-block h3 {
/*display: none;*/
font-family: monospace;
/*font-family: "Gill Sans", sans-serif;*/
padding-top: 10px;
margin-top: revert;
/*font-size: 150%;*/
font-size: 1.5em;
}
.meta-block h3:before {
content: "🎯\FE0F"; /* for hedging her bets; see .emoji for details */
}
.text-link {
text-decoration: none;
}
#fail_images.vpadding .meta-block {
/*margin-block: 100vh;*/
padding-top: calc(var(--height-image));
padding-bottom: 2px;
/*padding-bottom: calc(50% - var(--height-image) );*/
}
#fail_images.vpadding .meta-block h3, .meta-block:first-child h3 {
border-top: none;
}
.block-diff {
display: none;
/*visibility: collapse;*/
position: absolute;
top: 0px;
/*z-index: 10;*/
}
.block-diff.show_diff,
.all_diff .block-diff {
display: block;
z-index: 2;
}
.diff_separate .block-diff {
display: block;
position: relative;
top: auto;
}
.image-container, img, .block {
max-width: 100%;
user-select: none;
}
/* TODO: исправить конфликт с image split compare */
.split_compare .image-container,
.split_compare img,
.split_compare .block {
max-width: unset;
}
table {
position: relative;
width: 100%;
text-align: left;
border-collapse: collapse;
line-height: normal;
/*overflow: hidden;*/ /* ugly chrome-based */
}
th, td {
padding: 0.15rem;
border: none;
}
tr {
padding: 0.15rem;
border: none;
cursor: pointer;
}
tr a {
text-decoration: none;
}
th {
z-index: 1;
}
th.table-sticky {
position: sticky;
top: 2.7em; /* FIXME */
}
.mini th.table-sticky {
position: sticky;
top: 3.53em; /* FIXME */
}
#options.sticky + #table th.table-sticky {
top: calc(var(--options-height) + 2.74em); /* FIXME */
}
.mini .sidebar #options.sticky + #table th.table-sticky {
top: calc(var(--options-height-min) + 3.50em); /* FIXME */
}
.filter {
display: block;
padding: 10px 4px;
font-size: 16px;
font-weight: bold;
}
.filter * {
font-weight: normal;
}
.mini .filter {
font-size: 12px;
}
#options.sticky + #table .filter.sticky {
top: calc(var(--options-height) + 0.0em); /* FIXME */
}
.mini .sidebar #options.sticky + #table .filter.sticky {
top: calc(var(--options-height-min) + 0.0em); /* FIXME */
}
.filter input {
margin-left: 10px;
}
#options .filter.sticky {
}
input[type=range] { vertical-align: middle; }
a, a:visited {
}
.text-left-margin {
margin-left: 6px;
}
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;}
summary {
cursor: pointer;
list-style: none;
}
summary:before {
content: "📁\FE0F +";
margin: -5px 5px 0 0;
}
details[open] summary:before {
content: "📂\FE0F ";
}
.red {
color: #f53200;
color: oklch(62.8% 0.25768330773615683 32.82);
}
.yellow {
color: #d3a100;
color: oklch(76.67% 0.17685825418032033 90.53);
}
.green {
color: #6b9a00;
color: oklch(62.8% 0.25768330773615683 128.19);
}
table th:nth-child(3) {
text-align: center;
}
table td:nth-child(3) { /* Δ, % // TODO: add class */
text-align: right;
font-family: Consolas, monospace;
}
table td:nth-child(4) { /* Passed // TODO: add class */
text-align: center;
}

View File

@ -0,0 +1,50 @@
## License for the Code
Copyright 2016-2018, Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
## License for the Visual Design
The Emoji art in the twe-svg.zip archive comes from [Twemoji](https://twitter.github.io/twemoji),
and is used and redistributed under the CC-BY-4.0 [license terms](https://github.com/twitter/twemoji#license)
offered by the Twemoji project.
### Creative Commons Attribution 4.0 International (CC BY 4.0)
https://creativecommons.org/licenses/by/4.0/legalcode
or for the human readable summary: https://creativecommons.org/licenses/by/4.0/
#### You are free to:
**Share** — copy and redistribute the material in any medium or format
**Adapt** — remix, transform, and build upon the material for any purpose, even commercially.
The licensor cannot revoke these freedoms as long as you follow the license terms.
#### Under the following terms:
**Attribution** — You must give appropriate credit, provide a link to the license,
and indicate if changes were made.
You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
**No additional restrictions** — You may not apply legal terms or **technological measures**
that legally restrict others from doing anything the license permits.
#### Notices:
You do not have to comply with the license for elements of the material in the public domain
or where your use is permitted by an applicable exception or limitation. No warranties are given.
The license may not give you all of the permissions necessary for your intended use.
For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.

BIN
render/fonts/Mozilla/Twemoji.Mozilla.ttf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
render/fonts/NotoColorEmoji/Noto-COLRv1-noflags.ttf (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,93 @@
Copyright 2021 Google Inc. All Rights Reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,427 @@
Attribution-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-ShareAlike 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-ShareAlike 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

BIN
render/fonts/OpenMoji/OpenMoji-color-colr1_svg.woff2 (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -9,27 +9,123 @@ 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];
} }
// 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) { function makeTable(fields, data, attrs_func) {
const table = Tag('table', null, null, [ const table = Tag('table', null, null, [
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;
})()), })()),
]); ]);
for (const di in data) { for (const di in data) {
let d = data[di]; let d = data[di];
const attrs = attrs_func ? attrs_func(d) : null; const attrs = attrs_func ? attrs_func(d) : null;
@ -44,7 +140,6 @@ function makeTable(fields, data, attrs_func) {
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;
// if (f.good && f.bad) { // if (f.good && f.bad) {
// const rating = linearStep(value, f.good, f.bad); // const rating = linearStep(value, f.good, f.bad);
@ -55,7 +150,7 @@ function makeTable(fields, data, attrs_func) {
// }; // };
// attrs = {style: `background-color: rgb(${c.r}, ${c.g}, ${c.b})`}; // attrs = {style: `background-color: rgb(${c.r}, ${c.g}, ${c.b})`};
// } // }
ret.push(Tag('td', attrs, null, tags)); ret.push(Tag('td', null, null, tags));
} }
return ret; return ret;
})())); })()));
@ -67,9 +162,9 @@ function makeTable(fields, data, attrs_func) {
function buildTestResultsTable(data) { function buildTestResultsTable(data) {
const fields = [ const fields = [
{label: 'Test', field: 'test', tags_func: sectionLink}, {label: 'Test', field: 'test', tags_func: sectionLink},
{label: 'Channel', field: 'channel', tags_func: sectionLink}, {label: 'Channel', field: 'channel', tags_func: channelWithEmoji},
{label: 'Δ, %', field: 'diff_pct'}, {label: 'Δ, %', field: 'diff_pct', tags_func: colorPct},
{label: 'Failed', field: 'fail'}, {label: 'Passed', field: 'fail', tags_func: isPassed},
]; ];
return [makeTable(fields, data, rowAttribs)]; return [makeTable(fields, data, rowAttribs)];
@ -78,33 +173,499 @@ 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) {
const total = data.length; const total = data.length;
const fails = data.filter((d) => { return !!d.fail; }).length; const fails = data.filter((d) => { return !!d.fail; }).length;
const fails_pct = fails * 100.0 / total; const fails_pct = fails * 100.0 / total;
return [Text(`Tests failed: ${fails} / ${total} (${fails_pct.toFixed(3)}%)`)]; return emojiToTag(`🧪 Tests 💥: ${fails}/${total}🏁 (⚠${fails_pct.toFixed(3)}%)`);
} }
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("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}),
]); ]);
}); });
} }
window.onload = () => {
$('summary').replaceChildren(...buildSummary(data));
const filtered_data = filterData(data);
$('fail_table').replaceChildren(...buildTestResultsTable(filtered_data)); function buildData(table, images, data, sort, filter, exact_match) {
$('fail_images').replaceChildren(...buildTestResultImages(filtered_data)); if (sort) {
data = filterData(data);
}
if (filter) {
if (exact_match) {
data = data.filter((d) => {
const test = d.test === filter;
const channel = d.channel === filter;
return test || channel;
});
} else {
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, exact_match) => {
buildData(table, images, data, sort, filter, exact_match);
}, ()=>{}, 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) ? default_option : null;
default:
return value;
}
} else
if (value === null) {
return (default_option === "" || default_option) ? default_option : null;
} else
if (value === "") {
return value;
}
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, filter, exactmatch_input;
const switchFrequency = loadFromLocalStorage("rendertest_switchfrequency", 700);
let 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");
const filter_value = loadFromLocalStorage("rendertest_filter", "");
const exactMatch = loadFromLocalStorage("rendertest_exactmatch", false);
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, filter.value, exactmatch_input.checked);
// TODO: rewrite use reactive programming
tableSort = 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, filter.value, exactmatch_input.checked);
tableSort = 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", [
filter = Tag("input", {"type": "input", "name": "filter", "value": filter_value}, null, null, "input", (e) => {
saveToLocalStorage("rendertest_filter", e.target.value);
buildDataSlowMode(table, images, data, tableSort, e.target.value, exactmatch_input.checked);
}),
Tag("label", null, null, [
exactmatch_input = Tag("input", {"type": "checkbox", "name": "exactmatch", ...uglyChecked(exactMatch)}, null, null, "change", (e) => {
saveToLocalStorage("rendertest_exactmatch", e.target.checked);
buildDataSlowMode(table, images, data, tableSort, filter.value, exactmatch_input.checked);
}),
Text("exact match")
])
]),
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, filter_value, exactmatch_input.checked);
// 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}%`;
}
}
});
} }

View File

@ -11,6 +11,7 @@ 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
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 +151,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 +192,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 +221,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>

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

@ -0,0 +1,49 @@
body {
background: black;
color: #EEE;
scrollbar-color: #992a8d #222; /*#5e0080? #67005C?*/
}
.panel {
background: #222;
border: #333 1px solid;
border-radius: 10px;
color: cornflowerblue;
}
.meta-block h3 {
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: gold #222; /* #e100ca */
}
.filter {
background: #222;
}

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

@ -0,0 +1,38 @@
body {
background: white;
color: black;
}
.panel {
background: #FFF;
border: #e5f0ff 1px solid;
border-radius: 10px;
color: #2d6bdb;
}
.meta-block h3 {
color: goldenrod;
border-top: 3px lightgray solid;
}
table {
color: #eb4300;
border: #ffa6f6 1px solid;
border-radius: 6px;
}
th {
background: oklch(62.5% 0.291 308.12);
background: linear-gradient(0deg,#d99dff,#c4d7f9);
/* 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);*/
}
.filter {
background: #FFF;
}

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();