use crate::{Color, Node, NodeModifiers, Point, Rect, SceneContext, Shape};
pub trait Transition: Send + Sync {
fn compose(
&self,
progress: f64,
outgoing: Node,
incoming: Node,
context: &SceneContext<'_>,
) -> Node;
fn easing(&self) -> crate::Easing {
crate::Easing::Linear
}
}
impl Transition for std::sync::Arc<dyn Transition> {
fn compose(
&self,
progress: f64,
outgoing: Node,
incoming: Node,
context: &SceneContext<'_>,
) -> Node {
self.as_ref().compose(progress, outgoing, incoming, context)
}
fn easing(&self) -> crate::Easing {
self.as_ref().easing()
}
}
struct Failure(crate::ValidationError);
impl crate::Drawable for Failure {
fn bounds(&self, _: &crate::DrawContext) -> Result<Rect, crate::ValidationError> {
Err(self.0.clone())
}
fn draw(
&self,
_: &crate::DrawContext,
_: &mut crate::Canvas<'_>,
) -> Result<(), crate::ValidationError> {
Err(self.0.clone())
}
}
pub fn diagnostic(error: crate::ValidationError) -> Node {
Node::from(Failure(error))
}
pub(crate) fn failure(field: &str, message: &str) -> Node {
diagnostic(crate::ValidationError::invalid(
"transition",
field,
message,
))
}
fn sum(samples: impl IntoIterator<Item = (Node, f64)>) -> Node {
Node::weighted_sum(samples).expect("bounded built-in transition weights")
}
fn endpoint(
progress: f64,
outgoing: &Node,
incoming: &Node,
context: &SceneContext<'_>,
) -> Option<Node> {
if !progress.is_finite() || !(0.0..=1.0).contains(&progress) {
return Some(failure(
"progress",
"Transition progress must be finite in 0..=1",
));
}
if progress == 0.0 {
return Some(
Node::group([
outgoing.clone(),
Node::group([incoming.clone()]).opacity(0.0),
])
.clip_with(context.bounds(), crate::ClipMode::Geometric),
);
}
if progress == 1.0 {
return Some(
Node::group([
Node::group([outgoing.clone()]).opacity(0.0),
incoming.clone(),
])
.clip_with(context.bounds(), crate::ClipMode::Geometric),
);
}
None
}
fn select(mask: Node, outgoing: Node, incoming: Node, context: &SceneContext<'_>) -> Node {
sum([
(
outgoing.mask(mask.clone(), crate::MaskMode::InvertedAlpha),
1.0,
),
(incoming.mask(mask, crate::MaskMode::Alpha), 1.0),
])
.clip_with(context.bounds(), crate::ClipMode::Geometric)
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CrossFade;
impl Transition for CrossFade {
fn compose(
&self,
progress: f64,
outgoing: Node,
incoming: Node,
context: &SceneContext<'_>,
) -> Node {
if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
return node;
}
sum([(outgoing, 1.0 - progress), (incoming, progress)])
.clip_with(context.bounds(), crate::ClipMode::Geometric)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Slide {
pub direction: Direction,
}
#[derive(Clone, Copy, Debug)]
pub struct Push {
pub direction: Direction,
}
fn displacement(direction: Direction, context: &SceneContext<'_>) -> (f64, f64) {
match direction {
Direction::Left => (-context.size().width, 0.0),
Direction::Right => (context.size().width, 0.0),
Direction::Up => (0.0, -context.size().height),
Direction::Down => (0.0, context.size().height),
}
}
impl Transition for Slide {
fn compose(
&self,
progress: f64,
outgoing: Node,
incoming: Node,
context: &SceneContext<'_>,
) -> Node {
if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
return node;
}
let (horizontal, vertical) = displacement(self.direction, context);
let offset = (-horizontal * (1.0 - progress), -vertical * (1.0 - progress));
let mask = Node::from(context.bounds().fill(Color::WHITE)).translate(offset.0, offset.1);
let incoming = incoming
.clip_with(context.bounds(), crate::ClipMode::Geometric)
.translate(offset.0, offset.1);
sum([
(outgoing.mask(mask, crate::MaskMode::InvertedAlpha), 1.0),
(incoming, 1.0),
])
.clip_with(context.bounds(), crate::ClipMode::Geometric)
}
}
impl Transition for Push {
fn compose(
&self,
progress: f64,
outgoing: Node,
incoming: Node,
context: &SceneContext<'_>,
) -> Node {
if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
return node;
}
let (horizontal, vertical) = displacement(self.direction, context);
sum([
(
outgoing
.clip_with(context.bounds(), crate::ClipMode::Geometric)
.translate(horizontal * progress, vertical * progress),
1.0,
),
(
incoming
.clip_with(context.bounds(), crate::ClipMode::Geometric)
.translate(-horizontal * (1.0 - progress), -vertical * (1.0 - progress)),
1.0,
),
])
.clip_with(context.bounds(), crate::ClipMode::Geometric)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Wipe {
pub angle: f64,
pub feather: f64,
}
impl Transition for Wipe {
fn compose(
&self,
progress: f64,
outgoing: Node,
incoming: Node,
context: &SceneContext<'_>,
) -> Node {
if !self.angle.is_finite() || !self.feather.is_finite() || self.feather < 0.0 {
return failure("wipe", "Wipe requires finite angle and nonnegative feather");
}
if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
return node;
}
let normal = Point::new(libm::cos(self.angle), libm::sin(self.angle));
let projections = [
0.0,
context.size().width * normal.x,
context.size().height * normal.y,
context.size().width * normal.x + context.size().height * normal.y,
];
let minimum = projections.iter().copied().fold(f64::INFINITY, f64::min);
let maximum = projections
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
let position = minimum - self.feather * 0.5 + (maximum - minimum + self.feather) * progress;
let mask = if self.feather == 0.0 {
let radius = libm::hypot(context.size().width, context.size().height) * 2.0;
Node::from(Rect::new(-radius, -radius, position, radius).fill(Color::WHITE))
.pivot(crate::Pivot::Point(Point::ORIGIN))
.rotate(self.angle)
} else {
let first = Point::new(
normal.x * (position - self.feather * 0.5),
normal.y * (position - self.feather * 0.5),
);
let last = Point::new(
normal.x * (position + self.feather * 0.5),
normal.y * (position + self.feather * 0.5),
);
Node::from(
context.bounds().fill(
crate::LinearGradient::new(first, last)
.stop(0.0, Color::WHITE)
.stop(1.0, Color::WHITE.with_alpha(0.0))
.units(crate::GradientUnits::UserSpace)
.space(crate::GradientSpace::LinearRgb),
),
)
};
select(mask, outgoing, incoming, context)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "snake_case")
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum IrisShape {
#[default]
Circle,
Rectangle,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Iris {
pub shape: IrisShape,
}
impl Transition for Iris {
fn compose(
&self,
progress: f64,
outgoing: Node,
incoming: Node,
context: &SceneContext<'_>,
) -> Node {
if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
return node;
}
let center = context.center();
let mask = match self.shape {
IrisShape::Circle => {
match crate::Circle::new(center, libm::hypot(center.x, center.y) * progress) {
Ok(circle) => Node::from(circle.fill(Color::WHITE)),
Err(error) => return diagnostic(error),
}
}
IrisShape::Rectangle => Node::from(
Rect::new(
center.x * (1.0 - progress),
center.y * (1.0 - progress),
center.x * (1.0 + progress),
center.y * (1.0 + progress),
)
.fill(Color::WHITE),
),
};
select(mask, outgoing, incoming, context)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ZoomBlur;
impl Transition for ZoomBlur {
fn compose(
&self,
progress: f64,
outgoing: Node,
incoming: Node,
context: &SceneContext<'_>,
) -> Node {
if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
return node;
}
let blur = |node: Node, base: f64| {
let samples = (0..12).map(|index| {
node.clone()
.clip_with(context.bounds(), crate::ClipMode::Geometric)
.pivot(crate::Pivot::Point(context.center()))
.scale(
base + libm::sin(std::f64::consts::PI * progress)
* 0.2
* (f64::from(index) + 0.5)
/ 12.0,
)
});
Node::accumulate(samples).expect("fixed zoom samples")
};
CrossFade.compose(
progress,
blur(outgoing, 1.0 + progress * 0.25),
blur(incoming, 0.75 + progress * 0.25),
context,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "snake_case")
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Direction {
Left,
Right,
Up,
Down,
}