docs: add Anime.js animation integration design
This commit is contained in:
parent
cfa5ed2194
commit
8af63d959b
1 changed files with 95 additions and 0 deletions
95
docs/plans/2026-03-08-anime-animation-design.md
Normal file
95
docs/plans/2026-03-08-anime-animation-design.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Anime.js Animation Integration — Design
|
||||
|
||||
**Date:** 2026-03-08
|
||||
**Status:** Approved
|
||||
**Branch:** feat/vue-label-tab
|
||||
|
||||
## Problem
|
||||
|
||||
The current animation system mixes CSS keyframes, CSS transitions, and imperative inline-style bindings across three files. The seams between systems produce:
|
||||
|
||||
- Abrupt ball pickup (instant scale/borderRadius jump)
|
||||
- No spring snap-back on release to no target
|
||||
- Rigid CSS dismissals with no timing control
|
||||
- Bucket grid and badge pop on basic `@keyframes`
|
||||
|
||||
## Decision
|
||||
|
||||
Integrate **Anime.js v4** as a single animation layer. Vue reactive state is unchanged; Anime.js owns all DOM motion imperatively.
|
||||
|
||||
## Architecture
|
||||
|
||||
One new composable, minimal changes to two existing files, CSS cleanup in two files.
|
||||
|
||||
```
|
||||
web/src/composables/useCardAnimation.ts ← NEW
|
||||
web/src/components/EmailCardStack.vue ← modify
|
||||
web/src/views/LabelView.vue ← modify
|
||||
```
|
||||
|
||||
**Data flow:**
|
||||
```
|
||||
pointer events → Vue refs (isHeld, deltaX, deltaY, dismissType)
|
||||
↓ watched by
|
||||
useCardAnimation(cardEl, stackEl, isHeld, ...)
|
||||
↓ imperatively drives
|
||||
Anime.js → DOM transforms
|
||||
```
|
||||
|
||||
`useCardAnimation` is a pure side-effect composable — returns nothing to the template. The `cardStyle` computed in `EmailCardStack.vue` is removed; Anime.js owns the element's transform directly.
|
||||
|
||||
## Animation Surfaces
|
||||
|
||||
### Pickup morph
|
||||
```
|
||||
animate(cardEl, { scale: 0.55, borderRadius: '50%', y: -80 }, { duration: 200, ease: spring(1, 80, 10) })
|
||||
```
|
||||
Replaces the instant CSS transform jump on `onPointerDown`.
|
||||
|
||||
### Drag tracking
|
||||
Raw `cardEl.style.translate` update on `onPointerMove` — no animation, just position. Easing only at boundaries (pickup / release), not during active drag.
|
||||
|
||||
### Snap-back
|
||||
```
|
||||
animate(cardEl, { x: 0, y: 0, scale: 1, borderRadius: '1rem' }, { ease: spring(1, 80, 10) })
|
||||
```
|
||||
Fires on `onPointerUp` when no zone/bucket target was hit.
|
||||
|
||||
### Dismissals (replace CSS `@keyframes`)
|
||||
- **fileAway** — `animate(cardEl, { y: '-120%', scale: 0.85, opacity: 0 }, { duration: 280, ease: 'out(3)' })`
|
||||
- **crumple** — 2-step timeline: shrink + redden → `scale(0)` + rotate
|
||||
- **slideUnder** — `animate(cardEl, { x: '110%', rotate: 5, opacity: 0 }, { duration: 260 })`
|
||||
|
||||
### Bucket grid rise
|
||||
`animate(gridEl, { y: -8, opacity: 0.45 })` on `isHeld` → true; reversed on false. Spring easing.
|
||||
|
||||
### Badge pop
|
||||
`animate(badgeEl, { scale: [0.6, 1], opacity: [0, 1] }, { ease: spring(1.5, 80, 8), duration: 300 })` triggered on badge mount via Vue's `onMounted` lifecycle hook in a `BadgePop` wrapper component or `v-enter-active` transition hook.
|
||||
|
||||
## Constraints
|
||||
|
||||
### Reduced motion
|
||||
`useCardAnimation` checks `motion.rich.value` before firing any Anime.js call. If false, all animations are skipped — instant state changes only. Consistent with existing `useMotion` pattern.
|
||||
|
||||
### Bundle size
|
||||
Anime.js v4 core ~17KB gzipped. Only `animate`, `spring`, and `createTimeline` are imported — Vite ESM tree-shaking keeps footprint minimal. The `draggable` module is not used.
|
||||
|
||||
### Tests
|
||||
Existing `EmailCardStack.test.ts` tests emit behavior, not animation — they remain passing. Anime.js mocked at module level in Vitest via `vi.mock('animejs')` where needed.
|
||||
|
||||
### CSS cleanup
|
||||
Remove from `EmailCardStack.vue` and `LabelView.vue`:
|
||||
- `@keyframes fileAway`, `crumple`, `slideUnder`
|
||||
- `@keyframes badge-pop`
|
||||
- `.dismiss-label`, `.dismiss-skip`, `.dismiss-discard` classes (Anime.js fires on element refs directly)
|
||||
- The `dismissClass` computed in `EmailCardStack.vue`
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `web/package.json` | Add `animejs` dependency |
|
||||
| `web/src/composables/useCardAnimation.ts` | New — all Anime.js animation logic |
|
||||
| `web/src/components/EmailCardStack.vue` | Remove `cardStyle` computed + dismiss classes; call `useCardAnimation` |
|
||||
| `web/src/views/LabelView.vue` | Badge pop + bucket grid rise via Anime.js |
|
||||
| `web/src/assets/avocet.css` | Remove any global animation keyframes if present |
|
||||
Loading…
Reference in a new issue