Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions .cursor/plans/iterationdelay_preset_refactor_85bcd1fa.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
name: iterationDelay preset refactor
overview: Refactor all 13 ongoing presets so that `delay` (from `TimeAnimationOptions`) is passed through as actual WAAPI start delay, and a new `iterationDelay` preset parameter (on each namedEffect type) takes over the current "bake delay into keyframe offsets" behavior.
todos:
- id: types
content: 'Add `iterationDelay?: number` to all 13 ongoing namedEffect types in types.ts'
status: completed
- id: presets
content: Refactor all 13 ongoing preset files to use namedEffect.iterationDelay for keyframe-offset compression and pass through options.delay as actual start delay
status: completed
- id: tests
content: 'Update all 13 ongoing preset test files: move delay to iterationDelay in options, update assertions, add new tests for actual delay passthrough'
status: completed
- id: docs
content: Update docs in motion-presets/docs, motion/docs, and interact/docs to reflect new iterationDelay parameter and corrected delay semantics
status: completed
- id: rules
content: Update presets-main.md, ongoing-presets.md, and any interact rules that reference ongoing delay behavior
status: completed
isProject: false
---

# Refactor ongoing presets: `delay` to actual start delay, new `iterationDelay` parameter

## Problem

All 13 ongoing presets (except DVD) hijack `TimeAnimationOptions.delay` to simulate iteration delay. They:

1. Compute `timingFactor = duration / (duration + delay)` via `getTimingFactor()`
2. Compress keyframe offsets into `[0, timingFactor]`
3. Return `delay: 0` and `duration: duration + delay`

This means there is **no way** to set an actual WAAPI start delay on these presets.

## Solution

- `**delay`\*\* (from `TimeAnimationOptions`): pass through as actual WAAPI start delay
- `**iterationDelay`\*\* (new param on each `namedEffect` type): takes over the current keyframe-offset-compression behavior

DVD is **excluded** -- it already uses `delay` as actual start delay and does not use `getTimingFactor`.

---

## 1. Type definitions

**File:** `[packages/motion-presets/src/types.ts](packages/motion-presets/src/types.ts)`

Add `iterationDelay?: number` to each of these 13 types:

`Bounce`, `Breathe`, `Cross`, `Flash`, `Flip`, `Fold`, `Jello`, `Poke`, `Pulse`, `Rubber`, `Spin`, `Swing`, `Wiggle`

Example:

```typescript
export type Spin = {
type: 'Spin';
direction?: 'clockwise' | 'counter-clockwise';
iterationDelay?: number; // new
};
```

---

## 2. Preset implementations (13 files)

**Directory:** `packages/motion-presets/src/library/ongoing/`

**Files:** `Bounce.ts`, `Breathe.ts`, `Cross.ts`, `Flash.ts`, `Flip.ts`, `Fold.ts`, `Jello.ts`, `Poke.ts`, `Pulse.ts`, `Rubber.ts`, `Spin.ts`, `Swing.ts`, `Wiggle.ts`

For each file, apply the same transformation pattern. Using **Spin.ts** as the canonical example:

**Before:**

```typescript
const duration = options.duration || 1;
const delay = options.delay || 0;
const timingFactor = getTimingFactor(duration, delay) as number;
// ...
return [
{
...options,
delay: 0,
duration: duration + delay,
keyframes: [{ offset: 0 /* ... */ }, { offset: timingFactor /* ... */ }],
},
];
```

**After:**

```typescript
const duration = options.duration || 1;
const iterationDelay = namedEffect?.iterationDelay || 0;
const timingFactor = getTimingFactor(duration, iterationDelay) as number;
// ...
return [
{
...options,
duration: duration + iterationDelay,
keyframes: [{ offset: 0 /* ... */ }, { offset: timingFactor /* ... */ }],
},
];
```

Key changes per file:

- Read `iterationDelay` from `namedEffect` (already destructured/cast in each preset)
- Remove `const delay = options.delay || 0`
- Remove `delay: 0` from returned object (let `...options` pass through the original `delay`)
- Replace `duration + delay` with `duration + iterationDelay`
- Replace `getTimingFactor(duration, delay)` with `getTimingFactor(duration, iterationDelay)`

