ship the side art as a cached stylesheet so cached loads paint in one frame
Deploy / deploy (push) Successful in 3s
Deploy / deploy (push) Successful in 3s
This commit is contained in:
+5
-3
@@ -189,8 +189,10 @@ button {
|
|||||||
|
|
||||||
/* ── side art ───────────────────────────────────────────────────────────
|
/* ── side art ───────────────────────────────────────────────────────────
|
||||||
Full-height sakura in both margins: ANSI art (inc/art/sakura.ans, `cat`
|
Full-height sakura in both margins: ANSI art (inc/art/sakura.ans, `cat`
|
||||||
it), turned into HTML by inc/art.php and mirrored for the right side so the
|
it), turned by inc/art.php into one text layer per color, served as
|
||||||
branches hang in from the outer corners. Hidden when there's no margin. */
|
sideart.php's ::before content on the empty <b>s and mirrored for the right
|
||||||
|
side so the branches hang in from the outer corners. Hidden when there's
|
||||||
|
no margin. */
|
||||||
|
|
||||||
/* The art is drawn for .6em-wide characters; system monospaces range from
|
/* The art is drawn for .6em-wide characters; system monospaces range from
|
||||||
.5em up, which squashes it. So it gets its own: JetBrains Mono's printable
|
.5em up, which squashes it. So it gets its own: JetBrains Mono's printable
|
||||||
@@ -217,7 +219,7 @@ button {
|
|||||||
}
|
}
|
||||||
/* the art comes as one layer per color, stacked */
|
/* the art comes as one layer per color, stacked */
|
||||||
.side { display: grid; }
|
.side { display: grid; }
|
||||||
.side > span { grid-area: 1 / 1; direction: ltr; unicode-bidi: isolate; }
|
.side > b { grid-area: 1 / 1; font-weight: normal; direction: ltr; unicode-bidi: isolate; }
|
||||||
.side-left { left: 0; }
|
.side-left { left: 0; }
|
||||||
/* rtl only so the right panel clips on its inner edge; the layers stay ltr
|
/* rtl only so the right panel clips on its inner edge; the layers stay ltr
|
||||||
(above), or the bidi algorithm reorders runs of punctuation */
|
(above), or the bidi algorithm reorders runs of punctuation */
|
||||||
|
|||||||
+58
-38
@@ -1,9 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* ANSI art (a .ans file you can `cat` in a terminal) to HTML for the side
|
* ANSI art (a .ans file you can `cat` in a terminal) for the side panels.
|
||||||
* panels. Understands foreground colors: truecolor (38;2;r;g;b), 256-color
|
* Understands foreground colors: truecolor (38;2;r;g;b), 256-color (38;5;n),
|
||||||
* (38;5;n), the 16 basic ones, and resets; everything else is ignored.
|
* the 16 basic ones, and resets; everything else is ignored. The right panel
|
||||||
* $mirror flips it left-right, swapping /\ () <> [] {} so shapes still read.
|
* is the left mirrored, swapping /\ () <> [] {} so shapes still read.
|
||||||
|
*
|
||||||
|
* The art ships as a stylesheet (sideart.php), not HTML: one full-size text
|
||||||
|
* layer per color, each the `content` of a ::before on an empty <b>, stacked
|
||||||
|
* by style.css. It's render-blocking and cached, so the first paint already
|
||||||
|
* has it; inline, it made the page too big for one network round trip and
|
||||||
|
* Firefox painted the page half-arrived (a dark empty flash) on cached loads.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function ansi_color(array $p, ?string $cur): ?string
|
function ansi_color(array $p, ?string $cur): ?string
|
||||||
@@ -38,35 +44,55 @@ function ansi_color(array $p, ?string $cur): ?string
|
|||||||
return $cur;
|
return $cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ansi_html(), cached on disk until the .ans file changes (parsing costs ~30 ms a side). */
|
/**
|
||||||
function ansi_html_cached(string $file, bool $mirror = false): string
|
* The stylesheet for both panels, cached on disk until the .ans file (or this
|
||||||
|
* file) changes. Returns [path to the css, layers on the left, on the right].
|
||||||
|
*/
|
||||||
|
function ansi_css_cached(string $file): array
|
||||||
{
|
{
|
||||||
$dir = (defined('DATA_DIR') ? DATA_DIR : sys_get_temp_dir()) . '/art-cache';
|
$dir = (defined('DATA_DIR') ? DATA_DIR : sys_get_temp_dir()) . '/art-cache';
|
||||||
$base = $dir . '/' . md5($file . '|' . (int) $mirror);
|
$base = $dir . '/' . md5($file);
|
||||||
$key = $base . '-' . filemtime($file) . '-layers.html'; // bump the suffix when the markup changes
|
$key = $base . '-' . filemtime($file) . '-' . filemtime(__FILE__);
|
||||||
if (is_file($key)) {
|
if (!is_file("$key.css") || !is_file("$key.json")) {
|
||||||
return (string) file_get_contents($key);
|
@mkdir($dir, 0755, true);
|
||||||
|
array_map('unlink', glob($base . '-*') ?: []); // every deploy bumps the mtimes
|
||||||
|
$css = '';
|
||||||
|
$counts = [];
|
||||||
|
foreach (['left' => false, 'right' => true] as $side => $mirror) {
|
||||||
|
$layers = ansi_layers($file, $mirror);
|
||||||
|
$counts[$side] = count($layers);
|
||||||
|
foreach ($layers as $i => [$color, $text]) {
|
||||||
|
$css .= ".side-$side > b:nth-child(" . ($i + 1) . ')::before{content:' . ansi_css_string($text)
|
||||||
|
. ($color !== '' ? ";color:#$color" : '') . "}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (["$key.css" => $css, "$key.json" => json_encode($counts)] as $path => $data) {
|
||||||
|
$tmp = $path . '.' . getmypid();
|
||||||
|
if (@file_put_contents($tmp, $data) !== false) {
|
||||||
|
rename($tmp, $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$html = ansi_html($file, $mirror);
|
return ["$key.css", ...array_values((array) json_decode((string) @file_get_contents("$key.json"), true))];
|
||||||
@mkdir($dir, 0755, true);
|
|
||||||
array_map('unlink', glob($base . '-*.html') ?: []); // every deploy bumps the mtime
|
|
||||||
$tmp = $key . '.' . getmypid();
|
|
||||||
if (@file_put_contents($tmp, $html) !== false) {
|
|
||||||
rename($tmp, $key);
|
|
||||||
}
|
|
||||||
return $html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ansi_html(string $file, bool $mirror = false): string
|
/** A CSS string literal; newlines become \A (the space after it ends the escape). */
|
||||||
|
function ansi_css_string(string $text): string
|
||||||
|
{
|
||||||
|
return '"' . str_replace(['\\', '"', "\n"], ['\\\\', '\\"', '\\A '], $text) . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The art as [[hex color or '', text], ...]: one full-size layer per color. */
|
||||||
|
function ansi_layers(string $file, bool $mirror = false): array
|
||||||
{
|
{
|
||||||
$raw = (string) file_get_contents($file);
|
$raw = (string) file_get_contents($file);
|
||||||
if (!preg_match('/[\x80-\xff]/', $raw)) {
|
if (!preg_match('/[\x80-\xff]/', $raw)) {
|
||||||
$html = ansi_html_bytes($raw, $mirror);
|
$layers = ansi_layers_bytes($raw, $mirror);
|
||||||
if ($html !== null) {
|
if ($layers !== null) {
|
||||||
return $html;
|
return $layers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ansi_html_cells($raw, $mirror);
|
return ansi_layers_cells($raw, $mirror);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +138,7 @@ function ansi_parse_bytes(string $raw): ?array
|
|||||||
* color layer is cut out of the whole art with bitwise string ops, which run
|
* color layer is cut out of the whole art with bitwise string ops, which run
|
||||||
* in C. Returns null if there are too many colors to index in a byte.
|
* in C. Returns null if there are too many colors to index in a byte.
|
||||||
*/
|
*/
|
||||||
function ansi_html_bytes(string $raw, bool $mirror): ?string
|
function ansi_layers_bytes(string $raw, bool $mirror): ?array
|
||||||
{
|
{
|
||||||
static $from = null, $to = null; // byte 0 -> 0xff, anything else -> 0
|
static $from = null, $to = null; // byte 0 -> 0xff, anything else -> 0
|
||||||
$from ??= implode(array_map('chr', range(0, 255)));
|
$from ??= implode(array_map('chr', range(0, 255)));
|
||||||
@@ -139,21 +165,19 @@ function ansi_html_bytes(string $raw, bool $mirror): ?string
|
|||||||
$blank = implode("\n", array_fill(0, count($chars), str_repeat(' ', $width)));
|
$blank = implode("\n", array_fill(0, count($chars), str_repeat(' ', $width)));
|
||||||
$n = strlen($all);
|
$n = strlen($all);
|
||||||
|
|
||||||
$html = '';
|
$layers = [];
|
||||||
foreach ($index as $color => $k) {
|
foreach ($index as $color => $k) {
|
||||||
$sel = strtr($mask ^ str_repeat(chr($k), $n), $from, $to); // 0xff where this color, else 0
|
$sel = strtr($mask ^ str_repeat(chr($k), $n), $from, $to); // 0xff where this color, else 0
|
||||||
$layer = ($all & $sel) | ($blank & ~$sel);
|
$layer = ($all & $sel) | ($blank & ~$sel);
|
||||||
if (trim($layer) === '') {
|
if (trim($layer) !== '') { // skip colors that only touch blanks
|
||||||
continue; // only blanks in this color
|
$layers[] = [(string) $color, $layer];
|
||||||
}
|
}
|
||||||
$style = $color !== '' ? " style=\"color:#$color\"" : '';
|
|
||||||
$html .= "<span$style>" . htmlspecialchars($layer) . '</span>';
|
|
||||||
}
|
}
|
||||||
return $html;
|
return $layers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** General path: a [char, color] cell at a time, so multibyte characters work. */
|
/** General path: a [char, color] cell at a time, so multibyte characters work. */
|
||||||
function ansi_html_cells(string $raw, bool $mirror): string
|
function ansi_layers_cells(string $raw, bool $mirror): array
|
||||||
{
|
{
|
||||||
static $flip = ['/' => '\\', '\\' => '/', '(' => ')', ')' => '(', '<' => '>', '>' => '<',
|
static $flip = ['/' => '\\', '\\' => '/', '(' => ')', ')' => '(', '<' => '>', '>' => '<',
|
||||||
'[' => ']', ']' => '[', '{' => '}', '}' => '{'];
|
'[' => ']', ']' => '[', '{' => '}', '}' => '{'];
|
||||||
@@ -183,9 +207,6 @@ function ansi_html_cells(string $raw, bool $mirror): string
|
|||||||
}
|
}
|
||||||
unset($cells);
|
unset($cells);
|
||||||
|
|
||||||
// One full-size text layer per color, holding only that color's
|
|
||||||
// characters; CSS stacks them. A few dozen elements instead of a span per
|
|
||||||
// color run (thousands), which is what made the art slow to lay out.
|
|
||||||
$colors = [];
|
$colors = [];
|
||||||
foreach ($lines as $cells) {
|
foreach ($lines as $cells) {
|
||||||
foreach ($cells as [$ch, $color]) {
|
foreach ($cells as [$ch, $color]) {
|
||||||
@@ -194,7 +215,7 @@ function ansi_html_cells(string $raw, bool $mirror): string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$html = '';
|
$layers = [];
|
||||||
foreach (array_keys($colors) as $color) {
|
foreach (array_keys($colors) as $color) {
|
||||||
$rows = [];
|
$rows = [];
|
||||||
foreach ($lines as $cells) {
|
foreach ($lines as $cells) {
|
||||||
@@ -204,8 +225,7 @@ function ansi_html_cells(string $raw, bool $mirror): string
|
|||||||
}
|
}
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
}
|
}
|
||||||
$style = $color !== '' ? " style=\"color:#$color\"" : '';
|
$layers[] = [(string) $color, implode("\n", $rows)];
|
||||||
$html .= "<span$style>" . htmlspecialchars(implode("\n", $rows)) . '</span>';
|
|
||||||
}
|
}
|
||||||
return $html;
|
return $layers;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -10,8 +10,7 @@ $build = preg_match('/[0-9a-f]{7,40}/i', $build, $m) ? $m[0] : ($build ?: 'dev')
|
|||||||
</div>
|
</div>
|
||||||
<small>© <?= date('Y') ?> · build: <a href="<?= htmlspecialchars(REPO_URL . ($build !== 'dev' ? '/src/commit/' . $build : '')) ?>"><?= htmlspecialchars($build) ?></a></small>
|
<small>© <?= date('Y') ?> · build: <a href="<?= htmlspecialchars(REPO_URL . ($build !== 'dev' ? '/src/commit/' . $build : '')) ?>"><?= htmlspecialchars($build) ?></a></small>
|
||||||
</footer>
|
</footer>
|
||||||
<?php require_once __DIR__ . '/art.php'; ?>
|
<pre class="side side-left" aria-hidden="true"><?= str_repeat('<b></b>', $artLeft) ?></pre>
|
||||||
<pre class="side side-left" aria-hidden="true"><?= ansi_html_cached(__DIR__ . '/art/sakura.ans') ?></pre>
|
<pre class="side side-right" aria-hidden="true"><?= str_repeat('<b></b>', $artRight) ?></pre>
|
||||||
<pre class="side side-right" aria-hidden="true"><?= ansi_html_cached(__DIR__ . '/art/sakura.ans', mirror: true) ?></pre>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../config.php';
|
require_once __DIR__ . '/../config.php';
|
||||||
|
require_once __DIR__ . '/art.php';
|
||||||
|
[$artCss, $artLeft, $artRight] = ansi_css_cached(__DIR__ . '/art/sakura.ans');
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -10,6 +12,7 @@ require_once __DIR__ . '/../config.php';
|
|||||||
<title><?= htmlspecialchars($page_title ?? SITE_TITLE) ?></title>
|
<title><?= htmlspecialchars($page_title ?? SITE_TITLE) ?></title>
|
||||||
<link rel="icon" href="/img/favicon.ico" sizes="any">
|
<link rel="icon" href="/img/favicon.ico" sizes="any">
|
||||||
<link rel="stylesheet" href="/css/style.css?v=<?= filemtime(__DIR__ . '/../css/style.css') ?>">
|
<link rel="stylesheet" href="/css/style.css?v=<?= filemtime(__DIR__ . '/../css/style.css') ?>">
|
||||||
|
<link rel="stylesheet" href="/sideart.php?v=<?= filemtime(__DIR__ . '/art/sakura.ans') . '-' . filemtime(__DIR__ . '/art.php') ?>">
|
||||||
<script src="/js/htmx.min.js" defer></script>
|
<script src="/js/htmx.min.js" defer></script>
|
||||||
<script src="/js/idiomorph-ext.min.js" defer></script>
|
<script src="/js/idiomorph-ext.min.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
// The side art as a stylesheet (see inc/art.php). The URL carries ?v=, so it
|
||||||
|
// can be cached for good.
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
require_once __DIR__ . '/inc/art.php';
|
||||||
|
|
||||||
|
[$css] = ansi_css_cached(__DIR__ . '/inc/art/sakura.ans'); // render (or fail) before the headers
|
||||||
|
header('Content-Type: text/css; charset=utf-8');
|
||||||
|
header('Cache-Control: public, max-age=31536000, immutable');
|
||||||
|
readfile($css);
|
||||||
Reference in New Issue
Block a user