mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 20:16:50 -06:00
Every transition and Effect Option was built in PowerPoint (COM), exported as video and compared frame by frame with DeckCraft at the same progress (mean abs diff over 150 variants 35.7 -> 15.0, 0-255). - anim: transition_frame(kind, option, t, aspect) with measured curves (smoothstep fades, ease-in-out moves, Cover/Uncover/Pan curves), directions and option meanings, soft edges as per-corner opacity, 3-D effects as perspective-projected cards, Dynamic Content moving slide contents over cross-fading backgrounds (Source::OldBack/NewBack/OldContent/NewContent); layer_mesh subdivides perspective quads so textures stay straight. - New options: Page Curl Single, Glitter Hexagons, Shred In/Out, Zoom and Rotate; PowerPoint-style Effect Options labels. - render: composite_transition (CPU twin of the show), render_slide_part, render_transition; deckcraft-cli render-transition FILE --slide N --t T. - show: per-vertex opacity meshes, slide parts rendered once per transition. - pptx: PowerPoint's elements and defaults (p:strips diagonal wipes, p14:warp and p:newsflash for Zoom, p:zoom for Shape In/Out, glitter pattern, shred rectangle/dir, prstTrans invX per preset, pageCurlSingle, defaults for randomBar/doors/window/pan/zoom); PowerPoint reopens DeckCraft's files with the same effects. - Tests: measured curves, probes of which slide shows where at t, option coverage, per-frame triangle budget, PowerPoint XML and defaults round trip. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
119 lines
4.3 KiB
Rust
119 lines
4.3 KiB
Rust
//! Slide show timing for DeckCraft.
|
|
//!
|
|
//! - [`Timeline`]: a slide's animation list grouped into click steps, with per-shape (and
|
|
//! per-paragraph) [`AnimState`] at any point of the show.
|
|
//! - [`transition_layers`]: a slide transition frame as textured quads of the old/new slide images.
|
|
//! - [`morph_frame`]: a Morph transition frame (shapes matched by [`morph_pairs`], moved by
|
|
//! [`morph_xfrm`]).
|
|
//! - [`ShowState`]: slide show navigation (hidden slides, custom shows, loop, timings).
|
|
//!
|
|
//! Pure logic: no rendering, no UI toolkit. Every function accepts hostile numbers (NaN, huge or
|
|
//! negative times) and never panics.
|
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
#![forbid(unsafe_code)]
|
|
|
|
mod easing;
|
|
mod effects;
|
|
mod mesh;
|
|
mod morph;
|
|
mod path;
|
|
mod show;
|
|
mod timeline;
|
|
mod transition;
|
|
mod transition_fx;
|
|
mod transition_geom;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
#[cfg(test)]
|
|
mod transition_tests;
|
|
|
|
pub use deckcraft_model::Rgba;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub use easing::ease;
|
|
pub use mesh::{MeshVertex, layer_mesh};
|
|
pub use morph::{MorphFrame, MorphPath, MorphText, morph_frame, morph_pairs, morph_xfrm};
|
|
pub use path::{MotionPath, default_path};
|
|
pub use show::{LinkJump, ShowAction, ShowState, safe_url, show_order};
|
|
pub use timeline::{EffectTiming, Sequence, Timeline, TriggerPlay};
|
|
pub use transition::{DEFAULT_ASPECT, Layer, Source, transition_frame, transition_layers, uses_slide_parts};
|
|
|
|
/// Animation state of one shape (or one paragraph of a shape's text) at one moment of the show.
|
|
///
|
|
/// Same meaning as the renderer's `ShapeState`: translation in points, scale and rotation (degrees)
|
|
/// around the shape's centre, a reveal clip in shape-local unit space `[l, t, r, b]`, and a fill
|
|
/// colour override.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct AnimState {
|
|
pub visible: bool,
|
|
pub opacity: f64,
|
|
pub offset_x: f64,
|
|
pub offset_y: f64,
|
|
pub scale_x: f64,
|
|
pub scale_y: f64,
|
|
pub rotate: f64,
|
|
pub clip: Option<[f64; 4]>,
|
|
pub tint: Option<Rgba>,
|
|
}
|
|
|
|
impl Default for AnimState {
|
|
fn default() -> Self {
|
|
AnimState { visible: true, opacity: 1.0, offset_x: 0.0, offset_y: 0.0, scale_x: 1.0, scale_y: 1.0, rotate: 0.0, clip: None, tint: None }
|
|
}
|
|
}
|
|
|
|
impl AnimState {
|
|
/// A hidden state.
|
|
pub fn hidden() -> Self {
|
|
AnimState { visible: false, ..Default::default() }
|
|
}
|
|
/// Is this the identity (nothing to apply)?
|
|
pub fn is_identity(&self) -> bool {
|
|
*self == AnimState::default()
|
|
}
|
|
/// Apply `d` on top of `self`: opacities and scales multiply, offsets and rotations add, clips
|
|
/// intersect, a tint replaces.
|
|
pub fn compose(&self, d: &AnimState) -> AnimState {
|
|
let clip = match (self.clip, d.clip) {
|
|
(Some(a), Some(b)) => Some([a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])]),
|
|
(a, None) => a,
|
|
(None, b) => b,
|
|
};
|
|
AnimState {
|
|
visible: self.visible && d.visible,
|
|
opacity: self.opacity * d.opacity,
|
|
offset_x: self.offset_x + d.offset_x,
|
|
offset_y: self.offset_y + d.offset_y,
|
|
scale_x: self.scale_x * d.scale_x,
|
|
scale_y: self.scale_y * d.scale_y,
|
|
rotate: self.rotate + d.rotate,
|
|
clip,
|
|
tint: d.tint.or(self.tint),
|
|
}
|
|
}
|
|
/// Replace any non-finite number with its neutral value and clamp opacity and clip.
|
|
pub fn sanitized(mut self) -> AnimState {
|
|
let f = |v: f64, d: f64| if v.is_finite() { v } else { d };
|
|
self.opacity = f(self.opacity, 1.0).clamp(0.0, 1.0);
|
|
self.offset_x = f(self.offset_x, 0.0);
|
|
self.offset_y = f(self.offset_y, 0.0);
|
|
self.scale_x = f(self.scale_x, 1.0);
|
|
self.scale_y = f(self.scale_y, 1.0);
|
|
self.rotate = f(self.rotate, 0.0);
|
|
self.clip = self.clip.map(|c| {
|
|
let l = f(c[0], 0.0).clamp(0.0, 1.0);
|
|
let t = f(c[1], 0.0).clamp(0.0, 1.0);
|
|
let r = f(c[2], 1.0).clamp(0.0, 1.0);
|
|
let b = f(c[3], 1.0).clamp(0.0, 1.0);
|
|
[l, t, r.max(l), b.max(t)]
|
|
});
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Clamp a hostile number into `[lo, hi]`, mapping NaN to `nan`.
|
|
pub(crate) fn clampf(v: f64, lo: f64, hi: f64, nan: f64) -> f64 {
|
|
if v.is_nan() { nan } else { v.clamp(lo, hi) }
|
|
}
|