mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 20:16:52 -06:00
Merge branch 'worktree-agent-a128c7366e475da55'
This commit is contained in:
@@ -8,6 +8,7 @@ pub mod build;
|
||||
pub mod props;
|
||||
pub mod render_queue;
|
||||
pub mod styles;
|
||||
pub mod tracking;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -170,6 +170,10 @@ pub enum GroupKind {
|
||||
/// Effect spec id, e.g. `ec.blur.gaussian`.
|
||||
effect: String,
|
||||
},
|
||||
/// A motion tracker (Motion Trackers ▸ Tracker n) with its Tracker panel settings.
|
||||
Tracker {
|
||||
settings: Box<crate::tracking::TrackerSettings>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
//! Motion trackers in the property tree, laid out like After Effects':
|
||||
//!
|
||||
//! ```text
|
||||
//! Motion Trackers (match `motionTrackers`, first group of the layer)
|
||||
//! Tracker 1 (GroupKind::Tracker: type, target, options)
|
||||
//! Track Point 1 (match `trackPoint`)
|
||||
//! Feature Center, Feature Size, Search Offset, Search Size,
|
||||
//! Confidence, Attach Point, Attach Point Offset
|
||||
//! ```
|
||||
//!
|
||||
//! Feature Center, Confidence and Attach Point get one keyframe per analysed frame (layer time).
|
||||
//! The tracking algorithms live in `effectcraft-track`; applying tracks lives in the engine.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::build::Ids;
|
||||
use crate::props::{GroupKind, ParamUi, PropGroup};
|
||||
use crate::{Layer, LayerId, Uid};
|
||||
|
||||
/// Tracker panel ▸ Track Type.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TrackKind {
|
||||
Stabilize,
|
||||
#[default]
|
||||
Transform,
|
||||
/// Parallel (affine) corner pin: three points, the fourth corner completes the parallelogram.
|
||||
#[serde(alias = "parallel", alias = "parallelCornerPin")]
|
||||
Affine,
|
||||
/// Perspective corner pin: four points.
|
||||
#[serde(alias = "perspectiveCornerPin")]
|
||||
Perspective,
|
||||
/// Raw: track data only (nothing to apply).
|
||||
Raw,
|
||||
}
|
||||
|
||||
impl TrackKind {
|
||||
pub const ALL: [TrackKind; 5] = [TrackKind::Stabilize, TrackKind::Transform, TrackKind::Affine, TrackKind::Perspective, TrackKind::Raw];
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
TrackKind::Stabilize => "Stabilize",
|
||||
TrackKind::Transform => "Transform",
|
||||
TrackKind::Affine => "Parallel Corner Pin",
|
||||
TrackKind::Perspective => "Perspective Corner Pin",
|
||||
TrackKind::Raw => "Raw",
|
||||
}
|
||||
}
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
TrackKind::Stabilize => "stabilize",
|
||||
TrackKind::Transform => "transform",
|
||||
TrackKind::Affine => "affine",
|
||||
TrackKind::Perspective => "perspective",
|
||||
TrackKind::Raw => "raw",
|
||||
}
|
||||
}
|
||||
pub fn from_name(s: &str) -> Option<TrackKind> {
|
||||
let n = s.to_ascii_lowercase().replace([' ', '_', '-'], "");
|
||||
match n.as_str() {
|
||||
"parallel" | "parallelcornerpin" | "affinecornerpin" => return Some(TrackKind::Affine),
|
||||
"perspectivecornerpin" => return Some(TrackKind::Perspective),
|
||||
_ => {}
|
||||
}
|
||||
TrackKind::ALL.into_iter().find(|k| k.id() == n || k.label().to_ascii_lowercase().replace(' ', "") == n)
|
||||
}
|
||||
pub fn is_corner_pin(self) -> bool {
|
||||
matches!(self, TrackKind::Affine | TrackKind::Perspective)
|
||||
}
|
||||
/// Number of track points for this type (Transform/Stabilize: 2 with rotation or scale).
|
||||
pub fn point_count(self, rotation_or_scale: bool) -> usize {
|
||||
match self {
|
||||
TrackKind::Affine => 3,
|
||||
TrackKind::Perspective => 4,
|
||||
_ if rotation_or_scale => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Motion Tracker Options ▸ Channel.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TrackChannel {
|
||||
Rgb,
|
||||
#[default]
|
||||
Luminance,
|
||||
Saturation,
|
||||
}
|
||||
|
||||
/// Motion Tracker Options ▸ "If Confidence is Below … %".
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum LowConfidence {
|
||||
Continue,
|
||||
Stop,
|
||||
Extrapolate,
|
||||
#[default]
|
||||
Adapt,
|
||||
}
|
||||
|
||||
/// Motion Tracker Options.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct TrackerOptions {
|
||||
pub channel: TrackChannel,
|
||||
/// Process Before Match ▸ Blur radius in pixels (0 = off).
|
||||
pub blur: f64,
|
||||
/// Process Before Match ▸ Enhance.
|
||||
pub enhance: bool,
|
||||
pub subpixel: bool,
|
||||
/// Adapt Feature on Every Frame.
|
||||
pub adapt_every_frame: bool,
|
||||
/// Confidence threshold, percent.
|
||||
pub threshold: f64,
|
||||
pub action: LowConfidence,
|
||||
/// Follow each feature's rotation and scale while matching (EffectCraft extension).
|
||||
pub track_shape: bool,
|
||||
}
|
||||
|
||||
impl Default for TrackerOptions {
|
||||
fn default() -> Self {
|
||||
TrackerOptions {
|
||||
channel: TrackChannel::Luminance,
|
||||
blur: 0.0,
|
||||
enhance: false,
|
||||
subpixel: true,
|
||||
adapt_every_frame: false,
|
||||
threshold: 80.0,
|
||||
action: LowConfidence::Adapt,
|
||||
track_shape: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A tracker's non-animatable settings (Tracker panel).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct TrackerSettings {
|
||||
pub kind: TrackKind,
|
||||
pub position: bool,
|
||||
pub rotation: bool,
|
||||
pub scale: bool,
|
||||
/// Motion Target (None: no target; Stabilize always applies to the tracked layer).
|
||||
pub target: Option<LayerId>,
|
||||
pub options: TrackerOptions,
|
||||
}
|
||||
|
||||
impl TrackerSettings {
|
||||
pub fn new(kind: TrackKind) -> TrackerSettings {
|
||||
TrackerSettings { kind, position: true, ..Default::default() }
|
||||
}
|
||||
pub fn point_count(&self) -> usize {
|
||||
self.kind.point_count(self.rotation || self.scale)
|
||||
}
|
||||
}
|
||||
|
||||
/// Initial regions of a track point (layer pixels).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PointInit {
|
||||
pub center: [f64; 2],
|
||||
pub feature_size: [f64; 2],
|
||||
pub search_offset: [f64; 2],
|
||||
pub search_size: [f64; 2],
|
||||
pub attach_offset: [f64; 2],
|
||||
}
|
||||
|
||||
pub const MOTION_TRACKERS: &str = "motionTrackers";
|
||||
pub const TRACK_POINT: &str = "trackPoint";
|
||||
|
||||
/// The (empty) Motion Trackers group.
|
||||
pub fn motion_trackers(ids: &mut Ids) -> PropGroup {
|
||||
ids.group(MOTION_TRACKERS, "Motion Trackers")
|
||||
}
|
||||
|
||||
/// One Track Point group.
|
||||
pub fn track_point(ids: &mut Ids, name: &str, p: &PointInit) -> PropGroup {
|
||||
let pt = |ids: &mut Ids, m: &str, n: &str, v: [f64; 2]| ids.prop(m, n, Value::Vec2(v)).with_ui(ParamUi::Point).spatial();
|
||||
let vec = |ids: &mut Ids, m: &str, n: &str, v: [f64; 2]| ids.prop(m, n, Value::Vec2(v)).with_ui(ParamUi::Point);
|
||||
let attach = [p.center[0] + p.attach_offset[0], p.center[1] + p.attach_offset[1]];
|
||||
let fc = pt(ids, "featureCenter", "Feature Center", p.center);
|
||||
let fs = vec(ids, "featureSize", "Feature Size", p.feature_size);
|
||||
let so = vec(ids, "searchOffset", "Search Offset", p.search_offset);
|
||||
let ss = vec(ids, "searchSize", "Search Size", p.search_size);
|
||||
let cf = ids.prop("confidence", "Confidence", Value::Scalar(0.0)).with_ui(ParamUi::Percent);
|
||||
let ap = pt(ids, "attachPoint", "Attach Point", attach);
|
||||
let ao = vec(ids, "attachPointOffset", "Attach Point Offset", p.attach_offset);
|
||||
let mut g = ids.group(TRACK_POINT, name).with(fc).with(fs).with(so).with(ss).with(cf).with(ap).with(ao);
|
||||
g.kind = GroupKind::Indexed;
|
||||
g
|
||||
}
|
||||
|
||||
/// A Tracker group with its points.
|
||||
pub fn tracker(ids: &mut Ids, name: &str, settings: TrackerSettings, points: &[PointInit]) -> PropGroup {
|
||||
let mut g = ids.group("tracker", name);
|
||||
g.kind = GroupKind::Tracker { settings: Box::new(settings) };
|
||||
for (i, p) in points.iter().enumerate() {
|
||||
let tp = track_point(ids, &format!("Track Point {}", i + 1), p);
|
||||
g.children.push(tp.into());
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// Default track point layout for a tracker type, centred on a layer of size `size`.
|
||||
pub fn default_points(kind: TrackKind, n: usize, size: [f64; 2]) -> Vec<PointInit> {
|
||||
let m = size[0].min(size[1]).max(1.0);
|
||||
let f = (m * 0.04).clamp(16.0, 80.0).round();
|
||||
let s = (f * 2.0).round();
|
||||
let c = [size[0] / 2.0, size[1] / 2.0];
|
||||
let mk = |p: [f64; 2]| PointInit { center: p, feature_size: [f, f], search_offset: [0.0; 2], search_size: [s, s], attach_offset: [0.0; 2] };
|
||||
let (dx, dy) = ((size[0] * 0.18).max(2.0 * s), (size[1] * 0.18).max(2.0 * s));
|
||||
match (kind, n) {
|
||||
(TrackKind::Perspective, _) | (_, 4) => {
|
||||
vec![mk([c[0] - dx, c[1] - dy]), mk([c[0] + dx, c[1] - dy]), mk([c[0] - dx, c[1] + dy]), mk([c[0] + dx, c[1] + dy])]
|
||||
}
|
||||
(TrackKind::Affine, _) | (_, 3) => vec![mk([c[0] - dx, c[1] - dy]), mk([c[0] + dx, c[1] - dy]), mk([c[0] - dx, c[1] + dy])],
|
||||
(_, 2) => vec![mk([c[0] - dx, c[1]]), mk([c[0] + dx, c[1]])],
|
||||
_ => vec![mk(c)],
|
||||
}
|
||||
}
|
||||
|
||||
impl Layer {
|
||||
/// The Motion Trackers group, if the layer has one.
|
||||
pub fn motion_trackers(&self) -> Option<&PropGroup> {
|
||||
self.props.sub(MOTION_TRACKERS)
|
||||
}
|
||||
/// Trackers of the layer (groups with [`GroupKind::Tracker`]).
|
||||
pub fn trackers(&self) -> impl Iterator<Item = (&PropGroup, &TrackerSettings)> {
|
||||
self.motion_trackers().into_iter().flat_map(|g| g.groups()).filter_map(|g| match &g.kind {
|
||||
GroupKind::Tracker { settings } => Some((g, settings.as_ref())),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
pub fn tracker(&self, uid: Uid) -> Option<(&PropGroup, &TrackerSettings)> {
|
||||
self.trackers().find(|(g, _)| g.uid == uid)
|
||||
}
|
||||
/// The Motion Trackers group, created (first in the layer's tree, like AE) when missing.
|
||||
pub fn motion_trackers_mut(&mut self, ids: &mut Ids) -> &mut PropGroup {
|
||||
if self.props.sub(MOTION_TRACKERS).is_none() {
|
||||
self.props.children.insert(0, motion_trackers(ids).into());
|
||||
}
|
||||
self.props.sub_mut(MOTION_TRACKERS).expect("motion trackers group")
|
||||
}
|
||||
}
|
||||
|
||||
impl PropGroup {
|
||||
/// Tracker settings of a tracker group.
|
||||
pub fn tracker_settings(&self) -> Option<&TrackerSettings> {
|
||||
match &self.kind {
|
||||
GroupKind::Tracker { settings } => Some(settings),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn tracker_settings_mut(&mut self) -> Option<&mut TrackerSettings> {
|
||||
match &mut self.kind {
|
||||
GroupKind::Tracker { settings } => Some(settings),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Track Point groups of a tracker group, in order.
|
||||
pub fn track_points(&self) -> impl Iterator<Item = &PropGroup> {
|
||||
self.groups().filter(|g| g.match_id == TRACK_POINT)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tracker_tree_and_serde() {
|
||||
let mut n = 1;
|
||||
let mut ids = Ids(&mut n);
|
||||
let pts = default_points(TrackKind::Perspective, 4, [1920.0, 1080.0]);
|
||||
let g = tracker(&mut ids, "Tracker 1", TrackerSettings::new(TrackKind::Perspective), &pts);
|
||||
assert_eq!(g.track_points().count(), 4);
|
||||
let tp = g.track_points().next().unwrap();
|
||||
let names: Vec<&str> = tp.props().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["Feature Center", "Feature Size", "Search Offset", "Search Size", "Confidence", "Attach Point", "Attach Point Offset"]);
|
||||
let json = serde_json::to_string(&g).unwrap();
|
||||
let back: PropGroup = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, g);
|
||||
assert_eq!(back.tracker_settings().unwrap().kind, TrackKind::Perspective);
|
||||
assert_eq!(TrackKind::from_name("Parallel Corner Pin"), Some(TrackKind::Affine));
|
||||
assert_eq!(TrackKind::from_name("perspective"), Some(TrackKind::Perspective));
|
||||
assert_eq!(TrackerSettings { rotation: true, ..TrackerSettings::new(TrackKind::Transform) }.point_count(), 2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user