弹性变形 UI
本技能生成的界面中,每次状态变化都会变形:标签指示器像蠕虫一样拉伸到下一个标签,按钮挤压成加载圆圈再膨胀回去,产品图片从卡片上抬起、缩小成芯片并落入购物车。它源自构建一个 24 个组件的展示页面(位于 assets/showcase.html)以及在此过程中发现的错误和设计修正。
外观源于一个理念:拉伸是由某种原因引起的。元素变形是因为其各部件不一致(一条边先于另一条到达)、移动过快(沿路径伸长)、被挤压(另一个方向膨胀以保持体积)或刚被击中(摆动)。循环的随机摆动显得廉价;有原因的拉伸则像真实材料。
每次按步骤 0 执行:审计用户代码并确认
在构建或编辑任何内容之前,找出用户界面中哪些部分可以变得弹性化,并让他们选择。详情、判断标准和消息模板在 references/audit.md 中;简要版本:
- 找到代码:粘贴的代码、附加文件(在 claude.ai 上,列出
/mnt/user-data/uploads)、工作目录或仓库、工件链接(使用工件工具读取),或用户指向的源代码。如果没有代码,说明你找到了什么,并提供两种路径:分享代码,或从头构建新内容。 - 扫描它:
python scripts/audit_ui.py <路径>报告技术栈、现有动画、缺失的减少动效处理,以及每个 UI 类别的候选者及其文件:行号,每个都映射到展示组件和技术。 - 在上下文中阅读每个命中点,只保留真正的候选者。对于每个候选者,记录其位置、当前状态、它将变成的变形描述、工作量(小、中、大)以及任何风险(组件库标记、现有动画库、移动选择器会丢失的原生控件、非常长的列表)。
- 按可见性 × 频率 × 匹配度排序,合并重复项(所有使用一个共享的
Button的按钮算作一个项目),将列表限制在约十个项目,并列出你建议不要添加的项及其原因。 - 用用户的语言请求确认:编号的提议,然后一个问题邀请他们选择编号、接受所有推荐,或输入不在列表中的他们想要弹性化的额外部分。如果环境支持,使用可点击选择工具,否则使用纯文本。在此结束该轮;在收到回复前不做任何编辑。
- 构建他们选择的以及他们添加的任何内容,然后按照下面的工作流程进行,并逐项报告。
如果用户已经命名了他们想要的部分,仍要进行审计:将他们的部分放在首位,将其他部分作为可选提供,并保持问题简短。
文件
所有路径都是相对于本技能文件夹(包含此 SKILL.md 的文件夹),而不是用户的项目。在读取或运行它们时,要相对于技能文件夹解析它们。
| 路径 | 说明 | 何时打开 |
|---|---|---|
assets/stretchy-core.js | 弹簧引擎及其辅助函数(边缘对、分段指示器、体积缩放、凸起的 SVG 边缘、FLIP 重新排序、抛射物体) | 粘贴到您构建的每个页面。阅读一次以了解辅助函数。 |
assets/starter.html | 页面骨架:亮/暗主题标记、支持泰语的可变字体、减少动效处理、内联核心、一个示例组件 | 从此文件开始新页面 |
assets/showcase.html | 来自原始构建的所有 24 个工作组件。每个 JS 部分都以横幅开始,如 /* ---------- tabs ---------- */ | 当请求与目录组件匹配时,grep -n "/\* ---------- " assets/showcase.html 并仅读取该部分及其 CSS |
references/audit.md | 如何查找、扫描、判断和排序用户的代码,以及确认消息模板(泰语和英语) | 每次按步骤 0 执行 |
references/techniques.md | 11 种弹性拉伸技术的深入说明,包含代码和调节参数 | 在构建任何以前未构建过的东西之前 |
references/components.md | 目录:每个组件的配方、弹簧值和陷阱 | 当请求命名或类似于目录组件时 |
references/new-components.md | 如何设计任何组件的弹性版本,以及 14 个未在展示中展示的已设计示例 | 当请求超出目录范围时 |
scripts/audit_ui.py | 用户代码的扫描器:技术栈、现有动画,以及带 文件:行号 的弹性候选者 | 每次按步骤 0 执行 |
scripts/check_page.py | 无头浏览器检查:JS 错误、手机和桌面端横向滚动溢出、隐藏元素拦截点击,以及动画帧的接触表 | 每次构建后,在交付之前 |
如果存在视觉设计技能(例如 frontend-design 或 ui-ux-pro-max),也请阅读它。该技能决定页面的外观(调色板、字体、布局);本技能决定其移动方式。动效抛光无法挽救一个通用外观的页面。
工作流程(用户确认后)
- 为每个组件编写变形描述。 “当触发条件发生时,形状A变为形状B,并且它拉伸是因为原因。”示例:“当单击标签时,指示器移动到它,并且它拉伸是因为其前缘比后缘更硬。”如果您无法填写原因,请从下面的表格中选择一个。这个描述就是设计;其他都是实现。
- 从表格中选择技术。 大多数组件使用一个主要拉伸加上一个小的反馈摆动。
- 将静止状态定义为数字:宽度、高度、半径、位置、进度 0 到 1。弹簧驱动数字变化,您在
onUpdate中将其写入style。CSS 过渡仅负责颜色和透明度(以及简短内容的交叉淡入淡出)。 - 规划内容。 文本和图标在容器变形时不能重排或压扁。为每个内容状态提供一个独立的图层,保持最终大小,锚定(通常居中),并进行交叉淡入淡出:快速退出(约 150 毫秒),延迟约 100–150 毫秒后进入,并伴有轻微的缩放或模糊。容器的
overflow:hidden在内容增长时显示内容。 - 从
assets/starter.html开始构建,并内联核心。直接在弹簧回调中编写样式;切勿触发框架的逐帧重新渲染。 - 可访问性和中断检查(检查清单如下)。
- 测试和观察。 运行
scripts/check_page.py,然后捕获重要交互的帧接触表并实际观察它们。原始构建中的“加入购物车”动画在技术上是“工作”的,但看起来廉价,直到检查了帧;修复方法在“使其看起来更好”中。
拉伸的来源
| 情况 | 技术(详情见 references/techniques.md) | 示例 |
|---|---|---|
| 某些东西在槽位之间移动 | 边缘对:两个弹簧分别控制前缘和后缘,前缘弹簧较硬,根据方向交换 | 标签页、轮播圆点、开关、选择芯片、单选圆点、步进器、下拉高亮、评级胶囊、侧边栏导航 |
| 容器改变大小或形状 | 轴向分离:宽度、高度、半径使用不同刚度的独立弹簧,使一个轴领先 | 动态岛、FAB 到表单、从按钮弹出的模态框、下拉菜单、加入购物车按钮 |
| 某些东西快速变窄或变宽 | 体积保持:按速度缩放另一个轴,scaleY = clamp(1 − v/k) | 发送按钮、搜索字段、下拉菜单宽度 |
| 某些东西快速移动 | 速度拉伸:沿运动方向伸长,垂直方向变薄 | 滑块拇指、抛出的购物车商品、轮播卡片(斜切)、工具提示 |
| 大表面移动 | 凸起的边缘:将表面绘制为 SVG 路径,其边缘控制点偏移 −速度 | 侧边栏、底部导航面板 |
| 某个部分悬挂在一个移动部分上 | 滞后跟随者:一个较软的弹簧跟随主弹簧;利用间隙实现倾斜或偏移 | 滑块值气泡、手风琴内部文本 |
| 点击或到达的反馈 | 冲击: 向静止在 0 的弹簧添加速度,它摆动并稳定 | 复选框、芯片、购物车、星级评分、徽章、数量限制震动 |
| 模糊的块连接或分离 | 粘性滤镜: 模糊加透明度阈值,清晰的源合成在上面 | 粘性菜单 |
| 一个元素变成另一个元素 | 容器变换: 从触发器的矩形开始,弹簧过渡到目标矩形,内容交叉淡入淡出 | 模态框、FAB、购物车投掷 |
| 变化需要在中间暂停 | 通过中间形状的顺序变形,该形状承载含义(旋转器、勾选) | 发送按钮、加入购物车按钮 |
| 拖拽超过限制,或列表重新排序 | 橡皮筋(将过卷除以约 3)和带弹簧的 FLIP,保持速度 | 轮播、面板、排序后的产品列表 |
引擎概述
Spring 是一个阻尼谐波振荡器,使用半隐式欧拉法在每个帧内进行 8 个子步骤(帧增量上限为 1/30 秒,这样后台标签页不会导致失控)。to(target) 重新定位目标而不重置速度,这使得快速点击感觉连续而非重新开始。set(x) 无动画跳跃,drag(x, v) 跟随指针并记录速度,kick(v) 注入脉冲。阻尼在每个节拍乘以全局 bounce 因子,因此一个页面级控制(软 1.8,有弹性 1,弹性拉伸 0.5)可以实时重新调谐每个组件。在 prefers-reduced-motion: reduce 下,to() 立即跳转到目标并立即触发 onRest,因此等待动画的逻辑仍然执行。
阻尼比 ζ = c / (2√k)。UI 状态变化在 ζ 0.6–0.8 左右感觉正确;有趣味的反馈在 0.3–0.5;高于 1 则无过冲。展示中使用的预设值:
| 角色 | k | c | 备注 |
|---|---|---|---|
| 边缘对的前缘 | 420–520 | 30–34 | 先到达 |
| 后缘 | 140–170 | 17–19 | 滞后,产生拉伸 |
| 容器宽度(变形) | 240–320 | 18–24 | |
| 容器高度(变形) | 150–170 | 15–17 | 比宽度慢,因此形状先横向拉伸 |
| 指针跟随器(滑块) | 650 | 42 | 紧,但仍有速度可读 |
| 滞后跟随器(气泡) | 150 | 11 | 故意松散 |
| 冲击反馈 | 380–420 | 12–16 | 冲击 6–10 |
| 可见过冲的高度(手风琴) | 210 | 14 |
经验法则
- 限制每次变形。 控制:体积保持在 ±7–15% 以内。小而快的东西(抛出的芯片、滑块拇指):最多 35–70% 的拉伸。始终将大小限制在正数最小值,因为欠阻尼弹簧可能低于零。
- 按方向交换刚度。 在边缘对中,面向运动方向的边缘使用较硬的弹簧。如果没有交换,指示器将正确地向一个方向拉伸,而向另一个方向压缩。
- 返回时采用相反的时间。 如果打开时宽度领先于高度,则在关闭时让高度领先于宽度。形状因此走一条不同的路径,这被解读为弹性而非倒放。
- 状态变化用弹簧,形状重要的路径用时基曲线。 抛射物体需要可预测的弧线(二次贝塞尔,缓入缓出进度,约 800 毫秒)。独立的 x/y 弹簧会产生漂移路径。无论哪种方式,都要从逐帧速度推导出拉伸。
- 保持内容正直。 要沿任意方向拉伸某物而不旋转其图片或文本,使用
rotate(a) scale(1+s, 1−s·0.45) rotate(−a)。 - 每个组件一个大胆的动效。 组件的其余部分保持安静:颜色变化、短淡入淡出。
- 空闲时平静。 没有东西自己摆动。最多在页面加载时安排一个协调的时刻(展示在 900 毫秒后打开动态岛一次)。
使其看起来更好(构建中的经验)
第一个加入购物车版本沿两个独立弹簧飞行了一个纯色球,将按钮缩小成一个空的紫色椭圆,并压扁了购物车。用户说它不好看。修复方案具有通用性:
- 移动真实物体。 克隆产品图片,稍微抬起并带阴影,然后将其缩小成一个圆形芯片,在飞行过程中仍显示图片。抽象的圆点读起来像调试输出。
- 中间形状必须承载意义。 缩小的按钮是一个空块,除非它显示了一些东西:一个旋转器、一个自绘的勾选、一个计数。添加按钮现在变成一个绿色圆圈并打勾,然后成长为 − n + 步进器。
- 控件上的微妙体积。 按钮上 ±18% 的垂直凸起看起来像药丸形的蛋。±7% 读起来像柔软材料。
- 到达需要一个接收器。 购物车图标下沉并倾斜,一个环脉冲向外扩散,徽章弹出。有些东西必须明显地抓住被抛出的东西。
- 不要裁剪状态标记。 变形中的
overflow:hidden药丸内的徽章会被裁剪;将其放在裁剪区域外的包装器中。 - 不要重复信息。 购物车药丸显示“3 ชิ้น 195 บาท”,旁边还有一个徽章显示 3。现在只显示总数。
- 以固定比例绘制插图。 平铺中的百分比定位 CSS 形状在平铺变宽时会拉伸。使用带有 viewBox 的內联 SVG,在平铺中居中。
可访问性和健壮性检查清单
- 尊重减少动效(核心已实现;确保 CSS 关键帧和过渡也在
prefers-reduced-motion媒体查询中被禁用)。 - 使用真实的
button、input、role="switch"、role="tab"、role="slider"等,并带有正确的aria-*状态,在与启动动画相同的调用中更新。 - 被变形隐藏的内容应添加
inert(并且不得拦截点击:给不可见图层设置pointer-events:none,然后在可见状态时将其重新打开)。 - 当焦点控件消失到变形中时,有意识地移动焦点:到替换控件,在对话框关闭后回到触发器,并且仅在元素再次可见之后(
visibility:hidden元素不能接收焦点)。