mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-03 15:08:31 -06:00
Gradients: bound the stops sampled per pixel
Gradient sampling scans the stops linearly and shape painting samples once per pixel, so a gradient with a huge stop list multiplied every frame's work. Sampling now looks at the first 256 colour and opacity stops, and the plain JSON form (coerce_json) refuses gradients with more than 256.
This commit is contained in:
@@ -130,6 +130,24 @@ fn gradient_tracks_sample_separately() {
|
||||
assert_eq!((empty.color_at(0.3), empty.opacity_at(0.3)), ([1.0; 4], 1.0));
|
||||
}
|
||||
|
||||
/// A gradient's stop count multiplies every painted pixel's sampling: plain JSON with more than
|
||||
/// `MAX_GRADIENT_STOPS` is refused, and a longer stored list is sampled through its first ones.
|
||||
#[test]
|
||||
fn gradient_stop_count_is_bounded() {
|
||||
use crate::value::MAX_GRADIENT_STOPS;
|
||||
let g = Value::Gradient(Gradient::default());
|
||||
let many: Vec<serde_json::Value> = (0..=MAX_GRADIENT_STOPS).map(|_| serde_json::json!("#ff0000")).collect();
|
||||
assert_eq!(g.coerce_json(&serde_json::Value::Array(many.clone())), None);
|
||||
assert!(g.coerce_json(&serde_json::Value::Array(many[..MAX_GRADIENT_STOPS].to_vec())).is_some());
|
||||
let ops: Vec<serde_json::Value> = (0..=MAX_GRADIENT_STOPS).map(|i| serde_json::json!([i as f64 / MAX_GRADIENT_STOPS as f64, 1.0])).collect();
|
||||
assert_eq!(g.coerce_json(&serde_json::json!({"colors": ["#000000", "#ffffff"], "opacities": ops})), None);
|
||||
// A stored gradient with a huge tail: stops past the limit don't take part in sampling.
|
||||
let mut colors: Vec<(f64, [f32; 4])> = (0..MAX_GRADIENT_STOPS).map(|i| (i as f64 / MAX_GRADIENT_STOPS as f64 * 0.5, [0.0, 0.0, 0.0, 1.0])).collect();
|
||||
colors.extend((0..100_000).map(|_| (1.0, [1.0, 1.0, 1.0, 1.0])));
|
||||
let big = Gradient { colors, opacities: vec![(0.0, 1.0)] };
|
||||
assert_eq!(big.sample(1.0), [0.0, 0.0, 0.0, 1.0]);
|
||||
}
|
||||
|
||||
/// #612: the editor saves the typed form, so one colour stop (which the plain JSON parser
|
||||
/// rejects) and each stop's RGBA come back exactly.
|
||||
#[test]
|
||||
|
||||
@@ -210,7 +210,13 @@ impl Gradient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Most stops (colour or opacity) a gradient is sampled through. Gradients are sampled per
|
||||
/// pixel, so the stop count multiplies every painted pixel's work: stops past this are ignored
|
||||
/// when sampling, and a gradient written as JSON with more is refused.
|
||||
pub const MAX_GRADIENT_STOPS: usize = 256;
|
||||
|
||||
fn sample_stops<T: Copy>(stops: &[(f64, T)], t: f64, lerp: impl Fn(T, T, f64) -> T) -> Option<T> {
|
||||
let stops = &stops[..stops.len().min(MAX_GRADIENT_STOPS)];
|
||||
let first = stops.first()?;
|
||||
if t <= first.0 {
|
||||
return Some(first.1);
|
||||
@@ -243,7 +249,7 @@ fn gradient_json(j: &serde_json::Value) -> Option<Gradient> {
|
||||
}
|
||||
fn stops(a: &[J]) -> Option<Vec<(f64, [f32; 4])>> {
|
||||
let n = a.len();
|
||||
if n < 2 {
|
||||
if !(2..=MAX_GRADIENT_STOPS).contains(&n) {
|
||||
return None;
|
||||
}
|
||||
a.iter()
|
||||
@@ -259,6 +265,7 @@ fn gradient_json(j: &serde_json::Value) -> Option<Gradient> {
|
||||
J::Object(o) => {
|
||||
let colors = stops(o.get("colors")?.as_array()?)?;
|
||||
let op = match o.get("opacities") {
|
||||
Some(J::Array(a)) if a.len() > MAX_GRADIENT_STOPS => return None,
|
||||
Some(J::Array(a)) => Some(
|
||||
a.iter()
|
||||
.map(|s| {
|
||||
|
||||
Reference in New Issue
Block a user