组件/FusionSurface 融合表面

FusionSurface 融合表面

边上长出凸起、凸起可拉断成水滴的圆角表面

已验证自 0.6.0

安装

EXAMPLE.BASH
pnpm add @talex-touch/tuffex
EXAMPLE.TYPESCRIPT
import { TxFusionSurface } from '@talex-touch/tuffex/fusion-surface'
import '@talex-touch/tuffex/fusion-surface/style.css'
import '@talex-touch/tuffex/base.css' // 设计令牌与重置样式,全应用引入一次

用法

工具栏托盘

id 不变时改 center,凸起滑到新位置;从 buds 移除,凸起原地收回。

示例加载中...

分裂成水滴

detach 越过断裂点后颈部断开,凸起变成水滴,残留缩回边里。

示例加载中...

四条边

edge 选择长出的边;贴近拐角的凸起会被夹紧到直边上。

示例加载中...

最佳实践

  • 同一条边上的凸起不要重叠:每个凸起占 width 加两侧凹角。
  • 移动凸起时保持 id 不变;换 id 会收回旧凸起、另长一个新的。
  • 重播分裂时先关闭凸起(open: false 或移出 buds),或换一个新 id;调小 detach 只会把水滴移回来。
  • 根节点不设 background、border、box-shadow,也不裁剪(overflow: hidden、clip-path、contain: paint);凸起不占布局空间,父级要为它留出 padding。
  • 颜色用 var(--tx-…) token 以跟随主题;阴影交给 shadow,不写在子元素上。

API 参考

属性

属性名类型默认值说明
budsFusionSurfaceBud[][]要长出的凸起,字段见「类型」;移除的凸起先收回再卸载内容。
radiusnumber16主体圆角,不超过短边一半;也是凸起外角的默认值。
filletnumber12凸起与主体相接处凹角的最大半径,不超过凸起当前高度的一半。
breakAtnumber28颈部完全闭合时的 detach;约在其 96% 处断开。
fillstringvar(--tx-bg-color-overlay)表面填充色。
strokestringnone描边颜色,沿每个凸起、颈部和水滴走。
strokeWidthnumber1描边宽度(px)。
shadowstringvar(--tx-elevation-3)box-shadow 语法,画成剪影上的 drop-shadow();'none' 去掉阴影。
transition'snappy' | 'smooth' | 'bouncy' | SpringConfig'smooth'凸起运动的弹簧;没有时长写法,换目标时保留速度。
contentBlurnumber6凸起内容展开时的起始模糊(px);0 只保留淡入。

事件

事件名参数说明
break(id: string)凸起颈部断开的那一帧触发;每次分裂只触发一次。
settle()所有弹簧静止、帧循环休眠时触发;无需运动的变化不触发。

插槽

插槽名Props说明
default-主体内容,可交互,画在剪影之上。
bud{ bud: FusionSurfaceBud }单个凸起的内容层(带 data-bud),随凸起或水滴移动;关闭时为 inert。

类型

FusionSurfaceBud,即 buds 的一项:

字段类型默认值说明
idstring-跨更新的身份标识,必填。
edge'top' | 'right' | 'bottom' | 'left''top'长出的边;改边时在新边上重新长出,不绕过拐角。
openboolean | numbertrue展开或关闭,或 0..1 的展开比例。
centernumber边的中点沿边位置(px),上 / 下边从左量,左 / 右边从上量;会被夹紧。
widthnumber-沿边尺寸(px),必填;超出直边时收窄。
heightnumber-完全展开时向外的尺寸(px),必填。
radiusnumber表面的 radius凸起外角半径,受自身尺寸限制。
detachnumber0拉离主体的距离(px);越过断裂点后变成水滴。
driftnumber0被拉出部分的横向偏移(px),上 / 下边向右、左 / 右边向下为正。
EXAMPLE.TS
import type {
  FusionSurfaceBud,
  FusionSurfaceEdge, // 'top' | 'right' | 'bottom' | 'left'
  FusionSurfaceEmits,
  FusionSurfaceProps,
  FusionSurfaceTransition, // 'snappy' | 'smooth' | 'bouncy' | SpringConfig
  TxFusionSurfaceInstance,
  // fusionSurfacePath() 用到的类型,见「几何函数」
  FusionSurfaceBudShape,
  FusionSurfaceGeometry,
  FusionSurfaceGeometryInput,
  FusionSurfaceRect,
  FusionSurfaceSpan,
  FusionSurfaceSplit,
} from '@talex-touch/tuffex/fusion-surface'

CSS 变量

变量说明
--tx-fusion-surface-fill剪影填充,由 fill 写入。
--tx-fusion-surface-stroke描边颜色,由 stroke 写入。
--tx-fusion-surface-stroke-width描边宽度,由 strokeWidth 写入。
--tx-fusion-surface-filter剪影的 filter,由 shadow 写入。
--tx-fusion-surface-progress每帧写在凸起内容层上的展开程度(0..1),供 bud 插槽读取。

前四个只在传入对应属性时写入,未传时可由祖先元素或主题设置。

几何函数

fusionSurfacePath() 是组件的几何部分,纯函数、不依赖 Vue 与 DOM;宿主不能改成 TxFusionSurface 时,用它在宿主上方画凸起、颈部和水滴。

EXAMPLE.TS
import type { FusionSurfaceSplit } from '@talex-touch/tuffex/fusion-surface'
import {
  FUSION_SURFACE_BREAK_PINCH,
  fusionSurfacePath,
  fusionSurfacePinch,
  springSteps,
} from '@talex-touch/tuffex/fusion-surface'

