vello/piet-gpu/src/gradient.rs
Raph Levien 307bf8d227 More blend mode fixes
Adds a test to visualize the blend modes. Fixes a dumb bug in blend.h and also a more subtle issue where default blending is not the same as clipping, as the former needs to always push a blend group (to cause isolation) and the latter does not. This might be something we need to get back to.

This should fix the rendering, so it fairly closely resembles the Mozilla reference image. There's also a compile-time switch to disable sRGB conversion, which is (sadly) needed for compatible rendering.
2022-05-17 16:12:05 -07:00

250 lines
7.4 KiB
Rust

// Copyright 2021 The piet-gpu authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Also licensed under MIT license, at your choice.
//! Implementation of gradients.
use std::collections::hash_map::{Entry, HashMap};
use piet::kurbo::Point;
use piet::{Color, FixedLinearGradient, FixedRadialGradient, GradientStop};
/// Radial gradient compatible with COLRv1 spec
#[derive(Debug, Clone)]
pub struct Colrv1RadialGradient {
/// The center of the iner circle.
pub center0: Point,
/// The offset of the origin relative to the center.
pub center1: Point,
/// The radius of the inner circle.
pub radius0: f64,
/// The radius of the outer circle.
pub radius1: f64,
/// The stops.
pub stops: Vec<GradientStop>,
}
#[derive(Clone)]
pub struct BakedGradient {
ramp: Vec<u32>,
}
#[derive(Clone)]
pub struct LinearGradient {
pub(crate) start: [f32; 2],
pub(crate) end: [f32; 2],
pub(crate) ramp_id: u32,
}
#[derive(Clone)]
pub struct RadialGradient {
pub(crate) start: [f32; 2],
pub(crate) end: [f32; 2],
pub(crate) r0: f32,
pub(crate) r1: f32,
pub(crate) ramp_id: u32,
}
#[derive(Default)]
pub struct RampCache {
ramps: Vec<GradientRamp>,
map: HashMap<GradientRamp, usize>,
}
#[derive(Clone, Hash, PartialEq, Eq)]
struct GradientRamp(Vec<u32>);
pub const N_SAMPLES: usize = 512;
// TODO: make this dynamic
pub const N_GRADIENTS: usize = 256;
#[derive(Clone, Copy)]
struct PremulRgba([f64; 4]);
impl PremulRgba {
fn from_color(c: &Color) -> PremulRgba {
let rgba = c.as_rgba();
let a = rgba.3;
// TODO: sRGB nonlinearity? This is complicated.
PremulRgba([rgba.0 * a, rgba.1 * a, rgba.2 * a, a])
}
fn to_u32(&self) -> u32 {
let z = self.0;
let r = (z[0].max(0.0).min(1.0) * 255.0).round() as u32;
let g = (z[1].max(0.0).min(1.0) * 255.0).round() as u32;
let b = (z[2].max(0.0).min(1.0) * 255.0).round() as u32;
let a = (z[3].max(0.0).min(1.0) * 255.0).round() as u32;
r | (g << 8) | (b << 16) | (a << 24)
}
fn lerp(&self, other: PremulRgba, t: f64) -> PremulRgba {
fn l(a: f64, b: f64, t: f64) -> f64 {
a * (1.0 - t) + b * t
}
let a = self.0;
let b = other.0;
PremulRgba([
l(a[0], b[0], t),
l(a[1], b[1], t),
l(a[2], b[2], t),
l(a[3], b[3], t),
])
}
}
impl GradientRamp {
fn from_stops(stops: &[GradientStop]) -> GradientRamp {
let mut last_u = 0.0;
let mut last_c = PremulRgba::from_color(&stops[0].color);
let mut this_u = last_u;
let mut this_c = last_c;
let mut j = 0;
let v = (0..N_SAMPLES)
.map(|i| {
let u = (i as f64) / (N_SAMPLES - 1) as f64;
while u > this_u {
last_u = this_u;
last_c = this_c;
if let Some(s) = stops.get(j + 1) {
this_u = s.pos as f64;
this_c = PremulRgba::from_color(&s.color);
j += 1;
} else {
break;
}
}
let du = this_u - last_u;
let c = if du < 1e-9 {
this_c
} else {
last_c.lerp(this_c, (u - last_u) / du)
};
c.to_u32()
})
.collect();
GradientRamp(v)
}
/// For debugging/development.
pub(crate) fn dump(&self) {
for val in &self.0 {
println!("{:x}", val);
}
}
}
impl RampCache {
/// Add a gradient ramp to the cache.
///
/// Currently there is no eviction, so if the gradient is animating, there may
/// be resource leaks. In order to support lifetime management, the signature
/// should probably change so it returns a ref-counted handle, so that eviction
/// is deferred until the last handle is dropped.
///
/// This function is pretty expensive, but the result is lightweight.
fn add_ramp(&mut self, ramp: &[GradientStop]) -> usize {
let ramp = GradientRamp::from_stops(ramp);
match self.map.entry(ramp) {
Entry::Occupied(o) => *o.get(),
Entry::Vacant(v) => {
let idx = self.ramps.len();
self.ramps.push(v.key().clone());
v.insert(idx);
idx
}
}
}
pub fn add_linear_gradient(&mut self, lin: &FixedLinearGradient) -> LinearGradient {
let ramp_id = self.add_ramp(&lin.stops);
LinearGradient {
ramp_id: ramp_id as u32,
start: crate::render_ctx::to_f32_2(lin.start),
end: crate::render_ctx::to_f32_2(lin.end),
}
}
pub fn add_radial_gradient(&mut self, rad: &FixedRadialGradient) -> RadialGradient {
let ramp_id = self.add_ramp(&rad.stops);
RadialGradient {
ramp_id: ramp_id as u32,
start: crate::render_ctx::to_f32_2(rad.center + rad.origin_offset),
end: crate::render_ctx::to_f32_2(rad.center),
r0: 0.0,
r1: rad.radius as f32,
}
}
pub fn add_radial_gradient_colrv1(&mut self, rad: &Colrv1RadialGradient) -> RadialGradient {
let ramp_id = self.add_ramp(&rad.stops);
RadialGradient {
ramp_id: ramp_id as u32,
start: crate::render_ctx::to_f32_2(rad.center0),
end: crate::render_ctx::to_f32_2(rad.center1),
r0: rad.radius0 as f32,
r1: rad.radius1 as f32,
}
}
/// Dump the contents of a gradient. This is for debugging.
#[allow(unused)]
pub(crate) fn dump_gradient(&self, lin: &LinearGradient) {
println!("id = {}", lin.ramp_id);
self.ramps[lin.ramp_id as usize].dump();
}
/// Get the ramp data.
///
/// This concatenates all the ramps; we'll want a more sophisticated approach to
/// incremental update.
pub fn get_ramp_data(&self) -> Vec<u32> {
let mut result = Vec::with_capacity(N_SAMPLES * self.ramps.len());
for ramp in &self.ramps {
result.extend(&ramp.0);
}
result
}
}
#[cfg(test)]
mod test {
use super::RampCache;
use piet::kurbo::Point;
use piet::{Color, FixedLinearGradient, GradientStop};
#[test]
fn simple_ramp() {
let stops = vec![
GradientStop {
color: Color::WHITE,
pos: 0.0,
},
GradientStop {
color: Color::BLACK,
pos: 1.0,
},
];
let mut cache = RampCache::default();
let lin = FixedLinearGradient {
start: Point::new(0.0, 0.0),
end: Point::new(0.0, 1.0),
stops,
};
let our_lin = cache.add_linear_gradient(&lin);
cache.dump_gradient(&our_lin);
}
}