A faded chip keeps its node's colours, with less chroma
The part of a route that the user does not compose (a mirrored return leg, the return home) drew every hop as the same dark slate chip. Each faded chip now has the colours of its own node at FADE_CHROMA of their chroma, at the same lightness, so it still says which node it is. The PicoCalc has no colour of less chroma: there a faded hop keeps its label colour and drops its fill. Arrows keep faint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
796ff94dad
commit
bd6df0b9fa
5 files changed
+97
-45
No files matched your search
+6
-1
@@ -20,7 +20,12 @@ can change how things behave. It does not only add new things.
|
||||
chat. Before, your messages showed only the `★`.
|
||||
- **Your node's star is white.** The `★` that marks your node on the map, in routes, in
|
||||
lists, and in a chat is white, the same white as your name in the lists. Before, it was
|
||||
yellow. A star in a faded part of a route stays grey.
|
||||
yellow.
|
||||
- **The faded part of a route keeps the colours of its nodes.** The part of a route that
|
||||
you do not choose, for example the mirrored way back of a trace, shows each node in the
|
||||
colour of its key, but less vivid. Thus you can still see which node each one is.
|
||||
Before, each of these nodes was the same grey box. When a route is drawn with arrows,
|
||||
the faded part stays grey.
|
||||
- **In a chat, a name that MeshTerm cannot match to a node is black on grey.** The box
|
||||
around a sender or a mention with no known node has black text on a grey box. Thus it
|
||||
does not stand out among the coloured names. Before, it was light blue on slate.
|
||||
|
||||
@@ -423,7 +423,8 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
|
||||
`marks.SELF_MARK` carries the theme name `you` — the white of our name — so the console
|
||||
picks its own white, and a raster resolves it through `mark_rgb`. It was the map's
|
||||
yellow, then briefly our key's hue (set from the self-info at run time), and both were
|
||||
dropped. A star something *mutes* keeps its grey — a faded hop, a dimmed return leg.
|
||||
dropped. In arrows, a star something *mutes* keeps its `faint` grey — a faded hop, a
|
||||
dimmed return leg; a faded *chip* only loses chroma, and white has none to lose.
|
||||
- Path cuts: a path line drawn as **chips** that gets cut — a lane that ran out, a row
|
||||
scrolled past its edge — breaks the chip off on a half block in that chip's own fill
|
||||
(`▐` ends a line, `▌` opens one; `pathline.cut_mark`/`cut_to`), so half the cell is
|
||||
@@ -443,8 +444,12 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
|
||||
merged neighbouring greens). The PicoCalc is the same rule at its resolution: the fill
|
||||
is the bright slot's dim twin (`theme.DIM_TWIN`). A hash label and an annotation are
|
||||
the label colour throughout, never a grey; on a truecolor terminal the lit bytes are
|
||||
bold. A keyless chip is the same pair in grey; our `★` and a faded hop keep their dark
|
||||
greys; the composer's slot keeps the white chip (on the PicoCalc, a white `+` in a
|
||||
bold. A keyless chip is the same pair in grey; our `★` keeps its dark grey; a **faded
|
||||
hop** (a mirrored return leg, the return home: what the user doesn't compose) is its own
|
||||
chip at `pathline.FADE_CHROMA` of its chroma, same lightness, so it still says which node
|
||||
it is (JP, 2026-10-14; it was one dark slate) — the PicoCalc, with no colour of less
|
||||
chroma, keeps its label colour and drops its fill; arrows keep `faint`; the composer's
|
||||
slot keeps the white chip (on the PicoCalc, a white `+` in a
|
||||
break of the ribbon).
|
||||
- Chip seams are **one** interlocked chevron (previous fill on next). Two chips of the
|
||||
same fill are the exception the interlock can't draw — and so are two fills the eye
|
||||
|
||||
@@ -121,6 +121,16 @@ def distance(first: str, second: str) -> float:
|
||||
return math.dist(from_hex(first), from_hex(second))
|
||||
|
||||
|
||||
def desaturated(colour: str, keep: float) -> str:
|
||||
"""``colour`` with its chroma multiplied by ``keep``. The lightness and the hue do not change.
|
||||
|
||||
``1`` returns the colour, and ``0`` returns the grey of the same lightness. Less chroma
|
||||
at the same lightness always stays in the sRGB gamut, because the grey is in it.
|
||||
"""
|
||||
lightness, a, b = from_hex(colour)
|
||||
return to_hex((lightness, a * keep, b * keep))
|
||||
|
||||
|
||||
def shaded(colour: str, by: float, *, floor: float | None = None) -> str:
|
||||
"""``colour`` moved ``by`` in lightness, away from the end of the scale that is nearer.
|
||||
|
||||
|
||||
+36
-29
@@ -17,9 +17,10 @@ surface that the survey found can in the end use it.
|
||||
look, and each seam uses one cell. The label of the chip of a node has the hue that
|
||||
comes from its hash (:func:`~meshterm.ui.theme.node_style`). This is the colour that its
|
||||
name has on each surface. The fill is that hue, but darker (:data:`CHIP_LIGHTNESS`). Our
|
||||
node is the white ``★`` of the map on a neutral dark grey. A faded hop is dark slate. A
|
||||
keyless hop is light grey on grey. (On the 16-colour console of the PicoCalc, the fill
|
||||
is the dim twin of the hue. Refer to :func:`_slot_colours`.)
|
||||
node is the white ``★`` of the map on a neutral dark grey. A faded hop has the colours
|
||||
of its own chip, with less chroma (:data:`FADE_CHROMA`). A keyless hop is light grey on
|
||||
grey. (On the 16-colour console of the PicoCalc, the fill is the dim twin of the hue.
|
||||
Refer to :func:`_slot_colours`.)
|
||||
|
||||
Sometimes two chips have the *same* fill (a mirrored return leg, or a stretch of
|
||||
keyless greys). Sometimes they have two fills that are too close to tell apart (less
|
||||
@@ -87,7 +88,8 @@ surface that the survey found can in the end use it.
|
||||
``★`` of the app. The user already knows who both ends are, and the cells are for the
|
||||
hops that *are* news. These stars fade where the route is being *composed*, because our
|
||||
two ends are fixtures and not choices. A surface that only *shows* a walk passes
|
||||
``dim_self=False`` and keeps our stars in the ``you`` white.
|
||||
``dim_self=False`` and keeps our stars in the ``you`` white. (Only arrows show the
|
||||
difference. A faded chip loses chroma, and the white star has no chroma to lose.)
|
||||
|
||||
The existing call sites still render through ``path_text``. A separate pass for each
|
||||
surface will move them here. The insertion cursor of the composer is a hop in its own
|
||||
@@ -101,7 +103,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import NamedTuple
|
||||
|
||||
from rich.cells import cell_len
|
||||
@@ -215,18 +217,19 @@ _WHITE = "#ffffff"
|
||||
#: route is a fixture that the user already knows. It stays back and lets the hops that
|
||||
#: differ carry the colour.
|
||||
_YOU_BG = "#3f3f46"
|
||||
#: The chip of a dimmed hop: a dark slate fill with muted ink. It recedes like ``faint``
|
||||
#: text.
|
||||
_DIM_BG = "#334155"
|
||||
_DIM_FG = "#94a3b8"
|
||||
#: The part of its chroma (OKLab ``a`` and ``b``) that the chip of a faded hop keeps. A
|
||||
#: faded hop is a hop that the user does not compose: a mirrored return leg, or the
|
||||
#: automatic return home. Its chip has the colours that it has with no fade, with less
|
||||
#: chroma and the same lightness. Thus each faded hop still says which node it is, and the
|
||||
#: automatic part of the route recedes (JP, 2026-10-14). Before, each faded hop was the same
|
||||
#: dark slate chip.
|
||||
FADE_CHROMA = 0.35
|
||||
|
||||
#: On the 16-colour console, the page: slot 0, black. It is the fill of a chip that the
|
||||
#: console cannot fill, because the VT has no dark grey background. That chip is our ``★``,
|
||||
#: a faded hop, or the slot of the composer. Its text then sits on the page, and the chips
|
||||
#: on each side close on a point and open on a notch.
|
||||
_SLOT_PAGE = slot_hex(0)
|
||||
#: On the console, the text of a faded hop: slot 8, the dark grey.
|
||||
_SLOT_FAINT = slot_hex(8)
|
||||
#: On the console, white: slot 15. It is the text of a keyless chip and of the slot of the
|
||||
#: composer.
|
||||
_SLOT_WHITE = slot_hex(15)
|
||||
@@ -305,7 +308,8 @@ class PathHop:
|
||||
lit_bytes: For a hash label: the leading *bytes* that are drawn in the hue (the two
|
||||
tones of ``highlighted_hash``). The rest is muted or soft. ``0`` for names.
|
||||
dim: Fade the hop (and the arrow that leads into it). It is the resolved return leg
|
||||
of a planned route, which the user does not compose.
|
||||
of a planned route, which the user does not compose. A chip keeps its colours,
|
||||
with less chroma (:data:`FADE_CHROMA`). Arrows draw the hop ``faint``.
|
||||
style: An explicit colour override. It is a hex style (``"#rrggbb"``, attributes are
|
||||
permitted) or a theme name. It is for colourings of a context that win over the
|
||||
identity. Plain mode uses it as the label style, and powerline uses it as the
|
||||
@@ -1216,18 +1220,19 @@ def _spectrum_colours(hop: PathHop) -> _ChipColours:
|
||||
The label of a node is its hue. This is the colour that its name has in each list and
|
||||
in arrow mode. The fill is that hue, but darker (:data:`CHIP_LIGHTNESS`). The whole
|
||||
label is in the hue, also a hash label and an annotation. The lit bytes of a hash are
|
||||
bold. A keyless chip is the same pair in grey. Our ``★`` and a faded hop keep their dark
|
||||
greys, which already carry a light label. The PicoCalc draws the same rules at the
|
||||
resolution of its console (:func:`_slot_colours`).
|
||||
bold. A keyless chip is the same pair in grey. Our ``★`` keeps its dark grey, which
|
||||
already carries a light label. The PicoCalc draws the same rules at the resolution of
|
||||
its console (:func:`_slot_colours`).
|
||||
|
||||
The insertion slot wins over all other rules. It is the one chip that is not a node, and
|
||||
it has the ``cursor`` white. Thus it reads as chrome among identities, and not as a hop
|
||||
with a hue that is unlucky. (White is outside the node spectrum, and its dark text stays
|
||||
readable on it.) It is the only light chip on the ribbon. The fade is next, and it wins
|
||||
over the identity, also our own. A dimmed hop is a hop that nobody composed (an
|
||||
automatic return home, a mirrored return leg). Our end must recede with the rest of
|
||||
that automatic half, and not keep its hue among the greys. Plain mode says the same
|
||||
thing when it fades the name.
|
||||
readable on it.) It is the only light chip on the ribbon. The fade is next. A dimmed
|
||||
hop is a hop that nobody composed (an automatic return home, a mirrored return leg). It
|
||||
has the colours that it has with no fade, with less chroma (:data:`FADE_CHROMA`). Thus
|
||||
each faded hop still says which node it is, and that automatic half recedes (JP,
|
||||
2026-10-14). Our white star has no chroma, so its chip does not change. Plain mode
|
||||
draws the same hops ``faint``.
|
||||
|
||||
Our chip in a chat (``white_ink``) has the fill of the chip of its key, and a white
|
||||
label. A quiet chip (a node with no key in a chat) is black on a grey fill.
|
||||
@@ -1238,7 +1243,8 @@ def _spectrum_colours(hop: PathHop) -> _ChipColours:
|
||||
if resolved:
|
||||
return _ChipColours(_chip_fill(resolved), resolved, resolved)
|
||||
if hop.dim:
|
||||
return _ChipColours(_DIM_BG, _DIM_FG, _DIM_FG)
|
||||
normal = _spectrum_colours(replace(hop, dim=False))
|
||||
return _ChipColours(*(oklab.desaturated(c, FADE_CHROMA) for c in normal))
|
||||
hue = _style_hex(node_style(hop.key)) if hop.key else None
|
||||
if hop.white_ink and hue:
|
||||
return _ChipColours(_chip_fill(hue), _WHITE, _WHITE)
|
||||
@@ -1263,10 +1269,10 @@ def _slot_colours(hop: PathHop) -> _ChipColours:
|
||||
|
||||
A keyless chip is white on light grey, the same pair one bank down. Our ``★``, a faded
|
||||
hop, and the slot of the composer have no fill of their own, because the console has no
|
||||
dark grey background. They stand on the page (:data:`_SLOT_PAGE`): the star in our
|
||||
hue, a faded hop in the dark grey, and the slot as a white ``+`` in a break of the
|
||||
ribbon. The order of the rules is the same as on a truecolor terminal
|
||||
(:func:`_spectrum_colours`).
|
||||
dark grey background. They stand on the page (:data:`_SLOT_PAGE`): the white star, and
|
||||
the slot as a white ``+`` in a break of the ribbon. The console also has no colour with
|
||||
less chroma. Thus a faded hop keeps the colour of its label and loses its fill. The
|
||||
order of the rules is the same as on a truecolor terminal (:func:`_spectrum_colours`).
|
||||
|
||||
Our chip in a chat (``white_ink``) is white on the dim twin of its hue, the same rule as
|
||||
on a truecolor terminal. A quiet chip is black on the light grey of the keyless chip.
|
||||
@@ -1280,7 +1286,7 @@ def _slot_colours(hop: PathHop) -> _ChipColours:
|
||||
ink = resolved if resolved in DIM_TWIN else _SLOT_WHITE
|
||||
return _ChipColours(fill, ink, ink)
|
||||
if hop.dim:
|
||||
return _ChipColours(_SLOT_PAGE, _SLOT_FAINT, _SLOT_FAINT)
|
||||
return _slot_colours(replace(hop, dim=False))._replace(fill=_SLOT_PAGE)
|
||||
if hop.you:
|
||||
return _ChipColours(_SLOT_PAGE, _SLOT_WHITE, _SLOT_WHITE)
|
||||
hue = _style_hex(node_style(hop.key)) if hop.key else None
|
||||
@@ -1379,9 +1385,10 @@ def path_line(
|
||||
between them need.
|
||||
dim_self: Fade the bare stars (the default). On a surface where the user *composes*
|
||||
the route, to set out from us and to come home to us are fixtures. The user does
|
||||
not choose or remove them, the same as for a mirrored return leg. Thus they have
|
||||
the same automatic grey, and the colour on the line is for the nodes that the
|
||||
user really picks. Pass ``False`` where nobody composes anything (a record of a
|
||||
not choose or remove them, the same as for a mirrored return leg. Thus they fade
|
||||
as that leg does. (Arrows draw them ``faint``. A faded chip loses chroma, and our
|
||||
white star has no chroma to lose, so its chip does not change.) Pass ``False``
|
||||
where nobody composes anything (a record of a
|
||||
walk that is already made). Then there is no "not yours" to say, and our node
|
||||
reads in the ``you`` white of the app, as in other places. A ``dim_from`` that
|
||||
reaches a star still fades it in both cases.
|
||||
|
||||
+37
-12
@@ -9,6 +9,7 @@ cells. Thus these tests assert rendered strings and span styles, and not impress
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
|
||||
import pytest
|
||||
@@ -21,8 +22,6 @@ from meshterm.platforms import CARDPUTER_ZERO, PICOCALC_LYRA, set_platform
|
||||
from meshterm.ui import oklab
|
||||
from meshterm.ui.fontset import FONT_CODEPOINTS
|
||||
from meshterm.ui.pathline import (
|
||||
_DIM_BG,
|
||||
_DIM_FG,
|
||||
_YOU_BG,
|
||||
CHIP_LIGHTNESS,
|
||||
CRACK_HEAD,
|
||||
@@ -30,6 +29,7 @@ from meshterm.ui.pathline import (
|
||||
CURSOR_GLYPH,
|
||||
ELIDE_HEAD,
|
||||
ELIDE_TAIL,
|
||||
FADE_CHROMA,
|
||||
PATH_INK,
|
||||
POWERLINE_ROUND_CLOSE,
|
||||
POWERLINE_ROUND_OPEN,
|
||||
@@ -279,7 +279,7 @@ def test_the_picocalc_draws_chips_from_its_eight_backgrounds(picocalc: None) ->
|
||||
assert at("(0a)").fg == _rgb(red) # the annotation also has the colour of the label
|
||||
assert (at("3d").fg, at("3d").bg) == (_rgb(slot_hex(15)), _rgb(slot_hex(7)))
|
||||
assert at("Ridge").bg in (None, _rgb(slot_hex(0))) # faded: on the page
|
||||
assert at("Ridge").fg == _rgb(slot_hex(8))
|
||||
assert at("Ridge").fg == _rgb(yellow) # in the colour of its label, with no fill
|
||||
assert at(SELF_GLYPH).bg in (None, _rgb(slot_hex(0)))
|
||||
|
||||
|
||||
@@ -856,8 +856,33 @@ def test_a_seam_between_two_of_one_colour_is_the_thin_chevron_in_its_label() ->
|
||||
f"{_fill('aa')} on {_fill('77')}",
|
||||
) # a blend, one cell
|
||||
|
||||
faded = pathline._spectrum_colours(PathHop("B", dim=True))
|
||||
glyph, style = _seams(PathHop("A", dim=True), PathHop("B", dim=True))[0]
|
||||
assert (glyph, style) == (POWERLINE_THIN, f"{_DIM_FG} on {_DIM_BG}")
|
||||
assert (glyph, style) == (POWERLINE_THIN, f"{faded.ink} on {faded.fill}")
|
||||
|
||||
|
||||
def test_a_faded_chip_is_its_own_chip_with_less_chroma() -> None:
|
||||
"""A faded hop keeps the colours of its node, with less chroma and the same lightness.
|
||||
|
||||
The faded part of a route is the part that the user does not compose. Each node there
|
||||
still has its hue, so the user can see which node it is (JP, 2026-10-14). Before, each
|
||||
faded hop was the same dark slate chip.
|
||||
"""
|
||||
|
||||
def chroma(colour: str) -> float:
|
||||
_lightness, a, b = oklab.from_hex(colour)
|
||||
return math.hypot(a, b)
|
||||
|
||||
for key in ("00", "4c", "aa", None):
|
||||
normal = pathline._spectrum_colours(PathHop("N", key=key))
|
||||
faded = pathline._spectrum_colours(PathHop("N", key=key, dim=True))
|
||||
for before, after in zip(normal, faded, strict=True):
|
||||
assert after == oklab.desaturated(before, FADE_CHROMA)
|
||||
assert abs(oklab.from_hex(after)[0] - oklab.from_hex(before)[0]) < 0.01
|
||||
if key:
|
||||
assert chroma(after) < chroma(before) * 0.5
|
||||
star = pathline._spectrum_colours(PathHop(SELF_GLYPH, you=True, dim=True))
|
||||
assert star.ink == "#ffffff" # white has no chroma to lose
|
||||
|
||||
|
||||
def test_a_seam_blurs_by_perceived_distance_not_by_hue_gap() -> None:
|
||||
@@ -866,9 +891,9 @@ def test_a_seam_blurs_by_perceived_distance_not_by_hue_gap() -> None:
|
||||
The eye decides, and not the wheel. Sixteen steps of the first byte across the greens
|
||||
are one colour to a user. Sixteen steps across the reds are two colours. Thus the same
|
||||
gap of hue blurs on one side of the wheel and interlocks on the other. The test measures
|
||||
the fills, because the solid point is a fill on a fill. The greys follow the same metric
|
||||
with no exception. The keyless grey and the faded slate are far apart, and they keep
|
||||
their seam.
|
||||
the fills, because the solid point is a fill on a fill. A faded chip follows the same
|
||||
metric with no exception. A hue and its faded chip are far apart, and they keep their
|
||||
seam.
|
||||
"""
|
||||
green = _seams(PathHop("A", key="4c"), PathHop("B", key="5c"))[0]
|
||||
red = _seams(PathHop("A", key="00"), PathHop("B", key="10"))[0]
|
||||
@@ -880,16 +905,16 @@ def test_a_seam_blurs_by_perceived_distance_not_by_hue_gap() -> None:
|
||||
wrapped = _seams(PathHop("A", key="fc"), PathHop("B", key="02"))[0]
|
||||
assert wrapped[0] == POWERLINE_THIN # neighbours across the seam of the wheel, also
|
||||
|
||||
glyph, style = _seams(PathHop("A"), PathHop("B", dim=True))[0]
|
||||
assert glyph == POWERLINE_SEP and " on " in style # two greys, which still interlock
|
||||
glyph, style = _seams(PathHop("A", key="00"), PathHop("B", key="00", dim=True))[0]
|
||||
assert glyph == POWERLINE_SEP and " on " in style # a hue and its fade still interlock
|
||||
|
||||
|
||||
def test_bare_self_stands_us_on_a_star_and_fades_both_ends() -> None:
|
||||
"""``bare_self`` puts a ★ for us at both ends, and fades both.
|
||||
|
||||
To set out from us and to land back on us are fixtures of the route and not choices.
|
||||
Thus they have the same automatic grey as a mirrored return leg. In chips, they have the
|
||||
same dark slate, and not the loud white of you.
|
||||
Thus they fade as a mirrored return leg does: ``faint`` in arrows. A faded chip only
|
||||
loses chroma, and the white star has no chroma to lose, so its chip stays white.
|
||||
"""
|
||||
hops = [None, *(f"{i:02x}aa" for i in range(6)), None]
|
||||
line = path_line(
|
||||
@@ -909,7 +934,7 @@ def test_bare_self_stands_us_on_a_star_and_fades_both_ends() -> None:
|
||||
assert [str(s.style) for s in stars] == ["faint", "faint"]
|
||||
chips = PathLine(line.hops, mode="powerline").text()
|
||||
star_fills = [str(s.style) for s in chips.spans if chips.plain[s.start : s.end] == SELF_GLYPH]
|
||||
assert all("#ffffff" not in fill for fill in star_fills) # never the white of you
|
||||
assert star_fills and all(fill.startswith("#ffffff on ") for fill in star_fills)
|
||||
|
||||
wrapped = path_line(
|
||||
hops, prefix_bytes=2, self_name="Me", bare_self=True, mode="powerline"
|
||||
|
||||
Reference in new issue
Block a user