Commit 2ecba716 by PLN (Algolia)

midiviz: the wordmark is its own lens, bolted to the DJFs

Syne instanced at 800 (Qt renders a variable font's default instance no
matter the weight asked — the first cut read Regular), glow baked once per
geometry, word on 'l' separate from the wave on 'w'. Bolted to D1-D3 per
PLN — 'over the djfs'. The wave yields on shrink: clamped to the space
right of the word's span, overlap is impossible by construction.
parent b0170751
......@@ -476,6 +476,14 @@ BASE_W, BASE_H = 420, 260
# tree on the box at all.
WAVE_PATH = Path(__file__).resolve().parent / "ui" / "parvagues_wave.png"
WAVE_ALPHA = 0.12
# …and its wordmark, over D1-D4 (PLN, 2026-09-24: "PARVAGUES in nice our
# usual site font bloc letters over D1/2/3/4 alongside the wave same
# brighter pink highlight"). The site's display face is Syne (next/font,
# --font-syne, weight 800, tracking 0.15em, uppercase — Hero.js is the
# recipe) and the pink is --neon-high. The variable font's latin subset is
# vendored out of the built site for the same standalone reason as the wave.
SYNE_PATH = Path(__file__).resolve().parent / "ui" / "syne-extrabold.ttf"
WORD_ALPHA = 0.8
# ── the HL feed ────────────────────────────────────────────────────────────
# PLN, 2026-09-24: "please do indeed sub from the events, to indeed feed
......@@ -1177,7 +1185,20 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self.hl_t: dict[int, float] = {}
self._hl_cells_cache: dict[int, list[int]] = {}
self.wave = False # the ParVagues wave, behind all
self.word = False # …and the PARVAGUES lettering
self._wave_pm: QtGui.QPixmap | None = None
# The wordmark's face, registered once. A checkout without the
# asset leaves the id at -1 and the word simply never draws — the
# wave is decoration, and decoration never gets to crash the lens.
try:
self._syne_id = QtGui.QFontDatabase.addApplicationFont(
str(SYNE_PATH))
except Exception:
self._syne_id = -1
self._word_font = None
self._word_font_key = None
self._word_pm = None
self._word_pm_key = None
# The backdrop. `spec_src` is None until somebody asks: OFF means
# no thread and no subprocess, not a hidden one.
......@@ -1684,11 +1705,120 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self._wave_pm = pm
return self._wave_pm
def _wave_font(self, target_w: float, target_h: float):
"""Syne ExtraBold, letter-spaced like the site's hero (0.15em),
sized to fit the D1-D4 span exactly. The vendored ttf is the
variable font INSTANCED at 800, not the family default — Qt
resolves a variable font's default instance (Regular) however
loudly you ask for a weight, which is how the first cut read as
thin as a system sans."""
key = (round(target_w), round(target_h))
if self._word_font_key == key:
return self._word_font
fam_id = self._syne_id
if fam_id < 0:
return None
fam = QtGui.QFontDatabase.applicationFontFamilies(fam_id)[0]
f = QtGui.QFont(fam)
f.setPixelSize(max(6, target_h * 0.62))
f.setLetterSpacing(QtGui.QFont.SpacingType.PercentageSpacing,
115.0) # +0.15em, as tracked
fm = QtGui.QFontMetricsF(f)
word = "PARVAGUES"
total = sum(fm.horizontalAdvance(c) for c in word) \
+ fm.horizontalAdvance(" ") * 0.15 * (len(word) - 1)
f.setPixelSize(max(6, target_h * 0.62
* target_w / max(1.0, total)))
self._word_font, self._word_font_key = f, key
return f
def _word_pixmap(self, span_w: float, span_h: float):
"""The wordmark BAKED — glyphs, tracking and neon glow rendered
once per geometry, not per frame. The glow is the site hero's
`textShadow: 0 0 60px rgba(217,0,255,0.15)` in miniature: rings
of translucent magenta under a solid core. A per-frame fake of
this would cost two dozen drawText calls; baked, the frame pays
one blit."""
key = (round(span_w), round(span_h))
if self._word_pm_key == key:
return self._word_pm
f = self._wave_font(span_w, span_h)
if f is None:
return None
fm = QtGui.QFontMetricsF(f)
word = "PARVAGUES"
total = sum(fm.horizontalAdvance(c) for c in word) \
+ fm.horizontalAdvance(" ") * 0.15 * (len(word) - 1)
pad = max(2, int(f.pixelSize() * 0.35))
img = QtGui.QImage(round(total) + pad * 2, round(span_h) + pad * 2,
QtGui.QImage.Format.Format_ARGB32_Premultiplied)
img.fill(0)
qp = QtGui.QPainter(img)
try:
qp.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing,
True)
qp.setFont(f)
asc = fm.ascent()
base = pad + (span_h + asc - fm.descent()) / 2.0
x0 = pad
def draw_word(pen):
qp.setPen(pen)
x = x0
for c in word:
qp.drawText(QtCore.QPointF(x, base), c)
x += fm.horizontalAdvance(c) \
+ fm.horizontalAdvance(" ") * 0.15
# the glow: three rings of translucent neon, widest first —
# the cheap cousin of a blur, and per-frame it is free
r0 = max(1.0, f.pixelSize() * 0.16)
for rad, alpha in ((r0 * 1.8, 28), (r0, 44), (r0 * 0.5, 70)):
glow = QtGui.QPen(QtGui.QColor(217, 0, 255, alpha))
glow.setWidthF(max(2.0, f.pixelSize() * 0.10))
n = 12
for i in range(n):
a = 2 * math.pi * i / n
qp.save()
qp.translate(math.cos(a) * rad, math.sin(a) * rad)
draw_word(glow)
qp.restore()
# the core: --neon-high itself, the site's own pink
draw_word(QtGui.QPen(QtGui.QColor("#d900ff")))
finally:
qp.end()
pm = QtGui.QPixmap.fromImage(img)
self._word_pm, self._word_pm_key = pm, key
return pm
def _paint_word(self, p):
"""PARVAGUES over D1-D3 — PLN: "bolted to D1-D3 even better as
over the djfs :D" — in the site's own display face."""
span_w, span_h = 3 * self.cw, self.ch
pm = self._word_pixmap(span_w, span_h)
if pm is None:
return
row_y = self.my + 3 * self.ch # PHYSICAL_ORDER index of D
p.setOpacity(WORD_ALPHA)
try:
p.drawPixmap(round(self.mx + (span_w - pm.width()) / 2.0),
round(row_y + (span_h - pm.height()) / 2.0), pm)
finally:
p.setOpacity(1.0)
def _paint_wave(self, p, w, h):
pm = self._wave_pixmap()
if pm is None:
return
# PLN, 2026-09-24: "right now when i shrink it overlaps with wave,
# should never, position them as we shrink/grow smartly". The word
# is BOLTED to D1-D3 — that is its meaning — so the wave is the
# one that yields: its size clamps to whatever stands right of the
# word's span. At any window size, no overlap, by construction.
side = int(h * 0.92)
if self.word:
clear = w - (self.mx + 3 * self.cw) - 0.5 * self.cw
side = min(side, int(clear))
if side < 24:
return # too narrow to be anything but noise
scaled = pm.scaled(side, side,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
......@@ -1701,6 +1831,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
finally:
p.setOpacity(1.0)
def set_word(self, on: bool):
self.word = bool(on)
self._sync_menu()
self.update()
def toggle_word(self):
self.set_word(not self.word)
# ── frame ──────────────────────────────────────────────────────────
def tick(self):
if _QUIT.is_set():
......@@ -1858,6 +1996,8 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
# the opaque cells it survived only in the margins.
if self.wave:
self._paint_wave(p, w, h)
if self.word:
self._paint_word(p)
self._scanlines_blit(p)
self.painted += 1
finally:
......@@ -2027,7 +2167,40 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
p.fillRect(self.gut_x + pw * 4, top + 1, pw, hh - 1,
self.lut[FAM_FX2][14])
def _col_geo(self, now):
"""Per-column (x, width) with a recency-weighted focus column.
PLN, 2026-09-24: "can we have 2x wider matrix column, shrinking
slightly other cols, and algorithmically minimize overlaps when
moving several controls together, for readability". The algorithm
reuses the column glow's OWN decay as the focus weight: width
w(col) = 1 + charge(col), charge = the TAU_COL level normalized to
0..1. Full charge is exactly 2× a cold column, the others ease
narrower to pay for it, several columns moving together share the
extra room smoothly (no snaps — the widths ride the same decay the
glow does), and the resting matrix is perfectly uniform again.
Total width is invariant, so nothing outside the matrix moves.
"""
weights = []
for col in range(1, 9):
if self.col_t[col]:
lv = self._lv(now - self.col_t[col], tau=TAU_COL)
weights.append(1.0 + lv / (LEVELS - 1))
else:
weights.append(1.0)
total = self.mw_px
scale = total / sum(weights)
out, x = [], self.mx
for wcol in weights:
w = wcol * scale
out.append((x, w))
x += w
return out
def _paint_matrix(self, p, now, pulse):
mx, my, ch = self.mx, self.my, self.ch
cw = self.cw # the resting width, for chrome
geo = self._col_geo(now)
mx, my, cw, ch = self.mx, self.my, self.cw, self.ch
# per-orbit charge glow behind each column: "this orbit is live"
for col in range(1, 9):
......@@ -2035,11 +2208,12 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
continue
lv = self._lv(now - self.col_t[col], tau=TAU_COL)
if lv > 1:
x, wcol = geo[col - 1]
# A SURFACE, not ink: this is the slab a live column stands
# on. Sampled from the ink ramp it would paint a dark bar
# over a white page and hide the very column it advertises.
p.fillRect(round(mx + (col - 1) * cw), round(my),
max(1, round(cw) - self.inset), round(self.mh_px),
p.fillRect(round(x), round(my),
max(1, round(wcol) - self.inset), round(self.mh_px),
self.tint[FAM_LEVEL][min(2, lv // 6)])
p.setFont(self.f_micro)
......@@ -2049,8 +2223,9 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
lv = self._lv(now - self.col_t[col], floor=4, tau=TAU_COL) \
if self.col_t[col] \
else 4 + int(2 * pulse)
x, wcol = geo[col - 1]
p.setPen(self.lut[FAM_LEVEL][min(LEVELS - 1, lv)])
p.drawText(round(mx + (col - 1) * cw + (cw - self.uw) / 2),
p.drawText(round(x + (wcol - self.uw) / 2),
round(col_base), HEX[col])
# row letters: the grid's own single-glyph names, on the digits' baseline
p.setPen(self.chrome[4 + int(3 * pulse)])
......@@ -2068,7 +2243,11 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
# the map you consult while wiring; the value digits stay the thing
# your eye tracks while playing. Skipped entirely when the cell is
# too narrow to hold the digits without colliding with the value.
cc_room = round(self.cw) - self.inset >= self.uw * 2 + 8
# Room is judged on the NARROWEST column, so a reference digit
# that fits in one cell fits in all of them — a mixed-width pass
# would read as different maps per row.
cc_room = min(round(geo[i][1]) for i in range(8)) - self.inset \
>= self.uw * 2 + 8
if cc_room:
p.setPen(self.chrome[5])
for r, row in enumerate(grid.PHYSICAL_ORDER):
......@@ -2076,22 +2255,24 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
cc = grid.CELL_TO_CC.get((row, col))
if cc is None:
continue
x, wcol = geo[col - 1]
txt = "%d" % cc
p.drawText(
round(mx + (col - 1) * cw + (round(self.cw) - self.inset)
round(x + (round(wcol) - self.inset)
- self.uw * len(txt) - 2),
round(my + r * ch + self.u_asc + 1), txt)
two = self.cw > self.mw * 2.4
two = min(round(geo[i][1]) for i in range(8)) > self.mw * 2.4
p.setFont(self.f_main)
for r, row in enumerate(grid.PHYSICAL_ORDER):
for col in range(1, 9):
cc = grid.CELL_TO_CC.get((row, col))
if cc is None:
continue
x = round(mx + (col - 1) * cw)
x, wcol = geo[col - 1]
x = round(x)
y = round(my + r * ch)
cwi = max(2, round(cw) - self.inset)
cwi = max(2, round(wcol) - self.inset)
chi = max(2, round(ch) - self.inset)
fam = self._fam_for_cc(cc)
st = self.cc.get(cc)
......@@ -2349,6 +2530,38 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self._user_closed = True
self.close()
def restart(self):
"""Re-exec in place — same process, same argv, fresh code.
PLN, 2026-09-24: "add to mini menu a restart entry so we can
reload easily on such updates". midiviz iterates fast and its unit
restarts it on failure, but a code update needs a hand reload, and
the old path was Quit and pray the converger agreed. os.execv
replaces the process WITHOUT exiting, so systemd never sees an
exit code at all: no latch, no restart counter, no window flap —
the lens blinks and comes back with the new source. Everything
with a thread or a subprocess is torn down first, exactly as
closeEvent would, because none of it survives execv.
"""
self.timer.stop()
if self.rebind_timer is not None:
self.rebind_timer.stop()
if self.reader is not None:
self.reader.close()
if self.hl is not None:
self.hl.close()
self.hl = None
if self.spec_src is not None:
self.spec_src.close()
self.spec_src = None
self._save()
if self.tray is not None:
self.tray.hide()
# If the exec itself fails, the exception escapes and the process
# dies non-78 — which Restart=always reads as "a failure", and
# brings the lens back anyway. There is no path to a blank desk.
os.execv(sys.executable, [sys.executable] + sys.argv)
def toggle_pause(self):
self.paused = not self.paused
self._sync_menu()
......@@ -2384,6 +2597,10 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self.toggle_hl()
elif k == Qt.Key.Key_W:
self.toggle_wave()
elif k == Qt.Key.Key_L:
self.toggle_word()
elif k == Qt.Key.Key_R:
self.restart()
elif k == Qt.Key.Key_D:
self.cycle_theme()
elif k == Qt.Key.Key_C:
......@@ -2561,6 +2778,10 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self._act_wave.setCheckable(True)
self._act_wave.triggered.connect(lambda _c=False: self.toggle_wave())
m.addAction(self._act_wave)
self._act_word = QtGui.QAction("PARVAGUES lettering\tl", self)
self._act_word.setCheckable(True)
self._act_word.triggered.connect(lambda _c=False: self.toggle_word())
m.addAction(self._act_word)
self._act_pin = QtGui.QAction("Always on top\tt", self)
self._act_pin.setCheckable(True)
self._act_pin.triggered.connect(
......@@ -2575,6 +2796,9 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self._act_show.triggered.connect(
lambda _c=False: self.set_window_shown(not self.isVisible()))
m.addAction(self._act_show)
self._act_restart = QtGui.QAction("Restart\tR", self)
self._act_restart.triggered.connect(lambda _c=False: self.restart())
m.addAction(self._act_restart)
m.addSeparator()
a = QtGui.QAction("Quit\tq", self)
......@@ -2618,6 +2842,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self._act_spec.setChecked(self.spectro)
self._act_hl.setChecked(self.hl is not None)
self._act_wave.setChecked(self.wave)
self._act_word.setChecked(self.word)
self._act_pin.setChecked(self.on_top)
self._act_show.setChecked(self.isVisible())
......
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