const overlayPath = document.querySelector<SVGPathElement>('#composer-overlay path')!
let detach = 0
let velocity = 0
let split: FusionSurfaceSplit | null = null

function frame(dt: number) {
  // 与组件相同的弹簧,换目标时保留速度。
  ;[detach, velocity] = springSteps(detach, velocity, 44, 'smooth', dt)
  const pinch = fusionSurfacePinch(detach, 28)
  // 像组件一样锁存断裂(组件还会把 detach 限制在断裂点)。
  if (!split && pinch >= FUSION_SURFACE_BREAK_PINCH)
    split = { center: 208, width: 168, height: 36, detach, drift: 0, remnant: 1, tail: 1 }
  // …之后用 'snappy' 弹簧把 split.remnant 和 split.tail 降到 0。

  const { d } = fusionSurfacePath({
    width: 320,
    height: 48,
    radius: 16,
    includeBody: false, // 主体由宿主自己绘制
    buds: [{ id: 'message', center: 208, width: 168, height: 36, detach, pinch, split }],
  })
  overlayPath.setAttribute('d', d)
}

includeBody: false 的路径在宿主内闭合;要描边时先裁掉宿主内的部分,并沿 spans 断开宿主自己的边框。

输入

FusionSurfaceGeometryInput:

字段类型默认值说明
width / heightnumber-主体尺寸(px),两者都为正才绘制。
radiusnumber16主体圆角,不超过短边一半。
budsFusionSurfaceBudShape[][]当前这一帧的凸起。
includeBodybooleantrue为 false 时只输出凸起、颈部和水滴。
baseOverlapnumber2includeBody: false 时相连形状伸进主体的距离(px),用来盖住宿主边框。

FusionSurfaceBudShape 的其余字段与 FusionSurfaceBud 相同,但描述当前帧而不是目标:

字段类型默认值说明
heightnumber-当前向外的高度,从 0 增长即展开;小于 0.5 px 不绘制。分裂后是水滴在 split.scale 之前的高度。
filletnumber12凹角的最大半径。
pinchnumber0颈部收窄程度(0..1);为 1 时收成一点。
splitFusionSurfaceSplit | nullnull颈部断开后设置;此后忽略 pinch。

FusionSurfaceSplit:

字段类型默认值说明
center / width / height / detach / driftnumber-断开那一帧的凸起;之后移动凸起只移动水滴。
remnantnumber-残留缩回主体边的进度,从 1 降到 0。
tailnumber-水滴尖尾收圆的进度,从 1 降到 0。
scalenumber1水滴绕自身中心的缩放;关闭分裂的凸起时组件把它降到 0。

输出

FusionSurfaceGeometry:

字段类型说明
dstring顺时针闭合子路径(保留 2 位小数):先是并入相连凸起的主体,再是每滴水滴;不会出现 NaN 或 Infinity。
spansFusionSurfaceSpan[]{ id, edge, from, to }:相连凸起(或残留)与主体边的接合区间,宿主边框在此断开。
rectsFusionSurfaceRect[]{ id, edge, x, y, width, height }:凸起(分裂后为水滴)当前的外框,组件据此摆放 bud 内容。

辅助导出

  • fusionSurfacePinch(detach, breakAt):组件的颈部曲线,breakAt 的前四分之一为 0,之后缓入到 1。
  • FUSION_SURFACE_BREAK_PINCH:0.985,组件锁存分裂时的收窄程度。
  • springSteps(position, velocity, target, config, dt):组件的弹簧积分器;dt 以秒计,返回 [position, velocity]。

概述

  • 剪影是根节点内 z-index: -1 的 SVG,pointer-events: none 且 aria-hidden,位于插槽内容之下。
  • 加入 buds 的凸起原地长出;移除的凸起先收回,期间内容保持挂载并为 inert。
  • open、center、width、height、detach、drift 各是带速度的弹簧,换目标时转向而不重来。
  • 分裂会锁存:调小 detach 不会重新连上;关闭分裂的凸起时,水滴连同内容绕自身中心缩小。
  • shadow 跳过 inset 和带扩展半径的层;var() 层原样透传,只能含一层(如 --tx-elevation-*)。
  • 减少动态效果时(实时跟随),弹簧一帧到位,越过断裂点仍触发 break。

技术实现

  • 每个表面一个 requestAnimationFrame 循环:逐帧积分共享弹簧(liquid/src/spring.ts),重算一条锐利的 SVG 路径,并直接写入凸起内容层的样式;静止后休眠。
  • 相连凸起参照 uiarc.dev 的 Dock,只借鉴观察到的手法,未使用其代码;颈部是本库自己的轮廓模型。
  • 源码:packages/tuffex/packages/components/src/fusion-surface/。
查看源码
packages/tuffex/packages/components/src/fusion-surface/index.ts

使用场景

  • 工具栏或 Dock:工具从栏上长出选项托盘、子菜单或提示。
  • 面板从边上长出气泡或徽标,再让它脱离。
  • 发送消息:输入框把消息拉断,飘进会话;宿主自绘时用 fusionSurfacePath()。

相关组件

组件用途
Fusion 交融两个插槽经 goo 滤镜交融
Liquid 液态流体自由移动的块像水滴般融合
BorderBeam 流光边框沿边框游走或呼吸的光束