mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 23:01:27 +00:00
Merge remote-tracking branch 'upstream/main' into rq-readback-3335
# Conflicts: # crates/render/src/lib.rs
This commit is contained in:
+2
-2
@@ -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)
|
||||
|
||||
|
||||
Generated
+1
-14
@@ -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",
|
||||
@@ -7959,7 +7959,6 @@ dependencies = [
|
||||
"nix",
|
||||
"typed-path 0.13.0",
|
||||
"windows-sys 0.61.2",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7974,18 +7973,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"
|
||||
|
||||
+1
-1
@@ -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" }
|
||||
|
||||
@@ -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.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/effectcraft-effects.png" alt="A CC Particle World burst with Glow, and the Effect Controls panel listing the particle system's parameters" width="100%">
|
||||
</p>
|
||||
|
||||
## 3D
|
||||
|
||||
Classic 3D compositing: 3D layers with orientation and material options,
|
||||
|
||||
@@ -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<Backend, Failure> {
|
||||
}
|
||||
|
||||
/// 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<effectcraft_engine::Session, Failure> {
|
||||
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<Item
|
||||
/// `bench --gpu`: CPU vs GPU ms/frame for every comp (or `--comp`) at Full and Half. "cold"
|
||||
/// renders everything (no layer cache); "warm" reuses the layer cache (playback / scrubbing of
|
||||
/// unchanged layers: compositing cost); "viewer" is the GPU display path without readback;
|
||||
/// "auto" is Backend::Auto (Mercury GPU Acceleration) choosing per comp from measured times,
|
||||
/// "auto" is Backend::Auto (GPU Acceleration) choosing per comp from measured times,
|
||||
/// warm; "up/dn MB" are the GPU's uploads / readbacks per warm GPU frame.
|
||||
fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
||||
use effectcraft_render::Backend;
|
||||
|
||||
@@ -69,13 +69,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 <name> 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
|
||||
|
||||
@@ -817,7 +817,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.
|
||||
@@ -1172,16 +1172,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<PropPlan> {
|
||||
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<f64> = 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],
|
||||
@@ -1319,7 +1324,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 });
|
||||
|
||||
@@ -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<String> {
|
||||
@@ -453,11 +464,13 @@ fn camera_light_orient(auto_oriented: bool, light: Option<LightKind>) -> 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)
|
||||
}
|
||||
@@ -582,7 +595,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);
|
||||
}
|
||||
@@ -906,6 +919,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
BIN
Binary file not shown.
@@ -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\<name>.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.
|
||||
+1
@@ -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)
|
||||
|
||||
|
||||
@@ -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 <work>` 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 <work>/i3099-ai-mask.aep, which is then scrubbed (README: steps 5 and 6).
|
||||
(function () {
|
||||
var dir = "<work>/";
|
||||
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;
|
||||
})();
|
||||
+66
@@ -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))
|
||||
@@ -83,6 +83,45 @@ fn ntsc_and_odd_frame_rates_use_the_time_bases_after_effects_writes() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #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();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
mod helpers;
|
||||
|
||||
mod aepx;
|
||||
mod ai_mask;
|
||||
mod blend_modes;
|
||||
mod cameras;
|
||||
mod cases;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
},
|
||||
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)),
|
||||
|
||||
@@ -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<crate::ParamSpec>, 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<EffectSpec> {
|
||||
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)),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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)),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
],
|
||||
cc_color_offset,
|
||||
),
|
||||
spec("ec.color.cckernel", "CC Kernel", k_params, cc_kernel),
|
||||
spec("ec.color.cckernel", "Convolution Kernel", k_params, cc_kernel),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
],
|
||||
arbitrary_map,
|
||||
),
|
||||
spec("ec.color.lumetri", "Lumetri Color", lum, lumetri),
|
||||
spec("ec.color.lumetri", "Color Grading", lum, lumetri),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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)),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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),
|
||||
|
||||
@@ -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<crate::ParamSpec>, 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<EffectSpec> {
|
||||
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)),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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()),
|
||||
|
||||
@@ -180,7 +180,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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)),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
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()));
|
||||
|
||||
@@ -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;
|
||||
@@ -560,7 +563,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<Value> {
|
||||
None
|
||||
@@ -776,8 +779,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 +938,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 +1067,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 +1152,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 +1514,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 +1572,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() {
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
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"])),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
],
|
||||
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),
|
||||
|
||||
@@ -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<crate::ParamSpec>, 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<EffectSpec> {
|
||||
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"])),
|
||||
|
||||
@@ -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<Arc<dyn EffectPlugin>>; MAX_PLUGINS] = [const { OnceLock::new() }; MAX_PLUGINS];
|
||||
@@ -432,6 +459,12 @@ pub fn plugin(id: &str) -> Option<Arc<dyn EffectPlugin>> {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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)),
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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),
|
||||
|
||||
@@ -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<SpritePlan> {
|
||||
let pr = ctx.params;
|
||||
@@ -1144,7 +1144,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
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)),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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()),
|
||||
|
||||
@@ -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<Px>) {
|
||||
@@ -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<EffectSpec> {
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
],
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
],
|
||||
smoothie,
|
||||
),
|
||||
spec("ec.stylize.ccplastic", "CC Plastic", plastic_p, plastic),
|
||||
spec("ec.stylize.ccplastic", "Plastic Shading", plastic_p, plastic),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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;
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
spec(
|
||||
"ec.transition.cclightwipe",
|
||||
"CC Light Wipe",
|
||||
"Light Wipe",
|
||||
vec![
|
||||
cc_done(),
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
|
||||
@@ -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<EffectSpec> {
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
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<EffectSpec> {
|
||||
),
|
||||
spec(
|
||||
"ec.transition.ccwarpomatic",
|
||||
"CC WarpoMatic",
|
||||
"Warp Dissolve",
|
||||
vec![
|
||||
done(),
|
||||
p("layerToReveal", "Layer to Reveal", Value::Layer(None), ParamUi::Layer),
|
||||
|
||||
@@ -365,7 +365,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
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),
|
||||
|
||||
@@ -375,7 +375,7 @@ pub(crate) fn job_accel() -> Option<Arc<dyn Accelerator>> {
|
||||
}
|
||||
|
||||
/// 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() }
|
||||
}
|
||||
|
||||
@@ -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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
// 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<Value> {
|
||||
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)?;
|
||||
|
||||
@@ -1352,7 +1352,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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
|
||||
),
|
||||
|
||||
@@ -719,8 +719,8 @@ fn project_settings(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Value> {
|
||||
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" },
|
||||
|
||||
@@ -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<Value> {
|
||||
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<CommandSpec> {
|
||||
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
|
||||
),
|
||||
|
||||
@@ -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 => {}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -259,7 +259,7 @@ pub fn tutorials() -> Vec<Tutorial> {
|
||||
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",
|
||||
|
||||
@@ -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<Layer> {
|
||||
if !f.placed_page || f.width == 0 || f.height == 0 {
|
||||
return None;
|
||||
}
|
||||
let svg = format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{0}" height="{1}"><rect width="{0}" height="{1}" fill="#ffffff"/></svg>"##,
|
||||
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<Result<Doc, String>> {
|
||||
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<Result<Doc, String>> {
|
||||
/// `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<Result<Doc, String>> {
|
||||
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,
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -23,6 +23,25 @@ fn platform_entries(mac: bool) -> Vec<(Vec<String>, 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<String> =
|
||||
|
||||
@@ -304,7 +304,8 @@ pub fn parse_catalog(bytes: &[u8]) -> std::result::Result<(Catalog, Vec<String>)
|
||||
fn runs_here(r: &Release) -> bool {
|
||||
// Without a pre-release suffix (`0.18.0-dev` is 0.18.0).
|
||||
let app = env!("CARGO_PKG_VERSION").split(['-', '+']).next().unwrap_or_default();
|
||||
r.api == crate::effects::plugin::WASM_API_VERSION && r.min_app.as_deref().is_none_or(|m| !newer(m, app))
|
||||
(crate::effects::plugin::MIN_WASM_API_VERSION..=crate::effects::plugin::WASM_API_VERSION).contains(&r.api)
|
||||
&& r.min_app.as_deref().is_none_or(|m| !newer(m, app))
|
||||
}
|
||||
|
||||
/// The newest release this build can run (plug-in API and minimum app version).
|
||||
@@ -789,12 +790,13 @@ fn pick(s: &Session, c: &str, p: &Value) -> Result<(String, Package, Release)> {
|
||||
return Err(bad(
|
||||
c,
|
||||
format!(
|
||||
"{} {} needs plug-in API {} and EffectCraft {} or later; this is EffectCraft {} with WebAssembly plug-in API {}",
|
||||
"{} {} needs WebAssembly ABI {} and EffectCraft {} or later; this is EffectCraft {} with WebAssembly ABI {} to {}",
|
||||
pkg.name,
|
||||
rel.version,
|
||||
rel.api,
|
||||
rel.min_app.as_deref().unwrap_or("(any)"),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
crate::effects::plugin::MIN_WASM_API_VERSION,
|
||||
crate::effects::plugin::WASM_API_VERSION
|
||||
),
|
||||
));
|
||||
@@ -1125,8 +1127,9 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(latest_compatible(&p).map(|r| r.version.as_str()), Some("1.2.0"));
|
||||
assert!(runs_here(&rel("1.0.0", 1, None)), "API 1 modules still run on a build with a newer plug-in API");
|
||||
assert!(!runs_here(&rel("1.0.0", 2, None)), "WebAssembly modules are API 1: an API 2 release can't load");
|
||||
assert!(runs_here(&rel("1.0.0", 1, None)), "ABI 1 modules still run on a build with a newer ABI");
|
||||
assert!(runs_here(&rel("1.0.0", 2, None)), "ABI 2 modules run on this build");
|
||||
assert!(!runs_here(&rel("1.0.0", 3, None)), "a WebAssembly ABI this build doesn't implement can't load");
|
||||
let j = package_json(&p, None, &Status::default());
|
||||
assert_eq!(j["needsNewerApp"], true);
|
||||
assert!(latest_compatible(&Package::default()).is_none());
|
||||
|
||||
@@ -439,7 +439,7 @@ const RIGGED: &str = r#"{
|
||||
],
|
||||
"cameras": [{"type": "perspective", "name": "Shot", "perspective": {"yfov": 0.6, "znear": 0.1}}],
|
||||
"extensions": {"KHR_lights_punctual": {"lights": [
|
||||
{"type": "spot", "name": "Key Light", "color": [1, 0.8, 0.6], "intensity": 1.5, "spot": {"innerConeAngle": 0.25, "outerConeAngle": 0.5}},
|
||||
{"type": "spot", "name": "Screen Color Key", "color": [1, 0.8, 0.6], "intensity": 1.5, "spot": {"innerConeAngle": 0.25, "outerConeAngle": 0.5}},
|
||||
{"type": "point", "name": "Fill Light", "intensity": 0.5, "range": 4}
|
||||
]}}
|
||||
}"#;
|
||||
@@ -478,7 +478,7 @@ fn cameras_and_lights_from_model() {
|
||||
let r = s.execute("light.fromModel", json!({})).unwrap();
|
||||
let ls: Vec<Layer> = r["layers"].as_array().unwrap().iter().map(|v| layer_in(&s, cid(&s), v.as_u64().unwrap())).collect();
|
||||
assert_eq!(ls.len(), 2);
|
||||
let key = ls.iter().find(|l| l.name == "Key Light").unwrap();
|
||||
let key = ls.iter().find(|l| l.name == "Screen Color Key").unwrap();
|
||||
assert_eq!(key.source, LayerSource::Light { kind: LightKind::Spot });
|
||||
assert!((key.props.prop("lightOptions/coneAngle").unwrap().value.as_f64() - 1.0f64.to_degrees()).abs() < 1e-6);
|
||||
assert!((key.props.prop("lightOptions/coneFeather").unwrap().value.as_f64() - 50.0).abs() < 1e-6);
|
||||
|
||||
@@ -1221,7 +1221,35 @@ fn render_backend_says_why_it_renders_on_the_cpu() {
|
||||
assert_eq!(why(&mut s), json!("no GPU compositor is attached"));
|
||||
s.accel_note = Some("headless: renders on the CPU unless started with --gpu".into());
|
||||
assert_eq!(why(&mut s), json!("headless: renders on the CPU unless started with --gpu"));
|
||||
assert_eq!(s.execute("render.backend", json!({"backend": "cpu"})).unwrap()["why"], json!("the project's renderer is Mercury Software Only"));
|
||||
assert_eq!(s.execute("render.backend", json!({"backend": "cpu"})).unwrap()["why"], json!("the project's renderer is Software Rendering"));
|
||||
}
|
||||
|
||||
/// Display labels are descriptive; machine-readable settings remain the same.
|
||||
#[test]
|
||||
fn renderer_labels_keep_their_stable_machine_values() {
|
||||
let mut s = Session::default();
|
||||
for (backend, label, gpu) in [("cpu", "Software Rendering", false), ("gpu", "GPU Acceleration", true)] {
|
||||
let status = s.execute("render.backend", json!({"backend": backend})).unwrap();
|
||||
assert_eq!(status["renderer"], json!(label));
|
||||
assert_eq!(status["gpuAcceleration"], json!(gpu));
|
||||
assert_eq!(status["active"], json!("cpu"), "the headless test has no GPU adapter");
|
||||
}
|
||||
}
|
||||
|
||||
/// The label `render.backend` reports is one it accepts back, so a caller can round-trip the
|
||||
/// status instead of guessing the command's own values.
|
||||
#[test]
|
||||
fn renderer_status_labels_round_trip_through_the_command() {
|
||||
let mut s = Session::default();
|
||||
for (backend, label, gpu) in [("cpu", "Software Rendering", false), ("gpu", "GPU Acceleration", true)] {
|
||||
let reported = s.execute("render.backend", json!({"backend": backend})).unwrap()["renderer"].clone();
|
||||
assert_eq!(reported, json!(label));
|
||||
let parsed = effectcraft_render::Backend::parse(reported.as_str().unwrap()).expect("the reported renderer parses back");
|
||||
assert_eq!(parsed == effectcraft_render::Backend::Cpu, !gpu, "{reported}");
|
||||
let again = s.execute("render.backend", json!({"backend": reported})).unwrap();
|
||||
assert_eq!(again["renderer"], reported);
|
||||
assert_eq!(again["gpuAcceleration"], json!(gpu));
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings ▸ GPU Information names what the GPU compositor runs on, or why it is off, and the
|
||||
|
||||
@@ -133,8 +133,8 @@ pub struct JobCaches<'a> {
|
||||
pub cpu: &'a effectcraft_render::LayerCache,
|
||||
}
|
||||
|
||||
/// The compositor a job's frames render on: the GPU's (Mercury GPU Acceleration, with an
|
||||
/// accelerator) or the CPU's frame-parallel one (Mercury Software Only). The whole job renders
|
||||
/// The compositor a job's frames render on: the GPU's (GPU Acceleration, with an
|
||||
/// accelerator) or the CPU's frame-parallel one (Software Rendering). The whole job renders
|
||||
/// on one of them, never a mix: the two differ slightly. A GPU project with an accelerator
|
||||
/// measures both on its first frames and renders the job on the faster (#3335); the choice is
|
||||
/// in the render log. `EFFECTCRAFT_RQ_RENDERER=gpu|cpu|auto` overrides the measurement.
|
||||
|
||||
@@ -76,7 +76,7 @@ pub(crate) fn resolve_gpu_frames(env: Option<&str>, pref: Option<usize>, measure
|
||||
}
|
||||
|
||||
/// Overrides the Render Queue's measured choice of compositor for GPU projects with an
|
||||
/// accelerator (#3335): `gpu` (no measurement, the GPU), `cpu` (Mercury Software Only) or
|
||||
/// accelerator (#3335): `gpu` (no measurement, the GPU), `cpu` (Software Rendering) or
|
||||
/// `auto` (measure, the default).
|
||||
pub const RENDERER_ENV: &str = "EFFECTCRAFT_RQ_RENDERER";
|
||||
|
||||
@@ -347,7 +347,7 @@ impl<'a> Cx<'a> {
|
||||
cx
|
||||
}
|
||||
|
||||
/// The project's compositor: the GPU's for Mercury GPU Acceleration with an accelerator.
|
||||
/// The project's compositor: the GPU's for GPU Acceleration with an accelerator.
|
||||
fn project_renderer(&self) -> crate::JobRenderer {
|
||||
if self.project.settings.gpu_acceleration && self.job.accel.is_some() { crate::JobRenderer::Gpu } else { crate::JobRenderer::Cpu }
|
||||
}
|
||||
@@ -902,8 +902,8 @@ impl<'a> Cx<'a> {
|
||||
/// The render log's account of a job's compositor: which, why, and what was measured.
|
||||
pub(crate) fn describe_choice(c: &Choice) -> String {
|
||||
let name = match c.renderer {
|
||||
crate::JobRenderer::Gpu => "Mercury GPU Acceleration",
|
||||
crate::JobRenderer::Cpu => "Mercury Software Only",
|
||||
crate::JobRenderer::Gpu => "GPU Acceleration",
|
||||
crate::JobRenderer::Cpu => "Software Rendering",
|
||||
};
|
||||
let ms = |v: Option<f64>| v.map_or_else(|| "n/a".to_string(), |v| format!("{v:.1} ms/frame"));
|
||||
if c.gpu_ms.is_none() && c.cpu_ms.is_none() {
|
||||
@@ -1229,7 +1229,7 @@ mod tests {
|
||||
assert_eq!(cx.backend(), crate::JobRenderer::Cpu);
|
||||
assert_eq!(cx.gpu_cap(), None);
|
||||
assert_eq!(cx.renderer().active_accel().map(|a| a.name()), None, "CPU frames don't use the accelerator");
|
||||
assert_eq!(describe_choice(&cx.choice().unwrap()), "Mercury Software Only (measured on the first frames: GPU 117.0 ms/frame, CPU 68.0 ms/frame)");
|
||||
assert_eq!(describe_choice(&cx.choice().unwrap()), "Software Rendering (measured on the first frames: GPU 117.0 ms/frame, CPU 68.0 ms/frame)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -570,7 +570,7 @@ fn a_transfer_bound_gpu_job_renders_on_the_cpu() {
|
||||
for (i, f) in frames.iter().enumerate() {
|
||||
assert_eq!(f.get_pixel(10, 44).0, [255, 0, 0, 255], "frame {i} is not the CPU's");
|
||||
}
|
||||
assert!(log.contains("Renderer: Mercury Software Only (measured on the first frames: GPU "), "{log}");
|
||||
assert!(log.contains("Renderer: Software Rendering (measured on the first frames: GPU "), "{log}");
|
||||
// The queue item's next output module follows the first one's choice without measuring.
|
||||
let shared = Mutex::new(None);
|
||||
let opts = || JobOptions { renderer: Some(&shared), ..Default::default() };
|
||||
@@ -581,7 +581,7 @@ fn a_transfer_bound_gpu_job_renders_on_the_cpu() {
|
||||
let (frames, log) = run_pooled(&p, cid, &s, "/r/c_[#####].png", opts(), &again);
|
||||
assert_eq!(again.frames.load(Ordering::SeqCst), 0);
|
||||
assert!(frames.iter().all(|f| f.get_pixel(10, 44).0 == [255, 0, 0, 255]));
|
||||
assert!(log.contains("Renderer: Mercury Software Only (as the queue item's first output module)"), "{log}");
|
||||
assert!(log.contains("Renderer: Software Rendering (as the queue item's first output module)"), "{log}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -610,7 +610,7 @@ fn a_gpu_job_that_measures_no_faster_on_the_cpu_stays_on_the_gpu() {
|
||||
assert_eq!(frames.len(), 15);
|
||||
assert_eq!(fast.frames.load(Ordering::SeqCst), 15, "every kept frame, warm-up and measured ones included, is the GPU's");
|
||||
assert!(frames.iter().all(|f| f.pixels().all(|px| px.0 == [0, 255, 0, 255])));
|
||||
assert!(log.contains("Renderer: Mercury GPU Acceleration (measured on the first frames: GPU "), "{log}");
|
||||
assert!(log.contains("Renderer: GPU Acceleration (measured on the first frames: GPU "), "{log}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -625,7 +625,7 @@ fn gpu_cpu_and_measured_jobs_write_the_same_frames() {
|
||||
let echo = TimedAccel::new(5, false);
|
||||
p.settings.gpu_acceleration = false;
|
||||
let (cpu, log) = run_pooled(&p, cid, &s, "/i/a_[#####].png", JobOptions::default(), &echo);
|
||||
assert!(log.contains("Renderer: Mercury Software Only (the project's renderer)"), "{log}");
|
||||
assert!(log.contains("Renderer: Software Rendering (the project's renderer)"), "{log}");
|
||||
p.settings.gpu_acceleration = true;
|
||||
let gpu_slot = Mutex::new(Some(effectcraft_render::Backend::Gpu));
|
||||
let (gpu, _) = run_pooled(&p, cid, &s, "/i/b_[#####].png", JobOptions { renderer: Some(&gpu_slot), ..Default::default() }, &echo);
|
||||
|
||||
@@ -281,7 +281,7 @@ fn plugin_loading_reports_bad_modules_and_survives_failing_renders() {
|
||||
let files = [
|
||||
("a-good.wasm", wat_plugin("org.test.host.good", 1, "i32.const 0").into_bytes()),
|
||||
("b-garbage.wasm", b"\0asm\x01\0\0\0garbage".to_vec()),
|
||||
("c-v2.wasm", wat_plugin("org.test.host.v2", 2, "i32.const 0").into_bytes()),
|
||||
("c-v3.wasm", wat_plugin("org.test.host.v3", 3, "i32.const 0").into_bytes()),
|
||||
("d-empty.wasm", vec![]),
|
||||
("e-trap.wasm", wat_plugin("org.test.host.trap", 1, "unreachable").into_bytes()),
|
||||
("notes.txt", b"not a plug-in".to_vec()),
|
||||
@@ -296,7 +296,7 @@ fn plugin_loading_reports_bad_modules_and_survives_failing_renders() {
|
||||
let errs: Vec<String> = r["errors"].as_array().unwrap().iter().map(|e| e["path"].as_str().unwrap().to_string()).collect();
|
||||
assert_eq!(errs.len(), 3, "{r}");
|
||||
assert!(errs.iter().all(|p| !p.ends_with(".txt")));
|
||||
assert!(r["errors"].to_string().contains("API 2"), "{r}");
|
||||
assert!(r["errors"].to_string().contains("ABI 3"), "{r}");
|
||||
// A single file: errors come back as command errors; loading twice is refused.
|
||||
let good = dir.join("a-good.wasm").to_string_lossy().to_string();
|
||||
assert!(s.execute("effect.plugins.load", json!({"path": good})).unwrap_err().to_string().contains("already registered"));
|
||||
@@ -311,3 +311,130 @@ fn plugin_loading_reports_bad_modules_and_survives_failing_renders() {
|
||||
assert_eq!(px(&s), before);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// An ABI 2 plug-in (WebAssembly text) that shows the neighbouring frames it got: per pixel it
|
||||
/// writes |next.red − prev.red| into red, `flags / 8` into green, `prev_time` into blue and 1
|
||||
/// into alpha, so a render test can read the frames and their times back from the image.
|
||||
fn wat_neighbour_plugin(id: &str) -> String {
|
||||
let manifest = format!(r#"{{"api":2,"id":"{id}","name":"Host {id}","category":"Test Plug-ins","version":"1","params":[]}}"#);
|
||||
let len = manifest.len();
|
||||
let escaped = manifest.replace('"', "\\\"");
|
||||
format!(
|
||||
r#"(module
|
||||
(memory (export "memory") 2)
|
||||
(data (i32.const 16) "{escaped}")
|
||||
(func (export "ec_api_version") (result i32) i32.const 2)
|
||||
(func (export "ec_manifest_ptr") (result i32) i32.const 16)
|
||||
(func (export "ec_manifest_len") (result i32) i32.const {len})
|
||||
(func (export "ec_alloc") (param i32) (result i32) i32.const 1024)
|
||||
(func (export "ec_render2")
|
||||
(param $px i32) (param $w i32) (param $h i32) (param $params i32) (param $np i32)
|
||||
(param $t f64) (param $scale f64)
|
||||
(param $prev i32) (param $prev_t f64) (param $next i32) (param $next_t f64) (param $flags i32)
|
||||
(result i32)
|
||||
(local $i i32)
|
||||
(local $n i32)
|
||||
local.get $w
|
||||
local.get $h
|
||||
i32.mul
|
||||
local.set $n
|
||||
block $done
|
||||
loop $per_pixel
|
||||
local.get $i
|
||||
local.get $n
|
||||
i32.ge_u
|
||||
br_if $done
|
||||
;; red = |next.red - prev.red|
|
||||
local.get $px
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
local.get $next
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
f32.load
|
||||
local.get $prev
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
f32.load
|
||||
f32.sub
|
||||
f32.abs
|
||||
f32.store
|
||||
;; green = flags / 8
|
||||
local.get $px
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
local.get $flags
|
||||
f32.convert_i32_s
|
||||
f32.const 8
|
||||
f32.div
|
||||
f32.store offset=4
|
||||
;; blue = prev_time
|
||||
local.get $px
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
local.get $prev_t
|
||||
f32.demote_f64
|
||||
f32.store offset=8
|
||||
;; alpha = 1
|
||||
local.get $px
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
f32.const 1
|
||||
f32.store offset=12
|
||||
local.get $i
|
||||
i32.const 1
|
||||
i32.add
|
||||
local.set $i
|
||||
br $per_pixel
|
||||
end
|
||||
end
|
||||
i32.const 0))"#
|
||||
)
|
||||
}
|
||||
|
||||
/// The neighbouring frames are the layer's own pixels at other times: here a precomp whose
|
||||
/// nested solid fades from 0 to 100 % over one second, at 4 fps. At 0.5 s the neighbours are
|
||||
/// 0.25 s and 0.75 s, so red (the current frame) was 0.5 and |next − prev| is 0.5.
|
||||
#[test]
|
||||
fn abi_v2_plugins_render_with_real_neighbour_frames() {
|
||||
let mut s = effectcraft_host::session();
|
||||
let inner = s.execute("comp.new", json!({"name": "Inner", "width": 40, "height": 20, "frameRate": 4, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
||||
let solid = s.execute("layer.newSolid", json!({"color": [1.0, 0.0, 0.0], "width": 40, "height": 20})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("prop.addKey", json!({"layer": solid, "path": "transform/opacity", "time": 0.0, "value": 0})).unwrap();
|
||||
s.execute("prop.addKey", json!({"layer": solid, "path": "transform/opacity", "time": 1.0, "value": 100})).unwrap();
|
||||
let outer = s.execute("comp.new", json!({"name": "Outer", "width": 40, "height": 20, "frameRate": 4, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
||||
let layer = s.execute("layer.addItem", json!({"comp": outer, "item": inner, "time": 0})).unwrap()["layer"].as_u64().unwrap();
|
||||
let outer = effectcraft_engine::project::ItemId(outer);
|
||||
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let wasm = dir.path().join("neighbours.wasm");
|
||||
std::fs::write(&wasm, wat_neighbour_plugin("org.test.host.neighbours")).unwrap();
|
||||
let r = s.execute("effect.plugins.load", json!({"path": wasm.to_string_lossy()})).unwrap();
|
||||
assert_eq!(r["loaded"][0]["id"], "org.test.host.neighbours", "{r}");
|
||||
s.execute("effect.apply", json!({"layer": layer, "effect": "org.test.host.neighbours"})).unwrap();
|
||||
|
||||
let p = s.render(outer, Tick::from_seconds_f64(0.5), Default::default()).get(20, 10);
|
||||
assert!((p[0] - 0.5).abs() < 0.02, "precomp neighbours: {p:?}");
|
||||
// Both neighbours exist, the previous one is the 0.25 s frame, and the frame is opaque.
|
||||
assert!((p[1] - 3.0 / 8.0).abs() < 0.01, "flags: {p:?}");
|
||||
assert!((p[2] - 0.25).abs() < 0.01, "prev time: {p:?}");
|
||||
assert_eq!(p[3], 1.0, "{p:?}");
|
||||
|
||||
// At the very start of the layer only the next frame exists (the previous one is zeroed);
|
||||
// solids are filled with a quantised colour, hence the 8-bit step.
|
||||
let p = s.render(outer, Tick::ZERO, Default::default()).get(20, 10);
|
||||
assert!((p[0] - 0.25).abs() < 0.01, "the next frame's red, with no previous frame: {p:?}");
|
||||
assert!((p[1] - 2.0 / 8.0).abs() < 0.01, "only the next frame is valid: {p:?}");
|
||||
}
|
||||
|
||||
@@ -466,6 +466,11 @@ fn psd_smart_objects_with_perspective_and_warp_bake_as_placed() {
|
||||
|
||||
/// A PDF with two placed images (one turned a quarter) between a background and a shape.
|
||||
fn image_pdf() -> Vec<u8> {
|
||||
image_pdf_with("")
|
||||
}
|
||||
|
||||
/// [`image_pdf`] with `boxes` added to the page dictionary.
|
||||
fn image_pdf_with(boxes: &str) -> Vec<u8> {
|
||||
let content = "0.9 0.9 0.9 rg 0 0 120 80 re f\nq 60 0 0 30 10 40 cm /Im Do Q\nq 0 40 -20 0 110 10 cm /Im Do Q\n0 0 1 rg 20 5 30 20 re f\n";
|
||||
// 8×8 pixels in quadrants: red, green / white, black.
|
||||
let quad = |x: usize, y: usize| -> [u8; 3] { [[[255, 0, 0], [0, 255, 0]], [[255, 255, 255], [0, 0, 0]]][y / 4][x / 4] };
|
||||
@@ -474,7 +479,7 @@ fn image_pdf() -> Vec<u8> {
|
||||
&[
|
||||
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
|
||||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
|
||||
(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 80] /Resources << /XObject << /Im 5 0 R >> >> /Contents 4 0 R >>".into(), None),
|
||||
(3, format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 80] {boxes} /Resources << /XObject << /Im 5 0 R >> >> /Contents 4 0 R >>"), None),
|
||||
(4, "<< >>".into(), Some(content.as_bytes().to_vec())),
|
||||
(5, "<< /Type /XObject /Subtype /Image /Width 8 /Height 8 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>".into(), Some(px)),
|
||||
],
|
||||
@@ -546,6 +551,355 @@ fn retain_pdf(width: u32) -> Vec<u8> {
|
||||
)
|
||||
}
|
||||
|
||||
/// A 200 × 100 pt page: red left half, blue right half.
|
||||
fn halves_pdf() -> Vec<u8> {
|
||||
effectcraft_pdf::write::pdf(
|
||||
&[
|
||||
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
|
||||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
|
||||
(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Contents 4 0 R >>".into(), None),
|
||||
(4, "<< >>".into(), Some(b"1 0 0 rg 0 0 100 100 re f 0 0 1 rg 100 0 100 100 re f".to_vec())),
|
||||
],
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
/// #3099: footage from an After Effects project ([`effectcraft_project::Footage::placed_page`])
|
||||
/// keeps the size After Effects stored for a placed .ai, which can differ from the page as read
|
||||
/// here. The page fills that size at full and at reduced resolution alike: it was drawn at its own
|
||||
/// aspect at full resolution and stretched to the stored size below 3/4 (the art jumped when the
|
||||
/// viewer changed resolution, and the masks, stored against that size, came out too large at
|
||||
/// full resolution). Two placed items of one file at different stored sizes each get their own
|
||||
/// pixels, and neither reuses the file's frame at its own size, decoded first.
|
||||
#[test]
|
||||
fn placed_vector_footage_fills_its_stored_size_at_every_resolution() {
|
||||
use effectcraft_project::ItemKind;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
let path = tmp("stored-size.ai");
|
||||
std::fs::write(&path, halves_pdf()).unwrap();
|
||||
let mut s = effectcraft_host::session();
|
||||
let item = ItemId(s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap());
|
||||
let red = |p: [f32; 4]| p[3] > 0.99 && p[0] > 0.9 && p[2] < 0.1;
|
||||
let blue = |p: [f32; 4]| p[3] > 0.99 && p[2] > 0.9 && p[0] < 0.1;
|
||||
// At the file's own size first (its frame goes to the pool's cache).
|
||||
s.execute("comp.new", json!({"name": "Own", "width": 200, "height": 100, "frameRate": 25, "duration": 1})).unwrap();
|
||||
s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap();
|
||||
let own_comp = s.active_comp_id().unwrap();
|
||||
let own = s.render(own_comp, Tick::ZERO, RenderOpts::default());
|
||||
assert!(red(own.get(50, 90)) && blue(own.get(150, 90)));
|
||||
// Two placed items of the file with the sizes an After Effects project stores (other aspects).
|
||||
let proj = std::sync::Arc::make_mut(&mut s.project);
|
||||
let ItemKind::Footage(f) = &mut proj.item_mut(item).unwrap().kind else { panic!("footage") };
|
||||
(f.width, f.height, f.placed_page) = (100, 80, true);
|
||||
let tall = effectcraft_project::Footage { width: 60, height: 120, ..f.clone() };
|
||||
let tall = proj.add_item("tall", Default::default(), None, ItemKind::Footage(tall));
|
||||
for (item, (w, h)) in [(item, (100u32, 80u32)), (tall, (60, 120))] {
|
||||
s.execute("comp.new", json!({"name": format!("Stored {w}"), "width": w, "height": h, "frameRate": 25, "duration": 1})).unwrap();
|
||||
let lid = s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let (l, r, top, bottom) = (i64::from(w / 4), i64::from(w * 3 / 4), 3, i64::from(h) - 4);
|
||||
for collapse in [false, true] {
|
||||
s.execute("layer.setSwitch", json!({"layers": [lid], "switch": "collapse", "value": collapse})).unwrap();
|
||||
let full = s.render(cid, Tick::ZERO, RenderOpts::default());
|
||||
assert_eq!((full.width, full.height), (w, h));
|
||||
for y in [top, bottom] {
|
||||
let (a, b) = (full.get(l, y), full.get(r, y));
|
||||
assert!(red(a) && blue(b), "{w}×{h}, collapse {collapse}, full resolution, row {y}: {a:?} {b:?}");
|
||||
}
|
||||
let half = s.render(cid, Tick::ZERO, RenderOpts { scale: 0.5, ..RenderOpts::default() });
|
||||
assert_eq!((half.width, half.height), (w / 2, h / 2));
|
||||
for y in [top / 2, bottom / 2] {
|
||||
let (a, b) = (half.get(l / 2, y), half.get(r / 2, y));
|
||||
assert!(red(a) && blue(b), "{w}×{h}, collapse {collapse}, half resolution, row {y}: {a:?} {b:?}");
|
||||
}
|
||||
}
|
||||
// Create Shapes from Vector Layer draws the shapes where the footage is drawn.
|
||||
s.execute_checked("layer.create", json!({"op": "shapesFromVector", "layers": [lid]})).unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [lid], "switch": "video", "value": false})).unwrap();
|
||||
let shapes = s.render(cid, Tick::ZERO, RenderOpts::default());
|
||||
for y in [top, bottom] {
|
||||
let (a, b) = (shapes.get(l, y), shapes.get(r, y));
|
||||
assert!(red(a) && blue(b), "{w}×{h}, shapes, row {y}: {a:?} {b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #3099 review: Create Shapes from Vector Layer on a placed page keeps the page's frame. The
|
||||
/// footage's raster shows only the ArtBox (241 × 121) of `art_box_ai()`, whose grey media box
|
||||
/// reaches past it on every side; in a composition larger than the footage, the outlines show
|
||||
/// the same colours inside the frame and nothing (the background) outside it. Without the clip
|
||||
/// they showed the whole page: the grey background around the art.
|
||||
#[test]
|
||||
fn shapes_from_a_placed_page_stay_inside_its_frame() {
|
||||
use effectcraft_project::ItemKind;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
let path = tmp("placed-frame.ai");
|
||||
std::fs::write(&path, art_box_ai()).unwrap();
|
||||
let mut s = effectcraft_host::session();
|
||||
let item = ItemId(s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap());
|
||||
// As an After Effects project stores it: the ArtBox's size, placed.
|
||||
let ItemKind::Footage(f) = &mut std::sync::Arc::make_mut(&mut s.project).item_mut(item).unwrap().kind else { panic!("footage") };
|
||||
(f.width, f.height, f.placed_page) = (241, 121, true);
|
||||
s.execute("comp.new", json!({"name": "Big", "width": 400, "height": 300, "frameRate": 25, "duration": 1})).unwrap();
|
||||
let lid = s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
// The layer is centred: layer pixel (x, y) is comp (x + 79.5, y + 89.5).
|
||||
let at = |img: &Image, x: f64, y: f64| img.get((x + 79.5).floor() as i64, (y + 89.5).floor() as i64);
|
||||
let red = |p: [f32; 4]| p[3] > 0.99 && p[0] > 0.9 && p[1] < 0.1 && p[2] < 0.1;
|
||||
let blue = |p: [f32; 4]| p[3] > 0.99 && p[2] > 0.9 && p[0] < 0.1 && p[1] < 0.1;
|
||||
let empty = |p: [f32; 4]| p[0] < 0.02 && p[1] < 0.02 && p[2] < 0.02;
|
||||
let inside = [([20.0, 20.0], true), ([110.0, 100.0], true), ([130.0, 20.0], false), ([230.0, 110.0], false)];
|
||||
// Outside the frame, where the page's grey media box would show: left, right, above, below.
|
||||
let outside = [[-20.0, 60.0], [-55.0, 100.0], [260.0, 60.0], [300.0, 10.0], [120.0, -20.0], [120.0, 140.0]];
|
||||
let check = |img: &Image, what: &str| {
|
||||
for (p, want_red) in inside {
|
||||
let px = at(img, p[0], p[1]);
|
||||
assert!(if want_red { red(px) } else { blue(px) }, "{what}: inside the frame at {p:?}: {px:?}");
|
||||
}
|
||||
for p in outside {
|
||||
let px = at(img, p[0], p[1]);
|
||||
assert!(empty(px), "{what}: outside the frame at {p:?}: {px:?}");
|
||||
}
|
||||
};
|
||||
check(&s.render(cid, Tick::ZERO, RenderOpts::default()), "footage");
|
||||
s.execute_checked("layer.create", json!({"op": "shapesFromVector", "layers": [lid]})).unwrap();
|
||||
let comp = s.project.comp(cid).unwrap();
|
||||
assert!(!comp.layers.iter().find(|l| l.id.0 == lid).unwrap().switches.video, "the source is switched off");
|
||||
check(&s.render(cid, Tick::ZERO, RenderOpts::default()), "shapes");
|
||||
}
|
||||
|
||||
/// #3099 review: the images of a placed page become footage layers clipped to the frame too.
|
||||
/// `image_pdf_with` an ArtBox [20 15 100 70]: the upright image reaches past the frame's left
|
||||
/// edge, the turned one past its right and bottom edges, the grey page around it. After the
|
||||
/// conversion the layers inside the frame match the footage, and outside it nothing shows.
|
||||
#[test]
|
||||
fn images_from_a_placed_page_stay_inside_its_frame() {
|
||||
use effectcraft_project::{ItemKind, MatteKind, TrackMatte};
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
let path = tmp("placed-images.pdf");
|
||||
std::fs::write(&path, image_pdf_with("/ArtBox [20 15 100 70]")).unwrap();
|
||||
let mut s = effectcraft_host::session();
|
||||
let item = ItemId(s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap());
|
||||
let ItemKind::Footage(f) = &mut std::sync::Arc::make_mut(&mut s.project).item_mut(item).unwrap().kind else { panic!("footage") };
|
||||
(f.width, f.height, f.placed_page) = (80, 55, true);
|
||||
s.execute("comp.new", json!({"name": "Big", "width": 200, "height": 150, "frameRate": 25, "duration": 1})).unwrap();
|
||||
let lid = s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap()["layer"].as_u64().unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
// Centred: layer pixel (x, y) is comp (x + 60, y + 47.5).
|
||||
let at = |img: &Image, x: f64, y: f64| img.get((x + 60.0).floor() as i64, (y + 47.5).floor() as i64);
|
||||
let empty = |p: [f32; 4]| p[0] < 0.02 && p[1] < 0.02 && p[2] < 0.02;
|
||||
// Inside: both images and the grey page; outside: the images past the frame, the grey page.
|
||||
let inside = [[10.0, 8.0], [40.0, 8.0], [10.0, 22.0], [40.0, 22.0], [75.0, 25.0], [75.0, 50.0], [5.0, 40.0], [60.0, 10.0]];
|
||||
let outside = [[-5.0, 15.0], [85.0, 40.0], [75.0, 58.0], [-15.0, 50.0], [40.0, -5.0]];
|
||||
let footage = s.render(cid, Tick::ZERO, RenderOpts::default());
|
||||
for p in outside {
|
||||
assert!(empty(at(&footage, p[0], p[1])), "footage outside the frame at {p:?}: {:?}", at(&footage, p[0], p[1]));
|
||||
}
|
||||
let r = s.execute_checked("layer.create", json!({"op": "shapesFromVector", "layers": [lid]})).unwrap();
|
||||
assert_eq!(r["skipped"], json!([]), "{r}");
|
||||
// One hidden frame layer is the Alpha track matte of every layer made from the page.
|
||||
let comp = s.project.comp(cid).unwrap();
|
||||
let frame = comp.layers.iter().find(|l| l.name.ends_with(" Frame")).expect("the frame matte layer");
|
||||
assert!(!frame.switches.video);
|
||||
let made: Vec<_> = comp.layers.iter().filter(|l| l.id != frame.id && l.id.0 != lid).collect();
|
||||
assert_eq!(made.iter().filter(|l| l.name == "Image").count(), 2);
|
||||
for l in &made {
|
||||
assert_eq!(l.track_matte, Some(TrackMatte { layer: frame.id, kind: MatteKind::Alpha }), "{}", l.name);
|
||||
}
|
||||
let shapes = s.render(cid, Tick::ZERO, RenderOpts::default());
|
||||
for p in inside {
|
||||
let (a, b) = (at(&shapes, p[0], p[1]), at(&footage, p[0], p[1]));
|
||||
assert!((0..4).all(|k| (a[k] - b[k]).abs() < 0.05), "inside the frame at {p:?}: {a:?} vs footage {b:?}");
|
||||
}
|
||||
for p in outside {
|
||||
assert!(empty(at(&shapes, p[0], p[1])), "shapes outside the frame at {p:?}: {:?}", at(&shapes, p[0], p[1]));
|
||||
}
|
||||
}
|
||||
|
||||
/// A PDF with an ArtBox, imported here (not from an After Effects project), and a project saved
|
||||
/// with it before #3099: shown as a PDF viewer frames it (the crop box, 410 × 210) at its own
|
||||
/// size, exactly as before; the project file has no new field.
|
||||
#[test]
|
||||
fn a_pdf_with_an_art_box_imported_here_keeps_the_viewer_framing() {
|
||||
use effectcraft_project::{ItemKind, Project};
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
let bytes = art_box_ai();
|
||||
let path = tmp("viewer-art-box.pdf");
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
let mut s = effectcraft_host::session();
|
||||
let item = ItemId(s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap());
|
||||
let ItemKind::Footage(f) = &s.project.item(item).unwrap().kind else { panic!("footage") };
|
||||
assert_eq!((f.width, f.height, f.placed_page), (410, 210, false));
|
||||
s.execute("comp.new", json!({"name": "V", "width": 410, "height": 210, "frameRate": 25, "duration": 1})).unwrap();
|
||||
s.execute_checked("layer.addItem", json!({"item": item.0})).unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let direct = effectcraft_svg::rasterize(&effectcraft_pdf::parse(&bytes).unwrap(), 410, 210, 1.0);
|
||||
assert!(mean_diff(&s.render(cid, Tick::ZERO, RenderOpts::default()), &direct) < 1.0 / 255.0);
|
||||
let saved = s.project.to_json();
|
||||
assert!(!saved.contains("placed_page"), "the saved project is in the earlier format");
|
||||
s.project = std::sync::Arc::new(Project::from_json(&saved).unwrap());
|
||||
assert!(mean_diff(&s.render(cid, Tick::ZERO, RenderOpts::default()), &direct) < 1.0 / 255.0, "reopened");
|
||||
}
|
||||
|
||||
/// `i3099-a-boxes.ai` of `effectcraft-aep`'s `ai-mask-3099.aep` (its boxes as in
|
||||
/// `tests/fixtures/tools/ai-mask-3099.py`): MediaBox 420 × 220, CropBox, BleedBox and TrimBox
|
||||
/// inside it, ArtBox [60 50 300.6 170.4]. Here grey over the media box, the art box red on its
|
||||
/// left half and blue on its right half.
|
||||
fn art_box_ai() -> Vec<u8> {
|
||||
let page = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 420 220] /CropBox [5 5 415 215] /BleedBox [10 10 410 210] /TrimBox [20 20 400 200] /ArtBox [60 50 300.6 170.4] /Contents 4 0 R >>";
|
||||
let content = b"0.5 g 0 0 420 220 re f 1 0 0 rg 60 50 120.3 120.4 re f 0 0 1 rg 180.3 50 120.3 120.4 re f";
|
||||
effectcraft_pdf::write::pdf(
|
||||
&[
|
||||
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
|
||||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
|
||||
(3, page.into(), None),
|
||||
(4, "<< >>".into(), Some(content.to_vec())),
|
||||
],
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
/// `i3099-d-trim.ai` of `ai-mask-3099.aep`: MediaBox 418 × 218, TrimBox, ArtBox [40 30 250 150]
|
||||
/// (no CropBox). Grey over the media box, the art box red on its left half, blue on its right.
|
||||
fn trim_ai() -> Vec<u8> {
|
||||
let page = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 418 218] /TrimBox [9 9 409 209] /BleedBox [0 0 418 218] /ArtBox [40 30 250 150] /Contents 4 0 R >>";
|
||||
let content = b"0.5 g 0 0 418 218 re f 1 0 0 rg 40 30 105 120 re f 0 0 1 rg 145 30 105 120 re f";
|
||||
effectcraft_pdf::write::pdf(
|
||||
&[
|
||||
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
|
||||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
|
||||
(3, page.into(), None),
|
||||
(4, "<< >>".into(), Some(content.to_vec())),
|
||||
],
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
/// `i3099-c-rot.ai` of `ai-mask-3099.aep`: MediaBox [100 50 400 150] with `/Rotate 90`, grey,
|
||||
/// with a blue 20 × 10 marker 4 pt in from the media box's lower-left corner.
|
||||
fn rotated_ai() -> Vec<u8> {
|
||||
effectcraft_pdf::write::pdf(
|
||||
&[
|
||||
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
|
||||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
|
||||
(3, "<< /Type /Page /Parent 2 0 R /MediaBox [100 50 400 150] /Rotate 90 /Contents 4 0 R >>".into(), None),
|
||||
(4, "<< >>".into(), Some(b"0.5 g 100 50 300 100 re f 0 0 1 rg 104 54 20 10 re f".to_vec())),
|
||||
],
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
/// #3099 end to end, against After Effects 2026's own read-back of the committed project
|
||||
/// `ai-mask-3099.aep`: a placed .ai (ArtBox inside a larger crop box) parented to a Null parented
|
||||
/// to a Null, with two rectangular masks. After Effects sizes the footage 241 × 121 (the ArtBox)
|
||||
/// and puts the layer's corners at the comp points below (`toComp`). The art fills the layer
|
||||
/// between the masks at full and at half resolution; it was drawn from the crop box (410 × 210)
|
||||
/// scaled to the layer's width, so the masks cut the wrong part of it and half resolution
|
||||
/// squeezed it, and the Null left at its default anchor moved it. `artC` (`/Rotate 90`) is drawn
|
||||
/// unturned as After Effects draws it, `artD` (Continuously Rasterize) as `art`.
|
||||
#[test]
|
||||
fn aep_ai_layer_with_masks_under_two_nulls_renders_like_after_effects() {
|
||||
use effectcraft_engine::render::EvalCtx;
|
||||
use effectcraft_project::ItemKind;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
let dir = std::path::PathBuf::from(tmp("ai-mask-3099"));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let aep = dir.join("ai-mask-3099.aep");
|
||||
// The fixture belongs to `effectcraft-aep` (its sidecar and generator are there); this test
|
||||
// renders it through the wired session, which that crate cannot.
|
||||
std::fs::write(&aep, include_bytes!("../../../aep/tests/fixtures/ai-mask-3099.aep")).unwrap();
|
||||
let mut s = effectcraft_host::session();
|
||||
s.execute_checked("file.importAep", json!({"path": aep.to_string_lossy(), "allowLosses": true})).unwrap();
|
||||
// The project's footage paths are neutral (`C:\fixture\x\...`): the files are linked here.
|
||||
for (name, bytes, size) in
|
||||
[("i3099-a-boxes.ai", art_box_ai(), (241, 121)), ("i3099-c-rot.ai", rotated_ai(), (300, 100)), ("i3099-d-trim.ai", trim_ai(), (210, 120))]
|
||||
{
|
||||
let file = dir.join(name);
|
||||
std::fs::write(&file, bytes).unwrap();
|
||||
let item = s
|
||||
.project
|
||||
.items
|
||||
.values()
|
||||
.find(|i| matches!(&i.kind, ItemKind::Footage(f) if f.path.ends_with(name)))
|
||||
.map(|i| i.id)
|
||||
.unwrap_or_else(|| panic!("footage {name}"));
|
||||
let ItemKind::Footage(f) = &mut std::sync::Arc::make_mut(&mut s.project).item_mut(item).unwrap().kind else { panic!("footage") };
|
||||
assert_eq!((f.width, f.height, f.placed_page), (size.0, size.1, true), "{name}: After Effects' size of the placed .ai");
|
||||
(f.path, f.missing) = (file.to_string_lossy().into_owned(), false);
|
||||
}
|
||||
let cid = s.project.items.values().find(|i| i.name == "ai-mask").map(|i| i.id).expect("comp ai-mask");
|
||||
|
||||
// After Effects' toComp of the art layer's points (layer pixels → comp pixels).
|
||||
let ae = [
|
||||
([0.0, 0.0], [634.193413913741, 195.085717365354]),
|
||||
([241.0, 0.0], [913.539162876539, 269.936185209003]),
|
||||
([0.0, 121.0], [577.822625890412, 405.464362331113]),
|
||||
([40.0, 30.0], [666.58162514008, 259.669026149884]),
|
||||
([350.0, 160.0], [965.342375965623, 581.976354279664]),
|
||||
];
|
||||
let comp = s.project.comp(cid).unwrap();
|
||||
let art = comp.layer_by_name("art").unwrap();
|
||||
let (m, _) = EvalCtx::new(&s.project, cid, comp, Tick::ZERO).layer_to_comp(art);
|
||||
let to_comp = |[u, v]: [f64; 2]| [m.0[0][0] * u + m.0[0][1] * v + m.0[0][2], m.0[1][0] * u + m.0[1][1] * v + m.0[1][2]];
|
||||
for (p, want) in ae {
|
||||
let got = to_comp(p);
|
||||
assert!((got[0] - want[0]).abs() < 1e-3 && (got[1] - want[1]).abs() < 1e-3, "{p:?}: {got:?} vs After Effects {want:?}");
|
||||
}
|
||||
|
||||
// Inside the masks (M1: x 40..200, y 30..90; M2: x 220..350, y 100..160, cut by the layer).
|
||||
let red = |p: [f32; 4]| p[0] > 0.9 && p[1] < 0.1 && p[2] < 0.1;
|
||||
let blue = |p: [f32; 4]| p[2] > 0.9 && p[0] < 0.1 && p[1] < 0.1;
|
||||
let full = s.render(cid, Tick::ZERO, RenderOpts::default());
|
||||
let half = s.render(cid, Tick::ZERO, RenderOpts { scale: 0.5, ..RenderOpts::default() });
|
||||
for (p, want_red) in [([60.0, 45.0], true), ([100.0, 75.0], true), ([150.0, 40.0], false), ([190.0, 80.0], false), ([232.0, 112.0], false)] {
|
||||
let [x, y] = to_comp(p);
|
||||
let at = |img: &Image, k: f64| img.get((x * k).round() as i64, (y * k).round() as i64);
|
||||
for (img, k, what) in [(&full, 1.0, "full"), (&half, 0.5, "half")] {
|
||||
let px = at(img, k);
|
||||
let ok = if want_red { red(px) } else { blue(px) };
|
||||
assert!(ok, "{what} resolution, layer point {p:?}: {px:?} (want {})", if want_red { "red" } else { "blue" });
|
||||
}
|
||||
}
|
||||
// Outside the masks the art is cut away: the black background.
|
||||
for p in [[100.0, 110.0], [20.0, 60.0]] {
|
||||
let [x, y] = to_comp(p);
|
||||
let px = full.get(x.round() as i64, y.round() as i64);
|
||||
assert!(px[0] < 0.05 && px[1] < 0.05 && px[2] < 0.05, "layer point {p:?}: {px:?}");
|
||||
}
|
||||
// artC (/Rotate 90, position (1500, 800), anchor (150, 50)): After Effects draws the page
|
||||
// unturned, 300 × 100, the marker at its lower left (comp 1354..1374, 836..846).
|
||||
for (img, k, what) in [(&full, 1.0, "full"), (&half, 0.5, "half")] {
|
||||
let at = |x: f64, y: f64| img.get((x * k).round() as i64, (y * k).round() as i64);
|
||||
let marker = at(1364.0, 841.0);
|
||||
assert!(blue(marker), "{what} resolution, artC marker: {marker:?}");
|
||||
for (x, y) in [(1450.0, 780.0), (1630.0, 790.0), (1364.0, 770.0)] {
|
||||
let grey = at(x, y);
|
||||
assert!(grey[3] > 0.99 && (grey[0] - grey[2]).abs() < 0.02 && grey[0] > 0.1 && grey[0] < 0.9, "{what} resolution, artC at ({x}, {y}): {grey:?}");
|
||||
}
|
||||
}
|
||||
for (x, y) in [(1340.0, 800.0), (1500.0, 870.0)] {
|
||||
let px = full.get(x as i64, y as i64);
|
||||
assert!(px[0] < 0.05 && px[2] < 0.05, "outside artC ({x}, {y}): {px:?}");
|
||||
}
|
||||
// artD (ArtBox 210 × 120, Continuously Rasterize, the same masks, also under N2): placed by
|
||||
// After Effects' toComp of its corners.
|
||||
let (tl, tr, bl) = ([883.191424310055, 728.610562914909], [1126.6047325349, 793.832962280744], [827.286510567911, 937.250541393348]);
|
||||
let d_to_comp = |[u, v]: [f64; 2]| [0, 1].map(|i| tl[i] + u / 210.0 * (tr[i] - tl[i]) + v / 120.0 * (bl[i] - tl[i]));
|
||||
for (p, want_red) in [([60.0, 45.0], true), ([90.0, 75.0], true), ([150.0, 40.0], false), ([190.0, 80.0], false)] {
|
||||
let [x, y] = d_to_comp(p);
|
||||
for (img, k, what) in [(&full, 1.0, "full"), (&half, 0.5, "half")] {
|
||||
let px = img.get((x * k).round() as i64, (y * k).round() as i64);
|
||||
let ok = if want_red { red(px) } else { blue(px) };
|
||||
assert!(ok, "artD, {what} resolution, layer point {p:?}: {px:?} (want {})", if want_red { "red" } else { "blue" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_composition_retains_layer_sizes_and_positions() {
|
||||
use effectcraft_project::{ItemKind, LayerSource, Project};
|
||||
|
||||
+90
-11
@@ -14,19 +14,20 @@ fn is_svg(path: &str, bytes: &[u8]) -> bool {
|
||||
}
|
||||
|
||||
/// The vector document of an SVG / PDF / AI / EPS file (`None` for other formats): page
|
||||
/// `page` of a PDF, restricted to the footage's layer when it names one.
|
||||
/// `page` of a PDF framed as `framing` says, restricted to the footage's layer when it names one.
|
||||
pub fn vector_doc(
|
||||
path: &str,
|
||||
bytes: &[u8],
|
||||
layer: Option<&effectcraft_project::SourceLayer>,
|
||||
page: u32,
|
||||
framing: effectcraft_pdf::Framing,
|
||||
) -> Option<std::result::Result<effectcraft_svg::Doc, String>> {
|
||||
if is_svg(path, 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,
|
||||
@@ -111,14 +112,14 @@ pub(crate) fn decode(path: &str, bytes: &[u8], footage: &Footage, op: AlphaOp) -
|
||||
return Ok(Some(straight_to_image(px.width, px.height, &px.data, op)));
|
||||
}
|
||||
if is_svg(path, bytes) || is_pdf_like(path, bytes) {
|
||||
let doc = vector_doc(path, bytes, footage.layer.as_ref(), footage.page).unwrap_or(Err(String::new())).map_err(MediaError::Decode)?;
|
||||
let (w, h) = doc.pixel_size();
|
||||
let doc = footage_doc(path, bytes, footage).unwrap_or(Err(String::new())).map_err(MediaError::Decode)?;
|
||||
let ((w, h), to_image) = fit(&doc, footage, 1.0);
|
||||
// A declared size up to 30000² would be a 14 GB allocation that aborts the app (#2136):
|
||||
// past what Continuously Rasterize can draw, the footage reports an error instead.
|
||||
if u64::from(w) * u64::from(h) > u64::from(MAX_VECTOR_SIDE) * u64::from(MAX_VECTOR_SIDE) {
|
||||
return Err(MediaError::Decode(format!("{path}: {w}×{h} pixels is larger than vector footage can be drawn ({MAX_VECTOR_SIDE}×{MAX_VECTOR_SIDE})")));
|
||||
}
|
||||
let img = effectcraft_svg::rasterize(&doc, w, h, 1.0);
|
||||
let img = effectcraft_svg::rasterize_with(&doc, w, h, to_image);
|
||||
// The rasteriser produces premultiplied pixels already.
|
||||
let _ = op;
|
||||
return Ok(Some(img));
|
||||
@@ -129,12 +130,29 @@ pub(crate) fn decode(path: &str, bytes: &[u8], footage: &Footage, op: AlphaOp) -
|
||||
/// The largest side vector footage is drawn at, decoded or continuously rasterised.
|
||||
const MAX_VECTOR_SIDE: u32 = 16384;
|
||||
|
||||
/// A vector file rasterised at `scale` × its pixel size (Continuously Rasterize).
|
||||
pub(crate) fn rasterize_vector(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::SourceLayer>, page: u32, scale: f64) -> Option<Image> {
|
||||
let doc = vector_doc(path, bytes, layer, page)?.ok()?;
|
||||
let (w, h) = doc.pixel_size();
|
||||
let (sw, sh) = (((w as f64 * scale).ceil() as u32).clamp(1, MAX_VECTOR_SIDE), ((h as f64 * scale).ceil() as u32).clamp(1, MAX_VECTOR_SIDE));
|
||||
Some(effectcraft_svg::rasterize(&doc, sw, sh, scale))
|
||||
/// The vector footage `footage` (of the file `path` with these `bytes`) rasterised at `scale` ×
|
||||
/// its size as [`effectcraft_render::vector_fit`] draws it (Continuously Rasterize).
|
||||
pub(crate) fn rasterize_vector(path: &str, bytes: &[u8], footage: &Footage, scale: f64) -> Option<Image> {
|
||||
let doc = footage_doc(path, bytes, footage)?.ok()?;
|
||||
let ((sw, sh), to_image) = fit(&doc, footage, scale);
|
||||
Some(effectcraft_svg::rasterize_with(&doc, sw.clamp(1, MAX_VECTOR_SIDE), sh.clamp(1, MAX_VECTOR_SIDE), to_image))
|
||||
}
|
||||
|
||||
/// The page framing of vector footage: as After Effects places it for footage from an After
|
||||
/// Effects project ([`Footage::placed_page`]), as a PDF viewer shows it otherwise.
|
||||
fn framing(footage: &Footage) -> effectcraft_pdf::Framing {
|
||||
if footage.placed_page { effectcraft_pdf::Framing::Placed } else { effectcraft_pdf::Framing::Viewer }
|
||||
}
|
||||
|
||||
/// [`vector_doc`] of `footage`'s page, layer and framing.
|
||||
fn footage_doc(path: &str, bytes: &[u8], footage: &Footage) -> Option<std::result::Result<effectcraft_svg::Doc, String>> {
|
||||
vector_doc(path, bytes, footage.layer.as_ref(), footage.page, framing(footage))
|
||||
}
|
||||
|
||||
/// The image size and document → image mapping of `doc` drawn for `footage` at `scale`.
|
||||
fn fit(doc: &effectcraft_svg::Doc, footage: &Footage, scale: f64) -> ((u32, u32), effectcraft_svg::Affine) {
|
||||
let (size, [sx, sy]) = effectcraft_render::vector_fit(footage, [doc.width, doc.height], doc.pixel_size(), scale);
|
||||
(size, effectcraft_svg::Affine::scale_non_uniform(sx, sy))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -154,4 +172,65 @@ mod tests {
|
||||
let img = decode("small.svg", small, &footage, AlphaOp::new(footage.alpha, footage.premul_color)).unwrap().unwrap();
|
||||
assert_eq!((img.width, img.height), (40, 20));
|
||||
}
|
||||
|
||||
/// A 200 × 100 pt page: red left half, blue right half.
|
||||
fn halves_pdf() -> Vec<u8> {
|
||||
effectcraft_pdf::write::pdf(
|
||||
&[
|
||||
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
|
||||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
|
||||
(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Contents 4 0 R >>".into(), None),
|
||||
(4, "<< >>".into(), Some(b"1 0 0 rg 0 0 100 100 re f 0 0 1 rg 100 0 100 100 re f".to_vec())),
|
||||
],
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
/// An opaque red pixel.
|
||||
fn is_red(p: [f32; 4]) -> bool {
|
||||
p[3] > 0.99 && p[0] > 0.9 && p[2] < 0.1
|
||||
}
|
||||
|
||||
/// An opaque blue pixel.
|
||||
fn is_blue(p: [f32; 4]) -> bool {
|
||||
p[3] > 0.99 && p[2] > 0.9 && p[0] < 0.1
|
||||
}
|
||||
|
||||
/// #3099: an After Effects project keeps the size After Effects measured for a placed .ai or
|
||||
/// PDF, which can differ from this reader's page size. A placed page fills that size (the
|
||||
/// layer's anchor and masks are measured against it), decoded and continuously rasterised;
|
||||
/// other footage is drawn at the page's own size as before, whatever size it says.
|
||||
#[test]
|
||||
fn placed_vector_footage_fills_the_footage_size() {
|
||||
let pdf = halves_pdf();
|
||||
let mut footage = probe("art.ai", &pdf).unwrap().unwrap();
|
||||
assert_eq!((footage.width, footage.height), (200, 100));
|
||||
assert!(!footage.placed_page);
|
||||
let op = AlphaOp::new(footage.alpha, footage.premul_color);
|
||||
let own = decode("art.ai", &pdf, &footage, op).unwrap().unwrap();
|
||||
assert_eq!((own.width, own.height), (200, 100));
|
||||
assert!(is_red(own.get(50, 50)) && is_blue(own.get(150, 50)));
|
||||
|
||||
(footage.width, footage.height) = (100, 80);
|
||||
let unplaced = decode("art.ai", &pdf, &footage, op).unwrap().unwrap();
|
||||
assert!(unplaced == own, "not placed: drawn as before");
|
||||
assert_eq!(rasterize_vector("art.ai", &pdf, &footage, 0.5).map(|i| (i.width, i.height)), Some((100, 50)));
|
||||
|
||||
footage.placed_page = true;
|
||||
let img = decode("art.ai", &pdf, &footage, op).unwrap().unwrap();
|
||||
assert_eq!((img.width, img.height), (100, 80));
|
||||
for (x, y) in [(25, 3), (25, 76)] {
|
||||
assert!(is_red(img.get(x, y)), "red at ({x}, {y}): {:?}", img.get(x, y));
|
||||
}
|
||||
for (x, y) in [(75, 3), (75, 76)] {
|
||||
assert!(is_blue(img.get(x, y)), "blue at ({x}, {y}): {:?}", img.get(x, y));
|
||||
}
|
||||
|
||||
let sharp = rasterize_vector("art.ai", &pdf, &footage, 2.0).unwrap();
|
||||
assert_eq!((sharp.width, sharp.height), (200, 160));
|
||||
assert!(is_red(sharp.get(50, 155)) && is_blue(sharp.get(150, 155)));
|
||||
// Placed at the page's own size: 1:1.
|
||||
(footage.width, footage.height) = (200, 100);
|
||||
assert!(decode("art.ai", &pdf, &footage, op).unwrap().unwrap() == own, "placed at its own size: 1:1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,16 @@ fn colour_bars(w: u32, h: u32) -> Image {
|
||||
struct Key {
|
||||
path: Arc<str>,
|
||||
frame: i64,
|
||||
/// The alpha interpretation (bits 0–1) and how float values are encoded (bits 2–3: sRGB,
|
||||
/// as stored, a 2.4 power curve): they change the decoded pixels.
|
||||
/// The alpha interpretation (bits 0–1), how float values are encoded (bits 2–3: sRGB, as
|
||||
/// stored, a 2.4 power curve) and a placed PDF / Illustrator page's framing (bit 4,
|
||||
/// [`Footage::placed_page`]): they change the decoded pixels.
|
||||
interp: u8,
|
||||
matte: [u32; 3],
|
||||
/// A movie frame made at this size for a reduced-resolution render ((0, 0): as decoded).
|
||||
size: (u32, u32),
|
||||
/// A still at a reduced `size`: the footage's nominal size, which decides whether it is
|
||||
/// resampled (see `Inner::still_at_size`); (0, 0) otherwise.
|
||||
/// resampled (see `Inner::still_at_size`); also at its own size for a placed PDF /
|
||||
/// Illustrator page, which is drawn to fill it ([`Footage::placed_page`]); (0, 0) otherwise.
|
||||
nominal: (u32, u32),
|
||||
}
|
||||
|
||||
@@ -756,6 +758,8 @@ impl Inner {
|
||||
((d.0 as i128 * rate.num as i128 + den / 2) / den) as i64
|
||||
}
|
||||
|
||||
/// Where the frame of `footage` at `t` comes from (file, frame or media time) and its cache
|
||||
/// key, at the footage's own size.
|
||||
fn locate(footage: &Footage, t: Tick, float: FloatCurve) -> Loc {
|
||||
let alpha = match footage.alpha {
|
||||
AlphaMode::Straight => 0,
|
||||
@@ -768,7 +772,8 @@ impl Inner {
|
||||
FloatCurve::Srgb => 0,
|
||||
FloatCurve::Linear => 1,
|
||||
FloatCurve::Gamma24 => 2,
|
||||
} << 2);
|
||||
} << 2)
|
||||
| (u8::from(footage.placed_page) << 4);
|
||||
let matte = if footage.alpha == AlphaMode::Premultiplied { footage.premul_color.map(f32::to_bits) } else { [0; 3] };
|
||||
let key = |path: &str, frame| Key { path: path.into(), frame, interp, matte, size: (0, 0), nominal: (0, 0) };
|
||||
match footage.kind {
|
||||
@@ -818,6 +823,11 @@ impl Inner {
|
||||
loc.key.nominal = (footage.width, footage.height);
|
||||
}
|
||||
}
|
||||
// A placed PDF / Illustrator page is drawn to fill the footage's size (`layered::decode`,
|
||||
// #3099): two items of one file stored at different sizes have different pixels.
|
||||
if footage.placed_page && !movie {
|
||||
loc.key.nominal = (footage.width, footage.height);
|
||||
}
|
||||
let mut c = lock(&self.cache);
|
||||
// sequential playback of a movie: read the next frame ahead
|
||||
if playback && movie {
|
||||
@@ -1041,7 +1051,7 @@ impl FootageSource for MediaPool {
|
||||
return None;
|
||||
}
|
||||
let bytes = self.inner.read(&footage.path).ok()?;
|
||||
crate::layered::rasterize_vector(&footage.path, &bytes, footage.layer.as_ref(), footage.page, scale).map(Arc::new)
|
||||
crate::layered::rasterize_vector(&footage.path, &bytes, footage, scale).map(Arc::new)
|
||||
}
|
||||
|
||||
fn set_aux_footage(&self, paths: &[String]) {
|
||||
|
||||
@@ -29,7 +29,13 @@ ffmpeg elsewhere) to check the code tables against an independent implementation
|
||||
the app's existing object model. References retain object and generation numbers. A bounded
|
||||
compatibility repair recovers referenced objects from object streams when damaged xrefs omit
|
||||
them; every recovery or unresolved reference is reported in `Doc::skipped` (up to 64 messages).
|
||||
The existing page tree preserves inherited resources, MediaBox / CropBox and `/Rotate`.
|
||||
The existing page tree preserves inherited resources, MediaBox / CropBox and `/Rotate`. A page
|
||||
is its CropBox (else MediaBox), turned by `/Rotate` (`Framing::Viewer`, the default). Footage
|
||||
from an After Effects project is framed as After Effects places a PDF or `.ai`
|
||||
(`Framing::Placed`, `parse_page_framed`): the page's own ArtBox clipped to the CropBox (else
|
||||
MediaBox) when it has one, else the CropBox, else the MediaBox, with `/Rotate` ignored
|
||||
(measured in After Effects 2026 with own files, #3099). Not measured: a CropBox without an
|
||||
ArtBox, files saved by Illustrator itself, `.pdf` names.
|
||||
Content/image filters remain app-owned: Flate (with PNG predictors), LZW, ASCIIHex, ASCII85,
|
||||
RunLength and CCITTFax. Encrypted files remain refused; `page_count` returns zero when the
|
||||
reader refuses a document.
|
||||
|
||||
+26
-4
@@ -92,8 +92,25 @@ pub fn parse(bytes: &[u8]) -> Result<Doc, Error> {
|
||||
|
||||
/// Parse page `index` of a PDF (EPS files have one page).
|
||||
pub fn parse_page(bytes: &[u8], index: usize) -> Result<Doc, Error> {
|
||||
parse_page_framed(bytes, index, Framing::Viewer)
|
||||
}
|
||||
|
||||
/// Which region of a PDF page becomes the document, and whether `/Rotate` turns it.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Framing {
|
||||
/// As a PDF viewer shows the page: its CropBox (else MediaBox), turned by `/Rotate`.
|
||||
#[default]
|
||||
Viewer,
|
||||
/// As After Effects places a PDF or `.ai` as footage: the page's ArtBox clipped to the
|
||||
/// CropBox (else MediaBox) when it has one, else the CropBox, else the MediaBox, with
|
||||
/// `/Rotate` ignored (measured in After Effects 2026 with own files, #3099).
|
||||
Placed,
|
||||
}
|
||||
|
||||
/// [`parse_page`] with the page framed as `framing` says (EPS files are their bounding box).
|
||||
pub fn parse_page_framed(bytes: &[u8], index: usize, framing: Framing) -> Result<Doc, Error> {
|
||||
match sniff(bytes) {
|
||||
Some(Format::Pdf) => parse_pdf(bytes, index),
|
||||
Some(Format::Pdf) => parse_pdf(bytes, index, framing),
|
||||
Some(Format::Eps) => parse_eps(bytes),
|
||||
None => Err(Error::NotVector),
|
||||
}
|
||||
@@ -142,17 +159,22 @@ fn finish(nodes: Vec<Node>, flags: Vec<bool>, skipped: Vec<String>, width: f64,
|
||||
Doc { width, height, root, skipped }
|
||||
}
|
||||
|
||||
fn parse_pdf(bytes: &[u8], index: usize) -> Result<Doc, Error> {
|
||||
/// Page `index` of a PDF framed as `framing` says, as a document in points (y down).
|
||||
fn parse_pdf(bytes: &[u8], index: usize, framing: Framing) -> Result<Doc, Error> {
|
||||
let file = object::File::parse(bytes)?;
|
||||
let pages = page::pages(&file);
|
||||
if pages.is_empty() {
|
||||
return Err(Error::NoPages);
|
||||
}
|
||||
let pg = pages.get(index).ok_or(Error::PageOutOfRange(index))?;
|
||||
let [x0, y0, x1, y1] = pg.bbox;
|
||||
let (frame, rotate) = match framing {
|
||||
Framing::Viewer => (pg.bbox, pg.rotate),
|
||||
Framing::Placed => (pg.art.unwrap_or(pg.bbox), 0),
|
||||
};
|
||||
let [x0, y0, x1, y1] = frame;
|
||||
let (w, h) = (x1 - x0, y1 - y0);
|
||||
// Default user space (y up) → document pixels (y down), with the page's /Rotate.
|
||||
let (to_doc, dw, dh) = match pg.rotate {
|
||||
let (to_doc, dw, dh) = match rotate {
|
||||
90 => (Affine::new([0.0, 1.0, 1.0, 0.0, -y0, -x0]), h, w),
|
||||
180 => (Affine::new([-1.0, 0.0, 0.0, 1.0, x1, -y0]), w, h),
|
||||
270 => (Affine::new([0.0, -1.0, -1.0, 0.0, y1, x1]), h, w),
|
||||
|
||||
+37
-4
@@ -23,6 +23,9 @@ pub struct Page {
|
||||
pub resources: Dict,
|
||||
/// CropBox (else MediaBox) `[x0, y0, x1, y1]`.
|
||||
pub bbox: [f64; 4],
|
||||
/// The page's own ArtBox clipped to [`Page::bbox`] (ISO 32000-1 §14.11.2), when it has one
|
||||
/// that keeps an area there; not inherited (table 30).
|
||||
pub art: Option<[f64; 4]>,
|
||||
pub rotate: i32,
|
||||
}
|
||||
|
||||
@@ -31,13 +34,15 @@ pub fn pages(file: &File) -> Vec<Page> {
|
||||
let mut out = vec![];
|
||||
let Some(cat) = file.catalog() else { return out };
|
||||
let Some(root) = file.get_dict(cat, "Pages") else { return out };
|
||||
/// Collects the pages under `node`, passing on the attributes pages inherit (resources,
|
||||
/// MediaBox, CropBox, /Rotate); bounded in depth and page count.
|
||||
fn walk(file: &File, node: &Dict, inherited: (Option<Dict>, Option<Vec<f64>>, Option<Vec<f64>>, i32), out: &mut Vec<Page>, depth: usize) {
|
||||
if depth > 32 || out.len() > 10_000 {
|
||||
return;
|
||||
}
|
||||
let res = file.get_dict(node, "Resources").cloned().or(inherited.0);
|
||||
let media = file.get(node, "MediaBox").map(|m| file.nums(m)).filter(|v| v.len() == 4).or(inherited.1);
|
||||
let crop = file.get(node, "CropBox").map(|m| file.nums(m)).filter(|v| v.len() == 4).or(inherited.2);
|
||||
let media = file.get(node, "MediaBox").map(|m| file.nums(m)).filter(|v| is_area(v)).or(inherited.1);
|
||||
let crop = file.get(node, "CropBox").map(|m| file.nums(m)).filter(|v| is_area(v)).or(inherited.2);
|
||||
let rot = file.get_num(node, "Rotate").map(|r| r as i32).unwrap_or(inherited.3);
|
||||
match file.get(node, "Kids").and_then(Obj::array) {
|
||||
Some(kids) if file.get(node, "Type").and_then(Obj::name) != Some("Page") => {
|
||||
@@ -49,8 +54,16 @@ pub fn pages(file: &File) -> Vec<Page> {
|
||||
}
|
||||
_ => {
|
||||
let b = crop.or(media).unwrap_or_else(|| vec![0.0, 0.0, 612.0, 792.0]);
|
||||
let bbox = [b[0].min(b[2]), b[1].min(b[3]), b[0].max(b[2]), b[1].max(b[3])];
|
||||
out.push(Page { dict: node.clone(), resources: res.unwrap_or_default(), bbox, rotate: rot.rem_euclid(360) });
|
||||
let bbox = normalized(&b).unwrap_or([0.0, 0.0, 612.0, 792.0]);
|
||||
// The ArtBox is the page's own (not inherited), clipped to the crop box.
|
||||
let art = file
|
||||
.get(node, "ArtBox")
|
||||
.map(|m| file.nums(m))
|
||||
.filter(|v| is_area(v))
|
||||
.and_then(|v| normalized(&v))
|
||||
.map(|a| [a[0].max(bbox[0]), a[1].max(bbox[1]), a[2].min(bbox[2]), a[3].min(bbox[3])])
|
||||
.filter(|a| a[2] > a[0] && a[3] > a[1]);
|
||||
out.push(Page { dict: node.clone(), resources: res.unwrap_or_default(), bbox, art, rotate: rot.rem_euclid(360) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +71,26 @@ pub fn pages(file: &File) -> Vec<Page> {
|
||||
out
|
||||
}
|
||||
|
||||
/// A box `[x0, y0, x1, y1]` (corners in any order) with finite corners and a finite span that
|
||||
/// encloses an area.
|
||||
fn is_area(v: &[f64]) -> bool {
|
||||
match v {
|
||||
[x0, y0, x1, y1] => {
|
||||
let (w, h) = (x1 - x0, y1 - y0);
|
||||
v.iter().all(|c| c.is_finite()) && w.is_finite() && h.is_finite() && w.abs() > 0.0 && h.abs() > 0.0
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A box with its corners ordered (`x0 < x1`, `y0 < y1`), `None` unless [`is_area`].
|
||||
fn normalized(v: &[f64]) -> Option<[f64; 4]> {
|
||||
match v {
|
||||
[x0, y0, x1, y1] if is_area(v) => Some([x0.min(*x1), y0.min(*y1), x0.max(*x1), y0.max(*y1)]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The page's content streams, decoded and joined.
|
||||
pub fn page_content(file: &File, page: &Page) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
|
||||
@@ -112,6 +112,86 @@ fn pdf_forms_rotation_cmyk_and_alpha() {
|
||||
assert_eq!(img.get(30, 30)[3], 0.0, "form clipped to its bounding box");
|
||||
}
|
||||
|
||||
/// A one-page PDF with a MediaBox [0 0 420 220], `pages` added to the /Pages node and `page` to
|
||||
/// the page: grey over the media box, a blue 20 × 10 marker at PDF (64, 54).
|
||||
fn boxes_pdf(pages: &str, page: &str) -> Vec<u8> {
|
||||
let content = "0.5 g 0 0 420 220 re f 0 0 1 rg 64 54 20 10 re f";
|
||||
pdf(
|
||||
&[
|
||||
(1, "<< /Type /Catalog /Pages 2 0 R >>".to_string(), None),
|
||||
(2, format!("<< /Type /Pages /Kids [3 0 R] /Count 1 {pages} >>"), None),
|
||||
(3, format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 420 220] {page} /Contents 4 0 R >>"), None),
|
||||
(4, "<< >>".to_string(), Some(content.as_bytes().to_vec())),
|
||||
],
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
/// The first page of `bytes` framed as After Effects places it.
|
||||
fn placed(bytes: &[u8]) -> Doc {
|
||||
parse_page_framed(bytes, 0, Framing::Placed).unwrap()
|
||||
}
|
||||
|
||||
/// #3099: After Effects 2026 places a .ai or PDF as footage by the page's ArtBox and shows only
|
||||
/// that region (a page with MediaBox 420 × 220, CropBox 410 × 210, TrimBox 380 × 180 and ArtBox
|
||||
/// [60 50 300.6 170.4] imports as 241 × 121). `Framing::Placed` frames it so; the viewer framing
|
||||
/// (the default, used by File ▸ Import) stays the CropBox.
|
||||
#[test]
|
||||
fn placed_framing_is_the_art_box() {
|
||||
let bytes = boxes_pdf("/CropBox [5 5 415 215]", "/TrimBox [20 20 400 200] /ArtBox [60 50 300.6 170.4]");
|
||||
let doc = placed(&bytes);
|
||||
assert!((doc.width - 240.6).abs() < 1e-9 && (doc.height - 120.4).abs() < 1e-9, "{} × {}", doc.width, doc.height);
|
||||
assert_eq!(doc.pixel_size(), (241, 121));
|
||||
// The art box's top-left corner is the document's origin: the marker (PDF x 64..84, y 54..64)
|
||||
// is at x 4..24, y 106.4..116.4 from the top.
|
||||
let (w, h) = doc.pixel_size();
|
||||
let img = effectcraft_svg::rasterize(&doc, w, h, 1.0);
|
||||
let marker = img.get(10, 110);
|
||||
assert!(marker[2] > 0.99 && marker[0] < 0.01, "{marker:?}");
|
||||
assert!(img.get(30, 110)[2] < 0.6, "{:?}", img.get(30, 110));
|
||||
let viewer = parse(&bytes).unwrap();
|
||||
assert_eq!((viewer.width, viewer.height), (410.0, 210.0), "the viewer framing is unchanged");
|
||||
}
|
||||
|
||||
/// The ArtBox is clipped to the crop box (ISO 32000-1 §14.11.2); one that keeps no area there,
|
||||
/// a degenerate or unreadable one, or one on the /Pages node (not inherited, table 30) leaves the
|
||||
/// crop box (else the media box).
|
||||
#[test]
|
||||
fn placed_framing_clips_the_art_box_and_falls_back() {
|
||||
let size = |pages: &str, page: &str| {
|
||||
let d = placed(&boxes_pdf(pages, page));
|
||||
(d.width, d.height)
|
||||
};
|
||||
let crop = "/CropBox [5 5 415 215]";
|
||||
assert_eq!(size(crop, "/ArtBox [-100 -100 1000 1000]"), (410.0, 210.0), "larger than the page");
|
||||
assert_eq!(size(crop, "/ArtBox [300 100 900 900]"), (115.0, 115.0), "partly outside");
|
||||
let (w, h) = size(crop, "/ArtBox [300.6 170.4 60 50]");
|
||||
assert!((w - 240.6).abs() < 1e-9 && (h - 120.4).abs() < 1e-9, "corners in any order: {w} × {h}");
|
||||
// Finite corners whose span is not.
|
||||
let big = format!("1{}", "0".repeat(308));
|
||||
let unreadable = [format!("/ArtBox [-{big} 50 {big} 170]")];
|
||||
for page in ["/ArtBox [500 300 600 400]", "/ArtBox [60 50 60 170]", "/ArtBox [60 50 300]"].map(String::from).iter().chain(&unreadable) {
|
||||
assert_eq!(size(crop, page), (410.0, 210.0), "{page}");
|
||||
}
|
||||
assert_eq!(size("/ArtBox [60 50 300 170]", ""), (420.0, 220.0), "an ArtBox on /Pages is not inherited");
|
||||
// A crop box that is not a box leaves the media box.
|
||||
assert_eq!(size("", &format!("/CropBox [-{big} 5 {big} 215]")), (420.0, 220.0));
|
||||
}
|
||||
|
||||
/// After Effects ignores /Rotate on placed footage (a page [100 50 400 150] with /Rotate 90
|
||||
/// imports as 300 × 100, drawn unturned); the viewer framing turns it.
|
||||
#[test]
|
||||
fn placed_framing_ignores_rotate() {
|
||||
let bytes = boxes_pdf("", "/Rotate 90");
|
||||
let doc = placed(&bytes);
|
||||
assert_eq!((doc.width, doc.height), (420.0, 220.0));
|
||||
// Unturned: the marker at PDF (64..84, 54..64) is at x 64..84, y 156..166 from the top.
|
||||
let img = effectcraft_svg::rasterize(&doc, 420, 220, 1.0);
|
||||
assert!(img.get(70, 160)[2] > 0.99, "{:?}", img.get(70, 160));
|
||||
let viewer = parse(&bytes).unwrap();
|
||||
assert_eq!((viewer.width, viewer.height), (220.0, 420.0));
|
||||
}
|
||||
|
||||
const EPS: &str = "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n\
|
||||
/m {moveto} bind def /l {lineto} bind def\n\
|
||||
/cm { 6 array astore concat } bind def\n\
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "effectcraft-plugin"
|
||||
description = "EffectCraft effect plug-ins: loads sandboxed WebAssembly effect modules (plug-in API v1) into the effect registry"
|
||||
description = "EffectCraft effect plug-ins: loads sandboxed WebAssembly effect modules (WebAssembly ABI v1 and v2) into the effect registry"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
//! # effectcraft-plugin (L3)
|
||||
//!
|
||||
//! Loads **WebAssembly effect plug-ins** (plug-in ABI v1, see `docs/plugins.md`; the native
|
||||
//! plug-in API 2 additions such as layer parameters and host services are not part of it) into the effect
|
||||
//! registry ([`effectcraft_effects::plugin`]). A plug-in is a `.wasm` (or `.wat`) module with
|
||||
//! no imports that exports:
|
||||
//! Loads **WebAssembly effect plug-ins** (WebAssembly ABI v1 and v2, see `docs/plugins.md`; the
|
||||
//! native plug-in API 2 additions such as layer parameters and host services are not part of it)
|
||||
//! into the effect registry ([`effectcraft_effects::plugin`]). A plug-in is a `.wasm` (or
|
||||
//! `.wat`) module with no imports that exports:
|
||||
//!
|
||||
//! | export | signature | |
|
||||
//! |---|---|---|
|
||||
//! | `memory` | memory | linear memory shared with the host |
|
||||
//! | `ec_api_version` | `() -> i32` | must return 1 |
|
||||
//! | `ec_api_version` | `() -> i32` | 1 for ABI 1, 2 for ABI 2 (neighbouring frames) |
|
||||
//! | `ec_manifest_ptr`, `ec_manifest_len` | `() -> i32` | a UTF-8 JSON [`PluginManifest`] in memory |
|
||||
//! | `ec_alloc` | `(bytes: i32) -> i32` | a buffer of at least `bytes` bytes, 8-byte aligned; it may reuse the previous one |
|
||||
//! | `ec_render` | `(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, time: f64, scale: f64) -> i32` | process the pixels in place; 0 = ok |
|
||||
//! | `ec_render` (ABI 1) | `(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, time: f64, scale: f64) -> i32` | process the pixels in place; 0 = ok |
|
||||
//! | `ec_render2` (ABI 2) | `ec_render`'s arguments, then `prev: i32, prev_time: f64, next: i32, next_time: f64, flags: i32` | the same, with the neighbouring frames; 0 = ok |
|
||||
//!
|
||||
//! For each render the host asks `ec_alloc` for one buffer holding the parameters (`nparams`
|
||||
//! little-endian `f64`s) followed by the pixels (`width × height × 4` little-endian `f32`s,
|
||||
//! premultiplied RGBA, row-major), calls `ec_render`, and reads the pixels back.
|
||||
//! premultiplied RGBA, row-major), calls the render export, and reads the pixels back. An ABI 2
|
||||
//! module gets two more pixel buffers after the current frame: the layer's previous and next
|
||||
//! frames, resampled onto the current grid, and their layer times. The buffers are always
|
||||
//! allocated (zeroed when a frame does not exist); `flags` bit 0/1 says which are real.
|
||||
//!
|
||||
//! Modules run in the [wasmi](https://github.com/wasmi-labs/wasmi) interpreter (pure Rust,
|
||||
//! MIT/Apache-2.0) with deterministic floats, no imports (no files, network or clock) and a
|
||||
@@ -26,7 +30,9 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
use effectcraft_effects::EffectSpec;
|
||||
pub use effectcraft_effects::plugin::{EffectPlugin, PLUGIN_API_VERSION, PluginFrame, PluginManifest, PluginParams, WASM_API_VERSION, register_plugin};
|
||||
pub use effectcraft_effects::plugin::{
|
||||
EffectPlugin, MIN_WASM_API_VERSION, PLUGIN_API_VERSION, PluginFrame, PluginImage, PluginManifest, PluginParams, WASM_API_VERSION, register_plugin,
|
||||
};
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm;
|
||||
|
||||
+174
-5
@@ -163,11 +163,15 @@ fn wasm_plugins_are_sandboxed() {
|
||||
// No imports (files, clock, network…).
|
||||
let imports = r#"(module (import "env" "now" (func $now (result f64))) (memory (export "memory") 1))"#;
|
||||
assert!(load_err(load_wasm(imports.as_bytes(), "imports.wat")).contains("import"));
|
||||
// Garbage, wrong API version, bad manifests.
|
||||
// Garbage, an ABI this build doesn't implement, bad manifests. A module that claims ABI 2
|
||||
// must export `ec_render2` (this one still has only `ec_render`).
|
||||
assert!(load_wasm(b"\0asm nope", "bad.wasm").is_err());
|
||||
let v2 = gain_module("org.test.v2", "")
|
||||
.replace("(func (export \"ec_api_version\") (result i32) i32.const 1)", "(func (export \"ec_api_version\") (result i32) i32.const 2)");
|
||||
assert!(load_err(load_wasm(v2.as_bytes(), "v2.wat")).contains("API 2"));
|
||||
assert!(load_err(load_wasm(v2.as_bytes(), "v2.wat")).contains("ec_render2"));
|
||||
let v3 = gain_module("org.test.v3", "")
|
||||
.replace("(func (export \"ec_api_version\") (result i32) i32.const 1)", "(func (export \"ec_api_version\") (result i32) i32.const 3)");
|
||||
assert!(load_err(load_wasm(v3.as_bytes(), "v3.wat")).contains("ABI 3"));
|
||||
let reserved = gain_module("ec.blur.mine", "");
|
||||
assert!(load_err(load_wasm(reserved.as_bytes(), "reserved.wat")).contains("reserved"));
|
||||
}
|
||||
@@ -194,14 +198,14 @@ fn abi_violations_fail_to_load_with_reasons() {
|
||||
assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("ec_api_version"));
|
||||
let m = edit(&base, "(param $scale f64) (result i32)", "(param $scale f32) (result i32)");
|
||||
assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("ec_render"));
|
||||
// API versions other than 1.
|
||||
for v in ["0", "-1", "2147483647"] {
|
||||
// WebAssembly ABIs other than 1 and 2.
|
||||
for v in ["0", "-1", "3", "2147483647"] {
|
||||
let m = edit(
|
||||
&base,
|
||||
"(func (export \"ec_api_version\") (result i32) i32.const 1)",
|
||||
&format!("(func (export \"ec_api_version\") (result i32) i32.const {v})"),
|
||||
);
|
||||
assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains(&format!("API {v}")), "{v}");
|
||||
assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains(&format!("ABI {v}")), "{v}");
|
||||
}
|
||||
// Manifest out of bounds, negative or absurd lengths, not JSON.
|
||||
let ptr = "(func (export \"ec_manifest_ptr\") (result i32) i32.const 16)";
|
||||
@@ -317,3 +321,168 @@ fn wasm_rendering_is_deterministic() {
|
||||
let twin = load_wasm(gain_module("org.test.det2", body).as_bytes(), "det2.wat").unwrap();
|
||||
assert_eq!(bits(&render(twin, 0.37, true)), first);
|
||||
}
|
||||
|
||||
/// An ABI 2 module whose `ec_render2` writes what it got from the host into each pixel: red is
|
||||
/// |next.red − prev.red|, green `flags / 8`, blue `prev_time`, alpha 1.
|
||||
fn neighbour_module(id: &str) -> String {
|
||||
let manifest = format!(r#"{{"api":2,"id":"{id}","name":"Neighbours {id}","category":"Test Plug-ins","version":"1","params":[]}}"#);
|
||||
let len = manifest.len();
|
||||
let escaped = manifest.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
format!(
|
||||
r#"(module
|
||||
(memory (export "memory") 2)
|
||||
(data (i32.const 16) "{escaped}")
|
||||
(func (export "ec_api_version") (result i32) i32.const 2)
|
||||
(func (export "ec_manifest_ptr") (result i32) i32.const 16)
|
||||
(func (export "ec_manifest_len") (result i32) i32.const {len})
|
||||
(func (export "ec_alloc") (param i32) (result i32) i32.const 1024)
|
||||
(func (export "ec_render2")
|
||||
(param $px i32) (param $w i32) (param $h i32) (param $params i32) (param $np i32)
|
||||
(param $t f64) (param $scale f64)
|
||||
(param $prev i32) (param $prev_t f64) (param $next i32) (param $next_t f64) (param $flags i32)
|
||||
(result i32)
|
||||
(local $i i32) (local $n i32)
|
||||
local.get $w
|
||||
local.get $h
|
||||
i32.mul
|
||||
local.set $n
|
||||
block $done
|
||||
loop $per_pixel
|
||||
local.get $i
|
||||
local.get $n
|
||||
i32.ge_u
|
||||
br_if $done
|
||||
local.get $px
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
local.get $next
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
f32.load
|
||||
local.get $prev
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
f32.load
|
||||
f32.sub
|
||||
f32.abs
|
||||
f32.store
|
||||
local.get $px
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
local.get $flags
|
||||
f32.convert_i32_s
|
||||
f32.const 8
|
||||
f32.div
|
||||
f32.store offset=4
|
||||
local.get $px
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
local.get $prev_t
|
||||
f32.demote_f64
|
||||
f32.store offset=8
|
||||
local.get $px
|
||||
local.get $i
|
||||
i32.const 16
|
||||
i32.mul
|
||||
i32.add
|
||||
f32.const 1
|
||||
f32.store offset=12
|
||||
local.get $i
|
||||
i32.const 1
|
||||
i32.add
|
||||
local.set $i
|
||||
br $per_pixel
|
||||
end
|
||||
end
|
||||
i32.const 0))"#
|
||||
)
|
||||
}
|
||||
|
||||
fn neighbour_image(red: f32) -> effectcraft_effects::plugin::PluginImage {
|
||||
effectcraft_effects::plugin::PluginImage { width: 2, height: 1, pixels: vec![[red, 0.0, 0.0, 1.0]; 2], origin: [0.0; 2], scale: 1.0 }
|
||||
}
|
||||
|
||||
/// A host with frames at two layer times.
|
||||
struct Host {
|
||||
time: f64,
|
||||
fps: f64,
|
||||
prev: Option<(f64, effectcraft_effects::plugin::PluginImage)>,
|
||||
next: Option<(f64, effectcraft_effects::plugin::PluginImage)>,
|
||||
}
|
||||
|
||||
impl effectcraft_effects::plugin::PluginHost for Host {
|
||||
fn input_at(&self, layer_time: f64) -> Option<effectcraft_effects::plugin::PluginImage> {
|
||||
if let Some((t, img)) = &self.prev
|
||||
&& (t - layer_time).abs() < 1e-9
|
||||
{
|
||||
return Some(img.clone());
|
||||
}
|
||||
if let Some((t, img)) = &self.next
|
||||
&& (t - layer_time).abs() < 1e-9
|
||||
{
|
||||
return Some(img.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn layer_input(&self, _param_id: &str, _layer_time: f64) -> Option<effectcraft_effects::plugin::PluginImage> {
|
||||
None
|
||||
}
|
||||
|
||||
fn params_at(&self, _layer_time: f64) -> Option<(Vec<f64>, Vec<String>)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn frame_rate(&self) -> f64 {
|
||||
self.fps
|
||||
}
|
||||
|
||||
fn time(&self) -> f64 {
|
||||
self.time
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_v2_receives_neighbour_frames_and_times() {
|
||||
load_wasm(neighbour_module("org.test.neighbours").as_bytes(), "neighbours.wat").unwrap();
|
||||
let p = effectcraft_effects::plugin::plugin("org.test.neighbours").unwrap();
|
||||
assert!(effectcraft_effects::plugin::reads_other_times("org.test.neighbours"));
|
||||
assert!(effectcraft_effects::is_time_dependent("org.test.neighbours"));
|
||||
// The module has no parameters, so the flattened vector is empty.
|
||||
let params = effectcraft_effects::plugin::PluginParams { manifest: p.manifest(), flat: &[], texts: &[] };
|
||||
let mut px = vec![[0.0f32; 4]; 2];
|
||||
let host = Host { time: 0.5, fps: 4.0, prev: Some((0.25, neighbour_image(0.1))), next: Some((0.75, neighbour_image(0.6))) };
|
||||
let mut frame = effectcraft_effects::plugin::PluginFrame { width: 2, height: 1, pixels: &mut px, scale: 1.0, origin: [0.0; 2], layer_size: [2.0, 1.0] };
|
||||
p.render_v2(&mut frame, ¶ms, 0.5, &host).unwrap();
|
||||
assert!((px[0][0] - 0.5).abs() < 1e-6, "|next − prev|: {px:?}");
|
||||
assert!((px[0][1] - 3.0 / 8.0).abs() < 1e-6, "both flags: {px:?}");
|
||||
assert!((px[0][2] - 0.25).abs() < 1e-6, "previous frame's time: {px:?}");
|
||||
assert_eq!(px[0][3], 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_v2_without_a_host_gets_zeroed_neighbours() {
|
||||
let spec = load_wasm(neighbour_module("org.test.neighbours.empty").as_bytes(), "empty.wat").unwrap();
|
||||
let mut next_id = 1;
|
||||
let g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next_id), spec.name, [2.0, 1.0]);
|
||||
let params = effectcraft_effects::flatten_params(&g, &mut |p| p.value.clone());
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: [2.0, 1.0], seed: 1, adjustment: false, env: EffectEnv::default() };
|
||||
let buf = Buf { img: Image::filled(2, 1, [0.5, 0.5, 0.5, 1.0]), offset: [0.0; 2], scale: 1.0 };
|
||||
// No host: both neighbour buffers are zeroed and no flag is set.
|
||||
assert_eq!((spec.render)(&ctx, buf).img.get(0, 0), [0.0, 0.0, 0.0, 1.0]);
|
||||
// ABI 1 plug-ins keep the time-independent behaviour.
|
||||
let gain = load_wasm(gain_module("org.test.neighbours.v1", "").as_bytes(), "v1.wat").unwrap();
|
||||
assert!(!effectcraft_effects::plugin::reads_other_times("org.test.neighbours.v1"));
|
||||
assert!(!effectcraft_effects::is_time_dependent("org.test.neighbours.v1"));
|
||||
assert!(gain.id.starts_with("org.test."));
|
||||
}
|
||||
|
||||
+244
-43
@@ -1,10 +1,11 @@
|
||||
//! The wasmi-backed [`EffectPlugin`] (plug-in API v1 ABI, see the crate docs).
|
||||
//! The wasmi-backed [`EffectPlugin`] (WebAssembly ABI v1 and v2, see the crate docs).
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use wasmi::{Config, Engine, Instance, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc, WasmParams, WasmResults};
|
||||
|
||||
use crate::{EffectPlugin, PluginFrame, PluginManifest, PluginParams, WASM_API_VERSION};
|
||||
use crate::{EffectPlugin, MIN_WASM_API_VERSION, PluginFrame, PluginManifest, PluginParams, WASM_API_VERSION};
|
||||
use effectcraft_effects::plugin::{PluginHost, PluginImage};
|
||||
|
||||
/// Instruction budget per frame: a fixed allowance plus some per pixel. Generous for per-pixel
|
||||
/// work, but a runaway loop stops instead of hanging the render.
|
||||
@@ -15,32 +16,74 @@ const MAX_MEMORY: usize = 1 << 30;
|
||||
/// The longest manifest read.
|
||||
const MAX_MANIFEST: i32 = 1 << 20;
|
||||
|
||||
/// ABI 1: one buffer.
|
||||
type RenderFn = TypedFunc<(i32, i32, i32, i32, i32, f64, f64), i32>;
|
||||
/// ABI 2: the same call plus the previous and next frame buffers and their layer times.
|
||||
type Render2Fn = TypedFunc<(i32, i32, i32, i32, i32, f64, f64, i32, f64, i32, f64, i32), i32>;
|
||||
|
||||
enum Renderer {
|
||||
V1(RenderFn),
|
||||
V2(Render2Fn),
|
||||
}
|
||||
|
||||
/// One instantiated module (renders running on several threads each take their own).
|
||||
struct Inst {
|
||||
store: Store<StoreLimits>,
|
||||
memory: Memory,
|
||||
alloc: TypedFunc<i32, i32>,
|
||||
render: Renderer,
|
||||
}
|
||||
|
||||
/// An instance whose ABI version (and so its render export) is not selected yet.
|
||||
struct Pending {
|
||||
store: Store<StoreLimits>,
|
||||
instance: Instance,
|
||||
memory: Memory,
|
||||
alloc: TypedFunc<i32, i32>,
|
||||
render: RenderFn,
|
||||
}
|
||||
|
||||
impl Inst {
|
||||
impl Pending {
|
||||
/// A typed export by name, reporting a missing or mistyped export like [`missing`].
|
||||
fn func<P: WasmParams, R: WasmResults>(&self, name: &str) -> Result<TypedFunc<P, R>, String> {
|
||||
self.instance.get_typed_func::<P, R>(&self.store, name).map_err(|_| missing(name))
|
||||
}
|
||||
|
||||
/// Select the render export for `abi`.
|
||||
fn finish(self, abi: u32) -> Result<Inst, String> {
|
||||
let render = match abi {
|
||||
1 => Renderer::V1(self.instance.get_typed_func::<_, i32>(&self.store, "ec_render").map_err(|_| missing("ec_render"))?),
|
||||
2 => Renderer::V2(self.instance.get_typed_func::<_, i32>(&self.store, "ec_render2").map_err(|_| missing("ec_render2"))?),
|
||||
v => return Err(unsupported(v as i32)),
|
||||
};
|
||||
Ok(Inst { store: self.store, memory: self.memory, alloc: self.alloc, render })
|
||||
}
|
||||
}
|
||||
|
||||
/// The error for a missing or mistyped export.
|
||||
fn missing(name: &str) -> String {
|
||||
format!("the module exports no `{name}` (or with the wrong signature)")
|
||||
}
|
||||
|
||||
/// The error for an ABI this build does not implement.
|
||||
fn unsupported(v: i32) -> String {
|
||||
format!("the module implements WebAssembly ABI {v}, this build implements ABI {MIN_WASM_API_VERSION} to {WASM_API_VERSION}")
|
||||
}
|
||||
|
||||
/// The module's `ec_api_version`, when this build implements it.
|
||||
fn abi_of(v: i32) -> Result<u32, String> {
|
||||
match u32::try_from(v) {
|
||||
Ok(v) if (MIN_WASM_API_VERSION..=WASM_API_VERSION).contains(&v) => Ok(v),
|
||||
_ => Err(unsupported(v)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The error for a wasmi or trap failure.
|
||||
fn err(e: impl std::fmt::Display) -> String {
|
||||
e.to_string()
|
||||
}
|
||||
|
||||
fn instantiate(engine: &Engine, module: &Module) -> Result<Inst, String> {
|
||||
/// Instantiate `module` in a fresh, limited store and read its memory and allocator.
|
||||
fn begin(engine: &Engine, module: &Module) -> Result<Pending, String> {
|
||||
let mut store = Store::new(engine, StoreLimitsBuilder::new().memory_size(MAX_MEMORY).instances(1).build());
|
||||
store.limiter(|l| l);
|
||||
store.set_fuel(FUEL_BASE).map_err(err)?;
|
||||
@@ -48,14 +91,20 @@ fn instantiate(engine: &Engine, module: &Module) -> Result<Inst, String> {
|
||||
let instance = linker.instantiate_and_start(&mut store, module).map_err(|e| format!("instantiation failed: {e}"))?;
|
||||
let memory = instance.get_memory(&store, "memory").ok_or("the module exports no `memory`")?;
|
||||
let alloc = instance.get_typed_func::<i32, i32>(&store, "ec_alloc").map_err(|_| missing("ec_alloc"))?;
|
||||
let render = instance.get_typed_func::<(i32, i32, i32, i32, i32, f64, f64), i32>(&store, "ec_render").map_err(|_| missing("ec_render"))?;
|
||||
Ok(Inst { store, instance, memory, alloc, render })
|
||||
Ok(Pending { store, instance, memory, alloc })
|
||||
}
|
||||
|
||||
/// Instantiate `module` and select the render export for `abi`.
|
||||
fn instantiate(engine: &Engine, module: &Module, abi: u32) -> Result<Inst, String> {
|
||||
begin(engine, module)?.finish(abi)
|
||||
}
|
||||
|
||||
/// A loaded WebAssembly effect plug-in.
|
||||
pub struct WasmPlugin {
|
||||
manifest: PluginManifest,
|
||||
source: String,
|
||||
/// The WebAssembly ABI the module implements (`ec_api_version`).
|
||||
abi: u32,
|
||||
engine: Engine,
|
||||
module: Module,
|
||||
/// Idle instances.
|
||||
@@ -63,6 +112,7 @@ pub struct WasmPlugin {
|
||||
}
|
||||
|
||||
impl WasmPlugin {
|
||||
/// Load `bytes` and its manifest; errors on imports, a bad manifest or a missing export.
|
||||
pub fn new(bytes: &[u8], source: &str) -> Result<WasmPlugin, String> {
|
||||
let mut config = Config::default();
|
||||
config.consume_fuel(true);
|
||||
@@ -71,51 +121,42 @@ impl WasmPlugin {
|
||||
if let Some(i) = module.imports().next() {
|
||||
return Err(format!("plug-ins can't import anything (the module imports `{}.{}`)", i.module(), i.name()));
|
||||
}
|
||||
let mut inst = instantiate(&engine, &module)?;
|
||||
let version = inst.func::<(), i32>("ec_api_version")?;
|
||||
let v = version.call(&mut inst.store, ()).map_err(err)?;
|
||||
if u32::try_from(v).ok() != Some(WASM_API_VERSION) {
|
||||
return Err(format!("the module implements plug-in API {v}, this build implements API {WASM_API_VERSION} for WebAssembly"));
|
||||
}
|
||||
let ptr = inst.func::<(), i32>("ec_manifest_ptr")?.call(&mut inst.store, ()).map_err(err)?;
|
||||
let len = inst.func::<(), i32>("ec_manifest_len")?.call(&mut inst.store, ()).map_err(err)?;
|
||||
let mut pending = begin(&engine, &module)?;
|
||||
let version = pending.func::<(), i32>("ec_api_version")?;
|
||||
let abi = abi_of(version.call(&mut pending.store, ()).map_err(err)?)?;
|
||||
let ptr = pending.func::<(), i32>("ec_manifest_ptr")?.call(&mut pending.store, ()).map_err(err)?;
|
||||
let len = pending.func::<(), i32>("ec_manifest_len")?.call(&mut pending.store, ()).map_err(err)?;
|
||||
if !(0..=MAX_MANIFEST).contains(&len) {
|
||||
return Err(format!("bad manifest length {len}"));
|
||||
}
|
||||
let mut text = vec![0u8; len as usize];
|
||||
inst.memory.read(&inst.store, ptr as u32 as usize, &mut text).map_err(|e| format!("manifest out of bounds: {e}"))?;
|
||||
pending.memory.read(&pending.store, ptr as u32 as usize, &mut text).map_err(|e| format!("manifest out of bounds: {e}"))?;
|
||||
let manifest: PluginManifest = serde_json::from_slice(&text).map_err(|e| format!("bad manifest JSON: {e}"))?;
|
||||
manifest.validate()?;
|
||||
if let Some(p) = manifest.params.iter().find(|p| p.kind.needs_v2()) {
|
||||
return Err(format!("parameter `{}`: WebAssembly plug-ins use plug-in API 1 parameter types (no point3, layer, text or hidden)", p.id));
|
||||
}
|
||||
Ok(WasmPlugin { manifest, source: source.to_string(), engine, module, pool: Mutex::new(vec![inst]) })
|
||||
let inst = pending.finish(abi)?;
|
||||
Ok(WasmPlugin { manifest, source: source.to_string(), abi, engine, module, pool: Mutex::new(vec![inst]) })
|
||||
}
|
||||
|
||||
/// An idle instance, or a fresh one when the pool is empty.
|
||||
fn take(&self) -> Result<Inst, String> {
|
||||
if let Some(i) = self.pool.lock().ok().and_then(|mut p| p.pop()) {
|
||||
return Ok(i);
|
||||
}
|
||||
instantiate(&self.engine, &self.module)
|
||||
instantiate(&self.engine, &self.module, self.abi)
|
||||
}
|
||||
|
||||
/// Return a working instance to the pool.
|
||||
fn give_back(&self, inst: Inst) {
|
||||
if let Ok(mut p) = self.pool.lock() {
|
||||
p.push(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectPlugin for WasmPlugin {
|
||||
fn manifest(&self) -> &PluginManifest {
|
||||
&self.manifest
|
||||
}
|
||||
|
||||
fn source(&self) -> String {
|
||||
self.source.clone()
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64) -> Result<(), String> {
|
||||
/// ABI 1: parameters, then the pixels; `ec_render`.
|
||||
fn render_v1(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64) -> Result<(), String> {
|
||||
let n = frame.pixels.len();
|
||||
let params_bytes = params.flat.len() * 8;
|
||||
let total = params_bytes + n * 16;
|
||||
@@ -130,25 +171,19 @@ impl EffectPlugin for WasmPlugin {
|
||||
for v in params.flat {
|
||||
buf.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
for px in frame.pixels.iter() {
|
||||
for c in px {
|
||||
buf.extend_from_slice(&c.to_le_bytes());
|
||||
}
|
||||
}
|
||||
push_pixels(&mut buf, frame.pixels);
|
||||
inst.memory.write(&mut inst.store, base, &buf).map_err(|e| format!("ec_alloc returned a buffer out of bounds: {e}"))?;
|
||||
let px_ptr = (base + params_bytes) as i32;
|
||||
let r = inst
|
||||
.render
|
||||
.call(&mut inst.store, (px_ptr, frame.width as i32, frame.height as i32, base as i32, params.flat.len() as i32, time, frame.scale))
|
||||
.map_err(|e| format!("ec_render trapped: {e}"));
|
||||
let r = match &inst.render {
|
||||
Renderer::V1(f) => f
|
||||
.call(&mut inst.store, (px_ptr, frame.width as i32, frame.height as i32, base as i32, params.flat.len() as i32, time, frame.scale))
|
||||
.map_err(|e| format!("ec_render trapped: {e}")),
|
||||
Renderer::V2(_) => return Err("the module implements WebAssembly ABI 1 but exports ec_render2".into()),
|
||||
};
|
||||
let r = match r {
|
||||
Ok(0) => {
|
||||
inst.memory.read(&inst.store, base + params_bytes, &mut buf[params_bytes..]).map_err(err)?;
|
||||
for (px, b) in frame.pixels.iter_mut().zip(buf[params_bytes..].as_chunks::<16>().0.iter()) {
|
||||
for (c, v) in px.iter_mut().zip(b.as_chunks::<4>().0.iter()) {
|
||||
*c = f32::from_le_bytes([v[0], v[1], v[2], v[3]]);
|
||||
}
|
||||
}
|
||||
read_pixels(&buf[params_bytes..], frame.pixels);
|
||||
Ok(())
|
||||
}
|
||||
Ok(code) => Err(format!("ec_render returned error {code}")),
|
||||
@@ -159,4 +194,170 @@ impl EffectPlugin for WasmPlugin {
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// ABI 2: parameters, the current frame, then the previous and next frames; `ec_render2`.
|
||||
/// The neighbour buffers are always allocated (zeroed when absent), so a module can read
|
||||
/// both without a null check; `flags` says which hold real frames.
|
||||
fn render_v2(
|
||||
&self,
|
||||
frame: &mut PluginFrame,
|
||||
params: &PluginParams,
|
||||
time: f64,
|
||||
prev: Option<(f64, &PluginImage)>,
|
||||
next: Option<(f64, &PluginImage)>,
|
||||
) -> Result<(), String> {
|
||||
let n = frame.pixels.len();
|
||||
let params_bytes = params.flat.len() * 8;
|
||||
let frame_bytes = n * 16;
|
||||
let total = params_bytes + frame_bytes * 3;
|
||||
if total > i32::MAX as usize {
|
||||
return Err("frame too large for a 32-bit plug-in".into());
|
||||
}
|
||||
let prev_px = prev.map(|(t, img)| (t, img.on_grid(frame.width, frame.height, frame.origin, frame.scale)));
|
||||
let next_px = next.map(|(t, img)| (t, img.on_grid(frame.width, frame.height, frame.origin, frame.scale)));
|
||||
let prev = prev_px.as_ref().filter(|(_, px)| px.len() == n);
|
||||
let next = next_px.as_ref().filter(|(_, px)| px.len() == n);
|
||||
let mut inst = self.take()?;
|
||||
inst.store.set_fuel(FUEL_BASE + FUEL_PER_PIXEL * (n as u64 * 3)).map_err(err)?;
|
||||
let base = inst.alloc.call(&mut inst.store, total as i32).map_err(err)? as u32 as usize;
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(total);
|
||||
for v in params.flat {
|
||||
buf.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
push_pixels(&mut buf, frame.pixels);
|
||||
push_neighbour(&mut buf, prev, frame_bytes);
|
||||
push_neighbour(&mut buf, next, frame_bytes);
|
||||
inst.memory.write(&mut inst.store, base, &buf).map_err(|e| format!("ec_alloc returned a buffer out of bounds: {e}"))?;
|
||||
let px_ptr = (base + params_bytes) as i32;
|
||||
let prev_ptr = (base + params_bytes + frame_bytes) as i32;
|
||||
let next_ptr = (base + params_bytes + frame_bytes * 2) as i32;
|
||||
let flags = i32::from(prev.is_some()) | (i32::from(next.is_some()) << 1);
|
||||
let prev_time = prev.map_or(0.0, |(t, _)| *t);
|
||||
let next_time = next.map_or(0.0, |(t, _)| *t);
|
||||
let r = match &inst.render {
|
||||
Renderer::V2(f) => f
|
||||
.call(
|
||||
&mut inst.store,
|
||||
(
|
||||
px_ptr,
|
||||
frame.width as i32,
|
||||
frame.height as i32,
|
||||
base as i32,
|
||||
params.flat.len() as i32,
|
||||
time,
|
||||
frame.scale,
|
||||
prev_ptr,
|
||||
prev_time,
|
||||
next_ptr,
|
||||
next_time,
|
||||
flags,
|
||||
),
|
||||
)
|
||||
.map_err(|e| format!("ec_render2 trapped: {e}")),
|
||||
Renderer::V1(_) => return Err("the module implements WebAssembly ABI 2 but exports ec_render".into()),
|
||||
};
|
||||
let r = match r {
|
||||
Ok(0) => {
|
||||
let mut out = vec![0u8; frame_bytes];
|
||||
inst.memory.read(&inst.store, base + params_bytes, &mut out).map_err(err)?;
|
||||
read_pixels(&out, frame.pixels);
|
||||
Ok(())
|
||||
}
|
||||
Ok(code) => Err(format!("ec_render2 returned error {code}")),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
if r.is_ok() {
|
||||
self.give_back(inst);
|
||||
}
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
/// Append pixels to the little-endian buffer the module reads.
|
||||
fn push_pixels(buf: &mut Vec<u8>, px: &[[f32; 4]]) {
|
||||
for p in px {
|
||||
for c in p {
|
||||
buf.extend_from_slice(&c.to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The neighbour's pixels, or `frame_bytes` zeroed bytes when there is no frame.
|
||||
fn push_neighbour(buf: &mut Vec<u8>, neighbour: Option<&(f64, Vec<[f32; 4]>)>, frame_bytes: usize) {
|
||||
match neighbour {
|
||||
Some((_, px)) => push_pixels(buf, px),
|
||||
None => buf.resize(buf.len() + frame_bytes, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the little-endian pixels the module wrote back.
|
||||
fn read_pixels(bytes: &[u8], pixels: &mut [[f32; 4]]) {
|
||||
for (px, b) in pixels.iter_mut().zip(bytes.as_chunks::<16>().0.iter()) {
|
||||
for (c, v) in px.iter_mut().zip(b.as_chunks::<4>().0.iter()) {
|
||||
*c = f32::from_le_bytes([v[0], v[1], v[2], v[3]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The layer times of the frames around `time` on a `fps` grid: the frame before and after it
|
||||
/// at a frame boundary, the two frames that bracket it between frames.
|
||||
fn neighbour_times(time: f64, fps: f64) -> (f64, f64) {
|
||||
if !fps.is_finite() || fps <= 0.0 {
|
||||
return (time - 1.0 / 30.0, time + 1.0 / 30.0);
|
||||
}
|
||||
let step = 1.0 / fps;
|
||||
let at = time * fps;
|
||||
// Frame times are not exact in floating point: at NTSC rates `time * fps` often lands a few
|
||||
// ulps above or below an integer. Treat a near-integer as a frame boundary, so the current
|
||||
// frame is never handed back as its own neighbour.
|
||||
if (at - at.round()).abs() < 1e-6 { (time - step, time + step) } else { (at.floor() / fps, at.ceil() / fps) }
|
||||
}
|
||||
|
||||
impl EffectPlugin for WasmPlugin {
|
||||
fn manifest(&self) -> &PluginManifest {
|
||||
&self.manifest
|
||||
}
|
||||
|
||||
fn source(&self) -> String {
|
||||
self.source.clone()
|
||||
}
|
||||
|
||||
fn reads_other_times(&self) -> bool {
|
||||
self.abi >= 2
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64) -> Result<(), String> {
|
||||
if self.abi == 1 { self.render_v1(frame, params, time) } else { self.render_v2(frame, params, time, None, None) }
|
||||
}
|
||||
|
||||
fn render_v2(&self, frame: &mut PluginFrame, params: &PluginParams, time: f64, host: &dyn PluginHost) -> Result<(), String> {
|
||||
if self.abi == 1 {
|
||||
return self.render_v1(frame, params, time);
|
||||
}
|
||||
let (prev_time, next_time) = neighbour_times(time, host.frame_rate());
|
||||
let prev = host.input_at(prev_time);
|
||||
let next = host.input_at(next_time);
|
||||
let prev = prev.as_ref().map(|img| (prev_time, img));
|
||||
let next = next.as_ref().map(|img| (next_time, img));
|
||||
self.render_v2(frame, params, time, prev, next)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::neighbour_times;
|
||||
|
||||
/// NTSC frame times do not multiply back exactly; every frame boundary must still get the
|
||||
/// frames either side of it, never the frame itself.
|
||||
#[test]
|
||||
fn frame_boundaries_get_their_neighbours_at_ntsc_rates() {
|
||||
let fps = 24000.0 / 1001.0;
|
||||
for n in 0..2400 {
|
||||
// 24000/1001 fps at 254 016 000 000 ticks/s, as `Tick` stores it.
|
||||
let time = (n * 10_594_584_000_i64) as f64 / 254_016_000_000.0;
|
||||
let (prev, next) = neighbour_times(time, fps);
|
||||
assert!((prev - (time - 1.0 / fps)).abs() < 1e-9, "frame {n}: prev {prev}, current {time}");
|
||||
assert!((next - (time + 1.0 / fps)).abs() < 1e-9, "frame {n}: next {next}, current {time}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,13 @@ pub struct Footage {
|
||||
/// made from the footage starts with them, and Update Markers From Source copies new ones.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub markers: Vec<Marker>,
|
||||
/// PDF / Illustrator footage (`.pdf`, `.ai`; not EPS) from an After Effects project: the page
|
||||
/// is framed as After Effects places it (the ArtBox, `/Rotate` ignored) and drawn to fill
|
||||
/// `width` × `height`, the size After Effects stored, which the layers' anchors and masks are
|
||||
/// measured against (#3099). Other footage (and every project saved before this field) shows
|
||||
/// the page as a PDF viewer does, at its own size.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub placed_page: bool,
|
||||
}
|
||||
|
||||
fn is_default<T: Default + PartialEq>(v: &T) -> bool {
|
||||
@@ -307,6 +314,7 @@ impl Default for Footage {
|
||||
start_timecode: None,
|
||||
file_timecode: None,
|
||||
markers: vec![],
|
||||
placed_page: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@ pub enum Backend {
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// The backend `s` names, whatever case it is written in: the project's own values (`cpu`,
|
||||
/// `gpu`, `auto`), the labels Project Settings ▸ Video Rendering and Effects draws
|
||||
/// (`GPU Acceleration`, `Software Rendering`) and the older After Effects wording.
|
||||
pub fn parse(s: &str) -> Option<Backend> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"cpu" | "software" | "mercury software only" => Some(Backend::Cpu),
|
||||
"gpu" | "mercury gpu acceleration" => Some(Backend::Gpu),
|
||||
"cpu" | "software" | "mercury software only" | "software rendering" => Some(Backend::Cpu),
|
||||
"gpu" | "mercury gpu acceleration" | "gpu acceleration" => Some(Backend::Gpu),
|
||||
"auto" => Some(Backend::Auto),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -694,7 +694,9 @@ fn reads_other_times(layer: &Layer, effects: Option<usize>) -> bool {
|
||||
fx.groups().take(effects.unwrap_or(usize::MAX)).any(|g| {
|
||||
g.enabled
|
||||
&& matches!(&g.kind, effectcraft_project::GroupKind::Effect { effect }
|
||||
if effect.starts_with("ec.time.") || effectcraft_effects::reads_param_history(effect))
|
||||
if effect.starts_with("ec.time.")
|
||||
|| effectcraft_effects::reads_param_history(effect)
|
||||
|| (!effect.starts_with("ec.") && effectcraft_effects::plugin::reads_other_times(effect)))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,8 +76,9 @@ pub trait FootageSource: Send + Sync {
|
||||
fn cache_budget(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
/// Vector footage (SVG) rasterised at `scale` × its pixel size, for Continuously Rasterize.
|
||||
/// `None` when the footage is not vector or cannot be read.
|
||||
/// Vector footage (SVG, PDF, Illustrator, EPS: [`is_vector_footage`]) rasterised at `scale` ×
|
||||
/// its size as [`vector_fit`] draws it, for Continuously Rasterize. `None` when the footage
|
||||
/// is not vector or cannot be read.
|
||||
fn vector_frame(&self, _item: ItemId, _footage: &Footage, _scale: f64) -> Option<Arc<Image>> {
|
||||
None
|
||||
}
|
||||
@@ -216,9 +217,31 @@ impl FootageSource for CancellableFootage<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Footage that can be rasterised at any scale (SVG).
|
||||
/// Footage that can be rasterised at any scale: SVG, PDF, Illustrator and EPS files, and a
|
||||
/// placed page of an After Effects project (its codec is not stored there).
|
||||
pub fn is_vector_footage(f: &Footage) -> bool {
|
||||
matches!(f.codec.as_str(), "SVG" | "PDF" | "AI" | "EPS")
|
||||
matches!(f.codec.as_str(), "SVG" | "PDF" | "AI" | "EPS") || f.placed_page
|
||||
}
|
||||
|
||||
/// How vector footage `f` is drawn at `scale` × its size: the image size, and the x and y scale
|
||||
/// from document pixels (`doc` = the document's width and height, `own` = its pixel size, the
|
||||
/// size rounded up) to image pixels.
|
||||
///
|
||||
/// Footage from an After Effects project ([`Footage::placed_page`]) fills its stored size, which
|
||||
/// can round or measure the page differently: the layer's anchor and masks are measured against
|
||||
/// that size, and the renderer resamples a reduced-resolution frame to it, so drawn at its own
|
||||
/// size the art was scaled by the width ratio alone at full resolution (off against the masks)
|
||||
/// and stretched to the stored aspect below 3/4 resolution (#3099). Other footage is drawn 1:1 at
|
||||
/// the document's own size, as it always was.
|
||||
pub fn vector_fit(f: &Footage, doc: [f64; 2], own: (u32, u32), scale: f64) -> ((u32, u32), [f64; 2]) {
|
||||
let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
|
||||
let side = |px: u32| (f64::from(px) * scale).ceil().clamp(1.0, f64::from(u32::MAX)) as u32;
|
||||
let size = (f.width, f.height);
|
||||
let fills = f.placed_page && size != own && size.0 > 0 && size.1 > 0 && doc.iter().all(|d| d.is_finite() && *d > 0.0);
|
||||
if !fills {
|
||||
return ((side(own.0), side(own.1)), [scale, scale]);
|
||||
}
|
||||
((side(size.0), side(size.1)), [scale * f64::from(size.0) / doc[0], scale * f64::from(size.1) / doc[1]])
|
||||
}
|
||||
|
||||
/// No footage available (renders footage layers as transparent).
|
||||
|
||||
@@ -47,7 +47,7 @@ mod opts;
|
||||
mod pipeline;
|
||||
|
||||
pub use accel::{Accelerator, Backend, FrameMemory, FxStep, FxTarget};
|
||||
pub use footage::{CancellableFootage, FloatCurve, FootageSource, NoFootage, is_vector_footage, missing_item_problem};
|
||||
pub use footage::{CancellableFootage, FloatCurve, FootageSource, NoFootage, is_vector_footage, missing_item_problem, vector_fit};
|
||||
pub use frames::{
|
||||
adaptive_samples, blend_frames, cineon_frame, content_bounds, extended_region, field_frame, frame_position, frame_size, interpret_pixels, kurbo_path,
|
||||
render_frame, roi_inside,
|
||||
|
||||
+32
-11
@@ -329,7 +329,7 @@ fn card_dance_comp_camera_matches_the_default_view() {
|
||||
assert!(img.get(10, 50)[3] < 0.01);
|
||||
}
|
||||
|
||||
/// A black 2D solid with CC Particle World, under a camera layer panned right by `pan` px.
|
||||
/// A black 2D solid with Particle Emitter 3D, under a camera layer panned right by `pan` px.
|
||||
fn particle_world_scene(pan: f64) -> (Project, ItemId) {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut l = solid(&mut p, &comp, [0.0, 0.0, 0.0], 200, 100);
|
||||
@@ -359,7 +359,7 @@ fn particles_x(img: &crate::Image) -> f32 {
|
||||
sx / m
|
||||
}
|
||||
|
||||
/// #397: CC Particle World on a 2D layer is seen through the comp's camera layer, and the layer
|
||||
/// #397: Particle Emitter 3D on a 2D layer is seen through the comp's camera layer, and the layer
|
||||
/// cache notices the camera moving.
|
||||
#[test]
|
||||
fn particle_world_follows_the_comp_camera() {
|
||||
@@ -374,7 +374,7 @@ fn particle_world_follows_the_comp_camera() {
|
||||
assert!(b < a - 20.0, "panning the camera right moves the particles left: {a} -> {b}");
|
||||
}
|
||||
|
||||
/// A black solid with CC Particle World whose Birth Rate is 3 until layer time `cut`, then 0 a
|
||||
/// A black solid with Particle Emitter 3D whose Birth Rate is 3 until layer time `cut`, then 0 a
|
||||
/// frame later (`None`: 3 throughout); the layer starts at comp time `start`, stretched `stretch` %.
|
||||
fn burst_scene(cut: Option<f64>, start: f64, stretch: f64) -> (Project, ItemId) {
|
||||
burst_scene_keys(cut, start, stretch, false)
|
||||
@@ -624,7 +624,7 @@ fn a_text_expression_selector_is_not_served_from_the_disk_cache() {
|
||||
|
||||
/// A Birth Rate expression reading another layer: changing that layer changes the history but
|
||||
/// not the current rate (0) nor this layer's properties, which is all the layer cache key sees.
|
||||
/// CC Particle World layers with expressions therefore aren't layer cached.
|
||||
/// Particle Emitter 3D layers with expressions therefore aren't layer cached.
|
||||
#[test]
|
||||
fn particle_world_with_a_birth_rate_expression_is_not_served_from_the_layer_cache() {
|
||||
let scene = |opacity: f64| {
|
||||
@@ -657,8 +657,8 @@ fn particle_world_with_a_birth_rate_expression_is_not_served_from_the_layer_cach
|
||||
assert!(light(&b) > 1.0 && light(&b) < light(&a), "fewer births: {} vs {}", light(&b), light(&a));
|
||||
}
|
||||
|
||||
/// A grey solid (CC Mr. Mercury shades its blobs from it) with effect `id` (CC Particle Systems
|
||||
/// II, CC Mr. Mercury): Birth Rate `rate`,
|
||||
/// A grey solid (Liquid Particles shades its blobs from it) with effect `id` (CC Particle Systems
|
||||
/// II, Liquid Particles): Birth Rate `rate`,
|
||||
/// or keyed 3 until 0.5 s and 0 a frame later (`None`); Longevity 1 s.
|
||||
fn system_scene(id: &str, rate: Option<f64>) -> (Project, ItemId) {
|
||||
let (mut p, cid, comp) = setup();
|
||||
@@ -677,7 +677,7 @@ fn system_scene(id: &str, rate: Option<f64>) -> (Project, ItemId) {
|
||||
(p, cid)
|
||||
}
|
||||
|
||||
/// #1373 for CC Particle Systems II and CC Mr. Mercury through the renderer: a keyed burst
|
||||
/// #1373 for Particle Emitter 2D and Liquid Particles through the renderer: a keyed burst
|
||||
/// draws as the constant rate during it, still draws after the rate drops to 0 (unlike a rate
|
||||
/// of 0 throughout), and draws as nothing born once Longevity has run out.
|
||||
#[test]
|
||||
@@ -1057,6 +1057,27 @@ fn comp_frame_ends_the_frame_when_the_render_panics() {
|
||||
assert_eq!(a.ends.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
/// The renderer labels Project Settings ▸ Video Rendering and Effects draws parse back as the
|
||||
/// backend they name (in any case), and the older spellings still work.
|
||||
#[test]
|
||||
fn renderer_labels_parse_back_and_keep_the_older_spellings() {
|
||||
use crate::Backend;
|
||||
for (label, backend) in [
|
||||
("GPU Acceleration", Backend::Gpu),
|
||||
("gpu acceleration", Backend::Gpu),
|
||||
("Mercury GPU Acceleration", Backend::Gpu),
|
||||
("gpu", Backend::Gpu),
|
||||
("Software Rendering", Backend::Cpu),
|
||||
("software rendering", Backend::Cpu),
|
||||
("Mercury Software Only", Backend::Cpu),
|
||||
("cpu", Backend::Cpu),
|
||||
("Auto", Backend::Auto),
|
||||
] {
|
||||
assert_eq!(Backend::parse(label), Some(backend), "{label}");
|
||||
}
|
||||
assert_eq!(Backend::parse("Vulkan"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_backend_sends_2d_and_3d_comps_to_the_gpu() {
|
||||
let (mut p, cid, comp) = setup();
|
||||
@@ -1223,11 +1244,11 @@ fn effects_on_text_reach_past_the_text_bounds() {
|
||||
}
|
||||
assert!(
|
||||
painted > 50 && hi.saturating_sub(lo) > 100,
|
||||
"CC Tiler tiles the glyphs across the layer region, not just its text bounds: {painted} painted, x-span {lo}..{hi}"
|
||||
"Scaled Tiles tiles the glyphs across the layer region, not just its text bounds: {painted} painted, x-span {lo}..{hi}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #658: CC Smear drags text glyphs beyond the text layer's own bounds. Pixels smeared past
|
||||
/// #658: Directional Smear drags text glyphs beyond the text layer's own bounds. Pixels smeared past
|
||||
/// the painted rectangle used to fall off its edge, so the liquid trail never appeared.
|
||||
#[test]
|
||||
fn cc_smear_trails_text_past_its_bounds() {
|
||||
@@ -1246,7 +1267,7 @@ fn cc_smear_trails_text_past_its_bounds() {
|
||||
);
|
||||
p.comp_mut(cid).unwrap().layers = vec![l].into();
|
||||
let smeared = right(&render_frame(&p, cid, Tick::ZERO, 1.0));
|
||||
assert!(plain > 40 && smeared > plain + 25, "CC Smear trails the glyphs past the text bounds: right edge {plain} → {smeared}");
|
||||
assert!(plain > 40 && smeared > plain + 25, "Directional Smear trails the glyphs past the text bounds: right edge {plain} → {smeared}");
|
||||
}
|
||||
|
||||
/// A layer parameter pointing at a shape layer sees all of it (in its effect space), and the
|
||||
@@ -1502,7 +1523,7 @@ fn solo_excludes_other_layers_outside_the_soloed_layers_range() {
|
||||
assert_eq!(second.get(100, 50), [0.0, 0.0, 1.0, 1.0], "only B inside its range");
|
||||
}
|
||||
|
||||
/// #1252: a layer blended with a CC Particle World layer, under a camera panned by `pan` px.
|
||||
/// #1252: a layer blended with a Particle Emitter 3D layer, under a camera panned by `pan` px.
|
||||
fn scene_reader_dependency(pan: f64) -> (Project, ItemId) {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut source = solid(&mut p, &comp, [0.0, 0.0, 0.0], 200, 100);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! Time effects through the renderer: neighbouring frames via `EffectHost::self_at`, and the
|
||||
//! layer cache staying correct while scrubbing.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_effects::plugin::{EffectPlugin, PluginFrame, PluginManifest, PluginParams, register_plugin};
|
||||
use effectcraft_keyframe::{Keyframe, ShapePath, Value};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, MaskMode, Project, Solid};
|
||||
@@ -157,3 +160,89 @@ fn input_keys_ignore_later_effects() {
|
||||
set(&mut p, "effects/#1/blurriness", 8.0);
|
||||
assert_ne!(key(&p).0, one);
|
||||
}
|
||||
|
||||
/// A plug-in that reads the layer's neighbouring frames (WebAssembly ABI 2) makes its layer's
|
||||
/// cache key depend on the layer time, even when the source and every property are static. A
|
||||
/// plug-in that does not keeps the old key (#1977: a static layer stays cached across a scrub).
|
||||
#[test]
|
||||
fn abi2_plugin_layers_key_on_layer_time() {
|
||||
struct NeighbourReader {
|
||||
m: PluginManifest,
|
||||
}
|
||||
|
||||
impl EffectPlugin for NeighbourReader {
|
||||
fn manifest(&self) -> &PluginManifest {
|
||||
&self.m
|
||||
}
|
||||
|
||||
fn render(&self, _frame: &mut PluginFrame, _params: &PluginParams, _time: f64) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reads_other_times(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
struct Plain {
|
||||
m: PluginManifest,
|
||||
}
|
||||
|
||||
impl EffectPlugin for Plain {
|
||||
fn manifest(&self) -> &PluginManifest {
|
||||
&self.m
|
||||
}
|
||||
|
||||
fn render(&self, _frame: &mut PluginFrame, _params: &PluginParams, _time: f64) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A static white solid with one plug-in effect, registered by `plugin`.
|
||||
fn plugin_scene(plugin: Arc<dyn EffectPlugin>) -> (Project, ItemId) {
|
||||
let mut p = Project::default();
|
||||
let comp = Comp::new(200, 100, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
||||
let sid = p.add_item("Solid", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 200, height: 100, pixel_aspect: 1.0 }));
|
||||
let mut l = build::layer(&mut p, &comp, "Solid", LayerSource::Solid { item: sid }, (200, 100), None);
|
||||
let spec = register_plugin(plugin).unwrap();
|
||||
let mut next = p.next_id;
|
||||
let g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), spec.name, [200.0, 100.0]);
|
||||
p.next_id = next;
|
||||
l.props.sub_mut("effects").unwrap().children.push(g.into());
|
||||
p.comp_mut(cid).unwrap().layers.push(l);
|
||||
(p, cid)
|
||||
}
|
||||
|
||||
fn key(p: &Project, cid: ItemId, t: f64) -> Option<u64> {
|
||||
let comp = p.comp(cid).unwrap();
|
||||
let ctx = crate::EvalCtx {
|
||||
project: p,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: Tick::from_seconds_f64(t),
|
||||
expr: None,
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
reads: None,
|
||||
};
|
||||
crate::cache::layer_key(&ctx, &comp.layers[0], 1.0, false, false)
|
||||
}
|
||||
|
||||
let manifest = |id: &str| PluginManifest {
|
||||
api: 2,
|
||||
id: id.into(),
|
||||
name: "Key".into(),
|
||||
category: "Test Plug-ins".into(),
|
||||
version: "1".into(),
|
||||
author: String::new(),
|
||||
description: String::new(),
|
||||
params: vec![],
|
||||
};
|
||||
|
||||
let (plain, c1) = plugin_scene(Arc::new(Plain { m: manifest("org.test.render.time-key.plain") }));
|
||||
assert_eq!(key(&plain, c1, 0.25), key(&plain, c1, 0.5), "a plain plug-in keeps the old key");
|
||||
let (reader, c2) = plugin_scene(Arc::new(NeighbourReader { m: manifest("org.test.render.time-key.reader") }));
|
||||
assert_ne!(key(&reader, c2, 0.25), key(&reader, c2, 0.5), "an ABI 2 plug-in keys on the layer time");
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ impl PanelKind {
|
||||
PanelKind::EssentialGraphics => "Essential Graphics",
|
||||
PanelKind::ScriptPanel(_) => "ScriptUI Panel",
|
||||
PanelKind::Viewer(_) => "Composition",
|
||||
PanelKind::LumetriScopes => "Lumetri Scopes",
|
||||
PanelKind::LumetriScopes => "Video Scopes",
|
||||
PanelKind::Footage => "Footage",
|
||||
PanelKind::MediaBrowser => "Media Browser",
|
||||
PanelKind::Metadata => "Metadata",
|
||||
@@ -709,6 +709,16 @@ mod tests {
|
||||
w.apply(action, permissions).unwrap();
|
||||
}
|
||||
|
||||
/// Only the title changes: existing saved workspace layouts still deserialize.
|
||||
#[test]
|
||||
fn scopes_label_is_descriptive_and_its_saved_identity_stays_compatible() {
|
||||
let panel: PanelKind = serde_json::from_str("\"LumetriScopes\"").unwrap();
|
||||
assert_eq!(panel, LumetriScopes);
|
||||
assert_eq!(panel.title(), "Video Scopes");
|
||||
assert_eq!(panel.id(), "LumetriScopes");
|
||||
assert_eq!(serde_json::to_string(&panel).unwrap(), "\"LumetriScopes\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn after_effects_workspaces_exist_with_their_core_panels() {
|
||||
for name in WORKSPACES {
|
||||
|
||||
+28
-13
@@ -1054,7 +1054,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "基本图形"),
|
||||
("window.panel", "Info", "信息"),
|
||||
("help.inAppTutorials", "Learn", "学习"),
|
||||
("window.panel", "Lumetri Scopes", "Lumetri 范围"),
|
||||
("window.panel", "Video Scopes", "视频示波器"),
|
||||
("window.panel", "Mask Interpolation", "蒙版插值"),
|
||||
("window.panel", "Media Browser", "媒体浏览器"),
|
||||
("window.panel", "Metadata", "元数据"),
|
||||
@@ -1690,7 +1690,7 @@ const SWEDISH: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Grundläggande grafik"),
|
||||
("window.panel", "Info", "Info"),
|
||||
("help.inAppTutorials", "Learn", "Lär dig"),
|
||||
("window.panel", "Lumetri Scopes", "Lumetri-scope"),
|
||||
("window.panel", "Video Scopes", "Videoskop"),
|
||||
("window.panel", "Mask Interpolation", "Maskinterpolering"),
|
||||
("window.panel", "Media Browser", "Mediebläddrare"),
|
||||
("window.panel", "Metadata", "Metadata"),
|
||||
@@ -2336,7 +2336,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "基本圖形"),
|
||||
("window.panel", "Info", "資訊"),
|
||||
("help.inAppTutorials", "Learn", "學習"),
|
||||
("window.panel", "Lumetri Scopes", "Lumetri 示波器"),
|
||||
("window.panel", "Video Scopes", "視訊示波器"),
|
||||
("window.panel", "Mask Interpolation", "遮色片插補"),
|
||||
("window.panel", "Media Browser", "媒體瀏覽器"),
|
||||
("window.panel", "Metadata", "中繼資料"),
|
||||
@@ -2975,7 +2975,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "エッセンシャルグラフィックス"),
|
||||
("window.panel", "Info", "情報"),
|
||||
("help.inAppTutorials", "Learn", "学習"),
|
||||
("window.panel", "Lumetri Scopes", "Lumetriスコープ"),
|
||||
("window.panel", "Video Scopes", "ビデオスコープ"),
|
||||
("window.panel", "Mask Interpolation", "マスク補間"),
|
||||
("window.panel", "Media Browser", "メディアブラウザー"),
|
||||
("window.panel", "Metadata", "メタデータ"),
|
||||
@@ -3617,7 +3617,7 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Основна графіка"),
|
||||
("window.panel", "Info", "Інформація"),
|
||||
("help.inAppTutorials", "Learn", "Навчання"),
|
||||
("window.panel", "Lumetri Scopes", "Вимірювачі Lumetri"),
|
||||
("window.panel", "Video Scopes", "Відеоскопи"),
|
||||
("window.panel", "Mask Interpolation", "Інтерполяція маски"),
|
||||
("window.panel", "Media Browser", "Браузер медіа"),
|
||||
("window.panel", "Metadata", "Метадані"),
|
||||
@@ -4260,7 +4260,7 @@ const SPANISH: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Gráficos esenciales"),
|
||||
("window.panel", "Info", "Información"),
|
||||
("help.inAppTutorials", "Learn", "Aprender"),
|
||||
("window.panel", "Lumetri Scopes", "Ámbitos de Lumetri"),
|
||||
("window.panel", "Video Scopes", "Monitores de vídeo"),
|
||||
("window.panel", "Mask Interpolation", "Interpolación de máscara"),
|
||||
("window.panel", "Media Browser", "Navegador de medios"),
|
||||
("window.panel", "Metadata", "Metadatos"),
|
||||
@@ -4901,7 +4901,7 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Gráficos essenciais"),
|
||||
("window.panel", "Info", "Informações"),
|
||||
("help.inAppTutorials", "Learn", "Aprender"),
|
||||
("window.panel", "Lumetri Scopes", "Escopos Lumetri"),
|
||||
("window.panel", "Video Scopes", "Escopos de vídeo"),
|
||||
("window.panel", "Mask Interpolation", "Interpolação de máscara"),
|
||||
("window.panel", "Media Browser", "Navegador de mídia"),
|
||||
("window.panel", "Metadata", "Metadados"),
|
||||
@@ -5544,7 +5544,7 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Основная графика"),
|
||||
("window.panel", "Info", "Информация"),
|
||||
("help.inAppTutorials", "Learn", "Обучение"),
|
||||
("window.panel", "Lumetri Scopes", "Измерители Lumetri"),
|
||||
("window.panel", "Video Scopes", "Видеоскопы"),
|
||||
("window.panel", "Mask Interpolation", "Интерполяция маски"),
|
||||
("window.panel", "Media Browser", "Обозреватель медиа"),
|
||||
("window.panel", "Metadata", "Метаданные"),
|
||||
@@ -6185,7 +6185,7 @@ const FRENCH: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Graphiques essentiels"),
|
||||
("window.panel", "Info", "Informations"),
|
||||
("help.inAppTutorials", "Learn", "Apprentissage"),
|
||||
("window.panel", "Lumetri Scopes", "Oscilloscopes Lumetri"),
|
||||
("window.panel", "Video Scopes", "Oscilloscopes vidéo"),
|
||||
("window.panel", "Mask Interpolation", "Interpolation de masque"),
|
||||
("window.panel", "Media Browser", "Navigateur multimédia"),
|
||||
("window.panel", "Metadata", "Métadonnées"),
|
||||
@@ -6826,7 +6826,7 @@ const GERMAN: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Essential-Grafik"),
|
||||
("window.panel", "Info", "Info"),
|
||||
("help.inAppTutorials", "Learn", "Lernen"),
|
||||
("window.panel", "Lumetri Scopes", "Lumetri-Scopes"),
|
||||
("window.panel", "Video Scopes", "Videoskope"),
|
||||
("window.panel", "Mask Interpolation", "Maskeninterpolation"),
|
||||
("window.panel", "Media Browser", "Medienbrowser"),
|
||||
("window.panel", "Metadata", "Metadaten"),
|
||||
@@ -7464,7 +7464,7 @@ const POLISH: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Essential Graphics"),
|
||||
("window.panel", "Info", "Informacje"),
|
||||
("help.inAppTutorials", "Learn", "Nauka"),
|
||||
("window.panel", "Lumetri Scopes", "Zakresy Lumetri"),
|
||||
("window.panel", "Video Scopes", "Wskaźniki wideo"),
|
||||
("window.panel", "Mask Interpolation", "Interpolacja maski"),
|
||||
("window.panel", "Media Browser", "Przeglądarka multimediów"),
|
||||
("window.panel", "Metadata", "Metadane"),
|
||||
@@ -8106,7 +8106,7 @@ const CZECH: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "Základní grafika"),
|
||||
("window.panel", "Info", "Informace"),
|
||||
("help.inAppTutorials", "Learn", "Výuka"),
|
||||
("window.panel", "Lumetri Scopes", "Měřicí přístroje Lumetri"),
|
||||
("window.panel", "Video Scopes", "Videoskopy"),
|
||||
("window.panel", "Mask Interpolation", "Interpolace masek"),
|
||||
("window.panel", "Media Browser", "Prohlížeč médií"),
|
||||
("window.panel", "Metadata", "Metadata"),
|
||||
@@ -8748,7 +8748,7 @@ const GEORGIAN: &[(&str, &str, &str)] = &[
|
||||
("window.panel", "Essential Graphics", "ძირითადი გრაფიკა"),
|
||||
("window.panel", "Info", "ინფორმაცია"),
|
||||
("help.inAppTutorials", "Learn", "სწავლა"),
|
||||
("window.panel", "Lumetri Scopes", "Lumetri Scopes"),
|
||||
("window.panel", "Video Scopes", "ვიდეოსკოპები"),
|
||||
("window.panel", "Mask Interpolation", "ნიღბის ინტერპოლაცია"),
|
||||
("window.panel", "Media Browser", "მედიის ბრაუზერი"),
|
||||
("window.panel", "Metadata", "მეტამონაცემები"),
|
||||
@@ -8799,6 +8799,21 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[test]
|
||||
fn renamed_menu_translations_do_not_reintroduce_vendor_names() {
|
||||
for code in CATALOG_CODES {
|
||||
set_current(code);
|
||||
for (id, source) in [("window.panel", "Video Scopes")] {
|
||||
let rendered = command_label(id, source);
|
||||
let label = rendered.to_lowercase();
|
||||
for mark in ["lumetri", "mercury"] {
|
||||
assert!(!label.contains(mark), "{code}: {id} -> {rendered}");
|
||||
}
|
||||
}
|
||||
}
|
||||
set_current("en");
|
||||
}
|
||||
|
||||
/// The four options at the foot of the header's workspace menu say what After Effects says,
|
||||
/// read from its workspace button on 2026-10-31: reset, save over this one, save a copy, then
|
||||
/// edit the list.
|
||||
|
||||
+531
-478
File diff suppressed because it is too large
Load Diff
@@ -708,8 +708,8 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "Photoshopファイルを読み込み"),
|
||||
("Interpret Unlabeled Alpha As", "ラベルのないアルファの解釈"),
|
||||
("Join the ArtCraft Discord", "ArtCraftのDiscordに参加"),
|
||||
("Mercury GPU Acceleration ({})", "Mercury GPUアクセラレーション({})"),
|
||||
("Mercury Software Only", "Mercuryソフトウェアのみ"),
|
||||
("GPU Acceleration ({})", "GPUアクセラレーション({})"),
|
||||
("Software Rendering", "ソフトウェアレンダリング"),
|
||||
("OCIO (built-in ACES config)", "OCIO(組み込みACES設定)"),
|
||||
("Off (Final Quality)", "オフ(最終品質)"),
|
||||
("OpenGL (compatibility)", "OpenGL(互換)"),
|
||||
@@ -1119,7 +1119,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Script Console", "スクリプトコンソール"),
|
||||
("Essential Graphics", "エッセンシャルグラフィックス"),
|
||||
("ScriptUI Panel", "ScriptUIパネル"),
|
||||
("Lumetri Scopes", "Lumetriスコープ"),
|
||||
("Video Scopes", "ビデオスコープ"),
|
||||
("Media Browser", "メディアブラウザー"),
|
||||
("Content-Aware Fill", "コンテンツに応じた塗りつぶし"),
|
||||
("Create Nulls From Paths", "パスからヌルを作成"),
|
||||
@@ -1319,6 +1319,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("per second", "毎秒"),
|
||||
("Waveform", "波形"),
|
||||
("steps", "ステップ"),
|
||||
("GPU Acceleration (no GPU: software)", "GPUアクセラレーション(GPUなし:ソフトウェア)"),
|
||||
];
|
||||
|
||||
const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
@@ -1838,8 +1839,8 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "导入 Photoshop 文件"),
|
||||
("Interpret Unlabeled Alpha As", "将未标记的 Alpha 解释为"),
|
||||
("Join the ArtCraft Discord", "加入 ArtCraft Discord"),
|
||||
("Mercury GPU Acceleration ({})", "Mercury GPU 加速({})"),
|
||||
("Mercury Software Only", "仅 Mercury 软件"),
|
||||
("GPU Acceleration ({})", "GPU 加速({})"),
|
||||
("Software Rendering", "软件渲染"),
|
||||
("OCIO (built-in ACES config)", "OCIO(内置 ACES 配置)"),
|
||||
("Off (Final Quality)", "关闭(最终质量)"),
|
||||
("OpenGL (compatibility)", "OpenGL(兼容)"),
|
||||
@@ -2282,7 +2283,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Script Console", "脚本控制台"),
|
||||
("Essential Graphics", "基本图形"),
|
||||
("ScriptUI Panel", "ScriptUI 面板"),
|
||||
("Lumetri Scopes", "Lumetri 示波器"),
|
||||
("Video Scopes", "视频示波器"),
|
||||
("Media Browser", "媒体浏览器"),
|
||||
("Content-Aware Fill", "内容识别填充"),
|
||||
("Create Nulls From Paths", "从路径创建空对象"),
|
||||
@@ -2435,6 +2436,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Blend Mode", "混合模式"),
|
||||
("Show mesh", "显示网格"),
|
||||
("Record Options...", "记录选项..."),
|
||||
("GPU Acceleration (no GPU: software)", "GPU 加速(无 GPU:软件)"),
|
||||
];
|
||||
|
||||
/// Swedish (`sv`), written from the English source.
|
||||
@@ -2952,8 +2954,8 @@ const SWEDISH: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "Importera Photoshop-fil"),
|
||||
("Interpret Unlabeled Alpha As", "Tolka omärkt alfa som"),
|
||||
("Join the ArtCraft Discord", "Gå med i ArtCraft på Discord"),
|
||||
("Mercury GPU Acceleration ({})", "Mercury GPU-acceleration ({})"),
|
||||
("Mercury Software Only", "Endast Mercury-programvara"),
|
||||
("GPU Acceleration ({})", "GPU-acceleration ({})"),
|
||||
("Software Rendering", "Programvarurendering"),
|
||||
("OCIO (built-in ACES config)", "OCIO (inbyggd ACES-konfiguration)"),
|
||||
("Off (Final Quality)", "Av (slutlig kvalitet)"),
|
||||
("OpenGL (compatibility)", "OpenGL (kompatibilitet)"),
|
||||
@@ -3194,7 +3196,7 @@ const SWEDISH: &[(&str, &str)] = &[
|
||||
("Drop {} files to import", "Släpp {} filer för att importera"),
|
||||
("Effects", "Effekter"),
|
||||
("Label of {}: {}", "Etikett för {}: {}"),
|
||||
("Mercury GPU Acceleration (no GPU: software)", "Mercury GPU-acceleration (ingen GPU: programvara)"),
|
||||
("GPU Acceleration (no GPU: software)", "GPU-acceleration (ingen GPU: programvara)"),
|
||||
("Only one project can be opened per drop; skipped {} more", "Bara ett projekt kan öppnas per släpp; hoppade över {} till"),
|
||||
("Open in Essential Graphics", "Öppna i Grundläggande grafik"),
|
||||
("Original color: click to go back to it", "Originalfärg: klicka för att återgå till den"),
|
||||
@@ -3443,7 +3445,7 @@ const SWEDISH: &[(&str, &str)] = &[
|
||||
("Script Console", "Skriptkonsol"),
|
||||
("Essential Graphics", "Grundläggande grafik"),
|
||||
("ScriptUI Panel", "ScriptUI-panel"),
|
||||
("Lumetri Scopes", "Lumetri-scope"),
|
||||
("Video Scopes", "Videoskop"),
|
||||
("Media Browser", "Mediebläddrare"),
|
||||
("Content-Aware Fill", "Innehållsmedveten fyllning"),
|
||||
("Create Nulls From Paths", "Skapa nullobjekt från banor"),
|
||||
@@ -4612,7 +4614,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Script Console", "指令碼主控台"),
|
||||
("Essential Graphics", "基本圖形"),
|
||||
("ScriptUI Panel", "ScriptUI 面板"),
|
||||
("Lumetri Scopes", "Lumetri 示波器"),
|
||||
("Video Scopes", "視訊示波器"),
|
||||
("Media Browser", "媒體瀏覽器"),
|
||||
("Content-Aware Fill", "內容感知填色"),
|
||||
("Create Nulls From Paths", "從路徑建立空物件"),
|
||||
@@ -4765,6 +4767,9 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Blend Mode", "混合模式"),
|
||||
("Show mesh", "顯示網格"),
|
||||
("Record Options...", "記錄選項..."),
|
||||
("Software Rendering", "軟體算繪"),
|
||||
("GPU Acceleration ({})", "GPU 加速({})"),
|
||||
("GPU Acceleration (no GPU: software)", "GPU 加速(無 GPU:軟體)"),
|
||||
];
|
||||
|
||||
/// Ukrainian (uk): original translations of the same interface rows as the Chinese catalogs.
|
||||
@@ -5305,8 +5310,8 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "Імпорт файлу Photoshop"),
|
||||
("Interpret Unlabeled Alpha As", "Інтерпретувати непозначену альфу як"),
|
||||
("Join the ArtCraft Discord", "Приєднатися до Discord ArtCraft"),
|
||||
("Mercury GPU Acceleration ({})", "Прискорення Mercury GPU ({})"),
|
||||
("Mercury Software Only", "Лише програмний Mercury"),
|
||||
("GPU Acceleration ({})", "Прискорення GPU ({})"),
|
||||
("Software Rendering", "Програмне відтворення"),
|
||||
("OCIO (built-in ACES config)", "OCIO (вбудована конфігурація ACES)"),
|
||||
("Off (Final Quality)", "Вимкнено (остаточна якість)"),
|
||||
("OpenGL (compatibility)", "OpenGL (сумісність)"),
|
||||
@@ -5352,6 +5357,8 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("Project", "Проєкт"),
|
||||
("Total Offset", "Загальний зсув"),
|
||||
("Per Layer", "На шар"),
|
||||
("Video Scopes", "Відеоскопи"),
|
||||
("GPU Acceleration (no GPU: software)", "Прискорення GPU (без GPU: програмне)"),
|
||||
];
|
||||
|
||||
// Brazilian Portuguese.
|
||||
@@ -5984,7 +5991,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Guess", "Adivinhar"),
|
||||
("Import Image Sequence", "Importar sequência de imagens"),
|
||||
("Import PDF / Illustrator File", "Importar arquivo PDF / Illustrator"),
|
||||
("Mercury Software Only", "Apenas software Mercury"),
|
||||
("Software Rendering", "Renderização por software"),
|
||||
("OCIO (built-in ACES config)", "OCIO (config do ACES integrado)"),
|
||||
("Still Duration", "Duração da imagem fixa"),
|
||||
("Straight", "Direto"),
|
||||
@@ -6091,7 +6098,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Light", "Claro"),
|
||||
("Lines", "Linhas"),
|
||||
("Memory & CPU", "Memória e CPU"),
|
||||
("Mercury GPU Acceleration ({})", "Aceleração de GPU Mercury ({})"),
|
||||
("GPU Acceleration ({})", "Aceleração de GPU ({})"),
|
||||
("Off", "Desativado"),
|
||||
("Options", "Opções"),
|
||||
("Overlay", "Sobrepor"),
|
||||
@@ -6116,6 +6123,8 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Video", "Vídeo"),
|
||||
("channel", "canal"),
|
||||
("files", "arquivos"),
|
||||
("Video Scopes", "Escopos de vídeo"),
|
||||
("GPU Acceleration (no GPU: software)", "Aceleração de GPU (sem GPU: software)"),
|
||||
];
|
||||
|
||||
/// Polish (pl): original translations covering every row in the Chinese panel catalogs.
|
||||
@@ -6634,8 +6643,8 @@ const POLISH: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "Importuj plik Photoshopa"),
|
||||
("Interpret Unlabeled Alpha As", "Interpretuj nieoznaczoną alfę jako"),
|
||||
("Join the ArtCraft Discord", "Dołącz do Discorda ArtCraft"),
|
||||
("Mercury GPU Acceleration ({})", "Akceleracja Mercury GPU ({})"),
|
||||
("Mercury Software Only", "Tylko programowy Mercury"),
|
||||
("GPU Acceleration ({})", "Akceleracja GPU ({})"),
|
||||
("Software Rendering", "Renderowanie programowe"),
|
||||
("OCIO (built-in ACES config)", "OCIO (wbudowana konfiguracja ACES)"),
|
||||
("Off (Final Quality)", "Wył. (jakość końcowa)"),
|
||||
("OpenGL (compatibility)", "OpenGL (zgodność)"),
|
||||
@@ -6873,6 +6882,8 @@ const POLISH: &[(&str, &str)] = &[
|
||||
("Resolution:", "Rozdzielczość:"),
|
||||
("Guide Layer", "Warstwa pomocnicza"),
|
||||
("Layer Styles", "Style warstwy"),
|
||||
("Video Scopes", "Wskaźniki wideo"),
|
||||
("GPU Acceleration (no GPU: software)", "Akceleracja GPU (bez GPU: programowa)"),
|
||||
];
|
||||
|
||||
/// Czech (cs): original translations covering every row in the Chinese panel catalogs.
|
||||
@@ -7385,8 +7396,8 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "Importovat soubor Photoshop"),
|
||||
("Interpret Unlabeled Alpha As", "Interpretovat neoznačenou alfu jako"),
|
||||
("Join the ArtCraft Discord", "Připojit se na Discord ArtCraft"),
|
||||
("Mercury GPU Acceleration ({})", "GPU akcelerace Mercury ({})"),
|
||||
("Mercury Software Only", "Pouze softwarové zpracování Mercury"),
|
||||
("GPU Acceleration ({})", "Akcelerace GPU ({})"),
|
||||
("Software Rendering", "Softwarové vykreslování"),
|
||||
("OCIO (built-in ACES config)", "OCIO (vestavěná konfigurace ACES)"),
|
||||
("Off (Final Quality)", "Vypnuto (finální kvalita)"),
|
||||
("OpenGL (compatibility)", "OpenGL (kompatibilita)"),
|
||||
@@ -7437,6 +7448,8 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Variable fonts only", "Jen proměnná písma"),
|
||||
("Total Offset", "Celkový posun"),
|
||||
("Per Layer", "Na vrstvu"),
|
||||
("Video Scopes", "Videoskopy"),
|
||||
("GPU Acceleration (no GPU: software)", "Akcelerace GPU (bez GPU: software)"),
|
||||
];
|
||||
|
||||
/// Spanish: neutral, tú, sentence case; the same source strings as the Chinese catalogs, in the same
|
||||
@@ -7965,8 +7978,8 @@ const SPANISH: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "Importar archivo de Photoshop"),
|
||||
("Interpret Unlabeled Alpha As", "Interpretar alfa sin etiquetar como"),
|
||||
("Join the ArtCraft Discord", "Únete al Discord de ArtCraft"),
|
||||
("Mercury GPU Acceleration ({})", "Aceleración por GPU Mercury ({})"),
|
||||
("Mercury Software Only", "Solo software Mercury"),
|
||||
("GPU Acceleration ({})", "Aceleración por GPU ({})"),
|
||||
("Software Rendering", "Renderizado por software"),
|
||||
("OCIO (built-in ACES config)", "OCIO (configuración ACES integrada)"),
|
||||
("Off (Final Quality)", "Desactivado (calidad final)"),
|
||||
("OpenGL (compatibility)", "OpenGL (compatibilidad)"),
|
||||
@@ -8007,6 +8020,8 @@ const SPANISH: &[(&str, &str)] = &[
|
||||
("After Effects project", "Proyecto de After Effects"),
|
||||
("Lottie animation", "Animación de Lottie"),
|
||||
("Project", "Proyecto"),
|
||||
("Video Scopes", "Monitores de vídeo"),
|
||||
("GPU Acceleration (no GPU: software)", "Aceleración por GPU (sin GPU: software)"),
|
||||
];
|
||||
|
||||
/// Georgian (ka): every row of the Chinese catalogs, in the same order (the tests keep them in step).
|
||||
@@ -8533,8 +8548,8 @@ const GEORGIAN: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "Photoshop ფაილის იმპორტი"),
|
||||
("Interpret Unlabeled Alpha As", "უცნობი ალფას ინტერპრეტაცია"),
|
||||
("Join the ArtCraft Discord", "შემოგვიერთდით Discord-ზე"),
|
||||
("Mercury GPU Acceleration ({})", "GPU აჩქარება ({})"),
|
||||
("Mercury Software Only", "მხოლოდ Mercury Software"),
|
||||
("GPU Acceleration ({})", "GPU აჩქარება ({})"),
|
||||
("Software Rendering", "პროგრამული რენდერი"),
|
||||
("OCIO (built-in ACES config)", "OCIO (ჩაშენებული ACES კონფიგურაცია)"),
|
||||
("Off (Final Quality)", "გამორთული (საბოლოო ხარისხი)"),
|
||||
("OpenGL (compatibility)", "OpenGL (თავსებადობა)"),
|
||||
@@ -8766,6 +8781,7 @@ const GEORGIAN: &[(&str, &str)] = &[
|
||||
("Total Offset", "ჯამური წანაცვლება"),
|
||||
("Per Layer", "შრიდან შრემდე"),
|
||||
("Track", "ტრეკინგი"),
|
||||
("Video Scopes", "ვიდეოსკოპები"),
|
||||
];
|
||||
|
||||
/// Georgian rows for interface text no other catalog has yet: strings the panels already send
|
||||
@@ -8956,7 +8972,7 @@ const GEORGIAN_PANELS: &[(&str, &str)] = &[
|
||||
("Solid Settings", "სოლიდის პარამეტრები"),
|
||||
("Save as Template", "შაბლონად შენახვა"),
|
||||
("My Custom RGB", "ჩემი მორგებული RGB"),
|
||||
("Mercury GPU Acceleration (no GPU: software)", "Mercury GPU აჩქარება (GPU არ არის: პროგრამული)"),
|
||||
("GPU Acceleration (no GPU: software)", "GPU აჩქარება (GPU არ არის: პროგრამული)"),
|
||||
("Sort by {}, {}", "დალაგება: {}, {}"),
|
||||
("Sort by {}", "დალაგება: {}"),
|
||||
("Label of {}: {}", "{}-ის იარლიყი: {}"),
|
||||
@@ -9515,8 +9531,8 @@ const FRENCH: &[(&str, &str)] = &[
|
||||
("Import Photoshop File", "Importer un fichier Photoshop"),
|
||||
("Interpret Unlabeled Alpha As", "Interpréter l’alpha non étiqueté comme"),
|
||||
("Join the ArtCraft Discord", "Rejoindre le Discord ArtCraft"),
|
||||
("Mercury GPU Acceleration ({})", "Accélération GPU Mercury ({})"),
|
||||
("Mercury Software Only", "Mercury, logiciel uniquement"),
|
||||
("GPU Acceleration ({})", "Accélération GPU ({})"),
|
||||
("Software Rendering", "Rendu logiciel"),
|
||||
("OCIO (built-in ACES config)", "OCIO (configuration ACES intégrée)"),
|
||||
("Off (Final Quality)", "Désactivé (qualité finale)"),
|
||||
("OpenGL (compatibility)", "OpenGL (compatibilité)"),
|
||||
@@ -9931,7 +9947,7 @@ const FRENCH: &[(&str, &str)] = &[
|
||||
("Solid Settings", "Paramètres du solide"),
|
||||
("Save as Template", "Enregistrer en tant que modèle"),
|
||||
("My Custom RGB", "Mon RVB personnalisé"),
|
||||
("Mercury GPU Acceleration (no GPU: software)", "Accélération GPU Mercury (aucun GPU : logiciel)"),
|
||||
("GPU Acceleration (no GPU: software)", "Accélération GPU (aucun GPU : logiciel)"),
|
||||
("Sort by {}, {}", "Trier par {}, {}"),
|
||||
("Sort by {}", "Trier par {}"),
|
||||
("Label of {}: {}", "Étiquette de {} : {}"),
|
||||
@@ -10052,7 +10068,7 @@ const FRENCH: &[(&str, &str)] = &[
|
||||
("Script Console", "Console de scripts"),
|
||||
("Essential Graphics", "Graphiques essentiels"),
|
||||
("ScriptUI Panel", "Panneau ScriptUI"),
|
||||
("Lumetri Scopes", "Scopes Lumetri"),
|
||||
("Video Scopes", "Oscilloscopes vidéo"),
|
||||
("Media Browser", "Explorateur de médias"),
|
||||
("Content-Aware Fill", "Remplissage adapté au contenu"),
|
||||
("Create Nulls From Paths", "Créer des objets nuls à partir de tracés"),
|
||||
@@ -10271,6 +10287,26 @@ const FRENCH: &[(&str, &str)] = &[
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Check rendered feature labels only. Format names, compatibility tokens and user text
|
||||
/// are not subject to this guard, and a missing translation keeps its English fallback.
|
||||
#[test]
|
||||
fn renamed_panel_labels_do_not_reintroduce_vendor_names() {
|
||||
for language in ["en", "ja", "zh-hans", "zh-hant", "fr", "sv", "ka", "cs", "de", "es", "pl", "pt-br", "ru", "uk"] {
|
||||
set_current(language);
|
||||
for source in ["GPU Acceleration ({})", "GPU Acceleration (no GPU: software)", "Software Rendering", "Video Scopes"] {
|
||||
let rendered = tr(source);
|
||||
assert!(!rendered.is_empty(), "{language}: {source}");
|
||||
let label = rendered.to_lowercase();
|
||||
for mark in ["lumetri", "mercury", "mocha", "keylight"] {
|
||||
assert!(!label.contains(mark), "{language}: {source} -> {rendered}");
|
||||
}
|
||||
}
|
||||
// A real interchange label remains factual, not rewritten or rejected.
|
||||
assert!(!tr("Export Adobe Premiere Pro Project").is_empty());
|
||||
}
|
||||
set_current("en");
|
||||
}
|
||||
|
||||
/// The interface text that has moved into this catalog. Add a panel here when you convert it;
|
||||
/// `every_row_is_used_by_a_converted_panel` then looks for its rows' English sources in it.
|
||||
pub(super) const CONVERTED: &[(&str, &str)] = &[
|
||||
|
||||
@@ -338,11 +338,11 @@ fn interpret_footage_form(s: &effectcraft_engine::Session, proxy: bool, base: &m
|
||||
/// The Project Settings dialog's title and fields.
|
||||
fn project_settings_form(s: &effectcraft_engine::Session) -> (String, Vec<Field>) {
|
||||
let st = &s.project.settings;
|
||||
// The catalogue row is `Mercury GPU Acceleration ({})`, so the value goes through
|
||||
// The catalogue row is `GPU Acceleration ({})`, so the value goes through
|
||||
// tr_args: wrapping the assembled string in tr() would look up the backend's name.
|
||||
let gpu_label = match &s.accel {
|
||||
Some(a) => tr_args("Mercury GPU Acceleration ({})", &[&a.name()]),
|
||||
None => tr("Mercury GPU Acceleration (no GPU: software)").to_string(),
|
||||
Some(a) => tr_args("GPU Acceleration ({})", &[&a.name()]),
|
||||
None => tr("GPU Acceleration (no GPU: software)").to_string(),
|
||||
};
|
||||
let depth = match st.bit_depth.label() {
|
||||
l if l.starts_with("16") => 1,
|
||||
@@ -436,7 +436,7 @@ fn project_settings_form(s: &effectcraft_engine::Session) -> (String, Vec<Field>
|
||||
Field::choice(
|
||||
"renderer",
|
||||
"Video rendering and effects",
|
||||
&[(gpu_label.as_str(), json!("gpu")), ("Mercury Software Only", json!("software"))],
|
||||
&[(gpu_label.as_str(), json!("gpu")), ("Software Rendering", json!("software"))],
|
||||
usize::from(!st.gpu_acceleration),
|
||||
),
|
||||
],
|
||||
@@ -1050,6 +1050,18 @@ pub fn show_view_options(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Toke
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn renderer_form_keeps_descriptive_labels_and_stable_option_values() {
|
||||
crate::i18n::set_current("en");
|
||||
let (_, fields) = project_settings_form(&effectcraft_engine::Session::default());
|
||||
let renderer = fields.iter().find(|field| field.key == "renderer").unwrap();
|
||||
let FieldKind::Choice { options, .. } = &renderer.kind else { panic!("renderer must be a choice") };
|
||||
assert_eq!(
|
||||
options.as_slice(),
|
||||
&[("GPU Acceleration (no GPU: software)".to_string(), json!("gpu")), ("Software Rendering".to_string(), json!("software"))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_params_build_arrays_and_choices() {
|
||||
let f = Form {
|
||||
|
||||
@@ -756,7 +756,7 @@ pub struct UiState {
|
||||
/// Flowchart panel options (layers, effects, solids, direction, root comp).
|
||||
#[serde(default)]
|
||||
pub flowchart: crate::panels::flowchart::FlowOptions,
|
||||
/// Lumetri Scopes panel options.
|
||||
/// Video Scopes panel options.
|
||||
#[serde(default)]
|
||||
pub scopes: crate::panels::scopes_panel::ScopesState,
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ pub(crate) fn show(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
rt.updater.dismiss_status();
|
||||
}
|
||||
CheckResultResponse::CheckAgain => rt.updater.check_now(ctx),
|
||||
CheckResultResponse::RestartToUpdate => rt.updater.restart_to_update(),
|
||||
}
|
||||
// A check that found a release leaves the status idle and the startup prompt takes over.
|
||||
if matches!(rt.updater.status(), Status::Idle) && !rt.check_requested {
|
||||
|
||||
@@ -38,7 +38,7 @@ fn harness(panel: Panel, angle: f64) -> (Harness<'static, EffectcraftApp>, u64,
|
||||
s.execute("comp.new", json!({"name": "Angles", "width": 320, "height": 180, "duration": 4})).unwrap();
|
||||
let layer = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#40b080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let uid = if matches!(panel, Panel::EffectControls) {
|
||||
let fx = s.execute("effect.apply", json!({"layer": layer, "effect": "CC Bend It"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"layer": layer, "effect": "Two-Point Bend"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.find_group(fx).unwrap().get("bend").unwrap().uid
|
||||
} else {
|
||||
s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.prop("transform/rotation").unwrap().uid
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The colour picker every colour input opens: its hex field sets an effect's colour (CC Toner),
|
||||
//! The colour picker every colour input opens: its hex field sets an effect's colour (Multi-Tone Tint),
|
||||
//! the dialogs' colour button stores sRGB (egui's own `color_edit_button_rgba` read the stored
|
||||
//! sRGB values as linear, so mid-grey `#808080` was stored as `#373737`), the square, slider and
|
||||
//! HSB / RGB fields set the colour, and Cancel / Escape / the original swatch restore it.
|
||||
@@ -45,7 +45,7 @@ fn hex_code_sets_an_effect_colour() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Hex", "width": 320, "height": 180, "duration": 4})).unwrap();
|
||||
let layer = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#40b080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"layer": layer, "effect": "CC Toner"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"layer": layer, "effect": "Multi-Tone Tint"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let uid = s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.find_group(fx).unwrap().get("midtones").unwrap().uid;
|
||||
s.state.selected_layers = vec![LayerId(layer)];
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
|
||||
@@ -260,14 +260,14 @@ fn the_eyedropper_stays_armed_until_the_viewer_has_a_frame() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Key Light's Screen Colour eyedropper picks from the effect's input: the shown frame is
|
||||
/// Screen Color Key's Screen Colour eyedropper picks from the effect's input: the shown frame is
|
||||
/// already keyed (the default screen colour takes most of a green), so sampling it would miss.
|
||||
#[test]
|
||||
fn keyer_eyedropper_picks_the_screen_from_the_effect_input() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Key", "width": 320, "height": 180, "frameRate": 30, "duration": 1})).unwrap();
|
||||
let screen = s.execute("layer.newSolid", json!({"name": "Screen", "color": "#30b050"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"layers": [screen], "effect": "Key Light"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"layers": [screen], "effect": "Screen Color Key"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let prop = layer(&s, screen).effects().unwrap().groups().find(|g| g.uid == fx).and_then(|g| g.get("screenColour")).map(|p| p.uid).unwrap();
|
||||
s.state.selected_layers = vec![LayerId(screen)];
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
@@ -295,7 +295,7 @@ fn keyer_eyedropper_picks_the_screen_from_the_effect_input() {
|
||||
fn command_clicking_an_effect_twirl_closes_and_opens_its_groups() {
|
||||
let (app, x) = app();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app);
|
||||
let fx = h.state_mut().session.execute("effect.apply", json!({"layers": [x.small], "effect": "Lumetri Color"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let fx = h.state_mut().session.execute("effect.apply", json!({"layers": [x.small], "effect": "Color Grading"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
// Only this effect open, so its header is on screen.
|
||||
let l = layer(&h.state().session, x.small);
|
||||
let others: Vec<u64> = l.effects().unwrap().groups().map(|g| g.uid).filter(|u| *u != fx).collect();
|
||||
|
||||
@@ -13,7 +13,7 @@ fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
|
||||
fn drag_to(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
egui::DragAndDrop::set_payload(&h.ctx, DragPayload::Effect("CC Bend It".into()));
|
||||
egui::DragAndDrop::set_payload(&h.ctx, DragPayload::Effect("Two-Point Bend".into()));
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ fn undo(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
|
||||
#[test]
|
||||
fn lumetri_rgb_and_hue_saturation_curve_editors() {
|
||||
let (s0, l, fx) = with_effect("Lumetri Color");
|
||||
let (s0, l, fx) = with_effect("Color Grading");
|
||||
let mut h = harness(s0);
|
||||
only_groups(&mut h, l, fx, &["curves", "rgbCurves", "hueSaturationCurves"]);
|
||||
let pre = format!("effectControls.effect.{fx}.lumetri.rgbCurves");
|
||||
|
||||
@@ -77,12 +77,12 @@ fn position_anchor_and_effect_edits_present_intermediate_pixels() {
|
||||
(None, "transform/position", json!([22, 18, 0]), json!([18, 18, 0])),
|
||||
(None, "transform/anchor", json!([9, 8, 0]), json!([6, 8, 0])),
|
||||
(Some("Gaussian Blur"), "blurriness", json!(3), json!(6)),
|
||||
(Some("CC Radial Fast Blur"), "center", json!([4, 4]), json!([18, 12])),
|
||||
(Some("Fast Radial Blur"), "center", json!([4, 4]), json!([18, 12])),
|
||||
] {
|
||||
let mut session = Session::default();
|
||||
session.execute("comp.new", json!({"width":64,"height":36,"duration":1})).unwrap();
|
||||
let layer = session.execute("layer.newSolid", json!({"color":"#ffffff","width":24,"height":16})).unwrap()["layer"].as_u64().unwrap();
|
||||
if effect == Some("CC Radial Fast Blur") {
|
||||
if effect == Some("Fast Radial Blur") {
|
||||
// A uniform solid is invariant under a radial blur; give the point edit visible detail.
|
||||
session.execute("effect.apply", json!({"layers":[layer],"effect":"Gradient Ramp"})).unwrap();
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ fn nested_groups_keep_indentation_value_alignment_and_effect_gaps_at_fixed_compa
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"width":64,"height":36,"duration":1})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"name":"Plate","color":"#406080"})).unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"effect":"Lumetri Color"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"effect":"Color Grading"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let next = s.execute("effect.apply", json!({"effect":"Checkbox Control"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let g = s.active_comp().unwrap().layers[0].props.find_group(fx).unwrap();
|
||||
let basic = g.sub("basicCorrection").unwrap();
|
||||
|
||||
@@ -2370,7 +2370,7 @@ fn command_clicking_a_group_twirl_opens_its_whole_tree() {
|
||||
// twirls open with it, and closed again with it.
|
||||
let mut h = harness();
|
||||
let id = layer_id(&h, "Box");
|
||||
h.state_mut().session.execute("effect.apply", json!({"effect": "Lumetri Color", "layers": [id]})).unwrap();
|
||||
h.state_mut().session.execute("effect.apply", json!({"effect": "Color Grading", "layers": [id]})).unwrap();
|
||||
fn walk(g: &effectcraft_engine::project::PropGroup, out: &mut Vec<u64>) {
|
||||
for sg in g.groups() {
|
||||
out.push(sg.uid);
|
||||
|
||||
+9
-1
@@ -135,7 +135,15 @@ Units, the "absent = default" rule and the keyframe templates are the reader's
|
||||
text does not have; the runs are cut to the text. A range style splits `Hello` into runs of 1,
|
||||
2 and 2 characters (248, 340).
|
||||
- **Null objects.** The hidden placeholder solid of a null object is not an item (null layers
|
||||
have no source item here).
|
||||
have no source item here). A null's Anchor Point that is not stored is its top-left corner
|
||||
`[0, 0]`, not the centre, as After Effects 2026 reads it back (#3099).
|
||||
- **Placed Illustrator and PDF files** (`.ai`, `.pdf`; `Footage::placed_page`): the page is
|
||||
framed as After Effects places it (the ArtBox clipped to the crop box, `/Rotate` ignored) and
|
||||
drawn to fill the stored footage size, which masks and anchors are measured against (#3099).
|
||||
Create Shapes from Vector Layer on such a layer adds a hidden `<layer> Frame` shape layer, the
|
||||
Alpha track matte of every layer it makes, so they show only the frame, as the footage does.
|
||||
EPS files keep their bounding box at their own size: how After Effects sizes them is not
|
||||
measured.
|
||||
- **Track mattes.** The mode and the matte layer's id are in the target layer's header
|
||||
(aep-format.md 3.1). The matte layer can be any layer of the composition (407: below its
|
||||
target); it keeps its stored video switch (After Effects switches it off when the matte is set,
|
||||
|
||||
+14
-14
@@ -43,7 +43,7 @@ cpal or muda. Everything in L0 to L4, the egui UI and the web app also build for
|
||||
| L3 | `interchange` | Premiere Pro interop through timeline interchange (FCP7 XML, FCPXML, OTIO, EDL, AAF, OMF via FilmCraft's `filmcraft-interchange`): sequences ↔ compositions (§6a) |
|
||||
| L3 | `aep` | After Effects projects in one crate (like `lottie`): `container` reads and writes the `.aep` container (RIFX `Egg!`) as a lossless chunk tree with a byte-identical round trip, bounded for hostile input, reads the `.aepx` XML form into the same tree, and has a typed view of items, layers, properties, effects, masks, shapes, text, markers, footage, cameras, lights and layer styles, from our own observations of generated files ([aep-format.md](aep-format.md)); `import` maps the typed view into a project (items, settings, compositions, every layer kind, Transform, keyframes, expressions, masks, text, shapes, cameras, lights, mattes, markers, time remapping, layer styles, the effects of a shared table, footage) with a report of everything not imported ([aep-import.md](aep-import.md)); `export` builds every chunk in code (prototypes in the layout After Effects writes, cloned and edited only at verified fields) for the same scope, with a report of everything not exported, verified by opening the output in After Effects ([aep-export.md](aep-export.md)) |
|
||||
| L3 | `lottie` | Lottie JSON / dotLottie import and export (layers, precomps, eased and spatial keyframes, shapes, masks, mattes) with a warnings list for what Lottie cannot express |
|
||||
| L3 | `plugin` | Effect plug-ins: loads sandboxed WebAssembly effect modules (plug-in API v1; the wasmi interpreter behind the `wasm` feature, on for native hosts) into the effect registry ([plugins.md](plugins.md)) |
|
||||
| L3 | `plugin` | Effect plug-ins: loads sandboxed WebAssembly effect modules (WebAssembly ABI v1 and v2, the latter with neighbouring frames; the wasmi interpreter behind the `wasm` feature, on for native hosts) into the effect registry ([plugins.md](plugins.md)) |
|
||||
| L3 | `helper-host` | External effect helpers: finds helper programs, runs each as a child process and registers its effects (plug-in API 2) over a versioned pipe + shared-file protocol; a crashing helper is an error on the effect, never a crash ([external-helpers.md](external-helpers.md), [ADR 0001](adr/0001-external-effect-helpers.md)). Native only |
|
||||
| L4 | `engine` | `Session`: project, branching undo history, editor state, the command registry and menus, the ScriptUI window model (`scriptui`) |
|
||||
| L4 | `script` | Scripting: JavaScript (boa) with an After Effects-style object model (`app.project`, comps, layers, properties, render queue) and ScriptUI (dialogs, palettes, dockable panels; script-host threads so dialogs block `show()`), whose edits run engine commands; AE match names |
|
||||
@@ -121,7 +121,7 @@ are not rewritten, so an expression that computes an effect point on a shape or
|
||||
other values may need `+ [thisComp.width / 2, thisComp.height / 2]`. Effects that use the layer
|
||||
bounds (edge pinning, wipes, Offset, generators) now cover the whole comp-sized bounds instead of
|
||||
their bottom-right quarter. Tracking data of effects that analyse footage (Warp Stabilizer, 3D
|
||||
Camera Tracker, Mocha shape) is left as it is: footage has a source rectangle, whose effect space
|
||||
Camera Tracker, Tracked Shape Matte) is left as it is: footage has a source rectangle, whose effect space
|
||||
did not change.
|
||||
|
||||
## 4. Rendering
|
||||
@@ -331,7 +331,7 @@ before conversion (a quarter of the conversions; the result differs from convert
|
||||
in blocks where colours clip) and resampled from there, as other frames are from full size.
|
||||
|
||||
**GPU compositor** (`crates/gpu`; Project Settings ▸ Video Rendering and Effects ▸ Mercury GPU
|
||||
Acceleration, the default, or Mercury Software Only; `render.backend`). The CPU renderer is the
|
||||
Acceleration, the default, or Software Rendering; `render.backend`). The CPU renderer is the
|
||||
reference and keeps rendering layer content (sources, masks, CPU effects, layer styles) into the
|
||||
layer cache. The render crate defines an `Accelerator` trait; `effectcraft_gpu::Gpu` implements it
|
||||
on wgpu compute shaders. `RenderOpts::backend` picks `Cpu`, `Gpu` or `Auto` (an accelerator
|
||||
@@ -341,7 +341,7 @@ per comp, output scale and path (readback or viewer display) it averages CPU and
|
||||
(warm-up on both sides, the fastest warm-up frame starting the average, single stalls capped),
|
||||
picks the GPU unless the CPU is clearly faster, and re-measures the other side every 48 frames.
|
||||
Render Queue jobs don't use Auto: every frame of a job renders on the project's renderer (the
|
||||
GPU compositor with Mercury GPU Acceleration), so runs of a render don't differ by timing.
|
||||
GPU compositor with GPU Acceleration), so runs of a render don't differ by timing.
|
||||
Light comps (a few small layers, which the CPU composites only within their bounds) then stay on
|
||||
the CPU instead of paying for full-frame passes and a readback. A GPU frame walks the comp like
|
||||
`draw_comp`: cached layer buffers are uploaded once per buffer, then transformed with the CPU's
|
||||
@@ -370,16 +370,16 @@ Preserve Transparency / blend kernels (a 3D track matte renders solo through the
|
||||
Environment Light Background layers draw their sky in a kernel (`adv_sky`, from
|
||||
`Renderer::sky_draw`) before the rest of the run, in Classic and Advanced 3D comps. Wireframe
|
||||
outlines draw on the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU effects
|
||||
(`effects::GPU_EFFECTS`, 280 of them: blurs, colour correction, keying incl. Key Light, mattes,
|
||||
channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, CC Bend It, CC Page
|
||||
Turn, CC Bender, CC Blobbylize), Cartoon, bevels, shapes, the transitions (CC light family, CC
|
||||
transitions, Block Dissolve), perspective (Radial Shadow, CC Cylinder / Sphere / Spotlight /
|
||||
(`effects::GPU_EFFECTS`, 280 of them: blurs, colour correction, keying incl. Screen Color Key, mattes,
|
||||
channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, Two-Point Bend, CC Page
|
||||
Turn, Flexible Bend, Organic Displacement), Cartoon, bevels, shapes, the transitions (CC light family, CC
|
||||
transitions, Block Dissolve), perspective (Radial Shadow, Cylinder Wrap / Sphere / Spotlight /
|
||||
Environment, 3D Glasses), generators, noise, grain, text (Numbers, Timecode), time, 3D Channel (aux channels as an
|
||||
extra texture), Immersive Video, colour management (colour programs from
|
||||
`effects::color_program`, LUTs in a storage buffer) and the simulations' render passes (the
|
||||
simulation stays on the CPU and hands its plan — sprites, pieces, blobs, grids — to a tiled
|
||||
rasteriser; the particle effects too, `gpu::fx_particles`: CC Particle World / Systems II,
|
||||
Particle Playground with its Layer Map frames in one atlas, CC Ball Action, CC Pixel Polly, CC
|
||||
rasteriser; the particle effects too, `gpu::fx_particles`: Particle Emitter 3D / Systems II,
|
||||
Particle Playground with its Layer Map frames in one atlas, Ball Grid, Pixel Shatter, CC
|
||||
Scatterize; plans too large for one item table draw in several passes over the previous one),
|
||||
and the geometry generators (Lightning, Advanced Lightning, Radio Waves, Stroke, Scribble,
|
||||
Vegas, Write-on, Audio Spectrum / Waveform, Basic / Path Text: bolts, strokes, waves, audio
|
||||
@@ -390,7 +390,7 @@ as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); con
|
||||
effects run as one chain with one upload and one readback. Statistics that need the whole frame
|
||||
(Auto Levels / Contrast / Color, Equalize, Shadow/Highlight, Color Stabilizer, Remove Grain's
|
||||
noise level) are measured on the CPU from one readback and applied on the GPU; effects reading
|
||||
other frames (Echo, Posterize Time, Time Difference, Time Displacement, CC Force Motion Blur, CC
|
||||
other frames (Echo, Posterize Time, Time Difference, Time Displacement, Temporal Motion Blur, CC
|
||||
Wide Time, Pixel Motion Blur, Timewarp) upload the frames the host renders (`time_frames`) and
|
||||
combine them on the GPU; Time Displacement's per-pixel times and Timewarp's motion vectors
|
||||
(`time_fx::timewarp_plan`) are computed on the CPU. Numbers and Timecode rasterise their glyph
|
||||
@@ -433,12 +433,12 @@ sentinel is implemented here. Browser scope futures are not synchronously awaite
|
||||
forwards shared-device errors, while owned worker devices install their own handlers. On the web
|
||||
(WebGPU) the GPU composites viewer frames; steps that need a readback fall back to the CPU (the
|
||||
Info panel's pixel readout reads GPU frames back asynchronously). **GPU particles**
|
||||
(`effects::psim`, `gpu::particles`): CC Particle World, CC Particle Systems II and Particle
|
||||
(`effects::psim`, `gpu::particles`): Particle Emitter 3D, Particle Emitter 2D and Particle
|
||||
Playground cannons without interacting forces evolve every particle independently, so the GPU
|
||||
backend (reached through `EffectHost::particles` when the GPU compositor is active) simulates one
|
||||
particle per invocation from the CPU's birth schedule and keeps per-key state checkpoints on the
|
||||
GPU, like `SimCache`; the CPU simulation stays the oracle (tests compare ids and positions).
|
||||
CC Particle World, CC Particle Systems II and CC Mr. Mercury sample their Birth Rate at every
|
||||
Particle Emitter 3D, Particle Emitter 2D and Liquid Particles sample their Birth Rate at every
|
||||
step (`EffectHost::param_at`, #1373), so a keyed rate only decides the births of its own time;
|
||||
the rate is left out of the simulation key, and a checkpoint is resumed only with the same rate history (the CPU state's `hist`, the GPU
|
||||
checkpoint's births). The rate's keyframes are read at the step's exact layer time
|
||||
@@ -629,7 +629,7 @@ Unix milliseconds. The layer's Start Time becomes `dataStart + videoStart − fi
|
||||
(`videoStart` given, or the file's creation date on the desktop; reduced to a time of day when
|
||||
the data has no dates).
|
||||
|
||||
**Lumetri Scopes** (`raster::scopes`) are density plots of the viewer frame (rendered over the
|
||||
**Video Scopes** (`raster::scopes`) are density plots of the viewer frame (rendered over the
|
||||
comp background) with BT.601 / BT.709 / BT.2020 luma and colour-difference coefficients.
|
||||
|
||||
## 5. Expressions
|
||||
|
||||
+72
-72
@@ -48,21 +48,21 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Bilateral Blur | `ec.blur.bilateral` | 3 | GPU | 32 | Implemented |
|
||||
| CC Cross Blur | `ec.blur.cccross` | 3 | GPU | 32 | Implemented |
|
||||
| CC Radial Blur | `ec.blur.ccradial` | 4 | GPU | 32 | Implemented |
|
||||
| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented |
|
||||
| CC Vector Blur | `ec.blur.ccvector` | 4 | GPU | 32 | Implemented |
|
||||
| Camera Lens Blur | `ec.blur.cameralens` | 16 | GPU | 32 | Implemented |
|
||||
| Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
|
||||
| Channel Blur | `ec.blur.channel` | 6 | GPU | 32 | Implemented |
|
||||
| Compound Blur | `ec.blur.compound` | 4 | GPU | 32 | Implemented |
|
||||
| Cross Blur | `ec.blur.cccross` | 3 | GPU | 32 | Implemented |
|
||||
| Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented |
|
||||
| Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented |
|
||||
| Fast Radial Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented |
|
||||
| Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented |
|
||||
| Radial Blur | `ec.blur.radial` | 4 | GPU | 32 | Implemented |
|
||||
| Sharpen | `ec.blur.sharpen` | 1 | GPU | 32 | Implemented |
|
||||
| Smart Blur | `ec.blur.smart` | 3 | GPU | 32 | Implemented |
|
||||
| Unsharp Mask | `ec.blur.unsharp` | 3 | GPU | 32 | Implemented |
|
||||
| Variable Radial Blur | `ec.blur.ccradial` | 4 | GPU | 32 | Implemented |
|
||||
| Vector Blur | `ec.blur.ccvector` | 4 | GPU | 32 | Implemented |
|
||||
|
||||
## Channel
|
||||
|
||||
@@ -70,7 +70,6 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented |
|
||||
| Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented |
|
||||
| CC Composite | `ec.channel.cccomposite` | 3 | GPU | 32 | Implemented |
|
||||
| Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented |
|
||||
| Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented |
|
||||
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented |
|
||||
@@ -81,6 +80,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Set Matte | `ec.channel.setmatte` | 6 | GPU | 32 | Implemented |
|
||||
| Shift Channels | `ec.channel.shiftchannels` | 4 | GPU | 32 | Implemented |
|
||||
| Solid Composite | `ec.channel.solidcomposite` | 4 | GPU | 32 | Implemented |
|
||||
| Source Composite | `ec.channel.cccomposite` | 3 | GPU | 32 | Implemented |
|
||||
|
||||
## Color Correction
|
||||
|
||||
@@ -92,18 +92,18 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Black & White | `ec.color.blackwhite` | 8 | GPU | 32 | Implemented |
|
||||
| Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented |
|
||||
| Broadcast Colors | `ec.color.broadcast` | 3 | GPU | 32 | Implemented |
|
||||
| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | GPU | 32 | Implemented |
|
||||
| CC Color Offset | `ec.color.cccoloroffset` | 4 | GPU | 32 | Implemented |
|
||||
| CC Kernel | `ec.color.cckernel` | 12 | GPU | 32 | Implemented |
|
||||
| CC Toner | `ec.color.cctoner` | 7 | GPU | 32 | Implemented |
|
||||
| Change Color | `ec.color.changecolor` | 9 | GPU | 32 | Implemented |
|
||||
| Change to Color | `ec.color.changetocolor` | 9 | GPU | 32 | Implemented |
|
||||
| Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented |
|
||||
| Channel Offset | `ec.color.cccoloroffset` | 4 | GPU | 32 | Implemented |
|
||||
| Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented |
|
||||
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | GPU | 32 | Implemented |
|
||||
| Color Grading | `ec.color.lumetri` | 66 | GPU | 32 | Implemented |
|
||||
| Color Link | `ec.color.colorlink` | 6 | GPU | 32 | Implemented |
|
||||
| Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | GPU | 32 | Implemented |
|
||||
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | GPU | 32 | Implemented |
|
||||
| Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented |
|
||||
| Convolution Kernel | `ec.color.cckernel` | 12 | GPU | 32 | Implemented |
|
||||
| Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented |
|
||||
| Equalize | `ec.color.equalize` | 2 | GPU | 32 | Implemented |
|
||||
| Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented |
|
||||
@@ -112,7 +112,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Leave Color | `ec.color.leavecolor` | 5 | GPU | 32 | Implemented |
|
||||
| Levels | `ec.color.levels` | 36 | GPU | 32 | Implemented |
|
||||
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | GPU | 32 | Implemented |
|
||||
| Lumetri Color | `ec.color.lumetri` | 66 | GPU | 32 | Implemented |
|
||||
| Multi-Tone Tint | `ec.color.cctoner` | 7 | GPU | 32 | Implemented |
|
||||
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | GPU | 32 | Implemented |
|
||||
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | GPU | 32 | Implemented |
|
||||
| OCIO Display Transform | `ec.color.ociodisplay` | 9 | GPU | 32 | Implemented |
|
||||
@@ -133,40 +133,40 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Bezier Warp | `ec.distort.bezierwarp` | 13 | GPU | 32 | Implemented |
|
||||
| Bulge | `ec.distort.bulge` | 6 | GPU | 32 | Implemented |
|
||||
| CC Bend It | `ec.distort.ccbendit` | 4 | GPU | 32 | Implemented |
|
||||
| CC Bender | `ec.distort.ccbender` | 5 | GPU | 32 | Implemented |
|
||||
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | GPU | 32 | Implemented |
|
||||
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented |
|
||||
| CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented |
|
||||
| CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
|
||||
| CC Page Turn | `ec.distort.ccpageturn` | 7 | GPU | 32 | Implemented |
|
||||
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented |
|
||||
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | GPU | 32 | Implemented |
|
||||
| CC Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented |
|
||||
| CC Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented |
|
||||
| CC Split | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented |
|
||||
| CC Split 2 | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented |
|
||||
| CC Tiler | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented |
|
||||
| Corner Pin | `ec.distort.cornerpin` | 4 | GPU | 32 | Implemented |
|
||||
| Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented |
|
||||
| Directional Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented |
|
||||
| Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented |
|
||||
| Dual Lens Warp | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented |
|
||||
| Dual Split Warp | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented |
|
||||
| Flexible Bend | `ec.distort.ccbender` | 5 | GPU | 32 | Implemented |
|
||||
| Lens Distortion | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
|
||||
| Liquify | `ec.distort.liquify` | 13 | GPU | 32 | Implemented |
|
||||
| Magnify | `ec.distort.magnify` | 10 | GPU | 32 | Implemented |
|
||||
| Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented |
|
||||
| Mirror | `ec.distort.mirror` | 2 | GPU | 32 | Implemented |
|
||||
| Offset | `ec.distort.offset` | 2 | GPU | 32 | Implemented |
|
||||
| Optics Compensation | `ec.distort.opticscompensation` | 6 | GPU | 32 | Implemented |
|
||||
| Organic Displacement | `ec.distort.ccblobbylize` | 15 | GPU | 32 | Implemented |
|
||||
| Page Curl | `ec.distort.ccpageturn` | 7 | GPU | 32 | Implemented |
|
||||
| Perspective Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented |
|
||||
| Polar Coordinates | `ec.distort.polar` | 2 | GPU | 32 | Implemented |
|
||||
| Pulsed Ripple | `ec.distort.ccripplepulse` | 5 | GPU | 32 | Implemented |
|
||||
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
|
||||
| Reshape | `ec.distort.reshape` | 6 | GPU | 32 | Implemented |
|
||||
| Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented |
|
||||
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented |
|
||||
| Scaled Tiles | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented |
|
||||
| Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented |
|
||||
| Smear | `ec.distort.smear` | 8 | GPU | 32 | Implemented |
|
||||
| Spherize | `ec.distort.spherize` | 2 | GPU | 32 | Implemented |
|
||||
| Split Warp | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented |
|
||||
| Tile Grid | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented |
|
||||
| Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented |
|
||||
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented |
|
||||
| Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented |
|
||||
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | GPU | 32 | Implemented |
|
||||
| Two-Point Bend | `ec.distort.ccbendit` | 4 | GPU | 32 | Implemented |
|
||||
| Warp | `ec.distort.warp` | 5 | GPU | 32 | Implemented |
|
||||
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
|
||||
| Wave Warp | `ec.distort.wavewarp` | 7 | GPU | 32 | Implemented |
|
||||
@@ -194,11 +194,6 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Audio Spectrum | `ec.generate.audiospectrum` | 23 | GPU | 32 | Implemented |
|
||||
| Audio Waveform | `ec.generate.audiowaveform` | 17 | GPU | 32 | Implemented |
|
||||
| Beam | `ec.generate.beam` | 11 | GPU | 32 | Implemented |
|
||||
| CC Glue Gun | `ec.generate.ccgluegun` | 7 | GPU | 32 | Implemented |
|
||||
| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | GPU | 32 | Implemented |
|
||||
| CC Light Rays | `ec.generate.cclightrays` | 8 | GPU | 32 | Implemented |
|
||||
| CC Light Sweep | `ec.generate.cclightsweep` | 9 | GPU | 32 | Implemented |
|
||||
| CC Threads | `ec.generate.ccthreads` | 8 | GPU | 32 | Implemented |
|
||||
| Cell Pattern | `ec.generate.cellpattern` | 14 | GPU | 32 | Implemented |
|
||||
| Checkerboard | `ec.generate.checkerboard` | 10 | GPU | 32 | Implemented |
|
||||
| Circle | `ec.generate.circle` | 11 | GPU | 32 | Implemented |
|
||||
@@ -209,11 +204,16 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Gradient Ramp | `ec.generate.gradientramp` | 7 | GPU | 32 | Implemented |
|
||||
| Grid | `ec.generate.grid` | 12 | GPU | 32 | Implemented |
|
||||
| Lens Flare | `ec.generate.lensflare` | 4 | GPU | 32 | Implemented |
|
||||
| Light Rays | `ec.generate.cclightrays` | 8 | GPU | 32 | Implemented |
|
||||
| Light Sweep | `ec.generate.cclightsweep` | 9 | GPU | 32 | Implemented |
|
||||
| Paint Beads | `ec.generate.ccgluegun` | 7 | GPU | 32 | Implemented |
|
||||
| Paint Bucket | `ec.generate.paintbucket` | 11 | GPU | 32 | Implemented |
|
||||
| Radial Light Burst | `ec.generate.cclightburst` | 6 | GPU | 32 | Implemented |
|
||||
| Radio Waves | `ec.generate.radiowaves` | 31 | GPU | 32 | Implemented |
|
||||
| Scribble | `ec.generate.scribble` | 25 | GPU | 32 | Implemented |
|
||||
| Stroke | `ec.generate.stroke` | 11 | GPU | 32 | Implemented |
|
||||
| Vegas | `ec.generate.vegas` | 26 | GPU | 32 | Implemented |
|
||||
| Woven Threads | `ec.generate.ccthreads` | 8 | GPU | 32 | Implemented |
|
||||
| Write-on | `ec.generate.writeon` | 10 | GPU | 32 | Implemented |
|
||||
|
||||
## Immersive Video
|
||||
@@ -238,15 +238,15 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | GPU | 32 | Implemented |
|
||||
| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | GPU | 32 | Implemented |
|
||||
| Color Difference Key | `ec.key.colordifference` | 16 | GPU | 32 | Implemented |
|
||||
| Color Range | `ec.key.colorrange` | 8 | GPU | 32 | Implemented |
|
||||
| Difference Matte | `ec.key.differencematte` | 6 | GPU | 32 | Implemented |
|
||||
| Extract | `ec.key.extract` | 6 | GPU | 32 | Implemented |
|
||||
| Inner/Outer Key | `ec.key.innerouter` | 88 | GPU | 32 | Implemented |
|
||||
| Key Cleaner | `ec.key.keycleaner` | 4 | GPU | 32 | Implemented |
|
||||
| Key Light | `ec.keying.keylight` | 56 | GPU | 32 | Implemented |
|
||||
| Line Removal | `ec.key.ccsimplewireremoval` | 7 | GPU | 32 | Implemented |
|
||||
| Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented |
|
||||
| Screen Color Key | `ec.keying.keylight` | 56 | GPU | 32 | Implemented |
|
||||
| Screen Key | `ec.key.screen` | 11 | GPU | 32 | Implemented |
|
||||
| Unmult | `ec.key.unmult` | 6 | GPU | 32 | Implemented |
|
||||
|
||||
@@ -255,12 +255,12 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Matte Choker | `ec.matte.mattechoker` | 7 | GPU | 32 | Implemented |
|
||||
| Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented |
|
||||
| Object Matte | `ec.matte.objectmatte` | 13 | | 32 | Implemented |
|
||||
| Refine Hard Matte | `ec.matte.refinehard` | 16 | GPU | 32 | Implemented |
|
||||
| Refine Soft Matte | `ec.matte.refinesoft` | 18 | GPU | 32 | Implemented |
|
||||
| Roto Brush & Refine Edge | `ec.matte.rotobrush` | 25 | | 32 | Implemented |
|
||||
| Simple Choker | `ec.matte.simplechoker` | 2 | GPU | 32 | Implemented |
|
||||
| Tracked Shape Matte | `ec.obsolete.mochashape` | 6 | | 32 | Implemented |
|
||||
|
||||
## Noise & Grain
|
||||
|
||||
@@ -308,65 +308,65 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| 3D Glasses | `ec.perspective.3dglasses` | 8 | GPU | 32 | Implemented |
|
||||
| Bevel Alpha | `ec.perspective.bevelalpha` | 4 | GPU | 32 | Implemented |
|
||||
| Bevel Edges | `ec.perspective.beveledges` | 4 | GPU | 32 | Implemented |
|
||||
| CC Cylinder | `ec.perspective.cccylinder` | 15 | GPU | 32 | Implemented |
|
||||
| CC Environment | `ec.perspective.ccenvironment` | 4 | GPU | 32 | Implemented |
|
||||
| CC Sphere | `ec.perspective.ccsphere` | 14 | GPU | 32 | Implemented |
|
||||
| CC Spotlight | `ec.perspective.ccspotlight` | 8 | GPU | 32 | Implemented |
|
||||
| Cylinder Wrap | `ec.perspective.cccylinder` | 15 | GPU | 32 | Implemented |
|
||||
| Drop Shadow | `ec.perspective.dropshadow` | 6 | GPU | 32 | Implemented |
|
||||
| Environment Map | `ec.perspective.ccenvironment` | 4 | GPU | 32 | Implemented |
|
||||
| Radial Shadow | `ec.perspective.radialshadow` | 9 | GPU | 32 | Implemented |
|
||||
| Sphere Wrap | `ec.perspective.ccsphere` | 14 | GPU | 32 | Implemented |
|
||||
| Surface Spotlight | `ec.perspective.ccspotlight` | 8 | GPU | 32 | Implemented |
|
||||
|
||||
## Simulation
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| CC Ball Action | `ec.sim.ccballaction` | 8 | GPU | 32 | Implemented |
|
||||
| CC Bubbles | `ec.sim.ccbubbles` | 7 | GPU | 32 | Implemented |
|
||||
| CC Drizzle | `ec.sim.ccdrizzle` | 14 | GPU | 32 | Implemented |
|
||||
| CC Hair | `ec.sim.cchair` | 20 | GPU | 32 | Implemented |
|
||||
| CC Mr. Mercury | `ec.sim.ccmrmercury` | 23 | GPU | 32 | Implemented |
|
||||
| CC Particle Systems II | `ec.sim.ccparticlesystems2` | 21 | GPU | 32 | Implemented |
|
||||
| CC Particle World | `ec.sim.ccparticleworld` | 36 | GPU | 32 | Implemented |
|
||||
| CC Pixel Polly | `ec.sim.ccpixelpolly` | 10 | GPU | 32 | Implemented |
|
||||
| CC Rainfall | `ec.sim.ccrainfall` | 14 | GPU | 32 | Implemented |
|
||||
| CC Scatterize | `ec.sim.ccscatterize` | 4 | GPU | 32 | Implemented |
|
||||
| CC Snowfall | `ec.sim.ccsnowfall` | 19 | GPU | 32 | Implemented |
|
||||
| CC Star Burst | `ec.sim.ccstarburst` | 6 | GPU | 32 | Implemented |
|
||||
| Ball Grid | `ec.sim.ccballaction` | 8 | GPU | 32 | Implemented |
|
||||
| Bubbles | `ec.sim.ccbubbles` | 7 | GPU | 32 | Implemented |
|
||||
| Card Dance | `ec.sim.carddance` | 53 | GPU | 32 | Implemented |
|
||||
| Caustics | `ec.sim.caustics` | 28 | GPU | 32 | Implemented |
|
||||
| Foam | `ec.sim.foam` | 36 | GPU | 32 | Implemented |
|
||||
| Liquid Particles | `ec.sim.ccmrmercury` | 23 | GPU | 32 | Implemented |
|
||||
| Particle Emitter 2D | `ec.sim.ccparticlesystems2` | 21 | GPU | 32 | Implemented |
|
||||
| Particle Emitter 3D | `ec.sim.ccparticleworld` | 36 | GPU | 32 | Implemented |
|
||||
| Particle Playground | `ec.sim.particleplayground` | 92 | GPU | 32 | Implemented |
|
||||
| Pixel Scatter | `ec.sim.ccscatterize` | 4 | GPU | 32 | Implemented |
|
||||
| Pixel Shatter | `ec.sim.ccpixelpolly` | 10 | GPU | 32 | Implemented |
|
||||
| Rainfall | `ec.sim.ccrainfall` | 14 | GPU | 32 | Implemented |
|
||||
| Shatter | `ec.sim.shatter` | 60 | GPU | 32 | Implemented |
|
||||
| Snowfall | `ec.sim.ccsnowfall` | 19 | GPU | 32 | Implemented |
|
||||
| Starfield | `ec.sim.ccstarburst` | 6 | GPU | 32 | Implemented |
|
||||
| Surface Fibers | `ec.sim.cchair` | 20 | GPU | 32 | Implemented |
|
||||
| Water Ripples | `ec.sim.ccdrizzle` | 14 | GPU | 32 | Implemented |
|
||||
| Wave World | `ec.sim.waveworld` | 35 | GPU | 32 | Implemented |
|
||||
|
||||
## Stylize
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Block Reveal | `ec.stylize.ccblockload` | 5 | GPU | 32 | Implemented |
|
||||
| Brush Strokes | `ec.stylize.brushstrokes` | 7 | GPU | 32 | Implemented |
|
||||
| CC Block Load | `ec.stylize.ccblockload` | 5 | GPU | 32 | Implemented |
|
||||
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | GPU | 32 | Implemented |
|
||||
| CC Glass | `ec.stylize.ccglass` | 17 | GPU | 32 | Implemented |
|
||||
| CC HexTile | `ec.stylize.cchextile` | 5 | GPU | 32 | Implemented |
|
||||
| CC Kaleida | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented |
|
||||
| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | GPU | 32 | Implemented |
|
||||
| CC Plastic | `ec.stylize.ccplastic` | 18 | GPU | 32 | Implemented |
|
||||
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented |
|
||||
| CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented |
|
||||
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented |
|
||||
| CC Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented |
|
||||
| Cartoon | `ec.stylize.cartoon` | 12 | GPU | 32 | Implemented |
|
||||
| Channel Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented |
|
||||
| Color Emboss | `ec.stylize.coloremboss` | 4 | GPU | 32 | Implemented |
|
||||
| Edge Tile Repeat | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented |
|
||||
| Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented |
|
||||
| Film Burn | `ec.stylize.ccburnfilm` | 3 | GPU | 32 | Implemented |
|
||||
| Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented |
|
||||
| Glass Relief | `ec.stylize.ccglass` | 17 | GPU | 32 | Implemented |
|
||||
| Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented |
|
||||
| Hexagonal Tiles | `ec.stylize.cchextile` | 5 | GPU | 32 | Implemented |
|
||||
| Kaleidoscope | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented |
|
||||
| Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented |
|
||||
| Motion Tile | `ec.stylize.motiontile` | 8 | GPU | 32 | Implemented |
|
||||
| Palette Map | `ec.stylize.ccmrsmoothie` | 7 | GPU | 32 | Implemented |
|
||||
| Plastic Shading | `ec.stylize.ccplastic` | 18 | GPU | 32 | Implemented |
|
||||
| Posterize | `ec.stylize.posterize` | 1 | GPU | 32 | Implemented |
|
||||
| RGB Threshold | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented |
|
||||
| Roughen Edges | `ec.stylize.roughenedges` | 13 | GPU | 32 | Implemented |
|
||||
| Scatter | `ec.stylize.scatter` | 3 | GPU | 32 | Implemented |
|
||||
| Strobe Light | `ec.stylize.strobe` | 7 | GPU | 32 | Implemented |
|
||||
| Texturize | `ec.stylize.texturize` | 4 | GPU | 32 | Implemented |
|
||||
| Threshold | `ec.stylize.threshold` | 1 | GPU | 32 | Implemented |
|
||||
| Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented |
|
||||
|
||||
## Text
|
||||
|
||||
@@ -379,11 +379,11 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | GPU | 32 | Implemented |
|
||||
| CC Wide Time | `ec.time.ccwidetime` | 3 | GPU | 32 | Implemented |
|
||||
| Echo | `ec.time.echo` | 5 | GPU | 32 | Implemented |
|
||||
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | GPU | 32 | Implemented |
|
||||
| Posterize Time | `ec.time.posterizetime` | 1 | GPU | 32 | Implemented |
|
||||
| Temporal Blend | `ec.time.ccwidetime` | 3 | GPU | 32 | Implemented |
|
||||
| Temporal Motion Blur | `ec.time.ccforcemotionblur` | 4 | GPU | 32 | Implemented |
|
||||
| Time Difference | `ec.time.timedifference` | 5 | GPU | 32 | Implemented |
|
||||
| Time Displacement | `ec.time.timedisplacement` | 4 | GPU | 32 | Implemented |
|
||||
| Timewarp | `ec.time.timewarp` | 28 | GPU | 32 | Implemented |
|
||||
@@ -393,29 +393,28 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Block Dissolve | `ec.transition.blockdissolve` | 5 | GPU | 32 | Implemented |
|
||||
| CC Glass Wipe | `ec.transition.ccglasswipe` | 5 | GPU | 32 | Implemented |
|
||||
| CC Grid Wipe | `ec.transition.ccgridwipe` | 7 | GPU | 32 | Implemented |
|
||||
| CC Image Wipe | `ec.transition.ccimagewipe` | 7 | GPU | 32 | Implemented |
|
||||
| CC Jaws | `ec.transition.ccjaws` | 6 | GPU | 32 | Implemented |
|
||||
| CC Light Wipe | `ec.transition.cclightwipe` | 8 | GPU | 32 | Implemented |
|
||||
| CC Line Sweep | `ec.transition.cclinesweep` | 5 | GPU | 32 | Implemented |
|
||||
| CC Radial ScaleWipe | `ec.transition.ccradialscalewipe` | 3 | GPU | 32 | Implemented |
|
||||
| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | GPU | 32 | Implemented |
|
||||
| CC Twister | `ec.transition.cctwister` | 5 | GPU | 32 | Implemented |
|
||||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | GPU | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 44 | GPU | 32 | Implemented |
|
||||
| Glass Wipe | `ec.transition.ccglasswipe` | 5 | GPU | 32 | Implemented |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Grid Wipe | `ec.transition.ccgridwipe` | 7 | GPU | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | GPU | 32 | Implemented |
|
||||
| Light Wipe | `ec.transition.cclightwipe` | 8 | GPU | 32 | Implemented |
|
||||
| Line Sweep | `ec.transition.cclinesweep` | 5 | GPU | 32 | Implemented |
|
||||
| Linear Wipe | `ec.transition.linearwipe` | 3 | GPU | 32 | Implemented |
|
||||
| Luminance Image Wipe | `ec.transition.ccimagewipe` | 7 | GPU | 32 | Implemented |
|
||||
| Radial Scale Wipe | `ec.transition.ccradialscalewipe` | 3 | GPU | 32 | Implemented |
|
||||
| Radial Wipe | `ec.transition.radialwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Scale Wipe | `ec.transition.ccscalewipe` | 3 | GPU | 32 | Implemented |
|
||||
| Serrated Wipe | `ec.transition.ccjaws` | 6 | GPU | 32 | Implemented |
|
||||
| Twist Wipe | `ec.transition.cctwister` | 5 | GPU | 32 | Implemented |
|
||||
| Venetian Blinds | `ec.transition.venetian` | 4 | GPU | 32 | Implemented |
|
||||
| Warp Dissolve | `ec.transition.ccwarpomatic` | 8 | GPU | 32 | Implemented |
|
||||
|
||||
## Utility
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Apply Color LUT | `ec.utility.applylut` | 1 | GPU | 32 | Implemented |
|
||||
| CC Overbrights | `ec.utility.ccoverbrights` | 2 | GPU | 32 | Implemented |
|
||||
| Cineon Converter | `ec.utility.cineon` | 7 | GPU | 32 | Implemented |
|
||||
| Color Profile Converter | `ec.utility.colorprofileconverter` | 7 | GPU | 32 | Implemented |
|
||||
| Face Measurements | `ec.utility.facemeasurements` | 14 | | 32 | Implemented |
|
||||
@@ -423,4 +422,5 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Grow Bounds | `ec.utility.growbounds` | 1 | GPU | 32 | Implemented |
|
||||
| HDR Compander | `ec.utility.hdrcompander` | 3 | GPU | 32 | Implemented |
|
||||
| HDR Highlight Compression | `ec.utility.hdrcompression` | 1 | GPU | 32 | Implemented |
|
||||
| Out-of-Range Display | `ec.utility.ccoverbrights` | 2 | GPU | 32 | Implemented |
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Descriptive feature names
|
||||
|
||||
Related to #1365. This is a naming cleanup, not a trademark clearance or a product rename.
|
||||
|
||||
## Naming policy
|
||||
|
||||
Use short, descriptive labels for EffectCraft implementations. Do not reuse a vendor prefix or
|
||||
coined feature name as our branding. Qualify similar effects by what distinguishes them; each
|
||||
English display name remains unique across the registry.
|
||||
|
||||
- Renderer: **GPU Acceleration** / **Software Rendering**.
|
||||
- Panel: **Video Scopes**.
|
||||
- The table below covers the 69 former CC effects plus color grading, the shape matte and keying.
|
||||
|
||||
## Compatibility contract
|
||||
|
||||
Only presentation names change. Effect IDs, command IDs, parameter IDs, enum values, panel IDs,
|
||||
serialized types and AEP import/export match names stay unchanged. Old effect display names
|
||||
remain case-insensitive lookup and search aliases. Existing project instance names, custom names,
|
||||
expressions, animation and values are not rewritten. The UI displays a current label for an exact
|
||||
legacy default name, optionally followed by its numeric instance suffix; all other names stay
|
||||
verbatim. A user-entered name identical to a legacy default cannot be distinguished from it, but
|
||||
its saved value is still retained.
|
||||
|
||||
External format specifications, comparison documents, attribution records and compatibility
|
||||
fixtures may still name the external product or effect they actually describe.
|
||||
|
||||
## Display mapping
|
||||
|
||||
| Stable ID | Previous display name | Current display name |
|
||||
|---|---|---|
|
||||
| `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 |
|
||||
|
||||
`Roto Brush & Refine Edge` and `Warp Stabilizer` keep their current names; renaming them is
|
||||
deferred.
|
||||
|
||||
No effect algorithms or formats are changed.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user