convert.rs
raw
//! Conversions from guiduck scene vocabulary (kurbo/peniko) to tiny-skia
//! types. All conversions stay inside this backend crate.
use guiduck_scene::RenderError;
use guiduck_scene::Shape;
use guiduck_scene::geom::{Affine, Cap, Join, PathEl, Stroke};
use guiduck_scene::paint::color::{DynamicColor, Srgb};
use guiduck_scene::paint::{
BlendMode, Brush, Color, Compose, Extend, Fill, Gradient, GradientKind, ImageAlphaType,
ImageData, ImageFormat, ImageQuality, Mix,
};
pub fn transform(affine: Affine) -> tiny_skia::Transform {
let [a, b, c, d, e, f] = affine.as_coeffs();
tiny_skia::Transform::from_row(a as f32, b as f32, c as f32, d as f32, e as f32, f as f32)
}
pub fn color(color: Color) -> tiny_skia::Color {
let [r, g, b, a] = color.components;
tiny_skia::Color::from_rgba(
r.clamp(0.0, 1.0),
g.clamp(0.0, 1.0),
b.clamp(0.0, 1.0),
a.clamp(0.0, 1.0),
)
.expect("components clamped to [0, 1]")
}
fn dynamic_color(c: DynamicColor) -> tiny_skia::Color {
color(c.to_alpha_color::<Srgb>())
}
pub fn fill_rule(rule: Fill) -> tiny_skia::FillRule {
match rule {
Fill::NonZero => tiny_skia::FillRule::Winding,
Fill::EvenOdd => tiny_skia::FillRule::EvenOdd,
}
}
pub fn stroke(style: &Stroke) -> tiny_skia::Stroke {
tiny_skia::Stroke {
width: style.width as f32,
miter_limit: style.miter_limit as f32,
// tiny-skia has a single cap for both ends; kurbo distinguishes them.
// Widgets use symmetric caps in practice, so the start cap wins.
line_cap: match style.start_cap {
Cap::Butt => tiny_skia::LineCap::Butt,
Cap::Square => tiny_skia::LineCap::Square,
Cap::Round => tiny_skia::LineCap::Round,
},
line_join: match style.join {
Join::Bevel => tiny_skia::LineJoin::Bevel,
Join::Miter => tiny_skia::LineJoin::Miter,
Join::Round => tiny_skia::LineJoin::Round,
},
dash: if style.dash_pattern.is_empty() {
None
} else {
tiny_skia::StrokeDash::new(
style.dash_pattern.iter().map(|d| *d as f32).collect(),
style.dash_offset as f32,
)
},
}
}
/// Convert a shape to a tiny-skia path. Returns `None` for degenerate shapes
/// (empty paths, zero-area rects), which are legal to emit and simply draw
/// nothing.
pub fn path(shape: &Shape) -> Option<tiny_skia::Path> {
let mut builder = tiny_skia::PathBuilder::new();
push_path_elements(&mut builder, shape.to_path(0.1).iter());
builder.finish()
}
pub fn push_path_elements(
builder: &mut tiny_skia::PathBuilder,
elements: impl Iterator<Item = PathEl>,
) {
for el in elements {
match el {
PathEl::MoveTo(p) => builder.move_to(p.x as f32, p.y as f32),
PathEl::LineTo(p) => builder.line_to(p.x as f32, p.y as f32),
PathEl::QuadTo(c, p) => {
builder.quad_to(c.x as f32, c.y as f32, p.x as f32, p.y as f32);
}
PathEl::CurveTo(c0, c1, p) => builder.cubic_to(
c0.x as f32,
c0.y as f32,
c1.x as f32,
c1.y as f32,
p.x as f32,
p.y as f32,
),
PathEl::ClosePath => builder.close(),
}
}
}
pub fn spread_mode(extend: Extend) -> tiny_skia::SpreadMode {
match extend {
Extend::Pad => tiny_skia::SpreadMode::Pad,
Extend::Repeat => tiny_skia::SpreadMode::Repeat,
Extend::Reflect => tiny_skia::SpreadMode::Reflect,
}
}
pub fn filter_quality(quality: ImageQuality) -> tiny_skia::FilterQuality {
match quality {
ImageQuality::Low => tiny_skia::FilterQuality::Nearest,
ImageQuality::Medium => tiny_skia::FilterQuality::Bilinear,
ImageQuality::High => tiny_skia::FilterQuality::Bicubic,
}
}
fn gradient_stops(gradient: &Gradient) -> Vec<tiny_skia::GradientStop> {
gradient
.stops
.iter()
.map(|stop| tiny_skia::GradientStop::new(stop.offset, dynamic_color(stop.color)))
.collect()
}
/// Convert a brush to a tiny-skia shader.
///
/// `shader_transform` maps the brush's coordinate space (the same space the
/// geometry is authored in) to the target's pixel space, since tiny-skia
/// shaders carry their own transform rather than inheriting the painter's.
pub fn shader(
brush: &Brush,
shader_transform: Affine,
) -> Result<tiny_skia::Shader<'_>, RenderError> {
let ts = transform(shader_transform);
match brush {
Brush::Solid(c) => Ok(tiny_skia::Shader::SolidColor(color(*c))),
Brush::Gradient(gradient) => {
let stops = gradient_stops(gradient);
let mode = spread_mode(gradient.extend);
let shader = match gradient.kind {
GradientKind::Linear(linear) => tiny_skia::LinearGradient::new(
point(linear.start),
point(linear.end),
stops,
mode,
ts,
),
GradientKind::Radial(radial) => tiny_skia::RadialGradient::new(
point(radial.start_center),
radial.start_radius,
point(radial.end_center),
radial.end_radius,
stops,
mode,
ts,
),
GradientKind::Sweep(sweep) => tiny_skia::SweepGradient::new(
point(sweep.center),
sweep.start_angle.to_degrees(),
sweep.end_angle.to_degrees(),
stops,
mode,
ts,
),
};
shader.ok_or_else(|| RenderError::Backend("degenerate gradient".into()))
}
Brush::Image(image) => Err(RenderError::Backend(
format!(
"image brushes are drawn via DisplayItem::Image, not as fill brushes ({}x{})",
image.image.width, image.image.height
)
.into(),
)),
}
}
fn point(p: guiduck_scene::geom::Point) -> tiny_skia::Point {
tiny_skia::Point {
x: p.x as f32,
y: p.y as f32,
}
}
/// Convert image pixel data to a premultiplied RGBA pixmap.
pub fn image_to_pixmap(image: &ImageData) -> Result<tiny_skia::Pixmap, RenderError> {
let expected = image
.format
.size_in_bytes(image.width, image.height)
.ok_or_else(|| RenderError::Backend("image dimensions overflow".into()))?;
let data = image.data.as_ref();
if data.len() < expected {
return Err(RenderError::Backend(
format!(
"image data too short: {} bytes for {}x{} {:?}",
data.len(),
image.width,
image.height,
image.format
)
.into(),
));
}
let mut pixmap = tiny_skia::Pixmap::new(image.width, image.height)
.ok_or_else(|| RenderError::Backend("zero-sized image".into()))?;
let out = pixmap.data_mut();
for (src, dst) in data[..expected]
.chunks_exact(4)
.zip(out.chunks_exact_mut(4))
{
let [r, g, b, a] = match image.format {
ImageFormat::Rgba8 => [src[0], src[1], src[2], src[3]],
ImageFormat::Bgra8 => [src[2], src[1], src[0], src[3]],
other => {
return Err(RenderError::Backend(
format!("unsupported image format {other:?}").into(),
));
}
};
let [r, g, b] = match image.alpha_type {
ImageAlphaType::AlphaPremultiplied => [r, g, b],
ImageAlphaType::Alpha => [premultiply(r, a), premultiply(g, a), premultiply(b, a)],
};
dst.copy_from_slice(&[r, g, b, a]);
}
Ok(pixmap)
}
fn premultiply(channel: u8, alpha: u8) -> u8 {
((channel as u16 * alpha as u16 + 127) / 255) as u8
}
/// Map a peniko blend mode to tiny-skia's single blend enum.
///
/// peniko separates mix (color blending) and compose (Porter-Duff); tiny-skia
/// exposes one combined enum. A non-normal mix takes precedence; otherwise the
/// compose op is mapped.
pub fn blend_mode(blend: BlendMode) -> tiny_skia::BlendMode {
use tiny_skia::BlendMode as Ts;
match blend.mix {
Mix::Normal => match blend.compose {
Compose::Clear => Ts::Clear,
Compose::Copy => Ts::Source,
Compose::Dest => Ts::Destination,
Compose::SrcOver => Ts::SourceOver,
Compose::DestOver => Ts::DestinationOver,
Compose::SrcIn => Ts::SourceIn,
Compose::DestIn => Ts::DestinationIn,
Compose::SrcOut => Ts::SourceOut,
Compose::DestOut => Ts::DestinationOut,
Compose::SrcAtop => Ts::SourceAtop,
Compose::DestAtop => Ts::DestinationAtop,
Compose::Xor => Ts::Xor,
Compose::Plus => Ts::Plus,
Compose::PlusLighter => Ts::Plus,
},
Mix::Multiply => Ts::Multiply,
Mix::Screen => Ts::Screen,
Mix::Overlay => Ts::Overlay,
Mix::Darken => Ts::Darken,
Mix::Lighten => Ts::Lighten,
Mix::ColorDodge => Ts::ColorDodge,
Mix::ColorBurn => Ts::ColorBurn,
Mix::HardLight => Ts::HardLight,
Mix::SoftLight => Ts::SoftLight,
Mix::Difference => Ts::Difference,
Mix::Exclusion => Ts::Exclusion,
Mix::Hue => Ts::Hue,
Mix::Saturation => Ts::Saturation,
Mix::Color => Ts::Color,
Mix::Luminosity => Ts::Luminosity,
}
}