use std::collections::HashMap;
use smudge_core::app::FrameOutput;
use smudge_core::geometry::{Color, Corners, Rect};
use smudge_core::paint::{DisplayItem, FontBlob, PaintOp};
use vello::kurbo::{Affine, BezPath, Rect as KurboRect, RoundedRect, RoundedRectRadii, Shape};
use vello::peniko::{Blob, Color as PenikoColor, Fill, FontData};
use vello::{Glyph, Scene};
pub(crate) struct Encoder {
scene: Scene,
fonts: HashMap<(u64, u32), Option<FontData>>,
ring: BezPath,
offsets: Vec<Affine>,
}
impl Encoder {
pub(crate) fn new() -> Encoder {
Encoder {
scene: Scene::new(),
fonts: HashMap::new(),
ring: BezPath::new(),
offsets: Vec::new(),
}
}
pub(crate) fn encode(&mut self, frame: &FrameOutput<'_>) {
self.scene.reset();
let transform = Affine::scale(f64::from(frame.scale));
let mut depth = 0usize;
let mut offset = Affine::IDENTITY;
self.offsets.clear();
for item in frame.list.items() {
match item {
DisplayItem::Op(op) => encode_op(
&mut self.scene,
&mut self.fonts,
&mut self.ring,
transform * offset,
op,
),
DisplayItem::PushClip(rect) => {
depth += 1;
self.scene.push_clip_layer(
Fill::NonZero,
transform * offset,
&kurbo_rect(*rect),
);
}
DisplayItem::PopClip => {
if depth > 0 {
depth -= 1;
self.scene.pop_layer();
}
}
DisplayItem::PushOffset(delta) => {
self.offsets.push(offset);
offset *= Affine::translate((f64::from(delta.x), f64::from(delta.y)));
}
DisplayItem::PopOffset => {
offset = self.offsets.pop().unwrap_or(offset);
}
}
}
for _ in 0..depth {
self.scene.pop_layer();
}
}
pub(crate) fn scene(&self) -> &Scene {
&self.scene
}
}
fn encode_op(
scene: &mut Scene,
fonts: &mut HashMap<(u64, u32), Option<FontData>>,
ring: &mut BezPath,
transform: Affine,
op: &PaintOp,
) {
match *op {
PaintOp::FillRect {
rect,
radius,
color,
} => fill(scene, transform, color, rect, radius),
PaintOp::StrokeRect {
rect,
radius,
width,
color,
} => {
if !width.is_finite() || width <= 0.0 {
return;
}
match inset(rect, radius, width) {
Some((inner, inner_radius)) => {
ring.truncate(0);
ring.extend(rounded(rect, radius).path_elements(PATH_TOLERANCE));
ring.extend(rounded(inner, inner_radius).path_elements(PATH_TOLERANCE));
scene.fill(Fill::EvenOdd, transform, peniko_color(color), None, &*ring);
}
None => fill(scene, transform, color, rect, radius),
}
}
PaintOp::Glyphs(ref run) => {
if !run.size.is_finite() || run.size <= 0.0 {
return;
}
let Some(font) = font_data(fonts, &run.font) else {
return;
};
scene
.draw_glyphs(font)
.transform(transform)
.font_size(run.size)
.brush(peniko_color(run.color))
.draw(
Fill::NonZero,
run.glyphs.iter().map(|glyph| Glyph {
id: glyph.id,
x: glyph.x,
y: glyph.y,
}),
);
}
}
}
const PATH_TOLERANCE: f64 = 0.01;
fn rounded(rect: Rect, radius: Corners) -> RoundedRect {
RoundedRect::from_rect(kurbo_rect(rect), kurbo_radii(radius))
}
fn fill(scene: &mut Scene, transform: Affine, color: Color, rect: Rect, radius: Corners) {
let color = peniko_color(color);
if radius == Corners::ZERO {
scene.fill(Fill::NonZero, transform, color, None, &kurbo_rect(rect));
} else {
let shape = RoundedRect::from_rect(kurbo_rect(rect), kurbo_radii(radius));
scene.fill(Fill::NonZero, transform, color, None, &shape);
}
}
fn inset(rect: Rect, radius: Corners, inset: f32) -> Option<(Rect, Corners)> {
let rect = Rect::new(
rect.x0 + inset,
rect.y0 + inset,
rect.x1 - inset,
rect.y1 - inset,
);
if !(rect.width() > 0.0 && rect.height() > 0.0) {
return None;
}
let radius = Corners {
top_left: (radius.top_left - inset).max(0.0),
top_right: (radius.top_right - inset).max(0.0),
bottom_right: (radius.bottom_right - inset).max(0.0),
bottom_left: (radius.bottom_left - inset).max(0.0),
};
Some((rect, radius))
}
fn font_data<'a>(
fonts: &'a mut HashMap<(u64, u32), Option<FontData>>,
blob: &FontBlob,
) -> Option<&'a FontData> {
fonts
.entry((blob.id, blob.index))
.or_insert_with(|| {
let bytes: &[u8] = (*blob.data).as_ref();
skrifa::FontRef::from_index(bytes, blob.index)
.is_ok()
.then(|| FontData::new(Blob::new(blob.data.clone()), blob.index))
})
.as_ref()
}
pub(crate) fn peniko_color(color: Color) -> PenikoColor {
PenikoColor::from_rgba8(color.r, color.g, color.b, color.a)
}
fn kurbo_rect(rect: Rect) -> KurboRect {
KurboRect::new(
f64::from(rect.x0),
f64::from(rect.y0),
f64::from(rect.x1),
f64::from(rect.y1),
)
}
fn kurbo_radii(radius: Corners) -> RoundedRectRadii {
RoundedRectRadii::new(
f64::from(radius.top_left.max(0.0)),
f64::from(radius.top_right.max(0.0)),
f64::from(radius.bottom_right.max(0.0)),
f64::from(radius.bottom_left.max(0.0)),
)
}