Skip to content

feat: SVG composition animation — real-time slide preview with diff-based animation#54

Open
ShotaroKataoka wants to merge 24 commits intomainfrom
feat/svg-compose-animation-fixes
Open

feat: SVG composition animation — real-time slide preview with diff-based animation#54
ShotaroKataoka wants to merge 24 commits intomainfrom
feat/svg-compose-animation-fixes

Conversation

@ShotaroKataoka
Copy link
Copy Markdown
Contributor

What

LibreOffice SVG → optimized component split → S3 → WebUI animated preview with diff-based animation.

When the agent edits slides via run_python(save=True), the compose pipeline splits the SVG into individual components and uploads them as JSON to S3. The frontend fetches these, diffs against the previous state, and animates only the changed/new components with agent cursor fly-in, wireframe drag, and typewriter effects.

Demo

Initial load Agent edit
Instant display (no animation) Changed components animate in

How it works

Backend: Compose pipeline (mcp-server/tools/compose.py)

  • extract_optimized_defs() — shared SVG defs (gradients, clip-paths)
  • split_slide_components() — per-slide component extraction with bbox, class, text, svg
  • count_slides() — slide count from SVG structure
  • Background detection: slide-specific SlideBackground → master page fallback
  • S3 keys: epoch-keyed (defs_{epoch}.json, slide_{N}_{epoch}.json) for cache-busting

Backend: Integration (mcp-server/server.py)

  • _export_svg() — extracted from _run_measure for reuse
  • run_python post-processing: SVG export → compose for all slides on save=True
  • Works with or without measure_slides

API (api/index.py)

  • _latest_compose_key() — resolves highest epoch from S3 key listing
  • Returns defsUrl and per-slide composeUrl via CloudFront signed URLs

Frontend

  • AnimatedSlidePreview — fetches compose JSON, builds SVG, diff-based animation
    • prevCompRef null + initialLoad=true → instant (page load)
    • prevCompRef null + initialLoad=false → animate all (first agent write)
    • prevCompRef exists → animate only changed/new components
    • Agent cursor, wireframe overlay, typewriter text reveal
    • prefers-reduced-motion respected
  • SlideCarousel — auto-scroll to first changed slide on compose update
  • useWorkspace — URL stabilization for composeUrl / defsUrl

S3 key structure

decks/{deck_id}/compose/defs_{epoch}.json
decks/{deck_id}/compose/slide_{N}_{epoch}.json

Files changed (12 files, +741 -37)

File Change
mcp-server/tools/compose.py New — SVG split pipeline
mcp-server/server.py Compose integration in run_python, _export_svg extraction
api/index.py _latest_compose_key, defsUrl/composeUrl in response
web-ui/.../AnimatedSlidePreview.tsx New — diff-based animated preview
web-ui/.../SlideCarousel.tsx AnimatedSlidePreview integration, auto-scroll
web-ui/src/hooks/useWorkspace.ts composeUrl/defsUrl stabilization
web-ui/src/services/deckService.ts composeUrl/defsUrl types

…eview

SPEC: 20260413-0806_svg-composition-animation
Phase 1-3: compose module, API integration, WebUI animation

- mcp-server/tools/compose.py: extract_optimized_defs + split_slide_components
  (font strip, PNG→WebP, component split with bbox/class/text metadata)
- server.py: compose after _run_measure (sync, ~300ms for 8 slides)
- api/index.py: defsUrl + composeUrl presigned URLs in deck detail
- AnimatedSlidePreview.tsx: SVG build + diff animation (class+bbox key)
  cursor fly-in → wireframe drag → materialize + typewriter
- SlideCarousel: composeUrl → animated preview, fallback to WebP thumbnail
- DOMPurify sanitization, prefers-reduced-motion, version check fallback
- useWorkspace: presigned URL stabilization for compose/defs URLs
…build cache

- Disable DOMPurify sanitization (strips SVG clip-path, namespaces, fills)
- Fix slidesWithPreview filter to include composeUrl-only slides
- Fix hasSlides to check composeUrl in addition to previewUrl

