| 1 | # PPTX Animation Core |
| 2 | |
| 3 | The shared animation core owns the object-effect vocabulary, trigger |
| 4 | semantics, OOXML timing writer, semantic read-back, and package validation for |
| 5 | PowerPoint OOXML. Per-element animation remains opt-in: generated PPTX export |
| 6 | defaults to `none`, exactly as before this validation upgrade. |
| 7 | |
| 8 | ## 1. Ownership |
| 9 | |
| 10 | | Concern | Owner | |
| 11 | |---|---| |
| 12 | | Effect registry, timing writer, and read-back | `scripts/pptx_animations.py` | |
| 13 | | Sidecar parsing and SVG target discovery | `svg_to_pptx/animation_config.py` | |
| 14 | | SVG group-to-shape mapping | `svg_to_pptx/drawingml/converter.py` | |
| 15 | | Generated PPTX resolution and validation | `svg_to_pptx/pptx_package/builder.py` | |
| 16 | | Narration timing merge | `svg_to_pptx/pptx_package/narration.py` | |
| 17 | | Public authoring contract | `references/animations.md` | |
| 18 | | Customization stage | `workflows/stages/customize-animations.md` | |
| 19 | |
| 20 | **Hard rule**: only the generated SVG-to-PPTX route writes object |
| 21 | animations. Direct-PPTX routes preserve source animations and run structural |
| 22 | package validation; they do not resolve or author animation effects. |
| 23 | |
| 24 | --- |
| 25 | |
| 26 | ## 2. Domain Model |
| 27 | |
| 28 | `groups.<id>` accepts either one backward-compatible effect object or one |
| 29 | non-empty `effects[]` array; the forms are exclusive and every array row names |
| 30 | `effect`. Both expand into the same row model, so repeated shape targets are |
| 31 | valid. Legacy rows also accept `trigger`; omitted row settings inherit the |
| 32 | resolved slide animation. |
| 33 | |
| 34 | One resolved row contains these fields: |
| 35 | |
| 36 | | Field | Meaning | |
| 37 | |---|---| |
| 38 | | Target | Positive PowerPoint shape id written to `p:spTgt@spid` | |
| 39 | | Effect | One canonical PowerPoint-authored preset class / id / subtype / behavior-tree signature | |
| 40 | | Trigger | Row-specific `on-click`, `with-previous`, or `after-previous`; omitted values inherit the resolved slide Start mode | |
| 41 | | Trigger shape | Optional different top-level group; maps to PowerPoint `On Click of` | |
| 42 | | Duration | Finite positive schedule duration; scalable native behavior trees preserve their internal timing ratios | |
| 43 | | Delay | Finite non-negative row offset; shape-trigger rows use it as `TriggerDelayTime` | |
| 44 | | Order | Positive integer sidecar order; ties retain stable SVG group order, then `effects[]` index | |
| 45 | | Effect options | Effect-specific `direction`, `amount`, `color`, `font_name` (one installed PowerPoint face, required for Change Font; not a CSS list), `relative`, or `size` values from PowerPoint `EffectParameters` | |
| 46 | | Timing options | Repeat count/span, auto-reverse, rewind, accelerate/decelerate, bounce-end ratio, and restart policy | |
| 47 | | Completion | Optional dim/hide behavior and packaged `.m4a`/`.mp3`/`.wav` sound | |
| 48 | |
| 49 | Modes resolve before XML writing: |
| 50 | |
| 51 | | Mode | Resolution | |
| 52 | |---|---| |
| 53 | | `auto` | Generic entrance only: deterministic semantic mapping from the SVG group id | |
| 54 | | `mixed` | Generic entrance only: deterministic cycle over canonical PowerPoint entrance presets | |
| 55 | | `random` | Generic entrance only: stable seeded choice from the same canonical entrance pool | |
| 56 | | `none` | No object-animation sequence | |
| 57 | |
| 58 | The same effective input produces the same `random` choices. When enabled, |
| 59 | `--conversion-trace` records each resolved row and effect, so a generated deck |
| 60 | can be audited without replaying the resolver. |
| 61 | |
| 62 | `animation_config.py scaffold` is neutral: object defaults are `none`, and |
| 63 | empty `{}` group placeholders inherit no motion until populated. |
| 64 | |
| 65 | --- |
| 66 | |
| 67 | ## 3. Canonical Registry and Compatibility Inputs |
| 68 | |
| 69 | The canonical registry contains 203 PowerPoint-authored presets: |
| 70 | |
| 71 | | Category | Key prefix | Count | Example | |
| 72 | |---|---|---:|---| |
| 73 | | Entrance | `entrance_*` | 53 | `entrance_bounce` | |
| 74 | | Emphasis | `emphasis_*` | 33 | `emphasis_spin` | |
| 75 | | Motion path | `path_*` | 64 | `path_circle` | |
| 76 | | Exit | `exit_*` | 53 | `exit_faded_zoom` | |
| 77 | |
| 78 | The 29 established short names remain valid only as compatibility inputs. |
| 79 | Normalization resolves them to canonical PowerPoint-authored presets before |
| 80 | selection, XML writing, read-back, tracing, or validation. |
| 81 | |
| 82 | | Compatibility input | Canonical preset | |
| 83 | |---|---| |
| 84 | | `appear`, `cut` | `entrance_appear` | |
| 85 | | `fade` | `entrance_fade` | |
| 86 | | `fly`, `fly_left`, `fly_right`, `fly_top` | `entrance_fly` | |
| 87 | | `zoom` | `entrance_zoom` | |
| 88 | | `wipe`, `wipe_left`, `wipe_right`, `wipe_up`, `wipe_down` | `entrance_wipe` | |
| 89 | | `split`, `blinds`, `checkerboard`, `dissolve`, `random_bars`, `peek` | matching `entrance_*` preset | |
| 90 | | `wheel`, `box`, `circle`, `diamond`, `plus`, `strips`, `wedge`, `stretch`, `expand`, `swivel` | matching `entrance_*` preset | |
| 91 | |
| 92 | `cut` maps to `entrance_appear` because current PowerPoint exposes no separate |
| 93 | Cut object-animation preset. Old Fly/Wipe names desugar to the canonical effect |
| 94 | plus `effect_options.direction`; legacy `wheel` desugars to |
| 95 | `entrance_wheel` plus `amount: 4`. New output never writes those aliases. |
| 96 | |
| 97 | Together with the 29 accepted compatibility names, the public input surface |
| 98 | contains 232 keys. New selections, generated sidecars, conversion traces, |
| 99 | writers, and documentation examples use canonical keys; short names exist only |
| 100 | at compatibility input boundaries. |
| 101 | |
| 102 | The shipped `pptx_animation_presets.json` contains the PowerPoint-authored |
| 103 | `p:cTn` row for every native effect. Complex effects use combinations of |
| 104 | `p:set`, `p:anim`, `p:animClr`, `p:animEffect`, `p:animMotion`, `p:animRot`, |
| 105 | and `p:animScale`; reducing them to one filter would silently change the |
| 106 | effect. `pptx_animations.py --list` prints the full categorized public |
| 107 | registry; `pptx_animations.py --describe <effect>` prints that effect's exact |
| 108 | option values and shared timing/completion contract. |
| 109 | |
| 110 | Native presets map to the object-capable `MsoAnimEffect` values. Media play, |
| 111 | pause, stop, and play-from-bookmark are excluded because they require a |
| 112 | media/bookmark target rather than an SVG-derived shape. Exit effects use the |
| 113 | same entrance-capable `MsoAnimEffect` identity with PowerPoint's exit flag and |
| 114 | serialize as `presetClass="exit"`. |
| 115 | |
| 116 | Paragraph/text-range build controls are likewise outside this writer: generated |
| 117 | targets are top-level SVG groups, not paragraph ranges. For that target model, |
| 118 | the public contract covers all PowerPoint effect parameters, timing modifiers, |
| 119 | completion controls, sound, and object-trigger linkage; Speed and smooth |
| 120 | start/end remain derived rather than duplicated. |
| 121 | |
| 122 | **Hard rule — no downgrade**: |
| 123 | |
| 124 | - Keep all 29 established short names accepted as compatibility inputs. |
| 125 | - Reject an unknown effect, mode, or trigger; never substitute another value. |
| 126 | - Reject booleans and non-finite, out-of-range, or invalidly ordered values. |
| 127 | - Reject a missing slide, missing group, or structural-layer target. |
| 128 | - Keep the generated-route default at `none`; validation does not opt a deck in. |
| 129 | |
| 130 | --- |
| 131 | |
| 132 | ## 4. Target Resolution |
| 133 | |
| 134 | Generated object animation targets top-level SVG content groups. Explicit SVG |
| 135 | semantics are authoritative; the group-id chrome heuristic is only a fallback |
| 136 | for marker-free legacy SVGs. |
| 137 | |
| 138 | | Target state | Behavior | |
| 139 | |---|---| |
| 140 | | Ordinary content group | Animatable; a legacy block resolves one row and `effects[]` may resolve several rows against the same final shape | |
| 141 | | Legacy chrome-like id | Skipped unless explicitly named in `animations.json` | |
| 142 | | Explicit sidecar group override | May override only the legacy chrome-name heuristic | |
| 143 | | `data-pptx-layer` or explicit static role/placeholder | Structural and never animatable | |
| 144 | |
| 145 | An explicit sidecar entry cannot turn a Master/Layout/Slide structural layer or |
| 146 | an explicitly marked static page-frame role/placeholder into an animation |
| 147 | target. This boundary preserves PPTX structure even when a legacy id resembles |
| 148 | content. |
| 149 | |
| 150 | --- |
| 151 | |
| 152 | ## 5. OOXML Rules |
| 153 | |
| 154 | The writer emits animation timing after `p:transition` and before `p:extLst`. |
| 155 | Normally this is one root `p:timing`; nonzero `bounce_end` uses PowerPoint's |
| 156 | native `mc:AlternateContent` with a p14 Choice and non-bounce Fallback. Each |
| 157 | branch contains a `tmRoot`, a `mainSeq` when ordinary Start rows exist, one |
| 158 | `interactiveSeq` per trigger-shape row, unique branch-local `p:cTn@id` values, |
| 159 | and same-slide `p:spTgt` references. |
| 160 | |
| 161 | Trigger mapping: |
| 162 | |
| 163 | | Public trigger | Object row `p:cTn@nodeType` | |
| 164 | |---|---| |
| 165 | | `on-click` | `clickEffect` | |
| 166 | | `with-previous` | `withEffect` | |
| 167 | | `after-previous` | `afterEffect` | |
| 168 | |
| 169 | A row-level `trigger_shape` resolves to a different shape id and writes |
| 170 | PowerPoint's native `interactiveSeq` with `onClick` shape conditions. Its row |
| 171 | remains `clickEffect`; row `delay` becomes `TriggerDelayTime`. Ordinary rows |
| 172 | remain in `mainSeq` and keep the slide Start mode. |
| 173 | |
| 174 | Row `trigger` overrides slide Start in both forms. `trigger_shape` implies |
| 175 | `on-click` and conflicts with an explicit non-`on-click` Start. Repeated |
| 176 | `p:spTgt@spid` values are valid distinct Animation Pane rows. Ordinary rows |
| 177 | retain page-wide `order`; trigger-shape rows retain their relative order in |
| 178 | separate `interactiveSeq` branches and do not interleave with `mainSeq`. |
| 179 | |
| 180 | The writer does not emit `p:bldP` for grouped content or pictures. Microsoft |
| 181 | defines `p:bldP@spid` for a text-bearing `p:sp`; using it for `p:grpSp` or |
| 182 | `p:pic` creates an invalid build reference. Package validation still accepts a |
| 183 | valid source `p:bldP` that targets a text-bearing shape. |
| 184 | |
| 185 | Direct-PPTX preserve mode also tolerates an unchanged legacy `p:bldP` that |
| 186 | targets an existing group/picture. Earlier PPT Master exports wrote this form; |
| 187 | the direct routes fingerprint and preserve it instead of blocking those decks. |
| 188 | New generated output never writes it, and generated-package validation remains |
| 189 | strict. |
| 190 | |
| 191 | `entrance_appear` is the visibility-flip exception: its `p:set` behavior is |
| 192 | always 1ms. The configured positive duration remains the row's scheduling span |
| 193 | used when computing the next `after-previous` offset; read-back verifies the |
| 194 | 1ms behavior and the resulting timeline offset separately. The compatibility |
| 195 | inputs `appear` and `cut` normalize to this canonical preset. |
| 196 | |
| 197 | Other native presets with a |
| 198 | finite duration scale every finite behavior duration and start delay |
| 199 | proportionally, preserving multi-step timing such as bounce and teeter. |
| 200 | PowerPoint-authored instantaneous emphasis presets keep their `indefinite` |
| 201 | behavior duration; their configured duration remains the scheduling span for |
| 202 | the next `after-previous` row. |
| 203 | |
| 204 | --- |
| 205 | |
| 206 | ## 6. Validation and Read-Back |
| 207 | |
| 208 | Before export, `animation_config.py validate` uses the writer's effect-behavior |
| 209 | test for `bounce_end` and resolves declared sound paths against the project |
| 210 | root. Missing paths, non-files, and unsupported audio extensions fail this |
| 211 | project-level preflight; field-only validation remains filesystem-independent. |
| 212 | |
| 213 | Generated export reads every slide back before packaging and compares each |
| 214 | requested row with the serialized result: |
| 215 | |
| 216 | - row count and row order, including stable repeated-target rows; |
| 217 | - trigger, optional trigger shape, and shape target; |
| 218 | - resolved effect key, preset class, filter, `presetID`, and `presetSubtype`; |
| 219 | - exact effect options, repeat/reverse/rewind/acceleration/bounce/restart |
| 220 | semantics, completion behavior, sound relationship, and playback span; |
| 221 | - native behavior-tree signature, serialized behavior duration, and computed |
| 222 | timeline offset (`entrance_appear` and instantaneous native presets use the |
| 223 | exceptions above). |
| 224 | |
| 225 | After packaging, validation scans every slide part for root timing placement, |
| 226 | duplicate or malformed `p:cTn` ids, missing `p:spTgt` shapes, invalid build |
| 227 | targets, and unsupported generated effect tuples. A mismatch fails export |
| 228 | before the requested output file replaces an existing deck. |
| 229 | |
| 230 | Narration injection parses and merges the slide DOM. It adds audio timing under |
| 231 | the existing `tmRoot`, allocates fresh ids, and preserves object animation. |
| 232 | For bounce timing it updates both p14 Choice and Fallback; unsupported nested |
| 233 | timing containers still fail safely instead of being duplicated. |
| 234 | |
| 235 | Direct-PPTX routes run the structural package validator with generated-effect |
| 236 | enforcement disabled. This permits preservation of source/extension effects and |
| 237 | legacy group build rows while still rejecting corrupt timing IDs or missing |
| 238 | targets. Template fill and native enhancement fingerprint the source |
| 239 | object-animation tree before and after their allowed edits; any semantic change |
| 240 | fails. These routes have no object-animation write ownership. |
| 241 | |
| 242 | The conversion trace is also the authoritative input for downstream video |
| 243 | motion. `video_motion_plan.py` preserves the resolved effect/options, direction, |
| 244 | row order, base and repeat-aware playback duration, absolute offset, object |
| 245 | bounds, and narration-derived slide advance while adding only renderer-specific enhancement parameters. Video |
| 246 | renderers must not bypass this read-back result and infer motion from sidecar |
| 247 | delay values alone. |
| 248 | |
| 249 | --- |
| 250 | |
| 251 | ## 7. Compatibility Scope |
| 252 | |
| 253 | The compatibility contract covers PowerPoint OOXML and PowerPoint read-back. |
| 254 | Other presentation applications may interpret timing trees or filter values |
| 255 | differently; the exporter does not make an unconditional Keynote guarantee. |
| 256 | |
| 257 | Official references: |
| 258 | |
| 259 | - [Microsoft `MsoAnimEffect` enumeration](https://learn.microsoft.com/en-us/office/vba/api/powerpoint.msoanimeffect) |
| 260 | - [Microsoft `Sequence.AddEffect`](https://learn.microsoft.com/en-us/office/vba/api/powerpoint.sequence.addeffect) |
| 261 | - [Microsoft `Effect.Exit`](https://learn.microsoft.com/en-us/office/vba/api/powerpoint.effect.exit) |
| 262 | - [Microsoft animation-filter implementation notes](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/a96dab70-2e72-4319-928d-0eb4b275ce58) |
| 263 | - [Microsoft `p:bldP` implementation restrictions](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/40d17b6d-30c0-4c10-b042-b2597824a820) |
| 264 | - [Open XML SDK time-node values](https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.presentation.timenodevalues?view=openxml-3.0.1) |
| 265 | - [Open XML SDK shape target](https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.presentation.shapetarget?view=openxml-3.0.1) |
| 266 | |
| 267 | See [`pptx-transitions.md`](./pptx-transitions.md) for the symmetric page-motion |
| 268 | core, MCE handling, and slide-advance contract. |
| 269 | See [`video-motion-plan.md`](./video-motion-plan.md) for the downstream |
| 270 | animation-to-video contract. |
| 271 |