diff --git a/site/static/css/main.css b/site/static/css/main.css
@@ -88,11 +88,27 @@ a:hover { color: var(--cyan); }
.hero {
position: relative;
padding-block: clamp(3rem, 8vw, 6rem) 0;
- /* The splash screen's scattered-note field, reduced to two soft washes. */
+ background-color: var(--bg);
+}
+
+/* The splash screen, as the page opens on. ommp scatters music notes across a
+ dark field while it loads (about_modal.rs, render_splash_screen); the tile is
+ that same scatter, generated from the same hash by tools/note-field.py. It
+ fades out before the screenshot so the two are never competing. */
+.hero::before {
+ content: "";
+ position: absolute; inset: 0; z-index: 0;
+ pointer-events: none;
background:
- radial-gradient(60rem 32rem at 18% -8%, #3C145055, transparent 70%),
- radial-gradient(52rem 30rem at 88% 4%, #143C5055, transparent 70%);
+ radial-gradient(60rem 32rem at 18% -8%, #3C145066, transparent 70%),
+ radial-gradient(52rem 30rem at 88% 4%, #143C5066, transparent 70%),
+ url("../img/note-field.png") repeat;
+ background-size: auto, auto, 370px 740px;
+ -webkit-mask-image: linear-gradient(#000 48%, #0000 88%);
+ mask-image: linear-gradient(#000 48%, #0000 88%);
}
+.hero-in, .shot-hero { position: relative; z-index: 1; }
+
.hero-in { text-align: center; }
.hero-logo {
@@ -160,7 +176,6 @@ a:hover { color: var(--cyan); }
margin: clamp(3rem, 7vw, 5rem) auto 0;
}
.shot-hero img { box-shadow: 0 2rem 5rem -1.5rem #00000099; }
-.shot-detail img { box-shadow: 0 1.5rem 4rem -2rem #00000099; }
/* ── Bands ────────────────────────────────────────────────────────── */
@@ -217,18 +232,15 @@ a:hover { color: var(--cyan); }
/* ── Split sections ───────────────────────────────────────────────── */
-/* Copy beside its list, and the screenshot underneath at the full width of the
- wrap. These shots are close-ups of a modal whose contents are a fixed number
- of characters — squeezed into a column beside the copy they are simply too
- small to read, which defeats the point of showing them. */
-.split {
- display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: start;
- margin-bottom: clamp(2.25rem, 4vw, 3rem);
-}
+/* Copy in a fixed column, screenshot in the rest. The hero shot is the one that
+ earns full width; from here down the screenshots illustrate a point the copy
+ is already making, and blowing them up to fill the page only pushes the
+ reader further from it. */
+.split { display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: center; }
@media (min-width: 56rem) {
- .split { grid-template-columns: 1fr minmax(16rem, 22rem); }
+ .split { grid-template-columns: 22rem 1fr; }
+ .split-reverse .split-text { order: 2; }
}
-.split-text .sec-lede { margin-bottom: 0; }
.filters { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.filters code {
diff --git a/site/static/img/02-help.avif b/site/static/img/02-help.avif
Binary files differ.
diff --git a/site/static/img/03-search.avif b/site/static/img/03-search.avif
Binary files differ.
diff --git a/site/static/img/note-field.png b/site/static/img/note-field.png
Binary files differ.
diff --git a/site/templates/landing.html b/site/templates/landing.html
@@ -101,12 +101,10 @@
<!-- ── Search ────────────────────────────────────────────────────────── -->
<section class="band" data-section="Search">
- <div class="wrap">
- <div class="split">
- <div class="split-text">
- <h2 class="sec-title">Find it while you type</h2>
- <p class="sec-lede">Results narrow on every keystroke. Narrow further with a field, or by format.</p>
- </div>
+ <div class="wrap split">
+ <div class="split-text">
+ <h2 class="sec-title">Find it while you type</h2>
+ <p class="sec-lede">Results narrow on every keystroke. Narrow further with a field, or by format.</p>
<ul class="filters">
<li><code>artist:yorushika</code></li>
<li><code>album:elma</code></li>
@@ -114,30 +112,28 @@
<li><code>*.flac</code></li>
</ul>
</div>
- <figure class="shot shot-detail">
+ <figure class="shot">
<img src="{{ asset('img/03-search.avif') }}" alt="The search overlay filtering a library down to fifty of fifty-two tracks"
- width="2400" height="1560" loading="lazy">
+ width="3840" height="2160" loading="lazy">
</figure>
</div>
</section>
<!-- ── Keys ──────────────────────────────────────────────────────────── -->
<section class="band band-alt" data-section="Keys">
- <div class="wrap">
- <div class="split">
- <div class="split-text">
- <h2 class="sec-title">Keys where you expect them</h2>
- <p class="sec-lede">Vim motions for the lists, media keys for the music. The mouse works too — click a tab, double-click a track, <kbd>Ctrl</kbd>-drag a border to resize.</p>
- </div>
+ <div class="wrap split split-reverse">
+ <div class="split-text">
+ <h2 class="sec-title">Keys where you expect them</h2>
+ <p class="sec-lede">Vim motions for the lists, media keys for the music. The mouse works too — click a tab, double-click a track, <kbd>Ctrl</kbd>-drag a border to resize.</p>
<ul class="keys">
{%- for k in x.keys %}
<li><kbd>{{ k.key }}</kbd><span>{{ k.desc }}</span></li>
{%- endfor %}
</ul>
</div>
- <figure class="shot shot-detail">
+ <figure class="shot">
<img src="{{ asset('img/02-help.avif') }}" alt="The keybinding overlay, two columns covering playback and navigation"
- width="3100" height="1700" loading="lazy">
+ width="3840" height="2160" loading="lazy">
</figure>
</div>
</section>
diff --git a/site/tools/note-field.py b/site/tools/note-field.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+"""Generate the hero's background: ommp's splash-screen note field.
+
+The splash fills the terminal with dark and scatters music notes across it
+(src/ui/widgets/about_modal.rs, render_splash_screen). This reproduces that
+exact scatter — the same hash, the same four glyphs, the same three colours —
+as a tile the page can repeat.
+
+The scatter is periodic. A note falls on cell (x, y) when
+
+ (7x + 13y) mod 37 == 0
+
+and shifting x or y by 37 adds 7*37 or 13*37 to the hash, both multiples of 37.
+So a 37x37 tile repeats seamlessly. Which glyph and which colour a note gets
+depends on hash/37, whose period is 444 rather than 37, so those cycle sooner
+here than in the app. At this contrast — the brightest of the three is #3C1450
+on #0A051E — that is not a difference anyone can see.
+
+Rendered to a raster on this machine rather than shipped as text: the glyphs
+live in the CJK fonts, not in the page's latin-subset JetBrains Mono, and a
+visitor without them would get four kinds of tofu.
+"""
+
+import subprocess
+import sys
+
+# src/ui/widgets/about_modal.rs
+NOTES = ["♪", "♫", "♩", "◆"]
+DIM_COLORS = ["#3C1450", "#143C50", "#501432"] # DIM_PURPLE, DIM_CYAN, DIM_PINK
+PERIOD = 37
+
+# One terminal cell, at 2x so the tile stays sharp on a hidpi screen. The 1:2
+# ratio is roughly what a monospace cell is.
+CELL_W, CELL_H = 20, 40
+FONT_SIZE = 26
+
+OUT = sys.argv[1] if len(sys.argv) > 1 else "static/img/note-field.png"
+
+
+def font():
+ """A font with all four glyphs. Prefer the one the terminal itself falls
+ back to for U+266A, so the page's notes are the shapes ommp actually draws."""
+ listing = subprocess.run(
+ ["fc-list", ":charset=266a 266b 2669 25c6", "file"],
+ capture_output=True, text=True, check=True,
+ ).stdout
+ files = [l.split(":")[0].strip() for l in listing.splitlines() if l.strip()]
+ for preferred in ("NotoSansMonoCJK", "NotoSansCJK", "DejaVuSans"):
+ for f in files:
+ if preferred in f:
+ return f
+ if not files:
+ sys.exit("no font on this system has all four glyphs")
+ return files[0]
+
+
+def main():
+ args = ["magick", "-size", f"{PERIOD * CELL_W}x{PERIOD * CELL_H}", "xc:none",
+ "-font", font(), "-pointsize", str(FONT_SIZE), "-gravity", "none"]
+
+ drawn = 0
+ for y in range(PERIOD):
+ for x in range(PERIOD):
+ h = (x * 7 + y * 13) & 0xFFFFFFFF
+ if h % PERIOD:
+ continue
+ glyph = NOTES[(h // PERIOD) % len(NOTES)]
+ colour = DIM_COLORS[(h // PERIOD + 1) % len(DIM_COLORS)]
+ # annotate's origin is the baseline; sit the glyph in its cell.
+ args += ["-fill", colour,
+ "-annotate", f"+{x * CELL_W + 2}+{y * CELL_H + CELL_H - 12}", glyph]
+ drawn += 1
+
+ args += ["-strip", OUT]
+ subprocess.run(args, check=True)
+ print(f" {OUT}: {drawn} notes in a {PERIOD}x{PERIOD} cell tile")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/site/tools/shoot.sh b/site/tools/shoot.sh
@@ -1,6 +1,5 @@
#!/usr/bin/env bash
-# Capture ommp screenshots for the landing page, and crop them to what the
-# page actually shows.
+# Capture the ommp screenshots the landing page uses.
#
# Runs the real binary in a real terminal: album art only renders as an image
# under a graphics protocol, and Ghostty is the only installed terminal that
@@ -11,9 +10,10 @@
# 14 the wide shot. Everything fits — full album names, and all thirteen
# fields of the info panel including the sample rate and bit depth.
# At 20 the panel stops at "Disc #" and the best detail is lost.
-# 20 the modal close-ups. A modal is sized as a percentage of the terminal
-# but its contents are a fixed number of characters, so a wider terminal
-# just adds empty space inside it. 20 is where the columns sit tight.
+# 20 the modal shots. A modal is sized as a percentage of the terminal but
+# its contents are a fixed number of characters, so a wider terminal just
+# adds empty space inside it. 20 is where the columns sit tight, and where
+# the keybindings still read at the size the page shows them.
#
# The window is fullscreen for about fifteen seconds and then closes. Your
# player settings and your default sink's mute state are backed up and
@@ -83,29 +83,34 @@ stop
pactl set-sink-mute "$SINK" 0
# ── The modals ────────────────────────────────────────────────────────────
-# Nothing plays here, so nothing is audible.
+# Playing here too: the status bar shows through beside every modal, and a shot
+# that reads Stopped next to two that read Playing looks like a mistake.
python3 "$HERE/prep-state.py" "$STATE"
+pactl set-sink-mute "$SINK" 1
launch 20
+wtype " "
+sleep 8
wtype -M ctrl -k h -m ctrl; sleep 1.2; grim -o "$MONITOR" "$RAW/02-help.png"
wtype -k Escape; sleep 0.5
wtype -M ctrl -k s -m ctrl; sleep 0.8; wtype -d 40 "yorushika"; sleep 1.2
grim -o "$MONITOR" "$RAW/03-search.png"
stop
+pactl set-sink-mute "$SINK" 0
# ── Crop and encode ───────────────────────────────────────────────────────
# Kept at the captured 3840-wide scale rather than downsampled: the pixel grid
# of terminal text is already sharp, and resampling it both blurs the glyphs
# and *inflates* the PNG by inventing intermediate colours.
#
-# The modals are cropped to themselves. Uncropped they are a small rectangle in
-# the middle of a 4K frame, and no amount of page width makes them readable.
-crop() { magick "$RAW/$1.png" -crop "$2" +repage -strip "$OUT/$3.png"; }
-
-magick "$RAW/01-main.png" -strip "$OUT/01-main.png"
-crop 02-help 3100x1700+380+250 02-help
-crop 03-search 2400x1560+720+300 03-search
+# Whole frames, not crops: the page shows each screenshot beside the copy that
+# explains it, at a size where the full window still reads.
+for n in 01-main 02-help 03-search; do
+ magick "$RAW/$n.png" -strip "$RAW/$n-stripped.png"
+ avifenc -q 84 -s 0 "$RAW/$n-stripped.png" "$OUT/$n.avif" >/dev/null
+ rm -f "$RAW/$n-stripped.png"
+done
-for f in "$OUT"/0*.png; do
+for f in "$OUT"/0*.avif; do
printf " %-16s %-11s %s\n" "$(basename "$f")" \
"$(magick identify -format '%wx%h' "$f")" "$(du -h "$f" | cut -f1)"
done
|