mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 11:37:02 -06:00
Merge branch 'worktree-agent-a549a3d1a8812e372'
# Conflicts: # apps/effectcraft-cli/src/main.rs # crates/host/src/lib.rs # crates/render/src/lib.rs
This commit is contained in:
@@ -33,6 +33,16 @@ impl<'a> EvalCtx<'a> {
|
||||
}
|
||||
/// Value of a property at the context time (keyframes, then expression).
|
||||
pub fn value(&self, layer: &Layer, prop: &Property) -> Value {
|
||||
// Separated Position reads as the combination of X/Y/Z Position.
|
||||
if prop.match_id == "position"
|
||||
&& let Some(tr) = layer.transform()
|
||||
&& let Some(px) = tr.get("positionX")
|
||||
&& tr.get("position").is_some_and(|p| p.uid == prop.uid)
|
||||
{
|
||||
let z = tr.get("positionZ").map(|p| self.value(layer, p).as_f64()).unwrap_or(0.0);
|
||||
let y = tr.get("positionY").map(|p| self.value(layer, p).as_f64()).unwrap_or(0.0);
|
||||
return Value::Vec3([self.value(layer, px).as_f64(), y, z]);
|
||||
}
|
||||
let lt = layer.layer_time(self.time);
|
||||
let v = prop.value_at(lt);
|
||||
if prop.has_expression()
|
||||
@@ -63,6 +73,21 @@ impl<'a> EvalCtx<'a> {
|
||||
pub fn b(&self, layer: &Layer, g: &PropGroup, m: &str) -> bool {
|
||||
self.group_value(layer, g, m).map(|v| v.as_bool()).unwrap_or(false)
|
||||
}
|
||||
/// Layer Position, honouring Separate Dimensions (X/Y/Z Position properties).
|
||||
pub fn position(&self, layer: &Layer, tr: &PropGroup) -> [f64; 3] {
|
||||
match tr.get("positionX") {
|
||||
Some(px) => [self.value(layer, px).as_f64(), self.f(layer, tr, "positionY", 0.0), self.f(layer, tr, "positionZ", 0.0)],
|
||||
None => self.v3(layer, tr, "position", [0.0; 3]),
|
||||
}
|
||||
}
|
||||
/// Source time of a layer at the context time: Time Remap's value when enabled, else the
|
||||
/// (stretch-aware) layer time.
|
||||
pub fn source_time(&self, layer: &Layer) -> Tick {
|
||||
if let Some(tr) = layer.props.get("timeRemap") {
|
||||
return Tick::from_seconds_f64(self.value(layer, tr).as_f64());
|
||||
}
|
||||
layer.layer_time(self.time)
|
||||
}
|
||||
pub fn layer(&self, id: LayerId) -> Option<&'a Layer> {
|
||||
self.comp.layer(id)
|
||||
}
|
||||
@@ -71,7 +96,7 @@ impl<'a> EvalCtx<'a> {
|
||||
pub fn local_matrix(&self, layer: &Layer) -> Mat4 {
|
||||
let Some(tr) = layer.transform() else { return Mat4::IDENTITY };
|
||||
let three = layer.is_3d();
|
||||
let pos = self.v3(layer, tr, "position", [0.0; 3]);
|
||||
let pos = self.position(layer, tr);
|
||||
let rz = self.f(layer, tr, "rotation", 0.0);
|
||||
if layer.is_camera() || layer.is_light() {
|
||||
return Mat4::translate(Vec3::from(pos)) * Mat4::rotate_z(rz);
|
||||
|
||||
@@ -201,7 +201,7 @@ impl<'a> Renderer<'a> {
|
||||
return None;
|
||||
}
|
||||
let sub = Renderer { depth: self.depth + 1, ..*self };
|
||||
let lt = layer.layer_time(ctx.time);
|
||||
let lt = ctx.source_time(layer);
|
||||
Some(Buf { img: sub.comp_frame(*item, lt), offset: [0.0; 2], scale: s })
|
||||
}
|
||||
LayerSource::Footage { item } => {
|
||||
@@ -210,7 +210,7 @@ impl<'a> Renderer<'a> {
|
||||
if !f.has_video {
|
||||
return None;
|
||||
}
|
||||
let lt = layer.layer_time(ctx.time);
|
||||
let lt = ctx.source_time(layer);
|
||||
let img = self.footage.frame(*item, f, lt)?;
|
||||
let k = if img.width > 0 { f.width.max(1) as f64 / img.width as f64 } else { 1.0 };
|
||||
// Keep native pixels when downsampling is small; resample otherwise.
|
||||
@@ -392,6 +392,8 @@ pub fn render_frame(project: &Project, comp: ItemId, t: Tick, scale: f64) -> Ima
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_remap;
|
||||
|
||||
/// Layer-space bounds `[x0, y0, x1, y1]` of a layer's content at the context time (source size
|
||||
/// for solids/footage/precomps, glyph bounds for text, painted bounds for shapes). Used for viewer
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Time remapping and separated Position dimensions.
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_keyframe::{Keyframe, Value};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{Comp, ItemKind, LayerSource, Node, Project, Solid};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::render_frame;
|
||||
|
||||
fn s(x: f64) -> Tick {
|
||||
Tick::from_seconds_f64(x)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separated_position_drives_the_transform() {
|
||||
let mut p = Project::default();
|
||||
let comp = Comp::new(200, 100, FrameRate::FPS_30, s(2.0));
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
||||
let sid = p.add_item("S", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 20, height: 20, pixel_aspect: 1.0 }));
|
||||
let mut l = build::layer(&mut p, &comp, "S", LayerSource::Solid { item: sid }, (20, 20), None);
|
||||
let mut next = p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let mut px = ids.prop("positionX", "X Position", Value::Scalar(20.0));
|
||||
px.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(20.0)), Keyframe::new(s(1.0), Value::Scalar(180.0))];
|
||||
let py = ids.prop("positionY", "Y Position", Value::Scalar(50.0));
|
||||
let pz = ids.prop("positionZ", "Z Position", Value::Scalar(0.0));
|
||||
p.next_id = next;
|
||||
let tr = l.props.sub_mut("transform").unwrap();
|
||||
// The combined property is ignored while separated.
|
||||
tr.get_mut("position").unwrap().value = Value::Vec3([100.0, 10.0, 0.0]);
|
||||
tr.children.extend([Node::Prop(px), Node::Prop(py), Node::Prop(pz)]);
|
||||
p.comp_mut(cid).unwrap().layers.push(l);
|
||||
let a = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
assert!(a.get(20, 50)[3] > 0.99 && a.get(100, 10)[3] < 0.01);
|
||||
let b = render_frame(&p, cid, s(0.5), 1.0);
|
||||
assert!(b.get(100, 50)[3] > 0.99 && b.get(20, 50)[3] < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_remap_picks_the_source_frame() {
|
||||
let mut p = Project::default();
|
||||
let outer = Comp::new(100, 100, FrameRate::FPS_30, s(4.0));
|
||||
let mut inner = Comp::new(100, 100, FrameRate::FPS_30, s(4.0));
|
||||
// Inner comp: a solid that moves from x=10 (t=0) to x=90 (t=2).
|
||||
let sid = p.add_item("S", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 10, height: 10, pixel_aspect: 1.0 }));
|
||||
let mut l = build::layer(&mut p, &inner, "S", LayerSource::Solid { item: sid }, (10, 10), None);
|
||||
l.props.prop_mut("transform/position").unwrap().keys =
|
||||
vec![Keyframe::new(Tick::ZERO, Value::Vec3([10.0, 50.0, 0.0])), Keyframe::new(s(2.0), Value::Vec3([90.0, 50.0, 0.0]))];
|
||||
inner.layers.push(l);
|
||||
let iid = p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
|
||||
let cid = p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(outer.clone().into()));
|
||||
let mut pre = build::layer(&mut p, &outer, "Inner", LayerSource::Comp { item: iid }, (100, 100), None);
|
||||
let mut next = p.next_id;
|
||||
// Freeze on source time 2 s.
|
||||
let tr = Ids(&mut next).prop("timeRemap", "Time Remap", Value::Scalar(2.0));
|
||||
p.next_id = next;
|
||||
let mut tr = tr;
|
||||
tr.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(2.0)).hold()];
|
||||
pre.props.children.push(Node::Prop(tr));
|
||||
p.comp_mut(cid).unwrap().layers.push(pre);
|
||||
let img = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
assert!(img.get(90, 50)[3] > 0.99, "frozen at the end position");
|
||||
assert!(img.get(10, 50)[3] < 0.01);
|
||||
}
|
||||
Reference in New Issue
Block a user