Working with animation

PresentationML stores animation data in slide timing markup. The core element is <p:timing/>, which contains timing nodes and behavior elements such as <p:anim/>.

The animation model is loosely based on SMIL. Slide animations are time-based effects applied to slide objects or text. Slide transitions are related, but they are stored in <p:transition/> and occur before the slide's own animation timeline.

Animation structure

An animation behavior usually points at a target element on the slide and defines how a value changes over time.

<p:timing>
  <p:tnLst>
    <p:par>
      <p:cTn id="1" dur="indefinite" restart="never"/>
    </p:par>
  </p:tnLst>
</p:timing>

Important animation-related elements include:

PresentationML elementPurpose
<p:timing/>Container for slide timing and animation data
<p:tnLst/>Time node list
<p:anim/>Value animation behavior
<p:cBhvr/>Common behavior settings
<p:tgtEl/>Target element for the behavior
<p:tavLst/>Time-animated value list

Important <p:anim/> attributes include:

AttributeMeaning
byRelative offset from the starting value
fromStarting value
toEnding value
calcmodeInterpolation mode
valueTypeType of the animated property value

In ooxmlsdk, the corresponding generated types live under ooxmlsdk::schemas::p, including Animate, CommonBehavior, TimeAnimateValueList, Timing, and TargetElement.

Rust workflow

Open the target slide part and add or replace a minimal timing tree:

#![allow(unused)]
fn main() {
pub fn add_basic_animation_timing(
  path: &Path,
  slide_index: usize,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
  let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?;
  let presentation_part = document.presentation_part()?;
  let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else {
    return Err("slide index out of range".into());
  };
  let slide_xml = slide_part.data_as_str(&document)?.unwrap_or_default();
  let updated_xml = replace_or_insert_timing(slide_xml, basic_timing_xml())?;
  slide_part.set_data(&mut document, updated_xml.into_bytes())?;

  let mut buffer = Cursor::new(Vec::new());
  document.save(&mut buffer)?;
  Ok(buffer.into_inner())
}
}

Animation markup is sensitive to ids and target references. The example targets shape id 2 from the simple fixture; in a general writer, discover the target shape id from the slide XML and keep timing node ids unique.