Skip to content

Cursors, emoji, sound effects and GIFs

The kit draws its own mouse cursors (@remotion/mac-cursors), so they need no image files. Each cursor fades in over its fade seconds.

Component Props What it does
Cursor shape, style, x, y, size, scale, opacity, shadow a still cursor with its hotspot at (x, y). CursorShape::{Arrow, Pointer, IBeam, Grab, Grabbing, Crosshair} (CursorShape::ALL), CursorStyle::{Dark, Light}
CursorPath points: Vec<CursorPoint>, shape, style, size, travel, motion, fade, click: ClickLook moves through timed points and arrives at each on time. It leaves a point travel seconds before the next is due. CursorPoint::new(at, x, y).click() presses the cursor and sends out a ring when it arrives. position(t) and clicks() give the path
Click x, y, at, shape, style, size, fade, look: ClickLook one click at at seconds: the cursor presses and a ring spreads out
Drag from, to, start, run_time, motion, style, size, swap, fade, children the open hand closes, carries its children from from to to, and opens again
TypeAt typewriter: Typewriter, shape, style, size, lead, fade, click a text cursor clicks at the typewriter’s position lead seconds before it starts typing

Gallery scenes: Cursor, CursorPath, Click, Drag, TypeAt.

Cursor demo, first frame
Cursor 3.5 s
CursorPath demo, first frame
CursorPath 3.5 s
Click demo, first frame
Click 2 s
Drag demo, first frame
Drag 2.6 s
TypeAt demo, first frame
TypeAt 3 s

CursorMotion::Spring { stiffness, damping } (the default) and CursorMotion::Smooth { overshoot } both overshoot the target slightly and settle on it by the time it is due. ClickLook sets the ring’s color, radius and stroke_width, how far the cursor presses (depth, press seconds) and how long the ring lasts (run_time).

use fframes_kit::prelude::*;
fn demo<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
let path = ui.show(CursorPath {
points: vec![
CursorPoint::new(0.3, 400.0, 700.0),
CursorPoint::new(1.2, 960.0, 540.0).click(),
CursorPoint::new(2.4, 1300.0, 620.0).click(),
],
shape: CursorShape::Pointer,
travel: 0.9,
..ui.defaults()
});
let drag = ui.show(Drag {
from: (700.0, 300.0),
to: (1200.0, 300.0),
start: 1.0,
children: children(|_| svgr!(<rect x="640" y="250" width="120" height="100" rx="12" fill="#4f7cff" />)),
..ui.defaults()
});
svgr!(<g>{path}{drag}</g>)
}
use fframes_kit::prelude::*;
fn typing_cursor<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
let cursor = ui.show(Cursor { x: 400.0, y: 400.0, shape: CursorShape::Pointer, ..ui.defaults() });
let defaults: TypeAt = ui.defaults();
let typing = ui.show(TypeAt {
typewriter: Typewriter { text: "render the gallery".into(), x: 620.0, y: 553.0, delay: 1.0, ..defaults.typewriter },
..defaults
});
svgr!(<g>{cursor}{typing}</g>)
}

AnimatedEmoji (feature lottie, @remotion/animated-emoji) plays one of Google’s Noto animated emoji. These files are licensed CC BY 4.0 and the kit does not ship them. Download the ones a video uses into its media folder, and show emoji::ATTRIBUTION in the video or its description. Gallery scene: AnimatedEmoji.

AnimatedEmoji demo, first frame
AnimatedEmoji 3 s
  • emoji::NAMES lists the emoji by name and Noto code. emoji::url(name, tone) gives the download URL, and emoji::file_name(name, tone) the file name the component looks for, e.g. thumbs-up-medium.json.
  • The props are name, skin_tone: Option<SkinTone>, x, y, size (the longer side), start, speed, looping and fade. A skin tone applies only to the emoji whose skin_tones is set (hands and gestures); the others ignore it.
  • When the file is missing, the component draws nothing, and inspect warns with the URL to download and the name to save it as.
use fframes_kit::prelude::*;
fn thumbs<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
ui.show(AnimatedEmoji {
name: "thumbs-up",
skin_tone: Some(SkinTone::Medium),
size: 200.0,
..ui.defaults()
})
}
assert_eq!(emoji::file_name("thumbs-up", Some(SkinTone::Medium)), "thumbs-up-medium.json");

