use crate::layer::{
Affine2D, BillboardFacing, BlendMode, Brush, Camera, ClipShape, Color, CornerRadius,
Environment, LayerId, Light, Mat4, ParticleSystemData, Point, Rect, Sdf3DViewport, Shadow,
Size, Vec2,
};
#[derive(Clone, Debug)]
pub enum Transform {
Affine2D(Affine2D),
Mat4(Mat4),
}
impl Transform {
pub fn translate(x: f32, y: f32) -> Self {
Transform::Affine2D(Affine2D::translation(x, y))
}
pub fn scale(sx: f32, sy: f32) -> Self {
Transform::Affine2D(Affine2D::scale(sx, sy))
}
pub fn scale_centered(sx: f32, sy: f32, center_x: f32, center_y: f32) -> Self {
let tx = center_x * (1.0 - sx);
let ty = center_y * (1.0 - sy);
Transform::Affine2D(Affine2D {
elements: [sx, 0.0, 0.0, sy, tx, ty],
})
}
pub fn rotate(angle: f32) -> Self {
Transform::Affine2D(Affine2D::rotation(angle))
}
pub fn rotate_centered(angle: f32, center_x: f32, center_y: f32) -> Self {
let c = angle.cos();
let s = angle.sin();
let tx = center_x - center_x * c + center_y * s;
let ty = center_y - center_x * s - center_y * c;
Transform::Affine2D(Affine2D {
elements: [c, s, -s, c, tx, ty],
})
}
pub fn translate_3d(x: f32, y: f32, z: f32) -> Self {
Transform::Mat4(Mat4::translation(x, y, z))
}
pub fn scale_3d(x: f32, y: f32, z: f32) -> Self {
Transform::Mat4(Mat4::scale(x, y, z))
}
pub fn identity() -> Self {
Transform::Affine2D(Affine2D::IDENTITY)
}
pub fn is_2d(&self) -> bool {
matches!(self, Transform::Affine2D(_))
}
pub fn is_3d(&self) -> bool {
matches!(self, Transform::Mat4(_))
}
}
impl Default for Transform {
fn default() -> Self {
Transform::identity()
}
}
impl From<Affine2D> for Transform {
fn from(t: Affine2D) -> Self {
Transform::Affine2D(t)
}
}
impl From<Mat4> for Transform {
fn from(t: Mat4) -> Self {
Transform::Mat4(t)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Clone, Debug)]
pub struct Stroke {
pub width: f32,
pub cap: LineCap,
pub join: LineJoin,
pub miter_limit: f32,
pub dash: Vec<f32>,
pub dash_offset: f32,
}
impl Default for Stroke {
fn default() -> Self {
Self {
width: 1.0,
cap: LineCap::Butt,
join: LineJoin::Miter,
miter_limit: 4.0,
dash: Vec::new(),
dash_offset: 0.0,
}
}
}
impl Stroke {
pub fn new(width: f32) -> Self {
Self {
width,
..Default::default()
}
}
pub fn with_cap(mut self, cap: LineCap) -> Self {
self.cap = cap;
self
}
pub fn with_join(mut self, join: LineJoin) -> Self {
self.join = join;
self
}
pub fn with_dash(mut self, pattern: Vec<f32>, offset: f32) -> Self {
self.dash = pattern;
self.dash_offset = offset;
self
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextBaseline {
Top,
Middle,
#[default]
Alphabetic,
Bottom,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FontWeight {
Thin,
Light,
#[default]
Regular,
Medium,
Bold,
Black,
}
#[derive(Clone, Debug)]
pub struct TextStyle {
pub family: String,
pub size: f32,
pub weight: FontWeight,
pub color: Color,
pub align: TextAlign,
pub baseline: TextBaseline,
pub letter_spacing: f32,
pub line_height: f32,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
family: "system-ui".to_string(),
size: 14.0,
weight: FontWeight::Regular,
color: Color::BLACK,
align: TextAlign::Left,
baseline: TextBaseline::Alphabetic,
letter_spacing: 0.0,
line_height: 1.2,
}
}
}
impl TextStyle {
pub fn new(size: f32) -> Self {
Self {
size,
..Default::default()
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn with_weight(mut self, weight: FontWeight) -> Self {
self.weight = weight;
self
}
pub fn with_family(mut self, family: impl Into<String>) -> Self {
self.family = family.into();
self
}
}
#[derive(Clone, Debug)]
pub enum PathCommand {
MoveTo(Point),
LineTo(Point),
QuadTo { control: Point, end: Point },
CubicTo {
control1: Point,
control2: Point,
end: Point,
},
ArcTo {
radii: Vec2,
rotation: f32,
large_arc: bool,
sweep: bool,
end: Point,
},
Close,
}
#[derive(Clone, Debug, Default)]
pub struct Path {
commands: Vec<PathCommand>,
}
impl Path {
pub fn new() -> Self {
Self {
commands: Vec::new(),
}
}
pub fn from_commands(commands: Vec<PathCommand>) -> Self {
Self { commands }
}
pub fn move_to(mut self, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::MoveTo(Point::new(x, y)));
self
}
pub fn line_to(mut self, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::LineTo(Point::new(x, y)));
self
}
pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::QuadTo {
control: Point::new(cx, cy),
end: Point::new(x, y),
});
self
}
pub fn cubic_to(mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::CubicTo {
control1: Point::new(cx1, cy1),
control2: Point::new(cx2, cy2),
end: Point::new(x, y),
});
self
}
pub fn close(mut self) -> Self {
self.commands.push(PathCommand::Close);
self
}
pub fn arc_to(
mut self,
radii: Vec2,
rotation: f32,
large_arc: bool,
sweep: bool,
x: f32,
y: f32,
) -> Self {
self.commands.push(PathCommand::ArcTo {
radii,
rotation,
large_arc,
sweep,
end: Point::new(x, y),
});
self
}
pub fn rect(rect: Rect) -> Self {
Self::new()
.move_to(rect.x(), rect.y())
.line_to(rect.x() + rect.width(), rect.y())
.line_to(rect.x() + rect.width(), rect.y() + rect.height())
.line_to(rect.x(), rect.y() + rect.height())
.close()
}
pub fn circle(center: Point, radius: f32) -> Self {
let k = 0.552_284_8; let r = radius;
let cx = center.x;
let cy = center.y;
Self::new()
.move_to(cx + r, cy)
.cubic_to(cx + r, cy + r * k, cx + r * k, cy + r, cx, cy + r)
.cubic_to(cx - r * k, cy + r, cx - r, cy + r * k, cx - r, cy)
.cubic_to(cx - r, cy - r * k, cx - r * k, cy - r, cx, cy - r)
.cubic_to(cx + r * k, cy - r, cx + r, cy - r * k, cx + r, cy)
.close()
}
pub fn line(from: Point, to: Point) -> Self {
Self::new().move_to(from.x, from.y).line_to(to.x, to.y)
}
pub fn commands(&self) -> &[PathCommand] {
&self.commands
}
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
pub fn bounds(&self) -> Rect {
if self.commands.is_empty() {
return Rect::ZERO;
}
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for cmd in &self.commands {
match cmd {
PathCommand::MoveTo(p) | PathCommand::LineTo(p) => {
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
max_y = max_y.max(p.y);
}
PathCommand::QuadTo { control, end } => {
min_x = min_x.min(control.x).min(end.x);
min_y = min_y.min(control.y).min(end.y);
max_x = max_x.max(control.x).max(end.x);
max_y = max_y.max(control.y).max(end.y);
}
PathCommand::CubicTo {
control1,
control2,
end,
} => {
min_x = min_x.min(control1.x).min(control2.x).min(end.x);
min_y = min_y.min(control1.y).min(control2.y).min(end.y);
max_x = max_x.max(control1.x).max(control2.x).max(end.x);
max_y = max_y.max(control1.y).max(control2.y).max(end.y);
}
PathCommand::ArcTo { end, radii, .. } => {
min_x = min_x.min(end.x).min(end.x - radii.x);
min_y = min_y.min(end.y).min(end.y - radii.y);
max_x = max_x.max(end.x).max(end.x + radii.x);
max_y = max_y.max(end.y).max(end.y + radii.y);
}
PathCommand::Close => {}
}
}
if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
} else {
Rect::ZERO
}
}
pub fn rounded_rect(rect: Rect, corner_radius: impl Into<CornerRadius>) -> Self {
let r = corner_radius.into();
let x = rect.x();
let y = rect.y();
let w = rect.width();
let h = rect.height();
let max_r = (w.min(h) / 2.0).max(0.0);
let tl = r.top_left.min(max_r);
let tr = r.top_right.min(max_r);
let br = r.bottom_right.min(max_r);
let bl = r.bottom_left.min(max_r);
let k = 0.552_284_8;
let mut path = Self::new().move_to(x + tl, y);
path = path.line_to(x + w - tr, y);
if tr > 0.0 {
path = path.cubic_to(
x + w - tr * (1.0 - k),
y,
x + w,
y + tr * (1.0 - k),
x + w,
y + tr,
);
}
path = path.line_to(x + w, y + h - br);
if br > 0.0 {
path = path.cubic_to(
x + w,
y + h - br * (1.0 - k),
x + w - br * (1.0 - k),
y + h,
x + w - br,
y + h,
);
}
path = path.line_to(x + bl, y + h);
if bl > 0.0 {
path = path.cubic_to(
x + bl * (1.0 - k),
y + h,
x,
y + h - bl * (1.0 - k),
x,
y + h - bl,
);
}
path = path.line_to(x, y + tl);
if tl > 0.0 {
path = path.cubic_to(x, y + tl * (1.0 - k), x + tl * (1.0 - k), y, x + tl, y);
}
path.close()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ImageId(pub u64);
#[derive(Clone, Debug, Default)]
pub struct ImageOptions {
pub source_rect: Option<Rect>,
pub tint: Option<Color>,
pub opacity: f32,
}
impl ImageOptions {
pub fn new() -> Self {
Self {
source_rect: None,
tint: None,
opacity: 1.0,
}
}
pub fn with_tint(mut self, color: Color) -> Self {
self.tint = Some(color);
self
}
pub fn with_opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MeshId(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MaterialId(pub u64);
#[derive(Clone, Debug)]
pub struct MeshInstance {
pub transform: Mat4,
pub material: Option<MaterialId>,
}
#[derive(Clone, Copy, Debug, Default)]
#[repr(C)]
pub struct Vertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub uv: [f32; 2],
pub color: [f32; 4],
pub tangent: [f32; 4],
pub joints: [u32; 4],
pub weights: [f32; 4],
}
impl Vertex {
pub fn new(pos: [f32; 3]) -> Self {
Self {
position: pos,
normal: [0.0, 1.0, 0.0],
uv: [0.0, 0.0],
color: [1.0, 1.0, 1.0, 1.0],
tangent: [1.0, 0.0, 0.0, 1.0],
joints: [0; 4],
weights: [1.0, 0.0, 0.0, 0.0],
}
}
pub fn with_normal(mut self, n: [f32; 3]) -> Self {
self.normal = n;
self
}
pub fn with_uv(mut self, uv: [f32; 2]) -> Self {
self.uv = uv;
self
}
pub fn with_color(mut self, c: [f32; 4]) -> Self {
self.color = c;
self
}
pub fn with_tangent(mut self, t: [f32; 4]) -> Self {
self.tangent = t;
self
}
pub fn with_joints(mut self, joints: [u32; 4], weights: [f32; 4]) -> Self {
self.joints = joints;
self.weights = weights;
self
}
}
#[derive(Clone, Debug)]
pub struct MeshData {
pub vertices: Vec<Vertex>,
pub indices: Vec<u32>,
pub material: Material,
pub skin: Option<SkinningData>,
}
#[derive(Clone, Debug)]
pub struct Material {
pub base_color: [f32; 4],
pub metallic: f32,
pub roughness: f32,
pub emissive: [f32; 3],
pub base_color_texture: Option<TextureData>,
pub normal_map: Option<TextureData>,
pub normal_scale: f32,
pub displacement_map: Option<TextureData>,
pub displacement_scale: f32,
pub unlit: bool,
pub alpha_mode: AlphaMode,
pub receives_shadows: bool,
pub casts_shadows: bool,
}
impl Default for Material {
fn default() -> Self {
Self {
base_color: [1.0, 1.0, 1.0, 1.0],
metallic: 0.0,
roughness: 0.5,
emissive: [0.0, 0.0, 0.0],
base_color_texture: None,
normal_map: None,
normal_scale: 1.0,
displacement_map: None,
displacement_scale: 0.05,
unlit: false,
alpha_mode: AlphaMode::Opaque,
receives_shadows: true,
casts_shadows: true,
}
}
}
#[derive(Clone, Debug)]
pub struct TextureData {
pub rgba: Vec<u8>,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AlphaMode {
#[default]
Opaque,
Blend,
Mask,
}
#[derive(Clone, Debug)]
pub struct Bone {
pub name: String,
pub parent: Option<usize>,
pub inverse_bind_matrix: [f32; 16],
}
#[derive(Clone, Debug)]
pub struct Skeleton {
pub bones: Vec<Bone>,
}
#[derive(Clone, Debug)]
pub struct SkinningData {
pub joint_matrices: Vec<[f32; 16]>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BlurQuality {
Low,
#[default]
Medium,
High,
}
#[derive(Clone, Debug, PartialEq)]
pub enum LayerEffect {
Blur {
radius: f32,
quality: BlurQuality,
},
DropShadow {
offset_x: f32,
offset_y: f32,
blur: f32,
spread: f32,
color: Color,
},
Glow {
color: Color,
blur: f32,
range: f32,
opacity: f32,
},
ColorMatrix {
matrix: [f32; 20],
},
MaskImage {
image_url: String,
mask_mode: MaskMode,
},
}
#[derive(Clone, Debug, PartialEq, Default)]
pub enum MaskMode {
#[default]
Alpha,
Luminance,
}
#[derive(Clone, Debug)]
pub enum MaskImage {
Url(String),
Gradient(crate::layer::Gradient),
}
impl LayerEffect {
pub fn blur(radius: f32) -> Self {
Self::Blur {
radius,
quality: BlurQuality::default(),
}
}
pub fn blur_with_quality(radius: f32, quality: BlurQuality) -> Self {
Self::Blur { radius, quality }
}
pub fn drop_shadow(offset_x: f32, offset_y: f32, blur: f32, color: Color) -> Self {
Self::DropShadow {
offset_x,
offset_y,
blur,
spread: 0.0,
color,
}
}
pub fn glow(color: Color, blur: f32, range: f32, opacity: f32) -> Self {
Self::Glow {
color,
blur,
range,
opacity,
}
}
pub fn color_matrix_identity() -> Self {
Self::ColorMatrix {
matrix: [
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ],
}
}
pub fn grayscale() -> Self {
Self::ColorMatrix {
matrix: [
0.299, 0.587, 0.114, 0.0, 0.0, 0.299, 0.587, 0.114, 0.0, 0.0, 0.299, 0.587, 0.114, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ],
}
}
pub fn sepia() -> Self {
Self::ColorMatrix {
matrix: [
0.393, 0.769, 0.189, 0.0, 0.0, 0.349, 0.686, 0.168, 0.0, 0.0, 0.272, 0.534, 0.131, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ],
}
}
pub fn brightness(factor: f32) -> Self {
Self::ColorMatrix {
matrix: [
factor, 0.0, 0.0, 0.0, 0.0, 0.0, factor, 0.0, 0.0, 0.0, 0.0, 0.0, factor, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ],
}
}
pub fn contrast(factor: f32) -> Self {
let offset = 0.5 * (1.0 - factor);
Self::ColorMatrix {
matrix: [
factor, 0.0, 0.0, 0.0, offset, 0.0, factor, 0.0, 0.0, offset, 0.0, 0.0, factor, 0.0, offset, 0.0, 0.0, 0.0, 1.0, 0.0, ],
}
}
pub fn saturation(factor: f32) -> Self {
let inv = 1.0 - factor;
let r = 0.299 * inv;
let g = 0.587 * inv;
let b = 0.114 * inv;
Self::ColorMatrix {
matrix: [
r + factor,
g,
b,
0.0,
0.0, r,
g + factor,
b,
0.0,
0.0, r,
g,
b + factor,
0.0,
0.0, 0.0,
0.0,
0.0,
1.0,
0.0, ],
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transform3DParams {
pub sin_rx: f32,
pub cos_rx: f32,
pub sin_ry: f32,
pub cos_ry: f32,
pub perspective_d: f32,
}
#[derive(Clone, Debug, Default)]
pub struct LayerConfig {
pub id: Option<LayerId>,
pub position: Option<crate::Point>,
pub size: Option<Size>,
pub blend_mode: BlendMode,
pub opacity: f32,
pub depth: bool,
pub effects: Vec<LayerEffect>,
pub transform_3d: Option<Transform3DParams>,
}
impl LayerConfig {
pub fn new() -> Self {
Self::default()
}
pub fn id(mut self, id: LayerId) -> Self {
self.id = Some(id);
self
}
pub fn size(mut self, size: Size) -> Self {
self.size = Some(size);
self
}
pub fn position(mut self, position: crate::Point) -> Self {
self.position = Some(position);
self
}
pub fn blend_mode(mut self, mode: BlendMode) -> Self {
self.blend_mode = mode;
self
}
pub fn opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity;
self
}
pub fn with_depth(mut self) -> Self {
self.depth = true;
self
}
pub fn effect(mut self, effect: LayerEffect) -> Self {
self.effects.push(effect);
self
}
pub fn blur(self, radius: f32) -> Self {
self.effect(LayerEffect::blur(radius))
}
pub fn drop_shadow(self, offset_x: f32, offset_y: f32, blur: f32, color: Color) -> Self {
self.effect(LayerEffect::drop_shadow(offset_x, offset_y, blur, color))
}
pub fn glow(self, color: Color, blur: f32, range: f32, opacity: f32) -> Self {
self.effect(LayerEffect::glow(color, blur, range, opacity))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ShapeId(pub u32);
pub trait SdfBuilder {
fn rect(&mut self, rect: Rect, corner_radius: CornerRadius) -> ShapeId;
fn circle(&mut self, center: Point, radius: f32) -> ShapeId;
fn ellipse(&mut self, center: Point, radii: Vec2) -> ShapeId;
fn line(&mut self, from: Point, to: Point, width: f32) -> ShapeId;
fn arc(&mut self, center: Point, radius: f32, start: f32, end: f32, width: f32) -> ShapeId;
fn quad_bezier(&mut self, p0: Point, p1: Point, p2: Point, width: f32) -> ShapeId;
fn union(&mut self, a: ShapeId, b: ShapeId) -> ShapeId;
fn subtract(&mut self, a: ShapeId, b: ShapeId) -> ShapeId;
fn intersect(&mut self, a: ShapeId, b: ShapeId) -> ShapeId;
fn smooth_union(&mut self, a: ShapeId, b: ShapeId, radius: f32) -> ShapeId;
fn smooth_subtract(&mut self, a: ShapeId, b: ShapeId, radius: f32) -> ShapeId;
fn smooth_intersect(&mut self, a: ShapeId, b: ShapeId, radius: f32) -> ShapeId;
fn round(&mut self, shape: ShapeId, radius: f32) -> ShapeId;
fn outline(&mut self, shape: ShapeId, width: f32) -> ShapeId;
fn offset(&mut self, shape: ShapeId, distance: f32) -> ShapeId;
fn fill(&mut self, shape: ShapeId, brush: Brush);
fn stroke(&mut self, shape: ShapeId, stroke: &Stroke, brush: Brush);
fn shadow(&mut self, shape: ShapeId, shadow: Shadow);
}
pub trait DrawContext {
fn push_transform(&mut self, transform: Transform);
fn pop_transform(&mut self);
fn current_transform(&self) -> Transform;
fn push_clip(&mut self, shape: ClipShape);
fn pop_clip(&mut self);
fn push_opacity(&mut self, opacity: f32);
fn pop_opacity(&mut self);
fn push_blend_mode(&mut self, mode: BlendMode);
fn pop_blend_mode(&mut self);
fn set_foreground_layer(&mut self, _is_foreground: bool) {
}
fn set_z_layer(&mut self, _layer: u32) {
}
fn z_layer(&self) -> u32 {
0
}
fn set_3d_transform(&mut self, _rx_rad: f32, _ry_rad: f32, _perspective_d: f32) {}
fn set_3d_shape(&mut self, _shape_type: f32, _depth: f32, _ambient: f32, _specular: f32) {}
fn set_3d_light(&mut self, _direction: [f32; 3], _intensity: f32) {}
fn set_3d_translate_z(&mut self, _z: f32) {}
fn set_3d_group_raw(&mut self, _shapes: &[[f32; 16]]) {}
fn clear_3d(&mut self) {}
#[allow(clippy::too_many_arguments)]
fn set_css_filter(
&mut self,
_grayscale: f32,
_invert: f32,
_sepia: f32,
_hue_rotate_deg: f32,
_brightness: f32,
_contrast: f32,
_saturate: f32,
) {
}
fn clear_css_filter(&mut self) {}
fn set_mask_gradient(&mut self, _params: [f32; 4], _info: [f32; 4]) {}
fn clear_mask_gradient(&mut self) {}
fn set_corner_shape(&mut self, _shape: [f32; 4]) {}
fn clear_corner_shape(&mut self) {}
fn set_overflow_fade(&mut self, _fade: [f32; 4]) {}
fn clear_overflow_fade(&mut self) {}
fn fill_path(&mut self, path: &Path, brush: Brush);
fn stroke_path(&mut self, path: &Path, stroke: &Stroke, brush: Brush);
fn fill_rect(&mut self, rect: Rect, corner_radius: CornerRadius, brush: Brush);
fn fill_rect_with_per_side_border(
&mut self,
rect: Rect,
corner_radius: CornerRadius,
brush: Brush,
border_widths: [f32; 4],
border_color: Color,
) {
self.fill_rect(rect, corner_radius, brush);
let max_border = border_widths.iter().cloned().fold(0.0f32, |a, b| a.max(b));
if max_border > 0.0 {
let stroke = Stroke::new(max_border);
self.stroke_rect(rect, corner_radius, &stroke, Brush::Solid(border_color));
}
}
fn stroke_rect(
&mut self,
rect: Rect,
corner_radius: CornerRadius,
stroke: &Stroke,
brush: Brush,
);
fn fill_circle(&mut self, center: Point, radius: f32, brush: Brush);
fn stroke_circle(&mut self, center: Point, radius: f32, stroke: &Stroke, brush: Brush);
fn draw_text(&mut self, text: &str, origin: Point, style: &TextStyle);
fn draw_image(&mut self, image: ImageId, rect: Rect, options: &ImageOptions);
fn draw_rgba_pixels(&mut self, _data: &[u8], _width: u32, _height: u32, _dest: Rect) {
}
fn draw_shadow(&mut self, rect: Rect, corner_radius: CornerRadius, shadow: Shadow);
fn draw_inner_shadow(&mut self, rect: Rect, corner_radius: CornerRadius, shadow: Shadow);
fn draw_circle_shadow(&mut self, center: Point, radius: f32, shadow: Shadow);
fn draw_circle_inner_shadow(&mut self, center: Point, radius: f32, shadow: Shadow);
fn sdf_build(&mut self, f: &mut dyn FnMut(&mut dyn SdfBuilder));
fn set_camera(&mut self, camera: &Camera);
fn draw_mesh(&mut self, mesh: MeshId, material: MaterialId, transform: Mat4);
fn draw_mesh_instanced(&mut self, mesh: MeshId, instances: &[MeshInstance]);
fn draw_mesh_data(&mut self, _mesh: &MeshData, _transform: Mat4) {
}
fn add_light(&mut self, light: Light);
fn set_environment(&mut self, env: &Environment);
fn billboard_draw(
&mut self,
size: Size,
transform: Mat4,
facing: BillboardFacing,
f: &mut dyn FnMut(&mut dyn DrawContext),
);
fn viewport_3d_draw(
&mut self,
rect: Rect,
camera: &Camera,
f: &mut dyn FnMut(&mut dyn DrawContext),
);
fn draw_sdf_viewport(&mut self, _rect: Rect, _viewport: &Sdf3DViewport) {
}
fn draw_particles(&mut self, _rect: Rect, _particle_data: &ParticleSystemData) {
}
fn push_layer(&mut self, config: LayerConfig);
fn pop_layer(&mut self);
fn sample_layer(&mut self, id: LayerId, source_rect: Rect, dest_rect: Rect);
fn viewport_size(&self) -> Size;
fn is_3d_context(&self) -> bool;
fn current_opacity(&self) -> f32;
fn current_blend_mode(&self) -> BlendMode;
}
pub trait DrawContextExt: DrawContext {
fn fill<B: Into<Brush>>(&mut self, path: &Path, brush: B) {
self.fill_path(path, brush.into());
}
fn stroke<B: Into<Brush>>(&mut self, path: &Path, stroke: &Stroke, brush: B) {
self.stroke_path(path, stroke, brush.into());
}
fn fill_rounded_rect<B: Into<Brush>>(
&mut self,
rect: Rect,
corner_radius: CornerRadius,
brush: B,
) {
self.fill_rect(rect, corner_radius, brush.into());
}
fn sdf<F: FnMut(&mut dyn SdfBuilder)>(&mut self, mut f: F) {
self.sdf_build(&mut f);
}
fn billboard<F: FnMut(&mut dyn DrawContext)>(
&mut self,
size: Size,
transform: Mat4,
facing: BillboardFacing,
mut f: F,
) {
self.billboard_draw(size, transform, facing, &mut f);
}
fn viewport_3d<F: FnMut(&mut dyn DrawContext)>(
&mut self,
rect: Rect,
camera: &Camera,
mut f: F,
) {
self.viewport_3d_draw(rect, camera, &mut f);
}
}
impl<T: DrawContext + ?Sized> DrawContextExt for T {}
#[derive(Clone, Debug)]
pub enum DrawCommand {
PushTransform(Transform),
PopTransform,
PushClip(ClipShape),
PopClip,
PushOpacity(f32),
PopOpacity,
PushBlendMode(BlendMode),
PopBlendMode,
FillPath {
path: Path,
brush: Brush,
},
StrokePath {
path: Path,
stroke: Stroke,
brush: Brush,
},
FillRect {
rect: Rect,
corner_radius: CornerRadius,
brush: Brush,
},
StrokeRect {
rect: Rect,
corner_radius: CornerRadius,
stroke: Stroke,
brush: Brush,
},
FillCircle {
center: Point,
radius: f32,
brush: Brush,
},
StrokeCircle {
center: Point,
radius: f32,
stroke: Stroke,
brush: Brush,
},
DrawText {
text: String,
origin: Point,
style: TextStyle,
},
DrawImage {
image: ImageId,
rect: Rect,
options: ImageOptions,
},
DrawShadow {
rect: Rect,
corner_radius: CornerRadius,
shadow: Shadow,
},
DrawInnerShadow {
rect: Rect,
corner_radius: CornerRadius,
shadow: Shadow,
},
DrawCircleShadow {
center: Point,
radius: f32,
shadow: Shadow,
},
DrawCircleInnerShadow {
center: Point,
radius: f32,
shadow: Shadow,
},
SetCamera(Camera),
DrawMesh {
mesh: MeshId,
material: MaterialId,
transform: Mat4,
},
DrawMeshInstanced {
mesh: MeshId,
instances: Vec<MeshInstance>,
},
AddLight(Light),
SetEnvironment(Environment),
PushLayer(LayerConfig),
PopLayer,
SampleLayer {
id: LayerId,
source_rect: Rect,
dest_rect: Rect,
},
}
#[derive(Debug, Default)]
pub struct RecordingContext {
commands: Vec<DrawCommand>,
transform_stack: Vec<Transform>,
opacity_stack: Vec<f32>,
blend_mode_stack: Vec<BlendMode>,
viewport: Size,
is_3d: bool,
}
impl RecordingContext {
pub fn new(viewport: Size) -> Self {
Self {
commands: Vec::new(),
transform_stack: vec![Transform::identity()],
opacity_stack: vec![1.0],
blend_mode_stack: vec![BlendMode::Normal],
viewport,
is_3d: false,
}
}
pub fn commands(&self) -> &[DrawCommand] {
&self.commands
}
pub fn take_commands(&mut self) -> Vec<DrawCommand> {
std::mem::take(&mut self.commands)
}
pub fn clear(&mut self) {
self.commands.clear();
self.transform_stack = vec![Transform::identity()];
self.opacity_stack = vec![1.0];
self.blend_mode_stack = vec![BlendMode::Normal];
}
}
impl DrawContext for RecordingContext {
fn push_transform(&mut self, transform: Transform) {
self.commands
.push(DrawCommand::PushTransform(transform.clone()));
self.transform_stack.push(transform);
}
fn pop_transform(&mut self) {
self.commands.push(DrawCommand::PopTransform);
if self.transform_stack.len() > 1 {
self.transform_stack.pop();
}
}
fn current_transform(&self) -> Transform {
self.transform_stack.last().cloned().unwrap_or_default()
}
fn push_clip(&mut self, shape: ClipShape) {
self.commands.push(DrawCommand::PushClip(shape));
}
fn pop_clip(&mut self) {
self.commands.push(DrawCommand::PopClip);
}
fn push_opacity(&mut self, opacity: f32) {
self.commands.push(DrawCommand::PushOpacity(opacity));
let current = *self.opacity_stack.last().unwrap_or(&1.0);
self.opacity_stack.push(current * opacity);
}
fn pop_opacity(&mut self) {
self.commands.push(DrawCommand::PopOpacity);
if self.opacity_stack.len() > 1 {
self.opacity_stack.pop();
}
}
fn push_blend_mode(&mut self, mode: BlendMode) {
self.commands.push(DrawCommand::PushBlendMode(mode));
self.blend_mode_stack.push(mode);
}
fn pop_blend_mode(&mut self) {
self.commands.push(DrawCommand::PopBlendMode);
if self.blend_mode_stack.len() > 1 {
self.blend_mode_stack.pop();
}
}
fn fill_path(&mut self, path: &Path, brush: Brush) {
self.commands.push(DrawCommand::FillPath {
path: path.clone(),
brush,
});
}
fn stroke_path(&mut self, path: &Path, stroke: &Stroke, brush: Brush) {
self.commands.push(DrawCommand::StrokePath {
path: path.clone(),
stroke: stroke.clone(),
brush,
});
}
fn fill_rect(&mut self, rect: Rect, corner_radius: CornerRadius, brush: Brush) {
self.commands.push(DrawCommand::FillRect {
rect,
corner_radius,
brush,
});
}
fn stroke_rect(
&mut self,
rect: Rect,
corner_radius: CornerRadius,
stroke: &Stroke,
brush: Brush,
) {
self.commands.push(DrawCommand::StrokeRect {
rect,
corner_radius,
stroke: stroke.clone(),
brush,
});
}
fn fill_circle(&mut self, center: Point, radius: f32, brush: Brush) {
self.commands.push(DrawCommand::FillCircle {
center,
radius,
brush,
});
}
fn stroke_circle(&mut self, center: Point, radius: f32, stroke: &Stroke, brush: Brush) {
self.commands.push(DrawCommand::StrokeCircle {
center,
radius,
stroke: stroke.clone(),
brush,
});
}
fn draw_text(&mut self, text: &str, origin: Point, style: &TextStyle) {
self.commands.push(DrawCommand::DrawText {
text: text.to_string(),
origin,
style: style.clone(),
});
}
fn draw_image(&mut self, image: ImageId, rect: Rect, options: &ImageOptions) {
self.commands.push(DrawCommand::DrawImage {
image,
rect,
options: options.clone(),
});
}
fn draw_shadow(&mut self, rect: Rect, corner_radius: CornerRadius, shadow: Shadow) {
self.commands.push(DrawCommand::DrawShadow {
rect,
corner_radius,
shadow,
});
}
fn draw_inner_shadow(&mut self, rect: Rect, corner_radius: CornerRadius, shadow: Shadow) {
self.commands.push(DrawCommand::DrawInnerShadow {
rect,
corner_radius,
shadow,
});
}
fn draw_circle_shadow(&mut self, center: Point, radius: f32, shadow: Shadow) {
self.commands.push(DrawCommand::DrawCircleShadow {
center,
radius,
shadow,
});
}
fn draw_circle_inner_shadow(&mut self, center: Point, radius: f32, shadow: Shadow) {
self.commands.push(DrawCommand::DrawCircleInnerShadow {
center,
radius,
shadow,
});
}
fn sdf_build(&mut self, f: &mut dyn FnMut(&mut dyn SdfBuilder)) {
let mut builder = RecordingSdfBuilder::new();
f(&mut builder);
for (shape_id, shadow) in &builder.shadows {
if let Some(shape) = builder.shapes.get(shape_id.0 as usize) {
match shape {
SdfShape::Rect {
rect,
corner_radius,
} => {
self.draw_shadow(*rect, *corner_radius, *shadow);
}
SdfShape::Circle { center, radius } => {
self.draw_circle_shadow(*center, *radius, *shadow);
}
SdfShape::Ellipse { center, radii } => {
let rect =
Rect::from_center(*center, Size::new(radii.x * 2.0, radii.y * 2.0));
self.draw_shadow(rect, radii.x.min(radii.y).into(), *shadow);
}
_ => {
}
}
}
}
for (shape_id, brush) in builder.fills {
if let Some(shape) = builder.shapes.get(shape_id.0 as usize) {
match shape {
SdfShape::Rect {
rect,
corner_radius,
} => {
self.fill_rect(*rect, *corner_radius, brush);
}
SdfShape::Circle { center, radius } => {
self.fill_circle(*center, *radius, brush);
}
SdfShape::Ellipse { center, radii } => {
let path = Path::circle(*center, radii.x); self.fill_path(&path, brush);
}
SdfShape::Line { from, to, width } => {
let path = Path::line(*from, *to);
self.stroke_path(&path, &Stroke::new(*width), brush);
}
_ => {
}
}
}
}
for (shape_id, stroke, brush) in builder.strokes {
if let Some(shape) = builder.shapes.get(shape_id.0 as usize) {
match shape {
SdfShape::Rect {
rect,
corner_radius,
} => {
self.stroke_rect(*rect, *corner_radius, &stroke, brush);
}
SdfShape::Circle { center, radius } => {
self.stroke_circle(*center, *radius, &stroke, brush);
}
SdfShape::Ellipse { center, radii } => {
let path = Path::circle(*center, radii.x); self.stroke_path(&path, &stroke, brush);
}
SdfShape::Line { from, to, .. } => {
let path = Path::line(*from, *to);
self.stroke_path(&path, &stroke, brush);
}
_ => {
}
}
}
}
}
fn set_camera(&mut self, camera: &Camera) {
self.commands.push(DrawCommand::SetCamera(camera.clone()));
self.is_3d = true;
}
fn draw_mesh(&mut self, mesh: MeshId, material: MaterialId, transform: Mat4) {
self.commands.push(DrawCommand::DrawMesh {
mesh,
material,
transform,
});
}
fn draw_mesh_instanced(&mut self, mesh: MeshId, instances: &[MeshInstance]) {
self.commands.push(DrawCommand::DrawMeshInstanced {
mesh,
instances: instances.to_vec(),
});
}
fn add_light(&mut self, light: Light) {
self.commands.push(DrawCommand::AddLight(light));
}
fn set_environment(&mut self, env: &Environment) {
self.commands.push(DrawCommand::SetEnvironment(env.clone()));
}
fn billboard_draw(
&mut self,
_size: Size,
_transform: Mat4,
_facing: BillboardFacing,
f: &mut dyn FnMut(&mut dyn DrawContext),
) {
let mut sub_ctx = RecordingContext::new(self.viewport);
f(&mut sub_ctx);
self.commands.extend(sub_ctx.commands);
}
fn viewport_3d_draw(
&mut self,
_rect: Rect,
camera: &Camera,
f: &mut dyn FnMut(&mut dyn DrawContext),
) {
let was_3d = self.is_3d;
self.set_camera(camera);
f(self);
self.is_3d = was_3d;
}
fn push_layer(&mut self, config: LayerConfig) {
self.commands.push(DrawCommand::PushLayer(config));
}
fn pop_layer(&mut self) {
self.commands.push(DrawCommand::PopLayer);
}
fn sample_layer(&mut self, id: LayerId, source_rect: Rect, dest_rect: Rect) {
self.commands.push(DrawCommand::SampleLayer {
id,
source_rect,
dest_rect,
});
}
fn viewport_size(&self) -> Size {
self.viewport
}
fn is_3d_context(&self) -> bool {
self.is_3d
}
fn current_opacity(&self) -> f32 {
*self.opacity_stack.last().unwrap_or(&1.0)
}
fn current_blend_mode(&self) -> BlendMode {
self.blend_mode_stack
.last()
.copied()
.unwrap_or(BlendMode::Normal)
}
}
#[derive(Clone, Debug)]
enum SdfShape {
Rect {
rect: Rect,
corner_radius: CornerRadius,
},
Circle {
center: Point,
radius: f32,
},
Ellipse {
center: Point,
radii: Vec2,
},
Line {
from: Point,
to: Point,
width: f32,
},
Arc {
center: Point,
radius: f32,
start: f32,
end: f32,
width: f32,
},
QuadBezier {
p0: Point,
p1: Point,
p2: Point,
width: f32,
},
Union {
a: ShapeId,
b: ShapeId,
},
Subtract {
a: ShapeId,
b: ShapeId,
},
Intersect {
a: ShapeId,
b: ShapeId,
},
SmoothUnion {
a: ShapeId,
b: ShapeId,
radius: f32,
},
SmoothSubtract {
a: ShapeId,
b: ShapeId,
radius: f32,
},
SmoothIntersect {
a: ShapeId,
b: ShapeId,
radius: f32,
},
Round {
shape: ShapeId,
radius: f32,
},
Outline {
shape: ShapeId,
width: f32,
},
Offset {
shape: ShapeId,
distance: f32,
},
}
struct RecordingSdfBuilder {
shapes: Vec<SdfShape>,
fills: Vec<(ShapeId, Brush)>,
strokes: Vec<(ShapeId, Stroke, Brush)>,
shadows: Vec<(ShapeId, Shadow)>,
}
impl RecordingSdfBuilder {
fn new() -> Self {
Self {
shapes: Vec::new(),
fills: Vec::new(),
strokes: Vec::new(),
shadows: Vec::new(),
}
}
fn add_shape(&mut self, shape: SdfShape) -> ShapeId {
let id = ShapeId(self.shapes.len() as u32);
self.shapes.push(shape);
id
}
}
impl SdfBuilder for RecordingSdfBuilder {
fn rect(&mut self, rect: Rect, corner_radius: CornerRadius) -> ShapeId {
self.add_shape(SdfShape::Rect {
rect,
corner_radius,
})
}
fn circle(&mut self, center: Point, radius: f32) -> ShapeId {
self.add_shape(SdfShape::Circle { center, radius })
}
fn ellipse(&mut self, center: Point, radii: Vec2) -> ShapeId {
self.add_shape(SdfShape::Ellipse { center, radii })
}
fn line(&mut self, from: Point, to: Point, width: f32) -> ShapeId {
self.add_shape(SdfShape::Line { from, to, width })
}
fn arc(&mut self, center: Point, radius: f32, start: f32, end: f32, width: f32) -> ShapeId {
self.add_shape(SdfShape::Arc {
center,
radius,
start,
end,
width,
})
}
fn quad_bezier(&mut self, p0: Point, p1: Point, p2: Point, width: f32) -> ShapeId {
self.add_shape(SdfShape::QuadBezier { p0, p1, p2, width })
}
fn union(&mut self, a: ShapeId, b: ShapeId) -> ShapeId {
self.add_shape(SdfShape::Union { a, b })
}
fn subtract(&mut self, a: ShapeId, b: ShapeId) -> ShapeId {
self.add_shape(SdfShape::Subtract { a, b })
}
fn intersect(&mut self, a: ShapeId, b: ShapeId) -> ShapeId {
self.add_shape(SdfShape::Intersect { a, b })
}
fn smooth_union(&mut self, a: ShapeId, b: ShapeId, radius: f32) -> ShapeId {
self.add_shape(SdfShape::SmoothUnion { a, b, radius })
}
fn smooth_subtract(&mut self, a: ShapeId, b: ShapeId, radius: f32) -> ShapeId {
self.add_shape(SdfShape::SmoothSubtract { a, b, radius })
}
fn smooth_intersect(&mut self, a: ShapeId, b: ShapeId, radius: f32) -> ShapeId {
self.add_shape(SdfShape::SmoothIntersect { a, b, radius })
}
fn round(&mut self, shape: ShapeId, radius: f32) -> ShapeId {
self.add_shape(SdfShape::Round { shape, radius })
}
fn outline(&mut self, shape: ShapeId, width: f32) -> ShapeId {
self.add_shape(SdfShape::Outline { shape, width })
}
fn offset(&mut self, shape: ShapeId, distance: f32) -> ShapeId {
self.add_shape(SdfShape::Offset { shape, distance })
}
fn fill(&mut self, shape: ShapeId, brush: Brush) {
self.fills.push((shape, brush));
}
fn stroke(&mut self, shape: ShapeId, stroke: &Stroke, brush: Brush) {
self.strokes.push((shape, stroke.clone(), brush));
}
fn shadow(&mut self, shape: ShapeId, shadow: Shadow) {
self.shadows.push((shape, shadow));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recording_context() {
let mut ctx = RecordingContext::new(Size::new(800.0, 600.0));
ctx.push_transform(Transform::translate(10.0, 20.0));
ctx.fill_rect(
Rect::new(0.0, 0.0, 100.0, 50.0),
8.0.into(),
Color::BLUE.into(),
);
ctx.draw_text("Hello", Point::new(10.0, 30.0), &TextStyle::default());
ctx.pop_transform();
assert_eq!(ctx.commands().len(), 4);
}
#[test]
fn test_path_builder() {
let path = Path::new()
.move_to(0.0, 0.0)
.line_to(100.0, 0.0)
.line_to(100.0, 100.0)
.line_to(0.0, 100.0)
.close();
assert_eq!(path.commands().len(), 5);
}
#[test]
fn test_path_shortcuts() {
let rect = Path::rect(Rect::new(0.0, 0.0, 100.0, 50.0));
assert_eq!(rect.commands().len(), 5);
let circle = Path::circle(Point::new(50.0, 50.0), 25.0);
assert!(!circle.is_empty());
}
#[test]
fn test_transform_stack() {
let mut ctx = RecordingContext::new(Size::new(800.0, 600.0));
assert!(ctx.current_transform().is_2d());
ctx.push_transform(Transform::translate(10.0, 20.0));
ctx.push_transform(Transform::scale(2.0, 2.0));
ctx.pop_transform();
ctx.pop_transform();
ctx.pop_transform();
}
#[test]
fn test_opacity_stack() {
let mut ctx = RecordingContext::new(Size::new(800.0, 600.0));
assert_eq!(ctx.current_opacity(), 1.0);
ctx.push_opacity(0.5);
assert_eq!(ctx.current_opacity(), 0.5);
ctx.push_opacity(0.5);
assert_eq!(ctx.current_opacity(), 0.25);
ctx.pop_opacity();
assert_eq!(ctx.current_opacity(), 0.5);
}
#[test]
fn test_sdf_builder() {
let mut ctx = RecordingContext::new(Size::new(800.0, 600.0));
ctx.sdf(|sdf| {
let rect = sdf.rect(Rect::new(0.0, 0.0, 100.0, 50.0), 8.0.into());
sdf.fill(rect, Color::BLUE.into());
let circle = sdf.circle(Point::new(50.0, 50.0), 25.0);
sdf.fill(circle, Color::RED.into());
});
assert!(!ctx.commands().is_empty());
}
#[test]
fn test_stroke_configuration() {
let stroke = Stroke::new(2.0)
.with_cap(LineCap::Round)
.with_join(LineJoin::Bevel)
.with_dash(vec![5.0, 3.0], 0.0);
assert_eq!(stroke.width, 2.0);
assert_eq!(stroke.cap, LineCap::Round);
assert_eq!(stroke.join, LineJoin::Bevel);
assert_eq!(stroke.dash.len(), 2);
}
#[test]
fn test_text_style() {
let style = TextStyle::new(16.0)
.with_color(Color::WHITE)
.with_weight(FontWeight::Bold)
.with_family("Arial");
assert_eq!(style.size, 16.0);
assert_eq!(style.weight, FontWeight::Bold);
assert_eq!(style.family, "Arial");
}
#[test]
fn test_draw_context_ext() {
let mut ctx = RecordingContext::new(Size::new(800.0, 600.0));
let path = Path::rect(Rect::new(0.0, 0.0, 100.0, 50.0));
ctx.fill(&path, Color::BLUE);
ctx.fill_rounded_rect(Rect::new(10.0, 10.0, 80.0, 30.0), 4.0.into(), Color::RED);
assert_eq!(ctx.commands().len(), 2);
}
}