Merge pull request #1513 from vangel76/gap-behaviors-presets

#1510: Behaviors animation presets (22 original, expression-driven)
This commit is contained in:
BFlat
2026-10-30 01:26:43 +07:00
committed by GitHub
9 changed files with 332 additions and 8 deletions
+7 -2
View File
@@ -1,6 +1,7 @@
//! EffectCraft's own animation preset library (Effects & Presets ▸ * Animation Presets): the
//! Transitions category (original presets built on our Transition effects and layer opacity) and
//! the Backgrounds category (generators, noise and colour effects driven by expressions).
//! the Backgrounds category (generators, noise and colour effects driven by expressions) and the
//! Behaviors category (transform and opacity driven by expressions, no effects).
//!
//! Each preset adds effects (with values and keyframes) and/or keys layer properties, starting
//! at the current time, on every selected layer. The library is data
@@ -26,13 +27,17 @@ pub const TRANSITIONS_JSON: &str = include_str!("../../presets/transitions.json"
/// The original background presets (`presets/backgrounds.json`).
pub const BACKGROUNDS_JSON: &str = include_str!("../../presets/backgrounds.json");
/// The original behavior presets (`presets/behaviors.json`): expression-driven motion on a layer's
/// transform and opacity, no effects.
pub const BEHAVIORS_JSON: &str = include_str!("../../presets/behaviors.json");
/// Keys per animated property, so a hostile inline preset can't allocate without bound.
const MAX_KEYS: usize = 256;
/// Longest expression text an inline preset may carry.
const MAX_EXPR: usize = 4096;
fn presets() -> Vec<Value> {
[TRANSITIONS_JSON, BACKGROUNDS_JSON]
[TRANSITIONS_JSON, BACKGROUNDS_JSON, BEHAVIORS_JSON]
.iter()
.flat_map(|src| serde_json::from_str::<Value>(src).ok().and_then(|v| v.get("presets").and_then(Value::as_array).cloned()).unwrap_or_default())
.collect()
+33 -2
View File
@@ -78,7 +78,7 @@ fn transition_preset_by_name_and_bad_input() {
#[test]
fn preset_catalogue_is_unique_and_categorised() {
let all = crate::animation_presets();
let known = ["Transitions", "Backgrounds"];
let known = ["Transitions", "Backgrounds", "Behaviors"];
let mut ids = std::collections::HashSet::new();
let mut names = std::collections::HashSet::new();
for (id, name, cat) in &all {
@@ -90,7 +90,7 @@ fn preset_catalogue_is_unique_and_categorised() {
assert!(!ids.contains(&id.to_lowercase()) && !names.contains(&name.to_lowercase()), "text preset {id} clashes");
}
let n = |c: &str| all.iter().filter(|(_, _, k)| k == c).count();
assert!(n("Backgrounds") >= 15 && n("Transitions") >= 20);
assert!(n("Backgrounds") >= 15 && n("Transitions") >= 20 && n("Behaviors") >= 20);
}
#[test]
@@ -126,3 +126,34 @@ fn preset_expressions_and_point_fractions() {
assert_eq!(ramp.prop("end").unwrap().value, effectcraft_keyframe::Value::Vec2([80.0, 90.0]));
assert_eq!(ramp.prop("startColor").unwrap().keys.len(), 2);
}
/// #1510: Behaviors presets add no effects; each puts an enabled expression on a transform
/// property, and undo removes it again.
#[test]
fn every_behavior_preset_applies_and_undoes() {
let (mut s, solid) = session();
let list = s.execute("anim.builtinPresets", json!({"category": "Behaviors"})).unwrap();
let list = list.as_array().unwrap().clone();
assert!(list.len() >= 20, "{}", list.len());
let driven = |s: &Session| {
let l = &s.active_comp().unwrap().layers[0];
["transform/position", "transform/scale", "transform/rotation", "transform/opacity"]
.iter()
.filter(|p| l.props.prop(p).is_some_and(|pr| pr.has_expression()))
.copied()
.collect::<Vec<_>>()
};
for p in &list {
let id = p["id"].as_str().unwrap();
let r = s.execute("anim.applyBuiltinPreset", json!({"layers": [solid], "preset": id})).unwrap();
assert_eq!(r["skipped"], json!([]), "{id}: every path and expression applies");
assert_eq!(r["effects"], json!([]), "{id}: behaviors add no effects");
let applied = driven(&s);
assert_eq!(applied.len(), 1, "{id}: exactly one property is driven");
s.execute("edit.undo", json!({})).unwrap();
assert!(driven(&s).is_empty(), "{id}: undo removes the expression");
s.execute("edit.redo", json!({})).unwrap();
assert_eq!(driven(&s), applied, "{id}: redo restores the expression");
s.execute("edit.undo", json!({})).unwrap();
}
}