mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Comps expose controls (properties typed as text/color/slider/checkbox/point/angle/dropdown, Media Replacement, groups, comments); precomp layers get an Essential Properties group whose edits override per instance (Push to Comp / Revert); templates export/import as an open ZIP (manifest.json + project subset + media + poster). Stretched precomps keep protected regions at their original speed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
466 lines
20 KiB
Rust
466 lines
20 KiB
Rust
//! Lottie document → EffectCraft compositions (the inverse of [`crate::export`]).
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::sync::Arc;
|
|
|
|
use effectcraft_color::Label;
|
|
use effectcraft_keyframe::{ShapePath, Value};
|
|
use effectcraft_project::build::{self, Ids};
|
|
use effectcraft_project::{
|
|
AlphaMode, AutoOrient, Comp, Footage, FootageKind, GroupKind, ItemId, ItemKind, Layer, LayerId, LayerSource, Marker, MatteKind, Node, ParamUi, Project,
|
|
PropGroup, Property, Solid, TrackMatte,
|
|
};
|
|
use effectcraft_time::{FrameRate, Tick};
|
|
use serde_json::Value as Json;
|
|
|
|
use crate::anim::{self, Im, import_prop};
|
|
use crate::export::{fx_by_id, fx_by_type, fx_params, mask_mode_from};
|
|
use crate::{LottieError, Timebase, shapes, text};
|
|
|
|
/// An embedded image asset handed to the caller to store (the returned path becomes the
|
|
/// footage item's file).
|
|
#[derive(Clone, Debug)]
|
|
pub struct ImportedImage {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub bytes: Vec<u8>,
|
|
/// File extension from the data URI (`png`, `jpg`, …).
|
|
pub ext: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ImportResult {
|
|
/// The new top-level composition.
|
|
pub comp: ItemId,
|
|
/// Every item created (comps, solids, images, folder).
|
|
pub items: Vec<ItemId>,
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
fn f(j: &Json, k: &str) -> Option<f64> {
|
|
j.get(k).and_then(Json::as_f64)
|
|
}
|
|
|
|
fn s<'a>(j: &'a Json, k: &str) -> Option<&'a str> {
|
|
j.get(k).and_then(Json::as_str)
|
|
}
|
|
|
|
fn hex_color(sc: &str) -> [f32; 3] {
|
|
let h = sc.trim_start_matches('#');
|
|
let c = |i: usize| h.get(i..i + 2).and_then(|x| u8::from_str_radix(x, 16).ok()).unwrap_or(0) as f32 / 255.0;
|
|
[c(0), c(2), c(4)]
|
|
}
|
|
|
|
/// The Lottie JSON inside raw bytes (plain JSON, or a stored dotLottie archive).
|
|
fn document(bytes: &[u8]) -> Result<Json, LottieError> {
|
|
if bytes.starts_with(b"PK") {
|
|
let entries = crate::zip::read_stored(bytes);
|
|
let anim = entries
|
|
.iter()
|
|
.find(|(n, _)| n.starts_with("animations/") && n.ends_with(".json"))
|
|
.or_else(|| entries.iter().find(|(n, _)| n.ends_with(".json") && n != "manifest.json"))
|
|
.ok_or_else(|| LottieError::Parse("dotLottie archive without an uncompressed animation".into()))?;
|
|
return serde_json::from_slice(&anim.1).map_err(|e| LottieError::Parse(e.to_string()));
|
|
}
|
|
serde_json::from_slice(bytes).map_err(|e| LottieError::Parse(e.to_string()))
|
|
}
|
|
|
|
struct Ctx<'a> {
|
|
im: Im,
|
|
assets: BTreeMap<String, (ItemId, Option<(u32, u32)>)>,
|
|
fonts: Vec<(String, String, String)>,
|
|
store: &'a mut dyn FnMut(&ImportedImage) -> Option<String>,
|
|
base_dir: &'a str,
|
|
items: Vec<ItemId>,
|
|
folder: Option<ItemId>,
|
|
name: String,
|
|
}
|
|
|
|
impl Ctx<'_> {
|
|
fn folder(&mut self, project: &mut Project) -> ItemId {
|
|
if let Some(f) = self.folder {
|
|
return f;
|
|
}
|
|
let f = project.add_item(&format!("{} Assets", self.name), Label::None, None, ItemKind::Folder);
|
|
self.items.push(f);
|
|
self.folder = Some(f);
|
|
f
|
|
}
|
|
}
|
|
|
|
/// Import a Lottie document (JSON or a stored `.lottie` archive) into `project` as a new
|
|
/// composition. `store` saves embedded images and returns the file path to use;
|
|
/// `base_dir` resolves external image references.
|
|
pub fn import(
|
|
project: &mut Project,
|
|
bytes: &[u8],
|
|
fallback_name: &str,
|
|
base_dir: &str,
|
|
store: &mut dyn FnMut(&ImportedImage) -> Option<String>,
|
|
) -> Result<ImportResult, LottieError> {
|
|
let doc = document(bytes)?;
|
|
let (Some(w), Some(h)) = (f(&doc, "w"), f(&doc, "h")) else { return Err(LottieError::Parse("missing `w`/`h`".into())) };
|
|
let fr = f(&doc, "fr").filter(|v| *v > 0.0).unwrap_or(30.0);
|
|
let rate = FrameRate::from_f64(fr);
|
|
let tb = Timebase { rate };
|
|
let name = s(&doc, "nm").filter(|n| !n.is_empty()).unwrap_or(fallback_name).to_string();
|
|
let ip = f(&doc, "ip").unwrap_or(0.0);
|
|
let op = f(&doc, "op").unwrap_or(ip + fr).max(ip + 1.0);
|
|
let mut cx =
|
|
Ctx { im: Im { tb, warnings: vec![] }, assets: BTreeMap::new(), fonts: vec![], store, base_dir, items: vec![], folder: None, name: name.clone() };
|
|
if let Some(list) = doc.get("fonts").and_then(|f| f.get("list")).and_then(Json::as_array) {
|
|
for fo in list {
|
|
let n = s(fo, "fName").unwrap_or("").to_string();
|
|
let fam = s(fo, "fFamily").unwrap_or(&n).to_string();
|
|
let st = s(fo, "fStyle").unwrap_or("Regular").to_string();
|
|
cx.fonts.push((n, fam, st));
|
|
}
|
|
}
|
|
let (w, h) = (w.round().max(1.0) as u32, h.round().max(1.0) as u32);
|
|
let duration = tb.tick(op);
|
|
// Pass 1: asset items (precomps empty for now, images stored).
|
|
let assets = doc.get("assets").and_then(Json::as_array).cloned().unwrap_or_default();
|
|
for a in &assets {
|
|
let Some(id) = s(a, "id") else { continue };
|
|
if a.get("layers").is_some() {
|
|
let folder = cx.folder(project);
|
|
let (aw, ah) = (f(a, "w").map(|v| v as u32).unwrap_or(w), f(a, "h").map(|v| v as u32).unwrap_or(h));
|
|
let arate = f(a, "fr").map(FrameRate::from_f64).unwrap_or(rate);
|
|
let c = Comp::new(aw, ah, arate, duration);
|
|
let nm = s(a, "nm").unwrap_or(id).to_string();
|
|
let item = project.add_item(&nm, Label::Sandstone, Some(folder), ItemKind::Comp(Arc::new(c)));
|
|
cx.items.push(item);
|
|
cx.assets.insert(id.to_string(), (item, f(a, "w").zip(f(a, "h")).map(|(x, y)| (x as u32, y as u32))));
|
|
} else if let Some(p) = s(a, "p") {
|
|
let (aw, ah) = (f(a, "w").unwrap_or(0.0) as u32, f(a, "h").unwrap_or(0.0) as u32);
|
|
let nm = s(a, "nm").unwrap_or(id).to_string();
|
|
let path = if let Some((mime, data)) = crate::base64::data_uri(p) {
|
|
let ext = match mime.as_str() {
|
|
"image/jpeg" | "image/jpg" => "jpg",
|
|
"image/webp" => "webp",
|
|
"image/gif" => "gif",
|
|
"image/svg+xml" => "svg",
|
|
_ => "png",
|
|
};
|
|
(cx.store)(&ImportedImage { id: id.to_string(), name: nm.clone(), bytes: data, ext: ext.into() })
|
|
} else {
|
|
let u = s(a, "u").unwrap_or("");
|
|
let rel = format!("{u}{p}");
|
|
Some(if rel.starts_with('/') || cx.base_dir.is_empty() { rel } else { format!("{}/{rel}", cx.base_dir.trim_end_matches('/')) })
|
|
};
|
|
let Some(path) = path else {
|
|
cx.im.warn(format!("image asset `{id}` could not be stored"));
|
|
continue;
|
|
};
|
|
let folder = cx.folder(project);
|
|
let foot = Footage {
|
|
path,
|
|
kind: FootageKind::Still,
|
|
width: aw.max(1),
|
|
height: ah.max(1),
|
|
pixel_aspect: 1.0,
|
|
frame_rate: rate,
|
|
native_rate: None,
|
|
duration: Tick::ZERO,
|
|
has_video: true,
|
|
has_audio: false,
|
|
alpha: AlphaMode::Straight,
|
|
premul_color: [0.0; 3],
|
|
loop_count: 1,
|
|
codec: String::new(),
|
|
missing: false,
|
|
sequence: vec![],
|
|
color_profile: None,
|
|
..Default::default()
|
|
};
|
|
let item = project.add_item(&nm, Label::Lavender, Some(folder), ItemKind::Footage(foot));
|
|
cx.items.push(item);
|
|
cx.assets.insert(id.to_string(), (item, Some((aw, ah))));
|
|
} else {
|
|
cx.im.warn(format!("asset `{id}` is not supported (skipped)"));
|
|
}
|
|
}
|
|
// Pass 2: precomp contents.
|
|
for a in &assets {
|
|
let (Some(id), Some(layers)) = (s(a, "id"), a.get("layers").and_then(Json::as_array)) else { continue };
|
|
let Some((item, _)) = cx.assets.get(id).copied() else { continue };
|
|
let Some(mut c) = project.comp(item).cloned() else { continue };
|
|
let end = layers.iter().filter_map(|l| f(l, "op")).fold(0.0f64, f64::max);
|
|
if end > 0.0 {
|
|
c.duration = tb.tick(end);
|
|
c.work_area = (Tick::ZERO, c.duration);
|
|
}
|
|
build_layers(&mut cx, project, &mut c, layers);
|
|
if let Some(it) = project.item_mut(item) {
|
|
it.kind = ItemKind::Comp(Arc::new(c));
|
|
}
|
|
}
|
|
// The main comp.
|
|
let mut comp = Comp::new(w, h, rate, duration);
|
|
if ip > 0.0 {
|
|
comp.work_area = (tb.tick(ip), duration);
|
|
}
|
|
if let Some(ms) = doc.get("markers").and_then(Json::as_array) {
|
|
for m in ms {
|
|
comp.markers.push(Marker {
|
|
time: tb.tick(f(m, "tm").unwrap_or(0.0)),
|
|
duration: tb.tick(f(m, "dr").unwrap_or(0.0)),
|
|
comment: s(m, "cm").unwrap_or("").to_string(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
let layers = doc.get("layers").and_then(Json::as_array).cloned().unwrap_or_default();
|
|
build_layers(&mut cx, project, &mut comp, &layers);
|
|
let id = project.add_item(&name, Label::Sandstone, None, ItemKind::Comp(Arc::new(comp)));
|
|
cx.items.push(id);
|
|
project.fix_next_id();
|
|
Ok(ImportResult { comp: id, items: cx.items, warnings: cx.im.warnings })
|
|
}
|
|
|
|
fn transform(im: &Im, ks: &Json, tr: &mut PropGroup, next: &mut u64) {
|
|
let set = |tr: &mut PropGroup, m: &str, j: Option<&Json>, conv: &dyn Fn(&Json) -> Option<Value>| {
|
|
if let (Some(j), Some(p)) = (j, tr.get_mut(m)) {
|
|
import_prop(im, j, p, conv);
|
|
}
|
|
};
|
|
set(tr, "anchor", ks.get("a"), &|v| anim::to_vec3(v, 0.0));
|
|
set(tr, "scale", ks.get("s"), &|v| anim::to_vec3(v, 100.0));
|
|
set(tr, "rotation", ks.get("r").or(ks.get("rz")), &anim::to_scalar);
|
|
set(tr, "rotationX", ks.get("rx"), &anim::to_scalar);
|
|
set(tr, "rotationY", ks.get("ry"), &anim::to_scalar);
|
|
set(tr, "orientation", ks.get("or"), &|v| anim::to_vec3(v, 0.0));
|
|
set(tr, "opacity", ks.get("o"), &anim::to_scalar);
|
|
let Some(p) = ks.get("p") else { return };
|
|
if p.get("s").and_then(Json::as_bool) == Some(true) {
|
|
// Separated dimensions: X/Y/Z Position after Position.
|
|
let at = tr.children.iter().position(|c| c.match_id() == "position").map(|i| i + 1).unwrap_or(tr.children.len());
|
|
let base = tr.get("position").map(|p| p.value.as_vec3()).unwrap_or([0.0; 3]);
|
|
for (d, (key, m, name)) in
|
|
[("x", "positionX", "X Position"), ("y", "positionY", "Y Position"), ("z", "positionZ", "Z Position")].iter().enumerate().rev()
|
|
{
|
|
let mut pr = Property::new(*next, m, name, Value::Scalar(base[d])).with_ui(ParamUi::Number);
|
|
*next += 1;
|
|
pr.three_d_only = d == 2;
|
|
if let Some(j) = p.get(*key) {
|
|
import_prop(im, j, &mut pr, &anim::to_scalar);
|
|
}
|
|
tr.children.insert(at, Node::Prop(pr));
|
|
}
|
|
} else {
|
|
set(tr, "position", Some(p), &|v| anim::to_vec3(v, 0.0));
|
|
}
|
|
}
|
|
|
|
fn masks(im: &Im, ids: &mut Ids, list: &[Json], g: &mut PropGroup) {
|
|
for (i, m) in list.iter().enumerate() {
|
|
let mode = mask_mode_from(s(m, "mode").unwrap_or("a"));
|
|
let name = s(m, "nm").map(str::to_string).unwrap_or(format!("Mask {}", i + 1));
|
|
let color = build::MASK_COLORS[i % build::MASK_COLORS.len()];
|
|
let mut mg = build::mask(ids, &name, ShapePath::default(), mode, color);
|
|
if let GroupKind::Mask { inverted, .. } = &mut mg.kind {
|
|
*inverted = m.get("inv").and_then(Json::as_bool).unwrap_or(false);
|
|
}
|
|
let mut set = |k: &str, mid: &str, conv: &dyn Fn(&Json) -> Option<Value>| {
|
|
if let (Some(j), Some(p)) = (m.get(k), mg.get_mut(mid)) {
|
|
import_prop(im, j, p, conv);
|
|
}
|
|
};
|
|
set("pt", "path", &shapes::to_path);
|
|
set("o", "opacity", &anim::to_scalar);
|
|
set("x", "expansion", &anim::to_scalar);
|
|
set("f", "feather", &anim::to_vec2);
|
|
g.children.push(Node::Group(mg));
|
|
}
|
|
}
|
|
|
|
fn effects(im: &mut Im, ids: &mut Ids, list: &[Json], g: &mut PropGroup, size: [f64; 2]) {
|
|
for e in list {
|
|
let ty = e.get("ty").and_then(Json::as_u64).unwrap_or(0);
|
|
let params = e.get("ef").and_then(Json::as_array).cloned().unwrap_or_default();
|
|
let inner_ty = params.first().and_then(|p| p.get("ty")).and_then(Json::as_u64);
|
|
let map = s(e, "mn").and_then(fx_by_id).or_else(|| fx_by_type(ty, inner_ty));
|
|
let nm = s(e, "nm").unwrap_or("Effect");
|
|
let Some(map) = map else {
|
|
im.warn(format!("effect `{nm}` (type {ty}) is not supported (skipped)"));
|
|
continue;
|
|
};
|
|
let Some(spec) = effectcraft_effects::find(map.id) else { continue };
|
|
let mut fg = effectcraft_effects::instantiate(spec, ids, nm, size);
|
|
if e.get("en").and_then(Json::as_u64) == Some(0) {
|
|
fg.enabled = false;
|
|
}
|
|
for (i, (m, pty, scale, _)) in fx_params(map).iter().enumerate() {
|
|
if m.is_empty() {
|
|
continue;
|
|
}
|
|
let Some(pj) = params.get(i).and_then(|p| p.get("v")) else { continue };
|
|
let Some(p) = fg.get_mut(m) else { continue };
|
|
let (pty, scale) = (*pty, *scale);
|
|
let conv = move |v: &Json| -> Option<Value> {
|
|
match pty {
|
|
2 => anim::to_color(v),
|
|
3 => anim::to_vec2(v),
|
|
4 => anim::to_bool(v),
|
|
7 => anim::to_enum1(v),
|
|
_ => anim::to_scalar(v).map(|x| Value::Scalar(x.as_f64() / scale)),
|
|
}
|
|
};
|
|
import_prop(im, pj, p, &conv);
|
|
}
|
|
g.children.push(Node::Group(fg));
|
|
}
|
|
}
|
|
|
|
fn layer_source(cx: &mut Ctx, project: &mut Project, comp: &Comp, j: &Json) -> Option<(LayerSource, (u32, u32))> {
|
|
let ty = j.get("ty").and_then(Json::as_u64).unwrap_or(99);
|
|
let full = (comp.width, comp.height);
|
|
let asset = |cx: &Ctx| s(j, "refId").and_then(|r| cx.assets.get(r).copied());
|
|
Some(match ty {
|
|
0 => {
|
|
let Some((item, _)) = asset(cx) else {
|
|
cx.im.warn(format!("{}: missing precomp asset", s(j, "nm").unwrap_or("layer")));
|
|
return None;
|
|
};
|
|
let size = project.comp(item).map(|c| (c.width, c.height)).unwrap_or(full);
|
|
(LayerSource::Comp { item }, size)
|
|
}
|
|
1 => {
|
|
let sw = f(j, "sw").unwrap_or(comp.width as f64).round().max(1.0) as u32;
|
|
let sh = f(j, "sh").unwrap_or(comp.height as f64).round().max(1.0) as u32;
|
|
let color = hex_color(s(j, "sc").unwrap_or("#000000"));
|
|
let name = s(j, "nm").unwrap_or("Solid");
|
|
let folder = cx.folder(project);
|
|
let item = project.add_item(name, Label::Red, Some(folder), ItemKind::Solid(Solid { color, width: sw, height: sh, pixel_aspect: 1.0 }));
|
|
cx.items.push(item);
|
|
(LayerSource::Solid { item }, (sw, sh))
|
|
}
|
|
2 => {
|
|
let Some((item, size)) = asset(cx) else {
|
|
cx.im.warn(format!("{}: missing image asset", s(j, "nm").unwrap_or("layer")));
|
|
return None;
|
|
};
|
|
(LayerSource::Footage { item }, size.unwrap_or(full))
|
|
}
|
|
3 => (LayerSource::Null, (100, 100)),
|
|
4 => (LayerSource::Shape, full),
|
|
5 => (LayerSource::Text, full),
|
|
13 => {
|
|
cx.im.warn(format!("{}: camera layers are not imported", s(j, "nm").unwrap_or("camera")));
|
|
return None;
|
|
}
|
|
other => {
|
|
cx.im.warn(format!("{}: layer type {other} is not supported (skipped)", s(j, "nm").unwrap_or("layer")));
|
|
return None;
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Build the layers of a comp (Lottie order: first = top = layer #1).
|
|
fn build_layers(cx: &mut Ctx, project: &mut Project, comp: &mut Comp, layers: &[Json]) {
|
|
let tb = cx.im.tb;
|
|
let mut by_ind: BTreeMap<i64, LayerId> = BTreeMap::new();
|
|
// (layer id, parent ind, matte: (kind, explicit tp ind), json position)
|
|
let mut pending: Vec<(LayerId, Option<i64>, Option<(MatteKind, Option<i64>)>, usize)> = vec![];
|
|
let mut pos_ind: Vec<Option<i64>> = vec![];
|
|
for (ji, j) in layers.iter().enumerate() {
|
|
let ind = j.get("ind").and_then(Json::as_i64);
|
|
pos_ind.push(ind);
|
|
let Some((source, size)) = layer_source(cx, project, comp, j) else { continue };
|
|
let name = s(j, "nm").unwrap_or(source.type_name()).to_string();
|
|
let mut l: Layer = build::layer(project, comp, &name, source.clone(), size, None);
|
|
if let Some(i) = ind {
|
|
by_ind.insert(i, l.id);
|
|
}
|
|
let ip = f(j, "ip").unwrap_or(0.0);
|
|
let op = f(j, "op").unwrap_or(tb.frame(comp.duration));
|
|
l.in_point = tb.tick(ip);
|
|
l.out_point = tb.tick(op.max(ip));
|
|
l.start_time = tb.tick(f(j, "st").unwrap_or(0.0));
|
|
l.stretch = f(j, "sr").unwrap_or(1.0) * 100.0;
|
|
if j.get("ddd").and_then(Json::as_u64) == Some(1) {
|
|
l.switches.three_d = true;
|
|
}
|
|
if let Some(bm) = j.get("bm").and_then(Json::as_u64) {
|
|
l.blend_mode = crate::blend_from_lottie(bm);
|
|
}
|
|
if j.get("ao").and_then(Json::as_u64) == Some(1) {
|
|
l.auto_orient = AutoOrient::AlongPath;
|
|
}
|
|
if j.get("hd").and_then(Json::as_bool) == Some(true) {
|
|
l.switches.video = false;
|
|
}
|
|
if j.get("td").and_then(Json::as_u64) == Some(1) {
|
|
l.switches.video = false;
|
|
}
|
|
let mut next = project.next_id;
|
|
if let (Some(ks), Some(tr)) = (j.get("ks"), l.props.sub_mut("transform")) {
|
|
transform(&cx.im, ks, tr, &mut next);
|
|
}
|
|
let mut ids = Ids(&mut next);
|
|
match &source {
|
|
LayerSource::Shape => {
|
|
let items = j.get("shapes").and_then(Json::as_array).cloned().unwrap_or_default();
|
|
let groups = shapes::import_items(&mut cx.im, &mut ids, &items);
|
|
if let Some(c) = l.props.sub_mut("contents") {
|
|
c.children.extend(groups.into_iter().map(Node::Group));
|
|
}
|
|
}
|
|
LayerSource::Text => {
|
|
if let (Some(t), Some(tg)) = (j.get("t"), l.props.sub_mut("text")) {
|
|
text::import_text(&mut cx.im, &mut ids, t, tg, &cx.fonts);
|
|
}
|
|
}
|
|
LayerSource::Comp { .. } => {
|
|
if let Some(tm) = j.get("tm") {
|
|
let mut pr = ids.prop("timeRemap", "Time Remap", Value::Scalar(0.0)).with_ui(ParamUi::Number);
|
|
import_prop(&cx.im, tm, &mut pr, &anim::to_scalar);
|
|
l.props.children.insert(0, Node::Prop(pr));
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(list) = j.get("masksProperties").and_then(Json::as_array)
|
|
&& let Some(g) = l.props.sub_mut("masks")
|
|
{
|
|
masks(&cx.im, &mut ids, list, g);
|
|
}
|
|
if let Some(list) = j.get("ef").and_then(Json::as_array)
|
|
&& let Some(g) = l.props.sub_mut("effects")
|
|
{
|
|
effects(&mut cx.im, &mut ids, list, g, [size.0 as f64, size.1 as f64]);
|
|
}
|
|
if j.get("sy").and_then(Json::as_array).is_some_and(|a| !a.is_empty()) {
|
|
cx.im.warn(format!("{name}: layer styles are not imported"));
|
|
}
|
|
project.next_id = next;
|
|
let parent = j.get("parent").and_then(Json::as_i64);
|
|
let tt = j.get("tt").and_then(Json::as_u64).unwrap_or(0);
|
|
let matte = match tt {
|
|
1 => Some(MatteKind::Alpha),
|
|
2 => Some(MatteKind::AlphaInverted),
|
|
3 => Some(MatteKind::Luma),
|
|
4 => Some(MatteKind::LumaInverted),
|
|
_ => None,
|
|
}
|
|
.map(|k| (k, j.get("tp").and_then(Json::as_i64)));
|
|
pending.push((l.id, parent, matte, ji));
|
|
comp.layers.push(l);
|
|
}
|
|
for (id, parent, matte, ji) in pending {
|
|
let parent_id = parent.and_then(|p| by_ind.get(&p).copied()).filter(|p| *p != id);
|
|
let matte_id = matte.and_then(|(k, tp)| {
|
|
let target = match tp {
|
|
Some(tp) => by_ind.get(&tp).copied(),
|
|
// Legacy files: the matte is the layer directly above.
|
|
None => ji.checked_sub(1).and_then(|a| pos_ind.get(a).copied().flatten()).and_then(|i| by_ind.get(&i).copied()),
|
|
};
|
|
target.filter(|t| *t != id).map(|t| TrackMatte { layer: t, kind: k })
|
|
});
|
|
if let Some(l) = comp.layer_mut(id) {
|
|
l.parent = parent_id;
|
|
l.track_matte = matte_id;
|
|
}
|
|
}
|
|
}
|