### Special cases

**Swing.ts, Fold.ts, Breathe.ts** -- these presets use a **conditional keyframe sequence** when delay > 0 (switches to a decay-style FACTORS_SEQUENCE). Change the condition from `delay` to `iterationDelay`:

```typescript
// Before
const keyframes = delay ? FACTORS_SEQUENCE.map(...) : [/* standard keyframes */];

// After
const keyframes = iterationDelay ? FACTORS_SEQUENCE.map(...) : [/* standard keyframes */];
```

### `getNames` updates

Each file's `getNames()` currently reads `options.delay!`. Change to read from `namedEffect`:

```typescript
// Before
export function getNames(options: TimeAnimationOptions & AnimationExtraOptions) {
const timingFactor = getTimingFactor(options.duration!, options.delay!, true);
return [`motion-spin-${timingFactor}`];
}

// After
export function getNames(options: TimeAnimationOptions & AnimationExtraOptions) {
const iterationDelay = (options.namedEffect as Spin)?.iterationDelay || 0;
const timingFactor = getTimingFactor(options.duration!, iterationDelay, true);
return [`motion-spin-${timingFactor}`];
}
```

(The `namedEffect` type cast varies per preset -- `Spin`, `Bounce`, etc.)

**Fold.ts `getNames` special case** -- currently has an `if (!delay)` branch. Change to `if (!iterationDelay)`.

---

## 3. Tests (13 + 1 files)

**Directory:** `packages/motion-presets/src/library/ongoing/test/`

**Files:** `Bounce.spec.ts`, `Breathe.spec.ts`, `Cross.spec.ts`, `DVD.spec.ts`, `Flash.spec.ts`, `Flip.spec.ts`, `Fold.spec.ts`, `Jello.spec.ts`, `Poke.spec.ts`, `Pulse.spec.ts`, `Rubber.spec.ts`, `Spin.spec.ts`, `Swing.spec.ts`, `Wiggle.spec.ts`

For each test that currently passes `delay` in `TimeAnimationOptions`:

- Move `delay` value into `namedEffect.iterationDelay`
- Remove `delay` from the outer options (or add a separate test with actual `delay`)
- Update assertions: `delay: 0` in expected result should now either be absent or reflect the actual start delay
- `duration: duration + delay` in assertions becomes `duration: duration + iterationDelay`
- Keyframe offset values (e.g., `0.67`, `0.8`) remain the same since the formula is unchanged
- **Add new test cases** that verify `delay` is passed through as actual start delay when both `delay` and `iterationDelay` are provided

**DVD.spec.ts** -- no changes needed (DVD already uses delay correctly).

Also check `packages/motion-presets/src/test/utils.spec.ts` -- `getTimingFactor` tests don't need changes (utility behavior unchanged).

---

## 4. Documentation updates

### motion-presets docs

- `**[packages/motion-presets/docs/presets/ongoing/pulse.md](packages/motion-presets/docs/presets/ongoing/pulse.md)`\*\* -- Add `iterationDelay` parameter documentation and usage examples
- `**[packages/motion-presets/docs/presets/_template.md](packages/motion-presets/docs/presets/_template.md)`\*\* -- Add `iterationDelay` to ongoing preset template if applicable

### motion docs

- `**[packages/motion/docs/categories/ongoing-animations.md](packages/motion/docs/categories/ongoing-animations.md)`\*\* -- If it documents the delay-as-iteration-delay behavior, update to reflect the new `iterationDelay` param
- `**[packages/motion/docs/api/types.md](packages/motion/docs/api/types.md)**` -- If it documents `TimeAnimationOptions.delay` behavior for ongoing presets, update
- `**[packages/motion/docs/core-concepts.md](packages/motion/docs/core-concepts.md)**` -- If it documents delay semantics, verify accuracy

### interact docs

- `**[packages/interact/docs/guides/effects-and-animations.md](packages/interact/docs/guides/effects-and-animations.md)**` -- If it shows ongoing presets with delay, update examples
- `**[packages/interact/docs/api/types.md](packages/interact/docs/api/types.md)**` -- If it documents the ongoing delay pattern, update

Docs that do NOT reference the ongoing delay pattern do not need changes.

---

## 5. Rules updates

### motion-presets rules

- `**[packages/motion-presets/rules/presets/presets-main.md](packages/motion-presets/rules/presets/presets-main.md)**` -- Lines 64-65: `delay` is listed as "animation delay" in the animation options section. This is now correct for ongoing presets (actual start delay). Add `iterationDelay` documentation under a new "Ongoing-specific parameters" subsection or as a note.
- `**[packages/motion-presets/rules/presets/ongoing-presets.md](packages/motion-presets/rules/presets/ongoing-presets.md)**` -- Add `iterationDelay` parameter to each of the 13 ongoing preset parameter tables (not DVD). Include a brief explanation of what it does.

### interact rules

- `**[packages/interact/rules/viewenter.md](packages/interact/rules/viewenter.md)**`, `**[packages/interact/rules/click.md](packages/interact/rules/click.md)**`, `**[packages/interact/rules/hover.md](packages/interact/rules/hover.md)**`, `**[packages/interact/rules/integration.md](packages/interact/rules/integration.md)**`, `**[packages/interact/rules/full-lean.md](packages/interact/rules/full-lean.md)**` -- Review each for ongoing-preset delay examples. These files primarily deal with entrance/interaction triggers and use `delay` in the `TimeAnimationOptions` sense (which is now correct), so they likely need **no changes** unless they show ongoing preset examples with `delay` meaning iteration delay.

---

## 6. Utility function

`**[packages/motion-presets/src/utils.ts](packages/motion-presets/src/utils.ts)`\*\* -- `getTimingFactor(duration, delay)` does not need changes. Its interface is generic (two numbers). Callers will now pass `iterationDelay` instead of `delay`.

---

## Summary of scope

