Watch
1
0
Fork
You've already forked souveraine
0

motion: call the curve bank the shell has always carried

Appearance.animationCurves is Material 3 Expressive and every spatial
curve in it overshoots. Twelve souveraine surfaces, 31 hand-typed linear
animations, three calls into that bank from two files.

The card's entry rise is gated on inFlight; unguarded it is TASK-60's
second writer over the carry.
This commit is contained in:
Fimeg 2026-08-16 14:02:09 -04:00
commit 787075f47f
3 changed files with 76 additions and 13 deletions

View file

@ -309,16 +309,20 @@ PanelWindow {
NumberAnimation { target: handle; property: "scale"; to: 1.0; duration: 220; easing.type: Easing.InOutQuad }
}
// All three off the house bank rather than hand-typed linear ramps.
// Still gated on `!dragging` while the thumb is on the pill the
// handle tracks it directly and a Behavior would put a curve between
// the finger and the thing it is holding.
Behavior on width {
enabled: !rail.dragging
NumberAnimation { duration: 120 }
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
Behavior on color {
ColorAnimation { duration: 140 }
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
Behavior on y {
enabled: !rail.dragging
NumberAnimation { duration: 160; easing.type: Easing.OutCubic }
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
}

View file

@ -134,10 +134,13 @@ Item {
// and must not be smoothed, or the cards lag the fingers that are
// moving them.
enabled: root.open || ZoneTransition.travel === 0
NumberAnimation {
duration: 260
easing.type: Easing.OutCubic
}
// `elementMoveEnter` rather than a hand-typed OutCubic. The house
// curve bank is Material 3 Expressive and every one of its spatial
// curves overshoots the shell has carried it since ii and only
// `SubconsciousTicker` and `AgentMessage` had ever called it, so every
// navigation surface animated on a flat cubic. That is the missing
// whoosh, and it was a two-line import away the whole time.
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
// Ask the moment this becomes visible. Cheap, and the whole answer to why
@ -318,7 +321,27 @@ Item {
scale: 1 - 0.07 * zoneCard.depth
opacity: zoneCard.share * (1 - 0.4 * Math.min(1, zoneCard.depth))
Behavior on scale {
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
// The card arrives rather than fading in place.
//
// A separate transform, and deliberately with **no** Behavior of its
// own: `share` is continuous it is `progress` remapped so it is
// already smoothed by the Behavior on `progress` above. Folding it
// into `scale` would put a 350 ms animation under a value that
// changes every frame, which restarts the animation every frame and
// reads as lag rather than as motion. `depth` is step-like and is
// the one that wants a Behavior.
//
// Gated on `inFlight` for the same reason `depth` is: while the
// compositor carries a real window onto this rect, a card that also
// scales is TASK-60's two-writer fault exactly.
transform: Scale {
origin.x: zoneCard.width / 2
origin.y: zoneCard.height / 2
xScale: ZoneTransition.inFlight ? 1 : 0.90 + 0.10 * zoneCard.share
yScale: xScale
}
// No `scale` tied to the *gesture*, and that absence is the point of
@ -540,7 +563,9 @@ Item {
anchors.bottom: parent.bottom
anchors.margins: 12
height: ZoneTransition.trayHeight - 12
opacity: root.progress
// The verbs carry their own staggered fade now, so this band no longer
// multiplies one over the top of them squaring the ramp made the row
// arrive late and all at once, which is the opposite of the stagger.
transform: Translate { y: (1 - root.progress) * 24 }
Row {
@ -555,6 +580,7 @@ Item {
// where you left it which makes multitasking the only place it is
// findable, and therefore the only honest place to turn it off.
TrayVerb {
order: 0
symbol: root.subject && root.subject.floating === true
? "picture_in_picture_off" : "picture_in_picture"
caption: root.subject && root.subject.floating === true
@ -576,6 +602,7 @@ Item {
// `Some(w) if w >= count => self.workspaces.push()`, so asking for
// the zone past the end *is* the mint. One button, no new verb.
TrayVerb {
order: 1
symbol: "add_to_queue"
caption: qsTr("New zone")
active: root.subject !== null
@ -592,6 +619,7 @@ Item {
// a row of cards is the wrong place to take that choice away.
// Force lives on the sheet, behind a hold, where it is deliberate.
TrayVerb {
order: 2
symbol: "clear_all"
caption: qsTr("Close all")
active: root.restingZone !== null
@ -610,7 +638,8 @@ Item {
anchors.bottomMargin: 4
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
opacity: root.spoken.length > 0 ? 0.95 : 0.55
// Times `progress`, which the band used to apply for it.
opacity: root.progress * (root.spoken.length > 0 ? 0.95 : 0.55)
text: root.note
}
}
@ -623,12 +652,27 @@ Item {
id: verb
required property string symbol
required property string caption
// Where in the row this one sits, so the band arrives as a sentence
// rather than as a block. Named at the call site rather than taken from
// a Repeater index because the three verbs are three deliberate things,
// not a model.
required property int order
property bool active: true
signal triggered
width: 84
height: 78
opacity: verb.active ? 1 : 0.35
// Each verb starts 6% of the climb after the one before it, so the row
// lands left to right. Capped by the same `min(1, )` shape the cards
// use, so a fully open tray is fully opaque and not 94% of one.
readonly property real share: {
const start = verb.order * 0.06;
return Math.max(0, Math.min(1, (root.progress - start) / (1 - start)));
}
opacity: verb.share * (verb.active ? 1 : 0.35)
transform: Translate { y: (1 - verb.share) * 14 }
RippleButton {
id: hit
@ -639,6 +683,15 @@ Item {
buttonRadius: 26
enabled: verb.active
onClicked: verb.triggered()
// The bank's own bounce, which nothing outside the subconscious
// ticker had ever used. `expressiveDefaultSpatial` overshoots to
// 1.21, so the button comes back past its resting size and settles
// the difference between a control that acknowledges a thumb and
// one that merely changes colour under it.
scale: hit.down ? 0.90 : 1
Behavior on scale {
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: verb.symbol

View file

@ -51,8 +51,14 @@ Item {
border.color: Appearance.colors.colLayer1Active
scale: area.pressed ? 0.94 : 1.0
Behavior on scale { NumberAnimation { duration: 90 } }
Behavior on color { ColorAnimation { duration: 90 } }
// The house bounce, which overshoots on the way back a flat 90 ms
// ramp is a size change, not an acknowledgement.
Behavior on scale {
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
}
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
MaterialSymbol {
anchors.centerIn: parent