~raph/piet

ba7a2dabff9b45375a7e490601103a640004159b — Raph Levien 3 years ago 9544e65
Add Cairo

Add Cairo backend that can just render simple paths.

This also cleans up a few other things that were wrong.
M Cargo.toml => Cargo.toml +1 -0
@@ 2,5 2,6 @@

members = [
    "piet",
    "piet-cairo",
    "piet-direct2d",
]

A piet-cairo/Cargo.toml => piet-cairo/Cargo.toml +23 -0
@@ 0,0 1,23 @@
[package]
name = "piet-cairo"
version = "0.1.0"
authors = ["Raph Levien <raph.levien@gmail.com>"]
description = "Cairo backend for piet 2D graphics abstraction."
license = "MIT/Apache-2.0"
edition = "2018"
keywords = ["graphics", "2d"]
categories = ["rendering::graphics-api"]

[dependencies]
kurbo = "0.1.0"
piet = { path = "../piet" }

[dependencies.cairo-rs]
version = "0.5.0"
# We don't need glib
default-features = false

[dev-dependencies.cairo-rs]
version = "0.5.0"
features = ["png"]
default-features = false

A piet-cairo/examples/basic-cairo.rs => piet-cairo/examples/basic-cairo.rs +45 -0
@@ 0,0 1,45 @@
//! Basic example of rendering on Cairo.

use std::fs::File;

use kurbo::BezPath;

use cairo::{Context, Format, ImageSurface};

use piet::RenderContext;
use piet_cairo::CairoRenderContext;

const TEXTURE_WIDTH: i32 = 400;
const TEXTURE_HEIGHT: i32 = 200;

const HIDPI: f64 = 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() {
    let surface = ImageSurface::create(Format::ARgb32, TEXTURE_WIDTH, TEXTURE_HEIGHT)
        .expect("Can't create surface");
    let mut cr = Context::new(&surface);
    cr.scale(HIDPI, HIDPI);
    let mut piet_context = CairoRenderContext::new(&mut cr);
    draw_pretty_picture(&mut piet_context);
    let mut file = File::create("temp-cairo.png").expect("Couldn't create 'file.png'");
    surface.write_to_png(&mut file).expect("Error writing image file");
}

A piet-cairo/src/lib.rs => piet-cairo/src/lib.rs +141 -0
@@ 0,0 1,141 @@
//! The Cairo backend for the Piet 2D graphics abstraction.

use cairo::Context;

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

use piet::{RenderContext, RoundInto};

pub struct CairoRenderContext<'a> {
    // Cairo has this as Clone and with &self methods, but we do this to avoid
    // concurrency problems.
    ctx: &'a mut Context,
}

impl<'a> CairoRenderContext<'a> {
    pub fn new(ctx: &mut Context) -> CairoRenderContext {
        CairoRenderContext { ctx }
    }
}

pub enum Brush {
    Solid(u32),
}

pub enum StrokeStyle {
    // TODO: actual stroke style options
    Default,
}

impl<'a> RenderContext for CairoRenderContext<'a> {
    /// Cairo mostly uses raw f64, so this is as convenient as anything.
    type Point = Vec2;
    type Coord = f64;
    type Brush = Brush;
    type StrokeStyle = StrokeStyle;

    fn clear(&mut self, rgb: u32) {
        self.ctx.set_source_rgb(byte_to_frac(rgb >> 16), byte_to_frac(rgb >> 8), byte_to_frac(rgb));
        self.ctx.paint();
    }

    fn solid_brush(&mut self, rgba: u32) -> Brush {
        Brush::Solid(rgba)
    }

    fn line<V: RoundInto<Vec2>, C: RoundInto<f64>>(
        &mut self,
        p0: V,
        p1: V,
        brush: &Self::Brush,
        width: C,
        style: Option<&Self::StrokeStyle>,
    ) {
        self.ctx.new_path();
        let p0 = p0.round_into();
        let p1 = p1.round_into();
        self.ctx.move_to(p0.x, p0.y);
        self.ctx.line_to(p1.x, p1.y);
        self.set_stroke(width.round_into(), style);
        self.set_brush(brush);
        self.ctx.stroke();
    }

