Kiln 侧边栏弧形过渡动画实现:用嵌套元素与双轴 Easing 打造 macOS 风格展开/折叠 人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用【免费下载链接】KilnBuild, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.项目地址https://gitcode.com/gh_mirrors/ki/Kiln点击查看免费下载导读本文围绕 Kiln 开源仓库中 Chat Pane 项目的 Phase 3 过渡动画实现计划系统讲解如何用纯 CSS 动画为右侧聊天侧边栏实现展开 ⇄ 折叠的弧形arc运动路径——即 macOS 窗口最小化式的收起效果。文中将完整还原 Phase 3 的设计思路嵌套元素双轴 easing、动画状态机、浮动图标协调并对照 chat_bar.svelte/chat_bar.svelte) 中已经落地的真实实现逐段拆解读者可据此在自己的 Svelte 项目中直接复刻这套仅变换 transform 与 opacity、对合成器友好的侧边栏过渡方案。Phase 3 在整个 Chat Pane 项目中的定位Chat Pane 是 Kiln 前端的一个纯布局小项目目标是构建聊天侧边栏 聊天页的交互框架且明确限定只改动 (app)/layout.svelte/layout.svelte) 与 chat_bar.svelte/chat_bar.svelte) 两个文件chat.svelte属于另一位开发者、不在改动范围内见 project_overview.md。整个项目按三个 Phase 演进见 implementation_plan.mdPhase主题状态Phase 1核心侧边栏布局、展开/折叠、状态持久化、响应式 Dialog已完成Phase 2拖拽调整侧边栏宽度localStorage 持久化、min/max 约束已完成Phase 3展开/折叠过渡动画本文主题规划完成、实现已落地Phase 1/2 解决的是功能可用而 Phase 3 解决的是手感。其目标一句话概括在大屏lg:及以上下侧边栏从展开态收回到右下角浮动聊天图标的位置时不是生硬地瞬间消失而是沿着一条曲线弧路径平滑飞回如同 macOS 窗口最小化到 Dock 的动效phase_3.md 原文The sidebar animates to/from the floating chat icon position using a curved arc path (macOS minimize-style)。核心设计为什么用嵌套元素 双轴 Easing来模拟弧线CSS 本身没有沿弧线运动的变换原语——translate只能走直线。要让元素走出弧形轨迹Phase 3 给出的方案是经典的嵌套元素分轴动画外层 wrapper 只负责 X 轴translateX使用 ease-out 曲线起步快、结尾慢内层 wrapper 只负责 Y 轴translateY使用 ease-in 曲线起步慢、结尾快。当两个方向上一个先快后慢、一个先慢后快的运动被叠加到同一个视觉元素上时合成出的轨迹就是一条下凹的曲线弧——元素先水平飞出、再向下坠落到图标位置。这正是 macOS 最小化动画的视觉本质而且全程只动transform不会触发布局与重绘。原计划给出的两条贝塞尔曲线phase_3.md外层 X 轴cubic-bezier(0.33, 1, 0.68, 1)——标准 ease-outeaseOutCubic内层 Y 轴cubic-bezier(0.32, 0, 0.67, 0)——标准 ease-ineaseInCubic。这两条曲线互为镜像叠加后即可得到平滑的圆弧。值得说明的是仓库实际落地实现中把曲线简化成了linear见下文落地实现一节并改用 opacity 提前淡出 末端缩放来承担视觉缓动这是工程上权衡后的调整——计划给出的是理论曲线实现追求的是稳定可预期的关键帧。动画状态机idle / collapsing / expanding动画绝不能与expanded布尔值直接绑定切换否则会闪跳。Phase 3 要求引入一个三态动画状态变量实际实现对应 chat_bar.svelte/chat_bar.svelte#L44-L45) 中的类型定义type AnimState idle | collapsing | expanding let animState: AnimState idle状态含义进入条件idle静止态侧边栏完全展开或完全隐藏无动画运行动画结束回调collapsing折叠中侧边栏正在飞向浮动图标大屏 当前展开 点击 Xexpanding展开中侧边栏正从图标位置飞回大屏 当前折叠 点击浮动图标触发切换的逻辑在toggle()中chat_bar.svelte/chat_bar.svelte#L53-L65)function toggle() { if (isLargeScreen() animState idle) { if (expanded) { animState collapsing // 展开 → 折叠先进入 collapsing setChatBarExpanded(false) // 立即持久化状态 } else { animState expanding // 折叠 → 展开先进入 expanding setChatBarExpanded(true) } } else if (!isLargeScreen()) { setChatBarExpanded(!expanded) // 小屏无动画直接切换 Dialog } }两个关键细节只有idle时才能触发新动画防止用户连点导致动画互相打断、状态错乱setChatBarExpanded()在动画开始时就调用状态持久化sessionStorage localStorage与视觉动画解耦即使动画中途被中断刷新后恢复的也是最终目标态而不是中间帧。折叠Collapsing流程拆解按 phase_3.md 的流程定义折叠过程分五步置状态collapsing为外层与内层 wrapper 挂上折叠动画 CSS 类外层 translateX 向右飞出ease-out侧边栏整体水平向右侧图标方向移动内层 translateY 向下坠落ease-in内容块相对外层向下滑落与 X 轴运动合成弧线opacity 提前淡出透明度比位移更早完成营造飞走过程中逐渐消失的层次感animationend回调将expanded置为false、animState复位为idle。实际实现中还有一个衍生出来的状态sidebarVisible与iconHiddenchat_bar.svelte/chat_bar.svelte#L81-L84)$: sidebarVisible expanded || animState collapsing $: iconHidden expanded || animState collapsing || animState expanding折叠期间侧边栏名义上已不展开但仍需留在 DOM 里把动画播完所以sidebarVisible在collapsing期间仍为 true动画结束后外层容器靠chat-hidden类visibility: hidden; pointer-events: none真正隐藏既不遮挡点击也不参与可访问性树chat_bar.svelte/chat_bar.svelte#L347-L350)。动画结束回调onAnimationEndchat_bar.svelte/chat_bar.svelte#L69-L79)除了复位状态还顺带触发浮动图标的弹性入场function onAnimationEnd() { if (animState collapsing) { iconBounce true requestAnimationFrame(() { setTimeout(() { iconBounce false }, 350) }) } animState idle }即折叠完成后右下角的聊天图标会以scale(0.3 → 1.1 → 1)的弹簧曲线蹦出来cubic-bezier(0.34, 1.56, 0.64, 1)350ms见 chat_bar.svelte/chat_bar.svelte#L439-L453)——这个细节让收起后图标出现这一瞬不再突兀是原计划未提及、实现阶段补充的润色。展开Expanding流程拆解展开是折叠的逆过程phase_3.mdexpanded true、状态置expandingexpanded先置 true使 spacer占位 div立即恢复为布局预留宽度避免内容区跳动反向动画播放外层从左上方弧线飞回原位、内层同步回弹、opacity 恢复animationend后状态复位idle。实现中expanding与collapsing共用同一套关键帧只是from/to互换expand-x/expand-y/expand-opacity见 chat_bar.svelte/chat_bar.svelte#L421-L462)。一个值得注意的实现取舍展开动画总时长是 250ms比折叠的 400ms 短。这是符合直觉的设计——展开是呼之即来的主动操作应干脆利落折叠是目送离开的被动效果可稍慢以看清弧线轨迹。原计划只给了统一约 400ms 的指导值实现阶段按交互语义做了细分。CSS 动画细节从计划数值到落地实现Phase 3 明确列出的动画工程约束phase_3.md与实现对照如下约束项计划要求落地实现chat_bar.svelte/chat_bar.svelte#L352-L462)总时长~400ms折叠 400ms展开 250ms外层 X 轴cubic-bezier(0.33, 1, 0.68, 1)ease-outlineartranslateX(0 → calc(100% - 48px))内层 Y 轴cubic-bezier(0.32, 0, 0.67, 0)ease-inlineartranslateY(0 → calc(100% - 48px)) scale(1 → 0.3)opacity比位移更快淡出折叠 250ms 内于 70% 关键帧归零展开从 0.5 渐变回 1防回流overflow hidden 固定宽度.chat-collapse-x, .chat-expand-x { overflow: hidden } 内联宽度合成器友好仅 transform 与 opacitywill-change: transform, opacitychat_bar.svelte/chat_bar.svelte#L356-L369)额外项—transform-origin: bottom right让缩放以图标锚点为原点计划中的两条贝塞尔曲线在实现里被linear替代但弧线效果并未丢失原因是实现引入了一个计划未列出的关键元素Y 轴末端的scale(0.3)缩放。位移 缩放叠加再配合 opacity 在 400ms 总时长内提前250ms、70% 处归零视觉上仍然呈现出先横移、再下坠缩小消失的弧形收束感且linear的时间函数让关键帧行为完全确定、可预测。折叠侧三个关键帧的完整定义keyframes collapse-x { from { transform: translateX(0); } to { transform: translateX(calc(100% - 48px)); } /* 向右让出 48px 图标余量 */ } keyframes collapse-y { from { transform: translateY(0) scale(1); } to { transform: translateY(calc(100% - 48px)) scale(0.3); } } keyframes collapse-opacity { 0% { opacity: 1; } 70% { opacity: 0; } 100% { opacity: 0; } /* 提前完成淡出 */ }calc(100% - 48px)中的 48px 对应浮动图标的视觉尺寸余量——侧边栏并非完全飞出视口而是飞到位移剩余 48px 的位置后由 opacity 完成最后 48px 的隐没这样终点恰好落在图标附近视觉上无缝衔接。DOM 结构调整始终渲染 双 wrapper 条件 spacerPhase 3 要求对 DOM 结构做三处关键改动phase_3.md这与 Phase 1 的条件渲染侧边栏有本质区别lg:及以上始终渲染侧边栏面板移除视觉面板的{#if expanded}条件面板常驻 DOM只靠动画类切换可见性——否则元素从未渲染animationend无从触发动画也无从播放外层 wrapper 管 X、内层 wrapper 管 Y形成双轴解耦的动画载体{#if expanded}仅保留给 spacer占位 divspacer 是布局元素展开时立即出现以预留侧边栏宽度、折叠时立即消失以归还宽度它必须瞬间切换不能被动画拖慢。落地后的模板结构chat_bar.svelte/chat_bar.svelte#L189-L281)完全遵循了这一设计!-- Spacer仅用于布局占位随 expanded 即时切换 -- {#if expanded} div classhidden lg:block flex-shrink-0 style:width{sidebarWidth}px/div {/if} !-- 大屏侧边栏常驻 DOM可见性由动画控制 -- div classhidden lg:flex flex-row fixed top-6 right-4 bottom-4 chat-anim-outer class:chat-collapse-x{animState collapsing} class:chat-expand-x{animState expanding} class:chat-hidden{!sidebarVisible animState idle} style:width{sidebarWidth}px on:animationend{onAnimationEnd} div classchat-anim-inner flex flex-row w-full h-full class:chat-collapse-y{animState collapsing} class:chat-expand-y{animState expanding} !-- 拖拽手柄 侧边栏内容标题栏、Chat / 等 -- /div /div结构要点外层固定定位fixed top-6 right-4 bottom-4即贴合右侧、上下留边与 Phase 1 架构文档中viewport-pinned的定位思想一脉相承外层挂on:animationend因为 X 轴动画collapse-x/expand-x挂在它身上动画结束事件在此捕获class:chat-hidden条件里显式要求animState idle确保动画播完前永不隐藏。浮动图标协调三个时间点的显隐规则折叠/展开动画与浮动图标的切换必须严格错峰否则会出现图标和侧边栏同时存在或两者都消失的空窗。Phase 3 定义了四条规则phase_3.md实现中的iconHidden推导正是对这四条规则的编码时间点图标状态iconHidden取值折叠动画进行中隐藏不能被看到truecollapsing折叠完成、处于 idle显示falseexpandedfalse, animStateidle展开动画一开始立即隐藏trueexpanding展开完成保持隐藏侧边栏以静止态呈现trueexpandedtrue模板侧chat_bar.svelte/chat_bar.svelte#L283-L300)通过lg:hidden类实现逻辑隐藏但保留在文档流外button classfixed bottom-6 right-6 btn btn-circle btn-primary shadow-lg z-50 {iconHidden ? lg:hidden : } {iconBounce ? chat-icon-bounce : } on:click{() { if (window.matchMedia((min-width: 1024px)).matches) { toggle() // 大屏动画展开/折叠 } else { openDialog() // 小屏打开全屏 Dialog } }} aria-labelOpen assistant div classw-6 h-6ChatIcon //div /button另外注意z-50浮动图标必须位于最高层级因为它要悬浮在侧边栏动画路径的终点上方。与前两个 Phase 的联动动画不能破坏已有的布局与状态机制Phase 3 不是孤立动画它运行在 Phase 1/2 建立的机制之上实现时有三处联动需要特别当心1. 与状态持久化的联动Phase 1展开/折叠状态由 chat_ui_storage.ts 持久化读取时 sessionStorage 优先、localStorage 兜底、默认展开写入时两个存储同步更新。动画期间调用setChatBarExpanded()写入的是目标态而非中间态因此刷新页面永远恢复到折叠/展开的终点不会停在半途。而真正的状态流转由 chat_ui_state.ts 中的writablestore 驱动动画只是视觉层。2. 与自定义宽度的联动Phase 2侧边栏支持拖拽调宽min 280px、max 30vw见 chat_bar_sizing.ts。动画期间宽度以内联style:width固定——外层容器宽度在动画全程保持不变这正是 Phase 3固定宽度防止 reflow约束的来源宽度不变位移动画才不会被布局抖动干扰。折叠后的终点calc(100% - 48px)基于当前实际宽度计算所以任意自定义宽度下弧线终点都正确落在图标附近。3. 与响应式断点的联动Phase 1isLargeScreen()用window.matchMedia((min-width: 1024px))实时判定chat_bar.svelte/chat_bar.svelte#L47-L49)。只有大屏才走动画路径小屏下浮动图标直接打开全屏 Dialog侧边栏面板本身被hidden lg:flex隐藏完全不参与动画。另外在/assistant全屏聊天路由下整个chat_bar.svelte提前return侧边栏与图标都不渲染动画自然也不会触发chat_bar.svelte/chat_bar.svelte#L189)。测试策略为什么这个 Phase 只做手动验证Phase 3 原计划明确标注 Manual testing only. Visual animations are verified by inspectionphase_3.md。这与项目的整体测试策略一致chat_ui_storage.test.ts 用 vitest 对持久化逻辑做单测session 优先、local 兜底、非法值降级、存储抛异常不崩溃chat_bar_sizing.test.ts 对宽度钳制与断点默认值做单测但动画本身属于纯视觉行为关键帧曲线、弧线轨迹、淡出节奏只能靠肉眼在真实浏览器里逐断点验收架构文档architecture.md中同样注明chat_bar.svelte不做组件级测试、以手动 QA 为准。手动验证时的建议清单覆盖原计划的验收维度折叠路径大屏展开态点击 X确认侧边栏沿弧线向右下飞出、opacity 在位移结束前归零、结束后图标以弹性动画出现展开路径点击浮动图标确认侧边栏从图标位置弧线飞回、spacer 立即恢复、内容区无跳动连点防护动画进行中再次点击确认不会打断或错乱animState ! idle时 toggle 被忽略持久化回归折叠后刷新确认恢复折叠态session 优先新开标签页确认继承最近一次选择local 兜底断点回归缩放到lg以下确认侧边栏动画路径完全不可达、浮动图标打开的是 Dialog性能感知DevTools Performance 面板确认动画期间无布局/重绘只有合成器合成transform/opacity 的will-change生效。落地实现的性能保障合成器友好原则Phase 3 反复强调Only transform and opacity animated (compositor-friendly)这是该动画方案的性能底线落地实现中体现为三处will-change: transform, opacity预声明chat_bar.svelte/chat_bar.svelte#L356-L369)提示浏览器提前将该元素提升到独立合成层避免动画中途才提升导致的首次帧卡顿动画只触碰transform与opacity这两个属性不触发布局layout与绘制paint仅触发合成composite可在 GPU 上完成overflow: hidden 固定宽度侧边栏内容Chat /聊天组件体积大、DOM 重若在动画中触发回流会非常昂贵固定宽度把回流面锁死在外层容器内。这套嵌套双 wrapper 双轴动画 合成器友好的模式同样适用于抽屉导航、通知浮层、悬浮球收起等任何需要元素飞向锚点的交互场景是值得沉淀为团队通用组件的手势级动效方案。相关资源速查规划文档Phase 3 过渡动画、Phase 1 核心布局、Phase 2 拖拽调宽项目文档功能规格、架构说明、实施计划核心源码chat_bar.svelte/chat_bar.svelte)动画逻辑与 CSS、chat_ui_storage.ts状态持久化、chat_ui_state.tsSvelte store、chat_bar_sizing.ts宽度约束、layout.svelte/layout.svelte)布局集成测试证据chat_ui_storage.test.ts、chat_bar_sizing.test.ts赞分享人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用【免费下载链接】KilnBuild, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.项目地址https://gitcode.com/gh_mirrors/ki/Kiln点击查看免费下载相关推荐CSS Layout侧边栏可折叠侧边栏实现CSS Layout侧边栏可折叠侧边栏实现 你是否还在为网页布局中的侧边栏实现而烦恼是否想要一个既美观又实用的可折叠侧边栏解决方案本文将为你详细介绍如何使教程前端vue3-element-admin侧边栏折叠菜单展开/收起动画vue3 element admin侧边栏折叠菜单展开/收起动画 你是否在使用后台管理系统时遇到侧边栏折叠不流畅、菜单展开卡顿的问题vue3 elemen前端认证鉴权Ariakit Animated Disclosure用纯 CSS 过渡动画实现 Disclosure 内容高度展开与折叠Ariakit Animated Disclosure用纯 CSS 过渡动画实现 Disclosure 内容高度展开与折叠 本篇指南围绕 Ariakit 仓库UI组件前端上一篇Vue 3 中文文档怎么读才高效3 个学习阶段、4 类目录从安装到实战一次讲透下一篇用免费开源显示字体 Bebas Neue 打造品牌视觉5 步实战全记录创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考