| 1 | # PPTX Transition Core |
| 2 | |
| 3 | The shared transition core owns page-transition vocabulary, slide-advance |
| 4 | semantics, OOXML mutation, MCE preservation, package timing enablement, and |
| 5 | read-back validation for every PPTX route. |
| 6 | |
| 7 | ## 1. Ownership |
| 8 | |
| 9 | | Concern | Owner | |
| 10 | |---|---| |
| 11 | | Page transition registry | scripts/pptx_transitions.py | |
| 12 | | In-slide object animation | scripts/pptx_animations.py | |
| 13 | | Generated PPTX adapter | svg_to_pptx/pptx_package/builder.py | |
| 14 | | Template Fill adapter | template_fill_pptx/transitions.py | |
| 15 | | Native Enhance adapter | native_enhance_pptx_core.py | |
| 16 | | Public workflow | references/animations.md | |
| 17 | |
| 18 | **Hard rule**: adapters resolve route policy, then call the shared core. They |
| 19 | must not build, replace, or patch a transition with route-local XML or regex. |
| 20 | |
| 21 | --- |
| 22 | |
| 23 | ## 2. Domain Model |
| 24 | |
| 25 | | Layer | Meaning | OOXML | |
| 26 | |---|---|---| |
| 27 | | Enter | How the current slide appears from the preceding slide | Native effect, Effect Options, and duration | |
| 28 | | Advance | How the current slide leaves for the next slide | advClick and advTm | |
| 29 | |
| 30 | Enter policy: |
| 31 | |
| 32 | | Policy | Behavior | |
| 33 | |---|---| |
| 34 | | preserve | Keep the source visual transition, including unknown extensions | |
| 35 | | replace | Write the requested supported effect | |
| 36 | | none | Write no visual effect | |
| 37 | |
| 38 | Advance mode: |
| 39 | |
| 40 | | Mode | Behavior | |
| 41 | |---|---| |
| 42 | | preserve | Keep source advClick and advTm | |
| 43 | | click | Click advance only | |
| 44 | | after | Timed advance only | |
| 45 | | both | Click or timed advance, whichever occurs first | |
| 46 | | narration | Timed advance from audio duration plus padding; click disabled | |
| 47 | |
| 48 | **Hard rule**: enter=none may coexist with a timed advance. The valid result is |
| 49 | a timing-only p:transition with no visual-effect child. |
| 50 | |
| 51 | --- |
| 52 | |
| 53 | ## 3. Compatibility Contract |
| 54 | |
| 55 | The native registry covers the complete current PowerPoint transition gallery: |
| 56 | 12 Subtle effects, 29 Exciting effects, and 7 Dynamic Content effects. New |
| 57 | selection, sidecars, plans, conversion traces, help, and writers use only these |
| 58 | 48 native keys. |
| 59 | |
| 60 | Eight established low-level names remain valid at input boundaries. They |
| 61 | normalize to one native effect plus native `effect_options`; they are not a |
| 62 | second transition registry: |
| 63 | |
| 64 | | Compatibility input | Native request | |
| 65 | |---|---| |
| 66 | | `strips` | `wipe` with `direction: right` | |
| 67 | | `circle` | `shape` with `shape: circle` | |
| 68 | | `diamond` | `shape` with `shape: diamond` | |
| 69 | | `plus` | `shape` with `shape: plus` | |
| 70 | | `newsflash` | `flash` | |
| 71 | | `pull` | `uncover` | |
| 72 | | `wedge` | `clock` with `style: wedge` | |
| 73 | | `wheel` | `clock` with `style: clockwise` | |
| 74 | |
| 75 | Standard PresentationML effects use a direct `p:transition` carrier: |
| 76 | |
| 77 | | Effect | Required primary child and attributes | |
| 78 | |---|---| |
| 79 | | fade | p:fade | |
| 80 | | push | p:push dir=r | |
| 81 | | wipe | p:wipe dir=r | |
| 82 | | split | p:split | |
| 83 | | cut | p:cut | |
| 84 | | random_bars | p:randomBar dir=vert | |
| 85 | | shape | p:circle | |
| 86 | | uncover | p:pull dir=r | |
| 87 | | cover | p:cover dir=r | |
| 88 | | dissolve | p:dissolve | |
| 89 | | checkerboard | p:checker | |
| 90 | | blinds | p:blinds dir=vert | |
| 91 | | clock | p:wheel spokes=1 | |
| 92 | | random | p:random | |
| 93 | | box | p:zoom | |
| 94 | | comb | p:comb | |
| 95 | |
| 96 | Office 2010 effects use a `p14` Choice with a `p:fade` Fallback: |
| 97 | |
| 98 | | Effect | Required primary child and attributes | |
| 99 | |---|---| |
| 100 | | reveal | p14:reveal dir=r | |
| 101 | | flash | p14:flash | |
| 102 | | ripple | p14:ripple | |
| 103 | | honeycomb | p14:honeycomb | |
| 104 | | glitter | p14:glitter | |
| 105 | | vortex | p14:vortex dir=r | |
| 106 | | shred | p14:shred dir=out | |
| 107 | | switch | p14:switch dir=r | |
| 108 | | flip | p14:flip dir=r | |
| 109 | | gallery | p14:gallery dir=r | |
| 110 | | cube | p14:prism dir=r | |
| 111 | | doors | p14:doors dir=vert | |
| 112 | | zoom | p14:warp dir=in | |
| 113 | | pan | p14:pan dir=r | |
| 114 | | ferris_wheel | p14:ferris dir=r | |
| 115 | | conveyor | p14:conveyor dir=r | |
| 116 | | rotate | p14:prism dir=r isContent=1 | |
| 117 | | window | p14:window | |
| 118 | | orbit | p14:prism dir=r isContent=1 isInverted=1 | |
| 119 | | fly_through | p14:flythrough | |
| 120 | |
| 121 | Office 2012 effects use a `p15` Choice with a `p:fade` Fallback: |
| 122 | |
| 123 | | Effect | Required primary child and attributes | |
| 124 | |---|---| |
| 125 | | fall_over | p15:prstTrans prst=fallOver invX=1 | |
| 126 | | drape | p15:prstTrans prst=drape invX=1 | |
| 127 | | curtains | p15:prstTrans prst=curtains | |
| 128 | | wind | p15:prstTrans prst=wind | |
| 129 | | prestige | p15:prstTrans prst=prestige | |
| 130 | | fracture | p15:prstTrans prst=fracture | |
| 131 | | crush | p15:prstTrans prst=crush | |
| 132 | | peel_off | p15:prstTrans prst=peelOff invX=1 | |
| 133 | | page_curl | p15:prstTrans prst=pageCurlSingle invX=1 | |
| 134 | | airplane | p15:prstTrans prst=airplane | |
| 135 | | origami | p15:prstTrans prst=origami | |
| 136 | |
| 137 | `morph` uses `p159:morph option=byObject` in an Office 2015 Choice with a |
| 138 | `p:fade` Fallback. `none` is the explicit no-visual-effect input and therefore |
| 139 | is not a registry entry. |
| 140 | |
| 141 | ### 3.1 Native Effect Options |
| 142 | |
| 143 | Use `effect_options` only with an explicit native `effect`. Omitted options use |
| 144 | the PowerPoint-authored `default` reported by `--describe-transition`: |
| 145 | |
| 146 | | Effect | Supported options | |
| 147 | |---|---| |
| 148 | | `morph` | `morph_by`: `object`, `word`, `character` | |
| 149 | | `fade` | `style`: `smoothly`, `through_black` | |
| 150 | | `push`, `wipe`, `vortex`, `cube`, `pan`, `rotate`, `orbit` | `direction`: `left`, `right`, `up`, `down` | |
| 151 | | `split` | `orientation`: `horizontal`, `vertical`; `direction`: `out`, `in` | |
| 152 | | `reveal` | `direction`: `right`, `left`; `through_black`: boolean | |
| 153 | | `cut` | `through_black`: boolean | |
| 154 | | `random_bars`, `blinds`, `doors` | `orientation`: `vertical`, `horizontal` | |
| 155 | | `checkerboard` | `direction`: `across`, `down` | |
| 156 | | `comb`, `window` | `orientation`: `horizontal`, `vertical` | |
| 157 | | `shape` | `shape`: `circle`, `diamond`, `plus` | |
| 158 | | `uncover`, `cover` | `direction`: `left`, `right`, `up`, `down`, `up_left`, `up_right`, `down_left`, `down_right` | |
| 159 | | `fall_over`, `drape`, `wind`, `peel_off`, `airplane`, `origami` | `direction`: `right`, `left` | |
| 160 | | `page_curl` | `direction`: `right`, `left`; `pages`: `single`, `double` | |
| 161 | | `clock` | `style`: `clockwise`, `counterclockwise`, `wedge` | |
| 162 | | `ripple` | `origin`: `center`, `up_left`, `up_right`, `down_left`, `down_right` | |
| 163 | | `glitter` | `shape`: `diamond`, `hexagon`; `direction`: `right`, `left`, `up`, `down` | |
| 164 | | `shred` | `pattern`: `strips`, `rectangle`; `direction`: `out`, `in` | |
| 165 | | `switch`, `flip`, `gallery`, `ferris_wheel`, `conveyor` | `direction`: `right`, `left` | |
| 166 | | `box`, `zoom` | `direction`: `out`, `in` | |
| 167 | | `fly_through` | `direction`: `in`, `out`; `bounce`: boolean | |
| 168 | | All other native effects | No Effect Options | |
| 169 | |
| 170 | Example: |
| 171 | |
| 172 | ~~~json |
| 173 | { |
| 174 | "transition": { |
| 175 | "effect": "page_curl", |
| 176 | "effect_options": { |
| 177 | "direction": "left", |
| 178 | "pages": "double" |
| 179 | }, |
| 180 | "duration": 0.6 |
| 181 | } |
| 182 | } |
| 183 | ~~~ |
| 184 | |
| 185 | Inspect the exact contract, including compatibility desugaring: |
| 186 | |
| 187 | ~~~bash |
| 188 | python3 skills/ppt-master/scripts/pptx_animations.py --describe-transition page_curl |
| 189 | python3 skills/ppt-master/scripts/pptx_animations.py --describe-transition diamond |
| 190 | ~~~ |
| 191 | |
| 192 | Read-back reports the canonical native effect, its complete effective options, |
| 193 | the raw OOXML child, and raw attributes. This makes option loss a validation |
| 194 | failure rather than a silent downgrade. |
| 195 | |
| 196 | **Hard rule — no downgrade**: |
| 197 | |
| 198 | - Never rename or remove an established effect. |
| 199 | - Never omit its established direction or split attributes. |
| 200 | - Reject an unknown requested effect; never substitute fade. |
| 201 | - Preserve an unknown source effect when the route selects preserve. |
| 202 | - An extension counts as successful only when the primary Choice contains the |
| 203 | requested effect. A fallback alone is not success. |
| 204 | |
| 205 | ### 3.2 Deterministic Morph Identity |
| 206 | |
| 207 | The generated route may add an explicit `slides.<destination>.morph` block to |
| 208 | bind direct-root SVG groups across adjacent slides. The sidecar stable key is |
| 209 | lowered to the same top-level `p:cNvPr@name="!!<key>"` on both final |
| 210 | Slide-local objects. This does not create an Animation Pane row and does not |
| 211 | change either object's numeric shape id. |
| 212 | |
| 213 | The full plan is resolved before any SVG conversion so a source group named by |
| 214 | the following slide remains a stable top-level target. Names are written only |
| 215 | after flat/structured/preserve processing has finished; structured slide-shape |
| 216 | roster expectations are then refreshed. Package read-back requires: |
| 217 | |
| 218 | - the declared source to be the immediately preceding public slide; |
| 219 | - exactly one `!!<key>` object on each side; |
| 220 | - the same OOXML object container type on both sides; |
| 221 | - Morph by object on the destination; and |
| 222 | - no structural target, same-slide name collision, group/key conflict, or |
| 223 | undeclared shared `!!` name on a Morph edge. |
| 224 | |
| 225 | Morph without an explicit pair block retains PowerPoint's automatic matching |
| 226 | behavior. Explicit pairing is generated-route authoring; direct-PPTX routes |
| 227 | continue to preserve existing object names and transition XML. |
| 228 | |
| 229 | --- |
| 230 | |
| 231 | ## 4. Route Mapping |
| 232 | |
| 233 | | Route | Default enter | Default advance | Compatibility note | |
| 234 | |---|---|---|---| |
| 235 | | Generated PPTX CLI | fade, 0.4s | click | auto-advance maps to both | |
| 236 | | Recorded narration | Preserve resolved enter | narration | none remains visually none | |
| 237 | | Template Fill | preserve source | preserve source | explicit effects replace; legacy advance_after maps to both | |
| 238 | | Native Enhance | Confirmed global/per-slide plan effect | Confirmed timing module | With audio off, an enabled global transition or explicit global `none` applies to all pages; with audio on, the scope flag controls non-narrated pages | |
| 239 | |
| 240 | Template Fill changes source transitions only when its CLI or per-slide plan |
| 241 | selects a replacement, removal, or timed advance. Native Enhance uses its |
| 242 | confirmed plan. The public `create_pptx_with_native_svg` Python API retains its |
| 243 | legacy 0.5s default; the generated-deck CLI explicitly passes 0.4s. |
| 244 | |
| 245 | --- |
| 246 | |
| 247 | ## 5. OOXML Rules |
| 248 | |
| 249 | **Slide child order**: |
| 250 | |
| 251 | ~~~text |
| 252 | p:cSld |
| 253 | p:clrMapOvr |
| 254 | p:transition or transition mc:AlternateContent |
| 255 | p:timing |
| 256 | p:extLst |
| 257 | ~~~ |
| 258 | |
| 259 | One slide may contain at most one logical transition carrier: |
| 260 | |
| 261 | - one direct p:transition; or |
| 262 | - one root-level mc:AlternateContent whose Choice/Fallback branches contain |
| 263 | p:transition. |
| 264 | |
| 265 | Mutation rules: |
| 266 | |
| 267 | | Operation | Direct transition | AlternateContent | |
| 268 | |---|---|---| |
| 269 | | preserve | Leave unchanged | Leave wrapper and branches unchanged | |
| 270 | | advance-only | Patch direct attributes | Patch Choice and Fallback identically | |
| 271 | | replace | Replace the direct carrier | Remove the whole wrapper, then write one carrier | |
| 272 | | none | Remove visual carrier; retain timing-only carrier when needed | Remove the whole wrapper; retain timing-only carrier when needed | |
| 273 | |
| 274 | **MCE prefix rule**: Requires and Ignorable values contain textual prefix |
| 275 | names. Serialization must retain bindings for those exact names. Renaming an |
| 276 | effect prefix without updating these attributes corrupts compatibility. |
| 277 | |
| 278 | **Package timing rule**: when a route writes advTm, set |
| 279 | ppt/presProps.xml p:presentationPr/p:showPr useTimings=1. Do not write showPr |
| 280 | into ppt/presentation.xml. |
| 281 | |
| 282 | --- |
| 283 | |
| 284 | ## 6. Validation and Read-Back |
| 285 | |
| 286 | Reject: |
| 287 | |
| 288 | - unknown effect names; |
| 289 | - options without an explicit native effect; |
| 290 | - unknown option fields or values for the selected effect; |
| 291 | - non-finite values, including NaN and Infinity; |
| 292 | - duration less than or equal to zero; |
| 293 | - negative advance or narration padding; |
| 294 | - booleans passed as numeric API values; |
| 295 | - multiple logical transition carriers; |
| 296 | - unresolved MCE Requires or Ignorable prefixes. |
| 297 | - invalid forced-Morph adjacency, identity uniqueness, object type, or |
| 298 | destination effect. |
| 299 | |
| 300 | Read-back must report the canonical native effect and complete effective |
| 301 | options, while keeping the primary Choice child separate from the fallback. It |
| 302 | must also report raw effect attributes, carrier type, duration, click mode, and |
| 303 | automatic advance time. Package validation must run after writing, not only |
| 304 | before mutation. |
| 305 | |
| 306 | Use inline smoke commands and gitignored projects/_smoke_* artifacts. Do not |
| 307 | add a tests directory or test_*.py files. |
| 308 |