Skip to Content
steep

Effects

What effects are, the order they run in, how to aim one, and each one built in.

An effect changes a recording's frames between the renderer and the file. You list them on the profile:

reel.Profile{
	Outputs: reel.MustFiles("demo.webp"),
	Effects: []reel.Effect{
		effects.Redact(effects.Env("API_TOKEN")),
		effects.Highlight(effects.Text("Error")),
		effects.Pause(),
		effects.Keys(time.Second),
	},
}

Each section below shows the same recording with one effect on. Redact and your own effects have pages of their own.

Order

Effects sort themselves by priority, so you can list them in any order. Each sees the frames of the one before, lowest priority first. Effects with the same priority run in the order you list them.

PriorityConstantEffectsWhy here
100reel.PriorityConcealRedact, RedactInputsNothing after them sees what they hide.
200reel.PriorityCutHideThe pauses at marks and the keys pressed in what it cuts are left out too.
300reel.PriorityZoomZoomIt draws its zoomed cells over what ran before it, and Mouse draws the pointer through it.
400reel.PriorityHighlightHighlightThe overlays after it aren't dimmed.
500reel.PriorityPausePause, HoldLastIts badge is drawn over what ran before it.
600reel.PriorityOverlayKeys, Mouse, ToastsThey show what they showed when a pause started.
700reel.PriorityDefaultAn effect with no priority of its own
800reel.PriorityCardIntro, Outro, steep.Logo, steep.OutroNo other effect has to draw on a card.

The gaps are there for your own effects.

Regions

Highlight, Zoom, and Redact work on a region of the terminal's cells:

RegionCovers
effects.Row(y)One row, from 0
effects.Rows(y0, y1)Rows from y0 up to but not including y1
effects.Cells(image.Rect(x0, y0, x1, y1))A rectangle of cells
effects.Text(s)Each cell that shows s, wherever it moves
effects.Regexp(re)Each match, on each line
effects.Env(names...)Each cell that shows the value of a variable
effects.Any(regions...)Any of them

Text, Regexp, and Env are matched on each frame, so they follow what they find. A Zoom holds what it matched when it started. Text is searched line by line, where a line is a row and the rows it soft-wrapped over, so it never matches across a line break a program made itself.

Timing

Those three also take timing options, except that Redact only takes Between:

OptionDoes
effects.Between(from, to)Limits it to the window between two marks. An empty label is the start or end.
effects.Ease(d)Eases it in over d before the window, and out over d after
effects.EaseIn(d), effects.EaseOut(d)Eases only one end

Without its from mark, an effect never comes on, and without its to mark, it stays on to the end. The exception is an effect that hides what it covers: Redact without its from mark covers the whole export, since where the secret starts isn't known. A missing mark is reported as a NoMarkError, and the outputs are still written.

Effects and .cast

A .cast output skips the effects, since it's the raw recording. Most effects are recorded in it as annotations, so an export of the file applies them again. Hide, Redact, and RedactInputs can't be combined with a .cast output at all, so write that in a separate run.

Keys

effects.Keys draws the keys pressed in the bottom right corner, so that someone watching can follow along.

The shop with chips in the corner showing the keys as they are pressed
Open in the inspector ↗
Effects: []reel.Effect{effects.Keys(time.Second)},

The argument is how long keys stay. Keys pressed within that time of each other show together, with repeats as one key and a count. They stay for that long after the last of them, fading out over the final third. Zero or less is no overlay.

The text typed by one input event, with enter and tab, is one chip, so typing hi\n shows hi⏎. Paste and mouse input aren't shown. Mouse draws the mouse.

Secrets

Keys shows what was typed, so add RedactInputs if a recording includes a password:

Effects: []reel.Effect{
	effects.RedactInputs(effects.Env("PASSWORD")),
	effects.Keys(time.Second),
},

With other effects

A Pause shows the keys as they were when it started, and the keys pressed in what a Hide cuts are left out. The overlay shares the bottom right corner, so don't move a Pause badge there.

The CLI's flag is --keys.

Toasts

A terminal program can send a desktop notification, with OSC 9 or OSC 777. A recording has nowhere to show one, unless you use effects.Toasts.

The shop with a toast sliding in at the corner saying the tea is ready
Open in the inspector ↗
Effects: []reel.Effect{effects.Toasts()},

