Fade the draft into the sharp frame when a drag settles

When a gesture stopped, the half-resolution draft was replaced by the
full-resolution frame in one step, a visible jump from soft to sharp.
FR-DSP-4 asks for a refinement that is smooth, not a jarring swap.

The canvas now keeps the last draft frame (`canvas-previous`) and draws
it over the sharp one, fading it out over 150 ms when the draft flag
clears. The fade costs no render: the draft is a refcount on the texture
it was drawn into, and the adjust pass ping-pongs between two output
targets, so the sharp frame is written into the other one. While a
gesture is drafting the layer is hidden and snapped opaque, so a new
drag shows its draft at once; past the fade it is hidden again, and a
settled canvas composites one image as before.
This commit is contained in:
2026-09-24 21:52:23 -04:00
parent 4642c77e18
commit ade627a5d0
4 changed files with 150 additions and 40 deletions
+45
View File
@@ -102,6 +102,16 @@ export component AppWindow inherits Window {
/// TRACES: FR-DSP-4
/// Whether `canvas` is a draft: half resolution, rendered mid-gesture.
in property <bool> canvas-draft;
/// The last draft frame, kept for the crossfade into the sharp one.
///
/// Set only on draft frames, so on the frame that settles it still holds
/// the draft that frame replaces. No render is spent on it: it is the
/// texture the draft was already drawn into, and `AdjustPass` ping-pongs
/// between two targets, so the sharp frame is written into the *other*
/// one (and at a different size, which reallocates rather than
/// overwrites). The fade is the compositor blending two textures it
/// already holds.
in property <image> canvas-previous;
in property <string> adapter: "detecting…";
in property <string> backend: "—";
in property <string> inference-backend: "detecting…";
@@ -1640,6 +1650,41 @@ in property <bool> panel-visible: true;
visible: root.total > 0 && root.load-error == "";
}
// TRACES: FR-DSP-4
// **The draft, fading out over the sharp frame that
// replaced it**, so settling reads as the picture coming
// into focus rather than a swap.
//
// Hidden while drafting — it would be the same picture
// twice — and snapped to opaque there, so the fade always
// starts from the draft. Only the leaving transition is
// animated: a new gesture shows its draft at once. Past
// the fade it is transparent and hidden, so a settled
// canvas composites exactly one image, as before.
//
// It fades into whatever sharp frame follows a draft. That
// is the settle in every case but one: holding a
// comparison within the settle delay of a drag renders the
// original straight away, and the last draft of the edit
// fades out over it rather than vanishing.
previous-frame := Image {
width: 100%;
height: 100%;
source: root.canvas-previous;
image-fit: contain;
image-rendering: canvas-image.image-rendering;
opacity: 0;
visible: canvas-image.visible && !root.canvas-draft && self.opacity > 0;
states [
drafting when root.canvas-draft: {
opacity: 1;
out {
animate opacity { duration: 150ms; easing: ease-out; }
}
}
]
}
// The region map, composited over the photograph at the
// same fitted rect.
//