SuperAPI AI 工具集

emil-design-eng

设计与创作

查看项目

更新于 10-27 16:21

技能介绍

用途

该技能用于指导界面设计与实现,核心是 Emil Kowalski 的设计工程哲学:在软件普遍“够用”的时代,品味是差异化因素。它帮助设计工程师构建“感觉对”的界面,关注用户不会刻意注意、但会累积成整体体验的隐形细节。

核心哲学

  • 品味是训练出来的,而非天生。好品味不是个人偏好,而是训练出的直觉:能看穿表象、识别什么能提升品质。方法包括置身于优秀作品之中、深入思考为什么某个东西感觉好、并持续练习。
  • 隐形细节会累积。大多数细节用户永远不会意识到,这正是重点:当功能完全符合预期时,用户会不加思索地继续使用。
  • 美感即杠杆。人们选择工具基于整体体验而非仅功能,好的默认值与好的动画是真实差异化因素。

首次响应规则

当技能首次被调用且没有具体问题时,只回复:“I'm ready to help you build interfaces that feel right, my knowledge comes from Emil Kowalski's design engineering philosophy.” 在用户提问前不提供其他信息。

审查格式(强制)

审查 UI 代码时必须使用含 Before、After、Why 三列的 Markdown 表格,每行一个问题,不得使用分行罗列 Before/After 的列表形式。示例行包括:transition: all 300ms → transition: transform 200ms ease-out(指定具体属性,避免 all);transform: scale(0) → transform: scale(0.95); opacity: 0(现实中没有任何东西从无到有出现);下拉菜单用 ease-in → 用自定义曲线的 ease-out(ease-in 显得迟钝,ease-out 给出即时反馈);按钮无 :active 状态 → :active 时 transform: scale(0.97)(按钮必须对按压有响应);popover 的 transform-origin: center → transform-origin: var(--transform-origin)(popover 应从触发元素缩放,模态框除外,模态框保持居中)。

动画决策框架

写动画代码前按顺序回答四个问题:

  1. 是否应该动画?按频率决定:每天 100 次以上(键盘快捷键、命令面板切换)永不做动画;每天数十次(悬停效果、列表导航)移除或大幅减少;偶尔(模态框、抽屉、toast)标准动画;罕见/首次(引导、反馈表单、庆祝)可增加愉悦感。永远不要为键盘发起的操作做动画。Raycast 没有开/关动画,这是每天使用数百次的最优体验。
  2. 目的是什么?有效目的包括:空间一致性(toast 从同一方向进出,使滑动关闭更直观)、状态指示、解释说明、反馈(按钮按下缩放)、防止突兀变化。如果目的只是“看起来酷”且用户经常看到,就不要动画。
  3. 用什么缓动?进入或退出用 ease-out;屏幕上移动/变形用 ease-in-out;悬停/颜色变化用 ease;匀速运动(跑马灯、进度条)用 linear;默认 ease-out。关键:使用自定义缓动曲线,内置 CSS 缓动太弱。示例:--ease-out: cubic-bezier(0.23, 1, 0.32, 1);--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1)(来自 Ionic Framework)。永远不要在 UI 动画中使用 ease-in。缓动曲线资源:使用 easing.dev 或 easings.co 寻找更强的自定义变体,不要从零创建。
  4. 应该多快?按钮按压反馈 100-160ms;工具提示、小 popover 125-200ms;下拉、选择 150-250ms;模态框、抽屉 200-500ms;营销/解释可更长。规则:UI 动画应保持在 300ms 以下。

感知性能

动画速度直接影响用户对应用性能的感知:快速旋转的加载指示器让加载感觉更快;180ms 的选择动画比 400ms 感觉更灵敏;第一个工具提示打开后,后续工具提示即时显示(跳过延迟和动画)让整个工具栏感觉更快。缓动会放大这种效果:200ms 的 ease-out 比 200ms 的 ease-in 感觉更快。

弹簧动画