A toast is a rounded panel with the title in bold over the body, each cut to 40 columns. It slides and fades in, stays, and fades out. Up to three show at once, stacked in a corner.

OptionDoes
WithToastCorner(c)Puts them in TopLeft, TopRight (the default), BottomLeft, or BottomRight
WithToastDuration(d)Sets how long each stays, fades included. The default is 2s.
WithClipboardToasts()Also shows "Copied to clipboard" when the program writes to the clipboard, without the text written

Only the notifications in the timeline's window are shown, and a Hide leaves out those that arrive in what it cuts.

The CLI's flag is --toasts.

Mouse

Mouse input doesn't show in a terminal, so a recording of it looks like the program changing by itself. effects.Mouse draws it, from the mouse events in the timeline.

A pointer gliding across the shop to a row, with a ripple where it clicks
Open in the inspector ↗
Effects: []reel.Effect{effects.Mouse()},

It draws a pointer that glides to each event's cell, a ripple where a button is pressed, a disc under the pointer while it's held, and a trail along a drag, which fades once the button is released. The pointer moves the way a hand moves a mouse: it sets off ahead of each event so it lands as the event is sent, eases in and out along a slight arc, and takes longer the farther it goes. A drag's cell-by-cell motion is smoothed into a curve. The pointer hides once the mouse is idle, and fades back in where it was when it next moves.

OptionSetsDefault
WithPointerStyle(s)PointerArrow or PointerDotArrow
WithPointerSize(px)The pointer's height, or a dot's diameterA cell's height, or half that for a dot
WithPointerColor(c)Its fillWhite
WithPointerGlide(d)How long it takes to glide to the next cell; farther glides take up to 4× as long150ms
WithPointerHide(d)How long it stays after the last event1s
WithButtonColor(button, c)The ripples, disc, and trail of a buttonBlue, green, and orange for left, middle, and right
WithMouseParts(p)Which of MousePointer, MouseClicks, and MouseDrags to drawAll

A session only sends a mouse event if the program asked for it, so events the program never saw aren't drawn either. If the pointer doesn't show, check that your program enabled the mouse.

The CLI's flag is --mouse, and _examples/reel-mouse has more.

Intro and Outro

A card reading The Tea Shop, the recording, then a card reading Thanks for watching
Effects: []reel.Effect{
	effects.Intro(1200*time.Millisecond,
		effects.Line{Text: "The Tea Shop"},
		effects.Line{Text: "a reel recording"},
	),
	effects.Outro(1200*time.Millisecond, effects.Line{Text: "Thanks for watching"}),
},

Intro shows a card before the first frame. Without lines, the card is the recording's title in bold, and a recording without a title gets no card.

Outro shows a card after the last. Without lines, it shows nothing. OutroFunc takes a function that's given the grid the card is laid out on, to show only as much as fits.

A duration of zero or less is no card, and Profile.Speed doesn't scale either.

Lines

A Line is text and a vt.Style. Text is drawn as is, so escape sequences aren't interpreted, and text that doesn't fit is cut off. Cards use the renderer's default colors, not ones the program set, so every card in a recording looks the same. On terminals larger than 80 by 24, the text is larger than the terminal's, so it stays readable when the recording is scaled down.

Drawing your own cards

effects.NewCardRenderer draws cards for your own effects.

In steep

steep.Logo and steep.Outro are built on these. steep.Outro shows the test's result.

The CLI's flags are --intro DUR, --intro-line TEXT, --outro DUR, and --outro-line TEXT.

Pause and HoldLast

The shop pausing on the filtered list with a pause badge, then again at the end
Effects: []reel.Effect{
	effects.Pause(
		effects.PauseAtMarks(900*time.Millisecond, "filter:typed"),
		effects.PauseAtEnd(time.Second),
	),
},

Pause freezes the frame at each of the points you give it, and the recording grows by that long. Without any, it pauses for a second at the end.

OptionPauses
PauseAtEnd(d)After the last terminal frame
PauseAtMarks(d, labels...)At each mark with one of those labels, or every mark without any
PauseAtMatch(d, re)At each mark whose label matches

Pauses aren't scaled by Profile.Speed. A label that names no mark in the window is reported as a NoMarkError after the outputs are written. In steep, failure:n marks are an exception, so a profile can pause on failure:1 whatever the result.

The badge

