# 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 | ## 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.