The crate’s sfx/ folder holds a small pack of CC0 sounds (@remotion/sfx). Each is under 1 s long, mono, and peaks at -3 dBFS:

Sfx File Sound
Whoosh whoosh.wav a stick swung through the air, for transitions
Click click.wav a mouse click
Ding ding.wav a bell
Pop pop.wav a short pop, for things that appear
SwooshDown swoosh-down.wav a falling sweep, for things that leave
Shutter shutter.wav a camera shutter
Switch switch.wav a toggle switch

Gallery scenes: SoundEffects, TransitionSfx.

SoundEffects demo, first frame
SoundEffects 4.2 s
TransitionSfx demo, first frame
TransitionSfx 3.5 s

sfx/LICENSE.md names the source of each file. Copy the files you use into the video’s media folder: include_media_dir! cannot read the pack folder itself, because of its LICENSE.md. Sfx::File("mine.wav") plays a file of your own, and a file of your own with a pack name replaces the kit’s.

A video’s audio is set in fn audio, so the helpers return AudioTracks to add to it. Each hit plays at sfx::DEFAULT_GAIN_DB (-6 dB). Its rate changes by up to sfx::RATE_VARIATION (4 %), so repeats do not sound alike. The change depends only on the hit’s index, so every render sounds the same.

  • sfx::at(&[1.0, 2.5], Sfx::Pop) gives one hit at each time.
  • sfx::on_transitions(&series_lengths, fps, Sfx::Whoosh) gives one hit where each transition of a series starts, i.e. where its two sequences start to overlap. sfx::transition_times gives those times.
  • sfx::on_beats(&beat, 0.0..8.0, Sfx::Click) gives one hit on each whole beat of the range.
  • sfx::on_clicks(&path) gives one click sound per click of a CursorPath.
use fframes::AudioMap;
use fframes_kit::prelude::*;
const FADE: Timing = Timing::Linear { duration: 0.5, easing: RateFunc::Smooth };
fn audio() -> AudioMap<'static> {
let series = [
SeriesLength::Sequence(2.0),
SeriesLength::Transition(FADE),
SeriesLength::Sequence(2.0),
];
let mut tracks = sfx::on_transitions(&series, 30, Sfx::Whoosh);
tracks.extend(sfx::on_beats(&Beat::from_bpm(120.0, 0.0), 0.0..4.0, Sfx::Click));
tracks.extend(sfx::at(&[3.2], Sfx::Ding));
AudioMap::from(tracks)
}

Check the levels with audio analyze. Hits that overlap add up.

Gif (feature gif, @remotion/gif) plays an animated GIF. Decode the file once with Gif::decode(bytes) in the video’s constructor and keep the GifData. The component’s props are gif, x, y, width, height, fit: GifFit::{Fill, Contain, Cover}, start, speed, looping and fade. Gallery scene: Gif.

Gif demo, first frame
Gif 2.5 s
  • Each frame shows for its own delay. A delay under 20 ms counts as 100 ms, as in browsers.
  • With looping the GIF starts again; without it, the last frame stays.
  • include_media_dir! does not take .gif files, so read the bytes with include_bytes! or from a file.
  • The web editor draws a placeholder rectangle of the same size instead of the GIF.
