# 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 ` | 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/// | 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-.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. A pane that moves carries it too: the springs keep its place in the pane and the quad is drawn through the pane's presented box, so through a slide, a zoom or a drag it rides with the text, and a layout change with no transition lands it with the text. A focus change (to another pane, or into a column's or the workspace's tag) glides from where the cursor is drawn to the new one, free of the old pane's edges while it crosses (.scratch/render/cursor/move/: ride-*, cross-*). 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;;;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 | | T1 chip shadow | `ChipShadow on\|off` (off) | a notice chip casts a shadow a cell right and down in its body: half blocks (▌ ▀ ▘) in a darker ground on blank cells, the ground 35% darker (OKLab) under text; never the cursor, a selection, a tag or another chip; truecolor only | .scratch/render/tty/shadow/ (forge, acme; 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 | | T7 thumb flash | `ThumbFlash on\|off` (off) | a pane whose view moves has its scroll thumb drawn toward the text's colour, 60% at once and back over 250 ms (quadratic); the thumb only, never a focus indicator; truecolor only; a few cells a frame while it fades | .scratch/render/tty/thumb-flash.mp4, thumb-flash-strip.png (pyte replay, forge) | pending | | T3 wipe | `PanelCurtain` (off) | the curtain becomes a wipe, left to right: new grid behind the head, old ahead, and a soft edge of two columns whose ink and ground sit 2/3 and 1/3 of the way to the old ground (OKLab; a palette colour steps at the half); only the head's columns change a frame; the head's cells are not hit-testable until settled | .scratch/render/tty/wipe/ (curtain-gui.mp4, curtain-edge-zoom.png) | 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). Not built: T5 (unfocused dim) is InactiveDim, which already works in a grid. T6 (a truecolor ramp on the active tag band) would change the focused tag's colour, a focus indicator no effect may alter, so it stays out. The §9.2 byte budget (deferring a sample after an oversized frame, DECRQM for sync 2026) is not built either: the terminal effects here change a few cells a frame, and only PanelSlide rewrites a whole pane per sample.