Tug class

A glass surface on a short tether - a weight on a rubber leash, not a soap bubble.

The model, all lengths as ratios of the surface's shorter side so the gesture feels identical at any size:

  • The pull is measured from the surface's own CENTER, not from where the finger landed - grabbing an edge and sliding inward is still a pull, and re-grabbing a returning surface needs no bookkeeping. The offset FOLLOWS the finger on a critically damped chase spring, integrated frame by frame on the widget's own ticker (the comet pattern): pointer events only move the TARGET. Retargeting a controller per event starves the simulation - a high-frequency mouse restarts it before it ever ticks and the surface freezes; and without any follow spring a grab near the edge would teleport straight to full extension. At rest under a motionless finger the tick snaps to the target once and stops writing - a held button costs nothing per frame.
  • Inside a dead zone (dead, 0.24 of the side) nothing moves: a press that is only a press cannot shiver, and breaking free gives the pull a beginning.
  • Past it the offset is cap * tanh((d - dead) / cap) along the same vector, with cap only 0.16 of the side. Asymptotic - near 1:1 while small, never a wall. (Deliberately tanh and not the package's morphRubberband: tanh saturates noticeably faster, and this exact hand feel is the one that was tuned by eye.) Travel and stretch trade against each other, and the balance here leans hard toward shape: the surface is being stretched, not relocated.
  • The deformation locks to the SCREEN axes - no ellipse rotated into the drag. Each axis grows on its own displacement, squared (1 + stretch * e^2), and neither axis ever squashes: the surface moves as a whole and stretches a little, it does not get pressed into a lens. The growth extends TOWARD the pull: the reported offset carries half of each axis's growth (continuous - e * |e|, no sign flip at zero), so the trailing edge stays planted while the leading edge reaches for the finger. The offset is clamped as a VECTOR before it splits into axes - per-axis clamping makes the area breathe on a circular sweep. A small lean rides inside the scale.
  • The finger COMPRESSES glass: pointer-down sinks the surface to press on an overshoot-free spring (popping back out past rest is the single most bubble-like thing a button can do).
  • Release hands the chase velocity to the return springs, so a flick lands with its momentum. Grabbing mid-return seeds the chase from the return's own velocity - continuity in both directions.

Two ways to consume it:

  • Default: the pull paints as a render transform above child. A MorphTag inside rides along, so a flight launches from wherever the surface stands.
  • Channel mode (channel set): the pull is written straight into a MorphPieceChannel, so the skin moves REAL mass geometry - a MorphPiece inside a MorphSkin. The mass itself then carries the tether: pull one piece toward another and the skin necks them into one body, which no paint transform can do. The skin paints the content with the full channel transform (one rigid body); Tug corrects it from inside so the content rides the tether, sinks with the press in full, and follows only follow of the surface stretch - glass deforms what is printed on it, a little.
Inheritance

Constructors

Tug({Key? key, required Widget child, double dead = 0.24, double cap = 0.16, double stretch = 0.14, double lean = 3, double press = 0.97, double follow = 0.6, MorphMotion? motion, MorphPieceChannel? channel})
Creates a glass tether around child.
const

Properties

cap double
Tether cap as a ratio of the shorter side - the asymptote the travel approaches past the dead zone.
final
channel MorphPieceChannel?
When set, Tug writes the pull into this channel and the skin moves the real mass geometry; the widget itself paints only the content correction. Fixed for the widget's lifetime: toggling it swaps the tree shape. The channel's value is not reset on unmount - the owner of the channel owns its value.
final
child Widget
The tuggable surface.
final
dead double
Dead-zone radius as a ratio of the shorter side.
final
follow double
How much of the surface stretch the content rides in channel mode (the press sink always applies in full). Zero keeps the content rigid; one glues it to the glass.
final
hashCode int
The hash code for this object.
no setterinherited
key Key?
Controls how one widget replaces another widget in the tree.
finalinherited
lean double
Peak lean in degrees, signed by the horizontal pull.
final
motion MorphMotion?
Profile whose closeMotion plays the return; defaults to MorphMotion.normal.
final
press double
Scale while the pointer is down: glass compresses, never pops.
final
runtimeType Type
A representation of the runtime type of the object.
no setterinherited
stretch double
Growth at a full pull as a ratio of the SHORTER side, eased in quadratically. Both axes gain the same absolute amount, so a wide pill stretches no further than a tall one. A sideways pull widens only; a diagonal grows both by half.
final

Methods

createElement() StatefulElement
Creates a StatefulElement to manage this widget's location in the tree.
inherited
createState() State<Tug>
Creates the mutable state for this widget at a given location in the tree.
override
debugDescribeChildren() List<DiagnosticsNode>
Returns a list of DiagnosticsNode objects describing this node's children.
inherited
debugFillProperties(DiagnosticPropertiesBuilder properties) → void
Add additional properties associated with the node.
inherited
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
toDiagnosticsNode({String? name, DiagnosticsTreeStyle? style}) DiagnosticsNode
Returns a debug representation of the object that is used by debugging tools and by DiagnosticsNode.toStringDeep.
inherited
toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) String
A string representation of this object.
inherited
toStringDeep({String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) String
Returns a string representation of this node and its descendants.
inherited
toStringShallow({String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) String
Returns a one-line detailed description of the object.
inherited
toStringShort() String
A short, textual description of this widget.
inherited

Operators

operator ==(Object other) bool
The equality operator.
inherited