SPEC: 20260413-0806_svg-composition-animation
… webp separation

- Fix compose.py: use slide-specific SlideBackground over master page
- Epoch-keyed compose S3 keys for proper update detection
- API: _latest_compose_key() to resolve latest epoch
- AnimatedSlidePreview: skip animation on initial load
- Remove WebP generation from run_python (kept in generate_pptx)
- Generate compose for all slides (frontend diff handles animation)
- count_slides() helper in compose.py

SPEC: 20260413-0806_svg-composition-animation
…ose on save-only

- Compose runs on save=True even without measure_slides
- SVG export shared between measure and compose (single conversion)
- Remove WebP from run_python (kept in generate_pptx)
- Auto-scroll to first changed slide before animation
- AnimatedSlidePreview accepts slideId for scroll targeting

SPEC: 20260413-0806_svg-composition-animation
…ration

- Extract _export_svg() from _run_measure for reuse by compose
- Compose generates SVG when measure_slides not present (save-only)
- Add Dockerfile cache-bust for forced image rebuild

SPEC: 20260413-0806_svg-composition-animation
…sition fix

- Add initialLoad prop: page-load compose = instant, session-first = animate all
- Scroll to top of changed slide with 24px padding instead of center

SPEC: 20260413-0806_svg-composition-animation
…a onAnimate callback

SPEC: 20260413-0806_svg-composition-animation
…ewriter fast

SPEC: 20260413-0806_svg-composition-animation
…hange detected

SPEC: 20260413-0806_svg-composition-animation
…p by composeUrl

- Diff uses text+class instead of raw SVG (stable across LibreOffice re-renders)
- Skip re-render when composeUrl base path unchanged (ignore defs-only changes)

SPEC: 20260413-0806_svg-composition-animation
…_1 matched slide_10/11)

SPEC: 20260413-0806_svg-composition-animation
…pose cleanup

- server.py: sourceHash (slide JSON md5) for cross-slide matching
- server.py: 2-level diff (slide-level sourceHash + component-level class+bbox)
- server.py: cleanup old epoch compose files after upload
- AnimatedSlidePreview: simplified to use backend changed flag only
- SlideCarousel: removed initialComposeIds, kept scroll via onAnimate

SPEC: 20260413-0806_svg-composition-animation
…nimation

- server.py: fallback to slot-number diff when sourceHash mismatches
- AnimatedSlidePreview: first render = instant, defer composeUrl changes during animation

SPEC: 20260413-0806_svg-composition-animation
…ction + defer animation

- AnimatedSlidePreview: interval-based check (no useEffect dep on composeUrl),
  skipAnimation prop for instant render on page load
- SlideCarousel: deckReadyRef tracks initial load, detects new slides for scroll
- Remove debug logs

SPEC: 20260413-0806_svg-composition-animation
@ShotaroKataoka ShotaroKataoka force-pushed the feat/svg-compose-animation-fixes branch from 10f0a36 to 06932b6 Compare April 13, 2026 13:51
…t B303

SPEC: 20260413-0806_svg-composition-animation
- AnimatedSlidePreview: reset lastComposeUrlRef when skipAnimation transitions true→false
- generate.py: fallback template blank-dark instead of non-existent default

SPEC: 20260413-0806_svg-composition-animation
…ecision

- Existing deck (slides on mount) → first compose instant, subsequent animate
- New deck (no slides on mount) → all composes animate
- No localStorage, no complex state — just mount-time fact

SPEC: 20260413-0806_svg-composition-animation
- Lint/bias block: 2-space → 4-space indent consistency
- Move 'import re' outside for-loop

SPEC: 20260413-0806_svg-composition-animation
@ShotaroKataoka ShotaroKataoka marked this pull request as ready for review April 13, 2026 23:31
Merged origin/main to incorporate:
- feat: PDF and image support for web_fetch agent tool (#47)
- fix: cdk bootstrap in buildspec (#64)
- build(deps): bump cryptography 46.0.6→46.0.7 (#56)
- build(deps-dev): bump @hono/node-server (#55)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant