~raph/piet

e987d565eb8043765afc16bb64470e25419890c5 — Raph Levien 3 years ago 8bf77ef
Add solid brushes, filled and stroked Bezier paths

Add some actual drawing functionality.
3 files changed, 204 insertions(+), 24 deletions(-)

M piet-direct2d/examples/basic.rs
M piet-direct2d/src/lib.rs
M piet/src/traits.rs
M piet-direct2d/examples/basic.rs => piet-direct2d/examples/basic.rs +23 -3
@@ 9,6 9,8 @@ use direct3d11::flags::{BindFlags, CreateDeviceFlags};
use direct3d11::helpers::ComWrapper;
use dxgi::flags::Format;

use kurbo::BezPath;

use piet::RenderContext;
use piet_direct2d::D2DRenderContext;



@@ 20,6 22,25 @@ const TEXTURE_HEIGHT_S: usize = TEXTURE_HEIGHT as usize;

const HIDPI: f32 = 2.0;

fn draw_pretty_picture<R: RenderContext>(rc: &mut R) {
    rc.clear(0xFF_FF_FF);
    let brush = rc.solid_brush(0x00_00_80_FF);
    rc.line((10.0, 10.0), (100.0, 50.0), &brush, 1.0, None);

    let mut path = BezPath::new();
    path.moveto((50.0, 10.0));
    path.quadto((60.0, 50.0), (100.0, 90.0));
    let brush = rc.solid_brush(0x00_80_00_FF);
    rc.stroke_path(path.elements().iter().cloned(), &brush, 1.0, None);

    let mut path = BezPath::new();
    path.moveto((10.0, 20.0));
    path.curveto((10.0, 80.0), (100.0, 80.0), (100.0, 60.0));
    let brush = rc.solid_brush(0x00_00_80_C0);
    // We'll make this `&path` by fixing kurbo.
    rc.fill_path(path.elements().iter().cloned(), &brush);
}

