Skip to content

Shapes: edit gradient color and opacity stops (#612) - #694

Open
tylerjenningsw wants to merge 2 commits into
storytold:mainfrom
tylerjenningsw:enhance/gradient-editor
Open

tylerjenningsw wants to merge 2 commits into
storytold:mainfrom
tylerjenningsw:enhance/gradient-editor

Conversation

@tylerjenningsw

@tylerjenningsw tylerjenningsw commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #612.

The Timeline's Edit Gradient… on a gradient property (Gradient Fill / Gradient Stroke ▸ Colors, Gradient Overlay) only painted its label. It now opens a Gradient Editor.

What it does

  • Stops: opacity stops sit above a preview bar (with a checkerboard under it, so opacity shows) and colour stops below. Click above or below the bar to add a stop. A new stop takes its own track's value there: a colour stop the colour track's RGBA, an opacity stop the opacity track's alpha. A stop can be dragged, or moved by typing its Location.
  • Editing a stop: set its Opacity, or its Color with the shared colour picker every colour input uses (UI: one After Effects-style colour picker with a hex field for every colour input #638: hex field, HSB/RGB, OK and Cancel; the stop keeps its alpha). Delete removes it, but never the last stop of a track, and never while typing in a field.
  • Order and selection: the tracks stay in order while editing, so the preview and newly sampled stops are always right. The selection follows its stop when it moves past another.
  • One undo step: the editor works on a draft. OK sets the property once with prop.set at the composition time it opened, snapped to the nearest frame, with timeBase: "comp". Offset and stretched layers therefore get the keyframe where it was opened. OK with no change does nothing, so it never adds a keyframe.
  • Cancel / Escape change nothing. Escape with the picker open closes only the picker and restores the colour it opened with.
  • Animated gradients: only the keyframe at that time changes, or one is added there. When the draft's stop counts no longer match a neighbouring keyframe's, the editor shows "These adjacent keyframes cannot interpolate because their stop counts differ." (Gradient keyframes only blend with equal counts.) The edit is still allowed, and the neighbours are untouched.
  • Agents: prop.editGradient {layer?, path|prop, comp?} is a command, so the control channel and MCP can open the editor. It checks the target is a gradient property before the UI opens anything, so a wrong target is an error, not success.
  • Saving: the draft is saved in the typed form, so a single colour stop (which plain-JSON gradients reject) and each stop's RGBA survive.

Shared colour picker

The dialog Escape handler only stepped aside for egui popups, and the shared picker isn't one, so in a modal dialog Escape with the picker open closed the dialog as well. The picker now marks the pass it was open in (color_picker::open_recently), and the handler leaves Escape to it. This is in its own commit. A test with the change reverted fails: Escape closes the editor.

Model

Gradient::color_at / opacity_at sample one track each. sample() multiplies opacity in, so feeding its result into a new stop would apply opacity twice (50% would become 25%).

Not in this PR

  • Effect Controls: no effect has a gradient parameter yet.
  • Opening the editor from inside the Layer Styles dialog. Its Gradient Overlay row says "Edit in the Timeline", which now works.
  • On-screen Start/End handles in the viewer (the issue's "clicking on the rectangle").
  • Linear/Radial buttons in the editor (Type is its own row), midpoints, a live preview in the viewer, and the toolbar's gradient defaults.

Checks (Windows 10, Rust 1.95, release; rebased onto main at 9a7d3e7, fresh build)

  • cargo fmt --check: passes.
  • cargo +1.95 clippy -p effectcraft-keyframe -p effectcraft-engine -p effectcraft-ui-egui --no-deps --all-targets --release -- -D warnings: passes.
  • effectcraft-keyframe unit tests: 33/33. New: per-track sampling with no double opacity, the typed one-stop and RGBA round trip.
  • ui-egui unit tests gradient_editor (4/4: stops stay in order and selected while moving, sampling, the baseline comparison, the warning rule) and i18n (catalogs): pass.
  • ui_gradient_value (1/1): fails on main with "no automation id timeline.prop.…value: the Timeline's Edit Gradient… isn't clickable", and passes here. It uses only APIs that exist on main.
  • ui_gradient_editor (12/12):
    • edits, OK and one undo, with recolouring through the shared picker: a click inside its colour field over the modal editor, a typed hex code, then closing it by clicking outside
    • the picker's Escape and Cancel restore the stop's colour and keep the editor and its other edits
    • Cancel and Escape
    • OK unchanged between two keyframes adds none
    • animated edits at a keyframe and between keyframes
    • offset and 200%-stretched layers
    • mismatched stop counts warn, and the neighbours stay
    • Delete, the last stop, and typing
    • a single colour stop saved and undone
    • a stop moved past its neighbour by Location, with the Opacity field in that same frame showing that stop's value (read from the accessibility tree)
    • a layer deleted while editing
    • prop.editGradient errors from execute_checked and through the UI
  • ui_project_delete (11/11) and ui_color_hex (8/8, the shared picker's own tests).
  • A headless snapshot (the snapshot example) of the editor opened by prop.editGradient on a rectangle's Gradient Fill.
  • The full cargo xtask ci wasn't run locally: it takes over 2.5 hours on this machine. On Windows it also stops on two failures that happen identically on main: effectcraft-cli's render_out_is_relative_to_the_working_directory (a \\?\ path prefix) and text's style_variations_change_advances_and_outlines (depends on an installed system font).

The Timeline's "Edit Gradient…" on a gradient property (Gradient Fill /
Gradient Stroke ▸ Colors, Gradient Overlay) only painted its label. It now
opens a Gradient Editor (prop.editGradient): opacity stops above a preview
bar and colour stops below. Click to add a stop (it takes that track's
own value there), drag or type a Location to move it, set its opacity or
colour (egui's colour picker), Delete removes it (never the last of a
track, never while typing). The tracks stay in order while editing and the
selection follows its stop.

The editor works on a draft. OK sets the property once with prop.set at
the composition time it opened, snapped to the frame, with
timeBase: "comp" (offset and stretched layers get the key where it was
opened): one undo step, and nothing at all when the draft is unchanged, so
OK adds no keyframe. Cancel and Escape change nothing; Escape with the
picker open closes only the picker. On an animated gradient only the
keyframe at that time changes; when its stop counts no longer match a
neighbouring keyframe's (gradients only blend with equal counts) the
editor says so. The draft is saved in the typed form, so one colour stop
and each stop's RGBA survive.

prop.editGradient checks its target is a gradient property before the UI
opens anything, so agents get an error for a wrong one. New Gradient
color_at / opacity_at sample one track each (sample() multiplies opacity
in, which would apply it twice to a new stop).

Tests: ui_gradient_value (fails on main: the value had no automation id),
ui_gradient_editor (11), gradient_editor and keyframe unit tests.
…ses only the picker (storytold#612)

The stop colour now uses widgets::srgb_color_button, the After Effects-style
picker every colour input opens (storytold#638): hex field, HSB/RGB fields, OK and
Cancel, sRGB as the project stores colours. The stop keeps its alpha.

The dialog Escape handler only stepped aside for egui popups, and this
picker isn't one, so in a modal dialog Escape with the picker open closed
the dialog too. The picker now marks the pass it was open in
(color_picker::open_recently), and the handler leaves Escape to it: Escape
restores the colour the picker opened with and closes only the picker.

Tests: ui_gradient_editor clicks inside the picker's colour field over the
modal editor, types a hex code, closes the picker by clicking outside it
and saves the stop; a new test checks that Escape and the picker's Cancel
restore the stop's colour and keep the editor and its other edits. That
test fails without the Escape change. ui_color_hex still passes.
@tylerjenningsw
tylerjenningsw force-pushed the enhance/gradient-editor branch from d987fc6 to 5c1484b Compare October 11, 2026 03:43

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gradient Edit

1 participant