diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md
index 93b34b9bf..5bff5cdb0 100644
--- a/ATTRIBUTION.md
+++ b/ATTRIBUTION.md
@@ -64,7 +64,7 @@ and licence texts are in [`NOTICE`](NOTICE).
| `crates/model/tests/fixtures/cube.obj` | EffectCraft contributors | Original work: Wavefront OBJ test fixture (unit cube, two materials) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/cube.mtl` | EffectCraft contributors | Original work: MTL test fixture (materials of cube.obj) | MIT OR Apache-2.0 |
-One small After Effects test project of `crates/aep` is in this repository: `crates/aep/tests/fixtures/synthetic.aep` (original work, made with After Effects 2026 from own data, working space None, the embedded colour profile removed; MIT OR Apache-2.0, with its `.attribution` sidecar). The other After Effects test projects of `crates/aep` are not in this repository. They are original work of an EffectCraft contributor, generated with After Effects 2026 as own test data (MIT OR Apache-2.0, each file with its `.attribution` sidecar); they are in [storytold/effectcraft-corpus](https://github.com/storytold/effectcraft-corpus) with their sidecars, fetched by `cargo xtask aep-corpus` (commit-pinned, SHA-256-verified), and the tests read them only from the folder named by `EFFECTCRAFT_AEP_FIXTURES` (opt-in). Their embedded colour profiles are replaced by stubs made with `crates/aep/examples/stubicc.rs` (a header and a description, no colour data), so no vendor profile data is in them.
+Two small After Effects test projects of `crates/aep` are in this repository: `crates/aep/tests/fixtures/synthetic.aep` (original work, made with After Effects 2026 from own data, working space None, the embedded colour profile removed; MIT OR Apache-2.0, with its `.attribution` sidecar) and `crates/aep/tests/fixtures/ai-mask-3099.aep` (original work, made with After Effects 2026 from four hand-written one-page PDFs named `.ai`, footage paths and host name neutralised, the embedded colour profiles replaced by stubs made with `crates/aep/examples/stubicc.rs`; MIT OR Apache-2.0, with its `.attribution` sidecar). The other After Effects test projects of `crates/aep` are not in this repository. They are original work of an EffectCraft contributor, generated with After Effects 2026 as own test data (MIT OR Apache-2.0, each file with its `.attribution` sidecar); they are in [storytold/effectcraft-corpus](https://github.com/storytold/effectcraft-corpus) with their sidecars, fetched by `cargo xtask aep-corpus` (commit-pinned, SHA-256-verified), and the tests read them only from the folder named by `EFFECTCRAFT_AEP_FIXTURES` (opt-in). Their embedded colour profiles are replaced by stubs made with `crates/aep/examples/stubicc.rs` (a header and a description, no colour data), so no vendor profile data is in them.
## Screenshots
@@ -99,7 +99,7 @@ Screenshots of EffectCraft itself, rendered headlessly, showing only procedurall
(sidecars `.attribution`).
- Bundled extension (ScriptUI panel) with its twelve built-in ease presets:
`extensions/scriptui-panels/Ease Presets.jsx` (sidecar `.attribution`).
-- Example effect plug-in: `examples/plugins/posterize-bands` (plug-in API v1, WebAssembly).
+- Example effect plug-in: `examples/plugins/posterize-bands` (WebAssembly ABI 1).
## Trained models (optional downloads, not bundled)
diff --git a/Cargo.lock b/Cargo.lock
index 8e06b3afe..732251f19 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1184,7 +1184,7 @@ dependencies = [
[[package]]
name = "craft-update"
version = "0.1.0"
-source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
+source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
dependencies = [
"chrono",
"dunce",
@@ -7960,7 +7960,6 @@ dependencies = [
"nix",
"typed-path 0.13.0",
"windows-sys 0.61.2",
- "zopfli",
]
[[package]]
@@ -7975,18 +7974,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
-[[package]]
-name = "zopfli"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11"
-dependencies = [
- "bumpalo",
- "crc32fast",
- "log",
- "simd-adler32",
-]
-
[[package]]
name = "zune-core"
version = "0.5.3"
diff --git a/Cargo.toml b/Cargo.toml
index abb2256c2..b85c3c6b8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,7 @@ craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-l
craft-quit = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
craft-ecproj-format = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "ca1b5e2edb8568335c6c14f390307bd5beae8238" }
# craft-update: app-specific pretend versions apply to startup and manual checks.
-craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] }
+craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] }
# internal
effectcraft-time = { path = "crates/time" }
effectcraft-geom = { path = "crates/geom" }
diff --git a/README.md b/README.md
index 81b5a0ba5..913f8aa06 100644
--- a/README.md
+++ b/README.md
@@ -129,18 +129,14 @@ or into the Timeline, between layers and at the time you point to.
308 built-in effects across its categories, each with its parameter names, order and
defaults: blur and sharpen, channel, color correction (Curves, Levels, Hue/Saturation, a full
-color-grading panel…), distort (Warp, Bulge, Turbulent Displace, CC Power Pin…), generate (Fractal Noise,
+color-grading panel…), distort (Warp, Bulge, Turbulent Displace, Perspective Pin…), generate (Fractal Noise,
Gradient Ramp, Stroke, Write-on, Audio Spectrum…), keying, matte, noise and grain, perspective,
-**simulation** (CC Particle World, CC Rainfall, Shatter, Card Dance, Caustics, Wave World…),
-stylize (Glow, CC Glass…), **time** (Echo, Posterize Time, Timewarp, Time Displacement…),
+**simulation** (Particle Emitter 3D, Rainfall, Shatter, Card Dance, Caustics, Wave World…),
+stylize (Glow, Glass Relief…), **time** (Echo, Posterize Time, Timewarp, Time Displacement…),
**audio** (Reverb, Parametric EQ, Delay, Stereo Mixer…), text, transitions, utility and expression
controls. All nine **Layer Styles** (Drop Shadow, Inner/Outer Glow, Bevel and Emboss, Satin,
overlays, Stroke) with Global Light. Preview plays audio in sync, with meters and waveforms.
-
-
-
-
## 3D
Classic 3D compositing: 3D layers with orientation and material options,
diff --git a/apps/effectcraft-cli/src/main.rs b/apps/effectcraft-cli/src/main.rs
index 2981519e9..b8634178e 100644
--- a/apps/effectcraft-cli/src/main.rs
+++ b/apps/effectcraft-cli/src/main.rs
@@ -35,8 +35,8 @@
//!
//! Project: --project F.ecproj (or a positional *.ecproj) | --demo | --empty (default: demo; mcp: empty)
//! Saving: --save (back to --project) | --save-as F.ecproj
-//! GPU: --gpu renders on the GPU compositor (Mercury GPU Acceleration) when an adapter exists;
-//! the default is the CPU (Mercury Software Only)
+//! GPU: --gpu renders on the GPU compositor (GPU Acceleration) when an adapter exists;
+//! the default is the CPU (Software Rendering)
//! Bridge: --bridge PORT drives a running `effectcraft --control PORT` instead of a headless session
//! Output: --json for one compact JSON document on stdout (errors: {"error": …}, exit 1)
//! ```
@@ -466,7 +466,7 @@ fn backend(args: &Args, default_demo: bool) -> Result {
}
/// A wired session; `--gpu` attaches the GPU compositor (renders then follow the project's
-/// renderer setting, Mercury GPU Acceleration by default).
+/// renderer setting, GPU Acceleration by default).
fn session(args: &Args) -> Result {
let mut s = effectcraft_host::session();
if args.flag("--autosave") {
@@ -1443,7 +1443,7 @@ fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option Result<(), Failure> {
use effectcraft_render::Backend;
diff --git a/crates/aep/README.md b/crates/aep/README.md
index db415a026..9185cc8e7 100644
--- a/crates/aep/README.md
+++ b/crates/aep/README.md
@@ -71,13 +71,19 @@ tests reach them only through `tests/it/common/mod.rs`, which hands a test its c
of its own cut from the combined file; a test whose fixture is missing (or with the variable not
set) prints `SKIPPED: fixture missing` and passes.
-`tests/fixtures/synthetic.aep` (115 KB) is the one project in the repository: made in After Effects 2026
+`tests/fixtures/synthetic.aep` (115 KB) is one of the two projects in the repository: made in After Effects 2026
with the working space set to None (one composition, an image sequence of three 8x8 PNGs, a
solid, a folder, a Levels and a Glow effect with expressions). Every After Effects project embeds
the working space's ICC profile (HP sRGB IEC61966-2.1 for the default) in a JSON settings chunk;
the one `Utf8` chunk of this file that held a profile was replaced by `{}`, and nothing else was
changed. The tests in `tests/it/import/synthetic.rs` damage copies of it and need no other file.
+`tests/fixtures/ai-mask-3099.aep` (131 KB, #3099) is the second: four hand-written one-page PDFs
+named `.ai` with distinct page boxes, placed in a composition, one of them with two masks under a
+Null parented to a Null (generated by `tests/fixtures/tools/ai-mask-3099.py` and
+`ai-mask-3099.jsx`, scrubbed with `scrub.py` and `stubicc`). `tests/it/import/ai_mask.rs` checks
+the import against After Effects' read-back, and the host's `vector_import` tests render it.
+
## Tools
- `tests/fixtures/tools/`: the generator of the test projects (regenerates the corpus with After
diff --git a/crates/aep/src/export/plan.rs b/crates/aep/src/export/plan.rs
index 5ba2c3879..cd8d63400 100644
--- a/crates/aep/src/export/plan.rs
+++ b/crates/aep/src/export/plan.rs
@@ -915,7 +915,7 @@ fn plan_transform(layer: &Layer, src: &LayerSrc, comp: &Comp, source_size: (u32,
let cam_light = matches!(src, LayerSrc::Camera(_) | LayerSrc::Light(_));
let is_shape = matches!(src, LayerSrc::Shape(_));
let mut props = Vec::new();
- let at = PropAt { comp, comp_name, layer: &layer.name, source: source_size, text: is_text || cam_light || is_shape };
+ let at = PropAt { comp, comp_name, layer: &layer.name, source: source_size, text: is_text || cam_light || is_shape, null: matches!(src, LayerSrc::Null) };
if let Some(tr) = layer.transform() {
for kind in PropKind::ALL {
// A camera's or light's point of interest is After Effects' Anchor Point.
@@ -1271,16 +1271,21 @@ struct PropAt<'a> {
source: (u32, u32),
/// The anchor is stored in pixels (text, camera, light and shape layers).
text: bool,
+ /// A null layer: an Anchor Point that is not written is its top-left corner (#3099).
+ null: bool,
}
+/// What is written for transform property `p` (`kind`) of a layer: its value or keys in the
+/// stored unit and its expression; `None` when it is at After Effects' default and has neither.
fn plan_prop(kind: PropKind, label: Option<&str>, p: &Property, at: PropAt<'_>, cp: &CompPlan, col: &mut Collector) -> Option {
- let PropAt { comp, comp_name, layer, source, text } = at;
+ let PropAt { comp, comp_name, layer, source, text, null } = at;
// A text layer's anchor is stored in pixels, with the origin as its default (385).
let (sw, sh) = if text { (1.0, 1.0) } else { (f64::from(source.0.max(1)), f64::from(source.1.max(1))) };
let what = format!("{comp_name}: '{layer}' {}", label.unwrap_or(kind.match_name()));
let dims = kind.dims();
let default: Vec = match kind {
- PropKind::Anchor if text => vec![0.0, 0.0, 0.0],
+ // After Effects 2026 reads a null's absent Anchor Point as `[0, 0]` (#3099).
+ PropKind::Anchor if text || null => vec![0.0, 0.0, 0.0],
PropKind::Anchor => vec![0.5, 0.5, 0.0],
PropKind::Position => vec![f64::from(comp.width) / 2.0, f64::from(comp.height) / 2.0, 0.0],
PropKind::Scale => vec![1.0, 1.0, 1.0],
@@ -1418,7 +1423,7 @@ fn plan_prop(kind: PropKind, label: Option<&str>, p: &Property, at: PropAt<'_>,
/// back disabled).
fn plan_remap(p: &Property, comp: &Comp, cp: &CompPlan, layer: &str, comp_name: &str, col: &mut Collector) -> PropPlan {
// Rotation has the same layout (one component, stored as is, not spatial).
- let at = PropAt { comp, comp_name, layer, source: (1, 1), text: false };
+ let at = PropAt { comp, comp_name, layer, source: (1, 1), text: false, null: false };
let planned = plan_prop(PropKind::Rotation, Some("Time Remap"), p, at, cp, col);
col.exported.time_remaps += 1;
let mut plan = planned.unwrap_or_else(|| PropPlan { kind: PropKind::Rotation, value: vec![0.0], keys: Vec::new(), expr: None });
diff --git a/crates/aep/src/import/build.rs b/crates/aep/src/import/build.rs
index 21bfc70d8..1a920950b 100644
--- a/crates/aep/src/import/build.rs
+++ b/crates/aep/src/import/build.rs
@@ -285,6 +285,8 @@ fn import_footage(project: &mut Project, cx: &mut Ctx<'_>, item: &AepItem<'_, '_
cx.col.missing_footage.push(shown);
}
let kind = if sequence.is_empty() { footage_kind(&path, f.native_frame_rate, f.audio_sample_rate.is_some()) } else { FootageKind::Sequence };
+ // A placed PDF / Illustrator file keeps After Effects' framing and size (#3099).
+ let placed_page = kind == FootageKind::Still && is_placed_page_path(&path);
let mut footage = Footage {
path,
kind,
@@ -295,6 +297,7 @@ fn import_footage(project: &mut Project, cx: &mut Ctx<'_>, item: &AepItem<'_, '_
// footage), and only then.
has_audio: kind == FootageKind::Audio || (kind == FootageKind::Video && f.audio_sample_rate.is_some()),
missing: !present,
+ placed_page,
sequence,
..Footage::default()
};
@@ -327,6 +330,14 @@ fn import_footage(project: &mut Project, cx: &mut Ctx<'_>, item: &AepItem<'_, '_
}
}
+/// Whether `path` names a file whose page is framed as After Effects places it
+/// ([`Footage::placed_page`]): an Illustrator or PDF file, by its extension. EPS files keep
+/// their bounding box at their own size: how After Effects sizes them was not measured (#3099).
+fn is_placed_page_path(path: &str) -> bool {
+ let ext = path.rsplit(['/', '\\']).next().and_then(|name| name.rsplit_once('.')).map(|(_, ext)| ext.to_ascii_lowercase());
+ matches!(ext.as_deref(), Some("pdf" | "ai"))
+}
+
/// The name After Effects shows for a footage item that stores none: the file's name, or for a
/// sequence the name with the frame range (`shot_[0101-0103].exr`).
fn default_footage_name(item: &AepItem<'_, '_>, f: &crate::Footage) -> Option {
@@ -453,11 +464,13 @@ fn camera_light_orient(auto_oriented: bool, light: Option) -> AutoOri
}
/// The unit a layer's anchor is stored in.
-fn anchor_unit(camera_or_light: bool, text_or_shape: bool, size: (u32, u32)) -> AnchorUnit {
+fn anchor_unit(camera_or_light: bool, text_or_shape: bool, null: bool, size: (u32, u32)) -> AnchorUnit {
if camera_or_light {
AnchorUnit::PointOfInterest
} else if text_or_shape {
AnchorUnit::Pixels
+ } else if null {
+ AnchorUnit::Null(size.0, size.1)
} else {
AnchorUnit::Source(size.0, size.1)
}
@@ -591,7 +604,7 @@ fn build_layers(project: &mut Project, cx: &mut Ctx<'_>, defs: &EffectDefinition
import_layer_styles(&layer, &mut l, &comp.global_light, &mut Ids(&mut project.next_id), &tm, &what, &mut cx.col);
// Text and shape layers store their anchor in pixels (385, 462); cameras and lights their
// point of interest.
- let anchor = anchor_unit(is_camera || light.is_some(), is_text || is_shape, size);
+ let anchor = anchor_unit(is_camera || light.is_some(), is_text || is_shape, is_null, size);
if let Some(tr) = l.props.sub_mut("transform") {
import_transform(&layer, &name, comp_size, anchor, &tm, tr, &mut cx.col);
}
@@ -1125,6 +1138,18 @@ fn resolve_parents(
mod tests {
use super::*;
+ /// #3099: an `.ai` or `.pdf` from an `.aep` is a placed page; an EPS (not measured in After
+ /// Effects) and other files are not.
+ #[test]
+ fn only_illustrator_and_pdf_files_are_placed_pages() {
+ for path in [r"C:\fixture\x\logo.ai", "/work/Logo.AI", "art.pdf", r"C:\a.b\page.PDF"] {
+ assert!(is_placed_page_path(path), "{path}");
+ }
+ for path in [r"C:\fixture\x\still.eps", "a.epsf", "a.epsi", "still.png", r"C:\dir.ai\file", "ai", ""] {
+ assert!(!is_placed_page_path(path), "{path}");
+ }
+ }
+
#[test]
fn unlisted_extensions_follow_what_the_file_stores() {
assert_eq!(footage_kind("C:/clips/a.MP4", 0, false), FootageKind::Video);
diff --git a/crates/aep/src/import/props.rs b/crates/aep/src/import/props.rs
index eb9653612..747f71de1 100644
--- a/crates/aep/src/import/props.rs
+++ b/crates/aep/src/import/props.rs
@@ -136,8 +136,12 @@ const IGNORED: [&str; 4] = ["ADBE Position_0", "ADBE Position_1", "ADBE Position
/// How the stored Anchor Point becomes pixels.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AnchorUnit {
- /// A fraction of a source this big (pixels): AV layers.
+ /// A fraction of a source this big (pixels): AV layers; the source's centre when absent.
Source(u32, u32),
+ /// A fraction of a null object's source this big (pixels); its top-left corner when absent:
+ /// After Effects 2026 reads a null's absent Anchor Point as `[0, 0]` (#3099: two nulls made
+ /// with `addNull`, one left at its default, read back `[0, 0, 0]` and `[10, 20, 0]`).
+ Null(u32, u32),
/// Pixels as stored, the origin when absent: text (385) and shape (462) layers, which have no
/// source.
Pixels,
@@ -169,6 +173,14 @@ pub(crate) fn import_transform(
let (sw, sh) = (f64::from(w), f64::from(h));
(Kind::Anchor(sw, sh), tr.anchor_px(sw, sh))
}
+ AnchorUnit::Null(w, h) => {
+ let (sw, sh) = (f64::from(w), f64::from(h));
+ let px = match tr.anchor_fraction.source {
+ ValueSource::Default | ValueSource::Unreadable => [0.0; 3],
+ ValueSource::Stored | ValueSource::Keyframed => tr.anchor_px(sw, sh),
+ };
+ (Kind::Anchor(sw, sh), px)
+ }
AnchorUnit::Pixels | AnchorUnit::PointOfInterest => (Kind::Anchor(1.0, 1.0), tr.text_anchor_px()),
};
let poi = anchor == AnchorUnit::PointOfInterest;
diff --git a/crates/aep/tests/fixtures/ai-mask-3099.aep b/crates/aep/tests/fixtures/ai-mask-3099.aep
new file mode 100644
index 000000000..7223fda82
Binary files /dev/null and b/crates/aep/tests/fixtures/ai-mask-3099.aep differ
diff --git a/crates/aep/tests/fixtures/ai-mask-3099.aep.attribution b/crates/aep/tests/fixtures/ai-mask-3099.aep.attribution
new file mode 100644
index 000000000..86efa3b65
--- /dev/null
+++ b/crates/aep/tests/fixtures/ai-mask-3099.aep.attribution
@@ -0,0 +1,7 @@
+asset: ai-mask-3099.aep
+title: Small After Effects test project: placed .ai files with masks under two parented Nulls (#3099)
+author: EffectCraft contributors
+source: original work (made in After Effects 2026 from own data: four hand-written one-page PDFs named .ai, generated by tools/ai-mask-3099.py, imported and laid out by tools/ai-mask-3099.jsx)
+license: MIT OR Apache-2.0
+added: 2026-11-08 by EffectCraft contributors
+notes: Scrubbed with tools/scrub.py (footage paths C:\fixture\x\.ai of the same length, host name HOST000, the XMP packet of an empty project of the contributor); its four embedded colour profiles replaced by stubs (description only, no colour data) with crates/aep/examples/stubicc.rs, then checked with the corpus repository's tools/aep/inspect-embedded.py (CLEAN). No Adobe asset is in it.
diff --git a/crates/aep/tests/fixtures/tools/README.md b/crates/aep/tests/fixtures/tools/README.md
index eee0174ee..f1c720b2e 100644
--- a/crates/aep/tests/fixtures/tools/README.md
+++ b/crates/aep/tests/fixtures/tools/README.md
@@ -29,6 +29,7 @@ single-case files some cases are merged from, and the contributor's own animatio
| `scrub.py` | fixture hygiene of a saved `.aep`: footage alias paths replaced by neutral ones of the same length (`C:\fixtures\...`), host name `HOST000...`, neutral ascend counts, the XMP packet replaced by the base project's |
| `layers-comps-scrub-aepx.py` | the same XMP swap for the `.aepx` of the pair |
| `patch-text.py` | the byte edits of the text cases 732 and 733 (see below) |
+| `ai-mask-3099.py`, `ai-mask-3099.jsx` | the committed `../ai-mask-3099.aep` (#3099): the `.py` writes four one-page PDFs named `.ai` with distinct page boxes, the `.jsx` (ExtendScript, run once through an After Effects MCP server's script call in an empty untitled project) imports them, builds composition `ai-mask` and returns After Effects' read-back that `../../it/import/ai_mask.rs` compares against |
## Run order (per file)
diff --git a/crates/aep/tests/fixtures/tools/ai-mask-3099.jsx b/crates/aep/tests/fixtures/tools/ai-mask-3099.jsx
new file mode 100644
index 000000000..bcac03d0b
--- /dev/null
+++ b/crates/aep/tests/fixtures/tools/ai-mask-3099.jsx
@@ -0,0 +1,94 @@
+// Issue 3099: how After Effects sizes a placed .ai (PDF) and where masks / parented layers land.
+// Generates ai-mask-3099.aep: run `python -I ai-mask-3099.py ` first, set `dir` to that
+// folder, then run this script (ExtendScript) in an empty, untitled project. It returns After
+// Effects' read-back (footage sizes, transforms, mask vertices, toComp points; the tests compare
+// against it) and saves /i3099-ai-mask.aep, which is then scrubbed (README: steps 5 and 6).
+(function () {
+ var dir = "/";
+ var out = {};
+ if (app.project.file !== null || app.project.numItems !== 0) {
+ return "ABORT: project not empty/untitled (" + app.project.numItems + " items)";
+ }
+ var target = new File(dir + "i3099-ai-mask.aep");
+ if (target.exists) { return "ABORT: target exists"; }
+ var names = ["i3099-a-boxes.ai", "i3099-b-frac.ai", "i3099-c-rot.ai", "i3099-d-trim.ai"];
+ var items = {};
+ out.footage = {};
+ for (var i = 0; i < names.length; i++) {
+ var io = new ImportOptions(new File(dir + names[i]));
+ io.importAs = ImportAsType.FOOTAGE;
+ var it = app.project.importFile(io);
+ items[names[i]] = it;
+ out.footage[names[i]] = { w: it.width, h: it.height, par: it.pixelAspect };
+ }
+ var comp = app.project.items.addComp("ai-mask", 1920, 1080, 1, 5, 25);
+ var art = comp.layers.add(items["i3099-a-boxes.ai"]);
+ art.name = "art";
+ var artD = comp.layers.add(items["i3099-d-trim.ai"]);
+ artD.name = "artD";
+ artD.collapseTransformation = true;
+ var artC = comp.layers.add(items["i3099-c-rot.ai"]);
+ artC.name = "artC";
+ artC.property("ADBE Transform Group").property("ADBE Position").setValue([1500, 800]);
+ var n1 = comp.layers.addNull(); n1.name = "N1";
+ var n2 = comp.layers.addNull(); n2.name = "N2";
+ n2.setParentWithJump(n1);
+ art.setParentWithJump(n2);
+ artD.setParentWithJump(n2);
+ var t = function (l, p, v) { l.property("ADBE Transform Group").property(p).setValue(v); };
+ t(n1, "ADBE Position", [700, 400]);
+ t(n1, "ADBE Rotate Z", 15);
+ t(n1, "ADBE Scale", [80, 120]);
+ t(n2, "ADBE Anchor Point", [10, 20]);
+ t(n2, "ADBE Position", [50, -30]);
+ t(n2, "ADBE Scale", [150, 150]);
+ t(art, "ADBE Position", [0, 0]);
+ t(artD, "ADBE Position", [300, 250]);
+ // Two rectangular masks on each art layer, in layer pixels.
+ var rect = function (l, nm, x0, y0, x1, y1) {
+ var m = l.property("ADBE Mask Parade").addProperty("ADBE Mask Atom");
+ m.name = nm;
+ var s = new Shape();
+ s.vertices = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
+ s.closed = true;
+ m.property("ADBE Mask Shape").setValue(s);
+ };
+ rect(art, "M1", 40, 30, 200, 90);
+ rect(art, "M2", 220, 100, 350, 160);
+ rect(artD, "M1", 40, 30, 200, 90);
+ rect(artD, "M2", 220, 100, 350, 160);
+ // World positions of layer points through the parent chain (toComp), read via a Point Control.
+ var probe = n1.property("ADBE Effect Parade").addProperty("ADBE Point Control");
+ var pt = probe.property(1);
+ var toComp = function (ln, p) {
+ pt.expression = "thisComp.layer('" + ln + "').toComp([" + p[0] + "," + p[1] + "])";
+ var v = pt.valueAtTime(0, false);
+ return [v[0], v[1]];
+ };
+ var rd = function (l) {
+ var tg = l.property("ADBE Transform Group");
+ var r = {
+ w: l.width, h: l.height,
+ anchor: tg.property("ADBE Anchor Point").value,
+ position: tg.property("ADBE Position").value,
+ scale: tg.property("ADBE Scale").value,
+ rot: tg.property("ADBE Rotate Z").value,
+ masks: []
+ };
+ var mp = l.property("ADBE Mask Parade");
+ for (var k = 1; k <= mp.numProperties; k++) {
+ r.masks.push(mp.property(k).property("ADBE Mask Shape").value.vertices);
+ }
+ return r;
+ };
+ out.layers = { art: rd(art), artD: rd(artD), artC: rd(artC), N1: rd(n1), N2: rd(n2) };
+ out.world = {};
+ var pts = { tl: [0, 0], tr: [art.width, 0], bl: [0, art.height], m1: [40, 30], m2: [350, 160] };
+ for (var key in pts) { out.world["art_" + key] = toComp("art", pts[key]); }
+ var ptsD = { tl: [0, 0], tr: [artD.width, 0], bl: [0, artD.height], m1: [40, 30] };
+ for (var key2 in ptsD) { out.world["artD_" + key2] = toComp("artD", ptsD[key2]); }
+ probe.remove();
+ app.project.save(target);
+ out.saved = target.fsName;
+ return out;
+})();
diff --git a/crates/aep/tests/fixtures/tools/ai-mask-3099.py b/crates/aep/tests/fixtures/tools/ai-mask-3099.py
new file mode 100644
index 000000000..6b5b7facf
--- /dev/null
+++ b/crates/aep/tests/fixtures/tools/ai-mask-3099.py
@@ -0,0 +1,66 @@
+"""Writes hand-made one-page PDFs named .ai with distinct page boxes (issue 3099 repro)."""
+import os
+import sys
+
+out = sys.argv[1]
+os.makedirs(out, exist_ok=True)
+
+
+def pdf(page_extra, content):
+ objs = [
+ b"<< /Type /Catalog /Pages 2 0 R >>",
+ b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
+ b"<< /Type /Page /Parent 2 0 R " + page_extra.encode() + b" /Contents 4 0 R >>",
+ b"<< /Length " + str(len(content)).encode() + b" >>\nstream\n" + content + b"\nendstream",
+ ]
+ data = b"%PDF-1.4\n"
+ offs = []
+ for i, o in enumerate(objs, 1):
+ offs.append(len(data))
+ data += str(i).encode() + b" 0 obj\n" + o + b"\nendobj\n"
+ x = len(data)
+ data += b"xref\n0 " + str(len(objs) + 1).encode() + b"\n0000000000 65535 f \n"
+ for o in offs:
+ data += b"%010d 00000 n \n" % o
+ data += b"trailer\n<< /Size " + str(len(objs) + 1).encode() + b" /Root 1 0 R >>\nstartxref\n" + str(x).encode() + b"\n%%EOF\n"
+ return data
+
+
+def boxes_content(boxes):
+ # One outline per box (different colours) plus a filled marker at the media box origin corner.
+ cols = ["0 0 0", "1 0 0", "0 0.6 0", "0 0 1", "1 0.5 0"]
+ s = b"0.85 0.85 0.85 rg "
+ x0, y0, x1, y1 = boxes[0]
+ s += b"%g %g %g %g re f\n" % (x0, y0, x1 - x0, y1 - y0)
+ for c, (a, b, cc, d) in zip(cols, boxes):
+ s += c.encode() + b" RG 1 w %g %g %g %g re S\n" % (a + 0.5, b + 0.5, cc - a - 1, d - b - 1)
+ # A solid square in the lower-left of the last (innermost) box: shows y orientation.
+ a, b, _, _ = boxes[-1]
+ s += b"0.2 0.2 0.8 rg %g %g 20 10 re f" % (a + 4, b + 4)
+ return s
+
+
+def arr(b):
+ return "[%s]" % " ".join("%g" % v for v in b)
+
+
+# A: every box distinct, nested, non-square.
+media, crop, bleed, trim, art = [0, 0, 420, 220], [5, 5, 415, 215], [10, 10, 410, 210], [20, 20, 400, 200], [60, 50, 300.6, 170.4]
+a = pdf(
+ f"/MediaBox {arr(media)} /CropBox {arr(crop)} /BleedBox {arr(bleed)} /TrimBox {arr(trim)} /ArtBox {arr(art)}",
+ boxes_content([media, crop, bleed, trim, art]),
+)
+# B: only a fractional media box.
+mb = [0, 0, 300.6, 120.4]
+b = pdf(f"/MediaBox {arr(mb)}", boxes_content([mb]))
+# C: media box with an offset origin and /Rotate 90.
+mc = [100, 50, 400, 150]
+c = pdf(f"/MediaBox {arr(mc)} /Rotate 90", boxes_content([mc]))
+# D: media + trim + art, no crop box (bleed around the artboard).
+md, td, ad = [0, 0, 418, 218], [9, 9, 409, 209], [40, 30, 250, 150]
+d = pdf(f"/MediaBox {arr(md)} /TrimBox {arr(td)} /BleedBox {arr(md)} /ArtBox {arr(ad)}", boxes_content([md, td, ad]))
+
+for name, data in [("i3099-a-boxes.ai", a), ("i3099-b-frac.ai", b), ("i3099-c-rot.ai", c), ("i3099-d-trim.ai", d)]:
+ with open(os.path.join(out, name), "wb") as f:
+ f.write(data)
+ print(name, len(data))
diff --git a/crates/aep/tests/it/export/built.rs b/crates/aep/tests/it/export/built.rs
index 0f9784428..dc9729a9d 100644
--- a/crates/aep/tests/it/export/built.rs
+++ b/crates/aep/tests/it/export/built.rs
@@ -101,6 +101,45 @@ fn every_ntsc_rate_reads_back_as_the_rate_it_was_written_for() {
}
}
+/// #3099: After Effects reads a null's absent Anchor Point as its top-left corner `[0, 0]`. A
+/// null anchored at its centre is written (it was left out as "the default" and came back at the
+/// corner); one at the corner is not, and both import back as they were.
+#[test]
+fn null_anchor_points_round_trip() {
+ let mut p = Project::default();
+ let c = add_comp(&mut p, "C", 800, 600, FrameRate::new(30, 1), 2.0, None);
+ add_layer(&mut p, c, "Corner", LayerSource::Null, (100, 100));
+ add_layer(&mut p, c, "Centre", LayerSource::Null, (100, 100));
+ for (idx, anchor) in [(0, [50.0, 50.0, 0.0]), (1, [0.0, 0.0, 0.0])] {
+ with_layer(&mut p, c, idx, |l| {
+ if let Some(a) = l.props.prop_mut("transform/anchor") {
+ a.value = Value::Vec3(anchor);
+ }
+ });
+ }
+ let r = export_all(&p);
+ let file = parse(&r.bytes).expect("parse");
+ let view = effectcraft_aep::Project::new(&file).expect("project");
+ let comp = view.all_items().into_iter().find(|i| i.comp().is_some()).expect("comp");
+ let stored: Vec<_> =
+ comp.comp().expect("c").layers().map(|l| (l.name().unwrap_or_default(), l.transform(800, 600).expect("transform").anchor_fraction.source)).collect();
+ assert_eq!(stored, [("Centre".to_string(), effectcraft_aep::ValueSource::Stored), ("Corner".to_string(), effectcraft_aep::ValueSource::Default)]);
+ let mut back = Project::default();
+ let none = |_: &str| false;
+ let res = effectcraft_aep::import::import(
+ &mut back,
+ &r.bytes,
+ &effectcraft_aep::import::ImportOptions { allow_losses: true, ..effectcraft_aep::import::ImportOptions::new(&none) },
+ )
+ .expect("import");
+ let comp = res.comps.first().and_then(|id| back.comp(*id)).expect("comp");
+ for (name, want) in [("Centre", [50.0, 50.0]), ("Corner", [0.0, 0.0])] {
+ let l = comp.layers.iter().find(|l| l.name == name).expect("layer");
+ let a = l.props.prop("transform/anchor").expect("anchor").value.as_vec2();
+ assert!(close(a[0], want[0]) && close(a[1], want[1]), "{name}: {a:?}");
+ }
+}
+
#[test]
fn layer_header_fields() {
let mut p = Project::default();
diff --git a/crates/aep/tests/it/import.rs b/crates/aep/tests/it/import.rs
index 2d1878a37..aa4695029 100644
--- a/crates/aep/tests/it/import.rs
+++ b/crates/aep/tests/it/import.rs
@@ -4,6 +4,7 @@
mod helpers;
mod aepx;
+mod ai_mask;
mod blend_modes;
mod bounds;
mod cameras;
diff --git a/crates/aep/tests/it/import/ai_mask.rs b/crates/aep/tests/it/import/ai_mask.rs
new file mode 100644
index 000000000..e5f57abe0
--- /dev/null
+++ b/crates/aep/tests/it/import/ai_mask.rs
@@ -0,0 +1,113 @@
+//! A placed Illustrator file with masks under two parented Nulls (#3099), against After Effects
+//! 2026's own read-back of the committed project `tests/fixtures/ai-mask-3099.aep` (generator:
+//! `tests/fixtures/tools/ai-mask-3099.py` and `ai-mask-3099.jsx`). Most of these guard After
+//! Effects' values the importer already read so (the drawing of the page is tested in
+//! `effectcraft-media` and the host's `vector_import` tests); the Anchor Point of the null left
+//! at its default (`[0, 0]`, not the centre) was wrong before #3099.
+//!
+//! The four `.ai` files are hand-written one-page PDFs with distinct page boxes. After Effects
+//! sized them by their ArtBox (a, d), by the MediaBox rounded up (b: 300.6 × 120.4 → 301 × 121)
+//! and ignored `/Rotate 90` (c). Composition `ai-mask` (1920 × 1080): `art` (a) and `artD` (d,
+//! Continuously Rasterize) are children of Null `N2`, itself a child of Null `N1`; both carry two
+//! rectangular masks; `artC` (c) is on its own.
+
+use effectcraft_aep::import::{ImportOptions, import};
+use effectcraft_keyframe::Value;
+use effectcraft_project::{ItemKind, Layer, Project};
+
+use crate::import::helpers::comp;
+
+const AI_MASK: &[u8] = include_bytes!("../../fixtures/ai-mask-3099.aep");
+
+/// The fixture imported into an empty project (losses allowed, no footage file present).
+fn load() -> (Project, effectcraft_aep::import::ImportResult) {
+ let mut project = Project::default();
+ let none = |_: &str| false;
+ let r = import(&mut project, AI_MASK, &ImportOptions { allow_losses: true, ..ImportOptions::new(&none) }).unwrap();
+ (project, r)
+}
+
+/// The first two components of the static value of property `path` of `l`.
+fn vec2(l: &Layer, path: &str) -> [f64; 2] {
+ let v = l.props.prop(path).unwrap_or_else(|| panic!("{}: {path}", l.name)).value.as_vec2();
+ [v[0], v[1]]
+}
+
+/// `a` and `b` agree within float32 noise.
+fn near(a: [f64; 2], b: [f64; 2]) -> bool {
+ (a[0] - b[0]).abs() < 1e-3 && (a[1] - b[1]).abs() < 1e-3
+}
+
+/// The four `.ai` items have After Effects' sizes and are placed pages.
+#[test]
+fn placed_ai_sizes_are_after_effects_sizes() {
+ let (p, _) = load();
+ let mut sizes: Vec<(String, (u32, u32))> = p
+ .items
+ .values()
+ .filter_map(|i| match &i.kind {
+ ItemKind::Footage(f) if f.path.ends_with(".ai") => {
+ // Framed and drawn as After Effects places the page (#3099).
+ assert!(f.placed_page, "{}", i.name);
+ Some((i.name.clone(), (f.width, f.height)))
+ }
+ _ => None,
+ })
+ .collect();
+ sizes.sort();
+ assert_eq!(
+ sizes,
+ [
+ ("i3099-a-boxes.ai".to_string(), (241, 121)),
+ ("i3099-b-frac.ai".to_string(), (301, 121)),
+ ("i3099-c-rot.ai".to_string(), (300, 100)),
+ ("i3099-d-trim.ai".to_string(), (210, 120)),
+ ]
+ );
+}
+
+/// Parents, transforms (the default null anchor at `[0, 0]`) and both masks of each art layer
+/// as After Effects read them back.
+#[test]
+fn masks_anchors_and_parents_are_after_effects_values() {
+ let (p, r) = load();
+ let (_, c) = comp(&p, &r, "ai-mask");
+ let layer = |name: &str| c.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("no layer {name}"));
+ let (art, art_d, art_c, n1, n2) = (layer("art"), layer("artD"), layer("artC"), layer("N1"), layer("N2"));
+ // Parents: art, artD -> N2 -> N1.
+ assert_eq!(art.parent, Some(n2.id));
+ assert_eq!(art_d.parent, Some(n2.id));
+ assert_eq!(n2.parent, Some(n1.id));
+ assert_eq!(n1.parent, None);
+ assert_eq!(art_c.parent, None);
+ // Transforms as After Effects read them back (anchor = half the source size for footage).
+ for (l, anchor, position, scale) in [
+ (art, [120.5, 60.5], [0.0, 0.0], [100.0, 100.0]),
+ (art_d, [105.0, 60.0], [300.0, 250.0], [100.0, 100.0]),
+ (art_c, [150.0, 50.0], [1500.0, 800.0], [100.0, 100.0]),
+ (n1, [0.0, 0.0], [700.0, 400.0], [80.0, 120.0]),
+ (n2, [10.0, 20.0], [50.0, -30.0], [150.0, 150.0]),
+ ] {
+ assert!(near(vec2(l, "transform/anchor"), anchor), "{} anchor {:?}", l.name, vec2(l, "transform/anchor"));
+ assert!(near(vec2(l, "transform/position"), position), "{} position {:?}", l.name, vec2(l, "transform/position"));
+ assert!(near(vec2(l, "transform/scale"), scale), "{} scale {:?}", l.name, vec2(l, "transform/scale"));
+ }
+ match n1.props.prop("transform/rotation").map(|p| &p.value) {
+ Some(Value::Scalar(v)) => assert!((v - 15.0).abs() < 1e-9, "N1 rotation {v}"),
+ other => panic!("N1 rotation {other:?}"),
+ }
+ // Both art layers: M1 (40, 30)-(200, 90) and M2 (220, 100)-(350, 160) in layer pixels, the
+ // second reaching past the layer, as After Effects read the vertices back.
+ let rects = [[[40.0, 30.0], [200.0, 30.0], [200.0, 90.0], [40.0, 90.0]], [[220.0, 100.0], [350.0, 100.0], [350.0, 160.0], [220.0, 160.0]]];
+ for l in [art, art_d] {
+ let masks: Vec<_> = l.masks().expect("masks").groups().collect();
+ assert_eq!(masks.len(), 2, "{}", l.name);
+ for (m, want) in masks.iter().zip(&rects) {
+ let Some(Value::Path(path)) = m.get("path").map(|p| &p.value) else { panic!("{}: {} has no path", l.name, m.name) };
+ assert_eq!(path.vertices.len(), 4, "{}: {}", l.name, m.name);
+ for (v, w) in path.vertices.iter().zip(want) {
+ assert!(near(*v, *w), "{}: {} vertex {v:?} != {w:?}", l.name, m.name);
+ }
+ }
+ }
+}
diff --git a/crates/automation/tests/qa/compositing.rs b/crates/automation/tests/qa/compositing.rs
index d5de8f687..3cf8356fb 100644
--- a/crates/automation/tests/qa/compositing.rs
+++ b/crates/automation/tests/qa/compositing.rs
@@ -106,7 +106,7 @@ fn track_matte(qa: &mut Qa) {
fn adjustment_layer(qa: &mut Qa, normal: &Img) {
// Adjustment layer: Lumetri exposure, inside a feathered, expanded elliptical mask; then Glow.
qa.exec("layer.newAdjustment", json!({"name": "Grade"}));
- let fx = qa.exec("effect.apply", json!({"effect": "Lumetri Color", "layers": ["Grade"]}));
+ let fx = qa.exec("effect.apply", json!({"effect": "Color Grading", "layers": ["Grade"]}));
assert_eq!(fx["paths"], json!(["effects/#1"]), "effect.apply answers with the instance path: {fx}");
qa.tool("set_property", json!({"layer": "Grade", "path": "effects/#1/basicCorrection/tone/exposure", "value": 2}));
let before = qa.frame(None, 0.5);
diff --git a/crates/effects/src/blur2.rs b/crates/effects/src/blur2.rs
index 929f41553..880ad16c5 100644
--- a/crates/effects/src/blur2.rs
+++ b/crates/effects/src/blur2.rs
@@ -1,6 +1,6 @@
//! Blur & Sharpen, batch 2: edge-preserving blurs (Bilateral, Smart), per-channel and map-driven
//! blurs (Channel, Compound, CC Vector), bokeh (Camera Lens Blur), radial streaks and small
-//! utilities (Reduce Interlace Flicker, CC Cross Blur).
+//! utilities (Reduce Interlace Flicker, Cross Blur).
use effectcraft_color::luminance;
use effectcraft_keyframe::Value;
@@ -507,9 +507,9 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
// ---------------------------------------------------------------------------------------------
-// CC Radial Fast Blur
+// Fast Radial Blur
-/// Transparent pixels CC Radial Fast Blur adds on every side of a `w` × `h` buffer with Center
+/// Transparent pixels Fast Radial Blur adds on every side of a `w` × `h` buffer with Center
/// at `c` (buffer pixels) and smear `s`: content `r` from Center streaks out to `r / (1 − s)`,
/// so the farthest corner reaches `d · s / (1 − s)` past itself (at most 4096, as the other
/// growing effects). Shared with the GPU path so both grow the same way.
@@ -562,7 +562,7 @@ fn radial_fast_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
// ---------------------------------------------------------------------------------------------
-// CC Vector Blur
+// Vector Blur
fn vector_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
let amount = ctx.params.f("amount").max(0.0) * b.scale;
@@ -616,7 +616,7 @@ fn vector_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
// ---------------------------------------------------------------------------------------------
-// Reduce Interlace Flicker / CC Cross Blur
+// Reduce Interlace Flicker / Cross Blur
fn reduce_flicker(ctx: &EffectCtx, mut b: Buf) -> Buf {
let s = ctx.params.f("softness").max(0.0) * b.scale;
@@ -735,7 +735,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.blur.ccradialfast",
- "CC Radial Fast Blur",
+ "Fast Radial Blur",
vec![
p("center", "Center", Value::Vec2([0.5, 0.5]), ParamUi::Point),
p("amount", "Amount", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
@@ -745,7 +745,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.blur.ccvector",
- "CC Vector Blur",
+ "Vector Blur",
vec![
p("type", "Type", Value::Enum(0), popup(&["Natural", "Constant Length", "Perpendicular", "Direction Center", "Direction Fading"])),
p("amount", "Amount", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
@@ -765,7 +765,7 @@ pub fn specs() -> Vec {
},
spec(
"ec.blur.cccross",
- "CC Cross Blur",
+ "Cross Blur",
vec![
p("radiusX", "Radius X", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
p("radiusY", "Radius Y", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
diff --git a/crates/effects/src/blur3.rs b/crates/effects/src/blur3.rs
index aee2a2713..6369a8ced 100644
--- a/crates/effects/src/blur3.rs
+++ b/crates/effects/src/blur3.rs
@@ -1,6 +1,6 @@
-//! Blur & Sharpen: CC Radial Blur and Camera-Shake Deblur.
+//! Blur & Sharpen: Variable Radial Blur and Camera-Shake Deblur.
//!
-//! * **CC Radial Blur** — zoom (straight, fading, centred both ways) and rotational (plain or
+//! * **Variable Radial Blur** — zoom (straight, fading, centred both ways) and rotational (plain or
//! fading) blurs around a centre point, sampled along the ray / arc through each pixel with a
//! sample count set by Quality.
//! * **Camera-Shake Deblur** — frame-based restoration in the spirit of published video deblurring
@@ -22,7 +22,7 @@ fn spec(id: &'static str, name: &'static str, params: Vec, ren
EffectSpec { id, name, category: "Blur & Sharpen", params, render, gpu: false, float: true }
}
-// ---------------------------------------------------------------- CC Radial Blur
+// ---------------------------------------------------------------- Variable Radial Blur
pub const RADIAL_TYPES: [&str; 5] = ["Straight Zoom", "Fading Zoom", "Centered", "Rotate", "Rotate Fading"];
@@ -246,7 +246,7 @@ pub fn specs() -> Vec {
vec![
spec(
"ec.blur.ccradial",
- "CC Radial Blur",
+ "Variable Radial Blur",
vec![
p("type", "Type", Value::Enum(1), popup(&RADIAL_TYPES)),
p("amount", "Amount", num(10.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
diff --git a/crates/effects/src/channel2.rs b/crates/effects/src/channel2.rs
index 5be3c94e7..ef391f6f1 100644
--- a/crates/effects/src/channel2.rs
+++ b/crates/effects/src/channel2.rs
@@ -1,5 +1,5 @@
//! Channel effects, batch 2: layer-to-layer combinations (Blend, Calculations, Compound
-//! Arithmetic) and CC Composite. Second layers come from layer parameters through the effect
+//! Arithmetic) and Source Composite. Second layers come from layer parameters through the effect
//! host; with no layer chosen the effect uses the layer itself, as After Effects does.
use effectcraft_color::{BlendMode, blend_pixel, hsl_to_rgb, luminance, rgb_to_hsl};
@@ -69,7 +69,7 @@ fn blend(ctx: &EffectCtx, mut b: Buf) -> Buf {
// ---------------------------------------------------------------- Calculations
-/// Blending modes offered by Calculations / CC Composite (menu order).
+/// Blending modes offered by Calculations / Source Composite (menu order).
const CALC_MODES: [BlendMode; 22] = [
BlendMode::Normal,
BlendMode::Darken,
@@ -150,7 +150,7 @@ fn calculations(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Composite
+// ---------------------------------------------------------------- Source Composite
const CC_COMPOSITE_MODES: [&str; 16] = [
"In Front",
@@ -321,7 +321,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.channel.cccomposite",
- "CC Composite",
+ "Source Composite",
vec![
p("opacity", "Opacity", num(100.0), pct()),
p("compositeOriginal", "Composite Original", Value::Enum(0), popup(&CC_COMPOSITE_MODES)),
diff --git a/crates/effects/src/color2.rs b/crates/effects/src/color2.rs
index 25203cd7a..6639e649e 100644
--- a/crates/effects/src/color2.rs
+++ b/crates/effects/src/color2.rs
@@ -878,7 +878,7 @@ fn broadcast_colors(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
// ---------------------------------------------------------------------------------------------
-// CC Toner, CC Color Offset, CC Kernel
+// Multi-Tone Tint, Channel Offset, Convolution Kernel
fn cc_toner(ctx: &EffectCtx, mut b: Buf) -> Buf {
let g = |id: &str| {
@@ -1101,7 +1101,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.color.cctoner",
- "CC Toner",
+ "Multi-Tone Tint",
vec![
p("tones", "Tones", Value::Enum(1), popup(&["Duotone", "Tritone", "Pentone"])),
p("highlights", "Highlights", col(1.0, 1.0, 1.0), ParamUi::Color),
@@ -1115,7 +1115,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.color.cccoloroffset",
- "CC Color Offset",
+ "Channel Offset",
vec![
p("redPhase", "Red Phase", num(0.0), ParamUi::Angle),
p("greenPhase", "Green Phase", num(0.0), ParamUi::Angle),
@@ -1124,7 +1124,7 @@ pub fn specs() -> Vec {
],
cc_color_offset,
),
- spec("ec.color.cckernel", "CC Kernel", k_params, cc_kernel),
+ spec("ec.color.cckernel", "Convolution Kernel", k_params, cc_kernel),
]
}
diff --git a/crates/effects/src/color3.rs b/crates/effects/src/color3.rs
index d4cdedd98..d0d398100 100644
--- a/crates/effects/src/color3.rs
+++ b/crates/effects/src/color3.rs
@@ -1,5 +1,5 @@
-//! Color Correction effects, batch 3: Change Color, Video Limiter, CC Color Neutralizer,
-//! PS Arbitrary Map and Lumetri Color (Basic Correction, Creative, Curves, Color Wheels,
+//! Color Correction effects, batch 3: Change Color, Video Limiter, Color Neutralizer,
+//! PS Arbitrary Map and Color Grading (Basic Correction, Creative, Curves, Color Wheels,
//! Vignette).
use effectcraft_color::{hsl_to_rgb, luminance, rgb_to_hsl};
@@ -154,7 +154,7 @@ fn video_limiter(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Color Neutralizer
+// ---------------------------------------------------------------- Color Neutralizer
fn color_neutralizer(ctx: &EffectCtx, mut b: Buf) -> Buf {
let pairs = [("shadowsUnbalance", "shadowsBalance"), ("midtonesUnbalance", "midtonesBalance"), ("highlightsUnbalance", "highlightsBalance")];
@@ -225,7 +225,7 @@ fn arbitrary_map(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- Lumetri Color
+// ---------------------------------------------------------------- Color Grading
/// Smooth tone-curve adjustment: offsets for shadows/midtones/highlights (each -1..1), with the
/// end points fixed.
@@ -431,7 +431,7 @@ impl HslKey {
}
}
-/// Lumetri Color: Basic Correction (input LUT, white balance, HDR range, tone, saturation),
+/// Color Grading: Basic Correction (input LUT, white balance, HDR range, tone, saturation),
/// Creative (look, adjustments), Curves (tone sliders, RGB curves, hue/saturation curves),
/// Color Wheels, HSL Secondary (key, refine, correction), Vignette, in that order.
fn lumetri(ctx: &EffectCtx, mut b: Buf) -> Buf {
@@ -834,7 +834,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.color.cccolorneutralizer",
- "CC Color Neutralizer",
+ "Color Neutralizer",
vec![
p("shadowsUnbalance", "Shadows Unbalance", col(0.0, 0.0, 0.0), ParamUi::Color),
p("shadowsBalance", "Shadows Balance", col(0.0, 0.0, 0.0), ParamUi::Color),
@@ -860,7 +860,7 @@ pub fn specs() -> Vec {
],
arbitrary_map,
),
- spec("ec.color.lumetri", "Lumetri Color", lum, lumetri),
+ spec("ec.color.lumetri", "Color Grading", lum, lumetri),
]
}
diff --git a/crates/effects/src/display_names.rs b/crates/effects/src/display_names.rs
new file mode 100644
index 000000000..b30c00d46
--- /dev/null
+++ b/crates/effects/src/display_names.rs
@@ -0,0 +1,123 @@
+//! Display naming must not change file, command or script identities.
+use crate::{find, instantiate, lookup, name_matches};
+use effectcraft_project::build::Ids;
+
+const RENAMED: &[(&str, &str, &str)] = &[
+ ("ec.sim.ccballaction", "CC Ball Action", "Ball Grid"),
+ ("ec.distort.ccbendit", "CC Bend It", "Two-Point Bend"),
+ ("ec.distort.ccbender", "CC Bender", "Flexible Bend"),
+ ("ec.distort.ccblobbylize", "CC Blobbylize", "Organic Displacement"),
+ ("ec.stylize.ccblockload", "CC Block Load", "Block Reveal"),
+ ("ec.sim.ccbubbles", "CC Bubbles", "Bubbles"),
+ ("ec.stylize.ccburnfilm", "CC Burn Film", "Film Burn"),
+ ("ec.color.cccolorneutralizer", "CC Color Neutralizer", "Color Neutralizer"),
+ ("ec.color.cccoloroffset", "CC Color Offset", "Channel Offset"),
+ ("ec.channel.cccomposite", "CC Composite", "Source Composite"),
+ ("ec.blur.cccross", "CC Cross Blur", "Cross Blur"),
+ ("ec.perspective.cccylinder", "CC Cylinder", "Cylinder Wrap"),
+ ("ec.sim.ccdrizzle", "CC Drizzle", "Water Ripples"),
+ ("ec.perspective.ccenvironment", "CC Environment", "Environment Map"),
+ ("ec.distort.ccflomotion", "CC Flo Motion", "Dual Lens Warp"),
+ ("ec.time.ccforcemotionblur", "CC Force Motion Blur", "Temporal Motion Blur"),
+ ("ec.stylize.ccglass", "CC Glass", "Glass Relief"),
+ ("ec.transition.ccglasswipe", "CC Glass Wipe", "Glass Wipe"),
+ ("ec.generate.ccgluegun", "CC Glue Gun", "Paint Beads"),
+ ("ec.transition.ccgridwipe", "CC Grid Wipe", "Grid Wipe"),
+ ("ec.distort.ccgriddler", "CC Griddler", "Tile Grid"),
+ ("ec.sim.cchair", "CC Hair", "Surface Fibers"),
+ ("ec.stylize.cchextile", "CC HexTile", "Hexagonal Tiles"),
+ ("ec.transition.ccimagewipe", "CC Image Wipe", "Luminance Image Wipe"),
+ ("ec.transition.ccjaws", "CC Jaws", "Serrated Wipe"),
+ ("ec.stylize.cckaleida", "CC Kaleida", "Kaleidoscope"),
+ ("ec.color.cckernel", "CC Kernel", "Convolution Kernel"),
+ ("ec.distort.cclens", "CC Lens", "Lens Distortion"),
+ ("ec.generate.cclightburst", "CC Light Burst 2.5", "Radial Light Burst"),
+ ("ec.generate.cclightrays", "CC Light Rays", "Light Rays"),
+ ("ec.generate.cclightsweep", "CC Light Sweep", "Light Sweep"),
+ ("ec.transition.cclightwipe", "CC Light Wipe", "Light Wipe"),
+ ("ec.transition.cclinesweep", "CC Line Sweep", "Line Sweep"),
+ ("ec.sim.ccmrmercury", "CC Mr. Mercury", "Liquid Particles"),
+ ("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "Palette Map"),
+ ("ec.utility.ccoverbrights", "CC Overbrights", "Out-of-Range Display"),
+ ("ec.distort.ccpageturn", "CC Page Turn", "Page Curl"),
+ ("ec.sim.ccparticlesystems2", "CC Particle Systems II", "Particle Emitter 2D"),
+ ("ec.sim.ccparticleworld", "CC Particle World", "Particle Emitter 3D"),
+ ("ec.sim.ccpixelpolly", "CC Pixel Polly", "Pixel Shatter"),
+ ("ec.stylize.ccplastic", "CC Plastic", "Plastic Shading"),
+ ("ec.distort.ccpowerpin", "CC Power Pin", "Perspective Pin"),
+ ("ec.blur.ccradial", "CC Radial Blur", "Variable Radial Blur"),
+ ("ec.blur.ccradialfast", "CC Radial Fast Blur", "Fast Radial Blur"),
+ ("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "Radial Scale Wipe"),
+ ("ec.sim.ccrainfall", "CC Rainfall", "Rainfall"),
+ ("ec.stylize.ccrepetile", "CC RepeTile", "Edge Tile Repeat"),
+ ("ec.distort.ccripplepulse", "CC Ripple Pulse", "Pulsed Ripple"),
+ ("ec.transition.ccscalewipe", "CC Scale Wipe", "Scale Wipe"),
+ ("ec.sim.ccscatterize", "CC Scatterize", "Pixel Scatter"),
+ ("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "Line Removal"),
+ ("ec.distort.ccslant", "CC Slant", "Slant"),
+ ("ec.distort.ccsmear", "CC Smear", "Directional Smear"),
+ ("ec.sim.ccsnowfall", "CC Snowfall", "Snowfall"),
+ ("ec.perspective.ccsphere", "CC Sphere", "Sphere Wrap"),
+ ("ec.distort.ccsplit", "CC Split", "Split Warp"),
+ ("ec.distort.ccsplit2", "CC Split 2", "Dual Split Warp"),
+ ("ec.perspective.ccspotlight", "CC Spotlight", "Surface Spotlight"),
+ ("ec.sim.ccstarburst", "CC Star Burst", "Starfield"),
+ ("ec.generate.ccthreads", "CC Threads", "Woven Threads"),
+ ("ec.stylize.ccthreshold", "CC Threshold", "Channel Threshold"),
+ ("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "RGB Threshold"),
+ ("ec.distort.cctiler", "CC Tiler", "Scaled Tiles"),
+ ("ec.color.cctoner", "CC Toner", "Multi-Tone Tint"),
+ ("ec.transition.cctwister", "CC Twister", "Twist Wipe"),
+ ("ec.blur.ccvector", "CC Vector Blur", "Vector Blur"),
+ ("ec.stylize.ccvignette", "CC Vignette", "Vignette"),
+ ("ec.transition.ccwarpomatic", "CC WarpoMatic", "Warp Dissolve"),
+ ("ec.time.ccwidetime", "CC Wide Time", "Temporal Blend"),
+ ("ec.color.lumetri", "Lumetri Color", "Color Grading"),
+ ("ec.obsolete.mochashape", "Mocha shape", "Tracked Shape Matte"),
+ ("ec.keying.keylight", "Key Light", "Screen Color Key"),
+];
+
+#[test]
+fn descriptive_names_keep_every_legacy_lookup_and_search() {
+ for &(id, old, new) in RENAMED {
+ let spec = find(id).unwrap();
+ assert_eq!(spec.name, new, "{id}");
+ for query in [id.to_string(), old.to_string(), old.to_lowercase(), new.to_string()] {
+ assert_eq!(lookup(&query).map(|s| s.id), Some(id), "{query}");
+ }
+ assert!(name_matches(spec, &old.to_lowercase()), "{old}");
+ assert!(name_matches(spec, &new.to_lowercase()), "{new}");
+ }
+ for alias in ["Keylight", "Keylight (1.2)", "Key Light"] {
+ assert_eq!(lookup(alias).map(|s| s.id), Some("ec.keying.keylight"));
+ }
+}
+
+#[test]
+fn descriptive_display_names_do_not_collide() {
+ let mut names = std::collections::HashSet::new();
+ for spec in crate::registry() {
+ assert!(names.insert(spec.name.to_lowercase()), "duplicate name {}", spec.name);
+ let label = spec.name.to_lowercase();
+ assert!(!label.starts_with("cc "), "{}", spec.name);
+ for mark in ["lumetri", "mercury", "mocha", "keylight"] {
+ assert!(!label.contains(mark), "{}", spec.name);
+ }
+ }
+}
+
+#[test]
+fn legacy_instance_names_and_parameter_data_survive_refresh() {
+ for &(id, old, _) in RENAMED {
+ let spec = find(id).unwrap();
+ for stored in [old.to_string(), format!("{old} 2"), "My custom instance".to_string()] {
+ let mut next = 1;
+ let mut ids = Ids(&mut next);
+ let mut instance = instantiate(spec, &mut ids, &stored, [128.0, 72.0]);
+ let before = serde_json::to_value(&instance).unwrap();
+ crate::migrate::upgrade_instance(spec, &mut instance, &mut ids, [128.0, 72.0]);
+ assert_eq!(instance.name, stored, "{id}: never rewrite a stored instance name");
+ assert_eq!(serde_json::to_value(&instance).unwrap(), before, "{id}");
+ }
+ }
+}
diff --git a/crates/effects/src/distort2.rs b/crates/effects/src/distort2.rs
index 7add5fc58..bbd8738a8 100644
--- a/crates/effects/src/distort2.rs
+++ b/crates/effects/src/distort2.rs
@@ -1,6 +1,6 @@
//! Distort effects, batch 2: Turbulent Displace, Displacement Map (self as map), Optics
-//! Compensation, Magnify, Mesh Warp, Bezier Warp, CC Bend It, CC Lens, CC Page Turn, CC Tiler,
-//! CC Griddler.
+//! Compensation, Magnify, Mesh Warp, Bezier Warp, Two-Point Bend, Lens Distortion, Page Curl, Scaled Tiles,
+//! Tile Grid.
//!
//! Mesh Warp and Bezier Warp share a grid rasteriser: a (nx+1)×(ny+1) grid of destination points
//! and matching source points is split into triangles, bucketed per output row and filled
@@ -550,7 +550,7 @@ pub fn bezier_grid(ctx: &EffectCtx, offset: [f64; 2], scale: f64) -> (usize, Vec
(n, dest, srcp)
}
-// ---------------------------------------------------------------- CC Bend It
+// ---------------------------------------------------------------- Two-Point Bend
fn bend_it(ctx: &EffectCtx, mut b: Buf) -> Buf {
let mut beta = ctx.params.f("bend").to_radians();
@@ -606,7 +606,7 @@ fn bend_it(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Lens
+// ---------------------------------------------------------------- Lens Distortion
fn cc_lens(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = b.to_px(ctx.params.v2("center"));
@@ -629,7 +629,7 @@ fn cc_lens(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Page Turn
+// ---------------------------------------------------------------- Page Curl
fn page_turn(ctx: &EffectCtx, mut b: Buf) -> Buf {
let f = b.to_px(ctx.params.v2("foldPosition"));
@@ -679,7 +679,7 @@ fn page_turn(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Tiler / CC Griddler
+// ---------------------------------------------------------------- Scaled Tiles / Tile Grid
fn cc_tiler(ctx: &EffectCtx, mut b: Buf) -> Buf {
let s = (ctx.params.f("scale") / 100.0).max(0.001);
@@ -829,7 +829,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccbendit",
- "CC Bend It",
+ "Two-Point Bend",
vec![
p("bend", "Bend", num(0.0), ParamUi::Angle),
p("start", "Start", pt(0.5, 0.2), ParamUi::Point),
@@ -840,7 +840,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.cclens",
- "CC Lens",
+ "Lens Distortion",
vec![
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
p("size", "Size", num(50.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
@@ -850,7 +850,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccpageturn",
- "CC Page Turn",
+ "Page Curl",
vec![
p("foldPosition", "Fold Position", pt(0.7, 0.7), ParamUi::Point),
p("foldDirection", "Fold Direction", num(315.0), ParamUi::Angle),
@@ -864,7 +864,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.cctiler",
- "CC Tiler",
+ "Scaled Tiles",
vec![
p("scale", "Scale", num(50.0), slider(1.0, 10000.0, 1.0, 200.0, 1)),
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
@@ -874,7 +874,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccgriddler",
- "CC Griddler",
+ "Tile Grid",
vec![
p("horizontalScale", "Horizontal Scale", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)),
p("verticalScale", "Vertical Scale", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)),
diff --git a/crates/effects/src/distort3.rs b/crates/effects/src/distort3.rs
index a02ab5743..34fc282ed 100644
--- a/crates/effects/src/distort3.rs
+++ b/crates/effects/src/distort3.rs
@@ -1,5 +1,5 @@
-//! Distort effects, batch 3: CC Bender, CC Blobbylize, CC Power Pin, CC Ripple Pulse, CC Slant,
-//! CC Smear, CC Split, CC Split 2, Smear and Reshape (mask driven), Twirl (Legacy), Warp (the
+//! Distort effects, batch 3: Flexible Bend, Organic Displacement, Perspective Pin, Pulsed Ripple, Slant,
+//! Directional Smear, Split Warp, Dual Split Warp, Smear and Reshape (mask driven), Twirl (Legacy), Warp (the
//! fifteen warp styles) and Detail-preserving Upscale.
//!
//! All warps are inverse mapped: every output pixel finds the source point that lands on it.
@@ -59,7 +59,7 @@ fn elastic_exp(i: u32) -> f64 {
[1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 3.5, 4.5][(i as usize).min(7)]
}
-// ---------------------------------------------------------------- CC Bender
+// ---------------------------------------------------------------- Flexible Bend
fn cc_bender(ctx: &EffectCtx, mut b: Buf) -> Buf {
let amount = ctx.params.f("amount") / 100.0;
@@ -107,7 +107,7 @@ fn norm3(v: [f64; 3]) -> [f64; 3] {
[v[0] / l, v[1] / l, v[2] / l]
}
-/// f32 normal/dot helpers for the CC Blobbylize lighting pass. The GPU kernel runs in f32;
+/// f32 normal/dot helpers for the Organic Displacement lighting pass. The GPU kernel runs in f32;
/// matching its precision here keeps specular highlights within the CPU/GPU parity tolerance.
fn norm3f(v: [f32; 3]) -> [f32; 3] {
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
@@ -118,7 +118,7 @@ fn dot3f(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
-// ---------------------------------------------------------------- CC Blobbylize
+// ---------------------------------------------------------------- Organic Displacement
fn cc_blobbylize(ctx: &EffectCtx, mut b: Buf) -> Buf {
let soft = ctx.params.f("softness").max(0.0) * b.scale;
@@ -221,7 +221,7 @@ fn cc_blobbylize(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Power Pin
+// ---------------------------------------------------------------- Perspective Pin
fn cc_power_pin(ctx: &EffectCtx, mut b: Buf) -> Buf {
let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0));
@@ -304,7 +304,7 @@ fn cc_power_pin(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Ripple Pulse
+// ---------------------------------------------------------------- Pulsed Ripple
fn cc_ripple_pulse(ctx: &EffectCtx, mut b: Buf) -> Buf {
let level = ctx.params.f("pulseLevel") / 100.0;
@@ -350,7 +350,7 @@ fn cc_ripple_pulse(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Slant
+// ---------------------------------------------------------------- Slant
fn cc_slant(ctx: &EffectCtx, mut b: Buf) -> Buf {
let k = ctx.params.f("slant") / 100.0;
@@ -375,7 +375,7 @@ fn cc_slant(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Smear
+// ---------------------------------------------------------------- Directional Smear
fn cc_smear(ctx: &EffectCtx, mut b: Buf) -> Buf {
let reach = ctx.params.f("reach") / 100.0;
@@ -403,7 +403,7 @@ fn cc_smear(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
/// The transparent pixels to add on every side of a buffer `size` pixels big (with `offset` and
-/// `scale`) for CC Smear: it moves pixels by up to Reach × From→To, and they may move out to the
+/// `scale`) for Directional Smear: it moves pixels by up to Reach × From→To, and they may move out to the
/// layer bounds. A text or shape layer's buffer ends at its content while its bounds are
/// comp-sized, so the smear was cut off at the content's box (#659).
pub fn cc_smear_pad(ctx: &EffectCtx, size: [u32; 2], offset: [f64; 2], scale: f64) -> u32 {
@@ -417,7 +417,7 @@ pub fn cc_smear_pad(ctx: &EffectCtx, size: [u32; 2], offset: [f64; 2], scale: f6
(moved.min(4096.0) as u32).min(crate::util::layer_cover_pad(ctx, size, offset, scale))
}
-// ---------------------------------------------------------------- CC Split / CC Split 2
+// ---------------------------------------------------------------- Split Warp / Dual Split Warp
fn split_impl(ctx: &EffectCtx, mut b: Buf, s1: f64, s2: f64) -> Buf {
if s1 == 0.0 && s2 == 0.0 {
@@ -913,7 +913,7 @@ pub fn specs() -> Vec {
vec![
spec(
"ec.distort.ccbender",
- "CC Bender",
+ "Flexible Bend",
vec![
p("amount", "Amount", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
p("style", "Style", Value::Enum(0), popup(&["Bend", "Marilyn", "Sharp", "Boxer"])),
@@ -925,7 +925,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccblobbylize",
- "CC Blobbylize",
+ "Organic Displacement",
vec![
p("blobLayer", "Blob Layer", Value::Layer(None), ParamUi::Layer),
p("property", "Property", Value::Enum(0), popup(&crate::util::SRC_NAMES[..8])),
@@ -947,7 +947,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccpowerpin",
- "CC Power Pin",
+ "Perspective Pin",
vec![
p("topLeft", "Top Left", pt(0.0, 0.0), ParamUi::Point),
p("topRight", "Top Right", pt(1.0, 0.0), ParamUi::Point),
@@ -964,7 +964,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccripplepulse",
- "CC Ripple Pulse",
+ "Pulsed Ripple",
vec![
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
p("pulseLevel", "Pulse Level (animate)", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)),
@@ -976,7 +976,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccslant",
- "CC Slant",
+ "Slant",
vec![
p("slant", "Slant", num(0.0), slider(-1000.0, 1000.0, -100.0, 100.0, 1)),
p("stretching", "Stretching", Value::Bool(false), ParamUi::Checkbox),
@@ -987,7 +987,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccsmear",
- "CC Smear",
+ "Directional Smear",
vec![
p("from", "From", pt(0.35, 0.5), ParamUi::Point),
p("to", "To", pt(0.65, 0.5), ParamUi::Point),
@@ -998,7 +998,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccsplit",
- "CC Split",
+ "Split Warp",
vec![
p("pointA", "Point A", pt(0.25, 0.5), ParamUi::Point),
p("pointB", "Point B", pt(0.75, 0.5), ParamUi::Point),
@@ -1008,7 +1008,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.distort.ccsplit2",
- "CC Split 2",
+ "Dual Split Warp",
vec![
p("pointA", "Point A", pt(0.25, 0.5), ParamUi::Point),
p("pointB", "Point B", pt(0.75, 0.5), ParamUi::Point),
diff --git a/crates/effects/src/distort4.rs b/crates/effects/src/distort4.rs
index b724a5830..9913c6ea5 100644
--- a/crates/effects/src/distort4.rs
+++ b/crates/effects/src/distort4.rs
@@ -1,6 +1,6 @@
-//! Distort: CC Flo Motion, Liquify and Rolling Shutter Repair.
+//! Distort: Dual Lens Warp, Liquify and Rolling Shutter Repair.
//!
-//! * **CC Flo Motion** — two knots that pull the image toward themselves (positive amount) or
+//! * **Dual Lens Warp** — two knots that pull the image toward themselves (positive amount) or
//! push it away (negative), with a falloff exponent, optional tiling and supersampled
//! antialiasing.
//! * **Liquify** — brush strokes build a displacement mesh (a grid of layer-space offsets). The
@@ -32,7 +32,7 @@ fn spec(id: &'static str, name: &'static str, params: Vec, ren
EffectSpec { id, name, category: "Distort", params, render, gpu: false, float: true }
}
-// ---------------------------------------------------------------- CC Flo Motion
+// ---------------------------------------------------------------- Dual Lens Warp
fn flo_motion(ctx: &EffectCtx, mut b: Buf) -> Buf {
let knots = [(ctx.params.v2("knot1"), ctx.params.f("amount1") / 100.0), (ctx.params.v2("knot2"), ctx.params.f("amount2") / 100.0)];
@@ -560,7 +560,7 @@ pub fn specs() -> Vec {
vec![
spec(
"ec.distort.ccflomotion",
- "CC Flo Motion",
+ "Dual Lens Warp",
vec![
p("knot1", "Knot 1", Value::Vec2([0.25, 0.5]), ParamUi::Point),
p("amount1", "Amount 1", num(50.0), slider(-400.0, 100.0, -100.0, 100.0, 1)),
diff --git a/crates/effects/src/generate2.rs b/crates/effects/src/generate2.rs
index b4781b793..ad80cb9cd 100644
--- a/crates/effects/src/generate2.rs
+++ b/crates/effects/src/generate2.rs
@@ -1,5 +1,5 @@
//! Generate effects, batch 2: Cell Pattern (Worley), Ellipse, Lens Flare, Beam, Radio Waves,
-//! Advanced Lightning, CC Light Rays, CC Light Burst 2.5, CC Light Sweep.
+//! Advanced Lightning, Light Rays, Radial Light Burst, Light Sweep.
//!
//! All designs are original procedural renderings written from the public descriptions of what
//! each effect produces.
@@ -897,7 +897,7 @@ fn advanced_lightning(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Light Rays / Burst
+// ---------------------------------------------------------------- Light Rays / Burst
const RAY_STEPS: usize = 32;
@@ -1007,7 +1007,7 @@ fn light_burst(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Light Sweep
+// ---------------------------------------------------------------- Light Sweep
fn light_sweep(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = b.to_px(ctx.params.v2("center"));
@@ -1192,7 +1192,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.generate.cclightrays",
- "CC Light Rays",
+ "Light Rays",
vec![
p("intensity", "Intensity", num(100.0), slider(0.0, 600.0, 0.0, 200.0, 1)),
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
@@ -1207,7 +1207,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.generate.cclightburst",
- "CC Light Burst 2.5",
+ "Radial Light Burst",
vec![
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
p("intensity", "Intensity", num(100.0), slider(0.0, 600.0, 0.0, 200.0, 1)),
@@ -1220,7 +1220,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.generate.cclightsweep",
- "CC Light Sweep",
+ "Light Sweep",
vec![
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
p("direction", "Direction", num(-30.0), ang()),
diff --git a/crates/effects/src/generate3/registry.rs b/crates/effects/src/generate3/registry.rs
index f3e08c2d5..baa493110 100644
--- a/crates/effects/src/generate3/registry.rs
+++ b/crates/effects/src/generate3/registry.rs
@@ -180,7 +180,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.generate.ccgluegun",
- "CC Glue Gun",
+ "Paint Beads",
vec![
p("brushPosition", "Brush Position", pt(0.5, 0.5), ParamUi::Point),
p("strokeWidth", "Stroke Width", num(25.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
@@ -194,7 +194,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.generate.ccthreads",
- "CC Threads",
+ "Woven Threads",
vec![
p("width", "Width", num(50.0), slider(1.0, 1000.0, 1.0, 200.0, 1)),
p("height", "Height", num(50.0), slider(1.0, 1000.0, 1.0, 200.0, 1)),
diff --git a/crates/effects/src/keying2.rs b/crates/effects/src/keying2.rs
index a03fe804d..5512d1820 100644
--- a/crates/effects/src/keying2.rs
+++ b/crates/effects/src/keying2.rs
@@ -317,7 +317,7 @@ fn unmult(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---------------------------------------------------------------- CC Simple Wire Removal
+// ---------------------------------------------------------------- Line Removal
fn wire_removal(ctx: &EffectCtx, mut b: Buf) -> Buf {
let pa = ctx.params.v2("pointA");
@@ -464,7 +464,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.key.ccsimplewireremoval",
- "CC Simple Wire Removal",
+ "Line Removal",
vec![
p("pointA", "Point A", Value::Vec2([0.25, 0.25]), ParamUi::Point),
p("pointB", "Point B", Value::Vec2([0.75, 0.75]), ParamUi::Point),
diff --git a/crates/effects/src/keylight.rs b/crates/effects/src/keylight.rs
index 84085f4e3..fb36075cb 100644
--- a/crates/effects/src/keylight.rs
+++ b/crates/effects/src/keylight.rs
@@ -1,6 +1,6 @@
-//! Key Light — a full screen-colour keyer with the controls of After Effects' bundled
+//! Screen Color Key — a full screen-colour keyer with the controls of After Effects' bundled
//! "Keylight (1.2)" (that name is a trademark of its vendor, so the effect is registered as
-//! **Key Light**; `lookup("Keylight (1.2)")` finds it through [`KEYLIGHT_ALIASES`]).
+//! **Screen Color Key**; `lookup("Keylight (1.2)")` finds it through [`KEYLIGHT_ALIASES`]).
//!
//! The method is the textbook *screen difference* key, written from the public description of
//! the controls:
@@ -31,7 +31,7 @@ use crate::util::{Plane, gauss_plane, morph_frac, point_in_poly, premul, unpremu
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
/// Other names the effect answers to in [`crate::lookup`].
-pub const KEYLIGHT_ALIASES: &[&str] = &["Keylight (1.2)", "Keylight"];
+pub const KEYLIGHT_ALIASES: &[&str] = &["Keylight (1.2)", "Keylight", "Key Light"];
pub const VIEWS: [&str; 11] = [
"Source",
@@ -463,7 +463,7 @@ pub fn specs() -> Vec {
let cc = |id: &'static str, name: &'static str| p(id, name, num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1));
vec![EffectSpec {
id: "ec.keying.keylight",
- name: "Key Light",
+ name: "Screen Color Key",
category: "Keying",
params: vec![
p("view", "View", Value::Enum(10), popup(&VIEWS)),
@@ -622,7 +622,7 @@ mod tests {
let spec = crate::find("ec.keying.keylight").unwrap();
let mut next = 1;
let mut ids = Ids(&mut next);
- let mut g = crate::instantiate(spec, &mut ids, "Key Light", [10.0, 10.0]);
+ let mut g = crate::instantiate(spec, &mut ids, "Screen Color Key", [10.0, 10.0]);
// Flatten it the way older versions saved it.
let mut flat = vec![];
g.walk_mut(&mut |p| flat.push(p.clone()));
diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs
index bd239aa6f..019596f80 100644
--- a/crates/effects/src/lib.rs
+++ b/crates/effects/src/lib.rs
@@ -23,6 +23,8 @@ mod color3;
mod color_fx;
pub mod controls;
pub mod crypto_select;
+#[cfg(test)]
+mod display_names;
mod distort;
mod distort2;
mod distort3;
@@ -36,6 +38,7 @@ mod keying;
mod keying2;
pub mod keylight;
mod matte;
+
pub mod migrate;
mod misc;
pub mod mocha_shape;
@@ -448,7 +451,9 @@ pub fn adjustment_crop(offset: [f64; 2], canvas_offset: [f64; 2], size: (u32, u3
/// Effects that grow their own buffer by what they need (blur and glow margins, Repeat Edge
/// Pixels through [`Buf::cover`], displacement through [`cover_pad`]): a stack of only these
/// skips the renderer's cover of the whole layer rectangle (#1205).
-pub const SKIPS_COVER: &[&str] = &["ec.blur.gaussian", "ec.blur.fastbox", "ec.stylize.glow", "ec.distort.turbulentdisplace"];
+/// Drop Shadow also pads by its distance and softness on both CPU and GPU: covering the
+/// layer rectangle first only inflates working images on highly scaled text and shapes (#3357).
+pub const SKIPS_COVER: &[&str] = &["ec.blur.gaussian", "ec.blur.fastbox", "ec.stylize.glow", "ec.distort.turbulentdisplace", "ec.perspective.dropshadow"];
/// The grid phase [`Buf::align`] keeps and the step [`cover_pad`] grows in: covers the levels
/// of the resampling pyramid used for downscaled layers.
@@ -560,7 +565,7 @@ pub trait EffectHost: Sync {
}
/// Parameter `id` (a [`flatten_params`] id such as `birthRate`) of effect `effect_index` in
/// the layer's stack (the running effect's [`EffectEnv::effect_index`]) at another **layer
- /// time**: CC Particle World samples its Birth Rate at every step it simulates (#1373).
+ /// time**: Particle Emitter 3D samples its Birth Rate at every step it simulates (#1373).
/// `None` when unavailable.
fn param_at(&self, _effect_index: usize, _id: &str, _layer_time: f64) -> Option {
None
@@ -776,8 +781,82 @@ pub fn categories() -> Vec<&'static str> {
v
}
-/// After Effects' third-party display names of effects we register under a generic name.
-const ALIASES: &[(&str, &[&str])] = &[("ec.keying.keylight", keylight::KEYLIGHT_ALIASES)];
+/// Legacy display names accepted for saved scripts and searches. Never display these as
+/// native feature names; stable effect IDs and external format match names stay unchanged.
+const ALIASES: &[(&str, &[&str])] = &[
+ ("ec.sim.ccballaction", &["CC Ball Action"]),
+ ("ec.distort.ccbendit", &["CC Bend It"]),
+ ("ec.distort.ccbender", &["CC Bender"]),
+ ("ec.distort.ccblobbylize", &["CC Blobbylize"]),
+ ("ec.stylize.ccblockload", &["CC Block Load"]),
+ ("ec.sim.ccbubbles", &["CC Bubbles"]),
+ ("ec.stylize.ccburnfilm", &["CC Burn Film"]),
+ ("ec.color.cccolorneutralizer", &["CC Color Neutralizer"]),
+ ("ec.color.cccoloroffset", &["CC Color Offset"]),
+ ("ec.channel.cccomposite", &["CC Composite"]),
+ ("ec.blur.cccross", &["CC Cross Blur"]),
+ ("ec.perspective.cccylinder", &["CC Cylinder"]),
+ ("ec.sim.ccdrizzle", &["CC Drizzle"]),
+ ("ec.perspective.ccenvironment", &["CC Environment"]),
+ ("ec.distort.ccflomotion", &["CC Flo Motion"]),
+ ("ec.time.ccforcemotionblur", &["CC Force Motion Blur"]),
+ ("ec.stylize.ccglass", &["CC Glass"]),
+ ("ec.transition.ccglasswipe", &["CC Glass Wipe"]),
+ ("ec.generate.ccgluegun", &["CC Glue Gun"]),
+ ("ec.transition.ccgridwipe", &["CC Grid Wipe"]),
+ ("ec.distort.ccgriddler", &["CC Griddler"]),
+ ("ec.sim.cchair", &["CC Hair"]),
+ ("ec.stylize.cchextile", &["CC HexTile"]),
+ ("ec.transition.ccimagewipe", &["CC Image Wipe"]),
+ ("ec.transition.ccjaws", &["CC Jaws"]),
+ ("ec.stylize.cckaleida", &["CC Kaleida"]),
+ ("ec.color.cckernel", &["CC Kernel"]),
+ ("ec.distort.cclens", &["CC Lens"]),
+ ("ec.generate.cclightburst", &["CC Light Burst 2.5"]),
+ ("ec.generate.cclightrays", &["CC Light Rays"]),
+ ("ec.generate.cclightsweep", &["CC Light Sweep"]),
+ ("ec.transition.cclightwipe", &["CC Light Wipe"]),
+ ("ec.transition.cclinesweep", &["CC Line Sweep"]),
+ ("ec.sim.ccmrmercury", &["CC Mr. Mercury"]),
+ ("ec.stylize.ccmrsmoothie", &["CC Mr. Smoothie"]),
+ ("ec.utility.ccoverbrights", &["CC Overbrights"]),
+ ("ec.distort.ccpageturn", &["CC Page Turn"]),
+ ("ec.sim.ccparticlesystems2", &["CC Particle Systems II"]),
+ ("ec.sim.ccparticleworld", &["CC Particle World"]),
+ ("ec.sim.ccpixelpolly", &["CC Pixel Polly"]),
+ ("ec.stylize.ccplastic", &["CC Plastic"]),
+ ("ec.distort.ccpowerpin", &["CC Power Pin"]),
+ ("ec.blur.ccradial", &["CC Radial Blur"]),
+ ("ec.blur.ccradialfast", &["CC Radial Fast Blur"]),
+ ("ec.transition.ccradialscalewipe", &["CC Radial ScaleWipe"]),
+ ("ec.sim.ccrainfall", &["CC Rainfall"]),
+ ("ec.stylize.ccrepetile", &["CC RepeTile"]),
+ ("ec.distort.ccripplepulse", &["CC Ripple Pulse"]),
+ ("ec.transition.ccscalewipe", &["CC Scale Wipe"]),
+ ("ec.sim.ccscatterize", &["CC Scatterize"]),
+ ("ec.key.ccsimplewireremoval", &["CC Simple Wire Removal"]),
+ ("ec.distort.ccslant", &["CC Slant"]),
+ ("ec.distort.ccsmear", &["CC Smear"]),
+ ("ec.sim.ccsnowfall", &["CC Snowfall"]),
+ ("ec.perspective.ccsphere", &["CC Sphere"]),
+ ("ec.distort.ccsplit", &["CC Split"]),
+ ("ec.distort.ccsplit2", &["CC Split 2"]),
+ ("ec.perspective.ccspotlight", &["CC Spotlight"]),
+ ("ec.sim.ccstarburst", &["CC Star Burst"]),
+ ("ec.generate.ccthreads", &["CC Threads"]),
+ ("ec.stylize.ccthreshold", &["CC Threshold"]),
+ ("ec.stylize.ccthresholdrgb", &["CC Threshold RGB"]),
+ ("ec.distort.cctiler", &["CC Tiler"]),
+ ("ec.color.cctoner", &["CC Toner"]),
+ ("ec.transition.cctwister", &["CC Twister"]),
+ ("ec.blur.ccvector", &["CC Vector Blur"]),
+ ("ec.stylize.ccvignette", &["CC Vignette"]),
+ ("ec.transition.ccwarpomatic", &["CC WarpoMatic"]),
+ ("ec.time.ccwidetime", &["CC Wide Time"]),
+ ("ec.color.lumetri", &["Lumetri Color"]),
+ ("ec.obsolete.mochashape", &["Mocha shape", "mocha shape"]),
+ ("ec.keying.keylight", keylight::KEYLIGHT_ALIASES),
+];
/// Other names effect `id` answers to (in [`lookup`] and searches).
pub fn aliases(id: &str) -> &'static [&'static str] {
@@ -861,7 +940,7 @@ pub fn instantiate(spec: &EffectSpec, ids: &mut Ids, instance_name: &str, layer_
/// effects without their own table.
pub const PARAM_GROUPS: &[(&str, &str)] = &[
// Color Correction: Exposure, Levels (Individual Controls), Change to Color, Colorama,
- // Shadow/Highlight, Lumetri Color.
+ // Shadow/Highlight, Color Grading.
("master", "Master"),
("rgb", "RGB"),
("red", "Red"),
@@ -990,7 +1069,7 @@ pub const PARAM_GROUPS: &[(&str, &str)] = &[
("particleExploder", "Particle Exploder"),
("affects", "Affects"),
("options", "Options"),
- // Keying, Matte, Obsolete (Key Light's groups are in `keylight::GROUPS`).
+ // Keying, Matte, Obsolete (Screen Color Key's groups are in `keylight::GROUPS`).
("ultraSettings", "Ultra Settings"),
("additionalForeground", "Additional Foreground"),
("additionalBackground", "Additional Background"),
@@ -1075,7 +1154,7 @@ fn nested_group<'a>(g: &'a mut PropGroup, ids: &mut Ids, path: &str) -> Option<&
}
/// Effects the GPU compositor (`effectcraft-gpu`) implements with the CPU effect's semantics
-/// (Mercury GPU Acceleration; the Effects & Presets GPU badge). Expression controls are
+/// (GPU Acceleration; the Effects & Presets GPU badge). Expression controls are
/// pass-throughs and count as GPU effects too.
pub const GPU_EFFECTS: &[&str] = &[
"ec.blur.gaussian",
@@ -1437,7 +1516,7 @@ pub const TIME_DEPENDENT: &[&str] = &[
pub fn is_time_dependent(id: &str) -> bool {
// Simulations replay from layer time 0, so every frame differs even when the defaults
// happen to look static at the test's two sample times.
- id.starts_with("ec.sim.") || TIME_DEPENDENT.contains(&id)
+ id.starts_with("ec.sim.") || TIME_DEPENDENT.contains(&id) || (!id.starts_with("ec.") && plugin::reads_other_times(id))
}
/// Does effect `id` read its own parameters at earlier times (the stepped particle systems sample
@@ -1495,7 +1574,7 @@ mod tests {
assert_eq!(grow_sides(lo, hi, 20, 10), Some([0, 0, 30, 20]));
}
- /// #2261: a 20 × 20 layer with CC Radial Fast Blur's Center far away grows to at most 16 Mpx;
+ /// #2261: a 20 × 20 layer with Fast Radial Blur's Center far away grows to at most 16 Mpx;
/// big layers may still grow to four times their area, and small pads are untouched.
#[test]
fn padded_buffers_stay_within_the_budget() {
diff --git a/crates/effects/src/mocha_shape.rs b/crates/effects/src/mocha_shape.rs
index cf9e2bd88..c669580c6 100644
--- a/crates/effects/src/mocha_shape.rs
+++ b/crates/effects/src/mocha_shape.rs
@@ -1,4 +1,4 @@
-//! Matte ▸ Mocha shape: renders planar-tracked roto shapes as a matte.
+//! Matte ▸ Tracked Shape Matte: renders planar-tracked roto shapes as a matte.
//!
//! Mocha's own clipboard / export formats are not publicly specified, so the effect reads shape
//! data in a small documented JSON format of ours (in the hidden `shapeData` parameter, either the
@@ -184,7 +184,7 @@ fn mocha_shape(ctx: &EffectCtx, mut b: Buf) -> Buf {
pub fn specs() -> Vec {
vec![EffectSpec {
id: "ec.obsolete.mochashape",
- name: "Mocha shape",
+ name: "Tracked Shape Matte",
category: "Matte",
params: vec![
p("blendMode", "Blend mode", Value::Enum(0), popup(&["Add", "Subtract", "Intersect", "Difference"])),
diff --git a/crates/effects/src/perspective.rs b/crates/effects/src/perspective.rs
index 24c561ecd..69c63caf9 100644
--- a/crates/effects/src/perspective.rs
+++ b/crates/effects/src/perspective.rs
@@ -1,5 +1,5 @@
-//! Perspective effects (batch 2): Bevel Alpha, Bevel Edges, Radial Shadow, CC Cylinder,
-//! CC Sphere and CC Spotlight.
+//! Perspective effects (batch 2): Bevel Alpha, Bevel Edges, Radial Shadow, Cylinder Wrap,
+//! Sphere Wrap and Surface Spotlight.
//!
//! Angles follow the convention used by Drop Shadow: measured clockwise from "up", so a light
//! angle `a` sits in the direction `(sin a, -cos a)` (y grows downwards). 3D vectors use x right,
@@ -158,7 +158,7 @@ fn radial_shadow(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-/// Phong-style shading shared by CC Cylinder and CC Sphere.
+/// Phong-style shading shared by Cylinder Wrap and Sphere Wrap.
struct Shading {
l: [f64; 3],
color: [f32; 4],
@@ -451,11 +451,11 @@ pub fn specs() -> Vec {
],
radial_shadow,
),
- spec("ec.perspective.cccylinder", "CC Cylinder", cyl, cylinder),
- spec("ec.perspective.ccsphere", "CC Sphere", sph, sphere),
+ spec("ec.perspective.cccylinder", "Cylinder Wrap", cyl, cylinder),
+ spec("ec.perspective.ccsphere", "Sphere Wrap", sph, sphere),
spec(
"ec.perspective.ccspotlight",
- "CC Spotlight",
+ "Surface Spotlight",
vec![
p("from", "From", pt(0.3, 0.2), ParamUi::Point),
p("to", "To", pt(0.5, 0.5), ParamUi::Point),
diff --git a/crates/effects/src/perspective2.rs b/crates/effects/src/perspective2.rs
index 13846607f..2bc7e3e7e 100644
--- a/crates/effects/src/perspective2.rs
+++ b/crates/effects/src/perspective2.rs
@@ -1,4 +1,4 @@
-//! Perspective effects, batch 2: CC Environment (reflection mapping of an environment layer
+//! Perspective effects, batch 2: Environment Map (reflection mapping of an environment layer
//! onto the layer's luminance/alpha relief) and 3D Glasses (stereo pairs and anaglyphs from a
//! left and right view layer).
@@ -17,7 +17,7 @@ fn spec(id: &'static str, name: &'static str, params: Vec, ren
EffectSpec { id, name, category: "Perspective", params, render, gpu: false, float: true }
}
-// ---------------------------------------------------------------- CC Environment
+// ---------------------------------------------------------------- Environment Map
/// Environment direction (x right, y down, z towards the viewer) → normalised map coordinates.
fn env_uv(mapping: u32, r: [f64; 3]) -> (f64, f64) {
@@ -194,7 +194,7 @@ pub fn specs() -> Vec {
vec![
spec(
"ec.perspective.ccenvironment",
- "CC Environment",
+ "Environment Map",
vec![
p("environment", "Environment", Value::Layer(None), ParamUi::Layer),
p("mapping", "Mapping", Value::Enum(0), popup(&["Spherical", "Probe", "Vertical Cross"])),
diff --git a/crates/effects/src/plugin.rs b/crates/effects/src/plugin.rs
index 5c973ac57..8bbe17856 100644
--- a/crates/effects/src/plugin.rs
+++ b/crates/effects/src/plugin.rs
@@ -24,10 +24,12 @@
//! * rendering must be deterministic (same inputs, same output) and may run on several threads
//! at once.
//!
-//! API 2 (native plug-ins; the WebAssembly ABI stays at 1) adds [`EffectPlugin::padding`] and
-//! [`EffectPlugin::render_v2`], which gets a [`PluginHost`] to read the effect's input or other
-//! layers at other times, and the extra parameter kinds. External effect helpers
-//! (`effectcraft-helper-host`) are built on it.
+//! API 2 (native plug-ins) adds [`EffectPlugin::padding`] and [`EffectPlugin::render_v2`], which
+//! gets a [`PluginHost`] to read the effect's input or other layers at other times, and the extra
+//! parameter kinds. External effect helpers (`effectcraft-helper-host`) are built on it.
+//!
+//! The WebAssembly ABI is versioned separately ([`WASM_API_VERSION`]): ABI 1 is `ec_render`;
+//! ABI 2 adds `ec_render2`, which receives the layer's previous and next frames.
use std::sync::{Arc, OnceLock, RwLock};
@@ -39,9 +41,12 @@ use crate::{Buf, EffectCtx, EffectSpec, ParamSpec, Params, RenderFn};
/// The plug-in API version this build implements (manifests with API 1 or 2 are accepted).
pub const PLUGIN_API_VERSION: u32 = 2;
-/// The plug-in API a WebAssembly module implements (its `ec_api_version`). API 2 is for native
-/// and helper plug-ins; WebAssembly modules stay at 1.
-pub const WASM_API_VERSION: u32 = 1;
+/// The newest WebAssembly ABI this build implements (a module's `ec_api_version`). ABI 1 is
+/// `ec_render`; ABI 2 adds `ec_render2` and the layer's neighbouring frames. This is separate
+/// from the plug-in API version: a module's manifest `api` stays 1 or 2.
+pub const WASM_API_VERSION: u32 = 2;
+/// The oldest WebAssembly ABI this build still loads.
+pub const MIN_WASM_API_VERSION: u32 = 1;
/// The most plug-in effects one process can register (commercial effect suites register a few hundred each).
pub const MAX_PLUGINS: usize = 1024;
@@ -269,6 +274,22 @@ impl PluginImage {
fn from_buf(b: Buf) -> PluginImage {
PluginImage { width: b.img.width, height: b.img.height, pixels: b.img.data, origin: b.offset, scale: b.scale }
}
+
+ /// This image resampled onto a `width × height` buffer grid at `origin`/`scale` (the current
+ /// frame's); the pixels unchanged when the grids already match.
+ pub fn on_grid(&self, width: u32, height: u32, origin: [f64; 2], scale: f64) -> Vec<[f32; 4]> {
+ if self.width == width && self.height == height && self.origin == origin && self.scale == scale {
+ return self.pixels.clone();
+ }
+ let k = self.scale / scale.max(1e-9);
+ let src = crate::Image { width: self.width, height: self.height, data: self.pixels.clone() };
+ crate::util::gen_image(width, height, |x, y| {
+ let lx = (x as f64 + 0.5 - origin[0]) * k + self.origin[0];
+ let ly = (y as f64 + 0.5 - origin[1]) * k + self.origin[1];
+ src.sample_bilinear(lx, ly)
+ })
+ .data
+ }
}
/// Services available during an API 2 render ([`EffectPlugin::render_v2`]).
@@ -388,6 +409,12 @@ pub trait EffectPlugin: Send + Sync {
fn render_v2(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64, _host: &dyn PluginHost) -> Result<(), String> {
self.render(frame, params, time)
}
+ /// Whether rendering reads the layer at other times (its neighbouring frames). Default
+ /// false; the render cache keys a layer with such an effect on its layer time. WebAssembly
+ /// ABI 2 plug-ins return true.
+ fn reads_other_times(&self) -> bool {
+ false
+ }
}
static SLOTS: [OnceLock>; MAX_PLUGINS] = [const { OnceLock::new() }; MAX_PLUGINS];
@@ -432,6 +459,12 @@ pub fn plugin(id: &str) -> Option> {
SLOTS.iter().filter_map(|s| s.get()).find(|p| p.manifest().id == id).cloned()
}
+/// Whether the registered plug-in `id` reads the layer at other times (its neighbouring frames),
+/// so the render cache must key its layer on the layer time.
+pub fn reads_other_times(id: &str) -> bool {
+ SLOTS.iter().filter_map(|s| s.get()).any(|p| p.manifest().id == id && p.reads_other_times())
+}
+
fn leak(s: &str) -> &'static str {
Box::leak(s.to_string().into_boxed_str())
}
diff --git a/crates/effects/src/sim/registry.rs b/crates/effects/src/sim/registry.rs
index f55b99e27..4df07c50a 100644
--- a/crates/effects/src/sim/registry.rs
+++ b/crates/effects/src/sim/registry.rs
@@ -32,7 +32,7 @@ pub fn specs() -> Vec {
vec![
spec(
"ec.sim.ccrainfall",
- "CC Rainfall",
+ "Rainfall",
vec![
p("drops", "Drops", num(5000.0), slider(0.0, 100_000.0, 0.0, 20_000.0, 0)),
p("size", "Size", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 2)),
@@ -53,7 +53,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccsnowfall",
- "CC Snowfall",
+ "Snowfall",
vec![
p("flakes", "Flakes", num(10_000.0), slider(0.0, 200_000.0, 0.0, 50_000.0, 0)),
p("size", "Size", num(3.0), slider(0.0, 50.0, 0.0, 20.0, 2)),
@@ -79,7 +79,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccbubbles",
- "CC Bubbles",
+ "Bubbles",
vec![
p("bubbleAmount", "Bubble Amount", num(100.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
p("bubbleSpeed", "Bubble Speed", num(0.5), slider(-10.0, 10.0, -2.0, 2.0, 2)),
@@ -93,7 +93,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccdrizzle",
- "CC Drizzle",
+ "Water Ripples",
vec![
p("dripRate", "Drip Rate", num(10.0), slider(0.0, 200.0, 0.0, 50.0, 2)),
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 30.0, 0.0, 5.0, 2)),
@@ -114,7 +114,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccstarburst",
- "CC Star Burst",
+ "Starfield",
vec![
p("scatter", "Scatter", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
p("speed", "Speed", num(1.0), slider(-10.0, 10.0, -4.0, 4.0, 2)),
@@ -127,7 +127,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccparticleworld",
- "CC Particle World",
+ "Particle Emitter 3D",
vec![
p("birthRate", "Birth Rate", num(0.5), slider(0.0, 100.0, 0.0, 10.0, 2)),
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
@@ -170,7 +170,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccparticlesystems2",
- "CC Particle Systems II",
+ "Particle Emitter 2D",
vec![
p("birthRate", "Birth Rate", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
@@ -198,7 +198,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccmrmercury",
- "CC Mr. Mercury",
+ "Liquid Particles",
vec![
p("radiusX", "Radius X", num(50.0), slider(0.0, 2000.0, 0.0, 300.0, 1)),
p("radiusY", "Radius Y", num(50.0), slider(0.0, 2000.0, 0.0, 300.0, 1)),
diff --git a/crates/effects/src/sim2/registry.rs b/crates/effects/src/sim2/registry.rs
index 333916a16..dc64b7097 100644
--- a/crates/effects/src/sim2/registry.rs
+++ b/crates/effects/src/sim2/registry.rs
@@ -79,7 +79,7 @@ pub fn specs() -> Vec {
vec![
spec(
"ec.sim.ccballaction",
- "CC Ball Action",
+ "Ball Grid",
vec![
p("scatter", "Scatter", num(0.0), slider(0.0, 2000.0, 0.0, 300.0, 1)),
p("rotationAxis", "Rotation Axis", Value::Enum(0), popup(&["X Axis", "Y Axis", "Z Axis", "XY Axis", "XZ Axis", "YZ Axis", "XYZ Axis"])),
@@ -94,7 +94,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccpixelpolly",
- "CC Pixel Polly",
+ "Pixel Shatter",
vec![
p("force", "Force", num(100.0), slider(-1000.0, 1000.0, -300.0, 300.0, 1)),
p("gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)),
@@ -111,7 +111,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.sim.ccscatterize",
- "CC Scatterize",
+ "Pixel Scatter",
vec![
p("scatter", "Scatter", num(0.0), slider(0.0, 1000.0, 0.0, 200.0, 1)),
p("rightTwist", "Right Twist", num(0.0), ParamUi::Angle),
diff --git a/crates/effects/src/sim3.rs b/crates/effects/src/sim3.rs
index 88a16d981..c2a286390 100644
--- a/crates/effects/src/sim3.rs
+++ b/crates/effects/src/sim3.rs
@@ -1,4 +1,4 @@
-//! Simulation: Particle Playground and CC Hair.
+//! Simulation: Particle Playground and Surface Fibers.
//!
//! **Particle Playground** is a deterministic 2D particle system stepped from layer time 0 at
//! [`SPS`] steps per second through [`SimCache`] (seeking anywhere gives the same frame as
@@ -17,7 +17,7 @@
//! being rendered (their pixels are part of the cache key). Particles replace the layer's own
//! pixels, as in After Effects.
//!
-//! **CC Hair** grows strands from the layer's opaque pixels (Density per 1 000 px²), each a
+//! **Surface Fibers** grows strands from the layer's opaque pixels (Density per 1 000 px²), each a
//! quadratic curve of Length that droops with Weight, optionally steered and scaled by a hairfall
//! map layer (luminance → length, horizontal/vertical gradient → lean), and shades them with a
//! Kajiya–Kay-style strand model (diffuse ∝ sin(tangent, light), specular from the half vector).
@@ -934,7 +934,7 @@ impl PgPlan {
}
}
-// ---------------------------------------------------------------- CC Hair
+// ---------------------------------------------------------------- Surface Fibers
fn cc_hair(ctx: &EffectCtx, b: Buf) -> Buf {
let Some(mut plan) = hair_plan(ctx, &b) else { return b };
@@ -942,7 +942,7 @@ fn cc_hair(ctx: &EffectCtx, b: Buf) -> Buf {
plan.finish(b)
}
-/// CC Hair's strands as line sprites (each coloured from its root, see [`Tint::Root`]);
+/// Surface Fibers's strands as line sprites (each coloured from its root, see [`Tint::Root`]);
/// `None` = no hair (the layer passes through).
pub(crate) fn hair_plan(ctx: &EffectCtx, b: &Buf) -> Option {
let pr = ctx.params;
@@ -1144,7 +1144,7 @@ pub fn specs() -> Vec {
spec("ec.sim.particleplayground", "Particle Playground", pg, particle_playground),
spec(
"ec.sim.cchair",
- "CC Hair",
+ "Surface Fibers",
vec![
sp("length", "Length", px(30.0, 200.0)),
p("thickness", "Thickness", num(1.0), slider(0.05, 20.0, 0.1, 5.0, 2)),
diff --git a/crates/effects/src/stylize2.rs b/crates/effects/src/stylize2.rs
index 94eda6370..247854352 100644
--- a/crates/effects/src/stylize2.rs
+++ b/crates/effects/src/stylize2.rs
@@ -343,7 +343,7 @@ fn brush_strokes(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---- CC Kaleida ----
+// ---- Kaleidoscope ----
fn kaleida(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = b.to_px(ctx.params.v2("center"));
@@ -368,7 +368,7 @@ fn kaleida(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---- CC RepeTile ----
+// ---- Edge Tile Repeat ----
#[inline]
fn tile_coord(v: f64, size: f64) -> (i64, f64) {
@@ -491,7 +491,7 @@ fn color_emboss(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---- CC Vignette ----
+// ---- Vignette ----
fn vignette(ctx: &EffectCtx, mut b: Buf) -> Buf {
let amt = ctx.params.f("amount") as f32 / 100.0;
@@ -523,7 +523,7 @@ fn vignette(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ---- CC Threshold / CC Threshold RGB ----
+// ---- Channel Threshold / RGB Threshold ----
fn cc_threshold(ctx: &EffectCtx, mut b: Buf) -> Buf {
let t = ctx.params.f("threshold") as f32 / 255.0;
@@ -669,7 +669,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.stylize.cckaleida",
- "CC Kaleida",
+ "Kaleidoscope",
vec![
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
p("size", "Size", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)),
@@ -680,7 +680,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.stylize.ccrepetile",
- "CC RepeTile",
+ "Edge Tile Repeat",
vec![
p("expandRight", "Expand Right", num(0.0), slider(0.0, 4000.0, 0.0, 500.0, 0)),
p("expandLeft", "Expand Left", num(0.0), slider(0.0, 4000.0, 0.0, 500.0, 0)),
@@ -718,7 +718,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.stylize.ccvignette",
- "CC Vignette",
+ "Vignette",
vec![
p("amount", "Amount", num(50.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
p("angleOfView", "Angle of View", num(60.0), slider(0.0, 179.0, 0.0, 179.0, 1)),
@@ -729,7 +729,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.stylize.ccthreshold",
- "CC Threshold",
+ "Channel Threshold",
vec![
p("threshold", "Threshold", num(127.0), thr()),
p("channel", "Channel", Value::Enum(0), popup(&["Luminance", "RGB", "Saturation", "Alpha"])),
@@ -740,7 +740,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.stylize.ccthresholdrgb",
- "CC Threshold RGB",
+ "RGB Threshold",
vec![
p("redThreshold", "Red Threshold", num(127.0), thr()),
p("greenThreshold", "Green Threshold", num(127.0), thr()),
diff --git a/crates/effects/src/stylize3.rs b/crates/effects/src/stylize3.rs
index e8a141cf4..1121f737b 100644
--- a/crates/effects/src/stylize3.rs
+++ b/crates/effects/src/stylize3.rs
@@ -1,5 +1,5 @@
-//! Stylize effects, batch 3: CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie
-//! and CC Plastic.
+//! Stylize effects, batch 3: Block Reveal, Film Burn, Glass Relief, Hexagonal Tiles, Palette Map
+//! and Plastic Shading.
//!
//! Written from the public descriptions of what these effects do (progressive block loading,
//! burning film, bump-mapped glass / plastic surfaces lit by an effect light, hexagonal tiling,
@@ -27,7 +27,7 @@ fn pt(x: f64, y: f64) -> Value {
Value::Vec2([x, y])
}
-// ------------------------------------------------------------------ CC Block Load
+// ------------------------------------------------------------------ Block Reveal
/// Mean colour of every `s`×`s` block (grid of `ceil(w/s)`×`ceil(h/s)`).
fn block_means(img: &Image, s: usize) -> (usize, usize, Vec) {
@@ -135,7 +135,7 @@ fn block_load(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ------------------------------------------------------------------ CC Burn Film
+// ------------------------------------------------------------------ Film Burn
fn burn_film(ctx: &EffectCtx, mut b: Buf) -> Buf {
let burn = (ctx.params.f("burn") / 100.0).clamp(0.0, 1.0) as f32;
@@ -180,7 +180,7 @@ fn burn_film(ctx: &EffectCtx, mut b: Buf) -> Buf {
const BUMP_PROPS: [&str; 6] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness"];
-/// The bump-map layer parameter: CC Glass's Bump Map, CC Plastic's Bump Layer.
+/// The bump-map layer parameter: Glass Relief's Bump Map, Plastic Shading's Bump Layer.
pub fn bump_layer_id(ctx: &EffectCtx) -> &'static str {
if ctx.params.get("bumpLayer").is_some() { "bumpLayer" } else { "bumpMap" }
}
@@ -208,7 +208,7 @@ fn height_field(ctx: &EffectCtx, b: &Buf) -> Plane {
pl
}
-/// The effect light and surface of CC Glass / CC Plastic (shared with the GPU compositor).
+/// The effect light and surface of Glass Relief / Plastic Shading (shared with the GPU compositor).
pub struct BumpLight {
pub intensity: f32,
pub color: [f32; 3],
@@ -342,7 +342,7 @@ fn plastic(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ------------------------------------------------------------------ CC HexTile
+// ------------------------------------------------------------------ Hexagonal Tiles
/// Axial hex cell (pointy-top) containing (x, y) for circumradius `r`; returns the cell centre
/// and integer cell coordinates.
@@ -418,7 +418,7 @@ fn hextile(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ------------------------------------------------------------------ CC Mr. Smoothie
+// ------------------------------------------------------------------ Palette Map
const SMOOTHIE_PROPS: [&str; 8] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness", "Hue", "Saturation"];
@@ -518,7 +518,7 @@ pub fn specs() -> Vec {
vec![
spec(
"ec.stylize.ccblockload",
- "CC Block Load",
+ "Block Reveal",
vec![
p("completion", "Completion", num(0.0), pct()),
p("scanlines", "Scanlines", Value::Bool(true), ParamUi::Checkbox),
@@ -530,7 +530,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.stylize.ccburnfilm",
- "CC Burn Film",
+ "Film Burn",
vec![
p("burn", "Burn", num(0.0), pct()),
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
@@ -538,10 +538,10 @@ pub fn specs() -> Vec {
],
burn_film,
),
- spec("ec.stylize.ccglass", "CC Glass", glass_p, glass),
+ spec("ec.stylize.ccglass", "Glass Relief", glass_p, glass),
spec(
"ec.stylize.cchextile",
- "CC HexTile",
+ "Hexagonal Tiles",
vec![
p("render", "Render", Value::Enum(0), popup(&["Normal", "Mirrored", "Rotated"])),
p("radius", "Radius", num(50.0), slider(1.0, 4000.0, 2.0, 500.0, 1)),
@@ -553,7 +553,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.stylize.ccmrsmoothie",
- "CC Mr. Smoothie",
+ "Palette Map",
vec![
p("flowLayer", "Flow Layer", Value::Layer(None), ParamUi::Layer),
p("property", "Property", Value::Enum(4), popup(&SMOOTHIE_PROPS)),
@@ -565,7 +565,7 @@ pub fn specs() -> Vec {
],
smoothie,
),
- spec("ec.stylize.ccplastic", "CC Plastic", plastic_p, plastic),
+ spec("ec.stylize.ccplastic", "Plastic Shading", plastic_p, plastic),
]
}
diff --git a/crates/effects/src/time_fx.rs b/crates/effects/src/time_fx.rs
index 79b0ce479..b9d20e2a7 100644
--- a/crates/effects/src/time_fx.rs
+++ b/crates/effects/src/time_fx.rs
@@ -1,9 +1,9 @@
//! Time effects (Effect > Time): Echo, Posterize Time, Time Difference, Time Displacement,
-//! Timewarp, CC Force Motion Blur, CC Wide Time and Pixel Motion Blur.
+//! Timewarp, Temporal Motion Blur, Temporal Blend and Pixel Motion Blur.
//!
//! These read the layer at *other* layer times through [`EffectHost::self_at`]: its source with
//! its masks and the effects applied before them, as After Effects renders them (SceneYard:
-//! Echo after CC Particle Systems II echoes the fireworks, not the black solid under them;
+//! Echo after Particle Emitter 2D echoes the fireworks, not the black solid under them;
//! Posterize Time after Turbulent Displace steps the displacement). Timewarp is the exception:
//! it retimes the layer's source with its masks and ignores the effects above it (After
//! Effects 2026: a red solid with a Gradient Ramp and then Timewarp shows the red solid).
@@ -123,7 +123,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.time.ccforcemotionblur",
- "CC Force Motion Blur",
+ "Temporal Motion Blur",
vec![
p("motionBlurLevels", "Motion Blur Levels", num(8.0), slider(2.0, 64.0, 2.0, 32.0, 0)),
p("overrideShutterAngle", "Override Shutter Angle", Value::Bool(true), ParamUi::Checkbox),
@@ -134,7 +134,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.time.ccwidetime",
- "CC Wide Time",
+ "Temporal Blend",
vec![
p("forwardSteps", "Forward Steps", num(3.0), slider(0.0, 45.0, 0.0, 15.0, 0)),
p("backwardSteps", "Backward Steps", num(3.0), slider(0.0, 45.0, 0.0, 15.0, 0)),
@@ -994,7 +994,7 @@ fn matte_image(m: &[f32], like: &Image) -> Image {
out
}
-// ---------------------------------------------------------------- CC Force Motion Blur / CC Wide Time / Pixel Motion Blur
+// ---------------------------------------------------------------- Temporal Motion Blur / Temporal Blend / Pixel Motion Blur
fn force_motion_blur(ctx: &EffectCtx, b: Buf) -> Buf {
let n = ctx.params.f("motionBlurLevels").round().clamp(2.0, 64.0) as usize;
diff --git a/crates/effects/src/transition.rs b/crates/effects/src/transition.rs
index e465ffd1b..da3e85694 100644
--- a/crates/effects/src/transition.rs
+++ b/crates/effects/src/transition.rs
@@ -1,5 +1,5 @@
//! Transition effects (batch 2): Block Dissolve, Gradient Wipe (gradient layer or self), Iris Wipe, Card
-//! Wipe (3D cards with camera, lighting and jitter), CC Grid Wipe, CC Radial ScaleWipe, CC Scale Wipe and CC Light Wipe.
+//! Wipe (3D cards with camera, lighting and jitter), Grid Wipe, Radial Scale Wipe, Scale Wipe and Light Wipe.
//!
//! Every transition is the identity at 0 % completion. Angles are clockwise from "up".
@@ -595,7 +595,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.transition.ccgridwipe",
- "CC Grid Wipe",
+ "Grid Wipe",
vec![
cc_done(),
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
@@ -609,13 +609,13 @@ pub fn specs() -> Vec {
),
spec(
"ec.transition.ccradialscalewipe",
- "CC Radial ScaleWipe",
+ "Radial Scale Wipe",
vec![cc_done(), p("center", "Center", pt(0.5, 0.5), ParamUi::Point), p("reverse", "Reverse Transition", Value::Bool(false), ParamUi::Checkbox)],
radial_scale_wipe,
),
spec(
"ec.transition.ccscalewipe",
- "CC Scale Wipe",
+ "Scale Wipe",
vec![
p("stretch", "Stretch", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 2)),
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
@@ -625,7 +625,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.transition.cclightwipe",
- "CC Light Wipe",
+ "Light Wipe",
vec![
cc_done(),
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
diff --git a/crates/effects/src/transition2.rs b/crates/effects/src/transition2.rs
index 2e1738b28..6f19a660b 100644
--- a/crates/effects/src/transition2.rs
+++ b/crates/effects/src/transition2.rs
@@ -1,5 +1,5 @@
-//! Transition effects, batch 2: CC Glass Wipe, CC Image Wipe, CC Jaws, CC Line Sweep,
-//! CC Twister and CC WarpoMatic.
+//! Transition effects, batch 2: Glass Wipe, Luminance Image Wipe, Serrated Wipe, Line Sweep,
+//! Twist Wipe and Warp Dissolve.
//!
//! Written from the public descriptions of these transitions (gradient-driven wipes with glass
//! refraction or warping, jagged "jaws" opening, staggered line sweeps, a ribbon twist that
@@ -68,7 +68,7 @@ fn grad(pl: &Plane, x: usize, y: usize) -> (f32, f32) {
((pl.get_clamped(xi + 1, yi) - pl.get_clamped(xi - 1, yi)) * 0.5, (pl.get_clamped(xi, yi + 1) - pl.get_clamped(xi, yi - 1)) * 0.5)
}
-// ------------------------------------------------------------------ CC Glass Wipe
+// ------------------------------------------------------------------ Glass Wipe
fn glass_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = completion(ctx);
@@ -105,7 +105,7 @@ fn glass_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ------------------------------------------------------------------ CC Image Wipe
+// ------------------------------------------------------------------ Luminance Image Wipe
fn image_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = completion(ctx);
@@ -132,7 +132,7 @@ fn image_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ------------------------------------------------------------------ CC Jaws
+// ------------------------------------------------------------------ Serrated Wipe
fn jaws(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = completion(ctx) as f64;
@@ -193,7 +193,7 @@ fn jaws(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ------------------------------------------------------------------ CC Line Sweep
+// ------------------------------------------------------------------ Line Sweep
fn line_sweep(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = completion(ctx) as f64;
@@ -235,7 +235,7 @@ fn line_sweep(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ------------------------------------------------------------------ CC Twister
+// ------------------------------------------------------------------ Twist Wipe
fn twister(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = completion(ctx) as f64;
@@ -283,7 +283,7 @@ fn twister(ctx: &EffectCtx, mut b: Buf) -> Buf {
b
}
-// ------------------------------------------------------------------ CC WarpoMatic
+// ------------------------------------------------------------------ Warp Dissolve
fn warpomatic(ctx: &EffectCtx, mut b: Buf) -> Buf {
let c = completion(ctx);
@@ -350,7 +350,7 @@ pub fn specs() -> Vec {
vec![
spec(
"ec.transition.ccglasswipe",
- "CC Glass Wipe",
+ "Glass Wipe",
vec![
done(),
p("layerToReveal", "Layer to Reveal", Value::Layer(None), ParamUi::Layer),
@@ -362,7 +362,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.transition.ccimagewipe",
- "CC Image Wipe",
+ "Luminance Image Wipe",
vec![
done(),
p("borderSoftness", "Border Softness", num(0.0), pct()),
@@ -376,7 +376,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.transition.ccjaws",
- "CC Jaws",
+ "Serrated Wipe",
vec![
done(),
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
@@ -389,7 +389,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.transition.cclinesweep",
- "CC Line Sweep",
+ "Line Sweep",
vec![
done(),
p("direction", "Direction", num(0.0), ParamUi::Angle),
@@ -401,7 +401,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.transition.cctwister",
- "CC Twister",
+ "Twist Wipe",
vec![
done(),
p("backside", "Backside", Value::Layer(None), ParamUi::Layer),
@@ -413,7 +413,7 @@ pub fn specs() -> Vec {
),
spec(
"ec.transition.ccwarpomatic",
- "CC WarpoMatic",
+ "Warp Dissolve",
vec![
done(),
p("layerToReveal", "Layer to Reveal", Value::Layer(None), ParamUi::Layer),
diff --git a/crates/effects/src/utility.rs b/crates/effects/src/utility.rs
index e87c6d3ff..fca9d607e 100644
--- a/crates/effects/src/utility.rs
+++ b/crates/effects/src/utility.rs
@@ -365,7 +365,7 @@ pub fn specs() -> Vec {
spec("ec.utility.growbounds", "Grow Bounds", vec![p("pixels", "Pixels", num(0.0), slider(0.0, 4000.0, 0.0, 500.0, 0))], grow_bounds),
spec(
"ec.utility.ccoverbrights",
- "CC Overbrights",
+ "Out-of-Range Display",
vec![
p("channel", "Channel", Value::Enum(0), popup(&["Max RGB", "Red", "Green", "Blue", "Alpha", "Luminance"])),
p("highlightColor", "Highlight Color", col(1.0, 0.0, 0.0), ParamUi::Color),
diff --git a/crates/engine/src/background/offload.rs b/crates/engine/src/background/offload.rs
index f9038accb..b28527f02 100644
--- a/crates/engine/src/background/offload.rs
+++ b/crates/engine/src/background/offload.rs
@@ -375,7 +375,7 @@ pub(crate) fn job_accel() -> Option> {
}
/// Render options of an analysis's input frames: 100 %; with an accelerator, the project's
-/// renderer (Mercury GPU Acceleration: its GPU effects on the device; Software Only: the CPU).
+/// renderer (GPU Acceleration: its GPU effects on the device; Software Only: the CPU).
pub(crate) fn analysis_opts(accel: Option<&dyn Accelerator>) -> RenderOpts {
RenderOpts { scale: 1.0, backend: if accel.is_some() { Backend::Auto } else { Backend::Cpu }, ..Default::default() }
}
diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs
index 6c4b02c87..250ca836d 100644
--- a/crates/engine/src/cmd/create.rs
+++ b/crates/engine/src/cmd/create.rs
@@ -5,7 +5,7 @@
use effectcraft_color::Label;
use effectcraft_keyframe::Value as KV;
use effectcraft_project::build::{self, Ids};
-use effectcraft_project::{ItemKind, LayerSource, Solid};
+use effectcraft_project::{ItemKind, LayerSource, MatteKind, Solid, TrackMatte};
use serde_json::{Value, json};
use super::{CommandSpec, bad, has_layers, layers_p, str_p};
@@ -13,6 +13,8 @@ use crate::{EngineError, Result, Session, cmd};
const OPS: &str = "editableText|shapesFromText|masksFromText|shapesFromVector|instance3d";
+/// Layer ▸ Create (`layer.create`): the `op` ([`OPS`]) on the given or selected layers; returns
+/// the created layer ids and what was skipped.
fn create(s: &mut Session, p: &Value) -> Result {
let op = str_p(p, "op").ok_or_else(|| bad("layer.create", format!("missing `op` ({OPS})")))?;
let (cid, lids) = layers_p(s, p)?;
@@ -99,8 +101,11 @@ fn create(s: &mut Session, p: &Value) -> Result {
continue;
};
let bytes = s.services.read_file(&f.path).map_err(|e| EngineError::Other(format!("cannot read {}: {e}", f.path)))?;
- let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref(), f.page)
+ // In the source layer's pixels, as the footage is drawn (a placed page of an After
+ // Effects project fills the stored size, #3099).
+ let doc = crate::vector::footage_doc(&f, &bytes)
.unwrap_or_else(|| Err(format!("{}: not a vector file", f.path)))
+ .map(|d| crate::vector::fitted_to_footage(d, &f))
.map_err(EngineError::Other)?;
// Images become footage layers (PNG files written next to the source and
// imported), parented to the shape layer below it.
@@ -123,7 +128,7 @@ fn create(s: &mut Session, p: &Value) -> Result {
// Without images: one shape layer. With images: the shapes between them
// in layers of their own, so the stack keeps the document's paint order.
let segments = if image_items.is_empty() { vec![doc.clone()] } else { crate::vector::split_at_images(&doc) };
- let top = crate::vector::shapes_from_vector(proj, &comp, &src, segments.last().unwrap_or(&doc));
+ let mut top = crate::vector::shapes_from_vector(proj, &comp, &src, segments.last().unwrap_or(&doc));
// Top to bottom: the last segment, image n, segment n − 1, … image 1, segment 0.
let mut stack = vec![];
for k in (0..segments.len().saturating_sub(1)).rev() {
@@ -140,9 +145,25 @@ fn create(s: &mut Session, p: &Value) -> Result {
sl.name = format!("{} {}", top.name, k + 1);
stack.push(sl);
}
+ // A placed page shows only its frame (the footage's raster ends there): every
+ // layer made from it takes a hidden frame layer as its track matte (#3099).
+ let matte = crate::vector::frame_matte(proj, &comp, &src, &f);
+ if let Some(m) = &matte {
+ let tm = Some(TrackMatte { layer: m.id, kind: MatteKind::Alpha });
+ top.track_matte = tm;
+ for l in &mut stack {
+ l.track_matte = tm;
+ }
+ }
let mut ids = vec![top.id];
ids.extend(stack.iter().filter(|l| matches!(l.source, LayerSource::Shape)).map(|l| l.id));
- // Each directly above the source: the top layer first.
+ // The frame matte is made too (last: the selection stays on the top layer).
+ ids.extend(matte.as_ref().map(|m| m.id));
+ // Each directly above the source: the frame matte (when there is one) above
+ // all, then the top layer first.
+ if let Some(m) = matte {
+ insert_above(proj, cid, lid, m, true)?;
+ }
insert_above(proj, cid, lid, top, true)?;
for l in stack {
insert_above(proj, cid, lid, l, true)?;
diff --git a/crates/engine/src/cmd/effect.rs b/crates/engine/src/cmd/effect.rs
index 1d352d192..1ee132955 100644
--- a/crates/engine/src/cmd/effect.rs
+++ b/crates/engine/src/cmd/effect.rs
@@ -1352,7 +1352,7 @@ pub fn specs() -> Vec {
"Load Effect Plug-in...",
[],
None,
- "{path (.wasm / .wat plug-in, API v1) | folder (loads every .wasm in it)}",
+ "{path (.wasm / .wat plug-in, WebAssembly ABI 1 or 2) | folder (loads every .wasm in it)}",
super::always,
plugins_load
),
diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs
index f5bae86d0..8129759c5 100644
--- a/crates/engine/src/cmd/file.rs
+++ b/crates/engine/src/cmd/file.rs
@@ -719,8 +719,8 @@ fn project_settings(s: &mut Session, p: &Value) -> Result {
Ok(serde_json::to_value(&s.project.settings).unwrap_or_default())
}
-/// Video Rendering and Effects: report or set the renderer (Mercury GPU Acceleration when a GPU
-/// adapter exists, else Mercury Software Only).
+/// Video Rendering and Effects: report or set the renderer (GPU Acceleration when a GPU
+/// adapter exists, else Software Rendering).
fn render_backend(s: &mut Session, p: &Value) -> Result {
if let Some(b) = p.get("backend").or_else(|| p.get("renderer")) {
let gpu = match b {
@@ -748,14 +748,14 @@ pub fn backend_status(s: &Session) -> Value {
let gpu = s.project.settings.gpu_acceleration;
// Why it renders on the CPU (#262).
let why = if !gpu {
- Some("the project's renderer is Mercury Software Only".to_string())
+ Some("the project's renderer is Software Rendering".to_string())
} else if adapter.is_none() {
Some(s.accel_note.clone().unwrap_or_else(|| "no GPU compositor is attached".into()))
} else {
None
};
json!({
- "renderer": if gpu { "Mercury GPU Acceleration" } else { "Mercury Software Only" },
+ "renderer": if gpu { "GPU Acceleration" } else { "Software Rendering" },
"gpuAcceleration": gpu,
"adapter": adapter,
"active": if gpu && adapter.is_some() { "gpu" } else { "cpu" },
diff --git a/crates/engine/src/cmd/panels_cmds.rs b/crates/engine/src/cmd/panels_cmds.rs
index 40241e6f4..95bc57c3c 100644
--- a/crates/engine/src/cmd/panels_cmds.rs
+++ b/crates/engine/src/cmd/panels_cmds.rs
@@ -1,4 +1,4 @@
-//! Commands behind the Progress, Media Browser, Metadata and Lumetri Scopes panels.
+//! Commands behind the Progress, Media Browser, Metadata and Video Scopes panels.
use effectcraft_project::ItemId;
use effectcraft_raster::Image;
@@ -159,7 +159,7 @@ fn project_comment(s: &mut Session, p: &Value) -> Result {
Ok(Value::Null)
}
-// ---------------------------------------------------------------- Lumetri Scopes
+// ---------------------------------------------------------------- Video Scopes
/// The comp frame the scopes read (the viewer's image: over the comp background), at most
/// `max_side` pixels on the long side.
@@ -257,7 +257,7 @@ pub fn specs() -> Vec {
cmd!("project.setProjectComment", "Project Comment", [], None, "{comment}", always, project_comment),
query!(
"scopes.analyze",
- "Lumetri Scopes",
+ "Video Scopes",
"{comp?, time?|frame?, scope?: waveformRgb|waveformLuma|waveformYc|vectorscopeYuv|vectorscopeHls|histogram|paradeRgb|paradeYuv, standard?: rec601|rec709|rec2020, float?, clamp?, size?}",
scopes_analyze
),
diff --git a/crates/engine/src/cmd/render_queue.rs b/crates/engine/src/cmd/render_queue.rs
index 6ca5b9ba0..9881f578f 100644
--- a/crates/engine/src/cmd/render_queue.rs
+++ b/crates/engine/src/cmd/render_queue.rs
@@ -863,6 +863,10 @@ fn save_current_preview(s: &mut Session, p: &Value) -> Result {
storage: s.storage_quota.as_deref(),
label: "Save Current Preview".into(),
nested_switches: s.prefs.general.switches_affect_nested_comps,
+
+ caches: None,
+ renderer: None,
+ gpu_frames: s.prefs.memory.gpu_frames(),
};
let r = exporter.export(&job, &mut |_, _| true).map_err(EngineError::Other)?;
s.toast(format!("Saved preview to {}", r.path));
diff --git a/crates/engine/src/cmd/timeline.rs b/crates/engine/src/cmd/timeline.rs
index fbc11654f..691f373de 100644
--- a/crates/engine/src/cmd/timeline.rs
+++ b/crates/engine/src/cmd/timeline.rs
@@ -145,6 +145,10 @@ fn prerender(
storage: None,
label: format!("Pre-render {}", l.name),
nested_switches: cx.nested_switches,
+
+ caches: None,
+ renderer: None,
+ gpu_frames: None,
};
let r = exporter.export(&job, progress)?;
Ok(Prerendered { path: r.path, width: comp.width, height: comp.height, frame_rate: comp.frame_rate, duration: end - start })
diff --git a/crates/engine/src/footage/footage_reload.rs b/crates/engine/src/footage/footage_reload.rs
index e56e16a96..5a0576f10 100644
--- a/crates/engine/src/footage/footage_reload.rs
+++ b/crates/engine/src/footage/footage_reload.rs
@@ -176,9 +176,11 @@ fn psd_layer(s: &Session, path: &str, l: &SourceLayer) -> Option<(u32, Option<(u
Some((u32::try_from(found.index).ok()?, size))
}
+/// The pixel size of vector footage `f` as its file is now (its page, framing and layer), or
+/// `None` when the file cannot be read or shows nothing.
fn vector_size(s: &Session, f: &Footage) -> Option<(u32, u32)> {
let bytes = s.services.read_file(&f.path).ok()?;
- let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref(), f.page)?.ok()?;
+ let doc = crate::vector::footage_doc(f, &bytes)?.ok()?;
(!doc.root.children.is_empty()).then(|| doc.pixel_size())
}
@@ -229,6 +231,10 @@ fn refreshed(s: &Session, f: &Footage, probed: Footage) -> Footage {
// The layer is gone from the file: keep the item as it was.
None => (w, h) = (f.width, f.height),
},
+ // A placed page of an After Effects project keeps the size After Effects stored: the
+ // layers' anchors and masks are measured against it, and the page is drawn to fill it
+ // (the probe would measure the page as a viewer frames it, #3099).
+ None if f.placed_page => (w, h) = (f.width, f.height),
// A later page of a PDF: the probe measures the first page.
None if f.page != 0 => (w, h) = (f.width, f.height),
None => {}
diff --git a/crates/engine/src/home/demo.rs b/crates/engine/src/home/demo.rs
index 7503178fa..ec1c353bc 100644
--- a/crates/engine/src/home/demo.rs
+++ b/crates/engine/src/home/demo.rs
@@ -238,7 +238,7 @@ fn showcase_3d(p: &mut Project, solids: ItemId) -> Comp {
anim(&mut cam, "cameraOptions/focusDistance", keys(&[(0.0, Value::Scalar(1700.0)), (5.0, Value::Scalar(1900.0)), (10.0, Value::Scalar(1650.0))]));
// Key light: warm spot casting soft shadows. Fill: cool ambient.
- let mut key = build::layer(p, &c, "Key Light", LayerSource::Light { kind: effectcraft_project::LightKind::Spot }, (1920, 1080), None);
+ let mut key = build::layer(p, &c, "Screen Color Key", LayerSource::Light { kind: effectcraft_project::LightKind::Spot }, (1920, 1080), None);
set(&mut key, "transform/position", Value::Vec3([500.0, -500.0, -700.0]));
set(&mut key, "transform/poi", Value::Vec3([960.0, 600.0, 300.0]));
set(&mut key, "lightOptions/intensity", Value::Scalar(150.0));
diff --git a/crates/engine/src/home/learn.rs b/crates/engine/src/home/learn.rs
index cea729368..9c89b2dbf 100644
--- a/crates/engine/src/home/learn.rs
+++ b/crates/engine/src/home/learn.rs
@@ -259,7 +259,7 @@ pub fn tutorials() -> Vec {
target: Some("menu.Layer"),
accepts: &["layer.newLight"],
when: Value::Null,
- show: vec![("layer.newLight", json!({"kind": "Spot", "name": "Key Light"}))],
+ show: vec![("layer.newLight", json!({"kind": "Spot", "name": "Screen Color Key"}))],
}),
step(S {
title: "Add a camera",
diff --git a/crates/engine/src/interchange/vector.rs b/crates/engine/src/interchange/vector.rs
index 324d3a82a..b52517cb3 100644
--- a/crates/engine/src/interchange/vector.rs
+++ b/crates/engine/src/interchange/vector.rs
@@ -261,15 +261,73 @@ pub fn is_vector(f: &effectcraft_project::Footage) -> bool {
effectcraft_render::is_vector_footage(f) || f.path.to_ascii_lowercase().ends_with(".svg")
}
+/// `doc` (the document of footage `f`) in the footage's layer pixels, as the renderer draws it
+/// ([`effectcraft_render::vector_fit`]): a placed page of an After Effects project is scaled to
+/// fill the size stored for it; other footage is unchanged.
+pub fn fitted_to_footage(mut doc: Doc, f: &effectcraft_project::Footage) -> Doc {
+ let (_, [sx, sy]) = effectcraft_render::vector_fit(f, [doc.width, doc.height], doc.pixel_size(), 1.0);
+ if f.placed_page {
+ doc.root.transform = Affine::scale_non_uniform(sx, sy) * doc.root.transform;
+ (doc.width, doc.height) = (doc.width * sx, doc.height * sy);
+ }
+ doc
+}
+
+/// Create Shapes from Vector Layer on a placed page of an After Effects project: a hidden shape
+/// layer filling the frame the footage is drawn in (its `width` × `height` raster, in the layer
+/// pixels of [`fitted_to_footage`]'s document), to be the Alpha track matte of every layer made
+/// from the page, so bleed, art past the artboard and the page's background stay hidden as on
+/// the footage (#3099). `None` for other footage, whose shapes are converted whole as before.
+///
+/// A track matte is drawn at the composition's resolution, so its edge is the footage's raster
+/// edge on shape and image layers alike; a mask on an image layer would be drawn at the image's
+/// own resolution and blur its edge when the image is scaled up.
+pub fn frame_matte(proj: &mut Project, comp: &Comp, src: &Layer, f: &effectcraft_project::Footage) -> Option {
+ if !f.placed_page || f.width == 0 || f.height == 0 {
+ return None;
+ }
+ let svg = format!(
+ r##""##,
+ f.width, f.height
+ );
+ let doc = effectcraft_svg::parse(svg.as_bytes()).ok()?;
+ let mut l = build::layer(proj, comp, &format!("{} Frame", src.name), LayerSource::Shape, (0, 0), None);
+ add_doc_shapes(proj, &mut l, &doc);
+ copy_transform(proj, src, &mut l);
+ like_source(&mut l, src);
+ // The layers it mattes carry the source's opacity already.
+ if let Some(o) = l.props.prop_mut("transform/opacity") {
+ o.value = Value::Scalar(100.0);
+ o.keys.clear();
+ o.expr = None;
+ }
+ l.name = comp.unique_layer_name(&format!("{} Frame", src.name));
+ l.switches.video = false;
+ Some(l)
+}
+
+/// The vector document of footage `f` (its file read as `bytes`; `None` for other formats): its
+/// page, framed as [`effectcraft_project::Footage::placed_page`] says, restricted to its layer.
+pub fn footage_doc(f: &effectcraft_project::Footage, bytes: &[u8]) -> Option> {
+ let framing = if f.placed_page { effectcraft_pdf::Framing::Placed } else { effectcraft_pdf::Framing::Viewer };
+ vector_doc(&f.path, bytes, f.layer.as_ref(), f.page, framing)
+}
+
/// The vector document of an SVG / PDF / AI / EPS file (`None` for other formats): page
-/// `page` of a PDF, restricted to one layer when `layer` names it.
-pub fn vector_doc(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::SourceLayer>, page: u32) -> Option> {
+/// `page` of a PDF framed as `framing` says, restricted to one layer when `layer` names it.
+pub fn vector_doc(
+ path: &str,
+ bytes: &[u8],
+ layer: Option<&effectcraft_project::SourceLayer>,
+ page: u32,
+ framing: effectcraft_pdf::Framing,
+) -> Option> {
if path.to_ascii_lowercase().ends_with(".svg") || effectcraft_svg::looks_like_svg(bytes) {
return Some(effectcraft_svg::parse(bytes).map_err(|e| format!("{path}: {e}")));
}
effectcraft_pdf::sniff(bytes)?;
Some(
- effectcraft_pdf::parse_page(bytes, page as usize)
+ effectcraft_pdf::parse_page_framed(bytes, page as usize, framing)
.map(|d| match layer {
Some(l) => effectcraft_pdf::layer_doc_with_sizes(&d, l.index as usize, l.layer_size).0,
None => d,
diff --git a/crates/engine/src/menus.rs b/crates/engine/src/menus.rs
index ed52d6e03..70acbd1fb 100644
--- a/crates/engine/src/menus.rs
+++ b/crates/engine/src/menus.rs
@@ -1263,7 +1263,7 @@ Window
Essential Graphics | window.panel {"panel":"essentialGraphics"}
Info | window.panel {"panel":"info"} | Cmd+2
Learn | help.inAppTutorials
- Lumetri Scopes | window.panel {"panel":"lumetriScopes"}
+ Video Scopes | window.panel {"panel":"lumetriScopes"}
Mask Interpolation | window.panel {"panel":"maskInterpolation"}
Media Browser | window.panel {"panel":"mediaBrowser"}
Metadata | window.panel {"panel":"metadata"}
diff --git a/crates/engine/src/menus/tests.rs b/crates/engine/src/menus/tests.rs
index 913a0e92e..8de3cc6dc 100644
--- a/crates/engine/src/menus/tests.rs
+++ b/crates/engine/src/menus/tests.rs
@@ -23,6 +23,25 @@ fn platform_entries(mac: bool) -> Vec<(Vec, MenuEntry)> {
out
}
+/// Guard the generated display tree, not command IDs, format tokens or dynamic user names.
+#[test]
+fn builtin_menu_labels_do_not_reintroduce_vendor_feature_names() {
+ for mac in [true, false] {
+ let entries = platform_entries(mac);
+ for (path, entry) in &entries {
+ for label in path.iter().chain(std::iter::once(&entry.label)) {
+ let folded = label.to_lowercase();
+ assert!(!folded.starts_with("cc "), "{label}");
+ for mark in ["lumetri", "mercury", "mocha", "keylight"] {
+ assert!(!folded.contains(mark), "{label}");
+ }
+ }
+ }
+ let scopes = entries.iter().find(|(_, e)| e.command == "window.panel" && e.params["panel"] == "lumetriScopes").unwrap();
+ assert_eq!(scopes.1.label, "Video Scopes");
+ }
+}
+
#[test]
fn top_level_order_matches_after_effects() {
let mac: Vec =
diff --git a/crates/engine/src/render_queue.rs b/crates/engine/src/render_queue.rs
index cc141f3fd..7c10af40c 100644
--- a/crates/engine/src/render_queue.rs
+++ b/crates/engine/src/render_queue.rs
@@ -34,6 +34,42 @@ pub struct ExportJob<'a> {
pub label: String,
/// Settings ▸ General ▸ Switches Affect Nested Comps.
pub nested_switches: bool,
+ /// The layer caches of the Render Queue run this job belongs to (#3335): consecutive jobs
+ /// start warm, and the run trims the accelerator's pooled textures once at its end. `None`:
+ /// a one-off export (its own caches, trimmed when it ends).
+ pub caches: Option<&'a RunCaches>,
+ /// The compositor the queue item's output modules render on (#3335): the first module
+ /// chooses (measuring a GPU project's first frames) and stores it here, the others follow.
+ pub renderer: Option<&'a Mutex