Merge pull request #289 from storytold/fix/227-shape-effect-points

Effects: effect points on shape layers start at the layer centre (#227)

# Conflicts:
#	crates/ui-egui/src/panels/puppet_tool.rs
#	crates/ui-egui/src/panels/viewer.rs
This commit is contained in:
Brandon Thomas
2026-10-09 09:32:13 -04:00
36 changed files with 684 additions and 154 deletions
+11 -2
View File
@@ -242,6 +242,15 @@ build real projects through these interfaces; they are worked examples of everyt
Save / Don't Save / Cancel prompt (`ui_click` on `dialog.unsaved.save` / `dontSave` / `cancel`;
`dialog.unsaved.revert` for Revert); the control channel's `app.quit {"force": true}` skips it.
### Effect positions
Effect points, Puppet pins (`puppet.addPin`, `puppet.recordPin`), Paint, Liquify and Roto Brush
strokes and `effect.pickColor` are in **effect space**: layer pixels from the top-left of the
layer's effect bounds. For footage, solids and precomps that is the layer's own pixel grid. Shape
and text layers have comp-sized bounds centred on their content's origin, so in a `w` × `h` comp
the point `[w / 2, h / 2]` is that origin (a new effect's default point). `prop.get` / `prop.set`
read and write these values as they are.
### Motion tracking
Trackers live on the tracked layer (`Motion Trackers ▸ Tracker n ▸ Track Point n`) and are driven
@@ -372,7 +381,7 @@ effectcraft-cli run shot.ecproj camera.createFromSolve '{"kind":"solid","points"
The Roto Brush tool (Alt+W cycles Roto Brush / Refine Edge) paints in the Layer panel; agents use
`roto.stroke {layer, kind: fg|bg|refine|refineErase, points: [[x, y], …], frame?, radius?}`
(layer pixels, layer frames). The first foreground stroke applies the Roto Brush & Refine Edge
(effect space, layer frames). The first foreground stroke applies the Roto Brush & Refine Edge
effect and sets the base frame with a span of 20 frames each side. `roto.propagate {layer,
direction?: forward|backward|both, to?, wait?}` segments the span (in the background unless
`wait`); strokes on any other frame correct it and propagation restarts from there.
@@ -477,7 +486,7 @@ that closes the dialog). File ▸ Scripts: `file.scripts.list`, `file.runScript
states as a branch); `edit.history.goto {"index": n}` (or `id`, or `steps`) jumps to any of
them. MCP: the `history` tool.
* `puppet.recordPin {"layer": "#1", "pin": "Puppet Pin 1", "samples": [[t, x, y]…]}` records a
drag (t = seconds since it began, layer space) into Position keys at the comp frame rate from the
drag (t = seconds since it began, effect space) into Position keys at the comp frame rate from the
current time; `puppet.recordOptions {speed, smoothing, useDraftDeformation, showMesh}`.
* `puppet.selectPins {"layer": "#1", "pins": ["Puppet Pin 1"], "add"?, "toggle"?}` selects pins
like the viewer's click / Shift-click (`pins: []` deselects); Edit ▸ Clear (Delete) then removes
+34 -1
View File
@@ -82,13 +82,46 @@ a file that a newer EffectCraft saved shows a warning: fields this version doesn
dropped if it saves the project. Save, Save As, Save a Copy and Increment and Save keep the
file's format (`.ecprojx` stays XML).
`schema` changes when a saved value changes meaning; a project from an older schema is converted
when it is opened (`effectcraft_engine::upgrade_effects`, which also refreshes effect instances
from the registry) and saved with the current one, which older versions refuse to open rather than
render wrongly. Schema 2 put effect positions on shape and text layers in effect space (§4,
#227). Opening a schema 1 project moves what an effect on a shape or text layer (not an adjustment
layer, whose effects see the comp below as before) holds as a position by half the comp size,
so points that were set keep their place in the comp: point parameters with their keyframes, Paint
stroke paths and clone positions, Puppet mesh seeds, pin rest points and positions, and Liquify
and Roto Brush strokes. A point parameter still at its default and not animated is left alone:
the old default sat at the comp's bottom-right corner, where most effects did nothing, and the
same value is now the layer centre, as in After Effects. Expression controls (Point Control…)
keep their values, because their meaning is up to the expressions that read them, and expressions
are not rewritten, so an expression that computes an effect point on a shape or text layer from
other values may need `+ [thisComp.width / 2, thisComp.height / 2]`. Effects that use the layer
bounds (edge pinning, wipes, Offset, generators) now cover the whole comp-sized bounds instead of
their bottom-right quarter. Tracking data of effects that analyse footage (Warp Stabilizer, 3D
Camera Tracker, Mocha shape) is left as it is: footage has a source rectangle, whose effect space
did not change.
## 4. Rendering
`Renderer::comp_frame(comp, t)` walks the layers bottom to top. For each visible layer it renders
the **source** (solid, footage frame, text, shape contents, or a nested comp), applies **masks**,
then **effects** in order, then **layer styles**, then the **transform** into comp space (with motion blur sub-samples
when enabled), the **track matte**, and finally **blends** into the accumulator. Adjustment layers
apply their effects to the accumulator. Runs of 3D layers are composited per pixel through the
apply their effects to the accumulator.
Effects run in **effect space** (`effectcraft_render::effect_bounds`): layer pixels measured from
the top-left of the layer's effect bounds, which are the source rectangle, or for layers without
one (shape, text) a comp-sized rectangle centred on the layer's origin that their content
surrounds. A shape layer's default effect point (the bounds' centre) is therefore its content's
origin, as in After Effects. The renderer moves the layer buffer into effect space for the whole
stack (`Buf::rebase`, on the CPU and the GPU alike) and hands effects everything else in it too:
masks, other layers' pixels for layer parameters (each in its own effect space), the layer's own
frames at other times (`Renderer::layer_input`, also what Puppet, Roto Brush and the Layer panel
read) and the comp camera. Effect point handles, Puppet pins and the Layer panel map effect space
to the comp with `EvalCtx::effect_to_comp`. Adjustment layers run their effects on the comp below
as it is.
Runs of 3D layers are composited per pixel through the
active camera, with lights, shadows and depth of field (Classic 3D), or rasterised as meshes by
the Advanced 3D renderer (below).
+9
View File
@@ -108,6 +108,15 @@ The most important missing piece: it turns every other estimate here into a meas
- Regression evidence for the Turbulent Displace, Wave Warp and Bulge pinning of #227:
`edge_pinning_displaces_all_of_a_centred_shape_layer` (render) and
`edge_pinning_on_a_shape_layer` (GPU against CPU).
- Regression evidence for the effect points of #227 (shape and text layers measure effect points
from the top-left of their comp-sized bounds, so Twirl, Bulge, CC Lens, Ripple and Turbulent
Displace's Offset default to the layer centre): `default_effect_points_sit_at_the_centre_of_a_shape_layer`
and `layer_parameters_see_a_whole_shape_layer` (render), `effect_points_on_a_shape_layer` (GPU
against CPU), and the schema 1 → 2 conversion in
`opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space` (engine) and
`effect_points_move_into_effect_space_unless_left_at_their_default` /
`puppet_paint_liquify_and_roto_data_move_into_effect_space` (effects); see
[architecture.md](architecture.md) §3 for what the conversion keeps.
- Regression evidence for the interface items of [#227](https://github.com/storytold/effectcraft/issues/227),
checked headlessly on Windows: `spacebar_taps_preview_and_held_spacebar_pans_the_viewer` and
`spacebar_drag_scrolls_the_time_graph` (Spacebar previews on its release, held it is the Hand
+1 -1
View File
@@ -549,7 +549,7 @@ Since (M5.16): Key Light (After Effects' Keylight 1.2) is checked end to end on
plate with an uneven screen, spill and soft edges (`effect.apply "Keylight (1.2)"`, pick, Clip
Black / White, render). The colour eyedroppers of Keying effects sample the effect's input
rather than the keyed frame (Ctrl/Cmd+click averages 5 × 5 pixels); agents do the same with
`effect.pickColor {effect, param, x, y, average?}` (layer pixels). Searching "keylight" in
`effect.pickColor {effect, param, x, y, average?}` (effect space). Searching "keylight" in
Effects & Presets, or `list_effects`, finds it.
## Update: M4.9–M4.11 nested comps and motion blur