impeller_core/paint.rs
1//! How a shape is drawn.
2
3use crate::canvas::{Morphology, Rect};
4use crate::color::Color;
5use glam::Vec2;
6use impeller_geometry::dash::Dash;
7use impeller_geometry::stroke::StrokeStyle;
8use impeller_geometry::transform::{transformed_bounds, unbounded, Transform2D};
9use impeller_hal::{BlendMode, Extent2D, TileMode};
10use impeller_hal::{ColorFilter, Sampling};
11
12/// A color stop in a gradient.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct GradientStop {
15 pub color: Color,
16 /// Position along the gradient, from zero to one.
17 pub offset: f32,
18}
19
20impl GradientStop {
21 pub fn new(color: Color, offset: f32) -> Self {
22 Self { color, offset }
23 }
24}
25
26/// What fills a shape.
27#[derive(Debug, Clone, PartialEq)]
28pub enum Shader {
29 Solid(Color),
30 /// A gradient along the line between two points **in user space**.
31 ///
32 /// The endpoints travel through the canvas transform with the geometry, so
33 /// a gradient rotates and scales with the shape it fills rather than
34 /// staying pinned to the screen.
35 LinearGradient {
36 start: Vec2,
37 end: Vec2,
38 stops: Vec<GradientStop>,
39 /// What fills the shape beyond the two endpoints.
40 ///
41 /// A gradient is defined by two points and a shape is rarely exactly
42 /// that long, so this is not an edge case: clamping holds the end
43 /// colors, repeating tiles the ramp, and decal leaves the outside
44 /// empty. Set with [`Paint::with_tile_mode`], the same call an image
45 /// uses.
46 tile: TileMode,
47 },
48 /// A gradient outward from a center **in user space**, reaching its last
49 /// stop at `radius`.
50 RadialGradient {
51 center: Vec2,
52 radius: f32,
53 stops: Vec<GradientStop>,
54 /// What fills the shape beyond `radius`. See [`Shader::LinearGradient`].
55 tile: TileMode,
56 },
57 /// A texture, mapped onto a rectangle **in user space**.
58 ///
59 /// The rectangle travels through the canvas transform with the geometry, so
60 /// an image rotates and scales with the shape it fills. It is the region
61 /// the image covers, not the region drawn: filling a circle with this paint
62 /// draws a circular piece of the image.
63 ///
64 /// `slot` indexes the table supplied when the recording is drawn. The
65 /// canvas names a slot rather than holding a texture because it records
66 /// without touching a device, and a backend texture is not something it can
67 /// name. Assigning slots is the caller's business for now; a registry that
68 /// did it for them is a separate piece of design.
69 Image {
70 slot: u32,
71 rect: Rect,
72 /// Scales the sampled color, for drawing an image translucently.
73 alpha: f32,
74 tile: TileMode,
75 /// Which part of the image to draw, from zero to one in each axis.
76 ///
77 /// The whole image by default. Set it with [`Paint::with_source`], or
78 /// with [`Paint::with_source_pixels`] if you would rather state it in
79 /// texels and hand over the size you uploaded.
80 source: Rect,
81 /// Multiplies the sampled color. White changes nothing.
82 tint: Color,
83 /// How to read between texels. See [`Paint::with_sampling`].
84 sampling: Sampling,
85 },
86 /// A gradient around a center **in user space**, running between two angles
87 /// in radians, measured counter-clockwise from the positive X axis.
88 SweepGradient {
89 center: Vec2,
90 start_angle: f32,
91 end_angle: f32,
92 stops: Vec<GradientStop>,
93 /// What fills the directions the arc does not cover.
94 ///
95 /// A sweep of a full turn covers every direction and this changes
96 /// nothing; it is a partial sweep that has an outside.
97 tile: TileMode,
98 },
99 /// A caller's own fragment program.
100 ///
101 /// The one shader here this renderer did not write. `program` names one
102 /// registered with a context, and `uniforms` are the floats it reads --
103 /// laid out as the program's own declaration expects, which is a contract
104 /// between the caller's shader and the caller's code and not something
105 /// this renderer can check.
106 ///
107 /// At most [`RUNTIME_FLOATS`] of them; the rest are dropped rather than
108 /// overflowing into a block that has no room for them.
109 ///
110 /// [`RUNTIME_FLOATS`]: impeller_hal::RUNTIME_FLOATS
111 RuntimeEffect {
112 program: u32,
113 uniforms: Vec<f32>,
114 /// A texture slot the program may sample, if it declares one.
115 ///
116 /// The same table an image paint names, so a caller supplies them in
117 /// the same array beside the recording, in the order the program
118 /// declares its bindings.
119 ///
120 /// A binding the program declares and this leaves empty reads a
121 /// one-pixel opaque white texture -- what a solid fill binds, and what
122 /// keeps a pipeline's declared binding satisfied. Beyond
123 /// [`MAX_EFFECT_TEXTURES`] a caller is asking for a second descriptor
124 /// set layout, which would be one layout per program; the ceiling is
125 /// what buys a single shared one.
126 ///
127 /// [`MAX_EFFECT_TEXTURES`]: impeller_hal::MAX_EFFECT_TEXTURES
128 images: Vec<u32>,
129 },
130 /// A gradient between two circles **in user space**, reaching its first
131 /// stop on the first circle and its last on the second.
132 ///
133 /// This is the general form: a radial gradient is the case where the first
134 /// circle is a point at the second's center, and the two-point form is what
135 /// `dart:ui` reaches by giving `Gradient.radial` a focal point. It is kept
136 /// separate from [`Shader::RadialGradient`] rather than replacing it,
137 /// because the radial case folds its radius into a matrix and solves no
138 /// quadratic at all, and paying for the general solve on every ordinary
139 /// gradient would be a poor trade on the hardware this targets.
140 ///
141 /// Where no circle in the family passes through a point, nothing is drawn
142 /// there -- which is most of the plane when one circle is far outside the
143 /// other. That is a property of the two circles rather than of the tile
144 /// mode, which acts only where the parameter exists.
145 ConicalGradient {
146 start_center: Vec2,
147 start_radius: f32,
148 end_center: Vec2,
149 end_radius: f32,
150 stops: Vec<GradientStop>,
151 /// What fills the parameter outside the two circles. See
152 /// [`Shader::LinearGradient`].
153 tile: TileMode,
154 },
155}
156
157impl Shader {
158 /// How many color stops this paint carries, or zero where it carries none.
159 ///
160 /// Public because the threshold is: at or below [`MAX_STOPS`] a gradient
161 /// costs nothing beyond its material, and above it the recorder bakes a
162 /// small texture. Neither is visible in the result, so this exists for a
163 /// caller who cares about the cost rather than the picture.
164 ///
165 /// [`MAX_STOPS`]: impeller_hal::MAX_STOPS
166 pub fn stop_count(&self) -> usize {
167 match self {
168 Self::Solid(_) | Self::Image { .. } | Self::RuntimeEffect { .. } => 0,
169 Self::LinearGradient { stops, .. }
170 | Self::RadialGradient { stops, .. }
171 | Self::SweepGradient { stops, .. }
172 | Self::ConicalGradient { stops, .. } => stops.len(),
173 }
174 }
175
176 /// Whether this would draw anything at all.
177 pub fn is_visible(&self) -> bool {
178 match self {
179 Self::Solid(color) => !color.is_invisible(),
180 // What a caller's program draws is not knowable from here.
181 Self::RuntimeEffect { .. } => true,
182 Self::LinearGradient { stops, .. }
183 | Self::RadialGradient { stops, .. }
184 | Self::SweepGradient { stops, .. }
185 | Self::ConicalGradient { stops, .. } => stops.iter().any(|s| !s.color.is_invisible()),
186 // What the texture holds is unknown here, so only a zero alpha or
187 // an empty destination makes an image provably invisible.
188 Self::Image { alpha, rect, .. } => *alpha > 0.0 && !rect.is_empty(),
189 }
190 }
191}
192
193/// Which part of a blurred shape survives.
194///
195/// A mask blur produces two things a picture can be made from: the shape's own
196/// coverage, and that coverage blurred. Every style here is one combination of
197/// the two, and naming them is what `dart:ui` and Skia both do -- a shadow
198/// wants the blur outside and nothing inside, a glow wants it inside, and a
199/// soft-edged solid wants the shape intact with the blur around it.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
201pub enum MaskBlurStyle {
202 /// The blurred coverage, and nothing else. Soft on both sides of the edge.
203 #[default]
204 Normal,
205 /// The shape at full strength, with the blur outside it.
206 Solid,
207 /// The blur outside the shape only. What a drop shadow behind an opaque
208 /// object is: the part that would be hidden is not drawn at all.
209 Outer,
210 /// The blur inside the shape only, which is an inner shadow or a glow
211 /// confined to what it lights.
212 Inner,
213}
214
215/// A function applied to what a paint drew, rather than to the color it
216/// computed.
217///
218/// The distinction from a color filter is what the input is. A color filter
219/// sees one color at a time and cannot know its neighbors; this sees the
220/// picture, which is what a blur needs.
221///
222/// The distinction from [`Paint::mask_blur`] is what is blurred. A mask blur
223/// blurs coverage and then fills, which is only the same picture as blurring
224/// the result when the fill does not vary -- so it takes a solid color and
225/// refuses anything else. This blurs the result, which is defined for every
226/// paint and is what `dart:ui` means by an image filter. For a solid color the
227/// two agree, and there is a test that says so.
228#[derive(Debug, Clone, PartialEq, Default)]
229pub enum ImageFilter {
230 #[default]
231 None,
232 /// Blur what was drawn, by a standard deviation per axis in device pixels.
233 ///
234 /// Two, because `dart:ui`'s `ImageFilter.blur` takes `sigmaX` and
235 /// `sigmaY` separately and upstream's `MakeBlur` passes both through. A
236 /// blur is two separable passes here already, one along each axis, so the
237 /// second deviation costs nothing but the field: each pass uses its own,
238 /// and a pass whose deviation is zero is skipped rather than run as an
239 /// identity that would resample for no reason.
240 ///
241 /// [`Self::blur`] is the isotropic case, which is nearly every use.
242 Blur { sigma_x: f32, sigma_y: f32 },
243 /// Move what was drawn, in device pixels, by resampling it.
244 ///
245 /// Not the same as drawing under the transform, which is what the canvas's
246 /// own stack does. This draws at the size it was written and then moves
247 /// the finished image, so magnifying gives enlarged pixels where the
248 /// transform stack would give the shape redrawn larger. That difference is
249 /// the reason `dart:ui` has both, and a caller wanting the sharp one
250 /// already has `concat`.
251 Matrix { transform: Transform2D },
252 /// Spread what was drawn, taking the largest sample within these radii in
253 /// device pixels.
254 ///
255 /// `ImageFilter.dilate`. On premultiplied color and channel by channel, so
256 /// a translucent shape spreads its coverage along with its color.
257 Dilate { radius_x: f32, radius_y: f32 },
258 /// Shrink what was drawn, taking the smallest sample within these radii in
259 /// device pixels.
260 ///
261 /// `ImageFilter.erode`, and the dual of [`Self::Dilate`]: what one does to
262 /// a shape the other does to the space around it.
263 Erode { radius_x: f32, radius_y: f32 },
264 /// Recolor what was drawn, rather than what the paint computed.
265 ///
266 /// `dart:ui`'s `ColorFilter` is itself an `ImageFilter`, and upstream's
267 /// `DlImageFilter::MakeColorFilter` is the same thing. A paint carries a
268 /// color filter of its own, so the obvious question is what this adds, and
269 /// the answer is narrower than it first looks: **it can be half of a
270 /// [`Self::Compose`], and `Paint::color_filter` cannot.** Recoloring a
271 /// blurred result, or blurring a recolored one, has no other spelling here.
272 ///
273 /// On a single draw with a solid fill it is the same picture as the paint's
274 /// own filter, and a test pins that rather than leaving it to be discovered.
275 /// A paint's image filter applies per draw -- each draw gets a layer of its
276 /// own -- so there is no arrangement of two draws under one paint for the
277 /// two to disagree about. Where they would disagree is inside a
278 /// composition, which is the case this exists for.
279 Color(ColorFilter),
280 /// Run a caller's fragment program over what was drawn.
281 ///
282 /// `dart:ui`'s `ImageFilter.shader`, and upstream's
283 /// `DlImageFilter::MakeRuntimeEffect`. The program reads its input as a
284 /// texture with an identity transform -- the same guarantee upstream
285 /// re-rasterizes to provide, and one this renderer has by construction: a
286 /// filter is a pass whose target is the size of its source, covered by a
287 /// quad, so a fragment's clip position *is* its texture coordinate. A
288 /// program written for this samples exactly as the fixture programs do.
289 ///
290 /// The uniforms are the whole material block, because a program replaces
291 /// the shader that would have read it. `RUNTIME_FLOATS` is the bound and
292 /// anything past it is dropped, as it is for a runtime effect used as a
293 /// paint.
294 Runtime { program: u32, uniforms: Vec<f32> },
295 /// Filter with `inner`, then filter that result with `outer`.
296 ///
297 /// `ImageFilter.compose`, and the reason this type is not `Copy`: a filter
298 /// that holds filters cannot be a fixed size without a bound on the depth,
299 /// and `dart:ui` puts no bound on it.
300 ///
301 /// A caller can already nest [`Canvas::save_layer`] calls to the same
302 /// effect, and did before this existed. What this adds is that the bounds
303 /// come out right without being worked out by hand: the outer layer has to
304 /// be opened wide enough to hold whatever the inner one grew to, which
305 /// means knowing how far each filter in the chain reaches.
306 ///
307 /// [`Canvas::save_layer`]: crate::Canvas::save_layer
308 Compose {
309 outer: Box<ImageFilter>,
310 inner: Box<ImageFilter>,
311 },
312}
313
314/// How far a blur of the given deviations reaches, as an outset per target axis.
315///
316/// A function rather than a pair of numbers because the answer depends on which
317/// directions the blur will run along, and those come from the transform in
318/// force where the layer opened -- which `paint.rs` has no view of. See
319/// `BlurBasis` in `canvas.rs`.
320pub(crate) type BlurReach<'a> = &'a dyn Fn(Vec2) -> Vec2;
321
322impl ImageFilter {
323 /// A blur of the same deviation along both axes, which is nearly every use.
324 pub fn blur(sigma: f32) -> Self {
325 Self::Blur {
326 sigma_x: sigma,
327 sigma_y: sigma,
328 }
329 }
330
331 /// A blur with a deviation per axis, as `dart:ui`'s `ImageFilter.blur`
332 /// states one.
333 pub fn blur_xy(sigma_x: f32, sigma_y: f32) -> Self {
334 Self::Blur { sigma_x, sigma_y }
335 }
336
337 /// Whether a deviation describes a blur at all.
338 ///
339 /// Stated positively rather than as a negated comparison, which reads
340 /// badly on a type where two values can be incomparable: a sigma that is
341 /// not finite is not a blur, and neither is one that is zero or less.
342 pub(crate) fn blurs(sigma: f32) -> bool {
343 sigma.is_finite() && sigma > 0.0
344 }
345
346 /// Whether this would change anything.
347 pub fn is_identity(&self) -> bool {
348 match self {
349 Self::None => true,
350 // Stated positively rather than as a negated comparison, which
351 // reads badly on a type where two values can be incomparable: a
352 // sigma that is not finite is not a blur, and neither is one that
353 // is zero or less.
354 Self::Blur { sigma_x, sigma_y } => !Self::blurs(*sigma_x) && !Self::blurs(*sigma_y),
355 // A matrix that changes nothing is one that costs a layer for
356 // nothing, so it is worth recognizing.
357 Self::Matrix { transform } => {
358 !transform.is_finite() || *transform == Transform2D::IDENTITY
359 }
360 // Rounded before the comparison, on the same reasoning that rounds
361 // it before it is applied: a radius of a third of a pixel names no
362 // sample the filter could take, so it is not a filter.
363 Self::Dilate { radius_x, radius_y } | Self::Erode { radius_x, radius_y } => {
364 Morphology::dilate(*radius_x, *radius_y).is_identity()
365 }
366 // Composing two filters that each change nothing changes nothing,
367 // and costs two layers to say so.
368 // A color filter that recolors nothing is one, which keeps a
369 // caller who builds one from a default out of a layer they did not
370 // ask to pay for. Asked of the filter rather than compared against
371 // `ColorFilter::None`, which is what this used to do: a blend in
372 // `Dst` mode returns the destination untouched and an identity
373 // matrix recolors nothing, and both paid for a pass and a resample
374 // to arrive at what they were handed. `ColorFilter::is_identity`
375 // knows all three and is the same question.
376 Self::Color(filter) => filter.is_identity(),
377 // Never. What a caller's program does is the caller's business and
378 // nothing here can read it, so the only safe answer is that it does
379 // something -- and answering otherwise would drop the pass that
380 // runs it.
381 Self::Runtime { .. } => false,
382 Self::Compose { outer, inner } => outer.is_identity() && inner.is_identity(),
383 }
384 }
385
386 /// Filter with `inner` first and then with `outer`.
387 ///
388 /// The order `dart:ui` states, which is the order the names suggest once
389 /// you picture them nested: the inner filter is the one closer to what was
390 /// drawn. A composition where either half does nothing is the other half,
391 /// since the layer it would have taken is a copy of an image.
392 pub fn compose(outer: ImageFilter, inner: ImageFilter) -> Self {
393 if inner.is_identity() {
394 return outer;
395 }
396 if outer.is_identity() {
397 return inner;
398 }
399 Self::Compose {
400 outer: Box::new(outer),
401 inner: Box::new(inner),
402 }
403 }
404
405 /// The outermost filter that is not itself a composition, and everything
406 /// that has to run before it.
407 ///
408 /// A chain is applied innermost first, so drawing one means peeling from
409 /// the other end: open a layer for the outermost, and hand the remainder
410 /// back to be drawn inside it. The remainder is a filter in its own right,
411 /// which is what lets the caller recurse without knowing how deep it is.
412 ///
413 /// Recursive on the *outer* half, and that is the whole point. A first
414 /// version took `outer` to be a leaf and returned it directly, which is
415 /// true for anything `compose` builds left to right and false the moment a
416 /// caller composes two compositions -- `compose(compose(a, b), c)` was
417 /// refused as an unimplemented filter, having been assembled entirely out
418 /// of implemented ones.
419 pub(crate) fn peel(&self) -> (ImageFilter, ImageFilter) {
420 match self {
421 Self::Compose { outer, inner } => {
422 let (outermost, rest) = outer.peel();
423 (outermost, Self::compose(rest, (**inner).clone()))
424 }
425 leaf => (leaf.clone(), Self::None),
426 }
427 }
428
429 /// The region of device pixels a target must cover for this filter to run
430 /// over content occupying `min`..`max`.
431 ///
432 /// Both what goes in and what comes out, which is not the same question for
433 /// every filter and is the one that matters. A blur or a dilation only
434 /// grows, so its output contains its input and the distinction is invisible
435 /// -- the case that makes it visible is a matrix, which *moves* the image.
436 /// A composition puts the inner filter's layer inside the outer one's
437 /// target, and a layer's target is clipped to its parent's, so a target
438 /// sized only for where a matrix filter put things would crop the content
439 /// before the matrix ever ran: the inner layer draws the shape where it was
440 /// written and the composite is what moves it.
441 ///
442 /// Erosion is left alone rather than shrunk. Covering more than the result
443 /// needs costs a little memory; covering less loses drawing.
444 pub(crate) fn covering(&self, min: Vec2, max: Vec2, blur: BlurReach) -> (Vec2, Vec2) {
445 match self {
446 // A color filter changes what a pixel is and never which pixels
447 // there are, so it needs exactly the region it was given. A runtime
448 // effect is the same on the side that matters here: it may read
449 // anywhere in its input, and it writes only where the pass covers.
450 Self::None | Self::Erode { .. } | Self::Color(_) | Self::Runtime { .. } => (min, max),
451 Self::Blur { sigma_x, sigma_y } => {
452 // Per axis, which is the whole reason a filter states two: a
453 // blur along one axis alone must not widen the other, or the
454 // target holding it is bigger than the picture in it.
455 //
456 // Which axes, though, is the caller's to say. A blur runs along
457 // the directions the caller's own axes point once the transform
458 // has been applied, so the outset is the bounding box of the two
459 // reaches laid along those -- and asking here with the target's
460 // axes when the blur will run along turned ones sizes a target
461 // for one blur and fills it with another. That is not a
462 // conservative error: it is short exactly where the blur went.
463 let reach = blur(Vec2::new(*sigma_x, *sigma_y));
464 (min - reach, max + reach)
465 }
466 Self::Dilate { radius_x, radius_y } => {
467 let radius = Morphology::dilate(*radius_x, *radius_y).radius;
468 let reach = Vec2::new(radius[0], radius[1]);
469 (min - reach, max + reach)
470 }
471 Self::Matrix { transform } => {
472 // A matrix that carries the content across the vanishing line
473 // moves it further than a box can state, and this is a
474 // statement about how far a filter reaches -- so it widens to
475 // everything rather than reporting a reach that is too short.
476 let (moved_min, moved_max) =
477 transformed_bounds(*transform, min, max).unwrap_or_else(unbounded);
478 (min.min(moved_min), max.max(moved_max))
479 }
480 Self::Compose { outer, inner } => {
481 let (min, max) = inner.covering(min, max, blur);
482 outer.covering(min, max, blur)
483 }
484 }
485 }
486}
487
488/// Fill the shape, or trace its outline.
489#[derive(Debug, Clone, Copy, PartialEq, Default)]
490pub enum Style {
491 #[default]
492 Fill,
493 /// Stroke with the given width and joinery.
494 ///
495 /// The width is in the coordinate space the shape is drawn in, so it scales
496 /// with the canvas transform. A caller wanting a hairline that stays one
497 /// pixel wide under zoom divides by the current scale.
498 Stroke(StrokeStyle),
499}
500
501/// Everything about how a shape is painted.
502#[derive(Debug, Clone, PartialEq)]
503pub struct Paint {
504 pub shader: Shader,
505 pub style: Style,
506 /// Which part of the blurred shape survives. Ignored without a mask blur.
507 pub mask_blur_style: MaskBlurStyle,
508 /// A function applied to what this paint drew, after the shader and any
509 /// color filter.
510 ///
511 /// Costs a layer: what is drawn goes into a target of its own, is
512 /// filtered, and is composited back. See [`ImageFilter`] for how this
513 /// differs from a color filter and from a mask blur.
514 pub image_filter: ImageFilter,
515 /// A function applied to the color the shader produces, before blending.
516 ///
517 /// Beside the shader rather than inside it because it applies to all of
518 /// them, and after it rather than before because that is where `dart:ui`
519 /// puts a color filter and where it is useful: what is being recolored is
520 /// the result of the fill, not the inputs it was built from.
521 pub color_filter: ColorFilter,
522 /// Cut a stroke into a dash pattern before drawing it.
523 ///
524 /// On the paint rather than inside [`Style::Stroke`] for two reasons. It
525 /// keeps the geometry crate's stroke description a small copyable value,
526 /// which the tessellator wants and a pattern of arbitrary length would
527 /// end. And it says what it is: a dashed stroke is a stroke of a different
528 /// path, so this describes what happens to the path on the way in rather
529 /// than how the outline is built.
530 ///
531 /// Ignored by a fill, which has no length to measure along.
532 pub dash: Option<Dash>,
533 /// Blur the shape's coverage before filling it, in device pixels.
534 ///
535 /// What a soft shadow is made of: the same shape, softened, drawn behind
536 /// the thing casting it. Zero for none.
537 ///
538 /// `dart:ui` states this as blurring the mask and then applying the paint
539 /// through it, and that is what a shape gets. Two orders exist and they
540 /// agree only where the fill does not vary, because a blur is linear:
541 /// `blur(C·α)` is `C·blur(α)` for a constant `C` and for nothing else. A
542 /// solid color therefore takes the cheaper route and is drawn through a
543 /// blurred layer, which is that identity used; anything that varies is
544 /// drawn the way the specification says, with the fill laid across
545 /// everything the blur reaches and blurred coverage composited onto it.
546 ///
547 /// Two things still take a solid color only, and neither is about blurring.
548 /// A glyph run tints one color by its own nature, so a gradient over one is
549 /// refused here exactly as it is when nothing is blurred. And a mesh
550 /// carries a color per vertex, which is a second thing multiplying the
551 /// coverage rather than a fill to lay down.
552 ///
553 /// The three styles that combine a blurred mask with a sharp one need the
554 /// coverage twice and are not built for a varying fill; they take a solid
555 /// color, which is every use a shadow has.
556 pub mask_blur: f32,
557 pub blend: BlendMode,
558 /// How a color carried per vertex or per sprite combines with what this
559 /// paint produced.
560 ///
561 /// `dart:ui` passes this as an argument to `drawVertices` and `drawAtlas`
562 /// rather than on the paint. It sits here instead because those are the
563 /// only two calls that can carry such a color, and an argument every other
564 /// call would have to pass and ignore is worse than a field they leave
565 /// alone. [`BlendMode::Modulate`] multiplies, which is the identity for the
566 /// white a caller who supplied no color gets.
567 ///
568 /// Distinct from [`Self::blend`], which decides how the result reaches the
569 /// target. Both colors here are in the shader already, so every mode is
570 /// available to this one without an extension.
571 pub tint_blend: BlendMode,
572 /// Whether to antialias this shape's edges.
573 ///
574 /// Recorded per paint but applied per pass, because multisampling is a
575 /// property of the target rather than of a draw: a canvas that mixes the
576 /// two antialiases everything.
577 pub anti_alias: bool,
578}
579
580impl Default for Paint {
581 fn default() -> Self {
582 Self {
583 shader: Shader::Solid(Color::BLACK),
584 style: Style::Fill,
585 image_filter: ImageFilter::None,
586 mask_blur_style: MaskBlurStyle::default(),
587 color_filter: ColorFilter::None,
588 dash: None,
589 mask_blur: 0.0,
590 blend: BlendMode::SrcOver,
591 tint_blend: BlendMode::Modulate,
592 anti_alias: true,
593 }
594 }
595}
596
597impl Paint {
598 /// A solid fill.
599 pub fn fill(color: Color) -> Self {
600 Self {
601 shader: Shader::Solid(color),
602 ..Default::default()
603 }
604 }
605
606 /// A fill that runs between colors along a line in user space.
607 ///
608 /// Any number of them. Up to [`MAX_STOPS`] the colors travel with the
609 /// material; beyond that the recorder tabulates them into a texture the
610 /// shader samples, which costs one small upload per distinct gradient and
611 /// is otherwise invisible. Ask [`Shader::stop_count`] if you want to know
612 /// which will happen.
613 ///
614 /// [`MAX_STOPS`]: impeller_hal::MAX_STOPS
615 pub fn linear_gradient(start: Vec2, end: Vec2, stops: Vec<GradientStop>) -> Self {
616 Self {
617 shader: Shader::LinearGradient {
618 start,
619 end,
620 stops,
621 tile: TileMode::default(),
622 },
623 ..Default::default()
624 }
625 }
626
627 /// A fill that runs outward from a center in user space.
628 ///
629 /// Any number of stops; see [`Paint::linear_gradient`].
630 pub fn radial_gradient(center: Vec2, radius: f32, stops: Vec<GradientStop>) -> Self {
631 Self {
632 shader: Shader::RadialGradient {
633 center,
634 radius,
635 stops,
636 tile: TileMode::default(),
637 },
638 ..Default::default()
639 }
640 }
641
642 /// A fill that runs around a center in user space.
643 ///
644 /// Angles are in radians, counter-clockwise from the positive X axis. At
645 /// any number of stops; see [`Paint::linear_gradient`].
646 pub fn sweep_gradient(
647 center: Vec2,
648 start_angle: f32,
649 end_angle: f32,
650 stops: Vec<GradientStop>,
651 ) -> Self {
652 Self {
653 shader: Shader::SweepGradient {
654 center,
655 start_angle,
656 end_angle,
657 stops,
658 tile: TileMode::default(),
659 },
660 ..Default::default()
661 }
662 }
663
664 /// A fill computed by a program the caller supplied.
665 ///
666 /// See [`Shader::RuntimeEffect`]. The program has to be registered with
667 /// the context that will draw the recording, and the index it was given is
668 /// what goes here.
669 pub fn runtime_effect(program: u32, uniforms: Vec<f32>) -> Self {
670 Self {
671 shader: Shader::RuntimeEffect {
672 program,
673 uniforms,
674 images: Vec::new(),
675 },
676 ..Default::default()
677 }
678 }
679
680 /// Give a runtime effect a texture to sample.
681 ///
682 /// Ignored by every other kind of paint, which either samples nothing or
683 /// already names what it samples. Called more than once, the slots stack up
684 /// in the order the program declares its bindings.
685 pub fn with_effect_image(mut self, slot: u32) -> Self {
686 if let Shader::RuntimeEffect { images, .. } = &mut self.shader {
687 images.push(slot);
688 }
689 self
690 }
691
692 /// Give a runtime effect every texture it samples at once.
693 ///
694 /// The order is the order the program declares its bindings, which for a
695 /// program written in WGSL is the order of the binding numbers rather than
696 /// the order of the declarations.
697 pub fn with_effect_images(mut self, slots: &[u32]) -> Self {
698 if let Shader::RuntimeEffect { images, .. } = &mut self.shader {
699 images.clear();
700 images.extend_from_slice(slots);
701 }
702 self
703 }
704
705 /// A fill that runs between two circles in user space.
706 ///
707 /// The general gradient: the first stop lands on the first circle and the
708 /// last on the second, and every stop between them on a circle
709 /// interpolated between the two. `radial_gradient` is the case where the
710 /// first circle is a point at the second's center, and is worth reaching
711 /// for when that is what you mean -- it does less work per fragment.
712 ///
713 /// Any number of stops; see [`Paint::linear_gradient`].
714 pub fn conical_gradient(
715 start_center: Vec2,
716 start_radius: f32,
717 end_center: Vec2,
718 end_radius: f32,
719 stops: Vec<GradientStop>,
720 ) -> Self {
721 Self {
722 shader: Shader::ConicalGradient {
723 start_center,
724 start_radius,
725 end_center,
726 end_radius,
727 stops,
728 tile: TileMode::default(),
729 },
730 ..Default::default()
731 }
732 }
733
734 /// How to read a texture between its texels.
735 ///
736 /// Linear by default, which is what an image drawn at any size but its own
737 /// wants. [`Sampling::Nearest`] is for the cases where blending is the
738 /// wrong answer: pixel art, and a sprite drawn at exactly its own size
739 /// where certainty that no neighbor bled in matters more than smoothness.
740 ///
741 /// Ignored by a paint with no texture.
742 pub fn with_sampling(mut self, sampling: Sampling) -> Self {
743 if let Shader::Image { sampling: at, .. } = &mut self.shader {
744 *at = sampling;
745 }
746 self
747 }
748
749 /// Keep only part of the blurred shape. See [`MaskBlurStyle`].
750 ///
751 /// Ignored unless a mask blur is set, since there is nothing to take a
752 /// part of otherwise.
753 pub fn with_mask_blur_style(mut self, style: MaskBlurStyle) -> Self {
754 self.mask_blur_style = style;
755 self
756 }
757
758 /// Filter what this paint draws, rather than the color it computes.
759 ///
760 /// Unlike a mask blur this takes any shader, because blurring a result is
761 /// defined whatever produced it. It is the more expensive of the two: a
762 /// layer is allocated, drawn into and composited back, where a mask blur
763 /// of a solid color reaches the same picture the same way but is only
764 /// offered where that equivalence holds.
765 pub fn with_image_filter(mut self, filter: ImageFilter) -> Self {
766 self.image_filter = filter;
767 self
768 }
769
770 /// Apply a function to the color this paint produces, before it blends.
771 ///
772 /// Works on any shader, which is the point: the image tint that came
773 /// before it could only recolor an image. A gradient, a shape, a run of
774 /// glyphs all take one now.
775 pub fn with_color_filter(mut self, filter: ColorFilter) -> Self {
776 self.color_filter = filter;
777 self
778 }
779
780 /// Blur the shape's coverage, for a shadow or a glow.
781 ///
782 /// Ignored unless finite and positive. Applies to a fill or a stroke alike
783 /// -- what is blurred is whatever coverage the shape produces.
784 pub fn with_mask_blur(mut self, sigma: f32) -> Self {
785 self.mask_blur = if sigma.is_finite() && sigma > 0.0 {
786 sigma
787 } else {
788 0.0
789 };
790 self
791 }
792
793 /// Dash this paint's stroke, or clear an existing pattern with `None`.
794 ///
795 /// The lengths are in the space the shape is drawn in, so a pattern scales
796 /// with the canvas transform along with the line it cuts. A pattern that
797 /// cannot be walked -- empty, negative, or summing to zero -- draws the
798 /// line whole rather than drawing nothing, since an undashed line leads
799 /// back to the pattern and an absent one leads nowhere.
800 pub fn with_dash(mut self, dash: impl Into<Option<Dash>>) -> Self {
801 self.dash = dash.into();
802 self
803 }
804
805 /// A stroke of the given width, with default caps and joins.
806 /// A fill that samples an image across a rectangle in user space.
807 ///
808 /// `slot` indexes the table supplied when the recording is drawn, and
809 /// `rect` is the region the image covers rather than the region drawn: the
810 /// shape being filled decides what is painted, this decides where the image
811 /// sits under it.
812 pub fn image(slot: u32, rect: Rect) -> Self {
813 Self {
814 shader: Shader::Image {
815 slot,
816 rect,
817 alpha: 1.0,
818 tile: TileMode::default(),
819 source: Rect::new(0.0, 0.0, 1.0, 1.0),
820 tint: Color::WHITE,
821 sampling: Sampling::default(),
822 },
823 ..Default::default()
824 }
825 }
826
827 /// What happens outside the paint's own extent.
828 ///
829 /// Means the same thing for a gradient as for an image, which is why it is
830 /// one call rather than two: past the end of the ramp, past the radius, or
831 /// outside the swept arc, the color either holds, or repeats, or stops.
832 /// Ignored by a solid paint, which has no outside.
833 pub fn with_tile_mode(mut self, tile: TileMode) -> Self {
834 match &mut self.shader {
835 Shader::Image { tile: current, .. }
836 | Shader::LinearGradient { tile: current, .. }
837 | Shader::RadialGradient { tile: current, .. }
838 | Shader::SweepGradient { tile: current, .. }
839 | Shader::ConicalGradient { tile: current, .. } => *current = tile,
840 // Neither has an outside a tile mode could describe: a solid has
841 // no edge, and where a caller's program stops is the caller's
842 // arithmetic rather than this renderer's.
843 Shader::Solid(_) | Shader::RuntimeEffect { .. } => {}
844 }
845 self
846 }
847
848 /// Draw only part of the image, in coordinates from zero to one.
849 ///
850 /// What a sprite sheet needs, and a nine-patch border. Normalized rather
851 /// than in texels because a canvas records without touching a device and
852 /// has never seen the image: it holds a slot, not a texture, and cannot ask
853 /// how large it is. The caller who uploaded it can, which is what
854 /// [`Paint::with_source_pixels`] is for.
855 ///
856 /// Ignored by paints that are not images.
857 pub fn with_source(mut self, source: Rect) -> Self {
858 if let Shader::Image { source: at, .. } = &mut self.shader {
859 *at = source;
860 }
861 self
862 }
863
864 /// The same, stated in texels of an image of the given size.
865 ///
866 /// A degenerate size would divide by zero, so it leaves the paint alone --
867 /// drawing the whole image, which is what the paint already said.
868 pub fn with_source_pixels(self, source: Rect, size: Extent2D) -> Self {
869 if size.width == 0 || size.height == 0 {
870 return self;
871 }
872 let (w, h) = (size.width as f32, size.height as f32);
873 self.with_source(Rect::new(
874 source.left / w,
875 source.top / h,
876 source.right / w,
877 source.bottom / h,
878 ))
879 }
880
881 /// Multiply an image paint's sampled color. Ignored by other paints.
882 ///
883 /// What turns one monochrome icon sheet into every state a control has:
884 /// upload the shapes once as white on transparent, and tint per draw. A
885 /// white tint is the identity, which is what an image that never mentions
886 /// one already carries.
887 ///
888 /// Composes with everything else on the paint, so a tinted sprite from a
889 /// sheet is one call for the piece and one for the color.
890 pub fn with_tint(mut self, tint: Color) -> Self {
891 if let Shader::Image { tint: at, .. } = &mut self.shader {
892 *at = tint;
893 }
894 self
895 }
896
897 /// Scale an image paint's sampled color. Ignored by other paints.
898 pub fn with_image_alpha(mut self, alpha: f32) -> Self {
899 if let Shader::Image { alpha: current, .. } = &mut self.shader {
900 *current = alpha;
901 }
902 self
903 }
904
905 pub fn stroke(color: Color, width: f32) -> Self {
906 Self {
907 shader: Shader::Solid(color),
908 style: Style::Stroke(StrokeStyle::new(width)),
909 ..Default::default()
910 }
911 }
912
913 pub fn with_shader(mut self, shader: Shader) -> Self {
914 self.shader = shader;
915 self
916 }
917
918 pub fn with_style(mut self, style: Style) -> Self {
919 self.style = style;
920 self
921 }
922
923 pub fn with_blend(mut self, blend: BlendMode) -> Self {
924 self.blend = blend;
925 self
926 }
927
928 /// How a per-vertex or per-sprite color combines with this paint's result.
929 ///
930 /// Ignored by every call that carries no such color, there being nothing to
931 /// combine.
932 pub fn with_tint_blend(mut self, tint_blend: BlendMode) -> Self {
933 self.tint_blend = tint_blend;
934 self
935 }
936
937 /// Whether this paint's edges are smoothed. On by default.
938 ///
939 /// How that is paid for depends on the shape rather than on this flag. A
940 /// rectangle, rounded rectangle, oval or circle is drawn from a distance
941 /// field and antialiases itself inside its own fragment shader, costing a
942 /// pass nothing. Anything that reaches the tessellator instead -- an
943 /// arbitrary path, a stroke, a line, a double rounded rectangle -- has only
944 /// triangles to work with, so its pass becomes multisampled.
945 ///
946 /// That second case carries a requirement worth knowing before meeting it:
947 /// a multisampled pass must clear, so the pass it lands in needs something
948 /// to clear to. Drawn straight onto the frame, that means the canvas needs
949 /// a background -- see [`crate::Canvas::clear`], which is where a caller
950 /// refused for this reason should look. Drawn inside a layer it means
951 /// nothing to the caller, the layer's target clearing to transparent on
952 /// its own.
953 pub fn with_anti_alias(mut self, anti_alias: bool) -> Self {
954 self.anti_alias = anti_alias;
955 self
956 }
957
958 /// Whether drawing with this paint would change anything.
959 ///
960 /// A transparent fill and a zero-width stroke both draw nothing, and
961 /// skipping them early keeps empty geometry out of the batch rather than
962 /// tessellating it and discovering it was empty.
963 pub fn is_visible(&self) -> bool {
964 if !self.shader.is_visible() {
965 return false;
966 }
967 match &self.style {
968 Style::Fill => true,
969 // `can_draw` rather than `is_visible`, and the difference is a
970 // width of zero: a hairline, which the canvas widens to a device
971 // pixel before any geometry is built for it.
972 Style::Stroke(stroke) => stroke.can_draw(),
973 }
974 }
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980
981 #[test]
982 fn a_default_paint_is_an_opaque_antialiased_fill() {
983 let paint = Paint::default();
984 assert_eq!(paint.style, Style::Fill);
985 assert_eq!(paint.blend, BlendMode::SrcOver);
986 assert!(paint.anti_alias);
987 assert!(paint.is_visible());
988 }
989
990 #[test]
991 fn a_transparent_paint_draws_nothing_whatever_its_style() {
992 let clear = Color::WHITE.with_alpha(0.0);
993 assert!(!Paint::fill(clear).is_visible());
994 assert!(!Paint::stroke(clear, 4.0).is_visible());
995
996 // A gradient every stop of which is transparent draws nothing either.
997 let invisible = Paint::linear_gradient(
998 Vec2::ZERO,
999 Vec2::new(1.0, 0.0),
1000 vec![GradientStop::new(clear, 0.0), GradientStop::new(clear, 1.0)],
1001 );
1002 assert!(!invisible.is_visible());
1003 }
1004
1005 #[test]
1006 fn a_zero_width_stroke_draws_nothing() {
1007 // Animating a width to zero should stop drawing rather than emit
1008 // degenerate geometry for the tessellator to discard.
1009 // A width of zero is a hairline rather than nothing, so the paint is
1010 // visible; a negative one still is not a stroke.
1011 assert!(Paint::stroke(Color::BLACK, 0.0).is_visible());
1012 assert!(!Paint::stroke(Color::BLACK, -1.0).is_visible());
1013 assert!(Paint::stroke(Color::BLACK, 0.5).is_visible());
1014 }
1015}