弹簧比基于时长的动画更自然,因为它模拟真实物理,没有固定时长。适用场景:带惯性的拖拽交互、应感觉“活着”的元素(如 Apple 的 Dynamic Island)、可在动画中途被打断的手势、装饰性鼠标跟踪交互。基于弹簧的鼠标交互:使用 Motion(原 Framer Motion)的 useSpring 插值,而非立即更新。示例代码:import { useSpring } from 'framer-motion'; const springRotation = useSpring(mouseX * 0.1, { stiffness: 100, damping: 10 });。弹簧配置:Apple 方式(推荐,更易推理){ type: "spring", duration: 0.5, bounce: 0.2 };传统物理方式(更多控制){ type: "spring", mass: 1, stiffness: 100, damping: 10 }。bounce 保持微妙(0.1-0.3),大多数 UI 场景避免 bounce,用于拖拽关闭和趣味交互。可打断性优势:弹簧在被中断时保持速度,而 CSS 动画和关键帧从零重启。

组件构建原则

  • 按钮必须有响应感::active 时 transform: scale(0.97),过渡 transform 160ms ease-out,缩放应微妙(0.95-0.98),适用于任何可按压元素。
  • 永远不要从 scale(0) 开始动画:从 scale(0.9) 或更高开始并结合 opacity。
  • 让 popover 感知来源:popover 应从触发元素缩放,默认 transform-origin: center 对几乎所有 popover 都是错的;例外是模态框,保持居中。使用 transform-origin: var(--transform-origin)。
  • 工具提示:后续悬停跳过延迟。首次延迟防止误触,一旦一个工具提示打开,相邻工具提示应即时打开且无动画。示例 CSS 使用 data-starting-style、data-ending-style 和 data-instant。
  • 对可打断 UI 使用 CSS 过渡而非关键帧:过渡可在动画中途被打断并重定向,关键帧从零重启。
  • 用模糊掩盖不完美的过渡:当两个状态交叉淡入淡出感觉不对时,在过渡期间加 filter: blur(2px)。模糊通过混合两个状态弥合视觉间隙。模糊保持在 20px 以下,重度模糊开销大,尤其在 Safari。
  • 用 @starting-style 动画进入状态:无需 JavaScript 的现代 CSS 方式,替代 React 中 useEffect 设置 mounted: true 的模式;浏览器支持不足时回退到 data-mounted 属性模式。

CSS 变换技巧

  • translateY 使用百分比:translate() 中的百分比相对于元素自身尺寸,translateY(100%) 按自身高度移动元素,与具体尺寸无关。Sonner 用它定位 toast,Vaul 用它隐藏抽屉。优先使用百分比而非硬编码像素。
  • scale() 也会缩放子元素:与 width/height 不同,scale() 会缩放子元素,按钮按下时字体、图标、内容按比例缩放,这是特性而非缺陷。
  • 3D 变换制造深度:rotateX()、rotateY() 配合 transform-style: preserve-3d 可在 CSS 中创建真实 3D 效果,无需 JavaScript。
  • transform-origin:每个元素都有变换锚点,默认 center,应设置为触发元素所在位置以实现感知来源的交互。
  • clip-path 用于动画:inset 形状 clip-path: inset(top right bottom left) 定义矩形裁剪区域。可用于从左到右揭示、标签页完美颜色过渡(复制标签列表,将副本样式设为“活动”,裁剪副本只显示活动标签,切换时动画裁剪)、按住删除模式(彩色覆盖层从 inset(0 100% 0 0) 在 :active 时以 2s linear 过渡到 inset(0 0 0 0),释放时以 200ms ease-out 弹回)、滚动图片揭示(从 inset(0 0 100% 0) 到 inset(0 0 0 0),用 IntersectionObserver 或 Framer Motion 的 useInView)、对比滑块(叠加两张图,裁剪顶部图,根据拖拽位置调整右侧 inset 值)。

手势与拖拽交互

  • 基于动量的关闭:不要要求拖过阈值,计算速度 Math.abs(dragDistance) / elapsedTime,若速度超过约 0.11 则无论距离都关闭。示例代码:const timeTaken = new Date().getTime() - dragStartTime.current.getTime(); const velocity = Math.abs(swipeAmount) / timeTaken; if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) { dismiss(); }。
  • 边界阻尼:用户拖过自然边界时施加阻尼,拖得越多元素移动越少。

适用场景与限制

适用于 UI 组件设计、动画决策、交互打磨、代码审查等前端与设计工程场景。资料未提供该技能的具体安装方式、平台支持、收费信息或版本要求,这些事实暂未提供。