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
# 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
WORD_CONDENSE_FLOOR = 0.75 # never squash the bloc thinner than this
# ── the HL feed ────────────────────────────────────────────────────────────
# PLN, 2026-09-24: "please do indeed sub from the events, to indeed feed
......@@ -494,6 +495,8 @@ WORD_ALPHA = 0.8
# default at runtime (--no-hl for the off switch): a lens that only shows
# the controls was half the picture.
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 ──────
# 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,
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."""
"""Syne ExtraBold, sized to fill the row height as far as the
span allows at a 0.75 condense floor — PLN, 2026-09-24: "letters
could be a bit taller, like atm they occupy flattened rectangles
boxes, can be more square?" The arithmetic is honest: nine
ExtraBold Syne glyphs are ~9.4 em wide, three columns ~3.9 em, so
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))
if self._word_font_key == key:
return self._word_font
......@@ -1719,26 +1727,34 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
if fam_id < 0:
return None
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.setPixelSize(max(6, target_h * 0.62))
f.setPixelSize(max(6, px))
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)))
106.0)
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."""
`textShadow` in miniature, but TIGHT — PLN: "a little less
blurry? esp made fullscreen it gets very blurry" — because a wide
ring halo at 60px of type is indistinguishable from an out-of-
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))
if self._word_pm_key == key:
return self._word_pm
......@@ -1747,10 +1763,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
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,
def measure():
return sum(fm.horizontalAdvance(c) for c in word) \
+ fm.horizontalAdvance(" ") * 0.06 * (len(word) - 1)
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)
img.fill(0)
qp = QtGui.QPainter(img)
......@@ -1758,25 +1778,23 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
qp.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing,
True)
qp.setFont(f)
qp.scale(sx, 1.0)
asc = fm.ascent()
base = pad + (span_h + asc - fm.descent()) / 2.0
x0 = pad
def draw_word(pen):
qp.setPen(pen)
x = x0
x = float(pad)
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)):
+ fm.horizontalAdvance(" ") * 0.06
# the glow: two CLOSE rings, faint — a rim of neon, not a fog
r0 = max(1.0, f.pixelSize() * 0.09)
for rad, alpha in ((r0, 40), (r0 * 0.45, 60)):
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
glow.setWidthF(max(1.5, f.pixelSize() * 0.07))
for i in range(10):
a = 2 * math.pi * i / 10
qp.save()
qp.translate(math.cos(a) * rad, math.sin(a) * rad)
draw_word(glow)
......@@ -2168,26 +2186,27 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self.lut[FAM_FX2][14])
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
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.
moving several controls together, for readability" — then, of the
first cut's decay-driven widths: "its weird moving a djf makes
the e.g. col1 grow then shrink over time?" He was right: a width
that breathes on a decay curve is motion with no meaning. Now the
most recently touched column HOLDS 2× while it is in use and the
others give up a little width; the focus switches when another
column is touched and everything relaxes to uniform after
FOCUS_HOLD of silence. The only transitions are caused by hands.
"""
weights = []
focus, last = None, 0.0
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))
if self.col_t[col] > last:
focus, last = col, self.col_t[col]
if focus is None or now - last > FOCUS_HOLD:
weights = [1.0] * 8
else:
weights.append(1.0)
weights = [2.0 if c == focus else 1.0 for c in range(1, 9)]
total = self.mw_px
scale = total / sum(weights)
out, x = [], self.mx
......
......@@ -19,6 +19,7 @@ from __future__ import annotations
import socket
import struct
import sys
import threading
import time
from collections import deque
......@@ -182,6 +183,12 @@ class HLFeed:
self._q.append({"orbit": o, "t": now,
"params": msg["args"]})
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:
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