mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Blend: Ease In / Ease Out and separate Color Ease in Blend Options (#1124)
Squashed from #2539 onto main.
This commit is contained in:
+68
-10
@@ -579,11 +579,18 @@ impl Lerp {
|
||||
|
||||
/// The object at `t` (0 = `a`, 1 = `b`), with id 0.
|
||||
pub fn at(&self, t: f64) -> Node {
|
||||
self.at_eased(t, t)
|
||||
}
|
||||
|
||||
/// The object whose shape is at `t` and whose colours (fills, strokes, opacity, mesh and text
|
||||
/// colours) are at `tc`: a blend with its colours eased apart from its spacing.
|
||||
pub fn at_eased(&self, t: f64, tc: f64) -> Node {
|
||||
let (a, b) = (&self.a, &self.b);
|
||||
let base = if t < 0.5 { a } else { b };
|
||||
let mut n = match &self.shape {
|
||||
Shape::Group { children, clip } => {
|
||||
let mut g = Node::new(NodeId(0), NodeKind::Group { children: children.iter().map(|c| Arc::new(c.at(t))).collect(), clip: *clip });
|
||||
let mut g =
|
||||
Node::new(NodeId(0), NodeKind::Group { children: children.iter().map(|c| Arc::new(c.at_eased(t, tc))).collect(), clip: *clip });
|
||||
g.isolate = base.isolate;
|
||||
g.knockout = base.knockout;
|
||||
g
|
||||
@@ -593,8 +600,8 @@ impl Lerp {
|
||||
if let (NodeKind::Mesh(m), NodeKind::Mesh(ma), NodeKind::Mesh(mb)) = (&mut n.kind, &a.kind, &b.kind) {
|
||||
for ((p, x), y) in m.points.iter_mut().zip(&ma.points).zip(&mb.points) {
|
||||
p.p = x.p.lerp(y.p, t);
|
||||
p.color = lerp_color(&x.color, &y.color, t as f32);
|
||||
p.opacity = lerp_f32(x.opacity, y.opacity, t as f32);
|
||||
p.color = lerp_color(&x.color, &y.color, tc as f32);
|
||||
p.opacity = lerp_f32(x.opacity, y.opacity, tc as f32);
|
||||
for h in 0..4 {
|
||||
p.handles[h] = x.handles[h].lerp(y.handles[h], t);
|
||||
}
|
||||
@@ -620,7 +627,7 @@ impl Lerp {
|
||||
Shape::Placed => {
|
||||
let mut n = base.clone();
|
||||
match (&mut n.kind, &a.kind, &b.kind) {
|
||||
(NodeKind::Text(o), NodeKind::Text(x), NodeKind::Text(y)) => lerp_text(o, x, y, t),
|
||||
(NodeKind::Text(o), NodeKind::Text(x), NodeKind::Text(y)) => lerp_text(o, x, y, t, tc),
|
||||
(NodeKind::SymbolInstance { xf: o, .. }, NodeKind::SymbolInstance { xf: x, .. }, NodeKind::SymbolInstance { xf: y, .. }) => {
|
||||
*o = lerp_affine(*x, *y, t)
|
||||
}
|
||||
@@ -657,8 +664,8 @@ impl Lerp {
|
||||
}
|
||||
};
|
||||
n.id = NodeId(0);
|
||||
n.appearance = lerp_appearance(&a.appearance, &b.appearance, t).into();
|
||||
n.opacity = lerp_f32(a.opacity, b.opacity, t as f32);
|
||||
n.appearance = lerp_appearance(&a.appearance, &b.appearance, tc).into();
|
||||
n.opacity = lerp_f32(a.opacity, b.opacity, tc as f32);
|
||||
n.blend = base.blend;
|
||||
n.visible = true;
|
||||
n.locked = false;
|
||||
@@ -688,7 +695,7 @@ fn compound(path: PathData, rule: FillRule, counts: &[usize], total: usize) -> N
|
||||
/// Interpolate text `x` → `y` into `o` (a copy of the nearer): the transform by its parts, an
|
||||
/// area frame anchor by anchor, and each run's colours, stroke weight and size from the runs at
|
||||
/// the same place (by character) of both.
|
||||
fn lerp_text(o: &mut TextObject, x: &TextObject, y: &TextObject, t: f64) {
|
||||
fn lerp_text(o: &mut TextObject, x: &TextObject, y: &TextObject, t: f64, tc: f64) {
|
||||
o.xf = lerp_affine(x.xf, y.xf, t);
|
||||
if let (crate::text::TextKind::Area { frame: f }, crate::text::TextKind::Area { frame: fx }, crate::text::TextKind::Area { frame: fy }) =
|
||||
(&mut o.kind, &x.kind, &y.kind)
|
||||
@@ -710,7 +717,7 @@ fn lerp_text(o: &mut TextObject, x: &TextObject, y: &TextObject, t: f64) {
|
||||
}
|
||||
let total: usize = o.runs.iter().map(|r| r.text.chars().count()).sum::<usize>().max(1);
|
||||
let mut start = 0usize;
|
||||
let tf = t as f32;
|
||||
let tf = tc as f32;
|
||||
let mut resized = false;
|
||||
for r in &mut o.runs {
|
||||
let len = r.text.chars().count();
|
||||
@@ -1071,8 +1078,10 @@ pub fn blend_expand(keys: &[Arc<Node>], spec: &BlendSpec) -> Vec<Node> {
|
||||
let n = blend_step_count(a, b, spec.spacing, len);
|
||||
let lerp = Lerp::new(a, b, (spec.start(i), spec.start(i + 1)));
|
||||
for j in 1..=n {
|
||||
let t = j as f64 / (n + 1) as f64;
|
||||
let mut s = lerp.at(t);
|
||||
// Even position of the step, then where the easing puts its shape and its colours.
|
||||
let u = j as f64 / (n + 1) as f64;
|
||||
let (t, tc) = (if rail.is_some() { spec.ease.apply(u) } else { u }, spec.color_position(u));
|
||||
let mut s = lerp.at_eased(t, tc);
|
||||
if let Some(r) = &rail {
|
||||
r.place(&mut s, i, t);
|
||||
}
|
||||
@@ -1134,6 +1143,55 @@ mod tests {
|
||||
assert_eq!(out[0].geometric_bounds().unwrap().center(), Point::new(5.0, 5.0), "keys stay put");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn easing_keeps_the_ends_and_stays_monotone() {
|
||||
use crate::live::{BlendEase, BlendEaseType as K};
|
||||
for kind in [K::In, K::Out, K::InOut, K::OutIn] {
|
||||
for ramp in [0.0, 0.5, 1.0, f64::NAN, f64::INFINITY] {
|
||||
let e = BlendEase { kind, ramp };
|
||||
assert_eq!((e.apply(0.0), e.apply(1.0)), (0.0, 1.0));
|
||||
let ys: Vec<f64> = (0..=100).map(|i| e.apply(i as f64 / 100.0)).collect();
|
||||
assert!(ys.windows(2).all(|w| w[0] <= w[1]));
|
||||
assert_eq!(e.apply(f64::NAN), 0.0);
|
||||
if ramp == 0.0 {
|
||||
assert_eq!(e.apply(0.25), 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
let e = |kind| BlendEase { kind, ramp: 1.0 };
|
||||
assert!(e(K::In).apply(0.25) < 0.25);
|
||||
assert!(e(K::Out).apply(0.25) > 0.25);
|
||||
assert!(e(K::InOut).apply(0.25) < 0.25 && e(K::InOut).apply(0.75) > 0.75);
|
||||
assert!(e(K::OutIn).apply(0.25) > 0.25 && e(K::OutIn).apply(0.75) < 0.75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_easing_moves_colours_apart_from_positions() {
|
||||
let rect = |x: f64, c: Color| {
|
||||
let mut n = Node::path(NodeId(1), shapes::rectangle(Rect::new(x, 0.0, x + 10.0, 10.0)), Appearance::default());
|
||||
n.appearance = Appearance::basic(Paint::solid(c), Paint::None, 0.0).into();
|
||||
Arc::new(n)
|
||||
};
|
||||
let keys = vec![rect(0.0, Color::BLACK), rect(100.0, Color::WHITE)];
|
||||
let mid = |spec: &BlendSpec| {
|
||||
let out = blend_expand(&keys, spec);
|
||||
let n = &out[1];
|
||||
(n.geometric_bounds().unwrap().x0, n.appearance.fill_paint())
|
||||
};
|
||||
// Position easing runs along a spine: Align to Path gives the blend one.
|
||||
let base = BlendSpec { spacing: BlendSpacing::Steps(1), orientation: BlendOrientation::AlignToPath, ..Default::default() };
|
||||
let (x0, p0) = mid(&base);
|
||||
// Colour easing alone leaves the position and moves the colour.
|
||||
let colour = BlendSpec { color_shift: 50.0, ..base.clone() };
|
||||
let (x1, p1) = mid(&colour);
|
||||
assert!((x0 - x1).abs() < 1e-9 && p0 != p1);
|
||||
// Position easing alone leaves the colour and moves the position.
|
||||
let pos = BlendSpec { ease: crate::live::BlendEase { kind: crate::live::BlendEaseType::In, ramp: 1.0 }, ..base.clone() };
|
||||
let (x2, p2) = mid(&pos);
|
||||
assert!((x0 - x2).abs() > 1e-3 && p0 == p2);
|
||||
assert_eq!(mid(&pos).0, x2, "deterministic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affine_lerp_turns_the_short_way() {
|
||||
let x = Affine::rotate(170f64.to_radians());
|
||||
|
||||
@@ -99,7 +99,7 @@ pub use graph::{
|
||||
};
|
||||
pub use hit::{Hit, HitKind};
|
||||
pub use links::{LinkInfo, PlacementOptions};
|
||||
pub use live::{BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, GradientMesh, MeshPoint};
|
||||
pub use live::{BlendEase, BlendEaseType, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, GradientMesh, MeshPoint};
|
||||
pub use live_paint::{GapOptions, GapSize};
|
||||
pub use metadata::{CopyrightStatus, DocMetadata};
|
||||
pub use node::Knockout;
|
||||
|
||||
@@ -60,12 +60,75 @@ pub enum BlendOrientation {
|
||||
AlignToPath,
|
||||
}
|
||||
|
||||
/// Distribution of blend steps, as offered by the Blend panel's Type menu.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum BlendEaseType {
|
||||
#[default]
|
||||
In,
|
||||
Out,
|
||||
InOut,
|
||||
OutIn,
|
||||
}
|
||||
|
||||
impl BlendEaseType {
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::In => "easeIn",
|
||||
Self::Out => "easeOut",
|
||||
Self::InOut => "easeInOut",
|
||||
Self::OutIn => "easeOutIn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Blend panel Type and Ramp. Ramp is a fraction from 0 (even) to 1.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BlendEase {
|
||||
#[serde(default)]
|
||||
pub kind: BlendEaseType,
|
||||
#[serde(default, deserialize_with = "blend_ramp")]
|
||||
pub ramp: f64,
|
||||
}
|
||||
|
||||
impl BlendEase {
|
||||
pub fn is_linear(&self) -> bool {
|
||||
self.ramp <= 0.0
|
||||
}
|
||||
|
||||
/// Only the default Type and Ramp can be omitted without losing a chosen Type at Ramp 0.
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.kind == BlendEaseType::In && self.ramp == 0.0
|
||||
}
|
||||
|
||||
/// A monotone polynomial distribution, blended with even spacing by Ramp. Public help
|
||||
/// describes the four distributions, not their numeric curves; these curves are original.
|
||||
pub fn apply(&self, u: f64) -> f64 {
|
||||
let u = if u.is_finite() { u.clamp(0.0, 1.0) } else { 0.0 };
|
||||
let r = if self.ramp.is_finite() { self.ramp.clamp(0.0, 1.0) } else { 0.0 };
|
||||
let eased = match self.kind {
|
||||
BlendEaseType::In => u * u,
|
||||
BlendEaseType::Out => u * (2.0 - u),
|
||||
BlendEaseType::InOut => u * u * (3.0 - 2.0 * u),
|
||||
BlendEaseType::OutIn if u < 0.5 => 2.0 * u * (1.0 - u),
|
||||
BlendEaseType::OutIn => 0.5 + 2.0 * (u - 0.5) * (u - 0.5),
|
||||
};
|
||||
u + r * (eased - u)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BlendSpec {
|
||||
#[serde(default)]
|
||||
pub spacing: BlendSpacing,
|
||||
#[serde(default)]
|
||||
pub orientation: BlendOrientation,
|
||||
/// Acceleration of the steps' shapes and positions along the spine between two keys.
|
||||
#[serde(default, skip_serializing_if = "BlendEase::is_default")]
|
||||
pub ease: BlendEase,
|
||||
/// Independent color acceleration. -100..100, with 0 preserving the even color ramp.
|
||||
#[serde(default, rename = "colorShift", skip_serializing_if = "is_zero", deserialize_with = "blend_color_shift")]
|
||||
pub color_shift: f64,
|
||||
/// Replaced spine. `None` = the straight lines between the key objects' centres.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub spine: Option<PathData>,
|
||||
@@ -1757,10 +1820,58 @@ pub(crate) fn max_outset(nodes: &[Arc<Node>], symbols: Option<&crate::SymbolBoxe
|
||||
o
|
||||
}
|
||||
|
||||
/// Reading a native file never fails over one bad blend number: out-of-range values clamp,
|
||||
/// and anything that is not a finite number (NaN, null, text) becomes the default. The
|
||||
/// commands still refuse such input (`ease_param`).
|
||||
fn blend_number<'de, D: serde::Deserializer<'de>>(d: D, min: f64, max: f64) -> Result<f64, D::Error> {
|
||||
let v = serde_json::Value::deserialize(d)?;
|
||||
Ok(v.as_f64().filter(|n| n.is_finite()).map_or(0.0, |n| n.clamp(min, max)))
|
||||
}
|
||||
|
||||
fn blend_ramp<'de, D: serde::Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
|
||||
blend_number(d, 0.0, 1.0)
|
||||
}
|
||||
|
||||
fn blend_color_shift<'de, D: serde::Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
|
||||
blend_number(d, -100.0, 100.0)
|
||||
}
|
||||
|
||||
/// Whether an optional stored color shift has its unchanged default.
|
||||
fn is_zero(v: &f64) -> bool {
|
||||
*v == 0.0
|
||||
}
|
||||
|
||||
impl BlendSpec {
|
||||
/// Shift the color transition independently of shape placement. The exponential curve is
|
||||
/// original; the documented behavior specifies the direction, not numeric sample values.
|
||||
pub fn color_position(&self, u: f64) -> f64 {
|
||||
let u = if u.is_finite() { u.clamp(0.0, 1.0) } else { 0.0 };
|
||||
let shift = if self.color_shift.is_finite() { self.color_shift.clamp(-100.0, 100.0) } else { 0.0 };
|
||||
if shift == 0.0 { u } else { u.powf(2.0f64.powf(shift / 50.0)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A bad Ramp or Shift in a file clamps (non-finite values take the default) instead of
|
||||
/// failing the whole document.
|
||||
#[test]
|
||||
fn blend_easing_numbers_clamp_when_read() {
|
||||
use serde::de::value::{Error, F64Deserializer};
|
||||
let spec: BlendSpec = serde_json::from_str(r#"{"ease":{"kind":"out","ramp":7.5},"colorShift":-900}"#).unwrap();
|
||||
assert_eq!((spec.ease.ramp, spec.color_shift), (1.0, -100.0));
|
||||
let spec: BlendSpec = serde_json::from_str(r#"{"ease":{"ramp":-3},"colorShift":250}"#).unwrap();
|
||||
assert_eq!((spec.ease.ramp, spec.color_shift), (0.0, 100.0));
|
||||
let spec: BlendSpec = serde_json::from_str(r#"{"ease":{"ramp":null},"colorShift":"NaN"}"#).unwrap();
|
||||
assert_eq!((spec.ease.ramp, spec.color_shift), (0.0, 0.0));
|
||||
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
assert_eq!(blend_ramp(F64Deserializer::<Error>::new(bad)).unwrap(), 0.0);
|
||||
assert_eq!(blend_color_shift(F64Deserializer::<Error>::new(bad)).unwrap(), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// #1578: Arc bends in the box's real proportions. In half-width units (y scaled by the
|
||||
/// aspect), the top and bottom edges are concentric circles about the bend's centre, at their
|
||||
/// real distance from the centre line; an inner edge that would pass the centre stops there.
|
||||
|
||||
@@ -10,8 +10,8 @@ use std::sync::Arc;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::live::{
|
||||
self, BlendDefaults, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, EnvelopeMap, EnvelopeOptions, GradientMesh, MeshAppearance,
|
||||
PreserveShape, Spine,
|
||||
self, BlendDefaults, BlendEase, BlendEaseType, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, EnvelopeMap, EnvelopeOptions,
|
||||
GradientMesh, MeshAppearance, PreserveShape, Spine,
|
||||
};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection};
|
||||
use vectorcraft_geom::{Affine, Anchor, PathData, Point, Vec2};
|
||||
@@ -29,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Make",
|
||||
["Object", "Blend"],
|
||||
Some("Cmd+Alt+B"),
|
||||
"{ids?, steps?: n | distance?: pt | smooth?: bool, orientation?: page|path (default: the Blend Options set with nothing selected, else smooth colour and page), starts?: [anchor index | null, …] (per object of `ids`, else of the selection in paint order: the anchor of its first subpath the blend starts from; an open path's last anchor runs it the other way), spacing?: smooth|steps|distance (with value), value?: n} blend the selected objects (paint order) into a live blend; a blend among them takes the others in as more key objects (keeping its options, name and transparency) instead of nesting → {id}",
|
||||
"{ids?, steps?: n | distance?: pt | smooth?: bool, orientation?: page|path (default: the Blend Options set with nothing selected, else smooth colour and page), starts?: [anchor index | null, …] (per object of `ids`, else of the selection in paint order: the anchor of its first subpath the blend starts from; an open path's last anchor runs it the other way), spacing?: smooth|steps|distance (with value), value?: n, easingType?: easeIn|easeOut|easeInOut|easeOutIn, ramp?: pct (0..100), colorShift?: pct (-100..100; 0: even)} blend the selected objects (paint order) into a live blend; a blend among them takes the others in as more key objects (keeping its options, name and transparency) instead of nesting → {id}",
|
||||
has_doc,
|
||||
blend_make
|
||||
),
|
||||
@@ -47,7 +47,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Blend Options…",
|
||||
["Object", "Blend"],
|
||||
None,
|
||||
"{spacing?: smooth|steps|distance, value?: n, steps?: n, distance?: pt, orientation?: page|path, smooth?: bool} set the options of the selected blends; with no blend selected, the options new blends start with (a tool setting, not an undo step) → {defaults?: true}",
|
||||
"{spacing?: smooth|steps|distance, value?: n, steps?: n, distance?: pt, orientation?: page|path, smooth?: bool, easingType?: easeIn|easeOut|easeInOut|easeOutIn, ramp?: pct, colorShift?: pct} set the options of the selected blends; easingType chooses the distribution, ramp sets its strength (0..100; 0 = even), colorShift (-100..100; 0 = even) shifts colours independently of shape/position easing (omitted ones keep their value; out of range is an error); with no blend selected, the options new blends start with (spacing and orientation only; a tool setting, not an undo step) → {defaults?: true}",
|
||||
always,
|
||||
blend_options
|
||||
),
|
||||
@@ -83,7 +83,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Blend Info",
|
||||
[],
|
||||
None,
|
||||
"{} the options of the first selected blend, else those new blends start with → {target: blend|defaults, id?, spacing: smooth|steps|distance, steps, distance, orientation: page|path, keys?: [ids], starts?: [anchor|null], spine?: {anchors: [{x, y, in: [x, y], out: [x, y]}], closed, keyAnchors: [anchor each key sits on] | null (keys spread evenly: a spine from Replace Spine), explicit: false for the straight lines between the key centres}}",
|
||||
"{} the options of the first selected blend, else those new blends start with → {target: blend|defaults, id?, spacing: smooth|steps|distance, steps, distance, orientation: page|path, easingType?: easeIn|easeOut|easeInOut|easeOutIn, ramp?: pct, colorShift?: pct, keys?: [ids], starts?: [anchor|null], spine?: {anchors: [{x, y, in: [x, y], out: [x, y]}], closed, keyAnchors: [anchor each key sits on] | null (keys spread evenly: a spine from Replace Spine), explicit: false for the straight lines between the key centres}}",
|
||||
always,
|
||||
blend_info
|
||||
),
|
||||
@@ -451,6 +451,32 @@ fn orientation_param(p: &Value, current: BlendOrientation) -> BlendOrientation {
|
||||
}
|
||||
}
|
||||
|
||||
/// Blend panel Type and Ramp, validated before any document mutation.
|
||||
fn ease_param(p: &Value, cmd: &str, current: BlendEase) -> Result<BlendEase> {
|
||||
let kind = match p.get("easingType").filter(|v| !v.is_null()) {
|
||||
None => current.kind,
|
||||
Some(v) => match v.as_str() {
|
||||
Some("easeIn") => BlendEaseType::In,
|
||||
Some("easeOut") => BlendEaseType::Out,
|
||||
Some("easeInOut") => BlendEaseType::InOut,
|
||||
Some("easeOutIn") => BlendEaseType::OutIn,
|
||||
_ => return Err(bad(cmd, "easingType must be easeIn, easeOut, easeInOut or easeOutIn")),
|
||||
},
|
||||
};
|
||||
Ok(BlendEase { kind, ramp: percent_param(p, cmd, "ramp", current.ramp * 100.0, 0.0)? / 100.0 })
|
||||
}
|
||||
|
||||
/// Bounded percentage input; missing or null preserves the current value.
|
||||
fn percent_param(p: &Value, cmd: &str, key: &str, current: f64, min: f64) -> Result<f64> {
|
||||
match p.get(key).filter(|v| !v.is_null()) {
|
||||
None => Ok(current),
|
||||
Some(v) => v
|
||||
.as_f64()
|
||||
.filter(|n| n.is_finite() && (min..=100.0).contains(n))
|
||||
.ok_or_else(|| bad(cmd, format!("{key} must be a percentage {min}..100"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// `starts`: per object of `ids` (else of `roots`), the anchor its blend starts from.
|
||||
fn starts_param(p: &Value, roots: &[NodeId]) -> Result<Vec<(NodeId, u32)>> {
|
||||
const C: &str = "object.blend.make";
|
||||
@@ -533,6 +559,12 @@ fn blend_make(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let spacing = spacing_param(p, C, cur.spacing)?;
|
||||
let orientation = orientation_param(p, cur.orientation);
|
||||
let (ease0, color0) = match base.as_ref().map(|n| &n.kind) {
|
||||
Some(NodeKind::Blend { spec, .. }) => (spec.ease, spec.color_shift),
|
||||
_ => Default::default(),
|
||||
};
|
||||
let ease = ease_param(p, C, ease0)?;
|
||||
let color_shift = percent_param(p, C, "colorShift", color0, -100.0)?;
|
||||
let id = s.edit("Make Blend", |d, sel| {
|
||||
wrap(d, sel, &roots, |id, nodes| {
|
||||
let (children, starts) = merge_keys(nodes, &starts);
|
||||
@@ -543,7 +575,7 @@ fn blend_make(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
n.knockout = vectorcraft_doc::Knockout::On;
|
||||
n
|
||||
});
|
||||
let mut spec = BlendSpec { spacing, orientation, starts, ..Default::default() };
|
||||
let mut spec = BlendSpec { spacing, orientation, ease, color_shift, starts, ..Default::default() };
|
||||
if let NodeKind::Blend { children: old, spec: old_spec } = &n.kind {
|
||||
extend_spine(&mut spec, old, old_spec, &children);
|
||||
}
|
||||
@@ -772,7 +804,10 @@ fn selected_spec(s: &Session) -> Option<(NodeId, BlendSpec, Vec<NodeId>)> {
|
||||
fn blend_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.blend.options";
|
||||
let Some((_, cur, _)) = selected_spec(s) else {
|
||||
// Nothing to set them on: the options new blends start with.
|
||||
if ["easingType", "ramp", "colorShift"].iter().any(|key| p.get(*key).is_some_and(|v| !v.is_null())) {
|
||||
return Err(bad(C, "select a blend to set easing or color shift"));
|
||||
}
|
||||
// Nothing to set them on: the options new blends start with (easing is per blend).
|
||||
let d = s.prefs.blend_options.unwrap_or_default();
|
||||
let spacing = spacing_param(p, C, d.spacing)?;
|
||||
s.prefs.blend_options = Some(BlendDefaults { spacing, orientation: orientation_param(p, d.orientation) });
|
||||
@@ -780,9 +815,13 @@ fn blend_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let spacing = spacing_param(p, C, cur.spacing)?;
|
||||
let orientation = orientation_param(p, cur.orientation);
|
||||
let ease = ease_param(p, C, cur.ease)?;
|
||||
let color_shift = percent_param(p, C, "colorShift", cur.color_shift, -100.0)?;
|
||||
edit_blends(s, "Blend Options", |_, spec| {
|
||||
spec.spacing = spacing;
|
||||
spec.orientation = orientation;
|
||||
spec.ease = ease;
|
||||
spec.color_shift = color_shift;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -804,6 +843,9 @@ fn blend_info(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let mut v = blend_options_json(spec.spacing, spec.orientation);
|
||||
v["target"] = json!("blend");
|
||||
v["id"] = json!(id.0);
|
||||
v["easingType"] = json!(spec.ease.kind.id());
|
||||
v["ramp"] = json!(spec.ease.ramp * 100.0);
|
||||
v["colorShift"] = json!(spec.color_shift);
|
||||
v["starts"] = json!((0..keys.len()).map(|i| spec.start(i)).collect::<Vec<_>>());
|
||||
v["keys"] = json!(keys.iter().map(|k| k.0).collect::<Vec<_>>());
|
||||
if let Some(NodeKind::Blend { children, .. }) = s.doc()?.doc.node(id).map(|n| &n.kind)
|
||||
|
||||
@@ -153,3 +153,79 @@ fn start_points_round_trip_and_old_blends_load() {
|
||||
assert!(old.starts.is_empty() && old.start(0).is_none());
|
||||
assert!(serde_json::to_value(&old).unwrap().get("starts").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn easing_is_set_validated_saved_and_old_blends_are_even() {
|
||||
let mut s = session();
|
||||
assert!(s.execute("object.blend.options", &json!({"ramp": 50})).is_err());
|
||||
let (a, b) = (rect(&mut s, 0.0, 0.0, 10.0), rect(&mut s, 100.0, 0.0, 10.0));
|
||||
sel(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.blend.make", &json!({"steps": 3, "easingType": "easeIn", "ramp": 40})).unwrap());
|
||||
assert_eq!(spec(&s, g).1.ease.ramp, 0.4);
|
||||
sel(&mut s, &[g]);
|
||||
for kind in ["easeIn", "easeOut", "easeInOut", "easeOutIn"] {
|
||||
s.execute("object.blend.options", &json!({"easingType": kind, "colorShift": -50})).unwrap();
|
||||
let info = s.execute("object.blend.info", &json!({})).unwrap();
|
||||
assert_eq!((&info["easingType"], &info["ramp"], &info["colorShift"]), (&json!(kind), &json!(40.0), &json!(-50.0)));
|
||||
}
|
||||
let before = spec(&s, g).1;
|
||||
let depth = s.doc().unwrap().history.undo.len();
|
||||
for bad in [json!({"ramp": 101}), json!({"ramp": -1}), json!({"colorShift": "x"}), json!({"colorShift": 1e308}), json!({"easingType": "unknown"})]
|
||||
{
|
||||
assert!(s.execute("object.blend.options", &bad).is_err(), "{bad}");
|
||||
assert_eq!(spec(&s, g).1, before);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), depth);
|
||||
}
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(d, false)).unwrap();
|
||||
assert_eq!(&back.layers, &d.layers);
|
||||
s.execute("object.blend.options", &json!({"ramp": 0})).unwrap();
|
||||
let zero = spec(&s, g).1;
|
||||
let back: BlendSpec = serde_json::from_value(serde_json::to_value(&zero).unwrap()).unwrap();
|
||||
assert_eq!(back.ease, zero.ease, "a chosen Type survives at Ramp 0");
|
||||
let old: BlendSpec = serde_json::from_value(json!({"spacing": {"steps": 4}})).unwrap();
|
||||
assert!(old.ease.is_linear() && old.color_shift == 0.0);
|
||||
let v = serde_json::to_value(&old).unwrap();
|
||||
assert!(v.get("ease").is_none() && v.get("colorShift").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_with_bad_blend_easing_numbers_opens_clamped_while_commands_refuse() {
|
||||
let mut s = session();
|
||||
let (a, b) = (rect(&mut s, 0.0, 0.0, 10.0), rect(&mut s, 100.0, 0.0, 10.0));
|
||||
sel(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.blend.make", &json!({"steps": 3, "easingType": "easeOut", "ramp": 40, "colorShift": -50})).unwrap());
|
||||
let text = String::from_utf8(vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
let blend_of = |doc: &vectorcraft_doc::Document| match doc.node(g).unwrap().kind.clone() {
|
||||
NodeKind::Blend { spec, .. } => *spec,
|
||||
_ => panic!("not a blend"),
|
||||
};
|
||||
for (ramp, shift, want) in [("12.5", "-9000.0", (1.0, -100.0)), ("-3.0", "250.0", (0.0, 100.0)), ("null", "\"NaN\"", (0.0, 0.0))] {
|
||||
let t = text.replacen("\"ramp\":0.4", &format!("\"ramp\":{ramp}"), 1).replacen("\"colorShift\":-50.0", &format!("\"colorShift\":{shift}"), 1);
|
||||
assert!(t != text && t.contains("\"ramp\":") && t.contains(shift), "the saved blend carries ramp and colorShift");
|
||||
let doc = vectorcraft_format::load(t.as_bytes()).expect("one bad blend number must not fail the document");
|
||||
let spec = blend_of(&doc);
|
||||
assert_eq!((spec.ease.ramp, spec.color_shift), want, "{ramp} / {shift}");
|
||||
}
|
||||
// The commands still refuse the same values and leave the blend alone.
|
||||
sel(&mut s, &[g]);
|
||||
let before = spec(&s, g).1;
|
||||
for bad in [json!({"ramp": 1250}), json!({"ramp": -3}), json!({"colorShift": 250}), json!({"colorShift": "NaN"}), json!({"colorShift": 1e308})] {
|
||||
assert!(s.execute("object.blend.options", &bad).is_err(), "{bad}");
|
||||
assert_eq!(spec(&s, g).1, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_blend_easing_values_clamp_and_non_finite_become_default() {
|
||||
for (bad, ramp, shift) in [
|
||||
(json!({"ease": {"ramp": 1e308}}), 1.0, 0.0),
|
||||
(json!({"ease": {"ramp": -0.1}}), 0.0, 0.0),
|
||||
(json!({"colorShift": 101}), 0.0, 100.0),
|
||||
(json!({"colorShift": -101}), 0.0, -100.0),
|
||||
(json!({"ease": {"ramp": null}, "colorShift": "NaN"}), 0.0, 0.0),
|
||||
] {
|
||||
let spec: BlendSpec = serde_json::from_value(bad.clone()).unwrap_or_else(|e| panic!("{bad}: {e}"));
|
||||
assert_eq!((spec.ease.ramp, spec.color_shift), (ramp, shift), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,6 +472,8 @@ fn structured_junk() {
|
||||
("object.blend.make", json!({"ids": [1, 2, 3], "starts": [u64::MAX, -1, "x", null, 1e308]})),
|
||||
("object.blend.make", json!({"ids": [2, 2], "starts": {"a": 1}, "steps": 1e308})),
|
||||
("object.blend.options", json!({"spacing": "distance", "value": -1e308, "orientation": 5})),
|
||||
("object.blend.options", json!({"ramp": 1e308, "colorShift": -1e308, "easingType": [1]})),
|
||||
("object.blend.make", json!({"ids": [1, 2], "ramp": f64::NAN, "colorShift": [1]})),
|
||||
("object.blend.spine.moveAnchor", json!({"id": 2, "anchor": u64::MAX, "x": 1e308, "y": -1e308, "handle": "sideways"})),
|
||||
("object.blend.spine.addAnchor", json!({"id": u64::MAX, "x": 0, "y": 0})),
|
||||
("object.blend.spine.reshapeSegment", json!({"id": 2, "segment": u64::MAX, "t": f64::NAN, "dx": 1e308, "dy": -1e308})),
|
||||
|
||||
@@ -5335,5 +5335,11 @@ theme Appearance Външен вид
|
||||
Water Paper Мокра хартия
|
||||
Ruby… Руби…
|
||||
Ruby Руби
|
||||
Ease In Плавно начало
|
||||
Ease Out Плавен край
|
||||
Ease In Out Плавно начало и край
|
||||
Ease Out In Плавен край и начало
|
||||
Ramp: Рампа:
|
||||
Shift: Изместване:
|
||||
Fresco… Фреска…
|
||||
Fresco Фреска
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5378,5 +5378,11 @@ mark-style Japanese Japonské
|
||||
Water Paper Mokrý papír
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Pozvolný začátek
|
||||
Ease Out Pozvolný konec
|
||||
Ease In Out Pozvolný začátek a konec
|
||||
Ease Out In Pozvolný konec a začátek
|
||||
Ramp: Síla:
|
||||
Shift: Posun:
|
||||
Fresco… Freska…
|
||||
Fresco Freska
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5351,5 +5351,11 @@ swatch Bright Violet Kraftig violet
|
||||
Water Paper Vådt papir
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Langsom start
|
||||
Ease Out Langsom slutning
|
||||
Ease In Out Langsom start og slutning
|
||||
Ease Out In Langsom slutning og start
|
||||
Ramp: Styrke:
|
||||
Shift: Forskydning:
|
||||
Fresco… Fresko…
|
||||
Fresco Fresko
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4959,5 +4959,11 @@ effect Soft Weich
|
||||
Water Paper Feuchtes Papier
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Langsam starten
|
||||
Ease Out Langsam enden
|
||||
Ease In Out Langsam starten und enden
|
||||
Ease Out In Langsam enden und starten
|
||||
Ramp: Stärke:
|
||||
Shift: Verschiebung:
|
||||
Fresco… Fresko…
|
||||
Fresco Fresko
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5330,5 +5330,11 @@ effect Points Σημεία
|
||||
Water Paper Βρεγμένο χαρτί
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Ομαλή αρχή
|
||||
Ease Out Ομαλό τέλος
|
||||
Ease In Out Ομαλή αρχή και τέλος
|
||||
Ease Out In Ομαλό τέλος και αρχή
|
||||
Ramp: Ένταση:
|
||||
Shift: Μετατόπιση:
|
||||
Fresco… Νωπογραφία…
|
||||
Fresco Νωπογραφία
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2040 and column 19.
|
@@ -5320,5 +5320,11 @@ mark-style Japanese Japonesas
|
||||
Water Paper Papel mojado
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Entrada suave
|
||||
Ease Out Salida suave
|
||||
Ease In Out Entrada y salida suaves
|
||||
Ease Out In Salida y entrada suaves
|
||||
Ramp: Intensidad:
|
||||
Shift: Desplazamiento:
|
||||
Fresco… Fresco…
|
||||
Fresco Fresco
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5409,5 +5409,11 @@ swatch Bright Violet Kirkas violetti
|
||||
Water Paper Märkä paperi
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Hidas alku
|
||||
Ease Out Hidas loppu
|
||||
Ease In Out Hidas alku ja loppu
|
||||
Ease Out In Hidas loppu ja alku
|
||||
Ramp: Voimakkuus:
|
||||
Shift: Siirtymä:
|
||||
Fresco… Fresko…
|
||||
Fresco Fresko
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5377,5 +5377,11 @@ theme Appearance Apparence
|
||||
Water Paper Papier humide
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Accélération progressive
|
||||
Ease Out Décélération progressive
|
||||
Ease In Out Accélération et décélération
|
||||
Ease Out In Décélération et accélération
|
||||
Ramp: Intensité :
|
||||
Shift: Décalage :
|
||||
Fresco… Fresque…
|
||||
Fresco Fresque
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5364,5 +5364,11 @@ imageTrace Outline Körvonal
|
||||
Water Paper Nedves papír
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Lassú kezdés
|
||||
Ease Out Lassú befejezés
|
||||
Ease In Out Lassú kezdés és befejezés
|
||||
Ease Out In Lassú befejezés és kezdés
|
||||
Ramp: Erősség:
|
||||
Shift: Eltolás:
|
||||
Fresco… Freskó…
|
||||
Fresco Freskó
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5351,5 +5351,11 @@ swatch Bright Violet Violet cerah
|
||||
Water Paper Kertas Basah
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Awal Lambat
|
||||
Ease Out Akhir Lambat
|
||||
Ease In Out Awal dan Akhir Lambat
|
||||
Ease Out In Akhir dan Awal Lambat
|
||||
Ramp: Kekuatan:
|
||||
Shift: Pergeseran:
|
||||
Fresco… Fresko…
|
||||
Fresco Fresko
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3811 and column 19.
|
@@ -4993,5 +4993,11 @@ lighting Light Luce
|
||||
Water Paper Carta bagnata
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Inizio graduale
|
||||
Ease Out Fine graduale
|
||||
Ease In Out Inizio e fine graduali
|
||||
Ease Out In Fine e inizio graduali
|
||||
Ramp: Intensità:
|
||||
Shift: Spostamento:
|
||||
Fresco… Affresco…
|
||||
Fresco Affresco
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5040,5 +5040,11 @@ lighting Light ライト
|
||||
Yonroku-ban 四六判
|
||||
Ruby… ルビ…
|
||||
Ruby ルビ
|
||||
Ease In イーズイン
|
||||
Ease Out イーズアウト
|
||||
Ease In Out イーズインアウト
|
||||
Ease Out In イーズアウトイン
|
||||
Ramp: 強さ:
|
||||
Shift: シフト:
|
||||
Fresco… フレスコ…
|
||||
Fresco フレスコ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5288,5 +5288,11 @@ imageTrace Outline კონტური
|
||||
selects several, or draws one inside an artboard) რამდენიმეს მონიშნავს ან ახალს სამუშაო დაფის შიგნით ხატავს)
|
||||
Ruby… რუბი…
|
||||
Ruby რუბი
|
||||
Ease In ნელი დასაწყისი
|
||||
Ease Out ნელი დასასრული
|
||||
Ease In Out ნელი დასაწყისი და დასასრული
|
||||
Ease Out In ნელი დასასრული და დასაწყისი
|
||||
Ramp: სიძლიერე:
|
||||
Shift: წანაცვლება:
|
||||
Fresco… ფრესკა…
|
||||
Fresco ფრესკა
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4956,5 +4956,11 @@ lighting Light 광원
|
||||
Water Paper 물 종이
|
||||
Ruby… 루비…
|
||||
Ruby 루비
|
||||
Ease In 서서히 시작
|
||||
Ease Out 서서히 종료
|
||||
Ease In Out 서서히 시작 및 종료
|
||||
Ease Out In 서서히 종료 및 시작
|
||||
Ramp: 강도:
|
||||
Shift: 이동:
|
||||
Fresco… 프레스코…
|
||||
Fresco 프레스코
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5393,5 +5393,11 @@ swatch Bright Violet Felviolet
|
||||
Water Paper Nat papier
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Langzaam beginnen
|
||||
Ease Out Langzaam eindigen
|
||||
Ease In Out Langzaam beginnen en eindigen
|
||||
Ease Out In Langzaam eindigen en beginnen
|
||||
Ramp: Sterkte:
|
||||
Shift: Verschuiving:
|
||||
Fresco… Fresco…
|
||||
Fresco Fresco
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5230,5 +5230,11 @@ mark-style Japanese Japońskie
|
||||
Water Paper Mokry papier
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Łagodny początek
|
||||
Ease Out Łagodny koniec
|
||||
Ease In Out Łagodny początek i koniec
|
||||
Ease Out In Łagodny koniec i początek
|
||||
Ramp: Siła:
|
||||
Shift: Przesunięcie:
|
||||
Fresco… Fresk…
|
||||
Fresco Fresk
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5295,5 +5295,11 @@ mark-style Japanese Japonesas
|
||||
Water Paper Papel molhado
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Entrada suave
|
||||
Ease Out Saída suave
|
||||
Ease In Out Entrada e saída suaves
|
||||
Ease Out In Saída e entrada suaves
|
||||
Ramp: Intensidade:
|
||||
Shift: Deslocamento:
|
||||
Fresco… Afresco…
|
||||
Fresco Afresco
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4989,5 +4989,11 @@ lighting Light Свет
|
||||
Water Paper Мокрая бумага
|
||||
Ruby… Руби…
|
||||
Ruby Руби
|
||||
Ease In Плавное начало
|
||||
Ease Out Плавное окончание
|
||||
Ease In Out Плавное начало и окончание
|
||||
Ease Out In Плавное окончание и начало
|
||||
Ramp: Сила:
|
||||
Shift: Сдвиг:
|
||||
Fresco… Фреска…
|
||||
Fresco Фреска
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5334,5 +5334,11 @@ theme Appearance Utseende
|
||||
Water Paper Vått papper
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Långsam start
|
||||
Ease Out Långsamt slut
|
||||
Ease In Out Långsam start och slut
|
||||
Ease Out In Långsamt slut och start
|
||||
Ramp: Styrka:
|
||||
Shift: Förskjutning:
|
||||
Fresco… Fresk…
|
||||
Fresco Fresk
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4973,5 +4973,11 @@ lighting Light Світло
|
||||
Water Paper Мокрий папір
|
||||
Ruby… Рубі…
|
||||
Ruby Рубі
|
||||
Ease In Плавний початок
|
||||
Ease Out Плавне завершення
|
||||
Ease In Out Плавний початок і завершення
|
||||
Ease Out In Плавне завершення і початок
|
||||
Ramp: Сила:
|
||||
Shift: Зсув:
|
||||
Fresco… Фреска…
|
||||
Fresco Фреска
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 733 and column 40.
|
@@ -431,6 +431,7 @@ each of `items` is {{id, corners?}} with an object id, not {it}
|
||||
each row needs `from`
|
||||
each stop needs a position
|
||||
each subpath needs an anchors array
|
||||
easingType must be easeIn, easeOut, easeInOut or easeOutIn
|
||||
editingData must be true or false
|
||||
editingData must be true, false or "always"
|
||||
edits overlap
|
||||
@@ -935,6 +936,7 @@ scope must be selection or all (got {other})
|
||||
scribble is too long
|
||||
segment does not exist
|
||||
select a Shaper group
|
||||
select a blend to set easing or color shift
|
||||
select a graph
|
||||
select a group or layer with visible, unlocked members
|
||||
select a layer
|
||||
@@ -1255,6 +1257,7 @@ zoom must be positive
|
||||
{key} must be 0–{MAX_ADD_AROUND} pt
|
||||
{key} must be Unix seconds (an integer) or null
|
||||
{key} must be [x,y]
|
||||
{key} must be a percentage {min}..100
|
||||
{key} must be a positive number, not {v}
|
||||
{key} must be an array of non-negative integer object ids
|
||||
{key} must be one of {_1}
|
||||
|
||||
@@ -4973,5 +4973,11 @@ theme Appearance Giao diện
|
||||
Water Paper Giấy ướt
|
||||
Ruby… Ruby…
|
||||
Ruby Ruby
|
||||
Ease In Chậm lúc đầu
|
||||
Ease Out Chậm lúc cuối
|
||||
Ease In Out Chậm lúc đầu và cuối
|
||||
Ease Out In Chậm lúc cuối và đầu
|
||||
Ramp: Cường độ:
|
||||
Shift: Dịch chuyển:
|
||||
Fresco… Bích họa…
|
||||
Fresco Bích họa
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4023 and column 10.
|
@@ -5217,5 +5217,11 @@ lighting Light 光源
|
||||
Water Paper 水彩画纸
|
||||
Ruby… 注音…
|
||||
Ruby 注音
|
||||
Ease In 缓入
|
||||
Ease Out 缓出
|
||||
Ease In Out 缓入缓出
|
||||
Ease Out In 缓出缓入
|
||||
Ramp: 强度:
|
||||
Shift: 偏移:
|
||||
Fresco… 壁画…
|
||||
Fresco 壁画
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5012,5 +5012,11 @@ lighting Light 光源
|
||||
Water Paper 水彩畫紙
|
||||
Ruby… 注音…
|
||||
Ruby 注音
|
||||
Ease In 緩入
|
||||
Ease Out 緩出
|
||||
Ease In Out 緩入緩出
|
||||
Ease Out In 緩出緩入
|
||||
Ramp: 強度:
|
||||
Shift: 偏移:
|
||||
Fresco… 壁畫…
|
||||
Fresco 壁畫
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -94,6 +94,36 @@ fn settings(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
});
|
||||
let selected = info["target"] == "blend";
|
||||
// Position easing needs a spine to run along: a replaced one, or Align to Path.
|
||||
let spined = info["spine"]["explicit"] == true || info["orientation"] == "path";
|
||||
ui.add_enabled_ui(selected && spined, |ui| {
|
||||
let modes = ["easeIn", "easeOut", "easeInOut", "easeOutIn"];
|
||||
let labels = [tl!("Ease In"), tl!("Ease Out"), tl!("Ease In Out"), tl!("Ease Out In")];
|
||||
let mode = info["easingType"].as_str().unwrap_or(modes[0]);
|
||||
let index = modes.iter().position(|m| *m == mode).unwrap_or(0);
|
||||
widgets::label_row(ui, tl!("Type:"), LABEL_W, |ui| {
|
||||
if let Some(i) = widgets::dropdown(ui, "blend-panel-easing", labels[index], &labels, ui.available_width().max(FIELD_W))
|
||||
&& let Some(mode) = modes.get(i)
|
||||
{
|
||||
change = Some(json!({"easingType": mode}));
|
||||
}
|
||||
});
|
||||
widgets::label_row(ui, tl!("Ramp:"), LABEL_W, |ui| {
|
||||
let mut ramp = info["ramp"].as_f64().unwrap_or(0.0);
|
||||
if ui.add(egui::Slider::new(&mut ramp, 0.0..=100.0).suffix("%")).changed() {
|
||||
change = Some(json!({"ramp": ramp}));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_enabled_ui(selected, |ui| {
|
||||
widgets::label_row(ui, tl!("Shift:"), LABEL_W, |ui| {
|
||||
let mut shift = info["colorShift"].as_f64().unwrap_or(0.0);
|
||||
if ui.add(egui::Slider::new(&mut shift, -100.0..=100.0).suffix("%")).changed() {
|
||||
change = Some(json!({"colorShift": shift}));
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Some(p) = change {
|
||||
app.run("object.blend.options", p).ok();
|
||||
}
|
||||
|
||||
@@ -225,3 +225,37 @@ fn each_button_runs_its_blend_command() {
|
||||
let objects = top(&app);
|
||||
assert!(objects.len() >= 2 && objects.iter().all(|(_, blend)| !blend), "the keys are back: {objects:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_easing_type_ramp_and_shift_edit_the_blend_and_undo() {
|
||||
// Type and Ramp need a spine: Align to Path gives the blend one.
|
||||
let mut app = app();
|
||||
let id = NodeId(app.run("object.blend.make", json!({"smooth": true, "orientation": "path"})).unwrap()["id"].as_u64().unwrap());
|
||||
let mut f = Frames::new();
|
||||
for label in ["Type:", "Ease In", "Ramp:", "Shift:"] {
|
||||
f.text(&mut app, label);
|
||||
}
|
||||
f.click_text(&mut app, "Ease In");
|
||||
f.click_text(&mut app, "Ease Out In");
|
||||
assert_eq!(spec(&app, id).ease.kind, vectorcraft_doc::BlendEaseType::OutIn);
|
||||
// Click each real slider: its command updates the engine, and Undo restores the old value.
|
||||
for (index, key) in [(0, "ramp"), (1, "colorShift")] {
|
||||
let out = f.run(&mut app, vec![]);
|
||||
let nodes = &out.platform_output.accesskit_update.as_ref().unwrap().nodes;
|
||||
let mut sliders: Vec<_> = nodes.iter().filter(|(_, n)| n.role() == egui::accesskit::Role::Slider).collect();
|
||||
// The tree is not in layout order: Ramp sits above Shift.
|
||||
sliders.sort_by(|a, b| a.1.bounds().unwrap().y0.total_cmp(&b.1.bounds().unwrap().y0));
|
||||
let slider = sliders[index];
|
||||
let b = slider.1.bounds().unwrap();
|
||||
let at = pos2((b.x0 + 0.3 * (b.x1 - b.x0)) as f32, ((b.y0 + b.y1) * 0.5) as f32);
|
||||
let before = app.session.execute("object.blend.info", &json!({})).unwrap()[key].clone();
|
||||
f.click(&mut app, at);
|
||||
let after = app.session.execute("object.blend.info", &json!({})).unwrap()[key].clone();
|
||||
assert_ne!(after, before, "{key}");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.execute("object.blend.info", &json!({})).unwrap()[key], before);
|
||||
}
|
||||
app.run("ui.blendOptions", json!({})).unwrap();
|
||||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
|
||||
assert!(!text.contains("Ease In:") && !text.contains("Ramp:") && !text.contains("Color Ease"), "{text}");
|
||||
}
|
||||
|
||||
+8
-1
File diff suppressed because one or more lines are too long
+2
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user