use crate::paint::{GradientStop, ImageFilter, MaskBlurStyle, Paint, Shader, Style};
use crate::ramp::Ramp;
use crate::vertices::{Sprite, VertexMode, Vertices};
use crate::Color;
use glam::{Affine2, Mat2, Vec2};
use impeller_geometry::stroke::{LineCap, LineJoin, StrokeStyle};
use impeller_geometry::transform::{
invert_to_local, max_scale, preserves_axis_alignment, to_local_columns, transformed_bounds,
unbounded, viewport_projection, Transform2D,
};
use impeller_geometry::{FillRule, Path, PathBuilder, Rect as GeometryRect};
use impeller_hal::{
Batch, BlendMode, ClipState, ColorFilter, Error, Extent2D, Material, PassDescriptor,
PassViewport, Result, Sampling, Scissor, Stop, TileMode, Vertex, MAX_STOPS, MORPHOLOGY_TAPS,
};
use impeller_renderer::{Paint as RenderPaint, Renderer, TOLERANCE};
use impeller_text::{Atlas, PositionedGlyph};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub left: f32,
pub top: f32,
pub right: f32,
pub bottom: f32,
}
impl Rect {
pub fn new(left: f32, top: f32, right: f32, bottom: f32) -> Self {
Self {
left,
top,
right,
bottom,
}
}
pub fn from_size(width: f32, height: f32) -> Self {
Self::new(0.0, 0.0, width, height)
}
pub fn width(&self) -> f32 {
self.right - self.left
}
pub fn height(&self) -> f32 {
self.bottom - self.top
}
pub fn is_empty(&self) -> bool {
self.width() <= 0.0 || self.height() <= 0.0
}
pub fn outset(self, reach: f32) -> Self {
Self::new(
self.left - reach,
self.top - reach,
self.right + reach,
self.bottom + reach,
)
}
fn to_path(self) -> Path {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(self.left, self.top))
.line_to(Vec2::new(self.right, self.top))
.line_to(Vec2::new(self.right, self.bottom))
.line_to(Vec2::new(self.left, self.bottom))
.close();
b.as_rounded_rect(
GeometryRect::new(
Vec2::new(self.left, self.top),
Vec2::new(self.right, self.bottom),
),
0.0,
);
b.build()
}
pub fn to_rounded_path(self, radius: f32) -> Path {
if self.is_empty() {
return Path::default();
}
if radius.is_nan() || radius <= 0.0 {
return self.to_path();
}
let mut path = PathBuilder::new();
self.add_rounded_contour(&mut path, radius);
path.as_rounded_rect(
GeometryRect::new(
Vec2::new(self.left, self.top),
Vec2::new(self.right, self.bottom),
),
radius.min(self.width() / 2.0).min(self.height() / 2.0),
);
path.build()
}
pub fn to_rounded_path_with_radii(self, radii: RoundingRadii) -> Path {
if self.is_empty() {
return Path::default();
}
let radii = self.fitted_radii(radii);
if radii.iter().flatten().all(|v| *v <= 0.0) {
return self.to_path();
}
let mut path = PathBuilder::new();
self.add_rounded_contour_with_radii(&mut path, radii);
path.build()
}
pub fn add_rounded_outline(self, path: &mut PathBuilder, radii: RoundingRadii) {
if self.is_empty() {
return;
}
let radii = self.fitted_radii(radii);
self.add_rounded_contour_with_radii(path, radii);
}
fn fitted_radii(self, radii: RoundingRadii) -> RoundingRadii {
let (w, h) = (self.width(), self.height());
let mut r = radii;
for corner in r.iter_mut() {
for value in corner.iter_mut() {
*value = if value.is_nan() || *value <= 0.0 {
0.0
} else if value.is_infinite() {
w.max(h)
} else {
*value
};
}
}
let [tl, tr, bl, br] = r;
let mut scale = 1.0f32;
for (side, sum) in [
(w, tl[0] + tr[0]),
(h, tr[1] + br[1]),
(w, bl[0] + br[0]),
(h, tl[1] + bl[1]),
] {
if sum > side {
scale = scale.min(side / sum);
}
}
if scale < 1.0 {
for corner in r.iter_mut() {
for value in corner.iter_mut() {
*value *= scale;
}
}
}
r
}
fn add_rounded_contour(self, path: &mut PathBuilder, radius: f32) {
let radii = self.fitted_radii([[radius; 2]; 4]);
self.add_rounded_contour_with_radii(path, radii);
}
fn add_rounded_contour_with_radii(self, path: &mut PathBuilder, radii: RoundingRadii) {
let [tl, tr, bl, br] = radii;
let k = |v: f32| KAPPA * v;
let (l, t, r, b) = (self.left, self.top, self.right, self.bottom);
path.move_to(Vec2::new(l + tl[0], t))
.line_to(Vec2::new(r - tr[0], t))
.cubic_to(
Vec2::new(r - tr[0] + k(tr[0]), t),
Vec2::new(r, t + tr[1] - k(tr[1])),
Vec2::new(r, t + tr[1]),
)
.line_to(Vec2::new(r, b - br[1]))
.cubic_to(
Vec2::new(r, b - br[1] + k(br[1])),
Vec2::new(r - br[0] + k(br[0]), b),
Vec2::new(r - br[0], b),
)
.line_to(Vec2::new(l + bl[0], b))
.cubic_to(
Vec2::new(l + bl[0] - k(bl[0]), b),
Vec2::new(l, b - bl[1] + k(bl[1])),
Vec2::new(l, b - bl[1]),
)
.line_to(Vec2::new(l, t + tl[1]))
.cubic_to(
Vec2::new(l, t + tl[1] - k(tl[1])),
Vec2::new(l + tl[0] - k(tl[0]), t),
Vec2::new(l + tl[0], t),
)
.close();
}
}
fn uniform_circular_radius(radii: RoundingRadii) -> Option<f32> {
let radius = radii[0][0];
radii
.iter()
.flatten()
.all(|v| *v == radius)
.then_some(radius)
}
pub type RoundingRadii = [[f32; 2]; 4];
fn rsuperellipse_path(rect: Rect, radii: RoundingRadii) -> Path {
use impeller_geometry::superellipse::{CornerRadii, RoundSuperellipse};
let bounds = GeometryRect::new(
Vec2::new(rect.left, rect.top),
Vec2::new(rect.right, rect.bottom),
);
RoundSuperellipse::new(
bounds,
CornerRadii {
top_left: Vec2::new(radii[0][0], radii[0][1]),
top_right: Vec2::new(radii[1][0], radii[1][1]),
bottom_left: Vec2::new(radii[2][0], radii[2][1]),
bottom_right: Vec2::new(radii[3][0], radii[3][1]),
},
)
.to_path()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureSource {
Image(u32),
Layer(usize),
Ramp(usize),
}
#[derive(Clone)]
pub struct Pass {
pub batch: Batch,
pub descriptor: PassDescriptor,
pub sources: Vec<TextureSource>,
pub extent: Extent2D,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PointMode {
#[default]
Points,
Lines,
Polygon,
}
pub struct Recording {
pub passes: Vec<Pass>,
pub extent: Extent2D,
pub ramps: Vec<Ramp>,
}
impl Recording {
pub fn draw_count(&self) -> usize {
self.passes.iter().map(|p| p.batch.draw_count()).sum()
}
pub fn is_empty(&self) -> bool {
self.passes.iter().all(|p| p.batch.is_empty())
}
pub fn root(&self) -> &Pass {
self.passes.last().expect("a recording always has a root")
}
pub fn layer_count(&self) -> usize {
self.passes.len() - 1
}
}
#[derive(Debug, Clone, Copy)]
struct Target {
origin: Vec2,
extent: Extent2D,
}
impl Target {
fn frame(extent: Extent2D) -> Self {
Self {
origin: Vec2::ZERO,
extent,
}
}
fn projection(&self) -> Transform2D {
(viewport_projection(self.extent.width, self.extent.height)
* Affine2::from_translation(-self.origin))
.into()
}
fn path(&self) -> Path {
Rect::new(
self.origin.x,
self.origin.y,
self.origin.x + self.extent.width as f32,
self.origin.y + self.extent.height as f32,
)
.to_path()
}
}
#[derive(Debug, Clone)]
struct StencilClip {
path: Path,
transform: Transform2D,
scissor: Option<Scissor>,
}
#[derive(Debug)]
struct SavedState {
transform: Transform2D,
clip: Option<Scissor>,
clip_bounds: Rect,
depth: u32,
clips: usize,
layer: Option<LayerFrame>,
}
#[derive(Debug)]
struct LayerFrame {
batch: Batch,
sources: Vec<TextureSource>,
clips: Vec<StencilClip>,
paint: Layer,
filter: Option<ImageFilter>,
parent: Target,
anti_alias: bool,
blur_basis: BlurBasis,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct BlurBasis {
directions: [Vec2; 2],
}
impl BlurBasis {
const AXES: Self = Self {
directions: [Vec2::X, Vec2::Y],
};
fn of(transform: Transform2D) -> Self {
let Some(affine) = transform.to_affine() else {
return Self::AXES;
};
let (x, y) = (affine.matrix2.x_axis, affine.matrix2.y_axis);
let (lx, ly) = (x.length(), y.length());
if !lx.is_finite() || !ly.is_finite() || lx <= 0.0 || ly <= 0.0 {
return Self::AXES;
}
let directions = [x / lx, y / ly];
if directions[0].dot(directions[1]).abs() > 1e-3 {
return Self::AXES;
}
Self { directions }
}
fn is_axis_aligned(&self) -> bool {
*self == Self::AXES
}
fn reach(&self, sigma: Vec2) -> Vec2 {
let [u, v] = self.directions;
let (ru, rv) = (blur_reach(sigma.x), blur_reach(sigma.y));
Vec2::new(
u.x.abs() * ru + v.x.abs() * rv,
u.y.abs() * ru + v.y.abs() * rv,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Morphology {
pub radius: [f32; 2],
pub dilate: bool,
}
impl Morphology {
pub fn dilate(x: f32, y: f32) -> Self {
Self {
radius: Self::sane(x, y),
dilate: true,
}
}
pub fn erode(x: f32, y: f32) -> Self {
Self {
radius: Self::sane(x, y),
dilate: false,
}
}
fn sane(x: f32, y: f32) -> [f32; 2] {
let one = |v: f32| {
if v.is_finite() {
v.max(0.0).round()
} else {
0.0
}
};
[one(x), one(y)]
}
pub fn is_identity(&self) -> bool {
self.radius == [0.0, 0.0]
}
}
#[derive(Clone, Copy)]
enum Masked<'a> {
Path(&'a Path),
Mesh(&'a Vertices),
Glyphs {
glyphs: &'a [PositionedGlyph],
atlas: &'a Atlas,
slot: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Layer {
pub blur: Vec2,
pub alpha: f32,
pub blend: BlendMode,
pub matrix: Option<Transform2D>,
pub backdrop_blur: f32,
pub backdrop_id: Option<i64>,
pub morphology: Option<Morphology>,
pub color_filter: ColorFilter,
}
impl Default for Layer {
fn default() -> Self {
Self {
blur: Vec2::ZERO,
alpha: 1.0,
blend: BlendMode::SrcOver,
matrix: None,
backdrop_blur: 0.0,
backdrop_id: None,
morphology: None,
color_filter: ColorFilter::None,
}
}
}
impl Layer {
pub fn with_matrix(mut self, matrix: impl Into<Transform2D>) -> Self {
self.matrix = Some(matrix.into());
self
}
pub fn opacity(alpha: f32) -> Self {
Self {
alpha,
..Self::default()
}
}
pub fn with_blend(mut self, blend: BlendMode) -> Self {
self.blend = blend;
self
}
pub fn with_color_filter(mut self, filter: ColorFilter) -> Self {
self.color_filter = filter;
self
}
pub fn with_morphology(mut self, morphology: Morphology) -> Self {
self.morphology = if morphology.is_identity() {
None
} else {
Some(morphology)
};
self
}
fn scaled_by(self, scale: f32) -> Self {
if !scale.is_finite() || scale <= 0.0 {
return self;
}
Self {
blur: self.blur * scale,
backdrop_blur: self.backdrop_blur * scale,
..self
}
}
fn reach(&self, basis: BlurBasis) -> Vec2 {
let blur = basis.reach(self.blur);
match self.morphology {
Some(m) if m.dilate => blur + Vec2::new(m.radius[0], m.radius[1]),
_ => blur,
}
}
pub fn with_backdrop_blur(mut self, sigma: f32) -> Self {
self.backdrop_blur = if sigma.is_finite() && sigma > 0.0 {
sigma
} else {
0.0
};
self
}
pub fn with_backdrop_id(mut self, id: i64) -> Self {
self.backdrop_id = Some(id);
self
}
pub fn with_blur(self, sigma: f32) -> Self {
self.with_blur_xy(sigma, sigma)
}
pub fn with_blur_xy(mut self, sigma_x: f32, sigma_y: f32) -> Self {
let one = |sigma: f32| {
if sigma.is_finite() && sigma > 0.0 {
sigma.min(MAX_SIGMA)
} else {
0.0
}
};
self.blur = Vec2::new(one(sigma_x), one(sigma_y));
self
}
}
pub struct Canvas {
renderer: Renderer,
batch: Batch,
transform: Transform2D,
clip: Option<Scissor>,
clip_bounds: Rect,
depth: u32,
clips: Vec<StencilClip>,
stack: Vec<SavedState>,
sources: Vec<TextureSource>,
finished: Vec<Pass>,
extent: Extent2D,
target: Target,
background: Option<Color>,
anti_alias: bool,
samples: u32,
ramps: Vec<Ramp>,
backdrops: std::collections::HashMap<i64, BackdropShare>,
}
#[derive(Clone, Copy)]
struct Backdrop {
pass: usize,
of: Target,
moved_by: Option<Affine2>,
}
struct BackdropShare {
captured_from: Target,
captured: usize,
filtered: Vec<(ImageFilter, usize)>,
}
impl Canvas {
pub fn new(extent: Extent2D) -> Self {
let mut renderer = Renderer::new();
renderer.begin_frame(extent, TOLERANCE);
Self {
renderer,
batch: Batch::new(),
transform: Transform2D::IDENTITY,
clip: None,
clip_bounds: Rect::new(0.0, 0.0, extent.width as f32, extent.height as f32),
depth: 0,
clips: Vec::new(),
stack: Vec::new(),
sources: Vec::new(),
finished: Vec::new(),
extent,
target: Target::frame(extent),
background: None,
anti_alias: false,
samples: 4,
ramps: Vec::new(),
backdrops: std::collections::HashMap::new(),
}
}
pub fn with_samples(mut self, samples: u32) -> Self {
self.samples = samples.max(1);
self
}
pub fn extent(&self) -> Extent2D {
self.extent
}
pub fn clear(&mut self, color: Color) -> &mut Self {
self.background = Some(color);
self
}
pub fn transform(&self) -> Transform2D {
self.transform
}
pub fn clip(&self) -> Option<Scissor> {
self.clip
}
pub fn destination_clip_bounds(&self) -> Rect {
self.clip_bounds
}
pub fn local_clip_bounds(&self) -> Rect {
let Some(inverse) = self.transform.inverse() else {
return Rect::new(0.0, 0.0, 0.0, 0.0);
};
let (min, max) = transformed_bounds(
inverse,
Vec2::new(self.clip_bounds.left, self.clip_bounds.top),
Vec2::new(self.clip_bounds.right, self.clip_bounds.bottom),
)
.unwrap_or_else(unbounded);
Rect::new(min.x, min.y, max.x, max.y)
}
fn narrow_bounds(&mut self, min: Vec2, max: Vec2) {
let current = self.clip_bounds;
self.clip_bounds = Rect::new(
current.left.max(min.x),
current.top.max(min.y),
current.right.min(max.x),
current.bottom.min(max.y),
);
if self.clip_bounds.right < self.clip_bounds.left
|| self.clip_bounds.bottom < self.clip_bounds.top
{
self.clip_bounds = Rect::new(
self.clip_bounds.left,
self.clip_bounds.top,
self.clip_bounds.left,
self.clip_bounds.top,
);
}
}
pub fn clip_rect(&mut self, rect: Rect) -> Result<&mut Self> {
if !preserves_axis_alignment(self.transform) {
return self.clip_path(&rect.to_path());
}
let Some((min, max)) = transformed_bounds(
self.transform,
Vec2::new(rect.left, rect.top),
Vec2::new(rect.right, rect.bottom),
) else {
return self.clip_path(&rect.to_path());
};
let narrowed = Scissor::from_device_bounds(
(min - self.target.origin).into(),
(max - self.target.origin).into(),
self.target.extent,
);
self.clip = Some(match self.clip {
Some(existing) => existing.intersect(narrowed),
None => narrowed,
});
self.narrow_bounds(min, max);
Ok(self)
}
pub fn clip_out_rect(&mut self, rect: Rect) -> Result<&mut Self> {
if self.clip.is_some_and(Scissor::is_empty) {
return Ok(self);
}
let mut builder = PathBuilder::new().with_fill_rule(FillRule::EvenOdd);
let target = Rect::new(
self.target.origin.x,
self.target.origin.y,
self.target.origin.x + self.target.extent.width as f32,
self.target.origin.y + self.target.extent.height as f32,
);
builder
.move_to(Vec2::new(target.left, target.top))
.line_to(Vec2::new(target.right, target.top))
.line_to(Vec2::new(target.right, target.bottom))
.line_to(Vec2::new(target.left, target.bottom))
.close();
let corners = [
Vec2::new(rect.left, rect.top),
Vec2::new(rect.right, rect.top),
Vec2::new(rect.right, rect.bottom),
Vec2::new(rect.left, rect.bottom),
]
.map(|corner| self.transform.project_point2(corner));
if !corners.iter().all(|corner| corner.is_finite()) {
return Ok(self);
}
builder.move_to(corners[0]);
for corner in &corners[1..] {
builder.line_to(*corner);
}
builder.close();
let paint = RenderPaint {
material: Material::solid([1.0, 1.0, 1.0, 1.0]),
filter: ColorFilter::None,
blend: BlendMode::Src,
clip: self.clip,
stencil: ClipState::narrow(self.depth),
};
let path = builder.build();
self.renderer
.fill_into(&mut self.batch, &path, Affine2::IDENTITY, &paint)?;
self.depth += 1;
self.clips.push(StencilClip {
path,
transform: Transform2D::IDENTITY,
scissor: paint.clip,
});
Ok(self)
}
pub fn clip_path(&mut self, path: &Path) -> Result<&mut Self> {
if self.clip.is_some_and(Scissor::is_empty) {
return Ok(self);
}
let paint = RenderPaint {
material: Material::solid([1.0, 1.0, 1.0, 1.0]),
filter: ColorFilter::None,
blend: BlendMode::Src,
clip: self.clip,
stencil: ClipState::narrow(self.depth),
};
self.renderer
.fill_into(&mut self.batch, path, self.transform, &paint)?;
self.depth += 1;
self.clips.push(StencilClip {
path: path.clone(),
transform: self.transform,
scissor: paint.clip,
});
let bounds = path.bounds();
if let Some((min, max)) = transformed_bounds(self.transform, bounds.min, bounds.max) {
self.narrow_bounds(min, max);
}
Ok(self)
}
pub fn save(&mut self) -> &mut Self {
self.stack.push(SavedState {
transform: self.transform,
clip: self.clip,
clip_bounds: self.clip_bounds,
depth: self.depth,
clips: self.clips.len(),
layer: None,
});
self
}
pub fn save_layer(&mut self, layer: Layer) -> &mut Self {
if layer.backdrop_blur > 0.0 && layer.matrix.is_none() {
let (min, max) = unbounded();
let _ = self.save_layer_device_bounds_running(layer, min, max, None, None);
return self;
}
let layer = layer.scaled_by(max_scale_of(self.transform));
let widened = self.matrix_preimage(&layer);
let pending = self.open_layer(layer, None, None, None).unwrap_or(None);
if let Some(target) = widened {
self.aim_at(target);
}
self.seed_backdrop(pending);
self
}
fn matrix_preimage(&self, layer: &Layer) -> Option<Target> {
let matrix = layer.matrix?;
let parent = self.target;
let (min, max) = (
parent.origin,
parent.origin + Vec2::new(parent.extent.width as f32, parent.extent.height as f32),
);
let (back_min, back_max) = transformed_bounds(matrix.inverse()?, min, max)?;
let left = min.x.min(back_min.x).floor();
let top = min.y.min(back_min.y).floor();
let right = max.x.max(back_max.x).ceil();
let bottom = max.y.max(back_max.y).ceil();
let (width, height) = (right - left, bottom - top);
if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) {
return None;
}
Some(Target {
origin: Vec2::new(left, top),
extent: Extent2D::new(width as u32, height as u32),
})
}
fn open_layer(
&mut self,
layer: Layer,
filter: Option<ImageFilter>,
backdrop: Option<&ImageFilter>,
region: Option<Target>,
) -> Result<Option<Backdrop>> {
let parent = self.target;
let region = region.unwrap_or(parent);
let asked = match backdrop {
Some(filter) => filter.clone(),
None if layer.backdrop_blur > 0.0 => ImageFilter::Blur {
sigma_x: layer.backdrop_blur,
sigma_y: layer.backdrop_blur,
},
None => ImageFilter::None,
};
let filtered = if asked.is_identity() {
None
} else {
match self.backdrop_pass(layer.backdrop_id, parent, region, &asked) {
Ok(pending) => Some(pending),
Err(e) => {
self.push_layer_frame(layer, filter);
return Err(e);
}
}
};
self.push_layer_frame(layer, filter);
Ok(filtered)
}
fn backdrop_pass(
&mut self,
id: Option<i64>,
parent: Target,
region: Target,
filter: &ImageFilter,
) -> Result<Backdrop> {
let (filter, moved_by) = match filter {
ImageFilter::Matrix { transform } => {
let Some(affine) = transform.to_affine() else {
return Err(Error::Unsupported(
"a projective matrix is not available as a backdrop filter; \
it maps the image through a division this seed does not do",
));
};
if affine.matrix2.determinant() == 0.0 {
return Err(Error::Unsupported(
"a singular matrix is not available as a backdrop filter; \
it collapses the image and nothing says what it read",
));
}
(&ImageFilter::None, Some(affine))
}
other => (other, None),
};
let Some(id) = id else {
let cut = self.cut_pass();
let pass = self.filter_passes(cut, region, filter, BlurBasis::of(self.transform))?;
return Ok(Backdrop {
pass,
of: if pass == cut { parent } else { region },
moved_by,
});
};
let existing = self.backdrops.get(&id).map(|share| {
let already = share
.filtered
.iter()
.find(|(known, _)| known == filter)
.map(|(_, pass)| *pass);
(share.captured_from, share.captured, already)
});
let (over, captured) = match existing {
Some((from, _, Some(pass))) => {
return Ok(Backdrop {
pass,
of: from,
moved_by,
})
}
Some((from, captured, None)) => (from, captured),
None => {
let cut = self.cut_pass();
self.backdrops.insert(
id,
BackdropShare {
captured_from: parent,
captured: cut,
filtered: Vec::new(),
},
);
(parent, cut)
}
};
let pass = self.filter_passes(captured, over, filter, BlurBasis::of(self.transform))?;
if let Some(share) = self.backdrops.get_mut(&id) {
share.filtered.push((filter.clone(), pass));
}
Ok(Backdrop {
pass,
of: over,
moved_by,
})
}
fn cut_pass(&mut self) -> usize {
let target = self.target;
let batch = std::mem::take(&mut self.batch);
let sources = std::mem::take(&mut self.sources);
let clear = if self.in_layer() {
Some([0.0; 4])
} else {
self.background.take().map(|c| c.to_array())
};
self.finished.push(Pass {
batch,
descriptor: PassDescriptor {
clear,
samples: self.pass_samples(),
viewport: None,
},
sources,
extent: target.extent,
});
self.anti_alias = false;
let index = self.finished.len() - 1;
self.draw_whole_pass(index, target, target, BlendMode::Src, None);
self.rebuild_stencil_clips();
index
}
fn rebuild_stencil_clips(&mut self) {
for depth in 0..self.clips.len() {
let clip = self.clips[depth].clone();
let paint = RenderPaint {
material: Material::solid([1.0, 1.0, 1.0, 1.0]),
filter: ColorFilter::None,
blend: BlendMode::Src,
clip: clip.scissor,
stencil: ClipState::narrow(depth as u32),
};
let _ = self
.renderer
.fill_into(&mut self.batch, &clip.path, clip.transform, &paint);
}
}
fn in_layer(&self) -> bool {
self.stack.iter().any(|state| state.layer.is_some())
}
fn draw_whole_pass(
&mut self,
pass: usize,
source: Target,
into: Target,
blend: BlendMode,
moved_by: Option<Affine2>,
) {
let slot = self.slot_for(TextureSource::Layer(pass));
let offset = into.origin - source.origin;
let (iw, ih) = (into.extent.width as f32, into.extent.height as f32);
let (sw, sh) = (source.extent.width as f32, source.extent.height as f32);
let anchor = Vec2::new(-1.0 - 2.0 * offset.x / iw, 1.0 + 2.0 * offset.y / ih);
let scale = Mat2::from_diagonal(Vec2::new(0.5 * iw / sw, -0.5 * ih / sh));
let clip_to_texture = Affine2::from_mat2(scale) * Affine2::from_translation(-anchor);
let clip_to_texture = match moved_by {
None => clip_to_texture,
Some(matrix) => {
let texels = Affine2::from_translation(source.origin)
* Affine2::from_scale(Vec2::new(sw, sh));
let normalized = texels.inverse();
normalized * matrix.inverse() * texels * clip_to_texture
}
};
let material = Material::Image {
to_local: to_local_columns(Transform2D::from(clip_to_texture)),
slot,
alpha: 1.0,
tile: if moved_by.is_some() {
TileMode::Decal
} else {
TileMode::Clamp
},
sampling: Sampling::Linear,
source: [0.0, 0.0, 1.0, 1.0],
tint: [1.0, 1.0, 1.0, 1.0],
};
let paint = RenderPaint {
material,
filter: ColorFilter::None,
blend,
clip: None,
stencil: ClipState::UNCLIPPED,
};
let whole = into.path();
let _ = self
.renderer
.fill_into(&mut self.batch, &whole, Affine2::IDENTITY, &paint);
}
fn seed_backdrop(&mut self, pending: Option<Backdrop>) {
let Some(Backdrop {
pass,
of: parent,
moved_by,
}) = pending
else {
return;
};
let into = self.target;
self.draw_whole_pass(pass, parent, into, BlendMode::Src, moved_by);
}
fn push_layer_frame(&mut self, layer: Layer, filter: Option<ImageFilter>) {
self.stack.push(SavedState {
transform: self.transform,
clip: self.clip,
clip_bounds: self.clip_bounds,
depth: self.depth,
clips: self.clips.len(),
layer: Some(LayerFrame {
batch: std::mem::take(&mut self.batch),
sources: std::mem::take(&mut self.sources),
clips: std::mem::take(&mut self.clips),
paint: layer,
filter,
parent: self.target,
anti_alias: std::mem::take(&mut self.anti_alias),
blur_basis: BlurBasis::of(self.transform),
}),
});
self.clip = None;
self.depth = 0;
}
pub fn save_layer_bounds(&mut self, layer: Layer, bounds: Rect) -> &mut Self {
let (min, max) = transformed_bounds(
self.transform,
Vec2::new(bounds.left, bounds.top),
Vec2::new(bounds.right, bounds.bottom),
)
.unwrap_or_else(unbounded);
self.save_layer_device_bounds(layer, min, max)
}
pub fn save_layer_filtered(
&mut self,
layer: Layer,
bounds: Option<Rect>,
filter: &ImageFilter,
) -> Result<&mut Self> {
if matches!(filter, ImageFilter::Matrix { .. }) {
return Err(Error::Unsupported(
"a matrix filters a group through `Layer::with_matrix`, which \
states where the finished image goes",
));
}
let (min, max) = match bounds {
Some(bounds) => transformed_bounds(
self.transform,
Vec2::new(bounds.left, bounds.top),
Vec2::new(bounds.right, bounds.bottom),
)
.unwrap_or_else(unbounded),
None => unbounded(),
};
self.save_layer_device_bounds_running(layer, min, max, Some(filter.clone()), None)
}
pub fn save_layer_backdrop(
&mut self,
layer: Layer,
bounds: Option<Rect>,
backdrop: &ImageFilter,
) -> Result<&mut Self> {
let (min, max) = match bounds {
Some(bounds) => transformed_bounds(
self.transform,
Vec2::new(bounds.left, bounds.top),
Vec2::new(bounds.right, bounds.bottom),
)
.unwrap_or_else(unbounded),
None => unbounded(),
};
self.save_layer_device_bounds_running(layer, min, max, None, Some(backdrop))
}
fn save_layer_device_bounds(&mut self, layer: Layer, min: Vec2, max: Vec2) -> &mut Self {
let _ = self.save_layer_device_bounds_running(layer, min, max, None, None);
self
}
fn save_layer_device_bounds_running(
&mut self,
layer: Layer,
min: Vec2,
max: Vec2,
filter: Option<ImageFilter>,
backdrop: Option<&ImageFilter>,
) -> Result<&mut Self> {
let basis = BlurBasis::of(self.transform);
let layer = layer.scaled_by(max_scale_of(self.transform));
let reach = layer.reach(basis);
let captures_backdrop = backdrop.is_some() || layer.backdrop_blur > 0.0;
let (min, max) = if captures_backdrop && layer.matrix.is_none() {
let clip = self.clip_bounds;
(
Vec2::new(min.x.max(clip.left), min.y.max(clip.top)),
Vec2::new(max.x.min(clip.right), max.y.min(clip.bottom)),
)
} else {
(min, max)
};
let filter_reach = filter.as_ref().map(|filter| {
let unit = Vec2::ZERO;
let (min, max) = filter.covering(unit, unit, &|sigma| basis.reach(sigma));
(-min).max(max)
});
let (min, max) = (min - reach, max + reach);
let (min, max) = match filter_reach {
Some(spread) => (min - spread, max + spread),
None => (min, max),
};
let parent = self.target;
let left = min.x.floor().max(parent.origin.x);
let top = min.y.floor().max(parent.origin.y);
let right = max
.x
.ceil()
.min(parent.origin.x + parent.extent.width as f32);
let bottom = max
.y
.ceil()
.min(parent.origin.y + parent.extent.height as f32);
let narrowed = (right > left && bottom > top).then(|| Target {
origin: Vec2::new(left, top),
extent: Extent2D::new((right - left) as u32, (bottom - top) as u32),
});
let pending = self.open_layer(layer, filter, backdrop, narrowed)?;
let Some(target) = narrowed else {
self.seed_backdrop(pending);
return Ok(self);
};
self.aim_at(target);
self.seed_backdrop(pending);
Ok(self)
}
pub fn layer_depth(&self) -> usize {
self.stack.iter().filter(|s| s.layer.is_some()).count()
}
fn ramp_for(&mut self, stops: &[GradientStop]) -> Option<u32> {
(stops.len() > MAX_STOPS).then(|| self.ramp_slot(stops))
}
fn ramp_slot(&mut self, stops: &[GradientStop]) -> u32 {
let ramp = Ramp::bake(stops);
let index = match self.ramps.iter().position(|r| *r == ramp) {
Some(index) => index,
None => {
self.ramps.push(ramp);
self.ramps.len() - 1
}
};
self.slot_for(TextureSource::Ramp(index))
}
fn slot_for(&mut self, source: TextureSource) -> u32 {
if let Some(index) = self.sources.iter().position(|s| *s == source) {
return index as u32;
}
self.sources.push(source);
(self.sources.len() - 1) as u32
}
pub fn restore(&mut self) -> &mut Self {
let Some(previous) = self.stack.pop() else {
return self;
};
self.transform = previous.transform;
self.clip = previous.clip;
self.clip_bounds = previous.clip_bounds;
if let Some(frame) = previous.layer {
self.depth = previous.depth;
self.clips = frame.clips.clone();
self.finish_layer(frame);
return self;
}
self.clips.truncate(previous.clips);
if self.depth > previous.depth {
let whole = self.target.path();
while self.depth > previous.depth {
let paint = RenderPaint {
material: Material::solid([1.0, 1.0, 1.0, 1.0]),
filter: ColorFilter::None,
blend: BlendMode::Src,
clip: None,
stencil: ClipState::widen(self.depth),
};
let _ = self
.renderer
.fill_into(&mut self.batch, &whole, Affine2::IDENTITY, &paint);
self.depth -= 1;
}
}
self
}
pub fn save_depth(&self) -> usize {
self.stack.len()
}
pub fn translate(&mut self, x: f32, y: f32) -> &mut Self {
self.concat(Affine2::from_translation(Vec2::new(x, y)))
}
pub fn scale(&mut self, x: f32, y: f32) -> &mut Self {
self.concat(Affine2::from_scale(Vec2::new(x, y)))
}
pub fn rotate(&mut self, radians: f32) -> &mut Self {
self.concat(Affine2::from_angle(radians))
}
pub fn concat(&mut self, transform: impl Into<Transform2D>) -> &mut Self {
self.transform = self.transform * transform.into();
self
}
pub fn concat_4x4(&mut self, matrix: &[f32; 16]) -> &mut Self {
self.concat(Transform2D::from_column_major_4x4(matrix))
}
pub fn draw_path(&mut self, path: &Path, paint: &Paint) -> Result<&mut Self> {
if !paint.is_visible() || path.is_empty() {
return Ok(self);
}
if !paint.image_filter.is_identity() {
return self.draw_filtered(path, paint);
}
if let Some((bounds, radius)) = path.as_rounded_rect() {
let rect = Rect::new(bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y);
if let Some((material, pad, drawn)) = self.analytic_rrect_blur_draw(rect, radius, paint)
{
return self.draw_analytic(rect.outset(pad), material, &drawn);
}
}
if matches!(paint.shader, Shader::RuntimeEffect { .. }) && !paint.color_filter.is_identity()
{
return self.draw_effect_filtered(path, paint);
}
if paint.mask_blur > 0.0 {
return self.draw_masked(Masked::Path(path), paint);
}
if paint.anti_alias {
self.anti_alias = true;
}
if self.clip.is_some_and(Scissor::is_empty) {
return Ok(self);
}
let material = self.material_for(&paint.shader);
let (style, coverage) = match &paint.style {
Style::Stroke(stroke) => {
let (width, coverage) =
thin_stroke(self.transform, stroke.width, Zero::IsAHairline);
(Style::Stroke(stroke.with_width(width)), coverage)
}
other => (*other, 1.0),
};
let render_paint = RenderPaint {
material: material.with_opacity(coverage),
filter: paint.color_filter,
blend: paint.blend,
clip: self.clip,
stencil: ClipState::content(self.depth),
};
match &style {
Style::Fill => {
self.renderer
.fill_into(&mut self.batch, path, self.transform, &render_paint)?
}
Style::Stroke(stroke) => self.renderer.stroke_into(
&mut self.batch,
path,
stroke,
paint.dash.as_ref(),
self.transform,
&render_paint,
)?,
}
Ok(self)
}
fn material_for(&mut self, shader: &Shader) -> Material {
let to_clip = self.target.projection() * self.transform;
self.material_from(shader, to_clip)
}
fn material_at_texture_coords(&mut self, shader: &Shader) -> Material {
self.material_from(shader, Transform2D::IDENTITY)
}
fn material_from(&mut self, shader: &Shader, to_clip: Transform2D) -> Material {
let stops_of = |stops: &[crate::paint::GradientStop]| -> Vec<Stop> {
stops
.iter()
.map(|s| Stop::new(s.color.to_array(), s.offset))
.collect()
};
match shader {
Shader::Solid(color) => Material::solid(color.to_array()),
Shader::RuntimeEffect {
program,
uniforms,
images,
} => Material::Runtime {
program: *program,
uniforms: uniforms.clone(),
textures: {
let mut bound = [None; impeller_hal::MAX_EFFECT_TEXTURES];
for (slot, into) in images.iter().zip(bound.iter_mut()) {
*into = Some(self.slot_for(TextureSource::Image(*slot)));
}
bound
},
},
Shader::Image {
slot,
rect,
alpha,
tile,
source,
tint,
sampling,
} => {
let placement = to_clip
* Affine2::from_translation(Vec2::new(rect.left, rect.top))
* Affine2::from_scale(Vec2::new(
rect.right - rect.left,
rect.bottom - rect.top,
));
Material::Image {
to_local: invert_to_local(placement),
sampling: *sampling,
slot: self.slot_for(TextureSource::Image(*slot)),
alpha: *alpha,
source: [source.left, source.top, source.right, source.bottom],
tint: tint.to_array(),
tile: *tile,
}
}
Shader::LinearGradient {
start,
end,
stops,
tile,
} => {
let axis = *end - *start;
if !axis.is_finite() || !start.is_finite() {
return Material::Solid([0.0; 4]);
}
let ramp_slot = self.ramp_for(stops);
Material::LinearGradient {
axis: [axis.x, axis.y],
to_local: invert_to_local(to_clip * Affine2::from_translation(*start)),
stops: stops_of(stops),
tile: *tile,
ramp: ramp_slot,
}
}
Shader::RadialGradient {
center,
radius,
stops,
tile,
} => {
let placement = to_clip
* Affine2::from_translation(*center)
* Affine2::from_scale(Vec2::splat(*radius));
if !placement.is_finite() {
return Material::Solid([0.0; 4]);
}
if !radius.is_finite() || *radius <= 0.0 {
if matches!(tile, TileMode::Decal) {
return Material::Solid([0.0; 4]);
}
let last = stops
.iter()
.max_by(|a, b| a.offset.total_cmp(&b.offset))
.map(|s| s.color.to_array())
.unwrap_or([0.0; 4]);
return Material::solid(last);
}
let ramp_slot = self.ramp_for(stops);
Material::RadialGradient {
to_local: invert_to_local(placement),
stops: stops_of(stops),
tile: *tile,
ramp: ramp_slot,
}
}
Shader::ConicalGradient {
start_center,
start_radius,
end_center,
end_radius,
stops,
tile,
} => {
let axis = *end_center - *start_center;
let angle = if axis == Vec2::ZERO {
0.0
} else {
axis.y.atan2(axis.x)
};
let oriented =
to_clip * Affine2::from_translation(*start_center) * Affine2::from_angle(angle);
if !oriented.is_finite() || !start_radius.is_finite() || !end_radius.is_finite() {
return Material::Solid([0.0; 4]);
}
let ramp_slot = self.ramp_for(stops);
Material::ConicalGradient {
to_local: invert_to_local(oriented),
start_radius: *start_radius,
radius_delta: *end_radius - *start_radius,
separation: axis.length(),
stops: stops_of(stops),
tile: *tile,
ramp: ramp_slot,
}
}
Shader::SweepGradient {
center,
start_angle,
end_angle,
stops,
tile,
} => {
let placement = to_clip * Affine2::from_translation(*center);
if !placement.is_finite() || !start_angle.is_finite() || !end_angle.is_finite() {
return Material::Solid([0.0; 4]);
}
let ramp_slot = self.ramp_for(stops);
Material::SweepGradient {
to_local: invert_to_local(placement),
start_angle: *start_angle,
end_angle: *end_angle,
stops: stops_of(stops),
tile: *tile,
ramp: ramp_slot,
}
}
}
}
fn filter_layer(&self, filter: &ImageFilter) -> Option<(Layer, Option<ImageFilter>)> {
if let ImageFilter::Runtime { program, uniforms } = filter {
return Some((
Layer::opacity(1.0),
Some(ImageFilter::Runtime {
program: *program,
uniforms: uniforms.clone(),
}),
));
}
Some((
match *filter {
ImageFilter::Blur { sigma_x, sigma_y } => {
Layer::opacity(1.0).with_blur_xy(sigma_x, sigma_y)
}
ImageFilter::Matrix { transform } => Layer::opacity(1.0).with_matrix(transform),
ImageFilter::Dilate { radius_x, radius_y } => {
Layer::opacity(1.0).with_morphology(Morphology::dilate(radius_x, radius_y))
}
ImageFilter::Erode { radius_x, radius_y } => {
Layer::opacity(1.0).with_morphology(Morphology::erode(radius_x, radius_y))
}
ImageFilter::Color(filter) => Layer::opacity(1.0).with_color_filter(filter),
ImageFilter::Runtime { .. } => unreachable!("handled above"),
ImageFilter::None | ImageFilter::Compose { .. } => return None,
},
None,
))
}
fn draw_glyphs_filtered(
&mut self,
glyphs: &[PositionedGlyph],
atlas: &Atlas,
atlas_slot: u32,
paint: &Paint,
) -> Result<&mut Self> {
let (outermost, rest) = paint.image_filter.peel();
let Some((layer, runtime)) = self.filter_layer(&outermost) else {
return Err(Error::Unsupported("this image filter is not implemented"));
};
let (mut min, mut max) = (Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
for glyph in glyphs {
let corner = Vec2::from(glyph.position);
min = min.min(corner);
max = max.max(corner + Vec2::from(glyph.size));
}
if !min.is_finite() || !max.is_finite() {
return Err(Error::Unsupported(
"a glyph in this run is not at a finite position",
));
}
let (min, max) = transformed_bounds(self.transform, min, max).unwrap_or_else(unbounded);
let basis = BlurBasis::of(self.transform);
let (min, max) = rest.covering(min, max, &|sigma| basis.reach(sigma));
let _ = self.save_layer_device_bounds_running(
layer.with_blend(paint.blend),
min,
max,
runtime,
None,
);
let inner = paint
.clone()
.with_image_filter(rest)
.with_blend(BlendMode::SrcOver);
let failure = self.draw_glyphs(glyphs, atlas, atlas_slot, &inner).err();
self.restore();
match failure {
Some(e) => Err(e),
None => Ok(self),
}
}
fn draw_vertices_filtered(&mut self, mesh: &Vertices, paint: &Paint) -> Result<&mut Self> {
let (outermost, rest) = paint.image_filter.peel();
let Some((layer, runtime)) = self.filter_layer(&outermost) else {
return Err(Error::Unsupported("this image filter is not implemented"));
};
let positions = mesh.positions();
let (mut min, mut max) = (Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
for point in positions {
min = min.min(*point);
max = max.max(*point);
}
if !min.is_finite() || !max.is_finite() {
return Err(Error::Unsupported("a mesh position is not a finite number"));
}
let (min, max) = transformed_bounds(self.transform, min, max).unwrap_or_else(unbounded);
let basis = BlurBasis::of(self.transform);
let (min, max) = rest.covering(min, max, &|sigma| basis.reach(sigma));
let _ = self.save_layer_device_bounds_running(
layer.with_blend(paint.blend),
min,
max,
runtime,
None,
);
let inner = paint
.clone()
.with_image_filter(rest)
.with_blend(BlendMode::SrcOver);
let failure = self.draw_vertices(mesh, &inner).err();
self.restore();
match failure {
Some(e) => Err(e),
None => Ok(self),
}
}
fn draw_effect_filtered(&mut self, path: &Path, paint: &Paint) -> Result<&mut Self> {
let bounds = self.filter_bounds(path, paint);
let (min, max) = transformed_bounds(
self.transform,
Vec2::new(bounds.left, bounds.top),
Vec2::new(bounds.right, bounds.bottom),
)
.unwrap_or_else(unbounded);
self.save_layer_device_bounds(
Layer::opacity(1.0)
.with_color_filter(paint.color_filter)
.with_blend(paint.blend),
min,
max,
);
let inner = paint
.clone()
.with_color_filter(ColorFilter::None)
.with_blend(BlendMode::SrcOver);
let failure = self.draw_path(path, &inner).err();
self.restore();
match failure {
Some(e) => Err(e),
None => Ok(self),
}
}
fn draw_filtered(&mut self, path: &Path, paint: &Paint) -> Result<&mut Self> {
let (outermost, rest) = paint.image_filter.peel();
let Some((layer, runtime)) = self.filter_layer(&outermost) else {
return Err(Error::Unsupported("this image filter is not implemented"));
};
let bounds = self.filter_bounds(path, paint);
let (min, max) = transformed_bounds(
self.transform,
Vec2::new(bounds.left, bounds.top),
Vec2::new(bounds.right, bounds.bottom),
)
.unwrap_or_else(unbounded);
let basis = BlurBasis::of(self.transform);
let (min, max) = rest.covering(min, max, &|sigma| basis.reach(sigma));
let _ = self.save_layer_device_bounds_running(
layer.with_blend(paint.blend),
min,
max,
runtime,
None,
);
let inner = paint
.clone()
.with_image_filter(rest)
.with_blend(BlendMode::SrcOver);
let failure = self.draw_path(path, &inner).err();
self.restore();
match failure {
Some(e) => Err(e),
None => Ok(self),
}
}
fn filter_bounds(&self, path: &Path, paint: &Paint) -> Rect {
let bounds = path.bounds();
let reach = match &paint.style {
Style::Stroke(stroke) if stroke.width.is_finite() => {
thin_stroke(self.transform, stroke.width, Zero::IsAHairline).0 / 2.0
}
_ => 0.0,
};
Rect::new(
bounds.min.x - reach,
bounds.min.y - reach,
bounds.max.x + reach,
bounds.max.y + reach,
)
}
fn draw_mask_content(&mut self, content: Masked<'_>, paint: &Paint) -> Result<&mut Self> {
match content {
Masked::Path(path) => self.draw_path(path, paint),
Masked::Mesh(mesh) => self.draw_vertices(mesh, paint),
Masked::Glyphs {
glyphs,
atlas,
slot,
} => self.draw_glyphs(glyphs, atlas, slot, paint),
}
}
fn mask_bounds(&self, content: Masked<'_>, paint: &Paint) -> Rect {
match content {
Masked::Path(path) => self.filter_bounds(path, paint),
Masked::Mesh(mesh) => {
let (mut min, mut max) =
(Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
for position in mesh.positions() {
min = min.min(*position);
max = max.max(*position);
}
Rect::new(min.x, min.y, max.x, max.y)
}
Masked::Glyphs { glyphs, .. } => {
let (mut min, mut max) =
(Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
for glyph in glyphs {
let corner = Vec2::from(glyph.position);
min = min.min(corner);
max = max.max(corner + Vec2::from(glyph.size));
}
Rect::new(min.x, min.y, max.x, max.y)
}
}
}
fn draw_masked_through_coverage(
&mut self,
content: Masked<'_>,
paint: &Paint,
) -> Result<&mut Self> {
let bounds = self.mask_bounds(content, paint);
let held = bounds.outset(self.mask_blur_reach(content, paint));
let coverage = Paint::fill(Color::WHITE).with_anti_alias(paint.anti_alias);
let fill = paint
.clone()
.with_mask_blur(0.0)
.with_blend(BlendMode::SrcOver)
.with_style(Style::Fill);
self.save_layer_bounds(Layer::opacity(1.0).with_blend(paint.blend), held);
let failure = self.draw_rect(held, &fill).err().or_else(|| {
if paint.mask_blur_style == MaskBlurStyle::Normal {
let mask = Layer::opacity(1.0)
.with_blur(paint.mask_blur)
.with_blend(BlendMode::DstIn);
self.save_layer_bounds(mask, held);
let inner = self.draw_mask_content(content, &coverage).err();
self.restore();
return inner;
}
self.save_layer_bounds(Layer::opacity(1.0).with_blend(BlendMode::DstIn), held);
let inner = self.draw_mask_styles(
content,
&coverage,
paint.mask_blur,
paint.mask_blur_style,
bounds,
);
self.restore();
inner
});
self.restore();
match failure {
Some(e) => Err(e),
None => Ok(self),
}
}
fn draw_masked(&mut self, content: Masked<'_>, paint: &Paint) -> Result<&mut Self> {
if !matches!(paint.shader, Shader::Solid(_)) {
if !matches!(content, Masked::Glyphs { .. }) {
return self.draw_masked_through_coverage(content, paint);
}
return Err(Error::Unsupported(
"a mask blur over a glyph run takes a solid color; \
draw into a blurred layer for anything else",
));
}
let bounds = self.mask_bounds(content, paint);
let inner = paint
.clone()
.with_mask_blur(0.0)
.with_blend(BlendMode::SrcOver);
if paint.mask_blur_style == MaskBlurStyle::Normal {
self.save_layer_bounds(
Layer::opacity(1.0)
.with_blur(paint.mask_blur)
.with_blend(paint.blend),
bounds,
);
let failure = self.draw_mask_content(content, &inner).err();
self.restore();
return match failure {
Some(e) => Err(e),
None => Ok(self),
};
}
let held = bounds.outset(self.mask_blur_reach(content, paint));
self.save_layer_bounds(Layer::opacity(1.0).with_blend(paint.blend), held);
let failure = self.draw_mask_styles(
content,
&inner,
paint.mask_blur,
paint.mask_blur_style,
bounds,
);
self.restore();
match failure {
Some(e) => Err(e),
None => Ok(self),
}
}
fn mask_blur_reach(&mut self, content: Masked<'_>, paint: &Paint) -> f32 {
let sampled = blur_reach(paint.mask_blur);
let probe = paint.clone().with_mask_blur_style(MaskBlurStyle::Normal);
match self.analytic_style_blur(content, &probe, paint.mask_blur) {
Some((_, _, pad)) => sampled.max(pad),
None => sampled,
}
}
fn analytic_style_blur(
&mut self,
content: Masked<'_>,
inner: &Paint,
sigma: f32,
) -> Option<(Rect, Material, f32)> {
let Masked::Path(path) = content else {
return None;
};
let (bounds, radius) = path.as_rounded_rect()?;
let rect = Rect::new(bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y);
let asked = inner
.clone()
.with_mask_blur(sigma)
.with_mask_blur_style(MaskBlurStyle::Normal);
let (material, pad) = self.analytic_rrect_blur(rect, radius, &asked)?;
Some((rect, material, pad))
}
fn draw_mask_styles(
&mut self,
content: Masked<'_>,
inner: &Paint,
sigma: f32,
style: MaskBlurStyle,
bounds: Rect,
) -> Option<Error> {
let blurred = Layer::opacity(1.0).with_blur(sigma);
match style {
MaskBlurStyle::Solid | MaskBlurStyle::Outer => {
let blend = match style {
MaskBlurStyle::Solid => BlendMode::SrcOver,
_ => BlendMode::DstOut,
};
let first = match self.analytic_style_blur(content, inner, sigma) {
Some((rect, material, pad)) => {
self.draw_analytic(rect.outset(pad), material, inner).err()
}
None => {
self.save_layer_bounds(blurred, bounds);
let failed = self.draw_mask_content(content, inner).err();
self.restore();
failed
}
};
first.or_else(|| {
self.draw_mask_content(content, &inner.clone().with_blend(blend))
.err()
})
}
MaskBlurStyle::Inner => {
let first = self.draw_mask_content(content, inner).err();
if first.is_some() {
return first;
}
match self.analytic_style_blur(content, inner, sigma) {
Some((rect, material, pad)) => {
let composite = inner.clone().with_blend(BlendMode::DstIn);
self.draw_analytic(rect.outset(pad), material, &composite)
.err()
}
None => {
self.save_layer_bounds(blurred.with_blend(BlendMode::DstIn), bounds);
let second = self.draw_mask_content(content, inner).err();
self.restore();
second
}
}
}
MaskBlurStyle::Normal => unreachable!("handled by the caller"),
}
}
pub fn draw_rect(&mut self, rect: Rect, paint: &Paint) -> Result<&mut Self> {
if rect.is_empty() {
return Ok(self);
}
if let Some(material) = self.analytic_rrect(rect, 0.0, paint) {
return self.draw_analytic(rect, material, paint);
}
let path = rect.to_path();
self.draw_path(&path, paint)
}
pub fn draw_rrect(&mut self, rect: Rect, radius: f32, paint: &Paint) -> Result<&mut Self> {
if rect.is_empty() {
return Ok(self);
}
if let Some((material, pad, drawn)) = self.analytic_rrect_blur_draw(rect, radius, paint) {
return self.draw_analytic(rect.outset(pad), material, &drawn);
}
if let Some(material) = self.analytic_rrect(rect, radius, paint) {
return self.draw_analytic(rect, material, paint);
}
let path = rect.to_rounded_path(radius);
self.draw_path(&path, paint)
}
pub fn draw_rrect_with_radii(
&mut self,
rect: Rect,
radii: RoundingRadii,
paint: &Paint,
) -> Result<&mut Self> {
if rect.is_empty() {
return Ok(self);
}
if let Some(radius) = uniform_circular_radius(radii) {
return self.draw_rrect(rect, radius, paint);
}
let path = rect.to_rounded_path_with_radii(radii);
self.draw_path(&path, paint)
}
pub fn draw_rsuperellipse(
&mut self,
rect: Rect,
radii: RoundingRadii,
paint: &Paint,
) -> Result<&mut Self> {
if rect.is_empty() {
return Ok(self);
}
self.draw_path(&rsuperellipse_path(rect, radii), paint)
}
pub fn clip_rsuperellipse(&mut self, rect: Rect, radii: RoundingRadii) -> Result<&mut Self> {
self.clip_path(&rsuperellipse_path(rect, radii))
}
fn pad_for_sigma(sigma: f32) -> f32 {
sigma * (sigma / 47.6 + 2.5).min(3.5)
}
fn analytic_rrect_blur_draw(
&mut self,
rect: Rect,
radius: f32,
paint: &Paint,
) -> Option<(Material, f32, Paint)> {
let drawn = Self::erasing_blur_paint(paint);
let (material, pad) = self.analytic_rrect_blur(rect, radius, &drawn)?;
Some((material, pad, drawn))
}
fn erasing_blur_paint(paint: &Paint) -> Paint {
if paint.blend != BlendMode::Clear {
return paint.clone();
}
paint
.clone()
.with_shader(Shader::Solid(Color::WHITE))
.with_blend(BlendMode::DstOut)
.with_color_filter(ColorFilter::None)
}
fn analytic_rrect_blur(
&mut self,
rect: Rect,
radius: f32,
paint: &Paint,
) -> Option<(Material, f32)> {
if !paint.is_visible() || self.clip.is_some_and(Scissor::is_empty) {
return None;
}
if !paint.blend.respects_coverage() {
return None;
}
if !paint.image_filter.is_identity() {
return None;
}
if !matches!(paint.style, Style::Fill) {
return None;
}
if paint.mask_blur_style != MaskBlurStyle::Normal {
return None;
}
let Shader::Solid(color) = &paint.shader else {
return None;
};
if radius.is_nan() || radius < 0.0 {
return None;
}
if !paint.mask_blur.is_finite() || paint.mask_blur <= 0.0 {
return None;
}
let affine = self.transform.to_affine()?;
let sx = affine.matrix2.x_axis.length();
let sy = affine.matrix2.y_axis.length();
if !sx.is_finite() || !sy.is_finite() || sx <= 0.0 || sy <= 0.0 {
return None;
}
if (sx - sy).abs() > 1e-3 * sx.max(sy) {
return None;
}
let sigma = paint.mask_blur;
let radius = radius.min(rect.width() / 2.0).min(rect.height() / 2.0);
let material = self.rrect_blur_material(rect, radius, sigma, *color)?;
Some((material, Self::pad_for_sigma(sigma)))
}
fn rrect_blur_material(
&self,
rect: Rect,
radius: f32,
sigma: f32,
color: Color,
) -> Option<Material> {
let sigma = (sigma * std::f32::consts::SQRT_2).max(1.0);
let mut size = Vec2::new(rect.width(), rect.height());
if !size.x.is_finite() || !size.y.is_finite() || !sigma.is_finite() {
return None;
}
if size.x <= 0.0 || size.y <= 0.0 {
return None;
}
let min_edge = size.x.min(size.y);
let r_max = 0.5 * min_edge;
let r0 = radius.hypot(sigma * 1.15).min(r_max);
let r1 = radius.hypot(sigma * 2.0).min(r_max);
let exponent = 2.0 * r1 / r0;
let s_inv = 1.0 / sigma;
let falloff = |v: f32| (-(v * s_inv * 0.5).powi(2)).exp();
let delta = 1.25 * sigma * (falloff(size.x) - falloff(size.y));
size.x += delta.min(0.0);
size.y += (-delta).min(0.0);
let adjust = size * 0.5 - Vec2::splat(r1);
let scale = 0.5 * erf7(s_inv * 0.5 * (size.x.max(size.y) - 0.5 * radius));
let center = Vec2::new(
(rect.left + rect.right) / 2.0,
(rect.top + rect.bottom) / 2.0,
);
let to_clip = self.target.projection() * self.transform;
let material = Material::RoundedRectBlur {
color: color.to_array(),
to_local: invert_to_local(to_clip * Affine2::from_translation(center)),
adjust: [adjust.x, adjust.y],
r1,
exponent,
s_inv,
min_edge,
scale,
};
[adjust.x, adjust.y, r1, exponent, s_inv, min_edge, scale]
.iter()
.all(|v| v.is_finite())
.then_some(material)
}
fn analytic_rrect(&self, rect: Rect, radius: f32, paint: &Paint) -> Option<Material> {
if !paint.is_visible() || self.clip.is_some_and(Scissor::is_empty) {
return None;
}
let mitered = match &paint.style {
Style::Stroke(style) if style.width > 0.0 => {
if style.join == LineJoin::Bevel {
return None;
}
style.join == LineJoin::Miter
}
_ => false,
};
let (stroke, coverage) =
thin_stroke(self.transform, analytic_stroke(paint)?, Zero::IsNotAStroke);
let Shader::Solid(color) = &paint.shader else {
return None;
};
if radius.is_nan() || radius < 0.0 {
return None;
}
let to_clip = self.target.projection() * self.transform;
let center = Vec2::new(
(rect.left + rect.right) / 2.0,
(rect.top + rect.bottom) / 2.0,
);
Some(Material::RoundedRect {
color: dimmed(*color, coverage),
half_size: [rect.width() / 2.0, rect.height() / 2.0],
to_local: invert_to_local(to_clip * Affine2::from_translation(center)),
radius: radius.min(rect.width() / 2.0).min(rect.height() / 2.0),
outer_radius: if mitered && radius <= 0.0 {
0.0
} else {
radius.min(rect.width() / 2.0).min(rect.height() / 2.0) + stroke / 2.0
},
stroke,
})
}
fn draw_analytic(
&mut self,
rect: Rect,
material: Material,
paint: &Paint,
) -> Result<&mut Self> {
let reach = 1.0
+ thin_stroke(
self.transform,
analytic_stroke(paint).unwrap_or(0.0),
Zero::IsNotAStroke,
)
.0 / 2.0;
let outset = Rect::new(
rect.left - reach,
rect.top - reach,
rect.right + reach,
rect.bottom + reach,
);
let render_paint = RenderPaint {
material,
filter: paint.color_filter,
blend: paint.blend,
clip: self.clip,
stencil: ClipState::content(self.depth),
};
self.renderer.fill_into(
&mut self.batch,
&outset.to_path(),
self.transform,
&render_paint,
)?;
Ok(self)
}
pub fn draw_shadow(
&mut self,
path: &Path,
color: Color,
elevation: f32,
_transparent_occluder: bool,
) -> Result<&mut Self> {
if !elevation.is_finite() || elevation <= 0.0 || color.is_invisible() {
return Ok(self);
}
let sigma = sigma_for_radius(LIGHT_RADIUS * elevation);
let shade = tonal_shadow_color(color);
let scale = max_scale_of(self.transform);
let sigma = if scale.is_finite() && scale > 0.0 {
sigma / scale
} else {
sigma
};
let paint = Paint::fill(shade).with_mask_blur(sigma);
self.save();
self.translate(0.0, elevation);
let failure = self.draw_path(path, &paint).err();
self.restore();
match failure {
Some(e) => Err(e),
None => Ok(self),
}
}
pub fn draw_circle(&mut self, center: Vec2, radius: f32, paint: &Paint) -> Result<&mut Self> {
if radius.is_nan() || radius <= 0.0 {
return Ok(self);
}
let bounds = Rect::new(
center.x - radius,
center.y - radius,
center.x + radius,
center.y + radius,
);
if let Some((material, pad, drawn)) = self.analytic_rrect_blur_draw(bounds, radius, paint) {
return self.draw_analytic(bounds.outset(pad), material, &drawn);
}
if let Some(material) = self.analytic_rrect(bounds, radius, paint) {
return self.draw_analytic(bounds, material, paint);
}
let path = circle_path(center, radius);
self.draw_path(&path, paint)
}
pub fn draw_oval(&mut self, bounds: Rect, paint: &Paint) -> Result<&mut Self> {
if bounds.is_empty() {
return Ok(self);
}
if let Some(material) = self.analytic_ellipse(bounds, paint) {
return self.draw_analytic(bounds, material, paint);
}
let path = oval_path(bounds);
self.draw_path(&path, paint)
}
fn analytic_ellipse(&self, bounds: Rect, paint: &Paint) -> Option<Material> {
if !paint.is_visible() || self.clip.is_some_and(Scissor::is_empty) {
return None;
}
let (stroke, coverage) =
thin_stroke(self.transform, analytic_stroke(paint)?, Zero::IsNotAStroke);
let Shader::Solid(color) = &paint.shader else {
return None;
};
let to_clip = self.target.projection() * self.transform;
let center = Vec2::new(
(bounds.left + bounds.right) / 2.0,
(bounds.top + bounds.bottom) / 2.0,
);
Some(Material::Ellipse {
color: dimmed(*color, coverage),
half_size: [bounds.width() / 2.0, bounds.height() / 2.0],
to_local: invert_to_local(to_clip * Affine2::from_translation(center)),
stroke,
})
}
pub fn draw_glyphs(
&mut self,
glyphs: &[PositionedGlyph],
atlas: &Atlas,
atlas_slot: u32,
paint: &Paint,
) -> Result<&mut Self> {
if glyphs.is_empty() || !paint.is_visible() {
return Ok(self);
}
if self.clip.is_some_and(Scissor::is_empty) {
return Ok(self);
}
if !paint.image_filter.is_identity() {
return self.draw_glyphs_filtered(glyphs, atlas, atlas_slot, paint);
}
if paint.mask_blur > 0.0 {
return self.draw_masked(
Masked::Glyphs {
glyphs,
atlas,
slot: atlas_slot,
},
paint,
);
}
let color = match &paint.shader {
Shader::Solid(color) => color.to_array(),
_ => {
return Err(Error::Unsupported(
"glyphs take a solid color; fill a layer through a gradient instead",
))
}
};
let to_clip = self.target.projection() * self.transform;
let mut vertices = Vec::with_capacity(glyphs.len() * 4);
let mut indices = Vec::with_capacity(glyphs.len() * 6);
for glyph in glyphs {
let Some(rect) = atlas.get(glyph.key) else {
return Err(Error::Unsupported(
"a glyph in this run is not in the atlas",
));
};
if rect.width == 0 || rect.height == 0 {
continue;
}
let [u0, v0, u1, v1] = rect.uv(atlas.size());
let [x, y] = glyph.position;
let [w, h] = glyph.size;
let corners = [
([x, y], [u0, v0]),
([x + w, y], [u1, v0]),
([x + w, y + h], [u1, v1]),
([x, y + h], [u0, v1]),
];
let base = vertices.len() as u32;
for ([px, py], uv) in corners {
let clip = to_clip.project_homogeneous(Vec2::new(px, py));
vertices.push(Vertex::projected([clip.x, clip.y, clip.z], uv));
}
indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
}
if indices.is_empty() {
return Ok(self);
}
let slot = self.slot_for(TextureSource::Image(atlas_slot));
self.batch.push_mesh(
&vertices,
&indices,
Material::Glyph { color, slot },
paint.color_filter,
paint.blend,
self.clip,
ClipState::content(self.depth),
)?;
Ok(self)
}
pub fn draw_vertices(&mut self, mesh: &Vertices, paint: &Paint) -> Result<&mut Self> {
if mesh.is_empty() || !paint.is_visible() {
return Ok(self);
}
if self.clip.is_some_and(Scissor::is_empty) {
return Ok(self);
}
if !paint.image_filter.is_identity() {
return self.draw_vertices_filtered(mesh, paint);
}
if paint.mask_blur > 0.0 {
if mesh.colors().is_empty() {
return self.draw_masked(Masked::Mesh(mesh), paint);
}
return Err(Error::Unsupported(
"a mask blur over a mesh with per-vertex colors has no color for \
its halo; draw the mesh into a blurred layer instead",
));
}
let textured = !mesh.texture_coords().is_empty();
let at_coords = textured && !matches!(paint.shader, Shader::Image { .. });
let material = match (&paint.shader, textured) {
(Shader::Image { .. }, true) => self.mesh_material(&paint.shader)?,
_ if at_coords => self.material_at_texture_coords(&paint.shader),
_ => self.material_for(&paint.shader),
};
let to_clip = self.target.projection() * self.transform;
let coords = mesh.texture_coords();
let colors = mesh.colors();
let vertices: Vec<Vertex> = mesh
.positions()
.iter()
.enumerate()
.map(|(i, position)| {
let clip = to_clip.project_homogeneous(*position);
let uv = coords.get(i).copied().unwrap_or(Vec2::ZERO);
let vertex = Vertex::projected([clip.x, clip.y, clip.z], [uv.x, uv.y]);
match colors.get(i) {
Some(color) => vertex.with_color(premultiplied(*color)),
None => vertex,
}
})
.collect();
self.batch.push_mesh_tinted(
&vertices,
mesh.indices(),
material,
paint.color_filter,
paint.blend,
self.clip,
ClipState::content(self.depth),
paint.tint_blend,
at_coords,
)?;
Ok(self)
}
pub fn draw_atlas(
&mut self,
sprites: &[Sprite],
sheet: Extent2D,
paint: &Paint,
) -> Result<&mut Self> {
if sprites.is_empty() || !paint.is_visible() {
return Ok(self);
}
if sheet.width == 0 || sheet.height == 0 {
return Err(Error::Unsupported(
"a sprite sheet with no texels has nothing to read",
));
}
if !sprites.iter().all(|s| s.source.is_finite()) {
return Err(Error::Unsupported(
"a sprite's source rectangle is not a finite number",
));
}
let texels = Vec2::new(sheet.width as f32, sheet.height as f32);
let mut positions = Vec::with_capacity(sprites.len() * 4);
let mut coords = Vec::with_capacity(sprites.len() * 4);
let mut colors = Vec::with_capacity(sprites.len() * 4);
let mut indices = Vec::with_capacity(sprites.len() * 6);
for sprite in sprites {
let source = &sprite.source;
let size = Vec2::new(source.width, source.height);
let origin = Vec2::new(source.x, source.y);
let corners = [
Vec2::ZERO,
Vec2::new(size.x, 0.0),
size,
Vec2::new(0.0, size.y),
];
let base = positions.len() as u32;
for corner in corners {
positions.push(sprite.transform.transform_point2(corner));
coords.push((origin + corner) / texels);
colors.push(sprite.color);
}
indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
}
let mesh = Vertices::full(VertexMode::Triangles, positions, coords, colors, indices)?;
self.draw_vertices(&mesh, paint)
}
fn mesh_material(&mut self, shader: &Shader) -> Result<Material> {
let Shader::Image {
slot,
alpha,
tile,
tint,
source,
sampling,
..
} = shader
else {
return Err(Error::Unsupported("a mesh material needs an image paint"));
};
if *source != Rect::new(0.0, 0.0, 1.0, 1.0) {
return Err(Error::Unsupported(
"a textured mesh states its own coordinates; put the source rectangle in them",
));
}
let table_slot = self.slot_for(TextureSource::Image(*slot));
Ok(Material::Mesh {
slot: table_slot,
alpha: *alpha,
tint: tint.to_array(),
tile: *tile,
sampling: *sampling,
})
}
pub fn draw_points(
&mut self,
mode: PointMode,
points: &[Vec2],
paint: &Paint,
) -> Result<&mut Self> {
let stroke = match &paint.style {
Style::Stroke(stroke) => *stroke,
Style::Fill => StrokeStyle::default(),
};
if !stroke.width.is_finite() || stroke.width < 0.0 {
return Ok(self);
}
match mode {
PointMode::Points => {
let radius = point_radius(self.transform, stroke.width);
if !matches!(stroke.cap, LineCap::Round | LineCap::Square) {
return Ok(self);
}
let round = matches!(stroke.cap, LineCap::Round);
if paint.mask_blur > 0.0 || !paint.image_filter.is_identity() {
return self.draw_points_individually(points, &stroke, paint);
}
let material = self.material_for(&paint.shader);
let Material::Solid(color) = material else {
return self.draw_points_individually(points, &stroke, paint);
};
let (extent, coord) = if round {
(radius + 1.0, (radius + 1.0) / radius.max(1e-6))
} else {
(radius, 0.0)
};
let to_clip = self.target.projection() * self.transform;
let mut vertices: Vec<Vertex> = Vec::with_capacity(points.len() * 4);
let mut indices: Vec<u32> = Vec::with_capacity(points.len() * 6);
for point in points {
if !point.is_finite() {
continue;
}
let base = vertices.len() as u32;
for (dx, dy) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
let at = Vec2::new(point.x + dx * extent, point.y + dy * extent);
let clip = to_clip.project_homogeneous(at);
vertices.push(Vertex::projected(
[clip.x, clip.y, clip.z],
[dx * coord, dy * coord],
));
}
indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
}
if indices.is_empty() {
return Ok(self);
}
self.batch.push_mesh_tinted(
&vertices,
&indices,
Material::PointField { color },
paint.color_filter,
paint.blend,
self.clip,
ClipState::content(self.depth),
paint.tint_blend,
false,
)?;
}
PointMode::Lines => {
for pair in points.chunks_exact(2) {
self.draw_line(pair[0], pair[1], paint)?;
}
}
PointMode::Polygon => {
if points.len() >= 2 {
let mut b = PathBuilder::new();
b.move_to(points[0]);
for point in &points[1..] {
b.line_to(*point);
}
self.draw_path(&b.build(), paint)?;
}
}
}
Ok(self)
}
fn draw_points_individually(
&mut self,
points: &[Vec2],
stroke: &StrokeStyle,
paint: &Paint,
) -> Result<&mut Self> {
let radius = point_radius(self.transform, stroke.width);
let dot = paint.clone().with_style(Style::Fill);
for point in points {
if !point.is_finite() {
continue;
}
match stroke.cap {
LineCap::Round => {
self.draw_circle(*point, radius, &dot)?;
}
LineCap::Square => {
self.draw_rect(
Rect::new(
point.x - radius,
point.y - radius,
point.x + radius,
point.y + radius,
),
&dot,
)?;
}
LineCap::Butt => {}
}
}
Ok(self)
}
pub fn draw_recording(&mut self, recording: &Recording, paint: &Paint) -> Result<&mut Self> {
if recording.is_empty() || !paint.is_visible() {
return Ok(self);
}
if self.clip.is_some_and(Scissor::is_empty) {
return Ok(self);
}
let pass_offset = self.finished.len();
let ramp_offset = self.ramps.len();
self.ramps.extend(recording.ramps.iter().cloned());
for pass in &recording.passes {
let mut pass = pass.clone();
for source in &mut pass.sources {
*source = match *source {
TextureSource::Layer(index) => TextureSource::Layer(index + pass_offset),
TextureSource::Ramp(index) => TextureSource::Ramp(index + ramp_offset),
TextureSource::Image(index) => TextureSource::Image(index),
};
}
self.finished.push(pass);
}
let root = self.finished.len() - 1;
let slot = self.slot_for(TextureSource::Layer(root));
let extent = recording.extent;
let placement = Rect::new(0.0, 0.0, extent.width as f32, extent.height as f32);
let to_clip = self.target.projection() * self.transform;
if to_clip.determinant().abs() <= f32::EPSILON {
return Ok(self);
}
let picture_to_clip =
to_clip * Affine2::from_scale(Vec2::new(extent.width as f32, extent.height as f32));
if !picture_to_clip.is_finite() {
return Ok(self);
}
let material = Material::Image {
to_local: invert_to_local(picture_to_clip),
slot,
alpha: 1.0,
tile: TileMode::Clamp,
sampling: Sampling::Linear,
source: [0.0, 0.0, 1.0, 1.0],
tint: [1.0, 1.0, 1.0, 1.0],
};
let render_paint = RenderPaint {
material,
filter: paint.color_filter,
blend: paint.blend,
clip: self.clip,
stencil: ClipState::content(self.depth),
};
self.renderer.fill_into(
&mut self.batch,
&placement.to_path(),
self.transform,
&render_paint,
)?;
Ok(self)
}
pub fn draw_drrect(
&mut self,
outer: Rect,
outer_radius: f32,
inner: Rect,
inner_radius: f32,
paint: &Paint,
) -> Result<&mut Self> {
if outer.is_empty() {
return Ok(self);
}
self.draw_drrect_with_radii(
outer,
[[outer_radius; 2]; 4],
inner,
[[inner_radius; 2]; 4],
paint,
)
}
pub fn draw_drrect_with_radii(
&mut self,
outer: Rect,
outer_radii: RoundingRadii,
inner: Rect,
inner_radii: RoundingRadii,
paint: &Paint,
) -> Result<&mut Self> {
if outer.is_empty() {
return Ok(self);
}
let mut b = PathBuilder::new().with_fill_rule(FillRule::EvenOdd);
outer.add_rounded_contour_with_radii(&mut b, outer.fitted_radii(outer_radii));
if !inner.is_empty() {
inner.add_rounded_contour_with_radii(&mut b, inner.fitted_radii(inner_radii));
}
self.draw_path(&b.build(), paint)
}
pub fn draw_paint(&mut self, paint: &Paint) -> Result<&mut Self> {
let bounds = self.local_clip_bounds();
if bounds.is_empty() {
return Ok(self);
}
self.draw_rect(bounds, paint)
}
pub fn draw_color(&mut self, color: Color, blend: BlendMode) -> Result<&mut Self> {
self.draw_paint(&Paint::fill(color).with_blend(blend))
}
pub fn draw_image_nine(
&mut self,
slot: u32,
size: Extent2D,
center: Rect,
into: Rect,
paint: &Paint,
) -> Result<&mut Self> {
if size.width == 0 || size.height == 0 || into.is_empty() {
return Ok(self);
}
let (w, h) = (size.width as f32, size.height as f32);
if center.left < 0.0
|| center.top < 0.0
|| center.right > w
|| center.bottom > h
|| center.right < center.left
|| center.bottom < center.top
{
return Err(Error::Unsupported(
"a nine-patch center must lie within the image it divides",
));
}
if paint.mask_blur > 0.0 {
return Err(Error::Unsupported(
"a mask blur over a nine-patch; its pieces are drawn separately \
and would each be softened on their own",
));
}
let source_x = [0.0, center.left, center.right, w];
let source_y = [0.0, center.top, center.bottom, h];
let left_keep = center.left;
let right_keep = w - center.right;
let top_keep = center.top;
let bottom_keep = h - center.bottom;
let squeeze_x = ((into.right - into.left) / (left_keep + right_keep)).min(1.0);
let squeeze_y = ((into.bottom - into.top) / (top_keep + bottom_keep)).min(1.0);
let dest_x = [
into.left,
into.left + left_keep * squeeze_x,
into.right - right_keep * squeeze_x,
into.right,
];
let dest_y = [
into.top,
into.top + top_keep * squeeze_y,
into.bottom - bottom_keep * squeeze_y,
into.bottom,
];
let (half_u, half_v) = (0.5 / w, 0.5 / h);
let mut positions = Vec::with_capacity(36);
let mut coords = Vec::with_capacity(36);
let mut indices = Vec::with_capacity(54);
for row in 0..3 {
for column in 0..3 {
let source = Rect::new(
source_x[column] / w,
source_y[row] / h,
source_x[column + 1] / w,
source_y[row + 1] / h,
);
let destination = Rect::new(
dest_x[column],
dest_y[row],
dest_x[column + 1],
dest_y[row + 1],
);
if source.is_empty() || destination.is_empty() {
continue;
}
let mid_u = (source.left + source.right) * 0.5;
let mid_v = (source.top + source.bottom) * 0.5;
let u0 = (source.left + half_u).min(mid_u);
let u1 = (source.right - half_u).max(mid_u);
let v0 = (source.top + half_v).min(mid_v);
let v1 = (source.bottom - half_v).max(mid_v);
let base = positions.len() as u32;
positions.extend([
Vec2::new(destination.left, destination.top),
Vec2::new(destination.right, destination.top),
Vec2::new(destination.right, destination.bottom),
Vec2::new(destination.left, destination.bottom),
]);
coords.extend([
Vec2::new(u0, v0),
Vec2::new(u1, v0),
Vec2::new(u1, v1),
Vec2::new(u0, v1),
]);
indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
}
}
if positions.is_empty() {
return Ok(self);
}
let mesh = crate::vertices::Vertices::indexed(
crate::vertices::VertexMode::Triangles,
positions,
coords,
indices,
)?;
let painted = paint.clone().with_shader(Shader::Image {
slot,
rect: into,
alpha: 1.0,
tile: TileMode::Clamp,
source: Rect::new(0.0, 0.0, 1.0, 1.0),
tint: Color::WHITE,
sampling: Sampling::Linear,
});
self.draw_vertices(&mesh, &painted)
}
pub fn draw_line(&mut self, from: Vec2, to: Vec2, paint: &Paint) -> Result<&mut Self> {
if let Some((p0, p1)) = self.hairline_snap(from, to, paint) {
let mut b = PathBuilder::new();
b.move_to(p0).line_to(p1);
let path = b.build();
let held = self.transform;
self.transform = Transform2D::IDENTITY;
let drawn = self.draw_path(&path, paint).map(|_| ());
self.transform = held;
drawn?;
return Ok(self);
}
let mut b = PathBuilder::new();
b.move_to(from).line_to(to);
let path = b.build();
self.draw_path(&path, paint)
}
fn hairline_snap(&self, from: Vec2, to: Vec2, paint: &Paint) -> Option<(Vec2, Vec2)> {
let Style::Stroke(stroke) = &paint.style else {
return None;
};
if stroke.width != 0.0 {
return None;
}
if paint.mask_blur > 0.0 || !paint.image_filter.is_identity() {
return None;
}
if !matches!(paint.shader, Shader::Solid(_)) {
return None;
}
let affine = self.transform.to_affine()?;
let m = affine.matrix2;
if m.x_axis.y != 0.0 || m.y_axis.x != 0.0 {
return None;
}
let (p0, p1) = (affine.transform_point2(from), affine.transform_point2(to));
if !p0.is_finite() || !p1.is_finite() {
return None;
}
let to_half = |v: f32| v.floor() + 0.5;
if (p0.x - p1.x).abs() < CLOSE_ENOUGH {
let x = to_half(p0.x);
return Some((Vec2::new(x, p0.y), Vec2::new(x, p1.y)));
}
if (p0.y - p1.y).abs() < CLOSE_ENOUGH {
let y = to_half(p0.y);
return Some((Vec2::new(p0.x, y), Vec2::new(p1.x, y)));
}
None
}
fn covered(batch: &Batch, target: Target) -> Option<Rect> {
let (w, h) = (target.extent.width as f32, target.extent.height as f32);
let (mut left, mut top) = (f32::INFINITY, f32::INFINITY);
let (mut right, mut bottom) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
for vertex in batch.vertices() {
let [x, y, w_clip] = vertex.position;
if !w_clip.is_finite() || w_clip.abs() <= 1e-6 || !x.is_finite() || !y.is_finite() {
return None;
}
let dx = (x / w_clip + 1.0) * 0.5 * w;
let dy = (1.0 - y / w_clip) * 0.5 * h;
left = left.min(dx);
right = right.max(dx);
top = top.min(dy);
bottom = bottom.max(dy);
}
if !(right > left && bottom > top) {
return None;
}
Some(Rect::new(
target.origin.x + left,
target.origin.y + top,
target.origin.x + right,
target.origin.y + bottom,
))
}
fn narrowed(
&self,
batch: &Batch,
filter: Option<&ImageFilter>,
paint: Layer,
basis: BlurBasis,
layer: Target,
) -> Option<Target> {
if paint.blend.is_destructive() {
return None;
}
let content = Self::covered(batch, layer)?;
let reach = paint.reach(basis);
let (min, max) = (
Vec2::new(content.left - reach.x, content.top - reach.y),
Vec2::new(content.right + reach.x, content.bottom + reach.y),
);
let (min, max) = match filter {
Some(filter) => filter.covering(min, max, &|sigma| basis.reach(sigma)),
None => (min, max),
};
let left = min.x.floor().max(layer.origin.x);
let top = min.y.floor().max(layer.origin.y);
let right = max.x.ceil().min(layer.origin.x + layer.extent.width as f32);
let bottom = max
.y
.ceil()
.min(layer.origin.y + layer.extent.height as f32);
if !(right > left && bottom > top) {
return None;
}
let narrowed = Target {
origin: Vec2::new(left, top),
extent: Extent2D::new((right - left) as u32, (bottom - top) as u32),
};
if narrowed.extent == layer.extent {
return None;
}
Some(narrowed)
}
fn finish_layer(&mut self, frame: LayerFrame) {
let (filter, paint) = (frame.filter.clone(), frame.paint);
let mut batch = std::mem::replace(&mut self.batch, frame.batch);
let sources = std::mem::replace(&mut self.sources, frame.sources);
let opened = self.target;
let layer = match self.narrowed(&batch, filter.as_ref(), paint, frame.blur_basis, opened) {
Some(narrowed) => {
let (dx, dy) = (
(narrowed.origin.x - opened.origin.x) as u32,
(narrowed.origin.y - opened.origin.y) as u32,
);
batch.rebase_scissors(dx, dy, narrowed.extent);
narrowed
}
None => opened,
};
let viewport = (layer.extent != opened.extent).then(|| PassViewport {
offset: [
layer.origin.x - opened.origin.x,
layer.origin.y - opened.origin.y,
]
.map(|v| -v),
extent: opened.extent,
});
self.aim_at(frame.parent);
self.finished.push(Pass {
batch,
descriptor: PassDescriptor {
clear: Some([0.0; 4]),
samples: self.pass_samples(),
viewport,
},
sources,
extent: layer.extent,
});
self.anti_alias = frame.anti_alias;
let mut index = self.finished.len() - 1;
if frame.paint.blur.max_element() > 0.0 {
index = self.blur_passes(index, layer, frame.paint.blur, frame.blur_basis);
}
if let Some(morphology) = frame.paint.morphology {
index = self.morphology_passes(index, layer, morphology);
}
if let Some(filter) = frame.filter.clone() {
if let Ok(filtered) = self.filter_passes(index, layer, &filter, frame.blur_basis) {
index = filtered;
}
}
let slot = self.slot_for(TextureSource::Layer(index));
let parent = frame.parent;
let offset = layer.origin - parent.origin;
let anchor = Vec2::new(
-1.0 + 2.0 * offset.x / parent.extent.width as f32,
1.0 - 2.0 * offset.y / parent.extent.height as f32,
);
let scale = Mat2::from_diagonal(Vec2::new(
0.5 * parent.extent.width as f32 / layer.extent.width as f32,
-0.5 * parent.extent.height as f32 / layer.extent.height as f32,
));
let clip_to_texture = Affine2::from_mat2(scale) * Affine2::from_translation(-anchor);
let projection = self.target.projection();
let placement = frame.paint.matrix.and_then(|matrix| {
let in_clip = projection * matrix * projection.inverse()?;
in_clip.is_finite().then_some(())?;
Some((matrix, in_clip.inverse()?))
});
let clip_to_texture = Transform2D::from(clip_to_texture);
let clip_to_texture = match placement {
None => clip_to_texture,
Some((_, undo)) => clip_to_texture * undo,
};
let material = Material::Image {
to_local: to_local_columns(clip_to_texture),
slot,
alpha: frame.paint.alpha,
tile: TileMode::Clamp,
sampling: Sampling::Linear,
source: [0.0, 0.0, 1.0, 1.0],
tint: [1.0, 1.0, 1.0, 1.0],
};
let paint = RenderPaint {
material,
filter: frame.paint.color_filter,
blend: frame.paint.blend,
clip: self.clip,
stencil: ClipState::content(self.depth),
};
let whole = layer.path();
let _ = self.renderer.fill_into(
&mut self.batch,
&whole,
placement.map_or(Transform2D::IDENTITY, |(matrix, _)| matrix),
&paint,
);
}
fn filter_passes(
&mut self,
source: usize,
target: Target,
filter: &ImageFilter,
basis: BlurBasis,
) -> Result<usize> {
Ok(match filter {
ImageFilter::None => source,
ImageFilter::Blur { sigma_x, sigma_y } => {
self.blur_passes(source, target, Vec2::new(*sigma_x, *sigma_y), basis)
}
ImageFilter::Dilate { radius_x, radius_y } => {
self.morphology_passes(source, target, Morphology::dilate(*radius_x, *radius_y))
}
ImageFilter::Erode { radius_x, radius_y } => {
self.morphology_passes(source, target, Morphology::erode(*radius_x, *radius_y))
}
ImageFilter::Runtime { program, uniforms } => {
self.runtime_pass(source, target, *program, uniforms)
}
ImageFilter::Color(recolor) => self.recolor_pass(source, target, *recolor),
ImageFilter::Compose { outer, inner } => {
let inner = self.filter_passes(source, target, inner, basis)?;
self.filter_passes(inner, target, outer, basis)?
}
ImageFilter::Matrix { .. } => {
return Err(Error::Unsupported(
"a matrix inside a composition is not available as a filter; it \
moves the image rather than recomputing it in place, and only \
the outermost one folds into a backdrop's seed",
))
}
})
}
fn recolor_pass(&mut self, source: usize, target: Target, recolor: ColorFilter) -> usize {
let to_local = to_local_columns(Transform2D::from(
Affine2::from_mat2(Mat2::from_diagonal(Vec2::new(0.5, -0.5)))
* Affine2::from_translation(Vec2::new(1.0, -1.0)),
));
self.filter_pass_recolored(
source,
target,
Material::Image {
to_local,
slot: 0,
alpha: 1.0,
tile: TileMode::Clamp,
sampling: Sampling::Linear,
source: [0.0, 0.0, 1.0, 1.0],
tint: [1.0, 1.0, 1.0, 1.0],
},
recolor,
)
}
fn runtime_pass(
&mut self,
source: usize,
target: Target,
program: u32,
uniforms: &[f32],
) -> usize {
let mut textures = [None; impeller_hal::MAX_EFFECT_TEXTURES];
textures[0] = Some(0);
self.filter_pass(
source,
target,
Material::Runtime {
program,
uniforms: uniforms.to_vec(),
textures,
},
)
}
fn filter_pass(&mut self, source: usize, target: Target, material: Material) -> usize {
self.filter_pass_recolored(source, target, material, ColorFilter::None)
}
fn filter_pass_recolored(
&mut self,
source: usize,
target: Target,
material: Material,
recolor: ColorFilter,
) -> usize {
let mut batch = Batch::new();
let sources = vec![TextureSource::Layer(source)];
let paint = RenderPaint {
material,
filter: recolor,
blend: BlendMode::Src,
clip: None,
stencil: ClipState::UNCLIPPED,
};
let quad = target.path();
self.renderer.set_viewport(target.origin, target.extent);
let _ = self
.renderer
.fill_into(&mut batch, &quad, Affine2::IDENTITY, &paint);
self.renderer
.set_viewport(self.target.origin, self.target.extent);
self.finished.push(Pass {
batch,
descriptor: PassDescriptor {
clear: Some([0.0; 4]),
samples: 1,
viewport: None,
},
sources,
extent: target.extent,
});
self.finished.len() - 1
}
fn morphology_passes(
&mut self,
source: usize,
target: Target,
morphology: Morphology,
) -> usize {
let to_local = to_local_columns(Transform2D::from(
Affine2::from_mat2(Mat2::from_diagonal(Vec2::new(0.5, -0.5)))
* Affine2::from_translation(Vec2::new(1.0, -1.0)),
));
let axes = [
(
[1.0 / target.extent.width as f32, 0.0],
morphology.radius[0],
),
(
[0.0, 1.0 / target.extent.height as f32],
morphology.radius[1],
),
];
let mut sampled = source;
for (step, radius) in axes {
let mut left = radius;
while left > 0.0 {
let taken = left.min(MORPHOLOGY_TAPS as f32);
left -= taken;
let material = Material::Morphology {
to_local,
slot: 0,
step,
radius: taken,
dilate: morphology.dilate,
};
sampled = self.filter_pass(sampled, target, material);
}
}
sampled
}
fn halve_pass(&mut self, source: usize, into: Target) -> usize {
let to_local = to_local_columns(Transform2D::from(
Affine2::from_mat2(Mat2::from_diagonal(Vec2::new(0.5, -0.5)))
* Affine2::from_translation(Vec2::new(1.0, -1.0)),
));
self.filter_pass(
source,
into,
Material::Image {
to_local,
slot: 0,
alpha: 1.0,
tile: TileMode::Clamp,
sampling: Sampling::Linear,
source: [0.0, 0.0, 1.0, 1.0],
tint: [1.0, 1.0, 1.0, 1.0],
},
)
}
fn blur_downsample(sigma: f32, extent: u32) -> u32 {
let mut scale = 1u32;
while blur_radius(sigma / scale as f32) > BLUR_MAX_TAPS && extent / (scale * 2) >= 1 {
scale *= 2;
}
scale
}
fn blur_passes(
&mut self,
source: usize,
target: Target,
sigma: Vec2,
basis: BlurBasis,
) -> usize {
let to_local = to_local_columns(Transform2D::from(
Affine2::from_mat2(Mat2::from_diagonal(Vec2::new(0.5, -0.5)))
* Affine2::from_translation(Vec2::new(1.0, -1.0)),
));
let scale = if basis.is_axis_aligned() {
[
Self::blur_downsample(sigma.x, target.extent.width),
Self::blur_downsample(sigma.y, target.extent.height),
]
} else {
let smallest = target.extent.width.min(target.extent.height);
let together = Self::blur_downsample(sigma.x, smallest)
.max(Self::blur_downsample(sigma.y, smallest));
[together, together]
};
let reduced = |by: [u32; 2]| Target {
origin: target.origin,
extent: Extent2D::new(
(target.extent.width / by[0]).max(1),
(target.extent.height / by[1]).max(1),
),
};
let blurred = reduced(scale);
let sigma = Vec2::new(sigma.x / scale[0] as f32, sigma.y / scale[1] as f32);
let mut source = source;
let mut step_scale = [1u32; 2];
while step_scale[0] < scale[0] || step_scale[1] < scale[1] {
for axis in 0..2 {
if step_scale[axis] < scale[axis] {
step_scale[axis] *= 2;
}
}
source = self.halve_pass(source, reduced(step_scale));
}
let (w, h) = (blurred.extent.width as f32, blurred.extent.height as f32);
let [u, v] = basis.directions;
let steps = [(sigma.x, [u.x / w, u.y / h]), (sigma.y, [v.x / w, v.y / h])];
let mut sampled = source;
for (sigma, step) in steps {
if !ImageFilter::blurs(sigma) {
continue;
}
sampled = self.filter_pass(
sampled,
blurred,
Material::Blur {
to_local,
slot: 0,
step,
sigma,
},
);
}
sampled
}
fn aim_at(&mut self, target: Target) {
self.target = target;
self.renderer.set_viewport(target.origin, target.extent);
}
fn pass_samples(&self) -> u32 {
if self.anti_alias {
self.samples
} else {
1
}
}
pub fn finish(mut self) -> Recording {
while self.stack.iter().any(|s| s.layer.is_some()) {
self.restore();
}
let samples = self.pass_samples();
let root = Pass {
batch: self.batch,
descriptor: PassDescriptor {
clear: self.background.map(|c| c.to_array()),
samples,
viewport: None,
},
sources: self.sources,
extent: self.extent,
};
let mut passes = self.finished;
passes.push(root);
Recording {
passes,
extent: self.extent,
ramps: self.ramps,
}
}
}
fn max_scale_of(transform: Transform2D) -> f32 {
match transform.to_affine() {
Some(affine) => max_scale(&affine),
None => 1.0,
}
}
const KAPPA: f32 = 0.552_284_8;
const MIN_STROKE_PIXELS: f32 = 1.0;
fn thin_stroke(transform: Transform2D, width: f32, zero: Zero) -> (f32, f32) {
let Some(affine) = transform.to_affine() else {
return (width, 1.0);
};
if !width.is_finite() || width < 0.0 {
return (width, 1.0);
}
if width == 0.0 && zero == Zero::IsNotAStroke {
return (width, 1.0);
}
let basis = max_scale(&affine);
if !basis.is_finite() || basis <= 0.0 {
return (width, 1.0);
}
let scaled = basis * width;
if scaled >= MIN_STROKE_PIXELS {
return (width, 1.0);
}
let coverage = match scaled == 0.0 {
true => 1.0,
false => (scaled * 2.0).clamp(0.0, 1.0),
};
(MIN_STROKE_PIXELS / basis, coverage)
}
fn point_radius(transform: Transform2D, width: f32) -> f32 {
let radius = width / 2.0;
let Some(affine) = transform.to_affine() else {
return radius;
};
if !radius.is_finite() || radius < 0.0 {
return radius;
}
let basis = max_scale(&affine);
if !basis.is_finite() || basis <= 0.0 {
return radius;
}
radius.max(MIN_POINT_RADIUS_PIXELS / basis)
}
const CLOSE_ENOUGH: f32 = 1e-3;
const MIN_POINT_RADIUS_PIXELS: f32 = 0.5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Zero {
IsAHairline,
IsNotAStroke,
}
fn dimmed(color: crate::Color, factor: f32) -> [f32; 4] {
let mut c = color.to_array();
c[3] *= factor;
c
}
fn analytic_stroke(paint: &Paint) -> Option<f32> {
if !paint.anti_alias {
return None;
}
if !paint.blend.respects_coverage() {
return None;
}
if paint.dash.as_ref().is_some_and(|dash| dash.is_usable()) {
return None;
}
if paint.mask_blur > 0.0 {
return None;
}
if !paint.image_filter.is_identity() {
return None;
}
match &paint.style {
Style::Fill => Some(0.0),
Style::Stroke(stroke) => {
let width = stroke.width;
(width.is_finite() && width > 0.0).then_some(width)
}
}
}
fn oval_path(bounds: Rect) -> Path {
let (cx, cy) = (
(bounds.left + bounds.right) / 2.0,
(bounds.top + bounds.bottom) / 2.0,
);
let (rx, ry) = (bounds.width() / 2.0, bounds.height() / 2.0);
let (kx, ky) = (KAPPA * rx, KAPPA * ry);
let mut b = PathBuilder::new();
b.move_to(Vec2::new(cx + rx, cy))
.cubic_to(
Vec2::new(cx + rx, cy + ky),
Vec2::new(cx + kx, cy + ry),
Vec2::new(cx, cy + ry),
)
.cubic_to(
Vec2::new(cx - kx, cy + ry),
Vec2::new(cx - rx, cy + ky),
Vec2::new(cx - rx, cy),
)
.cubic_to(
Vec2::new(cx - rx, cy - ky),
Vec2::new(cx - kx, cy - ry),
Vec2::new(cx, cy - ry),
)
.cubic_to(
Vec2::new(cx + kx, cy - ry),
Vec2::new(cx + rx, cy - ky),
Vec2::new(cx + rx, cy),
)
.close();
b.build()
}
fn circle_path(center: Vec2, radius: f32) -> Path {
let (cx, cy) = (center.x, center.y);
let r = radius;
let k = KAPPA * r;
let mut b = PathBuilder::new();
b.as_rounded_rect(
GeometryRect::new(Vec2::new(cx - r, cy - r), Vec2::new(cx + r, cy + r)),
r,
);
b.move_to(Vec2::new(cx + r, cy))
.cubic_to(
Vec2::new(cx + r, cy + k),
Vec2::new(cx + k, cy + r),
Vec2::new(cx, cy + r),
)
.cubic_to(
Vec2::new(cx - k, cy + r),
Vec2::new(cx - r, cy + k),
Vec2::new(cx - r, cy),
)
.cubic_to(
Vec2::new(cx - r, cy - k),
Vec2::new(cx - k, cy - r),
Vec2::new(cx, cy - r),
)
.cubic_to(
Vec2::new(cx + k, cy - r),
Vec2::new(cx + r, cy - k),
Vec2::new(cx + r, cy),
)
.close();
b.build()
}
const LIGHT_RADIUS: f32 = 1.0;
const KERNEL_RADIUS_PER_SIGMA: f32 = 1.732_050_8;
fn sigma_for_radius(radius: f32) -> f32 {
if radius > 0.0 {
radius / KERNEL_RADIUS_PER_SIGMA + 0.5
} else {
0.0
}
}
const SHADOW_ALPHA: f32 = 0.25;
fn tonal_shadow_color(color: Color) -> Color {
let [r, g, b, a] = color.to_srgb();
let alpha = a * SHADOW_ALPHA;
let luminance = (r.min(g).min(b) + r.max(g).max(b)) * 0.5;
let alpha_adjust = (2.6 + (-2.666_67 + 1.066_67 * alpha) * alpha) * alpha;
let color_alpha = (3.544_762 + (-4.891_428 + 2.346_6 * luminance) * luminance) * luminance;
let color_alpha = (alpha_adjust * color_alpha).clamp(0.0, 1.0);
let grayscale_alpha = (alpha * (1.0 - 0.4 * luminance)).clamp(0.0, 1.0);
let color_scale = color_alpha * (1.0 - grayscale_alpha);
let tonal_alpha = color_scale + grayscale_alpha;
let unpremul_scale = if tonal_alpha != 0.0 {
color_scale / tonal_alpha
} else {
0.0
};
Color::srgb(
unpremul_scale * r,
unpremul_scale * g,
unpremul_scale * b,
tonal_alpha,
)
}
pub(crate) const MAX_SIGMA: f32 = 500.0;
pub(crate) const BLUR_MAX_TAPS: f32 = 32.0;
pub(crate) fn erf7(value: f32) -> f32 {
let x = value * std::f32::consts::FRAC_2_SQRT_PI;
let xx = x * x;
let series = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx);
series / (1.0 + series * series).sqrt()
}
pub(crate) fn blur_radius(sigma: f32) -> f32 {
((sigma - 0.5) * KERNEL_RADIUS_PER_SIGMA).max(0.0)
}
pub(crate) fn blur_reach(sigma: f32) -> f32 {
if sigma > 0.0 {
blur_radius(sigma).ceil()
} else {
0.0
}
}
fn premultiplied(color: Color) -> [f32; 4] {
let [r, g, b, a] = color.to_array();
[r * a, g * a, b * a, a]
}
#[cfg(test)]
mod tests {
use super::*;
fn canvas() -> Canvas {
Canvas::new(Extent2D::new(128, 128))
}
#[test]
fn a_backdrop_layer_under_a_clip_is_no_larger_than_the_clip() {
let mut canvas = canvas();
canvas.clear(Color::srgb(0.2, 0.3, 0.4, 1.0));
canvas
.draw_rect(
Rect::new(0.0, 0.0, 128.0, 128.0),
&Paint::fill(Color::srgb(0.9, 0.2, 0.2, 1.0)),
)
.expect("the ground");
canvas.save();
canvas
.clip_rect(Rect::new(40.0, 40.0, 80.0, 80.0))
.expect("the clip");
canvas.save_layer(Layer::opacity(1.0).with_backdrop_blur(4.0));
canvas
.draw_rect(
Rect::new(40.0, 40.0, 80.0, 80.0),
&Paint::fill(Color::srgb(1.0, 0.6, 0.0, 0.6)),
)
.expect("the panel");
canvas.restore();
canvas.restore();
let recording = canvas.finish();
let frame = recording.extent;
let narrowed: Vec<_> = recording
.passes
.iter()
.map(|pass| pass.extent)
.filter(|extent| *extent != frame)
.collect();
assert_eq!(
narrowed.len(),
3,
"three passes should be sized to the clip, and {} are: {:?}",
narrowed.len(),
recording
.passes
.iter()
.map(|pass| pass.extent)
.collect::<Vec<_>>()
);
for extent in narrowed {
assert!(
extent.width <= 48 && extent.height <= 48,
"a pass came out {}x{} for a clip forty pixels across",
extent.width,
extent.height
);
}
}
#[test]
fn a_shared_backdrop_layer_is_narrowed_while_its_capture_is_not() {
let mut canvas = canvas();
canvas.clear(Color::srgb(0.2, 0.3, 0.4, 1.0));
let panel = |canvas: &mut Canvas, clip: Rect| {
canvas.save();
canvas.clip_rect(clip).expect("the clip");
canvas.save_layer(
Layer::opacity(1.0)
.with_backdrop_blur(4.0)
.with_backdrop_id(7),
);
canvas
.draw_rect(clip, &Paint::fill(Color::srgb(1.0, 0.6, 0.0, 0.6)))
.expect("the panel");
canvas.restore();
canvas.restore();
};
panel(&mut canvas, Rect::new(8.0, 8.0, 48.0, 48.0));
panel(&mut canvas, Rect::new(80.0, 80.0, 120.0, 120.0));
let recording = canvas.finish();
let frame = recording.extent;
let extents: Vec<_> = recording.passes.iter().map(|pass| pass.extent).collect();
let narrowed: Vec<_> = extents.iter().filter(|e| **e != frame).collect();
assert_eq!(
narrowed.len(),
2,
"one narrowed target per panel and no more, in {extents:?}"
);
for extent in narrowed {
assert!(
extent.width <= 40 && extent.height <= 40,
"a panel target came out {}x{} for a clip forty pixels across",
extent.width,
extent.height
);
}
assert_eq!(
extents.len(),
6,
"a cut, two blur passes, a target each, and the frame's own, in \
{extents:?}"
);
}
#[test]
fn narrowing_to_the_clip_records_what_stating_it_would() {
let build = |bounded: bool| {
let mut canvas = canvas();
canvas.clear(Color::srgb(0.1, 0.1, 0.1, 1.0));
canvas
.draw_rect(
Rect::new(0.0, 20.0, 128.0, 60.0),
&Paint::fill(Color::srgb(0.9, 0.4, 0.1, 1.0)),
)
.expect("the ground");
let clip = Rect::new(24.0, 34.0, 104.0, 94.0);
canvas.save();
canvas.clip_rect(clip).expect("the clip");
let layer = Layer::opacity(1.0).with_backdrop_blur(6.0);
if bounded {
canvas.save_layer_bounds(layer, clip);
} else {
canvas.save_layer(layer);
}
canvas.restore();
canvas.restore();
canvas.finish()
};
let from_clip = build(false);
let from_bounds = build(true);
assert_eq!(from_clip.passes.len(), from_bounds.passes.len());
for (a, b) in from_clip.passes.iter().zip(from_bounds.passes.iter()) {
assert_eq!(a.extent, b.extent, "the two spellings size a pass alike");
assert_eq!(
a.batch.vertices(),
b.batch.vertices(),
"the two spellings record the same geometry"
);
}
}
#[test]
fn a_blur_reaches_the_radius_its_kernel_is_truncated_at() {
assert_eq!(blur_reach(8.0), 13.0, "ceil(7.5 * sqrt(3))");
assert_eq!(blur_reach(2.0), 3.0, "ceil(1.5 * sqrt(3))");
assert!(
blur_reach(50.0) < 3.0 * 50.0,
"the kernel is truncated nearer one and three quarter deviations \
than three"
);
assert_eq!(blur_reach(0.0), 0.0);
assert_eq!(blur_reach(0.5), 0.0, "the half pixel is subtracted first");
assert_eq!(blur_reach(-1.0), 0.0);
let mut previous = 0.0;
for tenth in 0..600 {
let reach = blur_reach(tenth as f32 / 10.0);
assert!(
reach >= previous,
"reach fell at sigma {}",
tenth as f32 / 10.0
);
previous = reach;
}
}
#[test]
fn drawing_accumulates_into_one_batch() {
let mut canvas = canvas();
let paint = Paint::fill(Color::WHITE);
canvas
.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &paint)
.unwrap();
canvas
.draw_rect(Rect::new(20.0, 20.0, 30.0, 30.0), &paint)
.unwrap();
let recording = canvas.finish();
assert_eq!(recording.draw_count(), 2);
}
#[test]
fn an_invisible_paint_records_nothing() {
let mut canvas = canvas();
let invisible = Paint::fill(Color::WHITE.with_alpha(0.0));
canvas
.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &invisible)
.unwrap();
canvas
.draw_circle(Vec2::new(5.0, 5.0), 4.0, &invisible)
.unwrap();
assert!(canvas.finish().is_empty());
}
#[test]
fn outsetting_grows_every_side_and_negating_it_shrinks_back() {
let rect = Rect::new(10.0, 20.0, 40.0, 60.0);
let grown = rect.outset(5.0);
assert_eq!(
(grown.left, grown.top, grown.right, grown.bottom),
(5.0, 15.0, 45.0, 65.0)
);
let back = grown.outset(-5.0);
assert_eq!(
(back.left, back.top, back.right, back.bottom),
(rect.left, rect.top, rect.right, rect.bottom)
);
}
#[test]
fn degenerate_shapes_record_nothing() {
let mut canvas = canvas();
let paint = Paint::fill(Color::WHITE);
canvas
.draw_rect(Rect::new(10.0, 10.0, 10.0, 20.0), &paint)
.unwrap();
canvas.draw_circle(Vec2::ZERO, 0.0, &paint).unwrap();
assert!(canvas.finish().is_empty());
}
#[test]
fn save_and_restore_return_the_previous_transform() {
let mut canvas = canvas();
canvas.translate(10.0, 20.0);
let outer = canvas.transform();
canvas.save();
canvas.scale(3.0, 3.0).rotate(0.5);
assert_ne!(canvas.transform(), outer);
canvas.restore();
assert_eq!(canvas.transform(), outer);
assert_eq!(canvas.save_depth(), 0);
}
#[test]
fn saves_nest() {
let mut canvas = canvas();
let identity = canvas.transform();
canvas.save();
canvas.translate(5.0, 0.0);
canvas.save();
canvas.translate(5.0, 0.0);
assert_eq!(canvas.save_depth(), 2);
canvas.restore();
assert_eq!(
canvas.transform(),
Transform2D::from(Affine2::from_translation(Vec2::new(5.0, 0.0)))
);
canvas.restore();
assert_eq!(canvas.transform(), identity);
}
#[test]
fn restoring_too_often_leaves_the_transform_alone() {
let mut canvas = canvas();
canvas.translate(7.0, 3.0);
let current = canvas.transform();
canvas.restore().restore().restore();
assert_eq!(canvas.transform(), current);
}
#[test]
fn transforms_compose_in_the_order_applied() {
let mut canvas = canvas();
canvas.translate(10.0, 0.0).scale(2.0, 2.0);
let mapped = canvas.transform().project_point2(Vec2::new(1.0, 0.0));
assert!((mapped - Vec2::new(12.0, 0.0)).length() < 1e-5);
}
#[test]
fn antialiasing_is_off_unless_something_asks_for_it() {
let mut canvas = canvas();
let aliased = Paint::fill(Color::WHITE).with_anti_alias(false);
canvas
.draw_rect(Rect::from_size(10.0, 10.0), &aliased)
.unwrap();
assert_eq!(canvas.finish().root().descriptor.samples, 1);
}
#[test]
fn one_antialiased_shape_antialiases_the_frame() {
let mut canvas = canvas();
canvas
.draw_rect(
Rect::from_size(10.0, 10.0),
&Paint::fill(Color::WHITE).with_anti_alias(false),
)
.unwrap();
let mut triangle = PathBuilder::new();
triangle
.move_to(Vec2::new(20.0, 20.0))
.line_to(Vec2::new(40.0, 20.0))
.line_to(Vec2::new(20.0, 40.0))
.close();
canvas
.draw_path(&triangle.build(), &Paint::fill(Color::WHITE))
.unwrap();
assert!(canvas.finish().root().descriptor.samples > 1);
}
#[test]
fn a_shape_that_antialiases_itself_leaves_the_pass_alone() {
let mut canvas = canvas();
canvas
.draw_circle(Vec2::splat(20.0), 8.0, &Paint::fill(Color::WHITE))
.unwrap();
canvas
.draw_rrect(
Rect::new(30.0, 30.0, 60.0, 50.0),
6.0,
&Paint::fill(Color::WHITE),
)
.unwrap();
assert_eq!(
canvas.finish().root().descriptor.samples,
1,
"an analytic shape should not multisample the pass"
);
}
#[test]
fn a_colored_shadow_is_tonally_adjusted_and_a_black_one_is_not() {
let black = super::tonal_shadow_color(Color::srgb(0.0, 0.0, 0.0, 1.0));
let [r, g, b, a] = black.to_srgb();
assert_eq!([r, g, b], [0.0, 0.0, 0.0], "a black shadow gained color");
assert!(
(a - 0.25).abs() < 1e-6,
"a black shadow should keep the quarter alpha, got {a}"
);
let red = super::tonal_shadow_color(Color::srgb(1.0, 0.0, 0.0, 1.0));
let [r, g, b, a] = red.to_srgb();
assert!(
(a - 0.537_140).abs() < 1e-4,
"the tonal alpha for a red shadow is {a}, not 0.53714"
);
assert!(
(r - 0.627_650).abs() < 1e-4,
"the tonal red for a red shadow is {r}, not 0.62765"
);
assert_eq!([g, b], [0.0, 0.0], "a red shadow gained other channels");
assert!(
a > 0.5,
"the remap left the alpha near the naive quarter: {a}"
);
let midtone = super::tonal_shadow_color(Color::srgb(0.6, 0.3, 0.1, 1.0));
let [r, g, b, a] = midtone.to_srgb();
for (got, want, name) in [
(a, 0.506_265, "alpha"),
(r, 0.345_193, "red"),
(g, 0.172_596, "green"),
(b, 0.057_532, "blue"),
] {
assert!(
(got - want).abs() < 1e-4,
"the tonal {name} for a mid-tone shadow is {got}, not {want} -- \
which is what running the remap on light rather than on encoded \
values would give"
);
}
}
#[test]
fn clearing_sets_the_background_rather_than_recording_a_draw() {
let mut canvas = canvas();
canvas.clear(Color::rgba8(20, 30, 40, 255));
let recording = canvas.finish();
assert!(recording.is_empty());
assert!(recording.root().descriptor.clear.is_some());
}
}