Commit e0a434c3 by PLN (Algolia)

devcon: PARVAGUES as the site's hero headline, navy Jersey on the gradient plate

parent 902d99cc
...@@ -390,6 +390,10 @@ class Theme(NamedTuple): ...@@ -390,6 +390,10 @@ class Theme(NamedTuple):
# every fourth), anything else = a square grid at that pitch -- the # every fourth), anything else = a square grid at that pitch -- the
# DevCon artwork's graph paper. # DevCon artwork's graph paper.
scan_grid: float scan_grid: float
# The PARVAGUES wordmark (`l`) as the site's hero headline instead of the
# neon: Jersey 15, hero tracking, navy on a baked gradient plate. The
# theme decides, not --word-face, so no other page ever wears the plate.
word_plate: bool
# The dark ink ramp, sampled at levels 1..4, IS the dark surface ramp -- which # The dark ink ramp, sampled at levels 1..4, IS the dark surface ramp -- which
...@@ -409,6 +413,7 @@ THEMES: dict[str, Theme] = { ...@@ -409,6 +413,7 @@ THEMES: dict[str, Theme] = {
scan=((0xa0, 0x60, 0xff, 16), (0xa0, 0x60, 0xff, 9)), scan=((0xa0, 0x60, 0xff, 16), (0xa0, 0x60, 0xff, 9)),
bar_frac=0.22, bold=False, ui_font="mono", bar_frac=0.22, bold=False, ui_font="mono",
fam_hue=FAM_HUE, tint_over_page=False, scan_grid=0.0, fam_hue=FAM_HUE, tint_over_page=False, scan_grid=0.0,
word_plate=False,
), ),
# Indoors on a pale desktop. Still a lens, still the violet arc, still a # Indoors on a pale desktop. Still a lens, still the violet arc, still a
# decay gradient the eye can follow -- the ink simply runs the other way. # decay gradient the eye can follow -- the ink simply runs the other way.
...@@ -429,6 +434,7 @@ THEMES: dict[str, Theme] = { ...@@ -429,6 +434,7 @@ THEMES: dict[str, Theme] = {
scan=((0x50, 0x3a, 0x78, 10), (0x50, 0x3a, 0x78, 6)), scan=((0x50, 0x3a, 0x78, 10), (0x50, 0x3a, 0x78, 6)),
bar_frac=0.22, bold=False, ui_font="mono", bar_frac=0.22, bold=False, ui_font="mono",
fam_hue=FAM_HUE, tint_over_page=False, scan_grid=0.0, fam_hue=FAM_HUE, tint_over_page=False, scan_grid=0.0,
word_plate=False,
), ),
# Direct sunlight, and built for nothing else. Near-white page, heavy ink # Direct sunlight, and built for nothing else. Near-white page, heavy ink
# at every decay level (a "dim" state outdoors is a blank state), thicker # at every decay level (a "dim" state outdoors is a blank state), thicker
...@@ -466,7 +472,7 @@ THEMES: dict[str, Theme] = { ...@@ -466,7 +472,7 @@ THEMES: dict[str, Theme] = {
# #df96ef, gate = mint #07ffda, gate2 = cyan #46dbee, family = orange # #df96ef, gate = mint #07ffda, gate2 = cyan #46dbee, family = orange
# #ff7700, other = the chrome's blue, note = the artwork's rose. # #ff7700, other = the chrome's blue, note = the artwork's rose.
fam_hue=(223, 16, 290, 171, 191, 28, 216, 350), fam_hue=(223, 16, 290, 171, 191, 28, 216, 350),
tint_over_page=True, scan_grid=14.0, tint_over_page=True, scan_grid=14.0, word_plate=True,
), ),
# Direct sunlight, and built for nothing else. Near-white page, heavy ink # Direct sunlight, and built for nothing else. Near-white page, heavy ink
# at every decay level (a "dim" state outdoors is a blank state), thicker # at every decay level (a "dim" state outdoors is a blank state), thicker
...@@ -494,6 +500,7 @@ THEMES: dict[str, Theme] = { ...@@ -494,6 +500,7 @@ THEMES: dict[str, Theme] = {
scan=None, scan=None,
bar_frac=0.34, bold=True, ui_font="mono", bar_frac=0.34, bold=True, ui_font="mono",
fam_hue=FAM_HUE, tint_over_page=False, scan_grid=0.0, fam_hue=FAM_HUE, tint_over_page=False, scan_grid=0.0,
word_plate=False,
), ),
} }
# Cycle order for the `d` key and the menu: darkest to brightest page, so the # Cycle order for the `d` key and the menu: darkest to brightest page, so the
...@@ -577,6 +584,14 @@ JERSEY_PATH = Path(__file__).resolve().parent / "ui" / "Jersey15-Regular.ttf" ...@@ -577,6 +584,14 @@ JERSEY_PATH = Path(__file__).resolve().parent / "ui" / "Jersey15-Regular.ttf"
WORD_FACES = ("syne", "jersey") WORD_FACES = ("syne", "jersey")
JERSEY_UI_SIZE = 1.3 # px multiplier when Jersey sets the digits JERSEY_UI_SIZE = 1.3 # px multiplier when Jersey sets the digits
WORD_ALPHA = 0.8 WORD_ALPHA = 0.8
# The DevCon hero, read off algolia.com/devcon (.devcon-hero__headline): Jersey
# 15 at weight 400, letter-spacing -2.72px on 136px (-2%), --devcon-navy. The
# plate is the artwork's own gradient (corail -> magenta -> cyan, measured on
# the intro loop and the header) with its pale grid, because navy type needs
# the bright page the site sets it on.
PLATE_INK = "#14283f"
PLATE_STOPS = ((0.0, "#fd956e"), (0.55, "#df96ef"), (1.0, "#46dbee"))
PLATE_GRID = (0xff, 0xff, 0xff, 46)
# ── the HL feed ──────────────────────────────────────────────────────────── # ── the HL feed ────────────────────────────────────────────────────────────
# PLN, 2026-09-24: "please do indeed sub from the events, to indeed feed # PLN, 2026-09-24: "please do indeed sub from the events, to indeed feed
...@@ -1860,18 +1875,20 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1860,18 +1875,20 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
spacing and kerned sub-advances disagreed, and that arithmetic is spacing and kerned sub-advances disagreed, and that arithmetic is
exactly how "PARVAGU" lost its tail. The bake measures its own exactly how "PARVAGU" lost its tail. The bake measures its own
ink instead (see _word_pixmap).""" ink instead (see _word_pixmap)."""
key = (self._word_face, round(target_w), round(target_h)) plate = self.theme.word_plate
face = "jersey" if plate else self._word_face
key = (face, plate, round(target_w), round(target_h))
if self._word_font_key == key: if self._word_font_key == key:
return self._word_font return self._word_font
fam_id = self._jersey_id if self._word_face == "jersey" \ fam_id = self._jersey_id if face == "jersey" else self._syne_id
else self._syne_id
if fam_id < 0: if fam_id < 0:
return None return None
fam = QtGui.QFontDatabase.applicationFontFamilies(fam_id)[0] fam = QtGui.QFontDatabase.applicationFontFamilies(fam_id)[0]
f = QtGui.QFont(fam) f = QtGui.QFont(fam)
f.setPixelSize(max(6, target_h * 0.92)) f.setPixelSize(max(6, target_h * 0.92))
f.setLetterSpacing(QtGui.QFont.SpacingType.PercentageSpacing, f.setLetterSpacing(QtGui.QFont.SpacingType.PercentageSpacing,
106.0) # +0.06em, as the hero tracks 98.0 if plate # -2%, the DevCon hero
else 106.0) # +0.06em, as the hero tracks
self._word_font, self._word_font_key = f, key self._word_font, self._word_font_key = f, key
return f return f
...@@ -1914,14 +1931,16 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1914,14 +1931,16 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
PARVAGU"), and it must FIT the D1 cell at the cell's margin — PARVAGU"), and it must FIT the D1 cell at the cell's margin —
which means the TYPE shrinks to the width, honestly, rather than which means the TYPE shrinks to the width, honestly, rather than
a squash that would make the letters strangers to themselves.""" a squash that would make the letters strangers to themselves."""
key = (round(span_w), round(span_h)) plate = self.theme.word_plate
key = (round(span_w), round(span_h), plate)
if self._word_pm_key == key: if self._word_pm_key == key:
return self._word_pm return self._word_pm
f = self._wave_font(span_w, span_h) f = self._wave_font(span_w, span_h)
if f is None: if f is None:
return None return None
ink_col = QtGui.QColor(PLATE_INK if plate else "#d900ff")
def bake(px, canvas_w, glow=True): def bake(px, canvas_w, glow=not plate):
fb = QtGui.QFont(f) fb = QtGui.QFont(f)
fb.setPixelSize(max(4, px)) fb.setPixelSize(max(4, px))
fmb = QtGui.QFontMetricsF(fb) fmb = QtGui.QFontMetricsF(fb)
...@@ -1947,9 +1966,10 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1947,9 +1966,10 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
qp.translate(math.cos(a) * rad, math.sin(a) * rad) qp.translate(math.cos(a) * rad, math.sin(a) * rad)
self._draw_word_layer(qp, base, pad_b, fmb, glow_pen) self._draw_word_layer(qp, base, pad_b, fmb, glow_pen)
qp.restore() qp.restore()
# the core: --neon-high itself, the site's own pink # the core: --neon-high itself, the site's own pink (or,
# on the plate, the DevCon navy -- pure, no glow)
self._draw_word_layer(qp, base, pad_b, fmb, self._draw_word_layer(qp, base, pad_b, fmb,
QtGui.QPen(QtGui.QColor("#d900ff"))) QtGui.QPen(ink_col))
finally: finally:
qp.end() qp.end()
return img, pad_b return img, pad_b
...@@ -1989,9 +2009,36 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1989,9 +2009,36 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
min(w_, right - left + 1 + pad), min(w_, right - left + 1 + pad),
img.height()) img.height())
pm = QtGui.QPixmap.fromImage(cropped) pm = QtGui.QPixmap.fromImage(cropped)
if plate:
pm = self._plate_pixmap(span_w, span_h, pm)
self._word_pm, self._word_pm_key = pm, key self._word_pm, self._word_pm_key = pm, key
return pm return pm
def _plate_pixmap(self, span_w, span_h, word_pm):
"""The DevCon hero as one baked picture: gradient, grid, word.
Built once per geometry like the word itself, so a frame is still
one blit and constructs no gradient."""
pw, ph = max(1, round(span_w)), max(1, round(span_h))
pm = QtGui.QPixmap(pw, ph)
pm.fill(QtGui.QColor(0, 0, 0, 0))
qp = QtGui.QPainter(pm)
try:
g = QtGui.QLinearGradient(0, 0, pw, ph)
for at, col in PLATE_STOPS:
g.setColorAt(at, QtGui.QColor(col))
qp.fillRect(0, 0, pw, ph, QtGui.QBrush(g))
qp.setPen(QtGui.QColor(*PLATE_GRID))
step = max(4, round(ph / 5))
for x in range(step, pw, step):
qp.drawLine(x, 0, x, ph)
for y in range(step, ph, step):
qp.drawLine(0, y, pw, y)
qp.drawPixmap(round((pw - word_pm.width()) / 2.0),
round((ph - word_pm.height()) / 2.0), word_pm)
finally:
qp.end()
return pm
def _col_has_ink(self, img, x): def _col_has_ink(self, img, x):
return any(img.pixelColor(x, y).alpha() > 8 return any(img.pixelColor(x, y).alpha() > 8
for y in range(img.height())) for y in range(img.height()))
...@@ -2007,7 +2054,9 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -2007,7 +2054,9 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
if pm is None: if pm is None:
return return
row_y = self.my + 3 * self.ch # PHYSICAL_ORDER index of D row_y = self.my + 3 * self.ch # PHYSICAL_ORDER index of D
p.setOpacity(WORD_ALPHA) # the plate is a sticker, opaque: idle marks bleeding through
# read as dirt on the gradient
p.setOpacity(1.0 if self.theme.word_plate else WORD_ALPHA)
try: try:
# clamped to the span's outer rect: at the smallest windows # clamped to the span's outer rect: at the smallest windows
# the glow ring can reach 1px past the inner margin, and a # the glow ring can reach 1px past the inner margin, and a
......
...@@ -1165,3 +1165,19 @@ def test_devcon_sets_its_digits_in_jersey_and_the_ribbon_keeps_its_grid(): ...@@ -1165,3 +1165,19 @@ def test_devcon_sets_its_digits_in_jersey_and_the_ribbon_keeps_its_grid():
assert w._hex_mono is False assert w._hex_mono is False
w.set_theme("dark", save=False) w.set_theme("dark", save=False)
assert "Jersey" not in w.f_main.family() and w._hex_mono is True assert "Jersey" not in w.f_main.family() and w._hex_mono is True
def test_only_devcon_wears_the_hero_plate_and_it_fills_its_span():
import pytest
mv, w = _themed("devcon")
if mv is None:
pytest.skip("no Qt available")
assert [n for n, th in mv.THEMES.items() if th.word_plate] == ["devcon"]
if w._jersey_id < 0:
pytest.skip("Jersey not vendored on this checkout")
pm = w._word_pixmap(300.0, 60.0)
assert (pm.width(), pm.height()) == (300, 60), "the plate is the span, exactly"
# a theme switch must not reuse the plate as the neon word
w.set_theme("dark", save=False)
neon = w._word_pixmap(300.0, 60.0)
assert neon is not pm
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment