{"id":35788376,"url":"https://github.com/codehz/auto-transition","last_synced_at":"2026-04-13T07:27:37.459Z","repository":{"id":330239497,"uuid":"1122068845","full_name":"codehz/auto-transition","owner":"codehz","description":null,"archived":false,"fork":false,"pushed_at":"2025-12-24T03:57:02.000Z","size":43,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-25T17:06:37.321Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codehz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-24T03:53:51.000Z","updated_at":"2025-12-24T05:25:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/codehz/auto-transition","commit_stats":null,"previous_names":["codehz/auto-transition"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/codehz/auto-transition","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehz%2Fauto-transition","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehz%2Fauto-transition/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehz%2Fauto-transition/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehz%2Fauto-transition/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codehz","download_url":"https://codeload.github.com/codehz/auto-transition/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehz%2Fauto-transition/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28400238,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-07T07:25:28.668Z","updated_at":"2026-04-13T07:27:37.435Z","avatar_url":"https://github.com/codehz.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @codehz/auto-transition\n\n一个轻量级的 React 组件库，旨在为容器内的子元素提供自动的**进入 (Enter)**、**退出 (Exit)** 和**移动 (Move)** 动画。\n\n它通过拦截底层的 DOM 操作（如 `appendChild`、`removeChild`）来实现动画，无需开发者手动管理复杂的动画状态。\n\n## 主要功能\n\n- **全自动动画**：自动识别子元素的添加、删除和位置变化并应用动画。\n- **高性能**：基于原生 Web Animations API 实现，确保流畅的 160fps 体验。\n- **布局感知**：自动计算元素在容器内的相对位置，支持平滑的位移和缩放过渡。\n- **高度可定制**：支持通过插件系统自定义动画效果，插件可直接读取预计算的几何上下文。\n- **无侵入性**：支持通过 `Slot` 将行为附着到现有布局节点。\n\n## 安装\n\n该项目依赖于 React 19+。\n\n```bash\nnpm install @codehz/auto-transition\n# 或者使用 bun\nbun add @codehz/auto-transition\n```\n\n## 快速上手\n\n只需将需要动画的列表或元素包裹在 `AutoTransition` 中即可：\n\n```tsx\nimport { AutoTransition } from \"@codehz/auto-transition\";\nimport { useState } from \"react\";\n\nfunction ListExample() {\n  const [items, setItems] = useState([1, 2, 3]);\n\n  return (\n    \u003cAutoTransition as=\"ul\" className=\"grid gap-2\"\u003e\n      {items.map((id) =\u003e (\n        \u003cli key={id} onClick={() =\u003e setItems(items.filter((i) =\u003e i !== id))}\u003e\n          项目 {id} (点击删除)\n        \u003c/li\u003e\n      ))}\n      \u003cbutton onClick={() =\u003e setItems([...items, Date.now()])}\u003e添加项目\u003c/button\u003e\n    \u003c/AutoTransition\u003e\n  );\n}\n```\n\n## API 参考\n\n### `AutoTransition` 组件 Props\n\n| 属性         | 类型               | 默认值       | 说明                                                                                            |\n| :----------- | :----------------- | :----------- | :---------------------------------------------------------------------------------------------- |\n| `as`         | `ElementType`      | `Slot`       | 容器渲染成的 HTML 标签或组件。省略时使用 `@radix-ui/react-slot`。                               |\n| `transition` | `TransitionPlugin` | 内置默认动画 | 用于自定义进入、退出和移动动画；每个 phase 都可以单独使用函数或 effect 组合式定义，也支持混搭。 |\n| `patch`      | `boolean`          | `false`      | 是否启用内置 `Activity` 补丁，拦截子节点被强制 `display: none` 的行为。                         |\n| `children`   | `ReactNode`        | -            | 需要应用动画的子元素。                                                                          |\n| `ref`        | `Ref\u003cHTMLElement\u003e` | -            | 转发给容器 DOM 元素的引用。                                                                     |\n\n### 推荐写法：`TransitionPlugin`\n\n新版推荐把动画拆成可组合的 effect，再用 phase 工厂拼成 enter / exit / move。这样 `fade`、`scale`、`blur`、`slide`、FLIP 补偿都可以自由组合，不需要继续堆 `fadeScaleBlurSlide...` 这类预设名。\n\n```tsx\nimport {\n  AutoTransition,\n  defineTransition,\n  transitionEffects,\n  transitionPhases,\n  type TransitionPlugin,\n} from \"@codehz/auto-transition\";\n\nconst floatingActionsTransition = defineTransition({\n  enter: transitionPhases.enter(\n    transitionEffects.common.fade({ from: 0, to: 1 }),\n    transitionEffects.common.scale({ from: 0.96, to: 1 }),\n    transitionEffects.common.blur({ from: \"8px\", to: \"0px\" }),\n    transitionEffects.enter.slide({ axis: \"y\", distance: 10 }),\n    { duration: 220, easing: \"ease-out\" },\n  ),\n  exit({ element, rect, anchorDelta }) {\n    const translate =\n      anchorDelta.x === 0 \u0026\u0026 anchorDelta.y === 0 ? \"\" : `translate(${anchorDelta.x}px, ${anchorDelta.y}px) `;\n\n    return element.animate(\n      [\n        {\n          position: \"absolute\",\n          top: `${rect.y}px`,\n          left: `${rect.x}px`,\n          width: `${rect.width}px`,\n          height: `${rect.height}px`,\n          margin: \"0\",\n          opacity: 1,\n          transform: `${translate}scale(1)`,\n        },\n        {\n          position: \"absolute\",\n          top: `${rect.y}px`,\n          left: `${rect.x}px`,\n          width: `${rect.width}px`,\n          height: `${rect.height}px`,\n          margin: \"0\",\n          opacity: 0,\n          transform: `${translate}scale(0.96)`,\n        },\n      ],\n      { duration: 200, easing: \"ease-in\" },\n    );\n  },\n  move: transitionPhases.move(transitionEffects.move.flipTranslate(), transitionEffects.move.flipScale(), {\n    duration: 320,\n    easing: \"cubic-bezier(0.22, 1, 0.36, 1)\",\n  }),\n} satisfies TransitionPlugin);\n\nfunction Example({ children }: { children: React.ReactNode }) {\n  return \u003cAutoTransition transition={floatingActionsTransition}\u003e{children}\u003c/AutoTransition\u003e;\n}\n```\n\n可组合 API 分成两层：\n\n- `transitionEffects.common.fade(options?)`\n- `transitionEffects.common.scale(options?)`\n- `transitionEffects.common.blur(options?)`\n- `transitionEffects.enter.slide(options?)`\n- `transitionEffects.exit.anchorTranslate(options?)`\n- `transitionEffects.move.flipTranslate(options?)`\n- `transitionEffects.move.flipScale(options?)`\n- `transitionPhases.enter(...effects, options?)`\n- `transitionPhases.exit.absolute(...effects, options?)`\n- `transitionPhases.move(...effects, options?)`\n\n其中：\n\n- `transitionEffects.common.fade()` 负责 opacity 时间线，值会相对元素当前 opacity 缩放。\n- `transitionEffects.common.scale()` 负责结构化的 `transform.scale`，会自动输出固定顺序的 transform 字符串。\n- `transitionEffects.common.blur()` 首版封装 `filter: blur(...)`，后续可以自然扩到更多 filter 片段。\n- `transitionEffects.enter.slide()` 负责进入时的位移片段。\n- `transitionEffects.exit.anchorTranslate()` 负责退出时的 `anchorDelta` 补偿，也可附带额外滑出距离。\n- `transitionEffects.move.flipTranslate()` / `flipScale()` 把 FLIP 的位移补偿和缩放补偿拆成两个独立 effect。\n- `transitionPhases.exit.absolute()` 会自动注入退出时需要的绝对定位 keyframe 基座，effect 只关注视觉属性本身。\n\neffect 合并规则：\n\n- 所有效果的 `offset` 会取并集并按升序输出。\n- 中间缺失值会沿用最近一次已知值；起点和终点缺失时，会分别用首个和末个已知值补齐。\n- `transform` 固定按 `translate -\u003e scale` 合成。\n- `filter` 当前固定按 `blur()` 合成。\n- 两个 effect 不能同时控制同一个原子字段，例如 `opacity`、`transform.scale`、`filter.blur`；冲突会直接抛错。\n\n`transitionPresets` 仍然保留，作为新组合 API 之上的薄封装，适合快速使用常见动画：\n\n- `transitionPresets.enter.fadeScale(options?)`\n- `transitionPresets.enter.fade(options?)`\n- `transitionPresets.enter.slideFade(options?)`\n- `transitionPresets.enter.pop(options?)`\n- `transitionPresets.exit.absoluteFadeScale(options?)`\n- `transitionPresets.exit.absoluteFade(options?)`\n- `transitionPresets.exit.absoluteSlideFade(options?)`\n- `transitionPresets.exit.absoluteShrink(options?)`\n- `transitionPresets.move.flip(options?)`\n- `transitionPresets.move.translate(options?)`\n- `transitionPresets.move.smooth(options?)`\n\n其中：\n\n- `enter.fade()` 只做透明度过渡，适合不希望缩放或位移的内容。\n- 默认内置 `enter` / `exit` 分别使用 `transitionPresets.enter.fade()` 和 `transitionPresets.exit.absoluteFade()`，不会附带缩放。\n- `enter.slideFade()` / `exit.absoluteSlideFade()` 支持通过 `axis`、`direction`、`distance` 快速做方向性滑入滑出。\n- `enter.pop()` 会带一个轻微 overshoot keyframe，适合按钮、标签、浮层等强调进入感的元素。\n- `exit.absoluteFadeScale()` 会自动处理退出元素的绝对定位 keyframes，并默认合并 `anchorDelta`。\n- `exit.absoluteShrink()` 是更明显一点的离场收缩预设。\n- `move.flip()` 会自动使用 `ctx.delta + ctx.anchorDelta`，默认附带缩放补偿；可通过 `includeScale: false` 关闭缩放。\n- `move.translate()` 是只保留位移补偿的轻量版 FLIP。\n- `move.smooth()` 使用更柔和的 easing 和更长的默认时长，适合卡片、面板这类需要“滑顺”感的布局变化。\n\n如果你想显式地把 `TransitionPlugin` 编译成纯函数式接口，也可以使用 `defineTransition(transition)`；传给 `transition` 时两种写法行为一致。\n\n```ts\nimport { defineTransition } from \"@codehz/auto-transition\";\n\nconst compiled = defineTransition(floatingActionsTransition);\n```\n\n对应的类型如下：\n\n```ts\ntype TransitionTiming\u003cCtx\u003e = KeyframeAnimationOptions | ((ctx: Ctx) =\u003e KeyframeAnimationOptions);\n\ntype EffectFrame = {\n  offset: number;\n  opacity?: number;\n  transform?: {\n    translate?: Point;\n    scale?: { x: number; y: number };\n  };\n  filter?: {\n    blur?: string;\n  };\n  transformOrigin?: string;\n  style?: Partial\u003cKeyframe\u003e;\n};\n\ntype TransitionPhaseHandler\u003cCtx\u003e = (ctx: Ctx) =\u003e Animation;\n\ntype TransitionEffect\u003cCtx\u003e = {\n  build(ctx: Ctx): EffectFrame[];\n};\n\ntype TransitionPhaseDefinition\u003cCtx\u003e = {\n  effects: TransitionEffect\u003cCtx\u003e[];\n  options?: TransitionTiming\u003cCtx\u003e;\n};\n\ntype TransitionPhaseLike\u003cCtx\u003e = TransitionPhaseHandler\u003cCtx\u003e | TransitionPhaseDefinition\u003cCtx\u003e;\n\ntype TransitionPlugin = {\n  enter?: TransitionPhaseLike\u003cEnterTransitionContext\u003e;\n  exit?: TransitionPhaseLike\u003cExitTransitionContext\u003e;\n  move?: TransitionPhaseLike\u003cMoveTransitionContext\u003e;\n};\n```\n\n### 兼容写法：纯函数式 `TransitionPlugin`\n\n如果你需要完全控制 `Animation` 对象，函数式 phase 仍然完全可用：\n\n```typescript\ntype TransitionBaseContext = {\n  element: Element;\n  parent: ParentBounds;\n};\n\ntype EnterTransitionContext = TransitionBaseContext \u0026 {\n  rect: Rect;\n};\n\ntype ExitTransitionContext = TransitionBaseContext \u0026 {\n  rect: Rect;\n  viewportRect: Rect;\n  anchorDelta: Point;\n};\n\ntype MoveTransitionContext = TransitionBaseContext \u0026 {\n  current: Rect;\n  previous: Rect;\n  delta: Point;\n  anchorDelta: Point;\n  scale: {\n    x: number;\n    y: number;\n  };\n};\n\nexport type CompiledTransitionPlugin = {\n  enter?(ctx: EnterTransitionContext): Animation;\n  exit?(ctx: ExitTransitionContext): Animation;\n  move?(ctx: MoveTransitionContext): Animation;\n};\n```\n\n`move` 的 `ctx.delta` 和 `ctx.scale` 已经按标准 FLIP 几何预计算好了；如果父容器因为 `right` / `bottom` 锚定、同一微任务内的 remove / insert / reorder 组合，或 replacement 式的“删旧插新”而在整次提交前后发生净位移，`ctx.anchorDelta` 会额外给出这段测量基准补偿。\n\n### 自定义插件示例\n\n下面这个示例直接使用预计算的 `ctx.delta` 和 `ctx.scale`，同时在退出时使用 `ctx.rect` 固定元素位置，并通过 `ctx.anchorDelta` 补偿当前批次提交前后测量基准产生的整体位移。\n\n```tsx\nimport type { TransitionPlugin } from \"@codehz/auto-transition\";\n\nconst floatingActionsTransition: TransitionPlugin = {\n  enter({ element }) {\n    return element.animate(\n      {\n        opacity: [0, 1],\n        transform: [\"translateY(8px) scale(0.96)\", \"translateY(0) scale(1)\"],\n      },\n      { duration: 220, easing: \"ease-out\" },\n    );\n  },\n  exit({ element, rect, anchorDelta }) {\n    const translate =\n      anchorDelta.x === 0 \u0026\u0026 anchorDelta.y === 0 ? \"\" : `translate(${anchorDelta.x}px, ${anchorDelta.y}px) `;\n\n    return element.animate(\n      [\n        {\n          position: \"absolute\",\n          top: `${rect.y}px`,\n          left: `${rect.x}px`,\n          width: `${rect.width}px`,\n          height: `${rect.height}px`,\n          margin: \"0\",\n          opacity: 1,\n          transform: `${translate}scale(1)`,\n        },\n        {\n          position: \"absolute\",\n          top: `${rect.y}px`,\n          left: `${rect.x}px`,\n          width: `${rect.width}px`,\n          height: `${rect.height}px`,\n          margin: \"0\",\n          opacity: 0,\n          transform: `${translate}scale(0.96)`,\n        },\n      ],\n      { duration: 200, easing: \"ease-in\" },\n    );\n  },\n  move({ element, delta, anchorDelta, scale }) {\n    return element.animate(\n      {\n        transform: [\n          `translate(${delta.x + anchorDelta.x}px, ${delta.y + anchorDelta.y}px) scale(${scale.x}, ${scale.y})`,\n          \"translate(0, 0) scale(1, 1)\",\n        ],\n      },\n      { duration: 220, easing: \"ease-in-out\" },\n    );\n  },\n};\n```\n\n### 默认动画行为\n\n- **Enter**: 默认只做透明度淡入，不附带缩放 (250ms ease-out)。\n- **Exit**: 冻结元素当前的绝对定位并淡出；`anchorDelta` 会按同一微任务内整次提交前后的净位移统一结算，因此 replacement 式的“删旧插新”也能保持离场元素的屏幕坐标稳定 (250ms ease-in)。\n- **Move**: 使用标准 FLIP，通过基于当前位置的位移补偿配合缩放过渡；当父容器因 `right` / `bottom` 锚定或同批次布局变更而整体平移时，也会自动附加这段批次级位移补偿 (250ms ease-in)。\n\n如果提供了自定义 `transition`，对应的 `enter` / `exit` / `move` hook 或 effect phase 会优先于内置动画执行。\n\n如果你自定义了 `transition.exit` 或 `transition.move`，推荐把对应的 `ctx.anchorDelta` 合并进 `transform`。不使用这个字段时，普通布局依然可以正常工作，只是在绝对定位父容器通过 `right` / `bottom` 定位、或同批次 replacement / reorder 导致测量基准漂移的场景下不会自动获得位移补偿。replacement 仍然保持 `exit + enter` 语义，而不是旧新元素之间的 morph。\n\n## 许可证\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodehz%2Fauto-transition","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodehz%2Fauto-transition","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodehz%2Fauto-transition/lists"}