1use std::collections::BTreeMap;
2
3use kurbo::{Affine, BezPath, PathEl, Shape};
4use oxframe_2d::{
5 DrawItem, Layer, LineCap, LineJoin, Paint as MPaint, Primitive, RasterImage, SceneGraph,
6 StrokeStyle,
7};
8use oxframe_core::{BlendMode, Color, Gradient, GradientKind};
9use tiny_skia::{
10 FillRule, FilterQuality, GradientStop, LinearGradient, Mask, Paint, PathBuilder, Pixmap,
11 PixmapPaint, RadialGradient, SpreadMode, Stroke, StrokeDash, Transform,
12};
13
14use crate::blur::gaussian_blur;
15use crate::{Image, RenderOptions, Renderer, scene_to_pixels};
16
17#[derive(Default)]
19pub struct CpuRenderer {
20 pixmap: Option<Pixmap>,
21 images: BTreeMap<u64, Pixmap>,
22 pool: Vec<Pixmap>,
24}
25
26impl CpuRenderer {
27 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn render_items(&mut self, items: &[DrawItem], view: Affine, w: u32, h: u32) -> Pixmap {
35 let mut target = Pixmap::new(w.max(1), h.max(1)).expect("valid pixmap size");
36 self.draw_items(&mut target, items, view);
37 target
38 }
39
40 fn take_layer(&mut self, w: u32, h: u32) -> Pixmap {
41 let (w, h) = (w.max(1), h.max(1));
42 match self
43 .pool
44 .iter()
45 .position(|p| p.width() == w && p.height() == h)
46 {
47 Some(i) => {
48 let mut p = self.pool.swap_remove(i);
49 p.fill(tiny_skia::Color::TRANSPARENT);
50 p
51 }
52 None => Pixmap::new(w, h).expect("valid pixmap size"),
53 }
54 }
55
56 fn draw_items(&mut self, base: &mut Pixmap, items: &[DrawItem], view: Affine) {
57 let full = Region {
58 x: 0,
59 y: 0,
60 w: base.width(),
61 h: base.height(),
62 };
63 let mut stack: Vec<(Pixmap, Region, Layer, Affine)> = Vec::new();
65 for (i, item) in items.iter().enumerate() {
66 let tf = view * item.transform;
67 match &item.prim {
68 Primitive::PushLayer(layer) => {
69 let parent = stack.last().map_or(full, |(_, r, _, _)| *r);
72 let region = layer_region(items, i, view, parent).unwrap_or(Region {
73 x: parent.x,
74 y: parent.y,
75 w: 1,
76 h: 1,
77 });
78 let buf = self.take_layer(region.w, region.h);
79 stack.push((buf, region, layer.clone(), tf));
80 }
81 Primitive::PopLayer => {
82 let Some((buf, region, layer, ltf)) = stack.pop() else {
83 continue;
84 };
85 let (dst, dst_region) = match stack.last_mut() {
86 Some((p, r, _, _)) => (p, *r),
87 None => (&mut *base, full),
88 };
89 composite(dst, dst_region, buf, region, &layer, ltf, &mut self.pool);
90 }
91 prim => {
92 let (dst, region) = match stack.last_mut() {
93 Some((p, r, _, _)) => (p, *r),
94 None => (&mut *base, full),
95 };
96 draw_prim(dst, prim, region.shift() * tf, &mut self.images);
97 }
98 }
99 }
100 while let Some((buf, region, layer, ltf)) = stack.pop() {
102 let (dst, dst_region) = match stack.last_mut() {
103 Some((p, r, _, _)) => (p, *r),
104 None => (&mut *base, full),
105 };
106 composite(dst, dst_region, buf, region, &layer, ltf, &mut self.pool);
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113struct Region {
114 x: i32,
115 y: i32,
116 w: u32,
117 h: u32,
118}
119
120impl Region {
121 fn shift(self) -> Affine {
123 Affine::translate((-f64::from(self.x), -f64::from(self.y)))
124 }
125}
126
127fn layer_region(items: &[DrawItem], start: usize, view: Affine, within: Region) -> Option<Region> {
130 let mut bb: Option<kurbo::Rect> = None;
131 let mut grow = 0.0f64;
132 let mut depth = 0usize;
133 for item in &items[start..] {
134 let tf = view * item.transform;
135 let r = match &item.prim {
136 Primitive::PushLayer(l) => {
137 depth += 1;
138 let k = scale_of(tf);
139 grow += 3.0 * l.blur * k;
140 if let Some(sh) = &l.shadow {
141 let off = linear(tf) * sh.offset.to_point();
142 grow += 3.0 * sh.blur * k + off.x.abs().max(off.y.abs());
143 }
144 None
145 }
146 Primitive::PopLayer => {
147 depth -= 1;
148 if depth == 0 {
149 break;
150 }
151 None
152 }
153 Primitive::Fill { path, .. } => Some(tf.transform_rect_bbox(path.bounding_box())),
154 Primitive::Stroke { path, style, .. } => {
155 let pad = style.width * scale_of(tf);
156 Some(
157 tf.transform_rect_bbox(path.bounding_box())
158 .inflate(pad, pad),
159 )
160 }
161 Primitive::Image { rect, .. } => Some(tf.transform_rect_bbox(*rect)),
162 };
163 if let Some(r) = r {
164 bb = Some(bb.map_or(r, |b| b.union(r)));
165 }
166 }
167 let r = bb?.inflate(grow + 2.0, grow + 2.0);
168 let x0 = r.x0.floor().max(f64::from(within.x)) as i32;
169 let y0 = r.y0.floor().max(f64::from(within.y)) as i32;
170 let x1 = r.x1.ceil().min(f64::from(within.x) + f64::from(within.w)) as i32;
171 let y1 = r.y1.ceil().min(f64::from(within.y) + f64::from(within.h)) as i32;
172 (x1 > x0 && y1 > y0).then(|| Region {
173 x: x0,
174 y: y0,
175 w: (x1 - x0) as u32,
176 h: (y1 - y0) as u32,
177 })
178}
179
180fn scale_of(tf: Affine) -> f64 {
181 tf.determinant().abs().sqrt()
182}
183
184fn composite(
185 dst: &mut Pixmap,
186 dst_region: Region,
187 mut buf: Pixmap,
188 region: Region,
189 layer: &Layer,
190 tf: Affine,
191 pool: &mut Vec<Pixmap>,
192) {
193 let (w, h) = (buf.width() as usize, buf.height() as usize);
194 let k = scale_of(tf);
195 if let Some(clip) = &layer.clip
196 && let Some(path) = to_skia_path(clip)
197 && let Some(mut mask) = Mask::new(buf.width(), buf.height())
198 {
199 mask.fill_path(
200 &path,
201 FillRule::Winding,
202 true,
203 to_skia_transform(region.shift() * tf),
204 );
205 buf.apply_mask(&mask);
206 }
207 let (dx, dy) = (region.x - dst_region.x, region.y - dst_region.y);
209 if layer.blur > 0.0 {
210 gaussian_blur(buf.data_mut(), w, h, layer.blur * k);
211 }
212 let paint = PixmapPaint {
213 opacity: layer.opacity.clamp(0.0, 1.0) as f32,
214 blend_mode: blend_mode(layer.blend),
215 quality: FilterQuality::Nearest,
216 };
217 if let Some(shadow) = &layer.shadow {
218 let mut sh = buf.clone();
220 let [r, g, b, a] = shadow.color.to_rgba8().map(u32::from);
221 for px in sh.data_mut().chunks_exact_mut(4) {
222 let cov = u32::from(px[3]) * a / 255;
223 px[0] = (r * cov / 255) as u8;
224 px[1] = (g * cov / 255) as u8;
225 px[2] = (b * cov / 255) as u8;
226 px[3] = cov as u8;
227 }
228 gaussian_blur(sh.data_mut(), w, h, shadow.blur * k);
229 let off = linear(tf) * shadow.offset.to_point();
230 dst.draw_pixmap(
231 dx,
232 dy,
233 sh.as_ref(),
234 &paint,
235 Transform::from_translate(off.x as f32, off.y as f32),
236 None,
237 );
238 pool.push(sh);
239 }
240 dst.draw_pixmap(dx, dy, buf.as_ref(), &paint, Transform::identity(), None);
241 pool.push(buf);
242}
243
244fn linear(tf: Affine) -> Affine {
246 let [a, b, c, d, _, _] = tf.as_coeffs();
247 Affine::new([a, b, c, d, 0.0, 0.0])
248}
249
250pub(crate) fn blend_mode(b: BlendMode) -> tiny_skia::BlendMode {
251 use tiny_skia::BlendMode as T;
252 match b {
253 BlendMode::Normal => T::SourceOver,
254 BlendMode::Multiply => T::Multiply,
255 BlendMode::Screen => T::Screen,
256 BlendMode::Overlay => T::Overlay,
257 BlendMode::Darken => T::Darken,
258 BlendMode::Lighten => T::Lighten,
259 BlendMode::ColorDodge => T::ColorDodge,
260 BlendMode::ColorBurn => T::ColorBurn,
261 BlendMode::HardLight => T::HardLight,
262 BlendMode::SoftLight => T::SoftLight,
263 BlendMode::Difference => T::Difference,
264 BlendMode::Exclusion => T::Exclusion,
265 BlendMode::Hue => T::Hue,
266 BlendMode::Saturation => T::Saturation,
267 BlendMode::Color => T::Color,
268 BlendMode::Luminosity => T::Luminosity,
269 }
270}
271
272fn draw_prim(dst: &mut Pixmap, prim: &Primitive, tf: Affine, images: &mut BTreeMap<u64, Pixmap>) {
273 match prim {
274 Primitive::Fill { path, paint } => {
275 if let (Some(path), Some(p)) = (to_skia_path(path), skia_paint(paint)) {
276 dst.fill_path(&path, &p, FillRule::Winding, to_skia_transform(tf), None);
277 }
278 }
279 Primitive::Stroke { path, paint, style } => {
280 if let (Some(path), Some(p)) = (to_skia_path(path), skia_paint(paint)) {
281 dst.stroke_path(&path, &p, &skia_stroke(style), to_skia_transform(tf), None);
282 }
283 }
284 Primitive::Image {
285 image,
286 rect,
287 opacity,
288 } => {
289 let src = match images.entry(image.id) {
290 std::collections::btree_map::Entry::Occupied(e) => e.into_mut(),
291 std::collections::btree_map::Entry::Vacant(e) => {
292 let Some(pm) = to_pixmap(image) else { return };
293 e.insert(pm)
294 }
295 };
296 let place = tf
297 * Affine::translate((rect.x0, rect.y0))
298 * Affine::scale_non_uniform(
299 rect.width() / f64::from(image.width),
300 rect.height() / f64::from(image.height),
301 );
302 let paint = PixmapPaint {
303 opacity: *opacity as f32,
304 quality: FilterQuality::Bicubic,
305 ..Default::default()
306 };
307 dst.draw_pixmap(0, 0, src.as_ref(), &paint, to_skia_transform(place), None);
308 }
309 Primitive::PushLayer(_) | Primitive::PopLayer => {}
310 }
311}
312
313pub(crate) fn to_skia_path(path: &BezPath) -> Option<tiny_skia::Path> {
314 let mut pb = PathBuilder::new();
315 for el in path.elements() {
316 match *el {
317 PathEl::MoveTo(p) => pb.move_to(p.x as f32, p.y as f32),
318 PathEl::LineTo(p) => pb.line_to(p.x as f32, p.y as f32),
319 PathEl::QuadTo(a, p) => pb.quad_to(a.x as f32, a.y as f32, p.x as f32, p.y as f32),
320 PathEl::CurveTo(a, b, p) => pb.cubic_to(
321 a.x as f32, a.y as f32, b.x as f32, b.y as f32, p.x as f32, p.y as f32,
322 ),
323 PathEl::ClosePath => pb.close(),
324 }
325 }
326 pb.finish()
327}
328
329fn to_skia_transform(a: Affine) -> Transform {
330 let [a, b, c, d, e, f] = a.as_coeffs().map(|v| v as f32);
331 Transform::from_row(a, b, c, d, e, f)
332}
333
334fn skia_color(c: Color) -> Option<tiny_skia::Color> {
335 tiny_skia::Color::from_rgba(
336 c.r.clamp(0.0, 1.0),
337 c.g.clamp(0.0, 1.0),
338 c.b.clamp(0.0, 1.0),
339 c.a.clamp(0.0, 1.0),
340 )
341}
342
343fn shader(g: &Gradient) -> Option<tiny_skia::Shader<'static>> {
344 let stops: Vec<GradientStop> = g
345 .stops
346 .iter()
347 .filter_map(|(o, c)| Some(GradientStop::new(*o as f32, skia_color(*c)?)))
348 .collect();
349 let pt = |p: kurbo::Point| tiny_skia::Point::from_xy(p.x as f32, p.y as f32);
350 match g.kind {
351 GradientKind::Linear { from, to } => LinearGradient::new(
352 pt(from),
353 pt(to),
354 stops,
355 SpreadMode::Pad,
356 Transform::identity(),
357 ),
358 GradientKind::Radial { center, radius } => RadialGradient::new(
359 pt(center),
360 0.0,
361 pt(center),
362 radius.max(1e-3) as f32,
363 stops,
364 SpreadMode::Pad,
365 Transform::identity(),
366 ),
367 }
368}
369
370fn skia_paint(p: &MPaint) -> Option<Paint<'static>> {
371 let mut paint = Paint::default();
372 match p {
373 MPaint::Solid(c) => paint.set_color(skia_color(*c)?),
374 MPaint::Gradient(g) => paint.shader = shader(g)?,
375 }
376 paint.anti_alias = true;
377 Some(paint)
378}
379
380fn skia_stroke(s: &StrokeStyle) -> Stroke {
381 Stroke {
382 width: s.width as f32,
383 line_cap: match s.cap {
384 LineCap::Butt => tiny_skia::LineCap::Butt,
385 LineCap::Round => tiny_skia::LineCap::Round,
386 LineCap::Square => tiny_skia::LineCap::Square,
387 },
388 line_join: match s.join {
389 LineJoin::Miter => tiny_skia::LineJoin::Miter,
390 LineJoin::Round => tiny_skia::LineJoin::Round,
391 LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
392 },
393 dash: (s.dash.len() >= 2 && s.dash.iter().any(|d| *d > 0.0))
394 .then(|| {
395 let mut d: Vec<f32> = s.dash.iter().map(|v| *v as f32).collect();
397 if d.len() % 2 == 1 {
398 d.extend_from_within(..);
399 }
400 StrokeDash::new(d, s.dash_offset as f32)
401 })
402 .flatten(),
403 ..Default::default()
404 }
405}
406
407fn to_pixmap(img: &RasterImage) -> Option<Pixmap> {
408 let mut pm = Pixmap::new(img.width, img.height)?;
409 for (dst, src) in pm.pixels_mut().iter_mut().zip(img.rgba.chunks_exact(4)) {
410 *dst = tiny_skia::ColorU8::from_rgba(src[0], src[1], src[2], src[3]).premultiply();
411 }
412 Some(pm)
413}
414
415pub(crate) fn demultiply(pixmap: &Pixmap) -> Vec<u8> {
417 let mut data = Vec::with_capacity(pixmap.data().len());
418 for px in pixmap.pixels() {
419 let c = px.demultiply();
420 data.extend_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
421 }
422 data
423}
424
425impl Renderer for CpuRenderer {
426 fn render(&mut self, graph: &SceneGraph, options: &RenderOptions) -> Image {
427 let (w, h) = (options.width.max(1), options.height.max(1));
428 let mut base = match self.pixmap.take() {
429 Some(p) if p.width() == w && p.height() == h => p,
430 _ => Pixmap::new(w, h).expect("valid pixmap size"),
431 };
432 let bg = options.background;
433 base.fill(skia_color(bg).unwrap_or(tiny_skia::Color::BLACK));
434 let view = scene_to_pixels(graph.size(), (w, h));
435 let items = graph.draw_list();
436 self.draw_items(&mut base, &items, view);
437 let data = demultiply(&base);
438 self.pixmap = Some(base);
439 Image {
440 width: w,
441 height: h,
442 data,
443 }
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use oxframe_2d::{NodeBuilder, Rect, View};
450
451 use super::*;
452
453 #[test]
454 fn draws_centered_rect() {
455 let graph = SceneGraph::new(100, 100);
456 graph.enter(|| {
457 View::current().add(
458 Rect::new()
459 .size(50.0)
460 .fill(Color::from_hex("#ff0000").unwrap()),
461 );
462 });
463 let img = CpuRenderer::new().render(
464 &graph,
465 &RenderOptions {
466 width: 100,
467 height: 100,
468 background: Color::BLACK,
469 },
470 );
471 let px = |x: usize, y: usize| &img.data[(y * 100 + x) * 4..][..4];
472 assert_eq!(px(50, 50), [255, 0, 0, 255]);
473 assert_eq!(px(10, 10), [0, 0, 0, 255]);
474 assert_eq!(px(74, 50), [255, 0, 0, 255]);
475 assert_eq!(px(76, 50), [0, 0, 0, 255]);
476 }
477}