Commit 05db016f by PLN (Algolia)

midiviz: the focus column holds instead of breathing; the wordmark sharpens

PLN: 'its weird moving a djf makes the col grow then shrink over time' —
width on a decay curve was motion with no meaning; the played column now
HOLDS 2x while in use, hands cause the only transitions, uniform after
FOCUS_HOLD of silence. Wordmark: two tight glow rings instead of a wide
fog, tighter tracking, condense floored at 0.75 — nine ExtraBold glyphs
are ~9.4em and three columns ~3.9em, so the rest goes to height. oscfeed
says 'first trigger received (dN)' once, so 'is Tidal even sending' is a
journalctl away.
parent f07fe81d
...@@ -484,6 +484,7 @@ WAVE_ALPHA = 0.12 ...@@ -484,6 +484,7 @@ WAVE_ALPHA = 0.12
# vendored out of the built site for the same standalone reason as the wave. # vendored out of the built site for the same standalone reason as the wave.
SYNE_PATH = Path(__file__).resolve().parent / "ui" / "syne-extrabold.ttf" SYNE_PATH = Path(__file__).resolve().parent / "ui" / "syne-extrabold.ttf"
WORD_ALPHA = 0.8 WORD_ALPHA = 0.8
WORD_CONDENSE_FLOOR = 0.75 # never squash the bloc thinner than this
# ── 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
...@@ -494,6 +495,8 @@ WORD_ALPHA = 0.8 ...@@ -494,6 +495,8 @@ WORD_ALPHA = 0.8
# default at runtime (--no-hl for the off switch): a lens that only shows # default at runtime (--no-hl for the off switch): a lens that only shows
# the controls was half the picture. # the controls was half the picture.
HL_ON_BY_DEFAULT = True HL_ON_BY_DEFAULT = True
# How long the focus column keeps its 2× after the last touch of it.
FOCUS_HOLD = 4.0
# ── the size belongs to the window manager, the density to the window ────── # ── the size belongs to the window manager, the density to the window ──────
# PLN, 2026-09-22: "only drag-> grab atm, can we have corner grabs to make it # PLN, 2026-09-22: "only drag-> grab atm, can we have corner grabs to make it
...@@ -1706,12 +1709,17 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1706,12 +1709,17 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
return self._wave_pm return self._wave_pm
def _wave_font(self, target_w: float, target_h: float): def _wave_font(self, target_w: float, target_h: float):
"""Syne ExtraBold, letter-spaced like the site's hero (0.15em), """Syne ExtraBold, sized to fill the row height as far as the
sized to fit the D1-D4 span exactly. The vendored ttf is the span allows at a 0.75 condense floor — PLN, 2026-09-24: "letters
variable font INSTANCED at 800, not the family default — Qt could be a bit taller, like atm they occupy flattened rectangles
resolves a variable font's default instance (Regular) however boxes, can be more square?" The arithmetic is honest: nine
loudly you ask for a weight, which is how the first cut read as ExtraBold Syne glyphs are ~9.4 em wide, three columns ~3.9 em, so
thin as a system sans.""" square letters would need either a 0.42× squash (its own
distortion) or a wider span. The compromise: condense at most to
0.75, and let the type take whatever height still fits. The
vendored ttf is the variable font INSTANCED at 800 — Qt resolves
a variable font's default instance (Regular) however loudly you
ask for a weight, which is how the first cut read thin."""
key = (round(target_w), round(target_h)) key = (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
...@@ -1719,26 +1727,34 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1719,26 +1727,34 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
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]
probe = QtGui.QFont(fam)
probe.setPixelSize(40)
probe.setLetterSpacing(QtGui.QFont.SpacingType.PercentageSpacing,
106.0) # +0.06em — tighter tracking
fmr = QtGui.QFontMetricsF(probe)
word = "PARVAGUES"
em_ratio = (sum(fmr.horizontalAdvance(c) for c in word)
+ fmr.horizontalAdvance(" ") * 0.06 * (len(word) - 1)
) / 40.0
px = min(target_h * 0.92,
target_w / (WORD_CONDENSE_FLOOR * em_ratio))
f = QtGui.QFont(fam) f = QtGui.QFont(fam)
f.setPixelSize(max(6, target_h * 0.62)) f.setPixelSize(max(6, px))
f.setLetterSpacing(QtGui.QFont.SpacingType.PercentageSpacing, f.setLetterSpacing(QtGui.QFont.SpacingType.PercentageSpacing,
115.0) # +0.15em, as tracked 106.0)
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 self._word_font, self._word_font_key = f, key
return f return f
def _word_pixmap(self, span_w: float, span_h: float): def _word_pixmap(self, span_w: float, span_h: float):
"""The wordmark BAKED — glyphs, tracking and neon glow rendered """The wordmark BAKED — glyphs, tracking and neon glow rendered
once per geometry, not per frame. The glow is the site hero's 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 `textShadow` in miniature, but TIGHT — PLN: "a little less
of translucent magenta under a solid core. A per-frame fake of blurry? esp made fullscreen it gets very blurry" — because a wide
this would cost two dozen drawText calls; baked, the frame pays ring halo at 60px of type is indistinguishable from an out-of-
one blit.""" focus lens. Two close rings only; the core carries the colour.
Width overrun is taken by a vector condense (a scaled painter,
still antialiased), never by raster scaling — so fullscreen
rebakes stay sharp."""
key = (round(span_w), round(span_h)) key = (round(span_w), round(span_h))
if self._word_pm_key == key: if self._word_pm_key == key:
return self._word_pm return self._word_pm
...@@ -1747,10 +1763,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1747,10 +1763,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
return None return None
fm = QtGui.QFontMetricsF(f) fm = QtGui.QFontMetricsF(f)
word = "PARVAGUES" word = "PARVAGUES"
total = sum(fm.horizontalAdvance(c) for c in word) \ def measure():
+ fm.horizontalAdvance(" ") * 0.15 * (len(word) - 1) return sum(fm.horizontalAdvance(c) for c in word) \
pad = max(2, int(f.pixelSize() * 0.35)) + fm.horizontalAdvance(" ") * 0.06 * (len(word) - 1)
img = QtGui.QImage(round(total) + pad * 2, round(span_h) + pad * 2, total = measure()
sx = min(1.0, span_w / max(1.0, total)) # the condense factor
pad = max(2, int(f.pixelSize() * 0.22))
img = QtGui.QImage(round(span_w * sx) + pad * 2,
round(span_h) + pad * 2,
QtGui.QImage.Format.Format_ARGB32_Premultiplied) QtGui.QImage.Format.Format_ARGB32_Premultiplied)
img.fill(0) img.fill(0)
qp = QtGui.QPainter(img) qp = QtGui.QPainter(img)
...@@ -1758,25 +1778,23 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1758,25 +1778,23 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
qp.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, qp.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing,
True) True)
qp.setFont(f) qp.setFont(f)
qp.scale(sx, 1.0)
asc = fm.ascent() asc = fm.ascent()
base = pad + (span_h + asc - fm.descent()) / 2.0 base = pad + (span_h + asc - fm.descent()) / 2.0
x0 = pad
def draw_word(pen): def draw_word(pen):
qp.setPen(pen) qp.setPen(pen)
x = x0 x = float(pad)
for c in word: for c in word:
qp.drawText(QtCore.QPointF(x, base), c) qp.drawText(QtCore.QPointF(x, base), c)
x += fm.horizontalAdvance(c) \ x += fm.horizontalAdvance(c) \
+ fm.horizontalAdvance(" ") * 0.15 + fm.horizontalAdvance(" ") * 0.06
# the glow: three rings of translucent neon, widest first — # the glow: two CLOSE rings, faint — a rim of neon, not a fog
# the cheap cousin of a blur, and per-frame it is free r0 = max(1.0, f.pixelSize() * 0.09)
r0 = max(1.0, f.pixelSize() * 0.16) for rad, alpha in ((r0, 40), (r0 * 0.45, 60)):
for rad, alpha in ((r0 * 1.8, 28), (r0, 44), (r0 * 0.5, 70)):
glow = QtGui.QPen(QtGui.QColor(217, 0, 255, alpha)) glow = QtGui.QPen(QtGui.QColor(217, 0, 255, alpha))
glow.setWidthF(max(2.0, f.pixelSize() * 0.10)) glow.setWidthF(max(1.5, f.pixelSize() * 0.07))
n = 12 for i in range(10):
for i in range(n): a = 2 * math.pi * i / 10
a = 2 * math.pi * i / n
qp.save() qp.save()
qp.translate(math.cos(a) * rad, math.sin(a) * rad) qp.translate(math.cos(a) * rad, math.sin(a) * rad)
draw_word(glow) draw_word(glow)
...@@ -2168,26 +2186,27 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -2168,26 +2186,27 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self.lut[FAM_FX2][14]) self.lut[FAM_FX2][14])
def _col_geo(self, now): def _col_geo(self, now):
"""Per-column (x, width) with a recency-weighted focus column. """Per-column (x, width) with a STEADY focus column.
PLN, 2026-09-24: "can we have 2x wider matrix column, shrinking PLN, 2026-09-24: "can we have 2x wider matrix column, shrinking
slightly other cols, and algorithmically minimize overlaps when slightly other cols, and algorithmically minimize overlaps when
moving several controls together, for readability". The algorithm moving several controls together, for readability" — then, of the
reuses the column glow's OWN decay as the focus weight: width first cut's decay-driven widths: "its weird moving a djf makes
w(col) = 1 + charge(col), charge = the TAU_COL level normalized to the e.g. col1 grow then shrink over time?" He was right: a width
0..1. Full charge is exactly 2× a cold column, the others ease that breathes on a decay curve is motion with no meaning. Now the
narrower to pay for it, several columns moving together share the most recently touched column HOLDS 2× while it is in use and the
extra room smoothly (no snaps — the widths ride the same decay the others give up a little width; the focus switches when another
glow does), and the resting matrix is perfectly uniform again. column is touched and everything relaxes to uniform after
Total width is invariant, so nothing outside the matrix moves. FOCUS_HOLD of silence. The only transitions are caused by hands.
""" """
weights = [] focus, last = None, 0.0
for col in range(1, 9): for col in range(1, 9):
if self.col_t[col]: if self.col_t[col] > last:
lv = self._lv(now - self.col_t[col], tau=TAU_COL) focus, last = col, self.col_t[col]
weights.append(1.0 + lv / (LEVELS - 1)) if focus is None or now - last > FOCUS_HOLD:
else: weights = [1.0] * 8
weights.append(1.0) else:
weights = [2.0 if c == focus else 1.0 for c in range(1, 9)]
total = self.mw_px total = self.mw_px
scale = total / sum(weights) scale = total / sum(weights)
out, x = [], self.mx out, x = [], self.mx
......
...@@ -19,6 +19,7 @@ from __future__ import annotations ...@@ -19,6 +19,7 @@ from __future__ import annotations
import socket import socket
import struct import struct
import sys
import threading import threading
import time import time
from collections import deque from collections import deque
...@@ -182,6 +183,12 @@ class HLFeed: ...@@ -182,6 +183,12 @@ class HLFeed:
self._q.append({"orbit": o, "t": now, self._q.append({"orbit": o, "t": now,
"params": msg["args"]}) "params": msg["args"]})
self.total += 1 self.total += 1
first = self.total == 1
if first:
# One line, once: the question "is Tidal even sending?" is
# answered by the journal, not by staring at a silent grid.
print("⚓ HL feed: first trigger received (d%d)" % o,
file=sys.stderr, flush=True)
def alive(self) -> bool: def alive(self) -> bool:
return self._sock is not None and self.error is None return self._sock is not None and self.error is None
......
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