use std::sync::LazyLock;
use fframes_kit::prelude::*;
static SPINNER: LazyLock<Option<GifData>> =
LazyLock::new(|| Gif::decode(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../gallery/media/gif/bounce.gif"))).ok());
fn spinner<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
let Some(gif) = SPINNER.as_ref() else {
return Svgr::empty();
};
ui.show(Gif { gif, x: 760.0, y: 340.0, width: 400.0, height: 400.0, fit: GifFit::Contain, ..ui.defaults() })
}

Components are plain structs. Set the fields you need and fill the rest with ..ui.defaults(): the defaults below come from the current theme (theme. values are fields of Theme).

A mouse cursor with its hotspot at (x, y) (the tip of the arrow, the middle of the I-beam). size is the height in pixels of the 24 unit design, scale grows or shrinks it about the hotspot (a press), shadow adds a soft shadow below it. The kit draws its own shapes; they are not the artwork of any operating system.

Field Type Default Description
shape CursorShape CursorShape::Arrow Which cursor is drawn: arrow, pointer, I-beam, grab, grabbing or crosshair.
style CursorStyle CursorStyle::Dark Colours of the cursor: Dark (black with a white outline) or Light (white with a black outline).
x f32 960.0 Horizontal position of the hotspot in pixels of the canvas.
y f32 540.0 Vertical position of the hotspot in pixels of the canvas, down.
size f32 40.0 Height in pixels of the 24 unit design.
scale f32 1.0 Grows or shrinks the cursor about its hotspot; 1 is normal, below 1 is a press, 0 or less draws nothing.
opacity f32 1.0 0 transparent to 1 opaque.
shadow bool true Adds a soft shadow below the cursor.

Methods:

  • fn draw(&self) -> Svgr: The cursor without a Ui; nothing when it is invisible.

Source

A cursor that moves through timed points, in the order of their times. It leaves a point travel seconds before the next one is due (or at once when they are closer) and arrives on time, with motion. A point with click presses the cursor and sends out a ring when it arrives; sfx::on_clicks gives the sounds. The cursor fades in over fade seconds from the first point’s time.

Field Type Default Description
points Vec<CursorPoint> Vec::new() The timed points the cursor passes through, in the order of their times; empty draws nothing.
shape CursorShape CursorShape::Arrow Which cursor is drawn: arrow, pointer, I-beam, grab, grabbing or crosshair.
style CursorStyle CursorStyle::Dark Colours of the cursor: Dark (black with a white outline) or Light (white with a black outline).
size f32 40.0 Height in pixels of the 24 unit design.
travel f32 0.6 Seconds a move takes, ending when the next point is due (shorter when the points are closer in time).
motion CursorMotion CursorMotion::default() How the cursor travels to the next point: a smooth ease or a spring, both arriving on time.
fade f32 theme.motion.duration.fast Seconds the cursor takes to fade in from the first point’s time.
click ClickLook ClickLook::themed(theme) How a click looks: the press depth and duration and the ring that goes out.

Methods:

  • fn position(&self, t: f32) -> (f32, f32): Where the hotspot is at t seconds: the first point before it, the last after.
  • fn clicks(&self) -> impl Iterator<Item = f32>: The times of the clicks, in seconds.

Source

A cursor at (x, y) that clicks at at seconds: it presses, then a ring goes out. It fades in over fade seconds from the start of its container.

Field Type Default Description
x f32 960.0 Horizontal position of the cursor in pixels of the canvas.
y f32 540.0 Vertical position of the cursor in pixels of the canvas, down.
at f32 0.5 Second of the click, local to the parent container.
shape CursorShape CursorShape::Pointer Which cursor is drawn: arrow, pointer, I-beam, grab, grabbing or crosshair.
style CursorStyle CursorStyle::Dark Colours of the cursor: Dark (black with a white outline) or Light (white with a black outline).
size f32 40.0 Height in pixels of the 24 unit design.
fade f32 theme.motion.duration.fast Seconds the cursor takes to fade in from the start of its container.
look ClickLook ClickLook::themed(theme) How the click looks: the press depth and duration and the ring that goes out.

Source

The grabbing hand picks up its children at from at start seconds and carries them to to in run_time seconds. The open hand closes over swap seconds before the move and opens again after it; the children move with the cursor. The cursor fades in over fade seconds from the start of the container.

Field Type Default Description
from (f32, f32) (760.0, 540.0) Where the cursor picks the children up, in pixels of the canvas; the children keep their own place there.
to (f32, f32) (1160.0, 540.0) Where the cursor drops the children, in pixels of the canvas.
start f32 0.6 Second the carry starts, local to the parent container; the hand closes just before.
run_time f32 0.9 Seconds the carry takes.
motion CursorMotion CursorMotion::default() How the cursor travels from from to to: a smooth ease or a spring, arriving on time.
style CursorStyle CursorStyle::Dark Colours of the cursor: Dark (black with a white outline) or Light (white with a black outline).
size f32 40.0 Height in pixels of the 24 unit design.
swap f32 0.12 Seconds the hand takes to close before the move and to open after it.
fade f32 theme.motion.duration.fast Seconds the cursor takes to fade in from the start of its container.
children Children Box::new(|_| Svgr::empty()) What is carried, drawn at its own place and moved by the cursor’s offset (a closure taking the Ui).

Methods:

  • fn position(&self, t: f32) -> (f32, f32): Where the cursor, and the offset of the children from where they were, is at t.
  • fn grip(&self, t: f32) -> f32: How closed the hand is at t, from 0 (open) to 1.

Source

A text cursor that clicks into a field and the Typewriter that then types there. The cursor sits just left of the text’s start, fades in over fade seconds from the start of the container and clicks lead seconds before the typing starts (the typewriter’s delay), so the delay should be at least fade + lead.

Field Type Default Description
typewriter Typewriter Typewriter { delay: 0.9, ..Typewriter::themed(theme) } The text that is typed after the click; its x, y and delay place the cursor and time the click.
shape CursorShape CursorShape::IBeam Which cursor is drawn; the I-beam by default.
style CursorStyle CursorStyle::Dark Colours of the cursor: Dark (black with a white outline) or Light (white with a black outline).
size f32 40.0 Height in pixels of the 24 unit design; also sets how far left of the text the cursor sits.
lead f32 0.4 Seconds the click comes before the typing starts.
fade f32 theme.motion.duration.fast Seconds the cursor takes to fade in from the start of its container.
click ClickLook ClickLook::themed(theme) How the click looks: the press depth and duration and the ring that goes out.

Methods:

  • fn cursor_at(&self) -> (f32, f32): Where the cursor’s hotspot is: left of the start of the text, at its middle.
  • fn click_at(&self) -> f32: The second of the click.

Source

A Noto animated emoji (Remotion’s <AnimatedEmoji>), played from start seconds of its container from the file file_name gives in the media folder. (x, y) is the top left of a size pixel square. It fades in over fade seconds and plays at speed, again and again with looping, otherwise its last frame stays. A missing file draws nothing, and inspect warns with the address to download it from. The tone applies to the emoji that have skin tones.

Field Type Default Description
name &str "grinning" Name of the emoji from NAMES, which also names its .json file in the media folder.
skin_tone Option<SkinTone> None Skin tone of a hand emoji; None for the default, and ignored for emoji without skin tones.
x f32 880.0 Left edge in pixels of the canvas.
y f32 460.0 Top edge in pixels of the canvas.
size f32 160.0 Side of the square the emoji fills, in pixels.
start f32 0.0 Second it starts playing, local to the parent container.
speed f32 1.0 Playback speed; 1 is normal, 2 twice as fast.
looping bool true Plays again and again; false keeps the last frame.
fade f32 theme.motion.duration.fast Seconds it takes to fade in from start.

Methods:

  • fn file(&self) -> String: The file this emoji plays.

Source

An animated GIF in the box (x, y, width, height), playing from start seconds of its container at speed. Each frame shows for its own delay (a delay under 20 ms is 100 ms, as in browsers); with looping it starts again, otherwise the last frame stays. It fades in over fade seconds. Decode the file once with Gif::decode and keep the GifData in the video. The editor (wasm32) draws a placeholder rectangle of the same size.

Field Type Default Description
gif &GifData &NO_GIF The decoded GIF from Gif::decode; the default one draws nothing.
x f32 0.0 Left edge of the box in pixels of the canvas.
y f32 0.0 Top edge of the box in pixels of the canvas.
width f32 320.0 Width of the box in pixels.
height f32 320.0 Height of the box in pixels.
fit GifFit GifFit::Fill How the GIF fills the box: stretched, contained or covering.
start f32 0.0 Second it starts playing, local to the parent container.
speed f32 1.0 Playback speed; 1 is normal, 2 twice as fast.
looping bool true Plays again and again; false keeps the last frame.
fade f32 theme.motion.duration.fast Seconds it takes to fade in from start.
placeholder Color theme.colors.surface Colour of the rectangle the editor draws in place of the GIF.

Methods:

  • fn decode(bytes: &[u8]) -> Result<GifData, GifError>: Decodes every frame of a GIF file, composited onto the frames before it, and turns each into an image once. Call it in the video’s constructor, not per frame.
  • fn frame_at(&self, t: f32) -> usize: The frame shown at t seconds of the container.

Source