mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-22 19:52:57 -05:00
Layer Styles (Drop Shadow, Inner Shadow, Outer/Inner Glow, Bevel and Emboss, Satin, Color/Gradient Overlay, Stroke) and Blending Options (Global Light, Fill Opacity, R/G/B, Knockout, Blend Interior Styles as Group) as a layerStyles property group with AE names, order and defaults. Rendered in layer space after effects (exterior passes composite with their own blend modes below the body), cached per pass, scaled with render resolution. Commands layer.style.* (menu Layer > Layer Styles), Global Light shared per comp, timeline eye switches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
417 lines
17 KiB
Rust
417 lines
17 KiB
Rust
//! Layer Styles pixel tests.
|
||
|
||
use effectcraft_color::Label;
|
||
use effectcraft_keyframe::{Gradient, Keyframe, Value};
|
||
use effectcraft_project::build::{self, Ids};
|
||
use effectcraft_project::styles::{self as st, GROUP};
|
||
use effectcraft_project::{Comp, ItemId, ItemKind, Layer, LayerSource, Project, Solid};
|
||
use effectcraft_time::{FrameRate, Tick};
|
||
|
||
use crate::{Image, NoFootage, RenderOpts, Renderer, render_frame};
|
||
|
||
const NORMAL: u32 = 0;
|
||
const MULTIPLY: u32 = 3;
|
||
|
||
fn setup() -> (Project, ItemId, Comp) {
|
||
let mut p = Project::default();
|
||
let comp = Comp::new(200, 100, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
||
(p, cid, comp)
|
||
}
|
||
|
||
fn solid(p: &mut Project, comp: &Comp, color: [f32; 3], w: u32, h: u32) -> Layer {
|
||
let sid = p.add_item("Solid", Label::Red, None, ItemKind::Solid(Solid { color, width: w, height: h, pixel_aspect: 1.0 }));
|
||
build::layer(p, comp, "Solid", LayerSource::Solid { item: sid }, (w, h), None)
|
||
}
|
||
|
||
fn add(p: &mut Project, comp: &Comp, l: &mut Layer, style: &str) {
|
||
let mut next = p.next_id;
|
||
st::add_style(l, &mut Ids(&mut next), style, &comp.global_light, true).unwrap();
|
||
p.next_id = next;
|
||
}
|
||
|
||
fn set(l: &mut Layer, path: &str, v: Value) {
|
||
l.props.prop_mut(&format!("{GROUP}/{path}")).unwrap_or_else(|| panic!("no {path}")).value = v;
|
||
}
|
||
|
||
/// A 40×40 square of `color` centred in a 200×100 comp (x 80..120, y 30..70) over an optional
|
||
/// full-frame background.
|
||
fn scene(color: [f32; 3], bg: Option<[f32; 3]>, f: impl FnOnce(&mut Project, &Comp, &mut Layer)) -> (Project, ItemId) {
|
||
let (mut p, cid, comp) = setup();
|
||
let mut l = solid(&mut p, &comp, color, 40, 40);
|
||
f(&mut p, &comp, &mut l);
|
||
let mut layers = vec![l];
|
||
if let Some(bg) = bg {
|
||
layers.push(solid(&mut p, &comp, bg, 200, 100));
|
||
}
|
||
p.comp_mut(cid).unwrap().layers = layers;
|
||
(p, cid)
|
||
}
|
||
|
||
fn render(p: &Project, cid: ItemId) -> Image {
|
||
render_frame(p, cid, Tick::ZERO, 1.0)
|
||
}
|
||
|
||
fn near(a: f32, b: f32, tol: f32) -> bool {
|
||
(a - b).abs() <= tol
|
||
}
|
||
|
||
#[test]
|
||
fn drop_shadow_offset_colour_and_opacity() {
|
||
let (p, cid) = scene([1.0; 3], Some([0.5; 3]), |p, c, l| {
|
||
add(p, c, l, "dropShadow");
|
||
set(l, "dropShadow/size", Value::Scalar(0.0));
|
||
set(l, "dropShadow/distance", Value::Scalar(10.0));
|
||
set(l, "dropShadow/opacity", Value::Scalar(100.0));
|
||
set(l, "dropShadow/blendMode", Value::Enum(NORMAL));
|
||
});
|
||
let img = render(&p, cid);
|
||
// Global light 120° → the shadow falls down-right by (5, 8.66).
|
||
assert!(near(img.get(123, 75)[0], 0.0, 0.02), "shadow: {:?}", img.get(123, 75));
|
||
assert!(near(img.get(82, 75)[0], 0.5, 0.01), "left of the shadow stays grey");
|
||
assert!(near(img.get(100, 50)[0], 1.0, 0.01), "layer on top");
|
||
assert!(near(img.get(100, 20)[0], 0.5, 0.01), "above the layer");
|
||
|
||
// 75 % Multiply (the default mode) over grey: 0.5 × (1 − 0.75).
|
||
let (p, cid) = scene([1.0; 3], Some([0.5; 3]), |p, c, l| {
|
||
add(p, c, l, "dropShadow");
|
||
set(l, "dropShadow/size", Value::Scalar(0.0));
|
||
set(l, "dropShadow/distance", Value::Scalar(10.0));
|
||
set(l, "dropShadow/color", Value::Color([0.0, 0.0, 0.0, 1.0]));
|
||
});
|
||
let img = render(&p, cid);
|
||
assert!(near(img.get(123, 75)[0], 0.125, 0.02), "{:?}", img.get(123, 75));
|
||
|
||
// Coloured shadow on a transparent comp extends the layer's bounds.
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "dropShadow");
|
||
set(l, "dropShadow/size", Value::Scalar(0.0));
|
||
set(l, "dropShadow/distance", Value::Scalar(10.0));
|
||
set(l, "dropShadow/opacity", Value::Scalar(50.0));
|
||
set(l, "dropShadow/blendMode", Value::Enum(NORMAL));
|
||
set(l, "dropShadow/color", Value::Color([0.0, 0.0, 1.0, 1.0]));
|
||
});
|
||
let img = render(&p, cid);
|
||
let px = img.straight(123, 75);
|
||
assert!(near(px[3], 0.5, 0.02) && near(px[2], 1.0, 0.01) && px[0] < 0.01, "{px:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn soft_shadow_falls_off() {
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "dropShadow");
|
||
set(l, "dropShadow/size", Value::Scalar(10.0));
|
||
set(l, "dropShadow/distance", Value::Scalar(0.0));
|
||
set(l, "dropShadow/opacity", Value::Scalar(100.0));
|
||
});
|
||
let img = render(&p, cid);
|
||
let a1 = img.get(121, 50)[3];
|
||
let a5 = img.get(125, 50)[3];
|
||
let a20 = img.get(140, 50)[3];
|
||
assert!(a1 > a5 && a5 > a20, "{a1} {a5} {a20}");
|
||
assert!(a20 < 0.01);
|
||
}
|
||
|
||
#[test]
|
||
fn stroke_positions() {
|
||
let render_stroke = |pos: u32| {
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "stroke");
|
||
set(l, "stroke/size", Value::Scalar(4.0));
|
||
set(l, "stroke/position", Value::Enum(pos));
|
||
});
|
||
render(&p, cid)
|
||
};
|
||
let red = |px: [f32; 4]| px[0] > 0.98 && px[1] < 0.02 && px[3] > 0.98;
|
||
let white = |px: [f32; 4]| px[0] > 0.98 && px[1] > 0.98 && px[3] > 0.98;
|
||
// Outside: 4 px ring beyond the right edge (x = 120).
|
||
let o = render_stroke(0);
|
||
assert!(red(o.get(122, 50)) && red(o.get(123, 50)), "{:?}", o.get(122, 50));
|
||
assert!(o.get(126, 50)[3] < 0.01);
|
||
assert!(white(o.get(118, 50)));
|
||
// Inside: the ring is within the layer.
|
||
let i = render_stroke(1);
|
||
assert!(red(i.get(118, 50)) && red(i.get(117, 50)));
|
||
assert!(white(i.get(110, 50)));
|
||
assert!(i.get(122, 50)[3] < 0.01);
|
||
// Centre: 2 px on each side.
|
||
let c = render_stroke(2);
|
||
assert!(red(c.get(119, 50)) && red(c.get(120, 50)) && red(c.get(121, 50)));
|
||
assert!(white(c.get(116, 50)));
|
||
assert!(c.get(124, 50)[3] < 0.01);
|
||
}
|
||
|
||
#[test]
|
||
fn colour_overlay_blend_modes() {
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "colorOverlay");
|
||
set(l, "colorOverlay/color", Value::Color([0.0, 0.0, 1.0, 1.0]));
|
||
set(l, "colorOverlay/opacity", Value::Scalar(50.0));
|
||
});
|
||
let px = render(&p, cid).get(100, 50);
|
||
assert!(near(px[0], 0.5, 0.01) && near(px[2], 1.0, 0.01), "{px:?}");
|
||
|
||
let (p, cid) = scene([0.5, 1.0, 1.0], None, |p, c, l| {
|
||
add(p, c, l, "colorOverlay");
|
||
set(l, "colorOverlay/color", Value::Color([1.0, 0.5, 0.0, 1.0]));
|
||
set(l, "colorOverlay/blendMode", Value::Enum(MULTIPLY));
|
||
});
|
||
let img = render(&p, cid);
|
||
let px = img.get(100, 50);
|
||
assert!(near(px[0], 0.5, 0.01) && near(px[1], 0.5, 0.01) && near(px[2], 0.0, 0.01), "{px:?}");
|
||
// Overlays stay inside the layer.
|
||
assert!(img.get(130, 50)[3] < 0.01);
|
||
}
|
||
|
||
#[test]
|
||
fn gradient_overlay_follows_angle() {
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "gradientOverlay");
|
||
});
|
||
let img = render(&p, cid);
|
||
// 90°: black at the bottom, white at the top.
|
||
let top = img.get(100, 31)[0];
|
||
let bottom = img.get(100, 68)[0];
|
||
assert!(top > 0.9 && bottom < 0.1, "{top} {bottom}");
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "gradientOverlay");
|
||
set(l, "gradientOverlay/angle", Value::Scalar(0.0));
|
||
set(l, "gradientOverlay/reverse", Value::Bool(true));
|
||
});
|
||
let img = render(&p, cid);
|
||
assert!(img.get(81, 50)[0] > 0.9 && img.get(118, 50)[0] < 0.1);
|
||
}
|
||
|
||
fn bevel_scene(angle: f64) -> Image {
|
||
let (p, cid) = scene([0.5; 3], None, |p, c, l| {
|
||
add(p, c, l, "bevelEmboss");
|
||
set(l, "bevelEmboss/size", Value::Scalar(6.0));
|
||
set(l, "blendingOptions/globalLightAngle", Value::Scalar(angle));
|
||
});
|
||
render(&p, cid)
|
||
}
|
||
|
||
#[test]
|
||
fn bevel_lights_the_side_facing_the_light() {
|
||
// Light from the upper left (120°): left and top edges brighten, right and bottom darken.
|
||
let img = bevel_scene(120.0);
|
||
let (l, r, t, b, mid) = (img.get(82, 50)[0], img.get(117, 50)[0], img.get(100, 32)[0], img.get(100, 67)[0], img.get(100, 50)[0]);
|
||
assert!(l > 0.6 && t > 0.6, "highlights {l} {t}");
|
||
assert!(r < 0.4 && b < 0.4, "shadows {r} {b}");
|
||
assert!(near(mid, 0.5, 0.02), "flat centre {mid}");
|
||
// Light from the lower right (−60°) swaps them.
|
||
let img = bevel_scene(-60.0);
|
||
assert!(img.get(82, 50)[0] < 0.4 && img.get(117, 50)[0] > 0.6);
|
||
}
|
||
|
||
#[test]
|
||
fn global_light_drives_linked_styles() {
|
||
let shadow_at = |use_global: bool| {
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "dropShadow");
|
||
set(l, "dropShadow/size", Value::Scalar(0.0));
|
||
set(l, "dropShadow/distance", Value::Scalar(10.0));
|
||
set(l, "dropShadow/opacity", Value::Scalar(100.0));
|
||
set(l, "dropShadow/useGlobalLight", Value::Bool(use_global));
|
||
set(l, "dropShadow/angle", Value::Scalar(90.0));
|
||
set(l, "blendingOptions/globalLightAngle", Value::Scalar(0.0));
|
||
});
|
||
render(&p, cid)
|
||
};
|
||
// Global light at 0° (from the right): shadow to the left.
|
||
let g = shadow_at(true);
|
||
assert!(g.get(75, 50)[3] > 0.98 && g.get(100, 75)[3] < 0.01);
|
||
// Own angle 90° (from above): shadow below.
|
||
let o = shadow_at(false);
|
||
assert!(o.get(100, 75)[3] > 0.98 && o.get(75, 50)[3] < 0.01);
|
||
}
|
||
|
||
#[test]
|
||
fn glows_and_inner_shadow() {
|
||
let (p, cid) = scene([0.0, 0.0, 0.0], None, |p, c, l| {
|
||
add(p, c, l, "outerGlow");
|
||
add(p, c, l, "innerGlow");
|
||
set(l, "outerGlow/opacity", Value::Scalar(100.0));
|
||
set(l, "outerGlow/size", Value::Scalar(8.0));
|
||
set(l, "innerGlow/opacity", Value::Scalar(100.0));
|
||
set(l, "innerGlow/size", Value::Scalar(8.0));
|
||
});
|
||
let img = render(&p, cid);
|
||
let out_near = img.straight(121, 50);
|
||
assert!(out_near[3] > 0.6 && out_near[0] > 0.9 && out_near[2] < 0.9, "glow colour {out_near:?}");
|
||
assert!(img.get(140, 50)[3] < 0.02, "glow fades");
|
||
let edge = img.get(119, 50);
|
||
let centre = img.get(100, 50);
|
||
assert!(edge[0] > 0.6 && centre[0] < 0.05, "inner glow from the edge {edge:?} {centre:?}");
|
||
|
||
// Inner glow from the centre instead.
|
||
let (p, cid) = scene([0.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "innerGlow");
|
||
set(l, "innerGlow/source", Value::Enum(0));
|
||
set(l, "innerGlow/opacity", Value::Scalar(100.0));
|
||
});
|
||
let img = render(&p, cid);
|
||
assert!(img.get(100, 50)[0] > 0.9 && img.get(119, 50)[0] < 0.6);
|
||
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "innerShadow");
|
||
set(l, "innerShadow/size", Value::Scalar(0.0));
|
||
set(l, "innerShadow/opacity", Value::Scalar(100.0));
|
||
set(l, "innerShadow/blendMode", Value::Enum(NORMAL));
|
||
});
|
||
let img = render(&p, cid);
|
||
// Light from the upper left: the shadow lines the top-left inside edges.
|
||
assert!(img.get(81, 50)[0] < 0.05 && img.get(100, 32)[0] < 0.05, "{:?}", img.get(81, 50));
|
||
assert!(img.get(117, 50)[0] > 0.95 && img.get(100, 66)[0] > 0.95);
|
||
assert!(img.get(76, 50)[3] < 0.01, "inner shadow stays inside");
|
||
}
|
||
|
||
#[test]
|
||
fn satin_shades_inside() {
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| add(p, c, l, "satin"));
|
||
let img = render(&p, cid);
|
||
let vals: Vec<f32> = (82..118).map(|x| img.get(x, 50)[0]).collect();
|
||
let min = vals.iter().cloned().fold(1.0, f32::min);
|
||
assert!(min < 0.9, "satin darkens somewhere: {min}");
|
||
assert!(img.get(130, 50)[3] < 0.01);
|
||
}
|
||
|
||
#[test]
|
||
fn fill_opacity_knockout_and_channels() {
|
||
// Fill 0: the content disappears but the stroke stays.
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "stroke");
|
||
set(l, "blendingOptions/advancedBlending/fillOpacity", Value::Scalar(0.0));
|
||
});
|
||
let img = render(&p, cid);
|
||
assert!(img.get(100, 50)[3] < 0.01 && img.get(122, 50)[0] > 0.98);
|
||
|
||
// Interior styles ignore Fill Opacity unless blended as a group.
|
||
let overlay = |group: bool| {
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "colorOverlay");
|
||
set(l, "blendingOptions/advancedBlending/fillOpacity", Value::Scalar(0.0));
|
||
set(l, "blendingOptions/advancedBlending/blendInteriorAsGroup", Value::Bool(group));
|
||
});
|
||
render(&p, cid).get(100, 50)
|
||
};
|
||
assert!(overlay(false)[3] > 0.98 && overlay(true)[3] < 0.01);
|
||
|
||
// Knockout with Fill 0 punches through the background.
|
||
let (p, cid) = scene([1.0; 3], Some([0.5; 3]), |p, c, l| {
|
||
add(p, c, l, "stroke");
|
||
set(l, "blendingOptions/advancedBlending/fillOpacity", Value::Scalar(0.0));
|
||
set(l, "blendingOptions/advancedBlending/knockout", Value::Enum(2));
|
||
});
|
||
let img = render(&p, cid);
|
||
assert!(img.get(100, 50)[3] < 0.01 && img.get(150, 50)[3] > 0.99);
|
||
|
||
// Red channel off: the white layer leaves red from below.
|
||
let (p, cid) = scene([1.0; 3], Some([0.0; 3]), |p, c, l| {
|
||
add(p, c, l, "stroke");
|
||
set(l, "blendingOptions/advancedBlending/red", Value::Bool(false));
|
||
});
|
||
let px = render(&p, cid).get(100, 50);
|
||
assert!(px[0] < 0.01 && px[1] > 0.99 && px[2] > 0.99, "{px:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn eye_switch_and_group_switch() {
|
||
let (mut p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "stroke");
|
||
});
|
||
assert!(render(&p, cid).get(122, 50)[3] > 0.98);
|
||
p.comp_mut(cid).unwrap().layers[0].props.group_mut(&format!("{GROUP}/stroke")).unwrap().enabled = false;
|
||
assert!(render(&p, cid).get(122, 50)[3] < 0.01);
|
||
p.comp_mut(cid).unwrap().layers[0].props.group_mut(&format!("{GROUP}/stroke")).unwrap().enabled = true;
|
||
p.comp_mut(cid).unwrap().layers[0].props.group_mut(GROUP).unwrap().enabled = false;
|
||
assert!(render(&p, cid).get(122, 50)[3] < 0.01);
|
||
}
|
||
|
||
#[test]
|
||
fn styles_respect_render_scale() {
|
||
let (p, cid) = scene([1.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "dropShadow");
|
||
add(p, c, l, "stroke");
|
||
set(l, "dropShadow/size", Value::Scalar(0.0));
|
||
set(l, "dropShadow/distance", Value::Scalar(20.0));
|
||
set(l, "dropShadow/opacity", Value::Scalar(100.0));
|
||
set(l, "stroke/size", Value::Scalar(4.0));
|
||
});
|
||
let full = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||
let half = render_frame(&p, cid, Tick::ZERO, 0.5);
|
||
// Shadow offset (10, 17.3): at half resolution the shadow sits at half the distance.
|
||
assert!(full.get(125, 85)[3] > 0.98);
|
||
assert!(half.get(62, 42)[3] > 0.98 && half.get(55, 25)[3] > 0.98);
|
||
// Stroke 4 px → 2 px at half.
|
||
assert!(half.get(61, 25)[0] > 0.98 && half.get(61, 25)[1] < 0.05);
|
||
assert!(half.get(64, 25)[3] < 0.01 || half.get(64, 25)[1] < 0.05);
|
||
}
|
||
|
||
#[test]
|
||
fn animated_style_properties() {
|
||
let (mut p, cid) = scene([1.0; 3], None, |p, c, l| add(p, c, l, "stroke"));
|
||
let l = &mut p.comp_mut(cid).unwrap().layers[0];
|
||
l.props.prop_mut(&format!("{GROUP}/stroke/size")).unwrap().keys =
|
||
vec![Keyframe::new(Tick::ZERO, Value::Scalar(2.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(10.0))];
|
||
let a = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||
let b = render_frame(&p, cid, Tick::from_seconds_f64(1.0), 1.0);
|
||
assert!(a.get(126, 50)[3] < 0.01 && b.get(126, 50)[3] > 0.98);
|
||
}
|
||
|
||
fn render_cached(p: &Project, cid: ItemId, cache: &crate::LayerCache) -> Image {
|
||
let mut r = Renderer::new(p, &NoFootage, RenderOpts::default());
|
||
r.cache = Some(cache);
|
||
r.comp_frame(cid, Tick::ZERO)
|
||
}
|
||
|
||
#[test]
|
||
fn style_edits_invalidate_the_cache() {
|
||
let (mut p, cid) = scene([1.0; 3], Some([0.5; 3]), |p, c, l| {
|
||
add(p, c, l, "dropShadow");
|
||
add(p, c, l, "stroke");
|
||
});
|
||
let cache = crate::LayerCache::default();
|
||
let a = render_cached(&p, cid, &cache);
|
||
assert_eq!(a.data, render(&p, cid).data);
|
||
let again = render_cached(&p, cid, &cache);
|
||
assert_eq!(a.data, again.data);
|
||
// Change a style value: the styled layer re-renders, its content (and the background) come
|
||
// from the cache.
|
||
let hits0 = cache.stats().hits;
|
||
let l = &mut p.comp_mut(cid).unwrap().layers[0];
|
||
l.props.prop_mut(&format!("{GROUP}/stroke/color")).unwrap().value = Value::Color([0.0, 1.0, 0.0, 1.0]);
|
||
let b = render_cached(&p, cid, &cache);
|
||
assert_eq!(b.data, render(&p, cid).data);
|
||
assert_ne!(a.data, b.data);
|
||
assert!(cache.stats().hits >= hits0 + 2, "content + background reused");
|
||
// Global light change (linked) also invalidates.
|
||
let l = &mut p.comp_mut(cid).unwrap().layers[0];
|
||
l.props.prop_mut(&format!("{GROUP}/blendingOptions/globalLightAngle")).unwrap().value = Value::Scalar(0.0);
|
||
let c = render_cached(&p, cid, &cache);
|
||
assert_eq!(c.data, render(&p, cid).data);
|
||
assert_ne!(b.data, c.data);
|
||
// Eye switch off.
|
||
p.comp_mut(cid).unwrap().layers[0].props.group_mut(&format!("{GROUP}/dropShadow")).unwrap().enabled = false;
|
||
let d = render_cached(&p, cid, &cache);
|
||
assert_eq!(d.data, render(&p, cid).data);
|
||
assert_ne!(c.data, d.data);
|
||
}
|
||
|
||
#[test]
|
||
fn gradient_glow_uses_gradient() {
|
||
let (p, cid) = scene([0.0; 3], None, |p, c, l| {
|
||
add(p, c, l, "outerGlow");
|
||
set(l, "outerGlow/colorType", Value::Enum(1));
|
||
set(l, "outerGlow/opacity", Value::Scalar(100.0));
|
||
set(
|
||
l,
|
||
"outerGlow/colors",
|
||
Value::Gradient(Gradient { colors: vec![(0.0, [1.0, 0.0, 0.0, 1.0]), (1.0, [0.0, 0.0, 1.0, 1.0])], opacities: vec![(0.0, 1.0), (1.0, 0.0)] }),
|
||
);
|
||
});
|
||
let img = render(&p, cid);
|
||
let px = img.straight(121, 50);
|
||
assert!(px[0] > px[2], "near the edge the glow takes the gradient's start colour {px:?}");
|
||
}
|