Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/wiki/Configuration:-Animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,52 @@ animations {
}
```

#### `screen-transition`

<sup>Since: next release</sup>

The cross-fade animation of the `do-screen-transition` action.

```kdl
animations {
screen-transition {
curve "ease-out-expo"
}
}
```

##### `custom-shader`

<sup>Since: next release</sup>

You can write a custom shader for drawing the screen during a screen transition animation.

See [this example shader](./examples/screen_transition_custom_shader.frag) for a full documentation with several animations to experiment with.

If a custom shader fails to compile, niri will print a warning and fall back to the default, or previous successfully compiled shader.
When running niri as a systemd service, you can see the warnings in the journal: `journalctl -ef /usr/bin/niri`

> [!WARNING]
>
> Custom shaders do not have a backwards compatibility guarantee.
> I may need to change their interface as I'm developing new features.

<!-- Example: screen transition will show the previous screen texture and gradually fade in the next screen texture on top of it. -->

```kdl
animations {
screen-transition {
custom-shader r"
vec4 screen_transition_color(vec3 coords_curr_geo, vec3 size_curr_geo) {
vec3 coords_tex = niri_geo_to_tex * coords_curr_geo;
vec4 color = texture2D(niri_tex_from, coords_tex.st);
return color * (1.0 - niri_clamped_progress);
}
"
}
}
```

### Synchronized Animations

<sup>Since: 0.1.5</sup>
Expand Down
170 changes: 170 additions & 0 deletions docs/wiki/examples/screen_transition_custom_shader.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Your shader must contain one function (see the bottom of this file).
//
// It should not contain any uniform definitions or anything else, as niri
// provides them for you.
//
// All symbols defined by niri will have a niri_ prefix, so don't use it for
// your own variables and functions.

// The function that you must define looks like this:
vec4 screen_transition_color(vec3 coords_geo, vec3 size_geo) {
vec4 color = /* ...compute the color... */;
return color;
}

// It takes as input:
//
// * coords_geo: coordinates of the current pixel relative to the output.
//
// These are homogeneous (the Z component is equal to 1) and scaled in such a
// way that the 0 to 1 coordinates cover the full output.
//
// * size_geo: size of the output in logical pixels.
//
// It is homogeneous (the Z component is equal to 1).
//
// The screen transition shader renders as an overlay above the live workspace.
// Making pixels transparent (alpha = 0) will reveal the new workspace
// underneath, while opaque pixels (alpha = 1) will show the captured old
// workspace snapshot. As the transition progresses, you should generally make
// more pixels transparent to reveal the new workspace.
//
// The function must return the color of the pixel (with premultiplied alpha).
// The pixel color will be further processed by niri (e.g. to apply final
// alpha).

// Now let's go over the uniforms that niri defines.
//
// You should only rely on the uniforms documented here. Any other uniforms can
// change or be removed without notice.

// The captured texture of the old workspace (before the switch).
uniform sampler2D niri_tex_from;

// Matrix that converts geometry coordinates into the old workspace texture
// coordinates.
//
// You must always use this matrix to sample the texture, like this:
// vec3 coords_tex = niri_geo_to_tex * coords_geo;
// vec4 color = texture2D(niri_tex_from, coords_tex.st);
// This ensures correct sampling when the output has a transform (rotation).
uniform mat3 niri_geo_to_tex;


// Unclamped progress of the animation.
//
// Goes from 0 to 1 but may overshoot and oscillate.
uniform float niri_progress;

// Clamped progress of the animation.
//
// Goes from 0 to 1, but will stop at 1 as soon as it first reaches 1. Will not
// overshoot or oscillate.
uniform float niri_clamped_progress;

// Random float in [0; 1), consistent for the duration of the animation.
uniform float niri_random_seed;

// Mouse position in logical output coordinates (0,0 at top-left).
// Set to (-1, -1) if the cursor is not on this output.
uniform vec2 niri_mouse_pos;

// Now let's look at some examples. You can copy everything below this line
// into your custom-shader to experiment.

// Example: gradually fade out the old workspace, equivalent to the default
// crossfade transition.
vec4 default_crossfade(vec3 coords_geo, vec3 size_geo) {
vec3 coords_tex = niri_geo_to_tex * coords_geo;
vec4 color = texture2D(niri_tex_from, coords_tex.st);

// Fade out the old workspace to reveal the new one underneath.
color *= (1.0 - niri_clamped_progress);

return color;
}

// Example: horizontal wipe from left to right revealing the new workspace.
vec4 horizontal_wipe(vec3 coords_geo, vec3 size_geo) {
vec3 coords_tex = niri_geo_to_tex * coords_geo;
vec4 color = texture2D(niri_tex_from, coords_tex.st);

// The wipe edge moves from left (x=0) to right (x=1) with progress.
// Pixels to the left of the edge become transparent, revealing the
// new workspace underneath.
float alpha = smoothstep(niri_clamped_progress - 0.05,
niri_clamped_progress + 0.05,
coords_geo.x);

return color * alpha;
}

// Example: the old workspace slides upward off the screen.
vec4 slide_up(vec3 coords_geo, vec3 size_geo) {
// Shift the sampling position upward as the transition progresses.
vec3 shifted = vec3(coords_geo.x,
coords_geo.y + niri_clamped_progress,
1.0);

// Pixels that have shifted beyond the old workspace should be
// transparent, letting the new workspace show through.
if (shifted.y > 1.0)
return vec4(0.0);

vec3 coords_tex = niri_geo_to_tex * shifted;
vec4 color = texture2D(niri_tex_from, coords_tex.st);

return color;
}

// Example: pixels randomly dissolve to reveal the new workspace.
vec4 pixel_dissolve(vec3 coords_geo, vec3 size_geo) {
vec3 coords_tex = niri_geo_to_tex * coords_geo;
vec4 color = texture2D(niri_tex_from, coords_tex.st);

// Generate a pseudo-random value per pixel, offset by the random seed
// so each transition looks different.
vec2 seed = coords_geo.xy + vec2(niri_random_seed);
float random = fract(sin(dot(seed, vec2(12.9898, 78.233))) * 43758.5453);

// As progress increases, more pixels cross the threshold and become
// transparent, revealing the new workspace.
float threshold = niri_clamped_progress * 1.2;
float alpha = step(threshold, random);

return color * alpha;
}

// Example: expanding circle from mouse position revealing the new workspace.
// Recommended setting: duration-ms 300
vec4 circle_reveal(vec3 coords_geo, vec3 size_geo) {
vec3 coords_tex = niri_geo_to_tex * coords_geo;
vec4 color = texture2D(niri_tex_from, coords_tex.st);

// Use the mouse position as the circle center, or the screen center
// if the cursor is off this output (sentinel value -1, -1).
vec2 center = (niri_mouse_pos.x < 0.0)
? vec2(0.5)
: niri_mouse_pos / size_geo.xy;

// Correct for aspect ratio so the reveal is a circle, not an ellipse.
vec2 aspect = size_geo.xy / length(size_geo.xy);
vec2 delta = (coords_geo.xy - center) * aspect;
float dist = length(delta);

// The circle expands with progress. Scale by ~1.5 to ensure
// it covers the full screen even when starting from a corner.
float radius = niri_clamped_progress * 1.5;

// Pixels inside the expanding circle become transparent (new workspace),
// pixels outside stay opaque (old workspace).
float alpha = smoothstep(radius - 0.05, radius, dist);

return color * alpha;
}

// This is the function that you must define.
vec4 screen_transition_color(vec3 coords_geo, vec3 size_geo) {
// You can pick one of the example functions or write your own.
return circle_reveal(coords_geo, size_geo);
}
52 changes: 52 additions & 0 deletions niri-config/src/animations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct Animations {
pub screenshot_ui_open: ScreenshotUiOpenAnim,
pub overview_open_close: OverviewOpenCloseAnim,
pub recent_windows_close: RecentWindowsCloseAnim,
pub screen_transition: ScreenTransitionAnim,
}

impl Default for Animations {
Expand All @@ -37,6 +38,7 @@ impl Default for Animations {
screenshot_ui_open: Default::default(),
overview_open_close: Default::default(),
recent_windows_close: Default::default(),
screen_transition: Default::default(),
}
}
}
Expand Down Expand Up @@ -71,6 +73,8 @@ pub struct AnimationsPart {
pub overview_open_close: Option<OverviewOpenCloseAnim>,
#[knuffel(child)]
pub recent_windows_close: Option<RecentWindowsCloseAnim>,
#[knuffel(child)]
pub screen_transition: Option<ScreenTransitionAnim>,
}

impl MergeWith<AnimationsPart> for Animations {
Expand All @@ -97,6 +101,7 @@ impl MergeWith<AnimationsPart> for Animations {
screenshot_ui_open,
overview_open_close,
recent_windows_close,
screen_transition,
);
}
}
Expand Down Expand Up @@ -326,6 +331,27 @@ impl Default for RecentWindowsCloseAnim {
}
}

#[derive(Debug, Clone, PartialEq)]
pub struct ScreenTransitionAnim {
pub anim: Animation,
pub custom_shader: Option<String>,
}

impl Default for ScreenTransitionAnim {
fn default() -> Self {
Self {
anim: Animation {
off: false,
kind: Kind::Easing(EasingParams {
duration_ms: 300,
curve: Curve::EaseOutExpo,
}),
},
custom_shader: None,
}
}
}

impl<S> knuffel::Decode<S> for WorkspaceSwitchAnim
where
S: knuffel::traits::ErrorSpan,
Expand Down Expand Up @@ -524,6 +550,32 @@ where
}
}

impl<S> knuffel::Decode<S> for ScreenTransitionAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().anim;
let mut custom_shader = None;
let anim = Animation::decode_node(node, ctx, default, |child, ctx| {
if &**child.node_name == "custom-shader" {
custom_shader = parse_arg_node("custom-shader", child, ctx)?;
Ok(true)
} else {
Ok(false)
}
})?;

Ok(Self {
anim,
custom_shader,
})
}
}

impl Animation {
pub fn new_off() -> Self {
Self {
Expand Down
12 changes: 12 additions & 0 deletions niri-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1615,6 +1615,18 @@ mod tests {
),
},
),
screen_transition: ScreenTransitionAnim {
anim: Animation {
off: false,
kind: Easing(
EasingParams {
duration_ms: 300,
curve: EaseOutExpo,
},
),
},
custom_shader: None,
},
},
gestures: Gestures {
dnd_edge_view_scroll: DndEdgeViewScroll {
Expand Down
13 changes: 13 additions & 0 deletions src/animation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,19 @@ impl Animation {
self.clock.now() >= self.start_time + self.duration
}

/// Returns whether the animation is done as seen from a custom time `at`.
///
/// Useful for delayed animations that derive their effective clock time from an offset
/// (e.g. `clock.now().saturating_sub(delay)`). Handles `should_complete_instantly`
/// the same way as `is_done`.
pub fn is_done_at(&self, at: Duration) -> bool {
if self.clock.should_complete_instantly() {
return true;
}

at >= self.start_time + self.duration
}

pub fn is_clamped_done(&self) -> bool {
if self.clock.should_complete_instantly() {
return true;
Expand Down
3 changes: 3 additions & 0 deletions src/backend/tty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,9 @@ impl Tty {
if let Some(src) = config.animations.window_open.custom_shader.as_deref() {
shaders::set_custom_open_program(gles_renderer, Some(src));
}
if let Some(src) = config.animations.screen_transition.custom_shader.as_deref() {
shaders::set_custom_screen_transition_program(gles_renderer, Some(src));
}
drop(config);

niri.update_shaders();
Expand Down
3 changes: 3 additions & 0 deletions src/backend/winit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ impl Winit {
if let Some(src) = config.animations.window_open.custom_shader.as_deref() {
shaders::set_custom_open_program(renderer, Some(src));
}
if let Some(src) = config.animations.screen_transition.custom_shader.as_deref() {
shaders::set_custom_screen_transition_program(renderer, Some(src));
}
drop(config);

niri.update_shaders();
Expand Down
Loading