mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-29 21:16:57 -06:00
Merge PR #222 from justinjohn0306: Animations: nine PowerPoint exit effects import as themselves (#147)
# Conflicts: # crates/anim/src/tests.rs # crates/pptx/tests/roundtrip.rs
This commit is contained in:
@@ -44,6 +44,15 @@ pub(crate) fn exit_base(effect: &str) -> &str {
|
||||
"swivelOut" => "swivel",
|
||||
"bounceOut" => "bounce",
|
||||
"dissolveOut" => "dissolve",
|
||||
"blindsOut" => "blinds",
|
||||
"boxOut" => "box",
|
||||
"checkerboardOut" => "checkerboard",
|
||||
"diamondOut" => "diamond",
|
||||
"peekOut" => "peek",
|
||||
"stripsOut" => "strips",
|
||||
"sinkDown" => "rise",
|
||||
"spinnerOut" => "spinner",
|
||||
"contract" => "expand",
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,3 +819,15 @@ fn honeycomb_cells_reach_the_bottom_before_the_slide_ends() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #147: exits PowerPoint plays downward leave downward here too.
|
||||
#[test]
|
||||
fn sink_down_and_peek_out_to_bottom_move_down() {
|
||||
for (effect, option) in [("sinkDown", ""), ("peekOut", "b"), ("floatOut", "d")] {
|
||||
let mut a = anim(1, AnimClass::Exit, effect, AnimStart::OnClick);
|
||||
a.option = option.into();
|
||||
let tl = Timeline::new(&slide_with(vec![a]), W, H);
|
||||
let s = tl.state(ShapeId(1), None, 0, 0.5);
|
||||
assert!(s.offset_y > 0.0, "{effect} {option}: {s:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +505,8 @@ fn build_spec(a: &Animation, xf: Xfrm, sw: f64, sh: f64, resolve: &dyn Fn(&deckc
|
||||
EffectSpec {
|
||||
class: a.class,
|
||||
effect,
|
||||
option: a.option.clone(),
|
||||
// Sink Down is Rise Up played backwards, leaving downward.
|
||||
option: if a.effect == "sinkDown" { "d".into() } else { a.option.clone() },
|
||||
accel: a.smooth_start,
|
||||
decel: a.smooth_end,
|
||||
bounce: a.bounce_end,
|
||||
|
||||
@@ -365,6 +365,15 @@ pub static ANIMATIONS: &[(&str, &str, AnimClass, u32, u32, &[&str])] = &[
|
||||
("swivelOut", "Swivel", AnimClass::Exit, 45, 2000, &["horz", "vert"]),
|
||||
("bounceOut", "Bounce", AnimClass::Exit, 26, 2000, &[]),
|
||||
("dissolveOut", "Dissolve Out", AnimClass::Exit, 9, 500, &[]),
|
||||
("blindsOut", "Blinds", AnimClass::Exit, 3, 500, &["horz", "vert"]),
|
||||
("boxOut", "Box", AnimClass::Exit, 4, 500, &["in", "out"]),
|
||||
("checkerboardOut", "Checkerboard", AnimClass::Exit, 5, 500, &["across", "down"]),
|
||||
("diamondOut", "Diamond", AnimClass::Exit, 8, 500, &["in", "out"]),
|
||||
("peekOut", "Peek Out", AnimClass::Exit, 12, 500, &["b", "l", "t", "r"]),
|
||||
("stripsOut", "Strips", AnimClass::Exit, 18, 500, &["lu", "ru", "ld", "rd"]),
|
||||
("sinkDown", "Sink Down", AnimClass::Exit, 37, 1000, &[]),
|
||||
("spinnerOut", "Spinner", AnimClass::Exit, 49, 1000, &[]),
|
||||
("contract", "Contract", AnimClass::Exit, 55, 500, &[]),
|
||||
("lines", "Lines", AnimClass::Path, 64, 2000, &["down", "up", "right", "left"]),
|
||||
("arcs", "Arcs", AnimClass::Path, 37, 2000, &["down", "up", "right", "left"]),
|
||||
("turns", "Turns", AnimClass::Path, 43, 2000, &["down", "up", "right", "left"]),
|
||||
|
||||
@@ -304,6 +304,12 @@ const SUBTYPES: &[(&str, &[(&str, u32)])] = &[
|
||||
("blinds", HV),
|
||||
("swivel", HV),
|
||||
("swivelOut", HV),
|
||||
("blindsOut", HV),
|
||||
("boxOut", INOUT),
|
||||
("checkerboardOut", &[("across", 10), ("down", 5)]),
|
||||
("diamondOut", INOUT),
|
||||
("peekOut", DIR4),
|
||||
("stripsOut", &[("lu", 9), ("ru", 3), ("ld", 12), ("rd", 6)]),
|
||||
("checkerboard", &[("across", 10), ("down", 5)]),
|
||||
("wheel", WHEEL),
|
||||
("wheelOut", WHEEL),
|
||||
@@ -352,6 +358,10 @@ pub fn fallback_effect(class: AnimClass) -> &'static str {
|
||||
|
||||
/// The preset id to write for an animation.
|
||||
pub fn preset_for(a: &deckcraft_model::Animation) -> u32 {
|
||||
// Float Down is PowerPoint's preset 47 (Float Up is 42).
|
||||
if matches!(a.effect.as_str(), "float" | "floatOut") && a.option == "d" {
|
||||
return 47;
|
||||
}
|
||||
if let Some(p) = a.preset_id {
|
||||
let consistent = match effect_for_preset(a.class, p) {
|
||||
Some(e) => e == a.effect,
|
||||
@@ -377,6 +387,25 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::xml::parse;
|
||||
|
||||
/// #147: these exits, with their directions, fell back to a plain fade (PowerPoint's ids).
|
||||
#[test]
|
||||
fn exit_presets_import_as_their_own_effects() {
|
||||
for (preset, sub, effect, option) in [
|
||||
(3, 10, "blindsOut", "horz"),
|
||||
(4, 32, "boxOut", "out"),
|
||||
(5, 5, "checkerboardOut", "down"),
|
||||
(8, 16, "diamondOut", "in"),
|
||||
(12, 4, "peekOut", "b"),
|
||||
(18, 6, "stripsOut", "rd"),
|
||||
(37, 0, "sinkDown", ""),
|
||||
(49, 0, "spinnerOut", ""),
|
||||
(55, 0, "contract", ""),
|
||||
] {
|
||||
let e = effect_for_preset(AnimClass::Exit, preset).expect("known exit");
|
||||
assert_eq!((e, option_from_subtype(e, sub).as_str()), (effect, option), "preset {preset}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_transition_kind_round_trips() {
|
||||
for (kind, _, _, _, opts) in deckcraft_model::anim::TRANSITIONS {
|
||||
|
||||
@@ -119,6 +119,12 @@ pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, _shapes:
|
||||
let Some(shape) = ctx.lookup(file_id) else { continue };
|
||||
let paragraph = tgt.path(&["txEl", "pRg"]).and_then(|p| p.u32("st"));
|
||||
let effect = maps::effect_for_preset(class, preset).unwrap_or_else(|| maps::fallback_effect(class));
|
||||
// PowerPoint writes Float Down as its own preset (47), not as Float with a direction.
|
||||
let (effect, option) = match (class, preset) {
|
||||
(AnimClass::Entrance, 47) => ("float", "d".to_string()),
|
||||
(AnimClass::Exit, 47) => ("floatOut", "d".to_string()),
|
||||
_ => (effect, maps::option_from_subtype(effect, sub)),
|
||||
};
|
||||
let start = match c.attr("nodeType") {
|
||||
Some("clickEffect") => AnimStart::OnClick,
|
||||
Some("afterEffect") => AnimStart::AfterPrevious,
|
||||
@@ -128,7 +134,7 @@ pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, _shapes:
|
||||
shape,
|
||||
class,
|
||||
effect: effect.to_string(),
|
||||
option: maps::option_from_subtype(effect, sub),
|
||||
option,
|
||||
start,
|
||||
duration_ms: max_dur(c, 0).unwrap_or(0),
|
||||
delay_ms: c.path(&["stCondLst", "cond"]).and_then(|d| ms_attr(d, "delay")).unwrap_or(0),
|
||||
|
||||
@@ -208,14 +208,14 @@ fn filter_for(effect: &str, option: &str) -> &'static str {
|
||||
"randombar(horizontal)"
|
||||
}
|
||||
}
|
||||
"blinds" => {
|
||||
"blinds" | "blindsOut" => {
|
||||
if option == "vert" {
|
||||
"blinds(vertical)"
|
||||
} else {
|
||||
"blinds(horizontal)"
|
||||
}
|
||||
}
|
||||
"box" => {
|
||||
"box" | "boxOut" => {
|
||||
if option == "out" {
|
||||
"box(out)"
|
||||
} else {
|
||||
@@ -229,7 +229,7 @@ fn filter_for(effect: &str, option: &str) -> &'static str {
|
||||
"circle(in)"
|
||||
}
|
||||
}
|
||||
"diamond" => {
|
||||
"diamond" | "diamondOut" => {
|
||||
if option == "out" {
|
||||
"diamond(out)"
|
||||
} else {
|
||||
@@ -246,7 +246,7 @@ fn filter_for(effect: &str, option: &str) -> &'static str {
|
||||
"plusOut" => "plus(out)",
|
||||
_ => "circle(in)",
|
||||
},
|
||||
"checkerboard" => {
|
||||
"checkerboard" | "checkerboardOut" => {
|
||||
if option == "down" {
|
||||
"checkerboard(down)"
|
||||
} else {
|
||||
@@ -261,13 +261,13 @@ fn filter_for(effect: &str, option: &str) -> &'static str {
|
||||
"8" => "wheel(8)",
|
||||
_ => "wheel(1)",
|
||||
},
|
||||
"strips" => match option {
|
||||
"strips" | "stripsOut" => match option {
|
||||
"ru" => "strips(upRight)",
|
||||
"ld" => "strips(downLeft)",
|
||||
"rd" => "strips(downRight)",
|
||||
_ => "strips(upLeft)",
|
||||
},
|
||||
"peek" => match option {
|
||||
"peek" | "peekOut" => match option {
|
||||
"t" => "slide(fromTop)",
|
||||
"l" => "slide(fromLeft)",
|
||||
"r" => "slide(fromRight)",
|
||||
@@ -328,7 +328,12 @@ fn behaviors(w: &mut W, x: &mut Exp, ids: &mut Ids, e: &Eff) {
|
||||
anim_prop(w, ids, e, dur, "ppt_x", "#ppt_x", fx);
|
||||
anim_prop(w, ids, e, dur, "ppt_y", "#ppt_y", fy);
|
||||
}
|
||||
"zoomOut" | "shrinkTurn" => {
|
||||
"floatOut" | "sinkDown" => {
|
||||
let to = if a.effect == "sinkDown" || a.option == "d" { "#ppt_y+.1" } else { "#ppt_y-.1" };
|
||||
anim_prop(w, ids, e, dur, "ppt_y", "#ppt_y", to);
|
||||
anim_effect(w, ids, e, dur, "fade", true);
|
||||
}
|
||||
"zoomOut" | "shrinkTurn" | "spinnerOut" | "contract" => {
|
||||
anim_prop(w, ids, e, dur, "ppt_w", "#ppt_w", "0");
|
||||
anim_prop(w, ids, e, dur, "ppt_h", "#ppt_h", "0");
|
||||
anim_effect(w, ids, e, dur, "fade", true);
|
||||
@@ -425,7 +430,11 @@ fn effect_par(w: &mut W, x: &mut Exp, ids: &mut Ids, e: &Eff, first_in_click: bo
|
||||
};
|
||||
let _ = first_in_click;
|
||||
let preset = crate::maps::preset_for(a);
|
||||
let sub = crate::maps::subtype_from_option(&a.effect, &a.option).or(a.preset_subtype.filter(|_| a.preset_id == Some(preset))).unwrap_or(0);
|
||||
let sub = if preset == 47 {
|
||||
0
|
||||
} else {
|
||||
crate::maps::subtype_from_option(&a.effect, &a.option).or(a.preset_subtype.filter(|_| a.preset_id == Some(preset))).unwrap_or(0)
|
||||
};
|
||||
let repeat = match a.repeat {
|
||||
u32::MAX => Some("indefinite".to_string()),
|
||||
0 | 1 => None,
|
||||
|
||||
@@ -1079,3 +1079,28 @@ fn single_guide_names_follow_the_preset() {
|
||||
assert!(xml.contains(&format!(r#"<a:prstGeom prst="{prst}"><a:avLst><a:gd name="{gd}" fmla="val 20000"/>"#)), "{prst} → {gd}: {xml}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #147: PowerPoint's Float Down is preset 47 (Float Up is 42); it imported as a plain fade.
|
||||
#[test]
|
||||
fn float_down_is_preset_47_both_ways() {
|
||||
let mut p = defaults::new_presentation(None);
|
||||
let mut s = new_slide(&mut p, "Float");
|
||||
let sh = shape(&mut p, "Building", 0.0, 0.0, 100.0, 100.0);
|
||||
let id = sh.id;
|
||||
s.shapes.push(sh);
|
||||
s.animations = vec![
|
||||
Animation { shape: id, class: AnimClass::Entrance, effect: "float".into(), option: "d".into(), ..Default::default() },
|
||||
Animation { shape: id, class: AnimClass::Exit, effect: "floatOut".into(), option: "d".into(), ..Default::default() },
|
||||
Animation { shape: id, class: AnimClass::Exit, effect: "floatOut".into(), option: "u".into(), ..Default::default() },
|
||||
];
|
||||
p.slides.push(Arc::new(s));
|
||||
let bytes = deckcraft_pptx::export(&p).expect("export");
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).expect("zip");
|
||||
let xml = std::io::read_to_string(z.by_name("ppt/slides/slide2.xml").expect("slide")).expect("read");
|
||||
assert!(xml.contains(r#"presetID="47" presetClass="entr" presetSubtype="0""#), "{xml}");
|
||||
assert!(xml.contains(r#"presetID="47" presetClass="exit" presetSubtype="0""#), "{xml}");
|
||||
assert!(xml.contains(r#"presetID="42" presetClass="exit" presetSubtype="0""#), "{xml}");
|
||||
let q = deckcraft_pptx::import(&bytes).expect("import");
|
||||
let got: Vec<(&str, &str)> = q.slides[1].animations.iter().map(|a| (a.effect.as_str(), a.option.as_str())).collect();
|
||||
assert_eq!(got, [("float", "d"), ("floatOut", "d"), ("floatOut", "u")]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user