M13.5: stroke taper/wave/multi-dashes, radial gradient highlight, variable-width mask feather, adaptive motion-blur samples

Shapes (SHP-2): Stroke Taper (pixels/percent start/end length, width, ease) and
Wave (amount, pixels/cycles, wavelength, phase) rendered as a variable-width
outline; Dash 2/Gap 2/Dash 3/Gap 3 (shape.dashes.add/remove, timeline +/-
buttons); radial gradient Highlight Length/Angle as a focal gradient.
Masks (MSK-1): mask feather points on the Mask Path (mask.featherPoint.*,
Mask Feather tool add/drag), per-point signed radius and tension, rendered
from the signed distance to the path; carried through vertex edits.
Motion blur (LYR-10): Adaptive Sample Limit comp setting; 2D layers take one
sample per pixel of shutter travel between Samples Per Frame and the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-02 23:22:53 -04:00
co-authored by Claude Opus 5.5
parent c406b71cba
commit bbaee1fa0f
33 changed files with 1915 additions and 50 deletions
+2
View File
@@ -162,6 +162,8 @@ const SHAPE_PROPS: &[(&str, &str, &str)] = &[
("stroke", "join", "ADBE Vector Stroke Line Join"),
("stroke", "miter", "ADBE Vector Stroke Miter Limit"),
("stroke", "dashes", "ADBE Vector Stroke Dashes"),
("stroke", "taper", "ADBE Vector Stroke Taper"),
("stroke", "wave", "ADBE Vector Stroke Wave"),
("trim", "start", "ADBE Vector Trim Start"),
("trim", "end", "ADBE Vector Trim End"),
("trim", "offset", "ADBE Vector Trim Offset"),
+1
View File
@@ -130,6 +130,7 @@ fn item(s: &Session, a: &J) -> R {
"hideShyLayers": c.hide_shy, "motionBlur": c.enable_motion_blur, "frameBlending": c.enable_frame_blending,
"draft3d": c.draft_3d, "shutterAngle": c.shutter_angle, "shutterPhase": c.shutter_phase,
"motionBlurSamplesPerFrame": c.motion_blur_samples,
"motionBlurAdaptiveSampleLimit": c.motion_blur_adaptive_limit,
"renderer": match c.renderer { effectcraft_project::Renderer::Classic3D => "ADBE Classic 3D", effectcraft_project::Renderer::Advanced3D => "ADBE Advanced 3d" },
"open": s.state.open_comps.contains(&cid), "active": s.active_comp_id() == Some(cid),
"hasVideo": true, "hasAudio": c.layers.iter().any(|l| l.switches.audio && matches!(l.source, LayerSource::Footage { .. } | LayerSource::Comp { .. })),