    fn fill_path<I: IntoIterator<Item = PathEl>>(&mut self, iter: I, brush: &Self::Brush) {
        self.set_path(iter);
        self.set_brush(brush);
        self.ctx.fill();
    }

    fn stroke_path<I: IntoIterator<Item = PathEl>, C: RoundInto<f64>>(
        &mut self,
        iter: I,
        brush: &Self::Brush,
        width: C,
        style: Option<&Self::StrokeStyle>,
    ) {
        self.set_path(iter);
        self.set_stroke(width.round_into(), style);
        self.set_brush(brush);
        self.ctx.stroke();
    }
}

impl<'a> CairoRenderContext<'a> {
    /// Set the source pattern to the brush.
    ///
    /// Cairo is super stateful, and we're trying to have more retained stuff.
    /// This is part of the impedance matching.
    fn set_brush(&mut self, brush: &Brush) {
        match *brush {
            Brush::Solid(rgba) =>
                self.ctx.set_source_rgba(byte_to_frac(rgba >> 24), byte_to_frac(rgba >> 16),
                    byte_to_frac(rgba >> 8), byte_to_frac(rgba))
        }
    }

    /// Set the stroke parameters.
    fn set_stroke(&mut self, width: f64, style: Option<&StrokeStyle>) {
        self.ctx.set_line_width(width);
        if let Some(style) = style {
            match style {
                // TODO: actual stroke style parameters
                StrokeStyle::Default => (),
            }
        }
    }

    fn set_path<I: IntoIterator<Item = PathEl>>(&mut self, iter: I) {
        // This shouldn't be necessary, we always leave the context in no-path
        // state. But just in case, and it should be harmless.
        self.ctx.new_path();
        let mut last = Vec2::default();
        for el in iter.into_iter() {
            match el {
                PathEl::Moveto(p) => {
                    self.ctx.move_to(p.x, p.y);
                    last = p;
                }
                PathEl::Lineto(p) => {
                    self.ctx.line_to(p.x, p.y);
                    last = p;
                }
                PathEl::Quadto(p1, p2) => {
                    let q = QuadBez::new(last, p1, p2);
                    let c = q.raise();
                    self.ctx.curve_to(c.p1.x, c.p1.y, c.p2.x, c.p2.y, p2.x, p2.y);
                    last = p2;
                }
                PathEl::Curveto(p1, p2, p3) => {
                    self.ctx.curve_to(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
                    last = p3;
                }
                PathEl::Closepath => self.ctx.close_path(),
            }
        }
    }
}

fn byte_to_frac(byte: u32) -> f64 {
    ((byte & 255) as f64) * (1.0 / 255.0)
}

M piet-direct2d/Cargo.toml => piet-direct2d/Cargo.toml +0 -1
@@ 9,7 9,6 @@ keywords = ["graphics", "2d"]
categories = ["rendering::graphics-api"]

[dependencies]
# TODO: don't check anything in with this dep
kurbo = "0.1.0"
piet = { path = "../piet" }


M piet/src/traits.rs => piet/src/traits.rs +3 -1
@@ 51,9 51,11 @@ pub trait RenderContext {
    /// 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?
    ///
    /// TODO: this is missing fill rule.
    fn fill_path<I: IntoIterator<Item = PathEl>>(&mut self, iter: I, brush: &Self::Brush);

    fn stroke_path<I: IntoIterator<Item = PathEl>, C: RoundInto<f32>>(
    fn stroke_path<I: IntoIterator<Item = PathEl>, C: RoundInto<Self::Coord>>(
        &mut self,
        iter: I,
        brush: &Self::Brush,