summaryrefslogtreecommitdiffstats
path: root/docs/chrome.md
blob: 1450dcb5134ac3055d3407c31b7b5237e8dc26c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# Chrome shaders

A frame is a rect too, so it is a pass too. `where="chrome"` on any entry in
`shaders` or in a `states` block runs that pass over a pane's **frame** (border,
padding, title, buttons) instead of its contents.

```kdl
states {
    // a bell nobody has answered: a quarter-second flash, then gone
    bell { tint where="chrome" channel="fg" color="#ffcc00" amount="(since < 250) * 255" }
}
```

Nothing else changes: same shaders, same `amount`, same
[expressions](shaders.md#strength-as-an-expression), same chain order. One new
word.

You have already seen one: the default bell flash (a blink, then a breathe on
the frame of a pane that rang, in theme's `bell` colour) is exactly this, a
chrome tint whose amount reads `since`. (The shimmer that crosses the pane's
body at the same moment is the same idea without the new word: an ordinary
content pass in the same chain.) Writing your own `bell { … }` replaces both;
`bell_indicator false` silences them with the mark.

## Two rules

**`channel="fg"`, almost always.** A border's *background* is the terminal's own
default; mixing that towards a colour paints a rectangle behind the glyphs
instead of colouring them, and leaves it there until the pane changes state.
`fg` also keeps the padding inside the border out of it, since a blank cell has
no foreground to colour, so the effect lands on exactly the rules, the title and
the buttons.

**The contents are cut out of the pass.** A full-strength chrome tint cannot
reach your text, and a full-strength content tint cannot reach the border. They
are two rects and two passes.

## Positions are the whole frame's

`x`/`y` are counted from the frame's top-left corner, with `cols`/`rows` the size
of the pane's rect, not of one side. One pass over the whole ring, so an effect
can travel round a border instead of restarting at every corner.

For anything that *travels*, though, the coordinate you want is distance along
the ring, which the language can work out. Clockwise from the top-left:

```
(y == 0 ? x : x == cols-1 ? cols - 1 + y
 : y == rows-1 ? 2 * cols + rows - 3 - x : 2 * cols + 2 * rows - 4 - y)
```

with the perimeter itself `2 * cols + 2 * rows - 4`. Three things fall out of it:

- `min(d, P - d)` is distance the short way round, so an effect crosses the
  corner it started from instead of stopping at a seam.
- `(p - head + P) % P` is "how far ahead of the head", which makes an arc rotate
  rather than a blob slide.
- `sin(TAU * p / P)` is that position **as an angle**, one cycle to the lap, and
  a sine wraps at the corner for free, where a ramp needs the trick above. Any
  whole number of lobes (`* 3 / P`) fits the ring exactly, which is what makes a
  standing wave stand.

There is no way to name a subexpression, so the formula is written out in full,
which is why a small effect is a long line.

Thirteen of them are ready to paste in
`contrib/chrome`:
a shine running round the border, a sine comet with softer shoulders, marching
ants, a standing wave, drifting sparkles, ripples leaving a corner in both
directions, a rotating arc, a bit-drift pattern, a heartbeat, a static gradient,
corner glints, a sheen across the title rule and an XOR barcode along it. Its
README has the two techniques they share: the angle idiom above, and overdrive
then clip (`* 14 - 3000` after a sine) to turn a glow into an arc.

## A border that says something

Hung off a [state](config.md#pane-states), this is chrome doing the job chrome is
for, in no room and no words:

```kdl
states {
    // a dead pane's frame breathes red until you deal with it
    dead { tint where="chrome" channel="fg" color="#ff0033" amount="abs(t/8%510-255)" }
    // one that has not started yet is outlined, not filled
    suspended { tint where="chrome" channel="fg" color="#8a8a95" amount=160 }
    // and the panes you are not in recede, frame and contents together
    unfocused { dim amount=60; dim where="chrome" channel="fg" amount=90 }
}
```

`since` is what makes a *flash* possible: it is how long the pane has been in
this state, where `t` only knows what time it is. The `bell` state ends when you
look at the pane, because that is what answering a bell is.

A pane that a small terminal has collapsed to a single row is chrome all the way
through, which makes this the only kind of pass that can still colour it. That
is the state where a colour is worth the most.

## Animation costs a clock

Anything that reads `t` or `since` asks the session to keep painting, at
`anim_ms` (50ms, so 20fps) and only while such a pass is actually on screen. A
pulse hung off `dead` costs a frame clock while a pane is dead and nothing for
the rest of the session; the same pass in the global `shaders` block costs it for
the whole session.

`anim_ms 0` turns the clock off, which leaves an animated shader advancing only
when something else causes a frame.