FusionSurface
A rounded surface that grows buds from its edges and pinches them into drops.
Installation
pnpm add @talex-touch/tuffex
import { TxFusionSurface } from '@talex-touch/tuffex/fusion-surface'
import '@talex-touch/tuffex/fusion-surface/style.css'
import '@talex-touch/tuffex/base.css' // tokens + resets, once per app
Usage
Toolbar Tray
With a stable id, a new center slides the bud over; removing it from buds closes it in place.
Split into a Drop
Once detach passes the break point, the neck snaps: the bud becomes a drop and the stub sinks back into the edge.
Buds on Every Edge
edge picks the side a bud grows from; a bud pushed toward a corner is clamped onto the straight edge.
Best Practices
- Keep buds on one edge apart: each takes its
widthplus a fillet on each side. - Keep a bud's
idstable while it moves; a newidcloses the old bud and grows another. - To replay a split, close the bud (
open: false, or drop it frombuds) or use a newid; loweringdetachonly moves the drop back. - Give the root no
background,border, orbox-shadow, and never clip it (overflow: hidden,clip-path,contain: paint); buds take no layout space, so pad the parent. - Color the surface with
var(--tx-…)tokens to follow the theme, and give the shadow toshadow, not to children.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
buds | FusionSurfaceBud[] | [] | Buds to grow (fields under Types); a removed bud closes before its content unmounts. |
radius | number | 16 | Body corner radius, capped at half the shorter side; also each bud's default outer radius. |
fillet | number | 12 | Largest radius of the concave fillet into the body; at most half the bud's current height. |
breakAt | number | 28 | detach at which the neck fully closes; it snaps at about 96% of it. |
fill | string | var(--tx-bg-color-overlay) | Surface fill color. |
stroke | string | none | Outline color; follows every bud, neck, and drop. |
strokeWidth | number | 1 | Outline width in px. |
shadow | string | var(--tx-elevation-3) | box-shadow syntax, drawn as drop-shadow() on the silhouette; 'none' removes it. |
transition | 'snappy' | 'smooth' | 'bouncy' | SpringConfig | 'smooth' | Spring for bud motion; no duration form, and velocity carries across retargets. |
contentBlur | number | 6 | Blur in px that bud content starts from as it opens; 0 keeps only the fade. |
Events
| Event | Params | Description |
|---|---|---|
break | (id: string) | Fires on the frame a bud's neck snaps; once per split. |
settle | () | Fires when every spring rests and the frame loop sleeps; not for changes that move nothing. |
Slots
| Slot | Props | Description |
|---|---|---|
default | - | Body content: interactive DOM over the silhouette. |
bud | { bud: FusionSurfaceBud } | One bud's content layer (with data-bud); rides the bud or drop and is inert while closed. |
Types
FusionSurfaceBud, one entry of buds:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | - | Identity across updates. Required. |
edge | 'top' | 'right' | 'bottom' | 'left' | 'top' | Edge it grows from; a new edge regrows it there instead of sliding around the corner. |
open | boolean | number | true | Open or closed, or a 0..1 amount open. |
center | number | middle of the edge | Position along the edge in px, from the left or top; clamped. |
width | number | - | Size along the edge in px; narrowed to fit the straight part. Required. |
height | number | - | Outward size when fully open, in px. Required. |
radius | number | the surface radius | Outer corner radius, capped by the bud's own size. |
detach | number | 0 | How far the bud is pulled away, in px; past the break point it becomes a drop. |
drift | number | 0 | Sideways offset of the pulled-away part in px: rightward on top/bottom, downward on left/right. |
import type {
FusionSurfaceBud,
FusionSurfaceEdge, // 'top' | 'right' | 'bottom' | 'left'
FusionSurfaceEmits,
FusionSurfaceProps,
FusionSurfaceTransition, // 'snappy' | 'smooth' | 'bouncy' | SpringConfig
TxFusionSurfaceInstance,
// fusionSurfacePath(), see Geometry function
FusionSurfaceBudShape,
FusionSurfaceGeometry,
FusionSurfaceGeometryInput,
FusionSurfaceRect,
FusionSurfaceSpan,
FusionSurfaceSplit,
} from '@talex-touch/tuffex/fusion-surface'
CSS Variables
| Variable | Description |
|---|---|
--tx-fusion-surface-fill | Silhouette fill, written by fill. |
--tx-fusion-surface-stroke | Outline color, written by stroke. |
--tx-fusion-surface-stroke-width | Outline width, written by strokeWidth. |
--tx-fusion-surface-filter | The silhouette's filter, written by shadow. |
--tx-fusion-surface-progress | How open a bud is (0..1), written on its content layer each frame for bud slot content. |
The first four are written only for props you pass, so an ancestor or theme can set them.
Geometry function
fusionSurfacePath() is the component's geometry as a pure function, with no Vue or DOM. Use it to draw buds, necks, and drops over a host that can't become a TxFusionSurface.
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) {
// The component's spring, velocity kept across retargets.
;[detach, velocity] = springSteps(detach, velocity, 44, 'smooth', dt)
const pinch = fusionSurfacePinch(detach, 28)
// Latch the break as the component does (it also caps detach at the break point).
if (!split && pinch >= FUSION_SURFACE_BREAK_PINCH)
split = { center: 208, width: 168, height: 36, detach, drift: 0, remnant: 1, tail: 1 }
// …then spring split.remnant and split.tail down to 0 ('snappy').
const { d } = fusionSurfacePath({
width: 320,
height: 48,
radius: 16,
includeBody: false, // the host draws its own body
buds: [{ id: 'message', center: 208, width: 168, height: 36, detach, pinch, split }],
})
overlayPath.setAttribute('d', d)
}
A path from includeBody: false closes inside the host. To stroke it, clip away the part inside the host and break the host's own border along spans.
Input
FusionSurfaceGeometryInput:
| Field | Type | Default | Description |
|---|---|---|---|
width / height | number | - | Body size in px; nothing is drawn unless both are positive. |
radius | number | 16 | Body corner radius, capped at half the shorter side. |
buds | FusionSurfaceBudShape[] | [] | The buds as they are on this frame. |
includeBody | boolean | true | false outputs only the buds, necks, and drops. |
baseOverlap | number | 2 | With includeBody: false, how far (px) each attached shape reaches into the body to cover the host's border. |
FusionSurfaceBudShape takes the other fields of FusionSurfaceBud, but describes the current frame rather than a target:
| Field | Type | Default | Description |
|---|---|---|---|
height | number | - | Current outward height; grow it from 0 to open, and under 0.5 px nothing is drawn. After a split, the drop's height before split.scale. |
fillet | number | 12 | Largest concave fillet radius. |
pinch | number | 0 | How far the neck has narrowed (0..1); 1 closes it to a point. |
split | FusionSurfaceSplit | null | null | Set once the neck snaps; pinch is ignored from then on. |
FusionSurfaceSplit:
| Field | Type | Default | Description |
|---|---|---|---|
center / width / height / detach / drift | number | - | The bud on the frame it snapped; moving the bud afterwards moves only the drop. |
remnant | number | - | Progress of the stub sinking into the body edge, from 1 down to 0. |
tail | number | - | Progress of the drop's pointed end rounding off, from 1 down to 0. |
scale | number | 1 | Scale of the drop about its own center; the component lowers it to 0 as a split bud closes. |
Output
FusionSurfaceGeometry:
| Field | Type | Description |
|---|---|---|
d | string | Closed clockwise subpaths to 2 decimals: the body with attached buds merged in, then one per drop. Never contains NaN or Infinity. |
spans | FusionSurfaceSpan[] | { id, edge, from, to }: where each attached bud, or its stub, meets the body edge. Break a host's border here. |
rects | FusionSurfaceRect[] | { id, edge, x, y, width, height }: each bud's current outer box, the drop's after a split; the component places bud content from it. |
Helpers
fusionSurfacePinch(detach, breakAt): the component's neck curve; 0 for the first quarter ofbreakAt, then easing in to 1.FUSION_SURFACE_BREAK_PINCH:0.985, the pinch at which the component latches the split.springSteps(position, velocity, target, config, dt): the component's spring integrator;dtis in seconds, and it returns[position, velocity].
Overview
- The silhouette is an SVG inside the root at
z-index: -1,pointer-events: noneandaria-hidden, under the slot content. - A bud added to
budsgrows in place; a removed bud closes first, its content still mounted andinert. open,center,width,height,detach, anddriftare springs with their own velocity, so a new target bends the motion instead of restarting it.- A split is latched: lowering
detachnever rejoins it. Closing a split bud shrinks the drop and its content about the drop's center. shadowskipsinsetand spread layers; avar()layer passes through whole and must hold one layer, like--tx-elevation-*.- Under reduced motion, tracked live, every spring lands in one frame; a pull past the break still fires
break.
Technologies
- One
requestAnimationFrameloop per surface integrates the shared springs (liquid/src/spring.ts), recomputes one sharp SVG path, and writes bud layer styles directly; it sleeps at rest. - The attached bud follows the Dock on uiarc.dev, whose technique was observed and no code taken; the neck is this library's own profile model.
- Source:
packages/tuffex/packages/components/src/fusion-surface/.
Use cases
- A toolbar or dock whose tools grow option trays, submenus, or tooltips out of the bar.
- A panel that grows a bubble or badge from its edge and lets it go.
- Sending a message: the composer pinches it off into the thread; over a host you draw yourself, use
fusionSurfacePath().
Related components
| Component | For |
|---|---|
| Fusion | Two slots fused by a goo filter |
| Liquid | Free pieces that merge like droplets |
| BorderBeam | A glow along an element's border |