mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #754 from vangel76/gap-surface-colors
#315: Front / Bevel / Side / Back Color for extruded shapes and text # Conflicts: # docs/gaps.md
This commit is contained in:
commit
261a5b125b
15 files changed
+413
-44
No files matched your search
@@ -4,7 +4,7 @@ use effectcraft_color::{BlendMode, Label};
|
||||
use effectcraft_keyframe::{Justify, TextDoc, Value as KV};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{
|
||||
Comp, FrameBlend, GroupKind, ItemId, ItemKind, Layer, LayerId, LayerSource, MaskMode, MatteKind, Project, PropGroup, Quality, Solid, TrackMatte,
|
||||
Comp, FrameBlend, GroupKind, ItemId, ItemKind, Layer, LayerId, LayerSource, MaskMode, MatteKind, Node, Project, PropGroup, Quality, Solid, TrackMatte,
|
||||
};
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::{Value, json};
|
||||
@@ -1186,6 +1186,26 @@ fn add_shape_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let uid = s.edit("Add Shape Item", None, |proj, _| {
|
||||
let mut next = proj.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
// Front / Bevel / Side / Back ▸ property: into the group's surface item (one per group).
|
||||
if let Some((surface, prop)) = build::shape_surface_kind(&kind) {
|
||||
let target = shape_item_target(proj, cid, lid, group_uid)?;
|
||||
let item = match target.children.iter().position(|c| matches!(c, Node::Group(g) if g.match_id == surface)) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
let g = build::shape_surface(&mut ids, surface).ok_or_else(|| bad("layer.addShapeItem", format!("unknown kind `{kind}`")))?;
|
||||
target.children.push(g.into());
|
||||
target.children.len() - 1
|
||||
}
|
||||
};
|
||||
let Some(Node::Group(g)) = target.children.get_mut(item) else { return Err(bad("layer.addShapeItem", "no such group")) };
|
||||
if g.get(prop).is_none() {
|
||||
let p = build::shape_surface_prop(&mut ids, prop).ok_or_else(|| bad("layer.addShapeItem", format!("unknown kind `{kind}`")))?;
|
||||
g.children.push(p.into());
|
||||
}
|
||||
let uid = g.uid;
|
||||
proj.next_id = next;
|
||||
return Ok(uid);
|
||||
}
|
||||
let g = match kind.as_str() {
|
||||
"group" => build::shape_group(&mut ids, "Group 1", vec![]),
|
||||
"rect" | "rectangle" => build::shape_rect(&mut ids, [200.0, 200.0], [0.0, 0.0], 0.0),
|
||||
@@ -1203,16 +1223,7 @@ fn add_shape_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let uid = g.uid;
|
||||
proj.next_id = next;
|
||||
let l = layer_mut(proj, cid, lid)?;
|
||||
let contents = l.props.sub_mut("contents").ok_or_else(|| bad("layer.addShapeItem", "not a shape layer"))?;
|
||||
let target = match group_uid {
|
||||
Some(u) => contents
|
||||
.find_group_mut(u)
|
||||
.and_then(|g| if g.match_id == "contents" { Some(g) } else { g.sub_mut("contents") })
|
||||
.ok_or_else(|| bad("layer.addShapeItem", "no such group"))?,
|
||||
None => contents,
|
||||
};
|
||||
target.children.push(g.into());
|
||||
shape_item_target(proj, cid, lid, group_uid)?.children.push(g.into());
|
||||
Ok(uid)
|
||||
})?;
|
||||
// The new item's property path, ready for prop.set / add_keyframe (`contents/trim#2/end`).
|
||||
@@ -1220,6 +1231,20 @@ fn add_shape_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"uid": uid, "path": path}))
|
||||
}
|
||||
|
||||
/// The contents a new shape item goes into: the shape group `group_uid` (its Contents), else the
|
||||
/// layer's Contents.
|
||||
fn shape_item_target(proj: &mut Project, cid: ItemId, lid: LayerId, group_uid: Option<u64>) -> Result<&mut PropGroup> {
|
||||
let l = layer_mut(proj, cid, lid)?;
|
||||
let contents = l.props.sub_mut("contents").ok_or_else(|| bad("layer.addShapeItem", "not a shape layer"))?;
|
||||
Ok(match group_uid {
|
||||
Some(u) => contents
|
||||
.find_group_mut(u)
|
||||
.and_then(|g| if g.match_id == "contents" { Some(g) } else { g.sub_mut("contents") })
|
||||
.ok_or_else(|| bad("layer.addShapeItem", "no such group"))?,
|
||||
None => contents,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, lid) = layer_p(s, p, "layer.setText")?;
|
||||
// The Character panel's recent fonts (Settings ▸ Type ▸ Number of Recent Fonts to Display).
|
||||
@@ -1570,7 +1595,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add (Shape)",
|
||||
[],
|
||||
None,
|
||||
"{layer?, kind: group|rect|ellipse|star|polygon|path|fill|stroke|gfill|gstroke|trim|repeater|round|offset|pucker|twist|zigzag|wiggle|merge, group?: uid|path} → {uid, path}",
|
||||
"{layer?, kind: group|rect|ellipse|star|polygon|path|fill|stroke|gfill|gstroke|trim|repeater|round|offset|pucker|twist|zigzag|wiggle|merge|frontColor|bevelColor|sideColor|backColor, group?: uid|path} → {uid, path} (frontColor…backColor: the group's Front / Bevel / Side / Back item, which colours that surface of every extruded paint in the group in Advanced 3D)",
|
||||
has_layers,
|
||||
add_shape_item
|
||||
),
|
||||
|
||||
@@ -284,7 +284,7 @@ fn free_transform(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
const SHAPE_ITEMS: &[&str] = &[
|
||||
"group", "rect", "ellipse", "star", "path", "fill", "stroke", "gfill", "gstroke", "trim", "repeater", "round", "offset", "pucker", "twist", "zigzag",
|
||||
"wiggle", "merge",
|
||||
"wiggle", "merge", "front", "bevel", "side", "back",
|
||||
];
|
||||
|
||||
/// Selected shape items (contents entries) of shape layers: (layer, uid).
|
||||
|
||||
@@ -431,7 +431,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Animate Text",
|
||||
[],
|
||||
None,
|
||||
"{layer?, properties: [anchor|position|scale|skew|rotation|opacity|transformAll|lineAnchor|lineSpacing|characterOffset|characterValue|blur|fillColor|fillHue|fillSaturation|fillBrightness|fillOpacity|strokeColor|strokeHue|strokeSaturation|strokeBrightness|strokeOpacity|strokeWidth|tracking|perChar3d], name?}",
|
||||
"{layer?, properties: [anchor|position|scale|skew|rotation|opacity|transformAll|lineAnchor|lineSpacing|characterOffset|characterValue|blur|fillColor|fillHue|fillSaturation|fillBrightness|fillOpacity|strokeColor|strokeHue|strokeSaturation|strokeBrightness|strokeOpacity|strokeWidth|tracking|frontColor|bevelColor|sideColor|backColor|perChar3d], name?}",
|
||||
has_text_layer,
|
||||
add_animator
|
||||
),
|
||||
|
||||
@@ -1022,6 +1022,14 @@ Animation
|
||||
Brightness | layer.addTextAnimator {"properties":["strokeBrightness"]}
|
||||
Opacity | layer.addTextAnimator {"properties":["strokeOpacity"]}
|
||||
Stroke Width | layer.addTextAnimator {"properties":["strokeWidth"]}
|
||||
Front
|
||||
Color | layer.addTextAnimator {"properties":["frontColor"]}
|
||||
Bevel
|
||||
Color | layer.addTextAnimator {"properties":["bevelColor"]}
|
||||
Side
|
||||
Color | layer.addTextAnimator {"properties":["sideColor"]}
|
||||
Back
|
||||
Color | layer.addTextAnimator {"properties":["backColor"]}
|
||||
---
|
||||
Tracking | layer.addTextAnimator {"properties":["tracking"]}
|
||||
Line Anchor | layer.addTextAnimator {"properties":["lineAnchor"]}
|
||||
|
||||
@@ -151,6 +151,44 @@ fn renderer_switch_adds_geometry_options() {
|
||||
assert_eq!(layer(&s, t).props.prop("geometryOptions/extrusionDepth").unwrap().value.as_f64(), 40.0);
|
||||
}
|
||||
|
||||
/// Contents ▸ Add ▸ Front / Bevel / Side / Back ▸ Color (#315): one surface item per group,
|
||||
/// reused by later adds, one undo step each; text gets the same as animator properties.
|
||||
#[test]
|
||||
fn surface_colors_for_extruded_shapes_and_text() {
|
||||
let mut s = session();
|
||||
s.execute("comp.renderer", json!({"renderer": "advanced3d"})).unwrap();
|
||||
let sh = s.execute("layer.newShape", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
let r = s.execute("layer.addShapeItem", json!({"layer": sh, "kind": "sideColor"})).unwrap();
|
||||
assert_eq!(r["path"], "contents/side");
|
||||
let side = r["uid"].as_u64().unwrap();
|
||||
s.execute("prop.set", json!({"layer": sh, "path": "contents/side/color", "value": [0.0, 1.0, 0.0, 1.0]})).unwrap();
|
||||
// Adding Side ▸ Color again reuses the item (and keeps its colour).
|
||||
assert_eq!(s.execute("layer.addShapeItem", json!({"layer": sh, "kind": "sideColor"})).unwrap()["uid"].as_u64(), Some(side));
|
||||
let l = layer(&s, sh);
|
||||
let contents = l.props.sub("contents").unwrap();
|
||||
assert_eq!(contents.groups().filter(|g| g.match_id == "side").count(), 1);
|
||||
assert_eq!(l.props.prop("contents/side/color").unwrap().value, KV::Color([0.0, 1.0, 0.0, 1.0]));
|
||||
// Into a shape group.
|
||||
s.execute("layer.addShapeItem", json!({"layer": sh, "kind": "group"})).unwrap();
|
||||
let r = s.execute("layer.addShapeItem", json!({"layer": sh, "kind": "frontColor", "group": "contents/group"})).unwrap();
|
||||
assert_eq!(r["path"], "contents/group/contents/front");
|
||||
assert!(s.execute("layer.addShapeItem", json!({"layer": sh, "kind": "sideShininess"})).is_err());
|
||||
s.undo();
|
||||
assert!(layer(&s, sh).props.group("contents/group/contents/front").is_none());
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
s.execute("prop.set", json!({"layer": sh, "path": "geometryOptions/extrusionDepth", "value": 40})).unwrap();
|
||||
let _ = s.render(cid, s.time(), Default::default());
|
||||
// Text: Animate ▸ Front / Bevel / Side / Back ▸ Color.
|
||||
let t = s.execute("layer.newText", json!({"text": "Hi"})).unwrap()["layer"].as_u64().unwrap();
|
||||
for k in ["frontColor", "bevelColor", "sideColor", "backColor"] {
|
||||
s.execute("layer.addTextAnimator", json!({"layer": t, "property": k})).unwrap();
|
||||
}
|
||||
let l = layer(&s, t);
|
||||
assert!(l.props.prop("text/animators/#3/properties/sideColor").is_some());
|
||||
s.execute("prop.set", json!({"layer": t, "path": "geometryOptions/extrusionDepth", "value": 20})).unwrap();
|
||||
let _ = s.render(cid, s.time(), Default::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_layer_toggle() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -203,9 +203,44 @@ fn rings(style: BevelStyle, bevel: f64, depth: f64) -> Vec<(f64, f64)> {
|
||||
out
|
||||
}
|
||||
|
||||
/// A surface of an extruded mesh, which After Effects colours and shades separately (the
|
||||
/// Front / Bevel / Side / Back material groups).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Surface {
|
||||
/// The front cap (facing the camera, on the layer plane).
|
||||
Front,
|
||||
/// The bevelled rings between the caps and the walls (front and back).
|
||||
Bevel,
|
||||
/// The straight walls.
|
||||
Side,
|
||||
/// The back cap.
|
||||
Back,
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
pub const ALL: [Surface; 4] = [Surface::Front, Surface::Bevel, Surface::Side, Surface::Back];
|
||||
/// Index into [`Surface::ALL`] (and into the primitives of [`extrude_surfaces`]).
|
||||
pub fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the extruded mesh of outlines (one primitive, material 0).
|
||||
pub fn extrude(outlines: &[Outline], p: &ExtrudeParams) -> Primitive {
|
||||
let mut prim = Primitive { material: Some(0), ..Default::default() };
|
||||
build(outlines, p, false).into_iter().next().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// [`extrude`] split by surface: one primitive (material 0) per [`Surface`], in
|
||||
/// [`Surface::ALL`] order. Together they hold the same triangles as [`extrude`].
|
||||
pub fn extrude_surfaces(outlines: &[Outline], p: &ExtrudeParams) -> [Primitive; 4] {
|
||||
let mut v = build(outlines, p, true).into_iter();
|
||||
std::array::from_fn(|_| v.next().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// The extruded mesh in one primitive (`split` false) or one per [`Surface`].
|
||||
fn build(outlines: &[Outline], p: &ExtrudeParams, split: bool) -> Vec<Primitive> {
|
||||
let mut prims = vec![Primitive { material: Some(0), ..Default::default() }; if split { 4 } else { 1 }];
|
||||
let slot = |s: Surface| if split { s.index() } else { 0 };
|
||||
let depth = p.depth.max(1e-3);
|
||||
let push = |prim: &mut Primitive, pos: [f64; 3], n: [f64; 3]| -> u32 {
|
||||
prim.positions.push([pos[0] as f32, pos[1] as f32, pos[2] as f32]);
|
||||
@@ -271,10 +306,11 @@ pub fn extrude(outlines: &[Outline], p: &ExtrudeParams) -> Primitive {
|
||||
};
|
||||
let (ni, nj) = (vn(i), vn(j));
|
||||
let nrm = |xy: P, pr: (f64, f64)| [xy[0] * pr.0, xy[1] * pr.0, pr.1];
|
||||
let ai = push(&mut prim, ring_pos[ci][k][i], nrm(ni, pa));
|
||||
let aj = push(&mut prim, ring_pos[ci][k][j], nrm(nj, pa));
|
||||
let bj = push(&mut prim, ring_pos[ci][k + 1][j], nrm(nj, pb));
|
||||
let bi = push(&mut prim, ring_pos[ci][k + 1][i], nrm(ni, pb));
|
||||
let Some(prim) = prims.get_mut(slot(if is_wall { Surface::Side } else { Surface::Bevel })) else { continue };
|
||||
let ai = push(prim, ring_pos[ci][k][i], nrm(ni, pa));
|
||||
let aj = push(prim, ring_pos[ci][k][j], nrm(nj, pa));
|
||||
let bj = push(prim, ring_pos[ci][k + 1][j], nrm(nj, pb));
|
||||
let bi = push(prim, ring_pos[ci][k + 1][i], nrm(ni, pb));
|
||||
prim.indices.extend([ai, aj, bj, ai, bj, bi]);
|
||||
}
|
||||
}
|
||||
@@ -286,10 +322,11 @@ pub fn extrude(outlines: &[Outline], p: &ExtrudeParams) -> Primitive {
|
||||
let tris = triangulate(&outer2, &holes2);
|
||||
let flat: Vec<(usize, usize)> = (0..cs.len()).flat_map(|ci| (0..cs[ci].pts.len()).map(move |i| (ci, i))).collect();
|
||||
for (front, nz) in [(true, -1.0), (false, 1.0)] {
|
||||
let Some(prim) = prims.get_mut(slot(if front { Surface::Front } else { Surface::Back })) else { continue };
|
||||
let base = prim.positions.len() as u32;
|
||||
for &(ci, i) in &flat {
|
||||
let r = if front { 0 } else { ring_pos[ci].len() - 1 };
|
||||
push(&mut prim, ring_pos[ci][r][i], [0.0, 0.0, nz]);
|
||||
push(prim, ring_pos[ci][r][i], [0.0, 0.0, nz]);
|
||||
}
|
||||
for t in &tris {
|
||||
let (a, b, c) = (base + t[0] as u32, base + t[1] as u32, base + t[2] as u32);
|
||||
@@ -297,7 +334,7 @@ pub fn extrude(outlines: &[Outline], p: &ExtrudeParams) -> Primitive {
|
||||
}
|
||||
}
|
||||
}
|
||||
prim
|
||||
prims
|
||||
}
|
||||
|
||||
/// Gap (pixels) between stacked extrusions of one layer: paint drawn later (a stroke over its
|
||||
@@ -309,9 +346,22 @@ pub const STACK_GAP: f64 = 0.02;
|
||||
/// `level × STACK_GAP` towards the camera (−z).
|
||||
pub fn extrude_stacked(outlines: &[Outline], p: &ExtrudeParams, level: usize) -> Primitive {
|
||||
let mut prim = extrude(outlines, p);
|
||||
stack(&mut prim, level);
|
||||
prim
|
||||
}
|
||||
|
||||
/// [`extrude_surfaces`] at stacking level `level` (see [`extrude_stacked`]).
|
||||
pub fn extrude_surfaces_stacked(outlines: &[Outline], p: &ExtrudeParams, level: usize) -> [Primitive; 4] {
|
||||
let mut prims = extrude_surfaces(outlines, p);
|
||||
for prim in &mut prims {
|
||||
stack(prim, level);
|
||||
}
|
||||
prims
|
||||
}
|
||||
|
||||
fn stack(prim: &mut Primitive, level: usize) {
|
||||
let dz = (level as f64 * STACK_GAP) as f32;
|
||||
for v in &mut prim.positions {
|
||||
v[2] -= dz;
|
||||
}
|
||||
prim
|
||||
}
|
||||
@@ -308,6 +308,35 @@ fn bevels_shrink_the_caps() {
|
||||
assert!(c.positions.len() > m.positions.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extrusions_split_into_front_bevel_side_and_back() {
|
||||
use super::extrude::{Surface, extrude_surfaces, extrude_surfaces_stacked};
|
||||
let square = Outline { outer: vec![[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [0.0, 100.0]], holes: vec![] };
|
||||
let p = ExtrudeParams { bevel: BevelStyle::Angular, bevel_depth: 10.0, hole_bevel: 1.0, depth: 50.0 };
|
||||
let whole = extrude(std::slice::from_ref(&square), &p);
|
||||
let parts = extrude_surfaces(std::slice::from_ref(&square), &p);
|
||||
// The parts hold exactly the triangles of the whole mesh.
|
||||
assert_eq!(parts.iter().map(|m| m.triangle_count()).sum::<usize>(), whole.triangle_count());
|
||||
let [front, bevel, side, back] = &parts;
|
||||
// Caps: flat, at the layer plane (front, facing −z) and at the extrusion depth (back, +z).
|
||||
assert!(front.positions.iter().all(|q| q[2] == 0.0) && front.normals.iter().all(|n| n[2] < -0.99));
|
||||
assert!(back.positions.iter().all(|q| q[2] == 50.0) && back.normals.iter().all(|n| n[2] > 0.99));
|
||||
// Walls: the full outline, between the bevels (z 10..40), with sideways normals.
|
||||
assert!(side.positions.iter().all(|q| (10.0..=40.0).contains(&q[2])) && side.normals.iter().all(|n| n[2].abs() < 1e-6));
|
||||
assert!(side.positions.iter().any(|q| q[0] == 0.0));
|
||||
// Bevels: slanted between the caps and the walls, front and back.
|
||||
assert!(bevel.triangle_count() > 0 && bevel.normals.iter().all(|n| n[2].abs() > 0.1 && n[2].abs() < 0.99));
|
||||
assert!(bevel.positions.iter().any(|q| q[2] == 0.0) && bevel.positions.iter().any(|q| q[2] == 50.0));
|
||||
assert_eq!(Surface::ALL.map(Surface::index), [0, 1, 2, 3]);
|
||||
// Without a bevel there is nothing on the Bevel surface.
|
||||
let flat = extrude_surfaces(std::slice::from_ref(&square), &ExtrudeParams { bevel: BevelStyle::None, ..p });
|
||||
assert_eq!(flat[Surface::Bevel.index()].triangle_count(), 0);
|
||||
assert!(flat[Surface::Side.index()].triangle_count() > 0);
|
||||
// Stacked like `extrude_stacked`.
|
||||
let up = extrude_surfaces_stacked(std::slice::from_ref(&square), &p, 2);
|
||||
assert!(up[0].positions.iter().all(|q| (q[2] + 2.0 * super::extrude::STACK_GAP as f32).abs() < 1e-6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contours_group_into_outlines_by_nesting() {
|
||||
let sq = |x: f64, y: f64, s: f64| vec![[x, y], [x + s, y], [x + s, y + s], [x, y + s]];
|
||||
|
||||
@@ -265,6 +265,11 @@ pub const TEXT_ANIMATOR_KINDS: &[(&str, &str)] = &[
|
||||
("strokeOpacity", "Stroke Color: Opacity"),
|
||||
("strokeWidth", "Stroke Width"),
|
||||
("tracking", "Tracking"),
|
||||
("-", "-"),
|
||||
("frontColor", "Front: Color"),
|
||||
("bevelColor", "Bevel: Color"),
|
||||
("sideColor", "Side: Color"),
|
||||
("backColor", "Back: Color"),
|
||||
];
|
||||
|
||||
fn vec3_prop(ids: &mut Ids, m: &str, name: &str, v: [f64; 3], ui: ParamUi, three_d: bool) -> Property {
|
||||
@@ -318,6 +323,11 @@ pub fn text_anim_props(ids: &mut Ids, kind: &str, three_d: bool) -> Vec<Property
|
||||
"strokeBrightness" => vec![pct_slider(ids, "strokeBrightness", "Stroke Brightness", 0.0, -100.0)],
|
||||
"strokeOpacity" => vec![pct_slider(ids, "strokeOpacity", "Stroke Opacity", 100.0, 0.0)],
|
||||
"strokeWidth" => vec![ids.prop("strokeWidth", "Stroke Width", Value::Scalar(0.0)).with_ui(ParamUi::Pixels)],
|
||||
// Colours of the extruded characters' surfaces (Advanced 3D).
|
||||
"frontColor" => vec![ids.prop("frontColor", "Front Color", Value::Color([1.0, 0.0, 0.0, 1.0])).with_ui(ParamUi::Color)],
|
||||
"bevelColor" => vec![ids.prop("bevelColor", "Bevel Color", Value::Color([1.0, 0.0, 0.0, 1.0])).with_ui(ParamUi::Color)],
|
||||
"sideColor" => vec![ids.prop("sideColor", "Side Color", Value::Color([1.0, 0.0, 0.0, 1.0])).with_ui(ParamUi::Color)],
|
||||
"backColor" => vec![ids.prop("backColor", "Back Color", Value::Color([1.0, 0.0, 0.0, 1.0])).with_ui(ParamUi::Color)],
|
||||
"tracking" => {
|
||||
let tt = fixed(ids.prop("trackingType", "Tracking Type", Value::Enum(0)).with_ui(popup(&["Before & After", "Before", "After"])));
|
||||
vec![tt, ids.prop("tracking", "Tracking Amount", Value::Scalar(0.0))]
|
||||
@@ -530,6 +540,35 @@ pub fn shape_repeater(ids: &mut Ids, copies: f64, offset_pos: [f64; 2]) -> PropG
|
||||
.with(tr)
|
||||
}
|
||||
|
||||
/// Shape contents items that colour the surfaces of an extruded shape (Advanced 3D): (match
|
||||
/// id, name), in Front, Bevel, Side, Back order. Each applies to every paint of its group and
|
||||
/// the groups nested in it.
|
||||
pub const SHAPE_SURFACES: [(&str, &str); 4] = [("front", "Front"), ("bevel", "Bevel"), ("side", "Side"), ("back", "Back")];
|
||||
|
||||
/// The `layer.addShapeItem` kind of a surface property (`sideColor`): (surface match id,
|
||||
/// property match id).
|
||||
pub fn shape_surface_kind(kind: &str) -> Option<(&'static str, &'static str)> {
|
||||
let (surface, prop) = SHAPE_SURFACES.iter().find_map(|(m, _)| kind.strip_prefix(m).map(|rest| (*m, rest)))?;
|
||||
match prop {
|
||||
"Color" => Some((surface, "color")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A surface property of a shape surface item (see [`SHAPE_SURFACES`]).
|
||||
pub fn shape_surface_prop(ids: &mut Ids, prop: &str) -> Option<Property> {
|
||||
match prop {
|
||||
"color" => Some(ids.prop("color", "Color", Value::Color([1.0, 0.0, 0.0, 1.0])).with_ui(ParamUi::Color)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// An empty shape surface item (`front`, `bevel`, `side` or `back`).
|
||||
pub fn shape_surface(ids: &mut Ids, surface: &str) -> Option<PropGroup> {
|
||||
let (m, name) = SHAPE_SURFACES.iter().find(|(m, _)| *m == surface)?;
|
||||
Some(ids.group(m, name))
|
||||
}
|
||||
|
||||
pub fn shape_simple_op(ids: &mut Ids, kind: &str) -> Option<PropGroup> {
|
||||
let g = match kind {
|
||||
"round" => indexed(ids.group("round", "Round Corners 1")).with(ids.prop("radius", "Radius", Value::Scalar(10.0)).with_ui(ParamUi::Pixels)),
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
//! the paths of nested groups;
|
||||
//! - paint items (fills, strokes, gradient fills/strokes) paint the paths above them, *as modified
|
||||
//! by every operator in the stack* — a Trim Paths below a Stroke still trims it;
|
||||
//! - items higher in the list draw on top.
|
||||
//! - items higher in the list draw on top;
|
||||
//! - surface items (Front, Bevel, Side, Back) colour those surfaces of the Advanced 3D
|
||||
//! extrusion of every paint in their group and its nested groups (the innermost wins).
|
||||
|
||||
use effectcraft_color::{BlendMode, blend_pixel};
|
||||
use effectcraft_effects::Buf;
|
||||
@@ -53,8 +55,14 @@ struct Draw {
|
||||
xf: Mat3,
|
||||
opacity: f32,
|
||||
blend: BlendMode,
|
||||
/// Front / Bevel / Side / Back colours of the paint's extrusion (Advanced 3D).
|
||||
surfaces: SurfaceColors,
|
||||
}
|
||||
|
||||
/// Straight RGB colours of an extrusion's surfaces, by [`effectcraft_model::extrude::Surface`]
|
||||
/// index (Front, Bevel, Side, Back); `None` keeps the paint's colour.
|
||||
pub type SurfaceColors = [Option<[f32; 3]>; 4];
|
||||
|
||||
fn mat_path(path: &BezPath, m: &Mat3) -> BezPath {
|
||||
effectcraft_path::transform(std::slice::from_ref(path), m).pop().unwrap_or_default()
|
||||
}
|
||||
@@ -179,6 +187,7 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<B
|
||||
let mut draws: Vec<Draw> = Vec::new();
|
||||
let mut live: Vec<usize> = Vec::new();
|
||||
let opacity = |g: &PropGroup| (ctx.f(layer, g, "opacity", 100.0) / 100.0) as f32;
|
||||
let mut surfaces: SurfaceColors = [None; 4];
|
||||
for node in &contents.children {
|
||||
let Node::Group(g) = node else { continue };
|
||||
if !g.enabled {
|
||||
@@ -224,6 +233,7 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<B
|
||||
xf: Mat3::IDENTITY,
|
||||
opacity: opacity(g),
|
||||
blend: blend_mode(ctx, layer, g),
|
||||
surfaces: [None; 4],
|
||||
});
|
||||
}
|
||||
"stroke" => {
|
||||
@@ -236,6 +246,7 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<B
|
||||
xf: Mat3::IDENTITY,
|
||||
opacity: opacity(g),
|
||||
blend: blend_mode(ctx, layer, g),
|
||||
surfaces: [None; 4],
|
||||
});
|
||||
}
|
||||
"gfill" | "gstroke" => {
|
||||
@@ -258,6 +269,7 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<B
|
||||
xf: Mat3::IDENTITY,
|
||||
opacity: opacity(g),
|
||||
blend: blend_mode(ctx, layer, g),
|
||||
surfaces: [None; 4],
|
||||
});
|
||||
}
|
||||
"trim" => {
|
||||
@@ -386,7 +398,24 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<B
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
m => {
|
||||
if let Some(i) = effectcraft_project::build::SHAPE_SURFACES.iter().position(|(s, _)| *s == m)
|
||||
&& g.get("color").is_some()
|
||||
&& let Some(slot) = surfaces.get_mut(i)
|
||||
{
|
||||
let c = ctx.color(layer, g, "color");
|
||||
*slot = Some([c[0], c[1], c[2]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Surface items colour every paint of the group, wherever they sit in it; nested groups'
|
||||
// own surface items were applied first and win.
|
||||
if surfaces.iter().any(Option::is_some) {
|
||||
for d in &mut draws {
|
||||
for (s, g) in d.surfaces.iter_mut().zip(surfaces) {
|
||||
*s = s.or(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
(draws, live)
|
||||
@@ -622,8 +651,9 @@ pub fn fill_outlines(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup) -> Vec<
|
||||
|
||||
/// The painted regions of a shape layer for Advanced 3D extrusion, bottom paint first: fills
|
||||
/// (gradient fills: the colour half way along) and strokes, each stroke turned into the region
|
||||
/// it covers (outline of the stroke with its width, joins, caps and dashes, overlaps resolved).
|
||||
pub fn extrusion_outlines(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup) -> Vec<(Vec<BezPath>, [f32; 4])> {
|
||||
/// it covers (outline of the stroke with its width, joins, caps and dashes, overlaps resolved),
|
||||
/// with the colours of their extrusion's surfaces.
|
||||
pub fn extrusion_outlines(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup) -> Vec<(Vec<BezPath>, [f32; 4], SurfaceColors)> {
|
||||
let mut arena = Vec::new();
|
||||
let (draws, _) = collect(ctx, layer, contents, &mut arena);
|
||||
draws
|
||||
@@ -641,7 +671,7 @@ pub fn extrusion_outlines(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup) ->
|
||||
Paint::Stroke { color, style } => (*color, vec![stroke_region(&paths, style)?]),
|
||||
Paint::Gradient { g, stroke: Some(style), .. } => (g.sample(0.5), vec![stroke_region(&paths, style)?]),
|
||||
};
|
||||
Some((region, [c[0], c[1], c[2], c[3] * d.opacity]))
|
||||
Some((region, [c[0], c[1], c[2], c[3] * d.opacity], d.surfaces))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -58,9 +58,21 @@ pub struct CharXf {
|
||||
/// Variable Font Axes offsets (axis tag, user units), the first `n_axes` used.
|
||||
pub axes: [([u8; 4], f64); 4],
|
||||
pub n_axes: u8,
|
||||
/// Front / Bevel / Side / Back Color of the extruded character (Advanced 3D), by
|
||||
/// `effectcraft_model::extrude::Surface` index, and the selection each was set with.
|
||||
pub surface: [Option<[f32; 4]>; 4],
|
||||
pub surface_k: [f32; 4],
|
||||
}
|
||||
|
||||
impl CharXf {
|
||||
/// The surface colours of an extrusion painted `base` (the character's fill or stroke):
|
||||
/// each animated surface colour blended over `base` by its selection.
|
||||
pub fn surface_colors(&self, base: [f32; 4]) -> crate::shapes::SurfaceColors {
|
||||
std::array::from_fn(|i| {
|
||||
let (c, k) = (self.surface.get(i).copied().flatten()?, self.surface_k.get(i).copied().unwrap_or(0.0));
|
||||
Some(std::array::from_fn(|d| base[d] + (c[d] - base[d]) * k))
|
||||
})
|
||||
}
|
||||
/// The variable-font axis offsets that move the outline.
|
||||
pub fn axis_deltas(&self) -> Vec<(String, f32)> {
|
||||
self.axes[..self.n_axes as usize]
|
||||
@@ -79,6 +91,10 @@ impl CharXf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Animator property match ids of Front / Bevel / Side / Back Color, in
|
||||
/// `effectcraft_model::extrude::Surface::ALL` order.
|
||||
pub const SURFACE_ANIM_PROPS: [&str; 4] = ["frontColor", "bevelColor", "sideColor", "backColor"];
|
||||
|
||||
/// Animator property match id of a Variable Font Axes property (`axis_wght`).
|
||||
pub const AXIS_PREFIX: &str = "axis_";
|
||||
|
||||
@@ -115,6 +131,8 @@ impl Default for CharXf {
|
||||
blur: [0.0; 2],
|
||||
axes: [([0; 4], 0.0); 4],
|
||||
n_axes: 0,
|
||||
surface: [None; 4],
|
||||
surface_k: [0.0; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,6 +292,7 @@ pub fn char_transforms(ctx: &EvalCtx, layer: &Layer, text: &PropGroup, lay: &Tex
|
||||
let char_range = props.get("characterRange").map(|p| ctx.value(layer, p).as_enum());
|
||||
let char_align = props.get("characterAlignment").map(|p| ctx.value(layer, p).as_enum());
|
||||
let blur = props.get("blur").map(|p| ctx.value(layer, p).as_vec2());
|
||||
let surface = SURFACE_ANIM_PROPS.map(|m| props.get(m).map(|p| ctx.value(layer, p).as_color()));
|
||||
let axes: Vec<([u8; 4], f64)> = props
|
||||
.props()
|
||||
.filter_map(|p| {
|
||||
@@ -385,6 +404,12 @@ pub fn char_transforms(ctx: &EvalCtx, layer: &Layer, text: &PropGroup, lay: &Tex
|
||||
for (tag, v) in &axes {
|
||||
c.add_axis(*tag, v * k);
|
||||
}
|
||||
for ((s, sk), v) in c.surface.iter_mut().zip(&mut c.surface_k).zip(surface) {
|
||||
if let Some(v) = v {
|
||||
*s = Some(v);
|
||||
*sk = (*sk + k as f32).clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
|
||||
@@ -543,10 +543,11 @@ pub fn extrusion_params(ctx: &EvalCtx, layer: &Layer) -> Option<model::extrude::
|
||||
/// space). Fills and strokes are both extruded (a stroke as the region it covers, bevelled like
|
||||
/// the fill), stacked in the layer's paint order (see [`model::extrude::extrude_stacked`]).
|
||||
/// Text extrudes each character in its own space, so per-character 3D animation (position,
|
||||
/// rotation, scale in Z) carries over.
|
||||
/// rotation, scale in Z) carries over. A paint whose Front / Bevel / Side / Back are coloured
|
||||
/// (shape surface items, text animators) comes as one mesh per surface, in that order.
|
||||
pub fn extruded_meshes(ctx: &EvalCtx, layer: &Layer, p: &model::extrude::ExtrudeParams) -> Vec<(Primitive, [f32; 4], Mat4)> {
|
||||
// (paths, colour, mesh → layer, stacking level)
|
||||
let mut groups: Vec<(Vec<kurbo::BezPath>, [f32; 4], Mat4, usize)> = vec![];
|
||||
// (paths, colour, mesh → layer, stacking level, surface colours)
|
||||
let mut groups: Vec<(Vec<kurbo::BezPath>, [f32; 4], Mat4, usize, crate::shapes::SurfaceColors)> = vec![];
|
||||
match layer.source {
|
||||
LayerSource::Text => {
|
||||
if let Some(tg) = crate::text::text_geom(ctx, layer) {
|
||||
@@ -561,13 +562,13 @@ pub fn extruded_meshes(ctx: &EvalCtx, layer: &Layer, p: &model::extrude::Extrude
|
||||
let op = (g.xf.opacity / 100.0).clamp(0.0, 1.0) as f32;
|
||||
if g.apply_fill && g.fill[3] * op > 0.0 {
|
||||
let c = [g.fill[0], g.fill[1], g.fill[2], g.fill[3] * op];
|
||||
groups.push((vec![g.local.clone()], c, g.m, usize::from(!stroke_on_top)));
|
||||
groups.push((vec![g.local.clone()], c, g.m, usize::from(!stroke_on_top), g.xf.surface_colors(c)));
|
||||
}
|
||||
if g.stroke_width > 0.0 && g.stroke[3] * op > 0.0 {
|
||||
let st = effectcraft_path::StrokeStyle { width: g.stroke_width, join: effectcraft_path::Join::Round, ..Default::default() };
|
||||
if let Some(region) = crate::shapes::stroke_region(std::slice::from_ref(&g.local), &st) {
|
||||
let c = [g.stroke[0], g.stroke[1], g.stroke[2], g.stroke[3] * op];
|
||||
groups.push((vec![region], c, g.m, usize::from(stroke_on_top)));
|
||||
groups.push((vec![region], c, g.m, usize::from(stroke_on_top), g.xf.surface_colors(c)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,22 +576,32 @@ pub fn extruded_meshes(ctx: &EvalCtx, layer: &Layer, p: &model::extrude::Extrude
|
||||
}
|
||||
LayerSource::Shape => {
|
||||
if let Some(c) = layer.props.sub("contents") {
|
||||
groups = crate::shapes::extrusion_outlines(ctx, layer, c).into_iter().enumerate().map(|(k, (ps, c))| (ps, c, Mat4::IDENTITY, k)).collect();
|
||||
groups =
|
||||
crate::shapes::extrusion_outlines(ctx, layer, c).into_iter().enumerate().map(|(k, (ps, c, s))| (ps, c, Mat4::IDENTITY, k, s)).collect();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
groups
|
||||
.into_iter()
|
||||
.filter_map(|(paths, color, m, level)| {
|
||||
let cs: Vec<Vec<[f64; 2]>> = paths.iter().flat_map(|bp| contours(bp, 0.25)).collect();
|
||||
let outlines = model::extrude::group_contours(cs);
|
||||
if outlines.is_empty() {
|
||||
return None;
|
||||
let mut out = vec![];
|
||||
for (paths, color, m, level, surfaces) in groups {
|
||||
let cs: Vec<Vec<[f64; 2]>> = paths.iter().flat_map(|bp| contours(bp, 0.25)).collect();
|
||||
let outlines = model::extrude::group_contours(cs);
|
||||
if outlines.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if surfaces.iter().all(Option::is_none) {
|
||||
out.push((model::extrude::extrude_stacked(&outlines, p, level), color, m));
|
||||
continue;
|
||||
}
|
||||
for (mesh, s) in model::extrude::extrude_surfaces_stacked(&outlines, p, level).into_iter().zip(surfaces) {
|
||||
if mesh.indices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
Some((model::extrude::extrude_stacked(&outlines, p, level), color, m))
|
||||
})
|
||||
.collect()
|
||||
let c = s.map_or(color, |s| [s[0], s[1], s[2], color[3]]);
|
||||
out.push((mesh, c, m));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn extruded_layer(r: &Renderer, ctx: &EvalCtx, layer: &Layer, p: &model::extrude::ExtrudeParams, b: &mut Builder) {
|
||||
|
||||
@@ -313,6 +313,85 @@ fn extruded_text_renders_as_a_mesh() {
|
||||
assert!(img.data.iter().filter(|q| q[3] > 0.5).count() > 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_surface_items_colour_the_extruded_surfaces() {
|
||||
let (mut p, cid) = project(200, 200);
|
||||
add(&mut p, cid, LayerSource::Shape, |l| {
|
||||
l.switches.three_d = true;
|
||||
let mut next = 10_000u64;
|
||||
let ids = &mut build::Ids(&mut next);
|
||||
let surface = |ids: &mut build::Ids, m: &str, c: [f64; 4]| {
|
||||
let mut g = build::shape_surface(ids, m).unwrap();
|
||||
g.children.push(build::shape_surface_prop(ids, "color").unwrap().into());
|
||||
if let Some(effectcraft_project::Node::Prop(pr)) = g.children.last_mut() {
|
||||
pr.value = Value::Color(c);
|
||||
}
|
||||
g
|
||||
};
|
||||
// A white square whose group colours its sides green; the layer's Contents colour every
|
||||
// front blue and every side red, but the group's own Side wins.
|
||||
let rect = build::shape_rect(ids, [80.0, 80.0], [0.0, 0.0], 0.0);
|
||||
let fill = build::shape_fill(ids, [1.0, 1.0, 1.0, 1.0]);
|
||||
let side = surface(ids, "side", [0.0, 1.0, 0.0, 1.0]);
|
||||
let group = build::shape_group(ids, "Group 1", vec![rect, fill, side]);
|
||||
let front = surface(ids, "front", [0.0, 0.0, 1.0, 1.0]);
|
||||
let outer_side = surface(ids, "side", [1.0, 0.0, 0.0, 1.0]);
|
||||
let contents = l.props.sub_mut("contents").unwrap();
|
||||
contents.children.extend([group.into(), front.into(), outer_side.into()]);
|
||||
l.props.children.push(build::extrusion_geometry_options(ids).into());
|
||||
set(l, "geometryOptions/extrusionDepth", Value::Scalar(40.0));
|
||||
set(l, "transform/position", Value::Vec3([100.0, 100.0, 0.0]));
|
||||
});
|
||||
let ctx = crate::EvalCtx { project: &p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None };
|
||||
let l = &ctx.comp.layers[0];
|
||||
let params = scene::extrusion_params(&ctx, l).unwrap();
|
||||
let meshes = scene::extruded_meshes(&ctx, l, ¶ms);
|
||||
// No bevel: Front, Side and Back (the Back keeps the fill's white).
|
||||
let colours: Vec<[f32; 4]> = meshes.iter().map(|m| m.1).collect();
|
||||
assert_eq!(colours, [[0.0, 0.0, 1.0, 1.0], [0.0, 1.0, 0.0, 1.0], [1.0, 1.0, 1.0, 1.0]]);
|
||||
assert!(meshes[0].0.normals.iter().all(|n| n[2] < -0.99), "front cap");
|
||||
assert!(meshes[1].0.normals.iter().all(|n| n[2].abs() < 1e-6), "walls");
|
||||
// Rendered (unlit: base colours) at an angle: the blue face and the green side both show.
|
||||
p.comp_mut(cid).unwrap().layers[0].props.prop_mut("transform/rotationY").unwrap().value = Value::Scalar(50.0);
|
||||
let img = render(&p, cid);
|
||||
let blue = img.data.iter().filter(|q| q[3] > 0.99 && q[2] > 0.9 && q[0] < 0.1 && q[1] < 0.1).count();
|
||||
let green = img.data.iter().filter(|q| q[3] > 0.99 && q[1] > 0.9 && q[0] < 0.1 && q[2] < 0.1).count();
|
||||
assert!(blue > 200 && green > 50, "blue {blue}, green {green}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_animators_colour_the_extruded_surfaces() {
|
||||
let (mut p, cid) = project(200, 100);
|
||||
add(&mut p, cid, LayerSource::Text, |l| {
|
||||
l.switches.three_d = true;
|
||||
let doc = effectcraft_keyframe::TextDoc { text: "IO".into(), size: 60.0, fill: [1.0, 1.0, 1.0, 1.0], apply_fill: true, ..Default::default() };
|
||||
set(l, "text/sourceText", Value::Text(Box::new(doc)));
|
||||
let mut next = 10_000u64;
|
||||
let ids = &mut build::Ids(&mut next);
|
||||
let props = ["sideColor", "bevelColor"].iter().flat_map(|k| build::text_anim_props(ids, k, false)).collect();
|
||||
let anim = build::text_animator(ids, "Animator 1", props);
|
||||
l.props.group_mut("text/animators").unwrap().children.push(anim.into());
|
||||
set(l, "text/animators/#1/properties/sideColor", Value::Color([0.0, 1.0, 0.0, 1.0]));
|
||||
// Half the selection: the bevel colour blends half way over the fill.
|
||||
set(l, "text/animators/#1/selectors/#1/advanced/amount", Value::Scalar(50.0));
|
||||
l.props.children.push(build::extrusion_geometry_options(ids).into());
|
||||
set(l, "geometryOptions/extrusionDepth", Value::Scalar(30.0));
|
||||
set(l, "geometryOptions/bevelStyle", Value::Enum(1));
|
||||
});
|
||||
let ctx = crate::EvalCtx { project: &p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None };
|
||||
let l = &ctx.comp.layers[0];
|
||||
let params = scene::extrusion_params(&ctx, l).unwrap();
|
||||
let meshes = scene::extruded_meshes(&ctx, l, ¶ms);
|
||||
// Two characters × (Front, Bevel, Side, Back).
|
||||
assert_eq!(meshes.len(), 8);
|
||||
for m in meshes.chunks(4) {
|
||||
assert_eq!(m[0].1, [1.0, 1.0, 1.0, 1.0], "front keeps the fill");
|
||||
assert_eq!(m[1].1, [1.0, 0.5, 0.5, 1.0], "bevel: red half way over white");
|
||||
assert_eq!(m[2].1, [0.5, 1.0, 0.5, 1.0], "side: green half way over white");
|
||||
assert_eq!(m[3].1, [1.0, 1.0, 1.0, 1.0], "back keeps the fill");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- motion blur, blend modes, mattes
|
||||
|
||||
fn solid(p: &mut Project, color: [f32; 3], w: u32, h: u32) -> ItemId {
|
||||
|
||||
@@ -683,6 +683,11 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("layer.addTextAnimator", "Brightness", "亮度"),
|
||||
("", "Stroke Color", "描边颜色"),
|
||||
("layer.addTextAnimator", "Stroke Width", "描边宽度"),
|
||||
("", "Front", "正面"),
|
||||
("", "Bevel", "斜面"),
|
||||
("", "Side", "侧面"),
|
||||
("", "Back", "背面"),
|
||||
("layer.addTextAnimator", "Color", "颜色"),
|
||||
("layer.addTextAnimator", "Tracking", "字符间距"),
|
||||
("layer.addTextAnimator", "Line Anchor", "行锚点"),
|
||||
("layer.addTextAnimator", "Line Spacing", "行距"),
|
||||
@@ -1308,6 +1313,11 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("layer.addTextAnimator", "Brightness", "亮度"),
|
||||
("", "Stroke Color", "筆畫色彩"),
|
||||
("layer.addTextAnimator", "Stroke Width", "筆畫寬度"),
|
||||
("", "Front", "正面"),
|
||||
("", "Bevel", "斜面"),
|
||||
("", "Side", "側面"),
|
||||
("", "Back", "背面"),
|
||||
("layer.addTextAnimator", "Color", "顏色"),
|
||||
("layer.addTextAnimator", "Tracking", "字距"),
|
||||
("layer.addTextAnimator", "Line Anchor", "行錨點"),
|
||||
("layer.addTextAnimator", "Line Spacing", "行距"),
|
||||
@@ -1930,6 +1940,11 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.addTextAnimator", "Brightness", "明度"),
|
||||
("", "Stroke Color", "線のカラー"),
|
||||
("layer.addTextAnimator", "Stroke Width", "線幅"),
|
||||
("", "Front", "前面"),
|
||||
("", "Bevel", "ベベル"),
|
||||
("", "Side", "側面"),
|
||||
("", "Back", "背面"),
|
||||
("layer.addTextAnimator", "Color", "カラー"),
|
||||
("layer.addTextAnimator", "Tracking", "トラッキング"),
|
||||
("layer.addTextAnimator", "Line Anchor", "行のアンカー"),
|
||||
("layer.addTextAnimator", "Line Spacing", "行間"),
|
||||
@@ -2555,6 +2570,11 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.addTextAnimator", "Brightness", "Яскравість"),
|
||||
("", "Stroke Color", "Колір обведення"),
|
||||
("layer.addTextAnimator", "Stroke Width", "Товщина обведення"),
|
||||
("", "Front", "Передня грань"),
|
||||
("", "Bevel", "Фаска"),
|
||||
("", "Side", "Бічна грань"),
|
||||
("", "Back", "Задня грань"),
|
||||
("layer.addTextAnimator", "Color", "Колір"),
|
||||
("layer.addTextAnimator", "Tracking", "Міжлітерний інтервал"),
|
||||
("layer.addTextAnimator", "Line Anchor", "Опорна точка рядка"),
|
||||
("layer.addTextAnimator", "Line Spacing", "Міжрядковий інтервал"),
|
||||
@@ -3181,6 +3201,11 @@ const SPANISH: &[(&str, &str, &str)] = &[
|
||||
("layer.addTextAnimator", "Brightness", "Brillo"),
|
||||
("", "Stroke Color", "Color de trazo"),
|
||||
("layer.addTextAnimator", "Stroke Width", "Ancho de trazo"),
|
||||
("", "Front", "Frente"),
|
||||
("", "Bevel", "Bisel"),
|
||||
("", "Side", "Lado"),
|
||||
("", "Back", "Parte posterior"),
|
||||
("layer.addTextAnimator", "Color", "Color"),
|
||||
("layer.addTextAnimator", "Tracking", "Espaciado"),
|
||||
("layer.addTextAnimator", "Line Anchor", "Anclaje de línea"),
|
||||
("layer.addTextAnimator", "Line Spacing", "Interlineado"),
|
||||
@@ -3805,6 +3830,11 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
|
||||
("layer.addTextAnimator", "Brightness", "Brilho"),
|
||||
("", "Stroke Color", "Cor do traçado"),
|
||||
("layer.addTextAnimator", "Stroke Width", "Largura do traçado"),
|
||||
("", "Front", "Frente"),
|
||||
("", "Bevel", "Chanfro"),
|
||||
("", "Side", "Lateral"),
|
||||
("", "Back", "Verso"),
|
||||
("layer.addTextAnimator", "Color", "Cor"),
|
||||
("layer.addTextAnimator", "Tracking", "Tracking"),
|
||||
("layer.addTextAnimator", "Line Anchor", "Âncora da linha"),
|
||||
("layer.addTextAnimator", "Line Spacing", "Espaçamento entre linhas"),
|
||||
|
||||
@@ -3268,6 +3268,11 @@ const SHAPE_ADD_ITEMS: &[(&str, &str)] = &[
|
||||
("gfill", "Gradient Fill"),
|
||||
("gstroke", "Gradient Stroke"),
|
||||
("-", ""),
|
||||
("frontColor", "Front: Color"),
|
||||
("bevelColor", "Bevel: Color"),
|
||||
("sideColor", "Side: Color"),
|
||||
("backColor", "Back: Color"),
|
||||
("-", ""),
|
||||
("merge", "Merge Paths"),
|
||||
("offset", "Offset Paths"),
|
||||
("pucker", "Pucker & Bloat"),
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ are grouped by area; the issue tracker has the rest.
|
||||
| 9 | **Effect behaviour bugs** | #580, #526 Fractal reset, #514 Apply Color LUT, #463 Curl Noise, #369 Motion Tile output size, #473 Cryptomatte selection, #474 multi-layer EXR decoded twice, #534–#536 community fixes pending | Each one breaks a look someone built | 10–20 h | G1/G2 · [effects-parity.md](effects-parity.md) |
|
||||
| 10 | **Colour pipeline differences** | #494 32-bpc blend modes clamp and apply opacity differently; #495 Working Gamma 2.4 (in, also for float footage: #750); #340 ProRes 4444 tagged bt709 but sRGB-encoded; ACES 2.0 / OCIO 2.5 configs (26.5) missing; #546 / #547 viewer colour | Wrong colour in delivery is a client-visible failure | 15–30 h | G1 · [file-format-parity.md](file-format-parity.md) |
|
||||
| 11 | **Text rendering and layout** | #453 CJK text in comps, #316 paragraph alignment inverted, #483 squeezed at 4:3, #417 soft at Fit, #583 font search, #550 toolbar font options | Text is in almost every motion graphic | 10–20 h | G2 · [ui-parity.md](ui-parity.md) |
|
||||
| 12 | **3D interaction and depth** | No 3D transform gizmos (#392, #347); Z position (#489); camera aperture (#260); reflections (#261); bevel options (#315); 3D issue reopened (#307) | Advanced 3D is After Effects' growth area since 2023 | 20–35 h | G2 · [ui-parity.md](ui-parity.md) |
|
||||
| 12 | **3D interaction and depth** | No 3D transform gizmos (#392, #347); Z position (#489); camera aperture (#260); reflections (#261); extruded surface materials beyond Front / Bevel / Side / Back Color (#315); 3D issue reopened (#307) | Advanced 3D is After Effects' growth area since 2023 | 20–35 h | G2 · [ui-parity.md](ui-parity.md) |
|
||||
| 13 | **3D model formats and materials** | After Effects 26 imports FBX, USD/USDZ, STL besides glTF and OBJ (`usd_plugins`), and Substance 3D materials with mesh displacement (26.0–26.2); we read glTF, OBJ, STL (binary and ASCII, with facet colours) and FBX 7.x (binary and ASCII: meshes, hierarchy, Lambert/Phong materials and textures; no FBX animation or skinning yet) | Product shots and 3D scenes come as FBX/USDZ | 25–45 h | — · [file-format-parity.md](file-format-parity.md) |
|
||||
| 14 | **Object Matte** (26.2, disk cache in 26.5) | Not present; MobileSAM is already in `effectcraft-segment` and could back it | The new one-click roto; Roto Brush covers part of it | 15–30 h | G7 · [target-app-parity.md](target-app-parity.md) |
|
||||
| 15 | **Hardware video decode and encode** | No VideoToolbox, NVENC, Quick Sync or VA-API; #498 asks for it; FilmCraft has VideoToolbox / NVENC / VA-API paths to borrow | 4K playback and export speed | 25–45 h | G5/G6 · [hardware-parity.md](hardware-parity.md) |
|
||||
|
||||
Reference in new issue
Block a user