Each pause shows a badge: two bars on a translucent disc, so it reads as paused rather than frozen, and a looping recording's end reads as finished.

OptionSets
WithBadgeCorner(c)The corner. The default is TopRight. BottomRight overlaps Keys.
WithBadgeColor(c)The bars' color
WithBadgeSize(px)Its diameter
WithoutBadge()No badge

A recording that plays once stays on its last frame, badge and all, so a profile with loop=once may want WithoutBadge.

HoldLast

effects.HoldLast(d) plays the last terminal frame for at least d, with no badge. A last frame that isn't the terminal's, such as a card, is left alone, so it holds the screen before an Outro's card. The speed doesn't scale it.

The CLI's flags are --pause, --pause-at MARK, and --hold-last DUR.

Highlight

The shop dimmed except for the row being brewed, during the brew
Effects: []reel.Effect{
	effects.Highlight(
		effects.Regexp(regexp.MustCompile(` Oolong.*3m `)),
		effects.Between("brew:start", "brew:done"),
		effects.DimOthers(0.6),
		effects.Ease(200*time.Millisecond),
	),
},

Highlight darkens everything outside a region, and can outline it. The region is matched on each frame, so a Text region follows its text.

OptionSetsDefault
DimOthers(factor)How much to darken, from 0, unchanged, to 1, black. Window chrome and margins are included.0.5
Outline(c, px)A border around each rectangle of cells. A nil c is a warm yellow, and a px of zero is an eighth of a cell's height.None
Padding(px)How many pixels around the cells stay undimmedA third of a cell's width
Between, Ease, EaseIn, EaseOutWhen it's onThe whole recording

The CLI's flag is --highlight, and _examples/reel-highlight has a larger example.

Zoom

The view easing in on the status area while a tea brews, then easing back out
Effects: []reel.Effect{
	effects.Zoom(
		effects.Cells(image.Rect(1, 10, 38, 14)),
		effects.Between("brew:start", "brew:done"),
		effects.Ease(250*time.Millisecond),
	),
},

Zoom shows a region larger, inside the same grid. Everything around the grid, such as window chrome, is kept, so the image keeps its size. Easing moves smoothly between the whole grid and the zoom.

OptionSetsDefault
Scale(factor)How many times largerAs large as fits, with a cell of margin on every side
Anchor(x, y)Where the region sits, as a fraction of the grid. (0, 0) is the top left.Centered: (0.5, 0.5)
Between, Ease, EaseIn, EaseOutWhen it's onThe whole recording

The zoomed cells are drawn again at the larger scale, so text stays sharp. That needs the recording's rasterizer to be a render.Renderer, which it is by default. With one of your own, the frame's image is scaled up instead.

A zoom holds the part of the screen its region matched when it started, wherever the text moves after. It never shows past the edges of the terminal, so a region near one is drawn nearer it than its anchor asks.

Since zooms draw from the cells, they cover what effects before them drew.

The CLI's flag is --zoom, and _examples/reel-zoom has a larger example.

Hide

The shop's tour with the filtering cut out, jumping from the menu to the next step
Effects: []reel.Effect{
	effects.Hide(effects.HideBetween("filter:start", "filter:done")),
},

Hide cuts parts of the recording out of playback, so it's shorter by them. A frame is hidden while any rule hides it.

RuleHides
HideBetween(hideLabel, showLabel)Each span from a mark to the next showLabel after it. An empty hideLabel starts hidden, and an empty showLabel hides the rest.
HideWhile(region)Each frame on which a region covers a cell, such as a password prompt
HideUnless(region)Each frame on which it doesn't, such as until the first prompt shows

Hiding from the start is how to skip setup. Nothing shows of it, not even a first frame:

s.Mark("setup:done")

effects.Hide(effects.HideBetween("", "setup:done"))

This is what Hide and Show do in a VHS tape.

Rules

  • Without a show mark after a span starts, the span runs to the end. A missing mark is a NoMarkError, and the outputs are still written.
  • A recording it cuts entirely is an error from the export.
  • Stills of marks show their marks whether Hide cuts them or not.

Hide is a cutter, so it can't be combined with a .cast output, which would still hold what's cut. That's an error wrapping reel.ErrRawOutput.

To cut a window out rather than the parts around it, use Timeline.Between.

The CLI's flag is --hide FROM,TO.

Last updated on

On this page