1use ravenrs_core::{Color, ImageData, Point, Rect};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
8#[non_exhaustive]
9pub enum PathSegment {
10 Move(Point),
12 Line(Point),
14 Cubic(Point, Point, Point),
16 Close,
18}
19
20#[derive(Clone, Debug, PartialEq)]
22pub struct Shape {
23 pub path: Vec<PathSegment>,
25 pub fill: Option<Color>,
27 pub stroke: Option<(f32, Color)>,
29}
30
31#[derive(Clone, Debug, PartialEq)]
34#[non_exhaustive]
35pub enum Layer {
36 Shape(Shape),
38 Gradient {
40 path: Vec<PathSegment>,
42 top: (f32, Color),
44 bottom: (f32, Color),
46 },
47 Dashed {
49 path: Vec<PathSegment>,
51 width: f32,
53 dash: f32,
55 color: Color,
57 },
58 Clipped {
61 rect: Rect,
63 layer: Box<Layer>,
65 },
66}
67
68fn outline_bounds(path: &[PathSegment]) -> Option<(f32, f32, f32, f32)> {
70 let mut found: Option<(f32, f32, f32, f32)> = None;
71 let mut grow = |p: &Point| {
72 let (x0, y0, x1, y1) = found.unwrap_or((p.x, p.y, p.x, p.y));
73 found = Some((x0.min(p.x), y0.min(p.y), x1.max(p.x), y1.max(p.y)));
74 };
75 for segment in path {
76 match segment {
77 PathSegment::Move(p) | PathSegment::Line(p) => grow(p),
78 PathSegment::Cubic(a, b, c) => {
79 grow(a);
80 grow(b);
81 grow(c);
82 }
83 PathSegment::Close => {}
84 }
85 }
86 found
87}
88
89fn inflated(bounds: (f32, f32, f32, f32), by: f32) -> Rect {
90 let (x0, y0, x1, y1) = bounds;
91 Rect::new(x0 - by, y0 - by, x1 - x0 + by * 2.0, y1 - y0 + by * 2.0)
92}
93
94impl Layer {
95 pub fn bounds(&self) -> Option<Rect> {
97 match self {
98 Self::Shape(shape) => {
99 let reach = shape.stroke.map_or(0.0, |(width, _)| width / 2.0);
100 outline_bounds(&shape.path).map(|b| inflated(b, reach + 1.0))
101 }
102 Self::Gradient { path, .. } => outline_bounds(path).map(|b| inflated(b, 1.0)),
103 Self::Dashed { path, width, .. } => outline_bounds(path).map(|b| inflated(b, width / 2.0 + 1.0)),
104 Self::Clipped { rect, layer } => {
105 let inner = layer.bounds()?;
106 let (x0, y0) = (rect.x().max(inner.x()), rect.y().max(inner.y()));
107 let (x1, y1) = (rect.max_x().min(inner.max_x()), rect.max_y().min(inner.max_y()));
108 (x1 > x0 && y1 > y0).then(|| Rect::new(x0, y0, x1 - x0, y1 - y0))
109 }
110 }
111 }
112
113 pub fn to_shape(&self) -> Shape {
115 match self {
116 Self::Shape(shape) => shape.clone(),
117 Self::Gradient { path, top, bottom } => {
118 let (a, b) = (top.1, bottom.1);
119 let middle =
120 Color::rgba((a.r + b.r) / 2.0, (a.g + b.g) / 2.0, (a.b + b.b) / 2.0, (a.a + b.a) / 2.0);
121 Shape::filled(path.clone(), middle)
122 }
123 Self::Dashed { path, width, color, .. } => {
124 Shape { path: path.clone(), fill: None, stroke: Some((*width, *color)) }
125 }
126 Self::Clipped { layer, .. } => layer.to_shape(),
127 }
128 }
129}
130
131const KAPPA: f32 = 0.552_284_8;
133
134impl Shape {
135 pub fn translated(&self, by: Point) -> Self {
137 let moved = |p: &Point| Point::new(p.x + by.x, p.y + by.y);
138 let path = self
139 .path
140 .iter()
141 .map(|segment| match segment {
142 PathSegment::Move(p) => PathSegment::Move(moved(p)),
143 PathSegment::Line(p) => PathSegment::Line(moved(p)),
144 PathSegment::Cubic(a, b, c) => PathSegment::Cubic(moved(a), moved(b), moved(c)),
145 PathSegment::Close => PathSegment::Close,
146 })
147 .collect();
148 Self { path, fill: self.fill, stroke: self.stroke }
149 }
150
151 pub fn filled(path: Vec<PathSegment>, fill: Color) -> Self {
153 Self { path, fill: Some(fill), stroke: None }
154 }
155}
156
157pub fn rectangle(rect: Rect, radius: f32) -> Vec<PathSegment> {
159 let (x0, x1, y0, y1) = (rect.x(), rect.max_x(), rect.y(), rect.max_y());
160 let mut path = Vec::with_capacity(5);
161 append_rectangle(&mut path, x0, y0, x1, y1, radius);
162 path
163}
164
165pub fn append_rectangle(path: &mut Vec<PathSegment>, x0: f32, y0: f32, x1: f32, y1: f32, radius: f32) {
167 let r = radius.clamp(0.0, (x1 - x0).min(y1 - y0) / 2.0);
168 let at = Point::new;
169 if r <= 0.0 {
170 path.extend([
171 PathSegment::Move(at(x0, y0)),
172 PathSegment::Line(at(x1, y0)),
173 PathSegment::Line(at(x1, y1)),
174 PathSegment::Line(at(x0, y1)),
175 PathSegment::Close,
176 ]);
177 return;
178 }
179 let k = r * (1.0 - KAPPA);
180 path.extend([
181 PathSegment::Move(at(x0 + r, y0)),
182 PathSegment::Line(at(x1 - r, y0)),
183 PathSegment::Cubic(at(x1 - k, y0), at(x1, y0 + k), at(x1, y0 + r)),
184 PathSegment::Line(at(x1, y1 - r)),
185 PathSegment::Cubic(at(x1, y1 - k), at(x1 - k, y1), at(x1 - r, y1)),
186 PathSegment::Line(at(x0 + r, y1)),
187 PathSegment::Cubic(at(x0 + k, y1), at(x0, y1 - k), at(x0, y1 - r)),
188 PathSegment::Line(at(x0, y0 + r)),
189 PathSegment::Cubic(at(x0, y0 + k), at(x0 + k, y0), at(x0 + r, y0)),
190 PathSegment::Close,
191 ]);
192}
193
194pub fn ellipse(rect: Rect) -> Vec<PathSegment> {
196 let center = rect.center();
197 let (cx, cy) = (center.x, center.y);
198 let (rx, ry) = (rect.width() / 2.0, rect.height() / 2.0);
199 let (kx, ky) = (rx * KAPPA, ry * KAPPA);
200 let at = Point::new;
201 vec![
202 PathSegment::Move(at(cx + rx, cy)),
203 PathSegment::Cubic(at(cx + rx, cy + ky), at(cx + kx, cy + ry), at(cx, cy + ry)),
204 PathSegment::Cubic(at(cx - kx, cy + ry), at(cx - rx, cy + ky), at(cx - rx, cy)),
205 PathSegment::Cubic(at(cx - rx, cy - ky), at(cx - kx, cy - ry), at(cx, cy - ry)),
206 PathSegment::Cubic(at(cx + kx, cy - ry), at(cx + rx, cy - ky), at(cx + rx, cy)),
207 PathSegment::Close,
208 ]
209}
210
211const MAX_SIDE: f32 = 4096.0;
213
214pub fn rasterize(shapes: &[Shape], rect: Rect, scale: f32) -> Option<ImageData> {
216 rasterize_all(shapes.iter().map(LayerRef::Shape), rect, scale)
217}
218
219pub fn rasterize_layers(layers: &[Layer], rect: Rect, scale: f32) -> Option<ImageData> {
221 rasterize_all(layers.iter().map(LayerRef::Layer), rect, scale)
222}
223
224enum LayerRef<'a> {
225 Shape(&'a Shape),
226 Layer(&'a Layer),
227}
228
229fn draw_shape(
230 pixmap: &mut tiny_skia::Pixmap,
231 shape: &Shape,
232 transform: tiny_skia::Transform,
233 mask: Option<&tiny_skia::Mask>,
234) {
235 let Some(path) = build_path(&shape.path) else { return };
236 if let Some(fill) = shape.fill {
237 let paint = paint_of(fill);
238 pixmap.fill_path(&path, &paint, tiny_skia::FillRule::Winding, transform, mask);
239 }
240 if let Some((width, color)) = shape.stroke {
241 let paint = paint_of(color);
242 pixmap.stroke_path(&path, &paint, &round_stroke(width), transform, mask);
243 }
244}
245
246fn round_stroke(width: f32) -> tiny_skia::Stroke {
247 tiny_skia::Stroke {
248 width,
249 line_join: tiny_skia::LineJoin::Round,
250 line_cap: tiny_skia::LineCap::Round,
251 ..tiny_skia::Stroke::default()
252 }
253}
254
255fn skia_color(color: Color) -> tiny_skia::Color {
256 tiny_skia::Color::from_rgba(
257 color.r.clamp(0.0, 1.0),
258 color.g.clamp(0.0, 1.0),
259 color.b.clamp(0.0, 1.0),
260 color.a.clamp(0.0, 1.0),
261 )
262 .unwrap_or(tiny_skia::Color::TRANSPARENT)
263}
264
265fn draw_layer(
266 pixmap: &mut tiny_skia::Pixmap,
267 layer: &Layer,
268 transform: tiny_skia::Transform,
269 mask: Option<&tiny_skia::Mask>,
270) {
271 match layer {
272 Layer::Shape(shape) => draw_shape(pixmap, shape, transform, mask),
273 Layer::Clipped { rect, layer } => {
274 let Some(mut own) = tiny_skia::Mask::new(pixmap.width(), pixmap.height()) else { return };
275 let Some(outline) = build_path(&rectangle(*rect, 0.0)) else { return };
276 own.fill_path(&outline, tiny_skia::FillRule::Winding, true, transform);
277 draw_layer(pixmap, layer, transform, Some(&own));
278 }
279 Layer::Gradient { path, top, bottom } => {
280 let Some(path) = build_path(path) else { return };
281 let stops = vec![
282 tiny_skia::GradientStop::new(0.0, skia_color(top.1)),
283 tiny_skia::GradientStop::new(1.0, skia_color(bottom.1)),
284 ];
285 let Some(shader) = tiny_skia::LinearGradient::new(
286 tiny_skia::Point::from_xy(0.0, top.0),
287 tiny_skia::Point::from_xy(0.0, bottom.0),
288 stops,
289 tiny_skia::SpreadMode::Pad,
290 tiny_skia::Transform::identity(),
291 ) else {
292 return;
293 };
294 let paint = tiny_skia::Paint { anti_alias: true, shader, ..tiny_skia::Paint::default() };
295 pixmap.fill_path(&path, &paint, tiny_skia::FillRule::Winding, transform, mask);
296 }
297 Layer::Dashed { path, width, dash, color } => {
298 let Some(path) = build_path(path) else { return };
299 let paint = paint_of(*color);
300 let stroke = tiny_skia::Stroke {
301 dash: tiny_skia::StrokeDash::new(vec![*dash, *dash], 0.0),
302 ..round_stroke(*width)
303 };
304 pixmap.stroke_path(&path, &paint, &stroke, transform, mask);
305 }
306 }
307}
308
309fn rasterize_all<'a>(
310 layers: impl Iterator<Item = LayerRef<'a>>,
311 rect: Rect,
312 scale: f32,
313) -> Option<ImageData> {
314 let scale = scale.min(MAX_SIDE / rect.width().max(1.0)).min(MAX_SIDE / rect.height().max(1.0));
315 let width = (rect.width() * scale).ceil().max(1.0) as u32;
316 let height = (rect.height() * scale).ceil().max(1.0) as u32;
317 let mut pixmap = tiny_skia::Pixmap::new(width, height)?;
318 let transform = tiny_skia::Transform::from_translate(-rect.x(), -rect.y()).post_scale(scale, scale);
319 for layer in layers {
320 match layer {
321 LayerRef::Shape(shape) => draw_shape(&mut pixmap, shape, transform, None),
322 LayerRef::Layer(layer) => draw_layer(&mut pixmap, layer, transform, None),
323 }
324 }
325 let mut pixels = pixmap.take();
327 for pixel in pixels.as_chunks_mut::<4>().0 {
328 let alpha = pixel[3];
329 if alpha != 0 && alpha != 255 {
330 for channel in &mut pixel[..3] {
331 *channel = (u16::from(*channel) * 255 / u16::from(alpha)).min(255) as u8;
332 }
333 }
334 }
335 ImageData::from_rgba8(width, height, pixels)
336}
337
338fn paint_of(color: Color) -> tiny_skia::Paint<'static> {
339 let mut paint = tiny_skia::Paint { anti_alias: true, ..tiny_skia::Paint::default() };
340 paint.set_color_rgba8(
341 (color.r.clamp(0.0, 1.0) * 255.0).round() as u8,
342 (color.g.clamp(0.0, 1.0) * 255.0).round() as u8,
343 (color.b.clamp(0.0, 1.0) * 255.0).round() as u8,
344 (color.a.clamp(0.0, 1.0) * 255.0).round() as u8,
345 );
346 paint
347}
348
349fn build_path(segments: &[PathSegment]) -> Option<tiny_skia::Path> {
350 let mut builder = tiny_skia::PathBuilder::new();
351 for segment in segments {
352 match segment {
353 PathSegment::Move(p) => builder.move_to(p.x, p.y),
354 PathSegment::Line(p) => builder.line_to(p.x, p.y),
355 PathSegment::Cubic(a, b, c) => builder.cubic_to(a.x, a.y, b.x, b.y, c.x, c.y),
356 PathSegment::Close => builder.close(),
357 }
358 }
359 builder.finish()
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 const RED: Color = Color::rgba(1.0, 0.0, 0.0, 1.0);
367
368 fn square() -> Shape {
369 Shape {
370 path: vec![
371 PathSegment::Move(Point::new(10.0, 10.0)),
372 PathSegment::Line(Point::new(20.0, 10.0)),
373 PathSegment::Line(Point::new(20.0, 20.0)),
374 PathSegment::Line(Point::new(10.0, 20.0)),
375 PathSegment::Close,
376 ],
377 fill: Some(RED),
378 stroke: None,
379 }
380 }
381
382 fn pixel(image: &ImageData, x: u32, y: u32) -> [u8; 4] {
383 let at = ((y * image.width() + x) * 4) as usize;
384 let p = &image.pixels()[at..at + 4];
385 [p[0], p[1], p[2], p[3]]
386 }
387
388 #[test]
389 fn shapes_land_where_the_page_says_at_the_scale() {
390 let image = rasterize(&[square()], Rect::new(10.0, 10.0, 20.0, 20.0), 2.0).expect("an image");
391 assert_eq!((image.width(), image.height()), (40, 40));
392 assert_eq!(pixel(&image, 10, 10), [255, 0, 0, 255], "inside");
393 assert_eq!(pixel(&image, 30, 30)[3], 0, "outside");
394 }
395
396 #[test]
397 fn translucent_colors_come_out_straight() {
398 let mut shape = square();
399 shape.fill = Some(Color::rgba(1.0, 0.0, 0.0, 0.5));
400 let image = rasterize(&[shape], Rect::new(10.0, 10.0, 20.0, 20.0), 1.0).expect("an image");
401 let [r, _, _, a] = pixel(&image, 5, 5);
402 assert!(r >= 254, "{r}");
403 assert!((126..=129).contains(&a), "{a}");
404 }
405
406 #[test]
407 fn a_huge_scale_is_held_back() {
408 let image = rasterize(&[square()], Rect::new(0.0, 0.0, 800.0, 800.0), 100.0).expect("an image");
409 assert!(image.width() <= 4096);
410 }
411
412 #[test]
413 fn a_gradient_goes_from_its_top_color_to_its_bottom_color() {
414 let layer = Layer::Gradient {
415 path: rectangle(Rect::new(0.0, 0.0, 10.0, 100.0), 0.0),
416 top: (0.0, Color::rgba(1.0, 0.0, 0.0, 0.8)),
417 bottom: (100.0, Color::rgba(1.0, 0.0, 0.0, 0.1)),
418 };
419 let image = rasterize_layers(&[layer], Rect::new(0.0, 0.0, 10.0, 100.0), 1.0).expect("an image");
420 let (high, low) = (pixel(&image, 5, 2)[3], pixel(&image, 5, 97)[3]);
421 assert!(high > 190 && low < 40, "{high} and {low}");
422 }
423
424 #[test]
425 fn a_dashed_line_has_gaps() {
426 let layer = Layer::Dashed {
427 path: vec![PathSegment::Move(Point::new(0.0, 5.0)), PathSegment::Line(Point::new(40.0, 5.0))],
428 width: 2.0,
429 dash: 8.0,
430 color: RED,
431 };
432 let image = rasterize_layers(&[layer], Rect::new(0.0, 0.0, 40.0, 10.0), 1.0).expect("an image");
433 assert!(pixel(&image, 4, 5)[3] > 200, "a dash");
434 assert_eq!(pixel(&image, 12, 5)[3], 0, "a gap");
435 }
436
437 #[test]
438 fn a_clipped_layer_shows_only_inside_its_rectangle() {
439 let area = Rect::new(0.0, 0.0, 40.0, 40.0);
440 let layer = Layer::Clipped {
441 rect: Rect::new(0.0, 0.0, 40.0, 15.0),
442 layer: Box::new(Layer::Shape(Shape::filled(rectangle(area, 0.0), RED))),
443 };
444 let image = rasterize_layers(&[layer], area, 1.0).expect("an image");
445 assert_eq!(pixel(&image, 5, 5)[3], 255, "inside");
446 assert_eq!(pixel(&image, 5, 30)[3], 0, "outside");
447 }
448
449 #[test]
450 fn an_empty_shape_is_skipped() {
451 let empty = Shape { path: Vec::new(), fill: Some(RED), stroke: None };
452 assert!(rasterize(&[empty], Rect::new(0.0, 0.0, 10.0, 10.0), 1.0).is_some());
453 }
454}