diff --git a/batch/colorschemes.php b/batch/colorschemes.php new file mode 100644 index 0000000000..938296c85a --- /dev/null +++ b/batch/colorschemes.php @@ -0,0 +1,163 @@ +run()); +} + +class ColorSchemes_Batch { + /** @var array */ + public $schemes = []; + /** @var array> */ + public $colors = []; + /** @var ?list */ + public $gradient; + /** @var bool */ + public $php = false; + + function __construct($arg) { + if (isset($arg["gradient"])) { + $this->gradient = $arg["gradient"]; + } else if (isset($arg["php"])) { + $this->php = true; + } + foreach (Discrete_ReviewField::$scheme_info as $name => $sch) { + $this->schemes[$name] = $sch; + if ($sch[0] !== 1) { + $this->colors[$name] = array_fill(0, $sch[1], null); + } + } + $this->colors["none"][0] = "222222"; + } + + /** @param string $s + * @return OklchColor */ + function from_hashcolor($s) { + $p = strlen($s) === 7 ? 1 : 0; + return OklchColor::from_rgb(intval(substr($s, $p, 2), 16), + intval(substr($s, $p + 2, 2), 16), + intval(substr($s, $p + 4, 2), 16)); + } + + /** @return int */ + function run_gradient() { + $mode = $this->gradient[3] ?? "shorter"; + if (count($this->gradient) < 3 + || count($this->gradient) > 4 + || !ctype_digit($this->gradient[0]) + || ($n = intval($this->gradient[0])) < 2 + || !preg_match('/\A#?([0-9a-fA-F]{6})\z/', $this->gradient[1], $m1) + || !preg_match('/\A#?([0-9a-fA-F]{6})\z/', $this->gradient[2], $m2) + || !in_array($mode, ["shorter", "longer", "increasing", "decreasing"])) { + throw new CommandLineException("`--gradient` expects NSTOPS COLOR1 COLOR2 [longer]"); + } + $c1 = self::from_hashcolor($m1[1]); + $c2 = self::from_hashcolor($m2[1]); + if (is_nan($c1->okh) && is_nan($c2->okh)) { + $c1->okh = $c2->okh = 0; + } else if (is_nan($c1->okh)) { + $c1->okh = $c2->okh; + } else if (is_nan($c2->okh)) { + $c2->okh = $c1->okh; + } + $hi = HclColor::hue_interpolate($c1->okh, $c2->okh, $mode); + for ($i = 0; $i < $n; ++$i) { + if ($i === 0) { + $c = $c1; + } else if ($i === $n - 1) { + $c = $c2; + } else { + $c = $c1->interpolate($i / ($n - 1), $c2, $hi); + } + fwrite(STDOUT, strtolower($c->hashcolor()) . "\n"); + } + return 0; + } + + private function write_php($j) { + $col = []; + $cat = []; + $rev = []; + foreach ($this->schemes as $name => $sch) { + if ($sch[0] === 2) { + $cat[] = "\"{$name}\" => true"; + } + if ($sch[0] !== 1) { + $col[] = "\"{$name}\" => \"{$j[$name]->colors}\""; + } + if ($sch[2]) { + $rev[] = "\"{$name}\" => \"{$sch[2]}\""; + } + } + fwrite(STDOUT, " public static \$scheme_colors = [" . join(", ", $col) . "];\n" + . " public static \$scheme_categorical = [" . join(", ", $cat) . "];\n" + . " public static \$scheme_reverse = [" . join(", ", $rev) . "];\n"); + } + + /** @return int */ + function run() { + if (isset($this->gradient)) { + return $this->run_gradient(); + } + $css = file_get_contents(SiteLoader::$root . "/stylesheets/style.css"); + preg_match_all('/^\.sv-?([a-z]*)(\d+)\s*\{\s*color\s*:\s*#([0-9a-fA-F]{6})\s*;(?:\s|\/\*.*?\*\/)*\}/m', $css, $ms, PREG_SET_ORDER); + foreach ($ms as $mx) { + $name = $mx[1] === "" ? "sv" : $mx[1]; + $idx = intval($mx[2]); + $color = strtolower($mx[3]); + if (!isset($this->schemes[$name]) + || $this->schemes[$name][0] === 1 + || $idx <= 0 + || $idx > $this->schemes[$name][1] + || isset($this->colors[$name][$idx - 1])) { + fwrite(STDERR, "Unexpected color {$mx[0]}\n"); + } else { + $this->colors[$name][$idx - 1] = $color; + } + } + $j = []; + foreach ($this->schemes as $name => $sch) { + if ($sch[0] === 1) { + continue; + } + if (!isset($this->colors[$name]) + || count(array_filter($this->colors[$name])) !== $sch[1]) { + fwrite(STDERR, "Some colors not set for scheme {$name}\n"); + } else { + $j[$name] = $jx = (object) []; + if ($sch[0] === 2) { + $jx->categorical = true; + } + $jx->colors = join("", $this->colors[$name]); + } + } + if ($this->php) { + $this->write_php($j); + } else { + foreach ($this->schemes as $name => $sch) { + if ($sch[0] === 1 && isset($j[$sch[2]])) { + $j[$sch[2]]->reverse = $name; + } + } + fwrite(STDOUT, json_encode($j, JSON_PRETTY_PRINT) . "\n"); + } + return 0; + } + + /** @return ColorSchemes_Batch */ + static function make_args($argv) { + $arg = (new Getopt)->long( + "help,h !", + "php", + "gradient[]+,g[]+ Compute gradient" + )->description("Analyze HotCRP CSS color schemes. +Usage: php batch/colorschemes.php") + ->helpopt("help") + ->maxarg(0) + ->parse($argv); + + return new ColorSchemes_Batch($arg); + } +} diff --git a/scripts/graph.js b/scripts/graph.js index 0994d2b787..b26de212f5 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -1481,7 +1481,7 @@ function score_ticks(rf) { rewrite: function () { this.selectAll("g.tick text").each(function () { var d = d3.select(this), value = +d.text(); - d.attr("fill", rf.rgb(value)); + d.attr("fill", rf.color(value)); if (!rf.default_numeric && value) d.text(rf.unparse_symbol(value, split)); }); diff --git a/scripts/script.js b/scripts/script.js index b3e799a3bb..32541a4bff 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -6010,8 +6010,8 @@ DiscreteValues_ReviewField.prototype.parse_value = function (txt) { return si >= 0 ? this.value_info(si + 1) : null; }; -DiscreteValues_ReviewField.prototype.rgb = function (val) { - return this.scheme_info.rgb(val); +DiscreteValues_ReviewField.prototype.color = function (val) { + return this.scheme_info.color(val); }; DiscreteValues_ReviewField.prototype.className = function (val) { @@ -6100,8 +6100,8 @@ Checkbox_ReviewField.prototype.parse_value = function (txt) { return null; }; -Checkbox_ReviewField.prototype.rgb = function (val) { - return this.scheme_info.rgb(val ? 2 : 1); +Checkbox_ReviewField.prototype.color = function (val) { + return this.scheme_info.color(val ? 2 : 1); }; Checkbox_ReviewField.prototype.className = function (val) { @@ -12720,7 +12720,7 @@ return function (n, scheme, flip) { categorical: (sci[0] & 2) !== 0, max: sci[1], rgb_array: rgb_array, - rgb: function (val) { + color: function (val) { var x = rgb_array(val); return sprintf("#%02x%02x%02x", x[0], x[1], x[2]); }, diff --git a/scripts/settings.js b/scripts/settings.js index fca77cbeac..34fb62db07 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -723,12 +723,16 @@ function rf_color() { } for (i = 1; i <= scheme.max && c; ++i) { if (c.children.length < i) - $(c).append(''); + $(c).append(''); c.children[i - 1].setAttribute("class", scheme.className(i)); } while (c && i <= c.children.length) { c.removeChild(c.lastChild); } + /*c.append($e("br"), $e("span", { + "class": "d-inline-block", + "style": "width:" + (0.75 * scheme.max) + "em;height:1em;background:linear-gradient(in oklch to right, " + scheme.color(1) + " 0% " + (50 / scheme.max) + "%, " + scheme.color(scheme.max) + " " + (100 - 50 / scheme.max) + "% 100%)" + }));*/ } handle_ui.on("change.rf-scheme", rf_color); diff --git a/src/pages/p_scorechart.php b/src/pages/p_scorechart.php index 876d7b6352..e13b558a88 100644 --- a/src/pages/p_scorechart.php +++ b/src/pages/p_scorechart.php @@ -24,6 +24,20 @@ class Scorechart_Page { private $hiLabel; /** @var bool */ private $flip; + /** @var string */ + private $scheme; + /** @var int */ + private $scheme_max; + /** @var bool */ + private $categorical; + /** @var bool */ + private $svflip; + /** @var int */ + private $scale; + + public static $scheme_colors = ["sv" => "9c3131a04b00a26300a179009d8f00929e007fad005fbd0000cc00", "bupu" => "4b8bc14181be3b76bb396bb73b5fb24053ab4646a34d389a54278f", "pkrd" => "e14da0d7448bcc3b76c13363b52b50a9243e9c1e2c8f1819821201", "viridis" => "440154472c7a3b518b2c718e21908d27ad815cc863aadc32dbcb39", "orbu" => "fca636f68443e86659d14d6fb23a818e2c8f6721963e15940d0887", "turbo" => "23171b4569ee26bce13ff3936be619ecd12eff821dcb2f0d900c00", "catx" => "1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf", "none" => "222222"]; + public static $scheme_categorical = ["catx" => true, "none" => true]; + public static $scheme_reverse = ["sv" => "svr", "svr" => "sv", "bupu" => "pubu", "pubu" => "bupu", "rdpk" => "pkrd", "pkrd" => "rdpk", "viridisr" => "viridis", "viridis" => "viridisr", "orbu" => "buor", "buor" => "orbu", "turbo" => "turbor", "turbor" => "turbo"]; /** @param array{int,int,int} $c1 * @param array{int,int,int} $c2 @@ -35,7 +49,7 @@ static function quality_color($c1, $c2, $f) { (int) ($c2[2] * $f + $c1[2] * (1 - $f) + 0.5)]; } - function __construct($v, $h, $lo, $hi, $flip) { + function __construct($v, $h, $lo, $hi, $flip, $sv, $scale) { foreach (explode(",", $v) as $value) { if ($value !== "") { $value = intval($value); @@ -49,8 +63,18 @@ function __construct($v, $h, $lo, $hi, $flip) { $this->valLight = $h; } $this->loLabel = $lo ?? "1"; - $this->hiLabel = $hi ?? (string) $this->valMax; - $this->flip = $flip; + $this->hiLabel = $hi ?? (string) ($this->valMax - 1); + $this->flip = $this->svflip = $flip; + if (!isset(self::$scheme_colors[$sv]) && isset(self::$scheme_reverse[$sv])) { + $this->svflip = !$this->svflip; + $sv = self::$scheme_reverse[$sv]; + } else if (!isset(self::$scheme_colors[$sv])) { + $sv = "sv"; + } + $this->scheme = $sv; + $this->scheme_max = strlen(self::$scheme_colors[$sv]) / 6; + $this->categorical = self::$scheme_categorical[$sv] ?? false; + $this->scale = intval($scale); } static function cacheable_headers() { @@ -70,26 +94,49 @@ static function fail($status, $text, $cacheable) { echo $text, "\r\n"; } + /** @return array{int,int,int} */ + private function rgb_array($i) { + $n = $this->valMax - 1; + if ($n <= 1 || $this->scheme_max <= 1) { + $f9 = $this->svflip ? 1 : $this->scheme_max; + } else if ($this->categorical && $this->svflip) { + $f9 = ($n - $i) % $this->scheme_max + 1; + } else if ($this->categorical) { + $f9 = ($i - 1) % $this->scheme_max + 1; + } else { + $f = ($this->scheme_max - 1) / ($n - 1); + if ($this->svflip) { + $f9 = max(min(round(($n - $i) * $f) + 1, $this->scheme_max), 1); + } else { + $f9 = max(min(round(($i - 1) * $f) + 1, $this->scheme_max), 1); + } + } + $k = intval(substr(self::$scheme_colors[$this->scheme], ($f9 - 1) * 6, 6), 16); + return [$k >> 16, ($k >> 8) & 255, $k & 255]; + } + private function make_s1() { + $scale = $this->scale; + // set shape constants - $blockHeight = $blockWidth = 3; - $blockSkip = 2; - $blockPad = 2; + $blockHeight = $blockWidth = 3 * $scale; + $blockSkip = 2 * $scale; + $blockPad = 2 * $scale; $maxY = max($this->maxY, 3); $picWidth = ($blockWidth + $blockPad) * ($this->valMax - 1) + $blockPad; $picHeight = $blockHeight * $maxY + $blockSkip * ($maxY + 1); - $pic = @imagecreate($picWidth + 1, $picHeight + 1); + $pic = @imagecreate($picWidth + 2 * $scale, $picHeight + $scale); $cWhite = imagecolorallocate($pic, 255, 255, 255); $cBlack = imagecolorallocate($pic, 0, 0, 0); $cgray = imagecolorallocate($pic, 190, 190, 255); imagecolortransparent($pic, $cWhite); - imagefilledrectangle($pic, 0, $picHeight, $picWidth + 1, $picHeight + 1, $cgray); - imagefilledrectangle($pic, 0, $picHeight - $blockHeight - $blockPad, 0, $picHeight + 1, $cgray); - imagefilledrectangle($pic, $picWidth, $picHeight - $blockHeight - $blockPad, $picWidth + 1, $picHeight + 1, $cgray); + imagefilledrectangle($pic, 0, $picHeight, $picWidth + 2 * $scale, $picHeight + $scale, $cgray); + imagefilledrectangle($pic, 0, $picHeight - $blockHeight - $blockPad, $scale - 1, $picHeight + $scale, $cgray); + imagefilledrectangle($pic, $picWidth + $scale, $picHeight - $blockHeight - $blockPad, $picWidth + 2 * $scale, $picHeight + $scale, $cgray); $cv_black = [0, 0, 0]; $cv_bad = [200, 128, 128]; @@ -99,11 +146,10 @@ private function make_s1() { for ($value = 1; $value < $this->valMax; $value++) { $vpos = $this->flip ? $this->valMax - $value : $value; $height = $this->values[$vpos]; - $frac = ($vpos - 1) / ($this->valMax - 1); - $cv_cur = self::quality_color($cv_bad, $cv_good, $frac); + $cv_cur = $this->rgb_array($vpos); $cFill = imagecolorallocate($pic, $cv_cur[0], $cv_cur[1], $cv_cur[2]); - $curX = $blockWidth * ($value - 1) + $blockPad * $value; + $curX = $blockWidth * ($value - 1) + $blockPad * $value + $scale - 1; $curY = $picHeight - ($blockHeight + $blockSkip) * $height + $blockHeight; for ($h = 1; $h <= $height; $h++) { @@ -117,22 +163,23 @@ private function make_s1() { } } - $lx = $blockPad; - $rx = $picWidth - $blockWidth - $blockPad; - $y = $picHeight - $blockHeight - $blockSkip - 3; + $font = $scale > 2 ? 5 : 1; + $lx = $blockPad + $scale - 1; + $rx = $picWidth - $blockWidth - $blockPad + $scale - 1; + $y = $picHeight - $blockSkip + 2 - imagefontheight($font); if ($this->values[$this->flip ? $this->valMax - 1 : 1] === 0) { - imagestring($pic, 1, $lx, $y, $this->loLabel, $cgray); + imagestring($pic, $font, $lx, $y, $this->loLabel, $cgray); } if ($this->values[$this->flip ? 1 : $this->valMax - 1] === 0) { - imagestring($pic, 1, $rx, $y, $this->hiLabel, $cgray); + imagestring($pic, $font, $rx, $y, $this->hiLabel, $cgray); } return $pic; } private function make_s2() { - $picWidth = 64; - $picHeight = 8; + $picWidth = 64 * $this->scale; + $picHeight = 8 * $this->scale; $pic = @imagecreate($picWidth, $picHeight); $bg = imagecolorallocate($pic, 255, 255, 255); @@ -146,11 +193,10 @@ private function make_s2() { $vpos = $this->flip ? $this->valMax - $value : $value; $height = $this->values[$vpos]; if ($height > 0) { - $frac = ($vpos - 1) / ($this->valMax - 1); - $cv_cur = self::quality_color($cv_bad, $cv_good, $frac); + $cv_cur = $this->rgb_array($vpos); $cFill = imagecolorallocate($pic, $cv_cur[0], $cv_cur[1], $cv_cur[2]); - imagefilledrectangle($pic, ($picWidth + 1) * $pos / $this->sum, 0, - ($picWidth + 1) * ($pos + $height) / $this->sum - 2, $picHeight, + imagefilledrectangle($pic, ($picWidth + $this->scale) * $pos / $this->sum, 0, + ($picWidth + $this->scale) * ($pos + $height) / $this->sum - 2 * $this->scale, $picHeight, $cFill); $pos += $height; } @@ -166,6 +212,8 @@ static function go_param($params) { $lo = $params["lo"] ?? null; $hi = $params["hi"] ?? null; $flip = !!($params["flip"] ?? null); + $sv = $params["sv"] ?? "sv"; + $scale = $params["scale"] ?? "1"; if ($v === null || ($v !== "" && !preg_match('/\A\d+(,\d+)*\z/', $v)) @@ -173,7 +221,8 @@ static function go_param($params) { || !ctype_digit($s) || ($sn = intval($s)) < 1 || $sn > 2 - || ($h !== null && !ctype_digit($h))) { + || ($h !== null && !ctype_digit($h)) + || !ctype_digit($scale)) { self::fail("400 Bad Request", "Invalid parameters", true); return; } @@ -193,7 +242,7 @@ static function go_param($params) { self::cacheable_headers(); header("Content-Type: image/png"); - $sc = new Scorechart_Page($v, $h !== null ? intval($h) : null, $lo, $hi, $flip); + $sc = new Scorechart_Page($v, $h !== null ? intval($h) : null, $lo, $hi, $flip, $sv, $scale); if ($sn !== 2) { imagepng($sc->make_s1()); } else { diff --git a/stylesheets/style.css b/stylesheets/style.css index 2003efa1fe..4439acddec 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -5128,36 +5128,36 @@ div.sc { .sv-catx9 { color: #bcbd22; } .sv-catx10 { color: #17becf; } .sv-orbu1 { color: #fca636; } -.sv-orbu2 { color: #f2844b; } -.sv-orbu3 { color: #e16462; } -.sv-orbu4 { color: #cc4778; } -.sv-orbu5 { color: #b12a90; } -.sv-orbu6 { color: #8f0da4; } -.sv-orbu7 { color: #6a00a8; } -.sv-orbu8 { color: #41049d; } +.sv-orbu2 { color: #f68443; } +.sv-orbu3 { color: #e86659; } +.sv-orbu4 { color: #d14d6f; } +.sv-orbu5 { color: #b23a81; } +.sv-orbu6 { color: #8e2c8f; } +.sv-orbu7 { color: #672196; } +.sv-orbu8 { color: #3e1594; } .sv-orbu9 { color: #0d0887; } .dark .sv-orbu6 { color: #a719bd; } .dark .sv-orbu7 { color: #8c34bf; } .dark .sv-orbu8 { color: #6e37bf; } .dark .sv-orbu9 { color: #5350b5; } .sv-pkrd1 { color: #e14da0; } -.sv-pkrd2 { color: #e2368e; } -.sv-pkrd3 { color: #dd247a; } -.sv-pkrd4 { color: #d21865; } -.sv-pkrd5 { color: #c10f55; } -.sv-pkrd6 { color: #ab0749; } -.sv-pkrd7 { color: #94023d; } -.sv-pkrd8 { color: #7d002f; } -.sv-pkrd9 { color: #67001f; } -.sv-pkrd1 { color: #e14da0; } -.sv-pkrd2 { color: #da498e; } -.sv-pkrd3 { color: #d4457c; } -.sv-pkrd4 { color: #cd416a; } -.sv-pkrd5 { color: #c73d59; } -.sv-pkrd6 { color: #c03847; } -.sv-pkrd7 { color: #b93435; } -.sv-pkrd8 { color: #b33023; } -.sv-pkrd9 { color: #ac2c11; } +.sv-pkrd2 { color: #d7448b; } +.sv-pkrd3 { color: #cc3b76; } +.sv-pkrd4 { color: #c13363; } +.sv-pkrd5 { color: #b52b50; } +.sv-pkrd6 { color: #a9243e; } +.sv-pkrd7 { color: #9c1e2c; } +.sv-pkrd8 { color: #8f1819; } +.sv-pkrd9 { color: #821201; } +.dark .sv-pkrd1 { color: #e14da0; } +.dark .sv-pkrd2 { color: #dd478e; } +.dark .sv-pkrd3 { color: #d8417d; } +.dark .sv-pkrd4 { color: #d23c6c; } +.dark .sv-pkrd5 { color: #cb375b; } +.dark .sv-pkrd6 { color: #c4344b; } +.dark .sv-pkrd7 { color: #bd303a; } +.dark .sv-pkrd8 { color: #b52e28; } +.dark .sv-pkrd9 { color: #ac2c11; } .sv-turbo1 { color: #23171b; } .sv-turbo2 { color: #4569ee; } .sv-turbo3 { color: #26bce1; }