mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
Align every effect's display name, category, parameter names, order, twirl-down groups, popup options, units, defaults and ranges with the public After Effects effect reference, and implement controls that were ignored or missing where cheap. One commit because the categories share the registry, migration tables and GPU kernels (only green states). Infrastructure - effects::migrate: saved instances are upgraded on project open (Session::replace_project → upgrade_effects): renamed ids (PARAM_ID_ALIASES), params moved into twirl-down groups, popup values remapped by label (POPUP_LABEL_ALIASES), refreshed names/UI hints, added params created with legacy_default values that keep old renders. - flatten_params also keys nested props by match path (group/param), so effects declare grouped params with `group/param` ids; layer-source companions sit next to grouped layer params. - lookup() resolves old effect names (EFFECT_NAME_ALIASES). - Registry-wide test: unique ids/names, non-empty names, defaults in range, popup defaults within options, value kinds match UI. - docs/effects.md generated from the registry (catalog.rs) with GPU / 32-bpc badges and per-effect status; a test keeps it current. Batches - Blur & Sharpen, Channel, 3D Channel, Perspective, Text, Utility: Camera Lens Blur iris/highlight groups, Compound Blur layer, Camera- Shake Deblur method/strength, Invert full channel list (CPU HLS/YIQ), Set Channels/Set Matte layers, Channel Combiner targets, 3D Channel Extract clamp/invert. - Color Correction: temporal smoothing + scene detect for Auto Color/ Contrast/Levels and Shadow/Highlight, Brightness & Contrast legacy switch, Change to Color tolerance group, Leave Color, Photo Filter presets, Exposure channels, Levels clip popups, Colorama phase source, Color Link sample list, Lumetri groups (CPU + GPU kernels). - Distort: Bulge, Displacement Map layer/behaviour, Magnify, Optics Compensation resize, Ripple conversion, Transform sampling, Turbulent Displace types/pinning/evolution, Wave Warp types/pinning, Twirl and Ripple radius fixes. - Generate: 4-Color Gradient, Checkerboard, Grid, Circle size/feather/ blending, Advanced Lightning types, Cell Pattern, Radio Waves, Fractal palettes, Scribble/Vegas names. - Keying, Matte, Obsolete: Color Difference Key partials/views, Unmult controls, Key Light groups, Refine Soft/Hard Matte, Roto Brush groups, Mocha shape moved to Matte, Spill Suppressor accuracy, Basic/Path Text groups. - Noise & Grain, Immersive Video, Audio, Time: Fractal/Turbulent Noise full control set (noise3.rs, GPU), grain look groups, Remove Grain, VR effects, Compressor/Gate/Distortion controls, Timewarp groups, VR Sphere to Plane name. - Simulation: AE twirl-down groups for all simulations, Foam strength, Particle Playground barrel radius and mapper targets. - Stylize, Transition: Glow looping/dimensions/alpha (CPU + GPU), Mosaic averaging, Cartoon groups, Roughen Edges evolution, Texturize layer, Gradient Wipe layer, Radial Wipe both, Card Wipe options. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
640 lines
25 KiB
Rust
640 lines
25 KiB
Rust
//! Photoshop import as a composition (File ▸ Import ▸ File… with Import As: Composition or
|
||
//! Composition – Retain Layer Sizes), and Layer ▸ Create ▸ Convert to Editable Text.
|
||
//!
|
||
//! Every pixel layer becomes a footage item that names its layer (`Footage::layer`), placed in
|
||
//! a composition the size of the document. Groups become precomps (pass-through groups collapse
|
||
//! so their blend modes act on the layers below, like Photoshop); type layers become editable
|
||
//! text layers; layer masks are baked into the layer's alpha and vector masks become masks;
|
||
//! layer effects map to Layer Styles; adjustment layers become adjustment layers with the
|
||
//! matching effect (best effort); clipped layers preserve the transparency underneath.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use effectcraft_color::BlendMode;
|
||
use effectcraft_color::Label;
|
||
use effectcraft_keyframe::{Gradient, Justify, ShapePath, TextDoc, Value};
|
||
use effectcraft_project::build::{self, Ids};
|
||
use effectcraft_project::styles::{self, STYLE_BLEND_MODES};
|
||
use effectcraft_project::{AlphaMode, Comp, Footage, FootageKind, ItemId, ItemKind, Layer, LayerSource, MaskMode, Project, PropGroup, Solid, SourceLayer};
|
||
use effectcraft_psd::{Adjustment, DValue, Descriptor, Node, Psd};
|
||
use effectcraft_time::{FrameRate, Tick};
|
||
|
||
/// Photoshop blend mode key → compositor mode (`pass` = pass through, handled by collapsing).
|
||
pub fn blend_mode(key: &str) -> BlendMode {
|
||
match key {
|
||
"diss" => BlendMode::Dissolve,
|
||
"dark" => BlendMode::Darken,
|
||
"mul " => BlendMode::Multiply,
|
||
"idiv" => BlendMode::ColorBurn,
|
||
"lbrn" => BlendMode::LinearBurn,
|
||
"dkCl" => BlendMode::DarkerColor,
|
||
"lite" => BlendMode::Lighten,
|
||
"scrn" => BlendMode::Screen,
|
||
"div " => BlendMode::ColorDodge,
|
||
"lddg" => BlendMode::LinearDodge,
|
||
"lgCl" => BlendMode::LighterColor,
|
||
"over" => BlendMode::Overlay,
|
||
"sLit" => BlendMode::SoftLight,
|
||
"hLit" => BlendMode::HardLight,
|
||
"vLit" => BlendMode::VividLight,
|
||
"lLit" => BlendMode::LinearLight,
|
||
"pLit" => BlendMode::PinLight,
|
||
"hMix" => BlendMode::HardMix,
|
||
"diff" => BlendMode::Difference,
|
||
"smud" => BlendMode::Exclusion,
|
||
"fsub" => BlendMode::Subtract,
|
||
"fdiv" => BlendMode::Divide,
|
||
"hue " => BlendMode::Hue,
|
||
"sat " => BlendMode::Saturation,
|
||
"colr" => BlendMode::Color,
|
||
"lum " => BlendMode::Luminosity,
|
||
_ => BlendMode::Normal,
|
||
}
|
||
}
|
||
|
||
/// Descriptor blend mode enum (`BlnM`) → compositor mode.
|
||
fn desc_blend(v: Option<&str>) -> Option<BlendMode> {
|
||
Some(match v? {
|
||
"Nrml" => BlendMode::Normal,
|
||
"Dslv" => BlendMode::Dissolve,
|
||
"Drkn" => BlendMode::Darken,
|
||
"Mltp" => BlendMode::Multiply,
|
||
"CBrn" => BlendMode::ColorBurn,
|
||
"linearBurn" => BlendMode::LinearBurn,
|
||
"darkerColor" => BlendMode::DarkerColor,
|
||
"Lghn" => BlendMode::Lighten,
|
||
"Scrn" => BlendMode::Screen,
|
||
"CDdg" => BlendMode::ColorDodge,
|
||
"linearDodge" => BlendMode::LinearDodge,
|
||
"lighterColor" => BlendMode::LighterColor,
|
||
"Ovrl" => BlendMode::Overlay,
|
||
"SftL" => BlendMode::SoftLight,
|
||
"HrdL" => BlendMode::HardLight,
|
||
"vividLight" => BlendMode::VividLight,
|
||
"linearLight" => BlendMode::LinearLight,
|
||
"pinLight" => BlendMode::PinLight,
|
||
"hardMix" => BlendMode::HardMix,
|
||
"Dfrn" => BlendMode::Difference,
|
||
"Xclu" => BlendMode::Exclusion,
|
||
"blendSubtraction" => BlendMode::Subtract,
|
||
"blendDivide" => BlendMode::Divide,
|
||
"H " => BlendMode::Hue,
|
||
"Strt" => BlendMode::Saturation,
|
||
"Clr " => BlendMode::Color,
|
||
"Lmns" => BlendMode::Luminosity,
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
/// What the import created.
|
||
pub struct PsdImport {
|
||
pub comp: ItemId,
|
||
pub folder: ItemId,
|
||
pub items: Vec<ItemId>,
|
||
pub warnings: Vec<String>,
|
||
}
|
||
|
||
struct Ctx<'a> {
|
||
psd: &'a Psd,
|
||
path: &'a str,
|
||
retain: bool,
|
||
folder: ItemId,
|
||
rate: FrameRate,
|
||
duration: Tick,
|
||
items: Vec<ItemId>,
|
||
warnings: Vec<String>,
|
||
}
|
||
|
||
/// Import `psd` (read from `path`) as a composition named `name`.
|
||
pub fn import(proj: &mut Project, psd: &Psd, path: &str, name: &str, retain: bool, rate: FrameRate, duration: Tick) -> PsdImport {
|
||
let folder = proj.add_item(&format!("{name} Layers"), Label::Yellow, None, ItemKind::Folder);
|
||
let mut cx = Ctx { psd, path, retain, folder, rate, duration, items: vec![], warnings: vec![] };
|
||
let comp = build_comp(proj, &mut cx, name, &psd.tree(), None);
|
||
PsdImport { comp, folder, items: cx.items, warnings: cx.warnings }
|
||
}
|
||
|
||
fn new_comp(cx: &Ctx) -> Comp {
|
||
Comp::new(cx.psd.width, cx.psd.height, cx.rate, cx.duration)
|
||
}
|
||
|
||
fn build_comp(proj: &mut Project, cx: &mut Ctx, name: &str, nodes: &[Node], parent: Option<ItemId>) -> ItemId {
|
||
let cid = proj.add_item(name, Label::Sandstone, parent, ItemKind::Comp(Arc::new(new_comp(cx))));
|
||
cx.items.push(cid);
|
||
let mut comp = new_comp(cx);
|
||
for n in nodes {
|
||
if let Some(l) = build_layer(proj, cx, &comp, n) {
|
||
comp.layers.push(l);
|
||
}
|
||
}
|
||
styles::sync_comp(&mut comp);
|
||
if let Some(item) = proj.item_mut(cid) {
|
||
item.kind = ItemKind::Comp(Arc::new(comp));
|
||
}
|
||
cid
|
||
}
|
||
|
||
fn footage_item(proj: &mut Project, cx: &mut Ctx, index: usize, name: &str) -> (ItemId, (u32, u32)) {
|
||
let l = &cx.psd.layers[index];
|
||
let retain = cx.retain;
|
||
let (w, h) = if retain && !l.rect.is_empty() { (l.rect.width(), l.rect.height()) } else { (cx.psd.width, cx.psd.height) };
|
||
let file = std::path::Path::new(cx.path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||
let f = Footage {
|
||
path: cx.path.to_string(),
|
||
kind: FootageKind::Still,
|
||
width: w,
|
||
height: h,
|
||
pixel_aspect: 1.0,
|
||
frame_rate: cx.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: "PSD".into(),
|
||
missing: false,
|
||
sequence: vec![],
|
||
color_profile: None,
|
||
layer: Some(SourceLayer { index: index as u32, name: name.to_string(), layer_size: retain && !l.rect.is_empty() }),
|
||
};
|
||
let id = proj.add_item(&format!("{name}/{file}"), Label::Lavender, Some(cx.folder), ItemKind::Footage(f));
|
||
cx.items.push(id);
|
||
(id, (w, h))
|
||
}
|
||
|
||
fn build_layer(proj: &mut Project, cx: &mut Ctx, comp: &Comp, n: &Node) -> Option<Layer> {
|
||
let (index, children) = match n {
|
||
Node::Layer(i) => (*i, None),
|
||
Node::Group { layer, children } => (*layer, Some(children)),
|
||
};
|
||
let pl = cx.psd.layers[index].clone();
|
||
let name = if pl.name.is_empty() { format!("Layer {}", index + 1) } else { pl.name.clone() };
|
||
let mut layer = if let Some(children) = children {
|
||
let sub = build_comp(proj, cx, &name, children, Some(cx.folder));
|
||
let mut l = build::layer(proj, comp, &name, LayerSource::Comp { item: sub }, (comp.width, comp.height), None);
|
||
// Pass-through groups composite their layers straight into the parent.
|
||
l.switches.collapse = pl.blend_key() == "pass";
|
||
l
|
||
} else if let Some(t) = &pl.text {
|
||
text_layer(proj, comp, &name, t)
|
||
} else if let Some(adj) = &pl.adjustment {
|
||
let mut l = adjustment_layer(proj, comp, &name);
|
||
add_adjustment_effect(proj, &mut l, adj, comp, &mut cx.warnings, &name);
|
||
l
|
||
} else if pl.has_pixels() || pl.fill.is_some() {
|
||
let (item, size) = footage_item(proj, cx, index, &name);
|
||
let mut l = build::layer(proj, comp, &name, LayerSource::Footage { item }, size, None);
|
||
if cx.retain && !pl.rect.is_empty() {
|
||
set_vec2(&mut l, "transform/position", [pl.rect.left as f64 + size.0 as f64 / 2.0, pl.rect.top as f64 + size.1 as f64 / 2.0]);
|
||
}
|
||
l
|
||
} else {
|
||
// Empty layers have nothing to show.
|
||
return None;
|
||
};
|
||
layer.name = comp.unique_layer_name(&name);
|
||
layer.blend_mode = blend_mode(pl.blend_key());
|
||
layer.switches.video = !pl.hidden;
|
||
layer.preserve_transparency = pl.clipping;
|
||
set_scalar(&mut layer, "transform/opacity", pl.opacity as f64 / 255.0 * 100.0);
|
||
// Vector mask → a mask in layer space.
|
||
let origin = layer_origin(&pl, cx.retain, &layer);
|
||
if let Some(vm) = pl.vector_mask.as_ref().filter(|v| !v.disabled)
|
||
&& let Some(masks) = layer.props.sub_mut("masks")
|
||
{
|
||
let mut ids = Ids(&mut proj.next_id);
|
||
for (k, sp) in vm.subpaths.iter().enumerate() {
|
||
let n = sp.knots.len();
|
||
let mut path = ShapePath { vertices: vec![], in_tangents: vec![], out_tangents: vec![], closed: sp.closed };
|
||
for kn in &sp.knots {
|
||
let v = [kn[1][0] - origin[0], kn[1][1] - origin[1]];
|
||
path.vertices.push(v);
|
||
path.in_tangents.push([kn[0][0] - kn[1][0], kn[0][1] - kn[1][1]]);
|
||
path.out_tangents.push([kn[2][0] - kn[1][0], kn[2][1] - kn[1][1]]);
|
||
}
|
||
if n == 0 {
|
||
continue;
|
||
}
|
||
let mode = if vm.inverted { MaskMode::Subtract } else { MaskMode::Add };
|
||
masks.children.push(build::mask(&mut ids, &format!("Mask {}", k + 1), path, mode, [255, 255, 0]).into());
|
||
}
|
||
}
|
||
// Layer effects → Layer Styles; Fill Opacity → Advanced Blending.
|
||
let gl = comp.global_light.clone();
|
||
if let Some(fx) = &pl.effects
|
||
&& fx.bool("masterFXSwitch") != Some(false)
|
||
&& layer.can_have_styles()
|
||
{
|
||
apply_effects(proj, &mut layer, fx, &gl);
|
||
}
|
||
if pl.fill_opacity < 255 && layer.can_have_styles() {
|
||
let mut ids = Ids(&mut proj.next_id);
|
||
if layer.props.sub(styles::GROUP).is_none() {
|
||
let g = styles::layer_styles(&mut ids, &gl);
|
||
let at = layer.props.children.iter().position(|c| c.match_id() == "transform").map(|i| i + 1).unwrap_or(layer.props.children.len());
|
||
layer.props.children.insert(at, g.into());
|
||
}
|
||
if let Some(p) = layer.props.prop_mut("layerStyles/blendingOptions/advancedBlending/fillOpacity") {
|
||
p.value = Value::Scalar(pl.fill_opacity as f64 / 255.0 * 100.0);
|
||
}
|
||
}
|
||
Some(layer)
|
||
}
|
||
|
||
/// Where layer space's origin sits in document pixels.
|
||
fn layer_origin(pl: &effectcraft_psd::Layer, retain: bool, layer: &Layer) -> [f64; 2] {
|
||
if let Some(t) = &pl.text {
|
||
return [t.transform[4], t.transform[5]];
|
||
}
|
||
if matches!(layer.source, LayerSource::Footage { .. }) && retain && !pl.rect.is_empty() {
|
||
return [pl.rect.left as f64, pl.rect.top as f64];
|
||
}
|
||
[0.0, 0.0]
|
||
}
|
||
|
||
fn set_scalar(l: &mut Layer, path: &str, v: f64) {
|
||
if let Some(p) = l.props.prop_mut(path) {
|
||
p.value = Value::Scalar(v);
|
||
}
|
||
}
|
||
|
||
fn set_vec2(l: &mut Layer, path: &str, v: [f64; 2]) {
|
||
if let Some(p) = l.props.prop_mut(path) {
|
||
p.value = match p.value {
|
||
Value::Vec3(o) => Value::Vec3([v[0], v[1], o[2]]),
|
||
_ => Value::Vec2(v),
|
||
};
|
||
}
|
||
}
|
||
|
||
/// Split a PostScript font name into family and style (`Inter-Bold` → `Inter`, `Bold`).
|
||
fn font_names(ps: &str) -> (String, String) {
|
||
match ps.split_once('-') {
|
||
Some((fam, style)) => (fam.to_string(), style.to_string()),
|
||
None => (ps.to_string(), "Regular".to_string()),
|
||
}
|
||
}
|
||
|
||
fn text_doc(t: &effectcraft_psd::TextInfo) -> TextDoc {
|
||
let mut d = TextDoc::default();
|
||
d.set_text(&t.text);
|
||
let [xx, xy, yx, yy, _, _] = t.transform;
|
||
let scale = (xx * yy - xy * yx).abs().sqrt().max(1e-6);
|
||
if let Some(s) = t.style.size {
|
||
d.size = s * scale;
|
||
}
|
||
if let Some(f) = &t.style.font {
|
||
let (fam, style) = font_names(f);
|
||
d.font = fam;
|
||
d.style = style;
|
||
}
|
||
if let Some(c) = t.style.color {
|
||
d.fill = [c[0] as f32, c[1] as f32, c[2] as f32, 1.0];
|
||
}
|
||
d.justify = match t.style.justification {
|
||
Some(1) => Justify::Right,
|
||
Some(2) => Justify::Center,
|
||
Some(3) => Justify::JustifyLastLeft,
|
||
Some(4) => Justify::JustifyLastRight,
|
||
Some(5) => Justify::JustifyLastCenter,
|
||
Some(6) => Justify::JustifyAll,
|
||
_ => Justify::Left,
|
||
};
|
||
if let Some(tr) = t.style.tracking {
|
||
d.tracking = tr;
|
||
}
|
||
if let Some(l) = t.style.leading {
|
||
d.leading = Some(l * scale);
|
||
}
|
||
d.faux_bold = t.style.faux_bold;
|
||
d.faux_italic = t.style.faux_italic;
|
||
if t.style.box_text
|
||
&& let Some([l, top, r, b]) = t.bounds
|
||
{
|
||
d.box_size = Some([(r - l) * scale, (b - top) * scale]);
|
||
d.box_pos = [l * scale, top * scale];
|
||
}
|
||
d
|
||
}
|
||
|
||
fn text_layer(proj: &mut Project, comp: &Comp, name: &str, t: &effectcraft_psd::TextInfo) -> Layer {
|
||
let mut l = build::layer(proj, comp, name, LayerSource::Text, (0, 0), None);
|
||
if let Some(p) = l.props.prop_mut("text/sourceText") {
|
||
p.value = Value::Text(Box::new(text_doc(t)));
|
||
}
|
||
set_vec2(&mut l, "transform/position", [t.transform[4], t.transform[5]]);
|
||
let [xx, xy, _, _, _, _] = t.transform;
|
||
let rot = xy.atan2(xx).to_degrees();
|
||
if rot.abs() > 1e-6 {
|
||
set_scalar(&mut l, "transform/rotation", rot);
|
||
}
|
||
l
|
||
}
|
||
|
||
fn adjustment_layer(proj: &mut Project, comp: &Comp, name: &str) -> Layer {
|
||
let folder = proj.folder_named("Solids").unwrap_or_else(|| proj.add_item("Solids", Label::Yellow, None, ItemKind::Folder));
|
||
let sid = proj.add_item(
|
||
name,
|
||
Label::Red,
|
||
Some(folder),
|
||
ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: comp.width, height: comp.height, pixel_aspect: 1.0 }),
|
||
);
|
||
let mut l = build::layer(proj, comp, name, LayerSource::Solid { item: sid }, (comp.width, comp.height), None);
|
||
l.switches.adjustment = true;
|
||
l.label = Label::Purple;
|
||
l
|
||
}
|
||
|
||
fn add_effect(proj: &mut Project, l: &mut Layer, effect: &str, comp: &Comp, params: &[(&str, Value)]) {
|
||
let Some(spec) = effectcraft_effects::lookup(effect) else { return };
|
||
let mut ids = Ids(&mut proj.next_id);
|
||
let mut g = effectcraft_effects::instantiate(spec, &mut ids, spec.name, [comp.width as f64, comp.height as f64]);
|
||
for (k, v) in params {
|
||
// Paths reach parameters in twirl-down groups (`master/exposure`).
|
||
if let Some(p) = g.prop_mut(k) {
|
||
p.value = v.clone();
|
||
}
|
||
}
|
||
if let Some(fx) = l.props.sub_mut("effects") {
|
||
fx.children.push(g.into());
|
||
}
|
||
}
|
||
|
||
fn add_adjustment_effect(proj: &mut Project, l: &mut Layer, adj: &Adjustment, comp: &Comp, warnings: &mut Vec<String>, name: &str) {
|
||
match adj {
|
||
Adjustment::Invert => add_effect(proj, l, "Invert", comp, &[]),
|
||
Adjustment::BrightnessContrast { brightness, contrast, legacy } => add_effect(
|
||
proj,
|
||
l,
|
||
"Brightness & Contrast",
|
||
comp,
|
||
&[("brightness", Value::Scalar(*brightness)), ("contrast", Value::Scalar(*contrast)), ("useLegacy", Value::Bool(*legacy))],
|
||
),
|
||
Adjustment::HueSaturation { hue, saturation, lightness, colorize } => {
|
||
let mut p = vec![("hue", Value::Scalar(*hue)), ("saturation", Value::Scalar(*saturation)), ("lightness", Value::Scalar(*lightness))];
|
||
if *colorize {
|
||
p = vec![
|
||
("colorize", Value::Bool(true)),
|
||
("colorizeHue", Value::Scalar(*hue)),
|
||
("colorizeSaturation", Value::Scalar(*saturation)),
|
||
("colorizeLightness", Value::Scalar(*lightness)),
|
||
];
|
||
}
|
||
add_effect(proj, l, "Hue/Saturation", comp, &p)
|
||
}
|
||
Adjustment::Levels { in_black, in_white, gamma, out_black, out_white } => add_effect(
|
||
proj,
|
||
l,
|
||
"Levels",
|
||
comp,
|
||
&[
|
||
("inBlack", Value::Scalar(in_black / 255.0)),
|
||
("inWhite", Value::Scalar(in_white / 255.0)),
|
||
("gamma", Value::Scalar(*gamma)),
|
||
("outBlack", Value::Scalar(out_black / 255.0)),
|
||
("outWhite", Value::Scalar(out_white / 255.0)),
|
||
],
|
||
),
|
||
Adjustment::Exposure { exposure, offset, gamma } => add_effect(
|
||
proj,
|
||
l,
|
||
"Exposure",
|
||
comp,
|
||
&[("master/exposure", Value::Scalar(*exposure)), ("master/offset", Value::Scalar(*offset)), ("master/gamma", Value::Scalar(*gamma))],
|
||
),
|
||
Adjustment::Other(k) => warnings.push(format!("{name}: adjustment `{k}` is not converted (layer kept as an empty adjustment layer)")),
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- layer effects → styles
|
||
|
||
fn style_mode(m: BlendMode) -> Value {
|
||
Value::Enum(STYLE_BLEND_MODES.iter().position(|x| x.1 == m).unwrap_or(0) as u32)
|
||
}
|
||
|
||
fn set(g: &mut PropGroup, k: &str, v: Value) {
|
||
if let Some(p) = g.get_mut(k) {
|
||
p.value = v;
|
||
}
|
||
}
|
||
|
||
fn color4(c: [f64; 3]) -> Value {
|
||
Value::Color([c[0], c[1], c[2], 1.0])
|
||
}
|
||
|
||
/// Copy the common style settings (`Md `, `Clr `, `Opct`, angle, distance, size…).
|
||
fn common(g: &mut PropGroup, d: &Descriptor) {
|
||
if let Some(m) = desc_blend(d.enum_value("Md ")) {
|
||
set(g, "blendMode", style_mode(m));
|
||
}
|
||
if let Some(c) = d.color("Clr ") {
|
||
set(g, "color", color4(c));
|
||
}
|
||
for (k, prop) in [("Opct", "opacity"), ("Nose", "noise"), ("Dstn", "distance"), ("blur", "size"), ("lagl", "angle"), ("ShdN", "jitter"), ("Inpr", "range")]
|
||
{
|
||
if let Some(v) = d.num(k) {
|
||
set(g, prop, Value::Scalar(v));
|
||
}
|
||
}
|
||
if let Some(b) = d.bool("uglg") {
|
||
set(g, "useGlobalLight", Value::Bool(b));
|
||
}
|
||
}
|
||
|
||
fn gradient_of(d: &Descriptor) -> Option<Gradient> {
|
||
let g = d.obj("Grad")?;
|
||
let list = |k: &str| match g.get(k) {
|
||
Some(DValue::List(v)) => v.clone(),
|
||
_ => vec![],
|
||
};
|
||
let colors: Vec<(f64, [f32; 4])> = list("Clrs")
|
||
.iter()
|
||
.filter_map(|c| match c {
|
||
DValue::Descriptor(s) => {
|
||
let col = s.color("Clr ")?;
|
||
Some((s.num("Lctn").unwrap_or(0.0) / 4096.0, [col[0] as f32, col[1] as f32, col[2] as f32, 1.0]))
|
||
}
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
let opacities: Vec<(f64, f32)> = list("Trns")
|
||
.iter()
|
||
.filter_map(|c| match c {
|
||
DValue::Descriptor(s) => Some((s.num("Lctn").unwrap_or(0.0) / 4096.0, (s.num("Opct").unwrap_or(100.0) / 100.0) as f32)),
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
(!colors.is_empty()).then(|| Gradient { colors, opacities: if opacities.is_empty() { vec![(0.0, 1.0), (1.0, 1.0)] } else { opacities } })
|
||
}
|
||
|
||
/// The effect descriptor for `key`, or the first enabled one of its `…Multi` list.
|
||
fn effect<'a>(fx: &'a Descriptor, key: &str, multi: &str) -> Option<&'a Descriptor> {
|
||
fx.obj(key).or_else(|| match fx.get(multi) {
|
||
Some(DValue::List(v)) => v.iter().find_map(|x| match x {
|
||
DValue::Descriptor(d) if d.bool("enab") != Some(false) => Some(d),
|
||
_ => None,
|
||
}),
|
||
_ => None,
|
||
})
|
||
}
|
||
|
||
fn apply_effects(proj: &mut Project, layer: &mut Layer, fx: &Descriptor, gl: &styles::GlobalLight) {
|
||
const MAP: &[(&str, &str, &str)] = &[
|
||
("DrSh", "dropShadowMulti", "dropShadow"),
|
||
("IrSh", "innerShadowMulti", "innerShadow"),
|
||
("OrGl", "outerGlowMulti", "outerGlow"),
|
||
("IrGl", "innerGlowMulti", "innerGlow"),
|
||
("ebbl", "bevelEmbossMulti", "bevelEmboss"),
|
||
("ChFX", "satinMulti", "satin"),
|
||
("SoFi", "solidFillMulti", "colorOverlay"),
|
||
("GrFl", "gradientFillMulti", "gradientOverlay"),
|
||
("FrFX", "frameFXMulti", "stroke"),
|
||
];
|
||
for (key, multi, id) in MAP {
|
||
let Some(d) = effect(fx, key, multi) else { continue };
|
||
let enabled = d.bool("enab").unwrap_or(true);
|
||
let mut ids = Ids(&mut proj.next_id);
|
||
let Some(uid) = styles::add_style(layer, &mut ids, id, gl, enabled) else { continue };
|
||
let Some(g) = layer.props.find_group_mut(uid) else { continue };
|
||
common(g, d);
|
||
match *id {
|
||
"dropShadow" => {
|
||
if let Some(v) = d.num("Ckmt") {
|
||
set(g, "spread", Value::Scalar(v));
|
||
}
|
||
if let Some(b) = d.bool("layerConceals") {
|
||
set(g, "knocksOut", Value::Bool(b));
|
||
}
|
||
}
|
||
"innerShadow" | "innerGlow" => {
|
||
if let Some(v) = d.num("Ckmt") {
|
||
set(g, "choke", Value::Scalar(v));
|
||
}
|
||
if d.enum_value("glwS") == Some("SrcC") {
|
||
set(g, "source", Value::Enum(0));
|
||
}
|
||
}
|
||
"outerGlow" => {
|
||
if let Some(v) = d.num("Ckmt") {
|
||
set(g, "spread", Value::Scalar(v));
|
||
}
|
||
}
|
||
"bevelEmboss" => {
|
||
let style = match d.enum_value("bvlS") {
|
||
Some("OtrB") => 0,
|
||
Some("Embs") => 2,
|
||
Some("PlEb") => 3,
|
||
Some("strokeEmboss") => 4,
|
||
_ => 1,
|
||
};
|
||
set(g, "style", Value::Enum(style));
|
||
let tech = match d.enum_value("bvlT") {
|
||
Some("PrBL") => 1,
|
||
Some("Slmt") => 2,
|
||
_ => 0,
|
||
};
|
||
set(g, "technique", Value::Enum(tech));
|
||
if let Some(v) = d.num("srgR") {
|
||
set(g, "depth", Value::Scalar(v));
|
||
}
|
||
set(g, "direction", Value::Enum(u32::from(d.enum_value("bvlD") == Some("Out "))));
|
||
if let Some(v) = d.num("Sftn") {
|
||
set(g, "soften", Value::Scalar(v));
|
||
}
|
||
if let Some(v) = d.num("Lald") {
|
||
set(g, "altitude", Value::Scalar(v));
|
||
}
|
||
for (k, p) in [("hglM", "highlightMode"), ("sdwM", "shadowMode")] {
|
||
if let Some(m) = desc_blend(d.enum_value(k)) {
|
||
set(g, p, style_mode(m));
|
||
}
|
||
}
|
||
for (k, p) in [("hglC", "highlightColor"), ("sdwC", "shadowColor")] {
|
||
if let Some(c) = d.color(k) {
|
||
set(g, p, color4(c));
|
||
}
|
||
}
|
||
for (k, p) in [("hglO", "highlightOpacity"), ("sdwO", "shadowOpacity")] {
|
||
if let Some(v) = d.num(k) {
|
||
set(g, p, Value::Scalar(v));
|
||
}
|
||
}
|
||
}
|
||
"satin" => {
|
||
if let Some(b) = d.bool("Invr") {
|
||
set(g, "invert", Value::Bool(b));
|
||
}
|
||
}
|
||
"gradientOverlay" => {
|
||
if let Some(gr) = gradient_of(d) {
|
||
set(g, "colors", Value::Gradient(gr));
|
||
}
|
||
if let Some(a) = d.num("Angl") {
|
||
set(g, "angle", Value::Scalar(a));
|
||
}
|
||
let st = match d.enum_value("Type") {
|
||
Some("Rdl ") => 1,
|
||
Some("Angl") => 2,
|
||
Some("Rflc") => 3,
|
||
Some("Dmnd") => 4,
|
||
_ => 0,
|
||
};
|
||
set(g, "style", Value::Enum(st));
|
||
if let Some(b) = d.bool("Rvrs") {
|
||
set(g, "reverse", Value::Bool(b));
|
||
}
|
||
if let Some(b) = d.bool("Algn") {
|
||
set(g, "alignWithLayer", Value::Bool(b));
|
||
}
|
||
if let Some(v) = d.num("Scl ") {
|
||
set(g, "scale", Value::Scalar(v));
|
||
}
|
||
}
|
||
"stroke" => {
|
||
if let Some(v) = d.num("Sz ") {
|
||
set(g, "size", Value::Scalar(v));
|
||
}
|
||
let pos = match d.enum_value("Styl") {
|
||
Some("InsF") => 1,
|
||
Some("CtrF") => 2,
|
||
_ => 0,
|
||
};
|
||
set(g, "position", Value::Enum(pos));
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Layer ▸ Create ▸ Convert to Editable Text: a footage layer showing a Photoshop type layer
|
||
/// becomes a text layer (`None` when the layer's source has no text data).
|
||
pub fn editable_text(proj: &mut Project, comp: &Comp, layer: &Layer, psd: &Psd, index: usize) -> Option<Layer> {
|
||
let t = psd.layers.get(index)?.text.as_ref()?;
|
||
let mut l = text_layer(proj, comp, &layer.name, t);
|
||
l.name = layer.name.clone();
|
||
l.blend_mode = layer.blend_mode;
|
||
l.switches.video = layer.switches.video;
|
||
l.in_point = layer.in_point;
|
||
l.out_point = layer.out_point;
|
||
l.start_time = layer.start_time;
|
||
if let (Some(o), Some(p)) = (layer.props.prop("transform/opacity"), l.props.prop_mut("transform/opacity")) {
|
||
*p = effectcraft_project::Property { uid: p.uid, ..o.clone() };
|
||
}
|
||
Some(l)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn blend_keys() {
|
||
assert_eq!(blend_mode("mul "), BlendMode::Multiply);
|
||
assert_eq!(blend_mode("smud"), BlendMode::Exclusion);
|
||
assert_eq!(blend_mode("pass"), BlendMode::Normal);
|
||
assert_eq!(desc_blend(Some("linearDodge")), Some(BlendMode::LinearDodge));
|
||
assert_eq!(font_names("Inter-Bold"), ("Inter".into(), "Bold".into()));
|
||
}
|
||
}
|