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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
|
# Effects: feel reviews
**The rule above all the others:** no effect may alter or cover a focus
indicator (the focused pane's tag colour, its grip, the cursor, the
selection) or reduce its contrast. The effects are sugar; the indicators are
how a person knows where they are. Every G stage is reviewed against this.
Lift, for one, falls only on pane bodies and rails, never on a tag, a grip or
a header, and its strength is capped so text and the selection keep min(their
contrast, 4.5) (tests in src/gui/gui.zig). The rule binds effects, not a
theme's structure: the rules between panes (2px, `rule_px`) are the theme's
borders, and the one over a tag band stays in the band's slack, never over
its text. A dim touches only the unfocused
panes, with the same floor, and the focused pane's text is never at less
contrast than theirs (tests in src/draw.zig).
**Density.** A pixel shell draws every size given in pixels in logical
pixels, `dp()` in gui.zig (SDL's window display scale, rounded, at least
one; `PARDES_TEST_SCALE` forces one in a harness, and a test window is 1x
otherwise): the rules (`rule_px`), the tag rule, the grip's ring and its
dirty gap and mode bars, the rail's gap to the text and its minimum, the
thumb's inset, the rim, a bar cursor's minimum width, underline and
strikethrough thickness, the rule-grab margin, and the Crt's scanline and
mask periods (`pardesScale`). Left as they are: what is measured in cells
or the tagline (the rail's width, `rail_px` scaled with the tagline; Lift's
blur and offsets; every motion, in cells), Shadertoy's iResolution,
fragCoord and iMouse (the display's pixels, as ghostty's), and the pet and
touch debug overlays.
Each effect of docs/render-pipeline.md §9 lands behind its own switch, off,
and is kept only after a feel review (§8.4): a frame series on the virtual
clock, a recording, and a verdict — keep, polish or drop — with one line of
why. The verdict is the user's.
| effect | switch | state | review material | verdict |
|---|---|---|---|---|
| Crt (bundled post pass) | `Crt 0..3` | kept, rewritten (no barrel) | fx-compare stills, live window | kept at the user's live look |
| Ripple, Glitch | — | removed | live window | dropped by the user after a live look |
| G1 lift | `Lift shadow\|rim\|auto` (off), `InactiveDim <percent>` | opt-in until the focus-lift default is decided | lift-shots-2 (off, shadow, rim, auto on acme, dusk, forge); frame series + mp4 per Motion flavour | shadow on acme: keep as is. Round 1 dropped glow and surface (surface lowered the focused text's contrast) and made rim a hairline just above the focused tag (light on a dark page, shade on a light one). auto on a dark page recedes the others (InactiveDim 30) |
| G3 cursor | follows `Motion`; `CursorBlink on\|off` (on) | glide on by default through the default flavour (smooth); blink on (decision 6) | .scratch/render/cursor/cursor-flavours-grid.mp4 (and quarter speed), raw/<flavour>/<motion>/ | pending |
| G4 bloom, vignette, grain | `Bloom`, `Vignette`, `Grain` `0..3` (off) | bundled post passes; none touches a tag, grip, notice or the cursor (pardesSpare); each capped per theme (pardesCap) | .scratch/render/{bloom,vignette,grain}/ (levels 0-3 on forge, dusk, acme; crops; mp4 stepping through the levels) | pending |
| G5 lapis | `Theme lapis` (its `decor`) | a native theme with ornament: tag plaques with hard shadows inside their bands (the focused one striped), dots, rail checker, title shadow | .scratch/render/lapis/ (whole window at 1x and 2x, crops, a slide mp4) | pending |
| G6 smooth scroll | follows `Motion` (on through the default smooth; `Motion off` lands at once) | a mouse wheel's notch glides, a touchpad follows the finger | .scratch/render/scroll/ (glide-<flavour>.mp4, quarter speed, strips) | pending |
| G7 panel transitions | `PanelSlide`, `PanelZoom`, `PanelVertical`, `PanelDissolve` (off) with `Motion` | a moving pane casts a shadow; a retargeted move keeps its speed; the GUI's dissolve is a crossfade | .scratch/render/transitions/ (slide smooth, bouncy, retargeted; dissolve) | pending |
| G8 notice chips | follows `Motion` and `Lift` | a notice floats in tier 4 on a lift (shadow) while Lift is on, drops in easing out and dissolves easing in, quicker than it came (150 ms against 180) | .scratch/render/notices/ (acme, forge) | pending |
| G9 selection glow | `SelectionGlow on\|off` (off) | a soft halo of the selection's colour round each block of it in a body, fading in over 100 ms | .scratch/render/selection/ (forge, acme, lapis; lapis-glow-zoom.png) | pending |
| G10 theme crossfade | always (a theme change) | the chrome's colours fade through OKLab, easing in and out (out when retargeted) | .scratch/render/crossfade/ (forge, lapis, acme cut short by dusk) | pending |
| G11 look-hover glow | `HoverGlow on\|off` (off) | a soft underline of the theme's accent under the word a look-hover would open, fading in over 80 ms | .scratch/render/hover/ (forge, acme, lapis; zooms) | pending |
| G12 ambient occlusion | `Occlusion on\|off` (off) | pane bodies darken toward their edges, at most 2% (§9.1: 1 to 2%) | .scratch/render/occlusion/ (on/off on forge, acme, lapis; acme-diff-x40.png) | pending |
| G13 parallax | `Parallax on\|off` (off) | a theme's page pattern under the bodies (lapis's dots) moves with the text at a quarter of its speed, glides included | .scratch/render/parallax/ (lapis, notches down and up) | pending |
| G14 afterglow | `Afterglow on\|off` (off) | a body cell whose text changed glows in the accent at 18% and fades over 250 ms | .scratch/render/afterglow/ (forge, acme: two in-place edits) | pending |
## Motion flavours
`Motion off|crisp|smooth|bouncy|playful` (default smooth, tuned so arrivals
land inside §8.1's 220 ms; bouncy and playful run longer, opt-in) is one parameter set
(animation.Motion) every fx animation reads, so a flavour is data, not a
branch in each effect. Input is never blocked, and a new target always
retargets from the current position and velocity.
| flavour | timing (ω, length) | follow-through (ζ) | anticipation | squash/stretch | exaggeration (gain) | secondary lag | arcs |
|---|---|---|---|---|---|---|---|
| off | instant | — | — | — | — | — | — |
| crisp | 60, 0.6× (leaves at full speed) | 1 (none) | — | — | 1 | in step | — |
| smooth | 34, 1.1× (slow in, slow out; ≤220 ms) | 1 (none) | — | — | 1 | 0.85 | 0.04 |
| bouncy | 24, 1.4× | 0.35 (overshoots ~30%, settles twice) | — | 0.15 | 1.25 | 0.8 | 0.08 |
| playful | 18, 1.7× | 0.28 (overshoots ~40%) | 0.15 of the move | 0.35 | 1.4 | 0.55 | 0.15 |
The flavours are distinct by design, not tuning: crisp leaves at full speed
and never passes its mark; smooth takes twice as long and eases in and out; bouncy clearly passes its mark and settles back once or twice;
playful winds up the other way first, flies well past, and stretches along
its path and squashes as it lands, its text with it (the user's choice); a
pointer inverts the same stretched box, and landed, a pane is exactly its
target. A motion only a few pixels big (Lift) is
exaggerated by the flavour's gain so its character shows. The flavours drive
the Lift spring (time-based), the notice drop and the pane moves (PanelSlide,
PanelZoom, PanelVertical opening or moving, whose length scales with the
flavour); a closing pane keeps its effect's own exit. Chart and side-by-side
video: .scratch/render/motion/compare/.
Which principles each motion uses:
- **Lift** (G1): timing, slow in / slow out (the spring), follow-through
(bouncy and playful lift past full and settle back), anticipation (playful
dips before it rises; below zero nothing is drawn, so it reads as a beat
before the lift), staging (only the focused pane lifts; nothing else moves
with a focus change), exaggeration (the flavour's gain). Squash, stretch,
arcs and secondary lag have nothing to act on in a lift.
- **Notice drop and pane moves**: timing and follow-through from the flavour's
spring over the move's length; anticipation (playful). A pane past its mark
never takes a neighbour's click, a rising one stays in its box, and a notice
past its row stays in its pane's body.
- **Cursor** (G3): timing (the flavour's spring, 2.4x its pace: it follows
input), follow-through (bouncy and playful pass the target), squash and
stretch and secondary action (the trailing corners lag by the flavour's
follow, so a jump smears and collapses), arcs (a jump over 8 cells bows by
the flavour's arc), anticipation (playful's wind-up). A step of a cell or
less, or any move in insert mode, lands at once, so typing never trails;
a scroll carries the cursor with the text. Only the focused cursor moves.
Notes on G1: the lift and the dim run on one focus spring per pane, at the
Motion flavour's pace, sampled at each frame's own time; while it moves the
GUI draws at the display's rate, and a grid snaps. A notice floats on a lift
of 1 while a shadow or rim is on. InactiveDim under `Lift auto` defaults to
30. The fade is in linear light, so on a dark page it is gentle: at 30
forge's text goes from 15.5:1 to 11.2:1 and dusk's from 6.4:1 to 4.8:1,
plainly quieter and still easy reading; at 10 forge barely moves (14.1:1).
On a light page the same percent bites far harder (acme: 7.0:1 at 10, the
4.5 floor at 30), which is why `auto` shades there instead of dimming.
Notes on G4: the three are post passes, so each is a frame of cost only
when it is in the chain. None moves (grain is still, by choice: moving
grain would redraw every frame for a texture), so none asks for frames.
The focus rule is kept by construction: the shell hands every pass the
rectangles of the tags (with their grips), the column and workspace tags,
the notices and the cursor, and a bundled pass leaves them as they are; its
strength is capped per theme so the text on the page and the selection's
text keep min(their contrast, 4.5) (Post.capsOf, tested over the native
themes). Bloom takes only what is brighter than the page, so a light page
does not bloom at all; its glow is a dual Kawase blur in float textures at
half size and down (3 to 5 steps by level), within §8.2's 5%. The vignette
is round, starts halfway to a corner and reaches 10, 18 and 28%. Grain is
1.5, 3 and 5 of 255 on page-coloured pixels only, a logical pixel big.
Found on the way: SDL GPU binds at most 4 KiB of a uniform block, so every
Shadertoy colour after the palette (iBackgroundColor and on) read zero; the
block is now cut in three with ghostty's names and order kept.
Notes on G5: lapis (src/themes/lapis.zig) is 0x4200.cafe's lapis.css in
pardes's chrome, drawn inside it (decision 1). The tags are the site's
trail and title bars: every tag a plaque in its own band, framed in gold
(2 px, the trail's border) with the trail's hard vermilion shadow (5 px)
down and right, the frame and the shadow both inside the band's rows and
columns, so no shadow reaches a body, a grip or the next pane; down, the
shadow gets what the band's slack under the text leaves (a pixel or three
at a tagline under the body's size, more at a larger one), to the right
its full offset. The focused pane's plaque is the code window's title bar,
gold and lapis stripes of 2 px, its words on plates of the focused ground
(the words' cells stay opaque, the blanks between let the stripes show),
so the focus indicator is plainer to see than any theme's tint, never
less; the unfocused ones are plain. Column and workspace tags take the
quieter object-label language: a 1 px frame and a 2 px shadow of vermilion
toward the page. The plaques are decor drawn under their cells (a group's
under range) with the cells blended premultiplied over them; blank tag
cells on the tag's ground are see-through there. They need an opaque
window: under WindowOpacity 100 the tags are plain. Also from the site:
the colours (lapis-deep page, vellum text, gold names and rules,
vermilion rubrics and tag rule, gold selection in lapis ink), the page's
gold dot grid (22 px, #e8c46a22) on page-coloured cells only, fixed to the
window; the dither frame's checker in the scroll track, anchored to its
rail (the thumb solid gold); the titles' vermilion offset shadow under a
file name's letters. Left out: Crimson Pro and Departure Mono, the
ornaments, a decor crossfade (it snaps while the colours fade). The tty
shows the colours alone: a tag row abuts its body and its neighbour, so
there is no cell for a shadow row or column.
Notes on G6: a mouse wheel's notch (SDL's whole-number step) scrolls the
core by its lines at once, so keys and clicks act where the text is, and
the picture follows on the Motion flavour's spring at half again its pace,
critically damped whatever the flavour (text never passes its mark; §8.1),
from its current speed, so notches in a row carry on without a stall. How
far the rows moved is measured on the next frame against the last (a
wrapped line's rows, a terminal's output), within a couple of rows' slack
for the cursor's line and its number moving with the scroll; the rows that
left are kept and drawn in the gap the lag opens, and a lag is never more
than the body's height (a flick past that jumps). A touchpad's fractions
keep moving the picture under the finger, as before; there is no momentum
(SDL reports no finger lift on a wheel). The glide runs on the core's
continuous path (Pardes.shell_continuous), so the shell draws every loop
while it moves and a virtual clock moves with it; idle, it costs nothing.
A PDF page scrolls as before. The test feed takes wheel events
(ESC]777;mouse;wheel;<amount>;<x>;<y>BEL).
Notes on G7: the geometry already followed the Motion flavour's spring
(sxrrwplk); a move retargeted mid-way now carries its speed on
(Track.launch, from the old move's speed along the new one's longest
side), so a second Newcol during a slide bends the motion instead of
stopping it dead. A pane that slides, zooms or rises stands off the page
while it moves: a soft shadow as Lift's, at sin(pi x progress) of its
strength (none as it sets out or lands), on the other panes' bodies and
rails alone and under the same contrast ceiling, moving with the pane. The
GUI's PanelDissolve is a crossfade: a changed cell's new form drawn whole,
its old one over it at 1 - progress; the blend is in sRGB, not linear
light (a two-colour mix in linear light needs both in one shader; open
point 3). The tty keeps its per-cell swap. GUI golden 18-mid-transition
changes (the slide's shadow at frame 6).
Notes on G8: most of it was there: the drop eases out on the Motion
flavour's spring (sxrrwplk), the dissolve eases in, and a notice floats in
tier 4 with Lift's shadow at its fade while Lift is on, on the bodies
alone. What G8 changed: leaving was slower than arriving (a 300 ms
dissolve against a 180 ms fall); §8.1 wants leaving the quicker, in 100 to
160 ms, so MessageDissolve now defaults to 150 (a test holds both bands).
Notes on G9: the halo is Lift's soft kind in the selection's own colour,
cast by each block of selected rows in a pane body (the rows that touch),
with the block as its caster, so the selection itself is never touched;
a cursor beside a block joins the caster, so it is never glowed over
either. It stays on bodies (never a tag or a grip), is at most 22% (§8.2:
coloured glows at most 25%) and less where the text round it would drop
below min(its contrast, 4.5); it comes up over 100 ms with an ease-out
(micro-feedback), on the core's continuous path, and is not drawn while
panes move. The blocks are read from the cells painted in the theme's
selection colour, a pixel shell's inference (§1.5, smell 4) until
selections are regions.
Notes on G10: a theme change already faded the chrome (tags, rules, grips,
rails) over 10 frames (160 ms, in §8.1's 150 to 250 for a state change),
mixing sRGB bytes linearly in time. Now each colour goes through OKLab, so
a fade between two hues keeps its lightness instead of dipping dark and
muddy, on a smooth ease-in-out; a change made mid-fade sets off from where
it shows and eases out, so it moves at once instead of stalling. The page,
its text and the syntax colours still change at once with the theme, as
before: fading them would re-resolve every cell each frame.
Notes on G11: the word a look-hover would open is already lit (the
affordance's tint); HoverGlow adds a strip three logical pixels tall along
its foot in the theme's accent, its edges blurred by one, at most 22% and
under the text's contrast floor, fading in over 80 ms (micro-feedback). It
stays on bodies and is split round a cursor, so it never touches one. The
word is found by the tint's colour, as G9 finds the selection.
Found while reviewing: with the machine loaded, test/gui_golden.py's
two-captures-agree settle can take a scene's frame before the scene's
change lands, and every later hash moves up a scene; a rerun on a quiet
machine matches.
Notes on G12: four soft strips cast from just outside each body inward,
clipped to it and round a cursor, black in linear light, 2% at most and
under the lift's contrast ceiling, in the pane's own group so it moves
with it. At §9.1's strength it is all but invisible on a dark page (a
level of 255 on forge) and faint on a light one (acme's page steps one
level toward the edges); acme-diff-x40.png shows where it falls.
Notes on G13: a body region now carries its first line (Region.line: a
file's scroll, a terminal's grid offset; on the wire, a u32 more a
region). Under Parallax the dots under each body are offset by a quarter
of its line times the row height, plus G6's glide offset while one runs,
so they drift with the text as it glides and settle with it (measured: 4
lines at 35 px move the 22 px grid 13 px, 35 mod 22). The cell shader
reads the offset from the instance's serial slot, free since G7 took the
dissolve's per-cell noise out. Without a pattern, nothing happens.
Notes on G14: the GUI keeps the grid it last drew and when each cell last
changed; a changed cell of a pane body glows in the theme's accent, 18% at
most and under the text's contrast floor, and fades with an ease-out over
250 ms, drawn as one flat rectangle a run of cells that changed together.
Never on a tag or a grip (bodies only), the cursor's cell or a selection;
a body whose first line moved (a scroll) starts over instead of lighting
up whole. It runs on the continuous path while anything glows, and costs
a compare of the grid a frame while on, nothing while off.
## Terminal track (§9.2)
Audit, after decision 2 (keep every whimsical effect and polish it, never
remove it): the six core-composed character transitions (ascii, edges,
fall, wave, scramble, typewriter) and curtain stay as they are, each
bounded to its pane's final rectangle and each cell's last sample its
canonical one; slide and vertical are the tty compositor's and follow the
Motion flavour (sxrrwplk); zoom stays (nearest-cell scaling, its look is
the effect); dissolve swaps each changed cell once at its threshold;
message fades and the theme's chrome fade are the core's (the latter in
OKLab since G10), and InactiveDim (T5's unfocused dim) works in a grid as
it does in a pixel shell.
| # | effect | switch | state | review material | verdict |
|---|---|---|---|---|---|
| T2 | cursor jump trail | `JumpTrail on\|off` (off) | a jump of three cells or more leaves 3 to 5 cells of trail behind the cursor, their ground drawn toward the text's colour in OKLab, most beside it, fading in 120 ms; truecolor terminals (vaxis's answer or COLORTERM); never the cursor's cell, a selection or outside the focused body; off under Motion off; a few cells a frame | .scratch/render/tty/trail/ (pyte replay of a real tty session at 60 fps: trail-forge.mp4, zooms) | pending |
| T4 ordered swap | `PanelDissolve` (off) | the terminal's dissolve swaps each changed cell once at its place in a 4x4 Bayer matrix (shifted per pane), an even patterned screen instead of noise; hit-testing follows the same order | .scratch/render/tty/dissolve/ (ordered-swap-sim: the matrix applied to two real frames; see note) | pending |
Note on T4's material: a pane opening in a tty session recorded through
pyte did not animate (PanelSlide did not either there), so the review
material applies the same matrix to two real frames of that session; the
unit test holds the order (a 4x4 block reveals exactly t x 16 cells).
|