| Area | Files | Changes |
| ------- | ----- | --------------------------------------------- |
| Types | 1 | Add `iterationDelay` to 13 types |
| Presets | 13 | Refactor delay -> iterationDelay logic |
| Tests | 13 | Update test options and assertions |
| Docs | 5-7 | Update delay semantics and add iterationDelay |
| Rules | 2-3 | Add iterationDelay to parameter tables |
| Utility | 0 | No changes |
7 changes: 4 additions & 3 deletions packages/motion-presets/docs/presets/ongoing/pulse.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ export type Pulse = BaseDataItemLike<'Pulse'> & {

### Parameters

| Parameter | Type | Default | Description | Examples |
| ----------- | -------- | ------- | --------------------------- | -------------------------- |
| `intensity` | `number` | `1.0` | Multiplier for scale amount | `0.5`, `1.0`, `1.5`, `2.0` |
| Parameter | Type | Default | Description | Examples |
| ---------------- | -------- | ------- | -------------------------------------------- | -------------------------- |
| `intensity` | `number` | `1.0` | Multiplier for scale amount | `0.5`, `1.0`, `1.5`, `2.0` |
| `iterationDelay` | `number` | `0` | Idle time (ms) appended after each iteration | `0`, `500`, `1000` |

### Intensity Control

Expand Down
22 changes: 17 additions & 5 deletions packages/motion-presets/rules/presets/ongoing-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ Visual: Element bounces up and down with a natural multi-step curve, like a ball
Parameters:

- `intensity`: number — 0 to 1, maps to bounce height factor 1–3 (default: `0`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Bounce', intensity: 0.5 }
{ type: 'Bounce', intensity: 0.5, iterationDelay: 500 }
```

---
Expand All @@ -50,6 +51,7 @@ Parameters:
- `direction`: 'vertical' | 'horizontal' | 'center' (default: `'vertical'`)
- `distance`: UnitLengthPercentage — movement distance (default: `{ value: 25, unit: 'px' }`)
- `perspective`: number — 3D perspective for center direction (default: `800`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Breathe', direction: 'horizontal', distance: { value: 15, type: 'px' } }
Expand All @@ -64,6 +66,7 @@ Visual: Element moves across the screen from side to side, horizontally or verti
Parameters:

- `direction`: EffectEightDirections — one of 'left', 'right', 'top', 'bottom', 'top-left', 'top-right', 'bottom-left', 'bottom-right' (default: `'right'`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Cross', direction: 'top-left' }
Expand All @@ -89,12 +92,12 @@ Parameters: None.

Visual: Element blinks by rapidly cycling opacity from visible to invisible and back.

Parameters: None.
Parameters:

- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{
type: 'Flash';
}
{ type: 'Flash', iterationDelay: 300 }
```

---
Expand All @@ -107,6 +110,7 @@ Parameters:

- `direction`: 'vertical' | 'horizontal' (default: `'horizontal'`)
- `perspective`: number — 3D perspective in px (default: `800`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Flip', direction: 'vertical' }
Expand All @@ -122,6 +126,7 @@ Parameters:

- `direction`: 'top' | 'right' | 'bottom' | 'left' (default: `'top'`)
- `angle`: number — fold angle in degrees (default: `15`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Fold', direction: 'right', angle: 30 }
Expand All @@ -136,6 +141,7 @@ Visual: Element wobbles with a skew-based jello-like deformation.
Parameters:

- `intensity`: number — 0 to 1, maps to skew factor 1–4 (default: `0.25`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Jello', intensity: 0.5 }
Expand All @@ -151,6 +157,7 @@ Parameters:

- `direction`: 'top' | 'right' | 'bottom' | 'left' (default: `'right'`)
- `intensity`: number — 0 to 1, maps to poke strength factor 1–4 (default: `0.5`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Poke', direction: 'left', intensity: 0.8 }
Expand All @@ -165,6 +172,7 @@ Visual: Element pulses by subtly scaling up and down.
Parameters:

- `intensity`: number — 0 to 1, adjusts the scale range (default: `0`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Pulse', intensity: 0.5 }
Expand All @@ -179,6 +187,7 @@ Visual: Element stretches non-uniformly on X and Y axes, creating a rubber-band
Parameters:

- `intensity`: number — 0 to 1, adjusts the stretch amplitude (default: `0.5`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Rubber', intensity: 0.8 }
Expand All @@ -193,6 +202,7 @@ Visual: Element rotates continuously around its center.
Parameters:

- `direction`: 'clockwise' | 'counter-clockwise' (default: `'clockwise'`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Spin', direction: 'counter-clockwise' }
Expand All @@ -208,6 +218,7 @@ Parameters:

- `direction`: 'top' | 'right' | 'bottom' | 'left' — swing pivot edge (default: `'top'`)
- `swing`: number — maximum swing angle in degrees (default: `20`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Swing', swing: 40, direction: 'right' }
Expand All @@ -222,6 +233,7 @@ Visual: Element shakes with combined rotation and vertical translation.
Parameters:

- `intensity`: number — 0 to 1, maps to wiggle strength factor 1–4 (default: `0.5`)
- `iterationDelay`: number — idle time in ms after each iteration cycle (default: `0`)

```typescript
{ type: 'Wiggle', intensity: 0.8 }
Expand Down
6 changes: 5 additions & 1 deletion packages/motion-presets/rules/presets/presets-main.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,17 @@ In the simplest case, a trigger and its effect are bound to the same element. Ho
These are set on the effect configuration level, not on the preset itself:

- `duration`: Animation duration in ms (entrance, ongoing)
- `delay`: Animation delay in ms (entrance, ongoing)
- `delay`: Animation start delay in ms (entrance, ongoing)
- `easing`: Easing function
- `iterations`: Number of iterations
- `alternate`: Alternate direction on each iteration
- `fill`: Animation fill mode
- `reversed`: Reverse the animation

**Ongoing-specific preset parameter:**

- `iterationDelay`: Idle time in ms appended after each active iteration cycle. Available on all ongoing presets except DVD. This compresses the active animation keyframes into a fraction of the total iteration duration, creating a pause between repetitions. Set on the `namedEffect`, not on the animation options.

**Scroll-specific animation options:**

- `rangeStart` / `rangeEnd`: `RangeOffset` controlling when the scroll animation starts/ends
Expand Down
Loading
Loading