95 lines
3.9 KiB
Markdown
95 lines
3.9 KiB
Markdown
# 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 |
|