fn main() {
    // Create the D2D factory
    let d2d = direct2d::factory::Factory::new().unwrap();


@@ 53,9 74,8 @@ fn main() {
    context.set_target(&target);
    context.set_dpi(96.0 * HIDPI, 96.0 * HIDPI);
    context.begin_draw();
    let mut piet_context = D2DRenderContext::new(&context);
    piet_context.clear(0xFF_FF_FF);
    piet_context.line((10.0, 10.0), (100.0, 50.0), 1.0);
    let mut piet_context = D2DRenderContext::new(&d2d, &mut context);
    draw_pretty_picture(&mut piet_context);
    context.end_draw().unwrap();

    let temp_texture = direct3d11::texture2d::Texture2D::create(&d3d)

M piet-direct2d/src/lib.rs => piet-direct2d/src/lib.rs +136 -18
@@ 1,23 1,30 @@
//! The Direct2D backend for the Piet 2D graphics abstraction.

use direct2d::brush::SolidColorBrush;
use direct2d::math::Point2F;
use direct2d::brush::{Brush, GenericBrush, SolidColorBrush};
use direct2d::enums::{FigureBegin, FigureEnd};
use direct2d::geometry::path::{FigureBuilder, GeometryBuilder};
use direct2d::geometry::Path;
use direct2d::math::{BezierSegment, Point2F, QuadBezierSegment};
use direct2d::render_target::{GenericRenderTarget, RenderTarget};

use kurbo::Vec2;
use kurbo::{PathEl, Vec2};

use piet::{RenderContext, RoundFrom, RoundInto};

/// It is an interesting question, whether to wrap, or whether this should move into
/// piet proper under a feature. (We want to impl RenderContext for this, but we can't
/// impl RenderContext for GenericRenderTarget directly here due to coherence).
pub struct D2DRenderContext {
pub struct D2DRenderContext<'a> {
    factory: &'a direct2d::Factory,
    // This is an owned clone, but after some direct2d refactor, it's likely we'll
    // hold a mutable reference.
    rt: GenericRenderTarget,
}

impl D2DRenderContext {
    pub fn new<RT: RenderTarget>(rt: &RT) -> D2DRenderContext {
impl<'a> D2DRenderContext<'a> {
    pub fn new<RT: RenderTarget>(
        factory: &'a direct2d::Factory,
        rt: &'a mut RT,
    ) -> D2DRenderContext<'a> {
        D2DRenderContext {
            factory,
            rt: rt.as_generic(),
        }
    }


@@ 51,6 58,13 @@ impl RoundFrom<(f32, f32)> for Point2 {
    }
}

impl RoundFrom<(f64, f64)> for Point2 {
    #[inline]
    fn round_from(vec: (f64, f64)) -> Point2 {
        Point2(Point2F::new(vec.0 as f32, vec.1 as f32))
    }
}

impl RoundFrom<Vec2> for Point2 {
    #[inline]
    fn round_from(vec: Vec2) -> Point2 {


@@ 65,25 79,129 @@ impl From<Point2> for Vec2 {
    }
}

impl RenderContext for D2DRenderContext {
enum PathBuilder<'a> {
    Geom(GeometryBuilder<'a>),
    Fig(FigureBuilder<'a>),
}

impl<'a> PathBuilder<'a> {
    fn finish_figure(self) -> GeometryBuilder<'a> {
        match self {
            PathBuilder::Geom(g) => g,
            PathBuilder::Fig(f) => f.end(),
        }
    }
}

fn to_point2f<P: RoundInto<Point2>>(p: P) -> Point2F {
    p.round_into().0
}

fn path_from_iterator<I: IntoIterator<Item = PathEl>>(
    d2d: &direct2d::Factory,
    is_filled: bool,
    i: I,
) -> Path {
    let mut path = Path::create(d2d).unwrap();
    {
        let mut builder = Some(PathBuilder::Geom(path.open().unwrap()));
        for el in i.into_iter() {
            match el {
                PathEl::Moveto(p) => {
                    // TODO: we don't know this now. Will get fixed in direct2d crate.
                    let is_closed = is_filled;
                    if let Some(b) = builder.take() {
                        let g = b.finish_figure();
                        let begin = if is_filled {
                            FigureBegin::Filled
                        } else {
                            FigureBegin::Hollow
                        };
                        let end = if is_closed {
                            FigureEnd::Closed
                        } else {
                            FigureEnd::Open
                        };
                        let f = g.begin_figure(to_point2f(p), begin, end);
                        builder = Some(PathBuilder::Fig(f));
                    }
                }
                PathEl::Lineto(p) => {
                    if let Some(PathBuilder::Fig(f)) = builder.take() {
                        let f = f.add_line(to_point2f(p));
                        builder = Some(PathBuilder::Fig(f));
                    }
                }
                PathEl::Quadto(p1, p2) => {
                    if let Some(PathBuilder::Fig(f)) = builder.take() {
                        let q = QuadBezierSegment::new(to_point2f(p1), to_point2f(p2));
                        let f = f.add_quadratic_bezier(&q);
                        builder = Some(PathBuilder::Fig(f));
                    }
                }
                PathEl::Curveto(p1, p2, p3) => {
                    if let Some(PathBuilder::Fig(f)) = builder.take() {
                        let c = BezierSegment::new(to_point2f(p1), to_point2f(p2), to_point2f(p3));
                        let f = f.add_bezier(&c);
                        builder = Some(PathBuilder::Fig(f));
                    }
                }
                _ => (),
            }
        }
    }
    path
}

impl<'a> RenderContext for D2DRenderContext<'a> {
    type Point = Point2;
    type Coord = f32;
    type Brush = GenericBrush;
    type StrokeStyle = direct2d::stroke_style::StrokeStyle;

    fn clear(&mut self, rgb_color: u32) {
        self.rt.clear(rgb_color);
    fn clear(&mut self, rgb: u32) {
        self.rt.clear(rgb);
    }

    fn line<V: RoundInto<Point2>, C: RoundInto<f32>>(&mut self, p0: V, p1: V, width: C) {
        let brush = SolidColorBrush::create(&self.rt)
            .with_color(0x00_00_80)
    fn solid_brush(&mut self, rgba: u32) -> GenericBrush {
        SolidColorBrush::create(&self.rt)
            .with_color((rgba >> 8, ((rgba & 255) as f32) * (1.0 / 255.0)))
            .build()
            .unwrap();
            .unwrap()
            .to_generic() // This does an extra COM clone; avoid somehow?
    }

    fn line<V: RoundInto<Point2>, C: RoundInto<f32>>(
        &mut self,
        p0: V,
        p1: V,
        brush: &Self::Brush,
        width: C,
        style: Option<&Self::StrokeStyle>,
    ) {
        self.rt.draw_line(
            p0.round_into().0,
            p1.round_into().0,
            &brush,
            brush,
            width.round_into(),
            None,
            style,
        );
    }

    fn fill_path<I: IntoIterator<Item = PathEl>>(&mut self, iter: I, brush: &Self::Brush) {
        let path = path_from_iterator(self.factory, true, iter);
        self.rt.fill_geometry(&path, brush);
    }

    fn stroke_path<I: IntoIterator<Item = PathEl>, C: RoundInto<f32>>(
        &mut self,
        iter: I,
        brush: &Self::Brush,
        width: C,
        style: Option<&Self::StrokeStyle>,
    ) {
        let path = path_from_iterator(self.factory, false, iter);
        self.rt
            .draw_geometry(&path, brush, width.round_into(), style);
    }
}

M piet/src/traits.rs => piet/src/traits.rs +45 -3
@@ 1,14 1,39 @@
//! Fundamental graphics traits.

use kurbo::Vec2;
use kurbo::{PathEl, Vec2};

use crate::{RoundFrom, RoundInto};

pub trait RenderContext {
    /// Backends specify their own types for coordinates.
    type Point: Into<Vec2> + RoundFrom<Vec2>;
    /// The type of a 2D point, for this backend.
    ///
    /// Generally this needs to be a newtype so that the `RoundFrom` traits
    /// can be implemented on it. Possibly this can be relaxed in the future,
    /// as we move towards a standard `RoundFrom`.
    type Point: Into<Vec2> + RoundFrom<Vec2> + RoundFrom<(f32, f32)> + RoundFrom<(f64, f64)>;

    /// The type of 1D measurements, for example stroke width.
    ///
    /// Generally this will be either f32 or f64.
    type Coord: Into<f64> + RoundFrom<f64>;

    /// The type of a "brush".
    ///
    /// Initially just a solid RGBA color, but will probably expand to gradients.
    type Brush;

    /// Parameters for the style of stroke operations.
    type StrokeStyle;

    /// Create a new brush resource.
    ///
    /// TODO: figure out how to document lifetime and rebuilding requirements. Should
    /// that be the responsibility of the client, or should the back-end take
    /// responsiblity? We could have a cache that is flushed when the Direct2D
    /// render target is rebuilt. Solid brushes are super lightweight, but
    /// other potentially retained objects will be heavier.
    fn solid_brush(&mut self, rgba: u32) -> Self::Brush;

    /// Clear the canvas with the given color.
    fn clear(&mut self, rgb: u32);



@@ 16,6 41,23 @@ pub trait RenderContext {
        &mut self,
        p0: V,
        p1: V,
        brush: &Self::Brush,
        width: C,
        style: Option<&Self::StrokeStyle>,
    );

    /// Fill a path given as an iterator.
    ///
    /// I'm also thinking of retained paths. But do we want to have a separate object for
    /// retained paths, or do we want to have a lightweight display list abstraction, so
    /// at worst you record a single `fill_path` into that?
    fn fill_path<I: IntoIterator<Item = PathEl>>(&mut self, iter: I, brush: &Self::Brush);

    fn stroke_path<I: IntoIterator<Item = PathEl>, C: RoundInto<f32>>(
        &mut self,
        iter: I,
        brush: &Self::Brush,
        width: C,
        style: Option<&Self::StrokeStyle>,
    );
}