Skip to main content

wassily_core/
shape.rs

1//! # Shape Builder
2//!
3//! A builder for creating shapes. `Shape` is the main way in which paths are built
4//! and drawn to the `Canvas`. The basic idea is to provide a list of points that
5//! are connected by lines or bezier curves. The shape can then be filled and/or stroked.
6//! Invoke the draw method to finish the build and draw the shape to a `Canvas`.
7//!
8//! ## Basic Usage
9//!
10//! The Shape builder follows a fluent API pattern:
11//! 1. Create a new shape with `Shape::new()`
12//! 2. Add geometry (circle, rectangle, polygon, etc.)
13//! 3. Set styling (fill color, stroke color, stroke weight)
14//! 4. Apply transformations (rotate, scale, translate)
15//! 5. Draw to canvas with `.draw(canvas)`
16//!
17//! ## Available Shapes
18//!
19//! - **Basic Shapes**: `circle()`, `ellipse()`, `rect()`, `line()`
20//! - **Polygons**: `polygon()`, `star()`, `regular_polygon()`
21//! - **Paths**: `polyline()`, `bezier_curve()`, `quad_curve()`
22//! - **Procedural**: `pearl()` (randomized polygon)
23/*!
24
25```no_run
26use wassily_core::*;
27use tiny_skia::Color;
28
29    let mut canvas = Canvas::new(500, 500);
30    canvas.fill(Color::from_rgba8(100, 149, 237, 255));
31    let pos = center(500, 500);
32    Shape::new()
33        .star(pos, 100.0, 175.0, 8)
34        .fill_color(Color::from_rgba8(173, 255, 47, 255))
35        .stroke_color(Color::from_rgba8(25, 25, 112, 255))
36        .stroke_weight(3.0)
37        .draw(&mut canvas);
38    canvas.save_png("./star.png");
39```
40Produces:
41
42
43<img src="https://raw.githubusercontent.com/jeffreyrosenbluth/wassily/main/assets/star.png" alt="Star image" width="400"/>
44 */
45
46use crate::{
47    canvas::Canvas,
48    points::{pt, TAU},
49    util::{chaiken, Trail},
50};
51use log::error;
52use num_traits::AsPrimitive;
53// use palette::{IntoColor, FromColor}; // TODO: Re-enable color conversion
54use rand::RngCore;
55use rand_distr::{Distribution, Normal};
56use tiny_skia::{
57    FillRule, LineCap, LineJoin, Paint, PathBuilder, Point, Rect, Stroke, StrokeDash, Transform,
58};
59
60/// Shape types
61#[derive(Debug, Clone, Copy)]
62enum ShapeType {
63    Poly,
64    PolyQuad,
65    PolyCubic,
66    Rect,
67    Circle,
68    Line,
69    Ellipse,
70}
71
72/// ShapeData holds the data required to draw a shape.
73#[derive(Debug, Clone)]
74struct ShapeData<'a> {
75    points: Vec<Point>,
76    fill_paint: Box<Option<Paint<'a>>>,
77    stroke: Stroke,
78    stroke_paint: Box<Option<Paint<'a>>>,
79    shape: ShapeType,
80    fillrule: FillRule,
81    transform: Transform,
82}
83
84impl<'a> ShapeData<'a> {
85    pub(crate) fn new(
86        points: Vec<Point>,
87        fill_paint: Box<Option<Paint<'a>>>,
88        stroke: Stroke,
89        stroke_paint: Box<Option<Paint<'a>>>,
90        shape: ShapeType,
91        fillrule: FillRule,
92        transform: Transform,
93    ) -> Self {
94        Self {
95            points,
96            fill_paint,
97            stroke,
98            stroke_paint,
99            shape,
100            fillrule,
101            transform,
102        }
103    }
104
105    fn draw(&self, canvas: &mut Canvas) {
106        let shape = self.shape;
107        match shape {
108            ShapeType::Poly => self.draw_poly(canvas),
109            ShapeType::PolyQuad => self.draw_quad(canvas),
110            ShapeType::PolyCubic => self.draw_cubic(canvas),
111            ShapeType::Rect => self.draw_rect(canvas),
112            ShapeType::Circle => self.draw_circle(canvas),
113            ShapeType::Line => self.draw_line(canvas),
114            ShapeType::Ellipse => self.draw_ellipse(canvas),
115        }
116    }
117
118    // Draw a polygon. Must have at least 2 points.
119    fn draw_poly(&self, canvas: &mut Canvas) {
120        if self.points.len() < 2 {
121            error!(
122                "Cannot draw a polygonal curve with less than 2 points, only {} points provided.",
123                self.points.len()
124            );
125            return;
126        }
127        let mut pb = PathBuilder::new();
128        let head = self.points[0];
129        let tail = &self.points[1..];
130        pb.move_to(head.x, head.y);
131        for p in tail {
132            pb.line_to(p.x, p.y);
133        }
134        if self.fill_paint.is_some() {
135            pb.close();
136        }
137        let path = pb.finish().unwrap();
138        if let Some(mut fp) = *self.fill_paint.clone() {
139            canvas.fill_path(&path, &mut fp, self.fillrule, self.transform, None);
140        }
141        if let Some(mut sp) = *self.stroke_paint.clone() {
142            canvas.stroke_path(&path, &mut sp, &self.stroke, self.transform, None);
143        }
144    }
145
146    // Draw a quadratic bezier curve. Must have at least 3 points.
147    fn draw_quad(&self, canvas: &mut Canvas) {
148        if self.points.len() < 3 {
149            error!(
150                "Cannot draw a quadratic bezier curve with less than 3 points, only {} points provided.",
151                self.points.len()
152            );
153            return;
154        }
155        let mut pb = PathBuilder::new();
156        let head = self.points[0];
157        pb.move_to(head.x, head.y);
158        let tail = self.points[1..].chunks_exact(2);
159        for t in tail {
160            let control = t[0];
161            let p = t[1];
162            pb.quad_to(control.x, control.y, p.x, p.y);
163        }
164        if self.fill_paint.is_some() {
165            pb.close();
166        }
167        let path = pb.finish().unwrap();
168        if let Some(mut fp) = *self.fill_paint.clone() {
169            canvas.fill_path(&path, &mut fp, self.fillrule, self.transform, None);
170        }
171        if let Some(mut sp) = *self.stroke_paint.clone() {
172            canvas.stroke_path(&path, &mut sp, &self.stroke, self.transform, None);
173        }
174    }
175
176    // Draw a cubic bezier curve. Must have at least 4 points. Note that the
177    // first and last points are the endpoints, the middle two points are control
178    // points for every group of 4 points.
179    fn draw_cubic(&self, canvas: &mut Canvas) {
180        if self.points.len() < 4 {
181            error!(
182                "Cannot draw a cubic bezier curve with less than 4 points, only {} points provided.",
183                self.points.len()
184            );
185            return;
186        }
187        let mut pb = PathBuilder::new();
188        let head = self.points[0];
189        pb.move_to(head.x, head.y);
190        let tail = self.points[1..].chunks_exact(3);
191        for t in tail {
192            let control1 = t[0];
193            let control2 = t[1];
194            let p = t[2];
195            pb.cubic_to(control1.x, control1.y, control2.x, control2.y, p.x, p.y);
196        }
197        if self.fill_paint.is_some() {
198            pb.close();
199        }
200        let path = pb.finish().unwrap();
201        if let Some(mut fp) = *self.fill_paint.clone() {
202            canvas.fill_path(&path, &mut fp, self.fillrule, self.transform, None);
203        }
204        if let Some(mut sp) = *self.stroke_paint.clone() {
205            canvas.stroke_path(&path, &mut sp, &self.stroke, self.transform, None);
206        }
207    }
208
209    // Draw a rectangle. Must have at least 2 points, top left and bottom right.
210    fn draw_rect(&self, canvas: &mut Canvas) {
211        if self.points.len() < 2 {
212            error!(
213                "Cannot draw a rectangle with less than 2 points, only {} points provided.",
214                self.points.len()
215            );
216            return;
217        }
218        let left = self.points[0].x;
219        let top = self.points[0].y;
220        let right = self.points[1].x;
221        let bottom = self.points[1].y;
222        let rect = Rect::from_ltrb(left, top, right, bottom).unwrap();
223        let path = PathBuilder::from_rect(rect);
224        if let Some(mut fp) = *self.fill_paint.clone() {
225            canvas.fill_path(&path, &mut fp, self.fillrule, self.transform, None);
226        }
227        if let Some(mut sp) = *self.stroke_paint.clone() {
228            canvas.stroke_path(&path, &mut sp, &self.stroke, self.transform, None);
229        }
230    }
231
232    // Draw a circle. Must have at least 2 points, center and a point whose x coordinate is the radius.
233    fn draw_circle(&self, canvas: &mut Canvas) {
234        if self.points.len() < 2 {
235            error!(
236                "Cannot draw a circle with less than 2 points, only {} points provided.",
237                self.points.len()
238            );
239            return;
240        }
241        let cx = self.points[0].x;
242        let cy = self.points[0].y;
243        let w = self.points[1].x;
244        let circle = PathBuilder::from_circle(cx, cy, w).unwrap();
245        if let Some(mut fp) = *self.fill_paint.clone() {
246            canvas.fill_path(&circle, &mut fp, self.fillrule, self.transform, None);
247        }
248        if let Some(mut sp) = *self.stroke_paint.clone() {
249            canvas.stroke_path(&circle, &mut sp, &self.stroke, self.transform, None);
250        }
251    }
252
253    // Draw an ellipse. Must have at least 2 points, center and a point whose x coordinate is the
254    // width and y coordinate is the height.
255    fn draw_ellipse(&self, canvas: &mut Canvas) {
256        if self.points.len() < 2 {
257            error!(
258                "Cannot draw a ellipse with less than 2 points, only {} points provided.",
259                self.points.len()
260            );
261            return;
262        }
263        let cx = self.points[0].x;
264        let cy = self.points[0].y;
265        let w = self.points[1].x;
266        let h = self.points[1].y;
267        let rect = Rect::from_xywh(cx - w / 2.0, cy - h / 2.0, w, h).unwrap();
268        let ellipse = PathBuilder::from_oval(rect).unwrap();
269        if let Some(mut fp) = *self.fill_paint.clone() {
270            canvas.fill_path(&ellipse, &mut fp, self.fillrule, self.transform, None);
271        }
272        if let Some(mut sp) = *self.stroke_paint.clone() {
273            canvas.stroke_path(&ellipse, &mut sp, &self.stroke, self.transform, None);
274        }
275    }
276
277    // Draw a line. Must have at least 2 points. Any more points are ignored.
278    fn draw_line(&self, canvas: &mut Canvas) {
279        if self.points.len() < 2 {
280            error!(
281                "Cannot draw a line with less than 2 points, only {} points provided.",
282                self.points.len()
283            );
284            return;
285        }
286        let x0 = self.points[0].x;
287        let y0 = self.points[0].y;
288        let x1 = self.points[1].x;
289        let y1 = self.points[1].y;
290        let mut pb = PathBuilder::new();
291        pb.move_to(x0, y0);
292        pb.line_to(x1, y1);
293        let path = pb.finish().unwrap();
294        if let Some(mut sp) = *self.stroke_paint.clone() {
295            canvas.stroke_path(&path, &mut sp, &self.stroke, self.transform, None);
296        }
297    }
298}
299
300/// The shape builder
301#[derive(Debug, Clone)]
302pub struct Shape<'a> {
303    fill_paint: Option<Paint<'a>>,
304    stroke_paint: Option<Paint<'a>>,
305    stroke_width: f32,
306    miter_limit: f32,
307    line_cap: LineCap,
308    line_join: LineJoin,
309    stroke_dash: Option<StrokeDash>,
310    points: Vec<Point>,
311    shape: ShapeType,
312    fillrule: FillRule,
313    transform: Transform,
314}
315
316impl Default for Shape<'_> {
317    fn default() -> Self {
318        let fill = Paint {
319            anti_alias: true,
320            ..Default::default()
321        };
322        let stroke = Paint {
323            anti_alias: true,
324            ..Default::default()
325        };
326        Self {
327            fill_paint: Some(fill),
328            stroke_paint: Some(stroke),
329            stroke_width: 1.0,
330            miter_limit: Default::default(),
331            line_cap: Default::default(),
332            line_join: Default::default(),
333            stroke_dash: None,
334            points: vec![],
335            shape: ShapeType::Poly,
336            fillrule: FillRule::Winding,
337            transform: Transform::identity(),
338        }
339    }
340}
341
342impl<'a> Shape<'a> {
343    /// Create an empty shape to start building.
344    pub fn new() -> Self {
345        Self::default()
346    }
347
348    /// Set the fill color of the shape.
349    pub fn fill_color(mut self, color: tiny_skia::Color) -> Self {
350        let mut paint = Paint {
351            anti_alias: true,
352            ..Default::default()
353        };
354        paint.set_color(color);
355        self.fill_paint = Some(paint);
356        self
357    }
358
359    /// Set the fill paint of the shape. This allows for more complex fills
360    /// such as gradients and patterns.
361    pub fn fill_paint(mut self, texture: &'a Paint) -> Self {
362        self.fill_paint = Some(texture.clone());
363        self
364    }
365
366    /// Don't fill the shape. If this is not called the shape will not be closed.
367    pub fn no_fill(mut self) -> Self {
368        self.fill_paint = None;
369        self
370    }
371    /// Don't stroke the shape.
372    pub fn no_stroke(mut self) -> Self {
373        self.stroke_paint = None;
374        self
375    }
376
377    /// Set the stroke color of the shape.
378    pub fn stroke_color(mut self, color: tiny_skia::Color) -> Self {
379        let mut paint = Paint {
380            anti_alias: true,
381            ..Default::default()
382        };
383        paint.set_color(color);
384        self.stroke_paint = Some(paint);
385        self
386    }
387
388    /// Set the stroke paint of the shape. This allows for more complex fills
389    /// such as gradients and patterns.
390    pub fn stroke_paint(mut self, paint: &'a Paint) -> Self {
391        self.stroke_paint = Some(paint.clone());
392        self
393    }
394
395    /// Set the stroke width of the shape.
396    pub fn stroke_weight(mut self, weight: f32) -> Self {
397        self.stroke_width = weight;
398        self
399    }
400
401    /// Set the line cap of the shape.
402    pub fn line_cap(mut self, cap: LineCap) -> Self {
403        self.line_cap = cap;
404        self
405    }
406
407    /// Set the line join of the shape.
408    pub fn line_join(mut self, join: LineJoin) -> Self {
409        self.line_join = join;
410        self
411    }
412
413    pub fn miter_limit(mut self, limit: f32) -> Self {
414        self.miter_limit = limit;
415        self
416    }
417
418    /// Set the stroke dash of the shape.
419    pub fn stroke_dash(mut self, dash: StrokeDash) -> Self {
420        self.stroke_dash = Some(dash);
421        self
422    }
423
424    /// Set the points that determine the shape.
425    pub fn points(mut self, pts: &[Point]) -> Self {
426        let points = pts.to_vec();
427        self.points = points;
428        self
429    }
430
431    /// Get points.
432    pub fn get_points(&self) -> &[Point] {
433        &self.points
434    }
435
436    /// Interpret the points as a rectangle. The first point is the top left corner
437    /// and the second point is the bottom right corner.
438    pub fn rect_ltrb(mut self, lt: Point, rb: Point) -> Self {
439        self.shape = ShapeType::Rect;
440        self.points = vec![lt, rb];
441        self
442    }
443
444    /// Inerpret the points as a rectangle. The first point is the top left corner
445    /// and the second point is the size of the rectangle.
446    pub fn rect_xywh(mut self, xy: Point, wh: Point) -> Self {
447        self.shape = ShapeType::Rect;
448        self.points = vec![xy, pt(xy.x + wh.x, xy.y + wh.y)];
449        self
450    }
451
452    /// Inerpret the points as a rectangle. The first point is the center of the rectangle
453    /// and the second point is the size of the rectangle.
454    pub fn rect_cwh(mut self, c: Point, wh: Point) -> Self {
455        self.shape = ShapeType::Rect;
456        let w2 = wh.x / 2.0;
457        let h2 = wh.y / 2.0;
458        let p = pt(c.x - w2, c.y - h2);
459        self.rect_xywh(p, wh)
460    }
461
462    /// Create circle from it's center and radius.
463    pub fn circle(mut self, center: Point, radius: f32) -> Self {
464        self.shape = ShapeType::Circle;
465        self.points = vec![center, pt(radius, radius)];
466        self
467    }
468
469    /// Create ellispse from it's center, width and heighr.
470    pub fn ellipse(mut self, center: Point, width: f32, height: f32) -> Self {
471        self.shape = ShapeType::Ellipse;
472        self.points = vec![center, pt(width, height)];
473        self
474    }
475
476    /// Create a polygon from it's center, radius and number of sides.
477    pub fn polygon(mut self, center: Point, radius: f32, sides: u32) -> Self {
478        self.shape = ShapeType::Poly;
479        let mut theta = 0.0;
480        let delta = TAU / sides as f32;
481        let mut pts = vec![];
482        while theta < TAU {
483            pts.push(pt(
484                center.x + radius * theta.cos(),
485                center.y + radius * theta.sin(),
486            ));
487            theta += delta;
488        }
489        self.points = pts;
490        self
491    }
492
493    /// Create a star from it's center, inner radius, outer radius, number of sides
494    /// and smoothness parameter.
495    pub fn star(mut self, center: Point, inner_radius: f32, outer_radius: f32, sides: u32) -> Self {
496        self.shape = ShapeType::Poly;
497        let mut theta = 0.0;
498        let delta = TAU / sides as f32;
499        let half_delta = delta / 2.0;
500        let mut pts = vec![];
501        while theta < TAU {
502            pts.push(pt(
503                center.x + outer_radius * theta.cos(),
504                center.y + outer_radius * theta.sin(),
505            ));
506            pts.push(pt(
507                center.x + inner_radius * (theta + half_delta).cos(),
508                center.y + inner_radius * (theta + half_delta).sin(),
509            ));
510            theta += delta;
511        }
512        self.points = pts;
513        self
514    }
515
516    /// A `pearl` is a deformed polygon using the normal distribution to alter the
517    /// location of each point. The `a` and `b` parameters control the width and height
518    /// of the pearl. The `sides` parameter controls the number of sides of the polygon.
519    /// The `smoothness` parameter controls the smoothnes of the polygon.
520    /// using the Chaiken algorithm.
521    pub fn pearl<R: RngCore>(
522        mut self,
523        center: Point,
524        a: f32,
525        b: f32,
526        sides: u32,
527        smoothness: u32,
528        rng: &mut R,
529    ) -> Self {
530        self.shape = ShapeType::Poly;
531        let mut points = vec![];
532        for i in 0..sides {
533            let normal = Normal::new(0.0, 0.25 * a.min(b)).unwrap();
534            let dx = normal.sample(rng);
535            let dy = normal.sample(rng);
536            let u = TAU * i as f32 / sides as f32;
537            let x1 = a * u.cos() + center.x + dx;
538            let y1 = b * u.sin() + center.y + dy;
539            points.push(pt(x1, y1));
540        }
541        self.points = chaiken(&points, smoothness, Trail::Closed)
542            .into_iter()
543            .collect();
544        self
545    }
546
547    /// Interpret the points as a quadratic bezier curve. The first point is the start point,
548    /// the second point is the control point and the third point is the end point and so on.
549    pub fn quad(mut self) -> Self {
550        self.shape = ShapeType::PolyQuad;
551        self
552    }
553
554    /// Interpret the points as a cubic bezier curve. The first point is the start point,
555    /// the second point is the first control point, the third point is the second control point
556    /// and the fourth point is the end point and so on.
557    pub fn cubic(mut self) -> Self {
558        self.shape = ShapeType::PolyCubic;
559        self
560    }
561
562    /// Create a line from it's start and end points.
563    pub fn line(mut self, from: Point, to: Point) -> Self {
564        self.points = vec![from, to];
565        self.shape = ShapeType::Line;
566        self
567    }
568
569    /// Set the fill rule for the shape.
570    pub fn fill_rule(mut self, fillrule: FillRule) -> Self {
571        self.fillrule = fillrule;
572        self
573    }
574
575    /// Set the transform for the shape.
576    pub fn transform(mut self, transform: &Transform) -> Self {
577        let t = self.transform.post_concat(*transform);
578        self.transform = t;
579        self
580    }
581
582    /// Interpret points as cartiesian coordinates with center at (0, 0).
583    pub fn cartesian<T: AsPrimitive<f32>>(mut self, width: T, height: T) -> Self {
584        self.transform = self
585            .transform
586            .post_scale(1.0, -1.0)
587            .post_translate(width.as_() / 2.0, height.as_() / 2.0);
588        self
589    }
590
591    /// Draw the shape to the canvas.
592    pub fn draw(self, canvas: &mut Canvas) {
593        let mut fill_paint: Box<Option<Paint<'_>>> = Box::new(None);
594        let mut stroke_paint: Box<Option<Paint<'_>>> = Box::new(None);
595        if let Some(fs) = self.fill_paint {
596            fill_paint = Box::new(Some(fs));
597        };
598        if let Some(ss) = self.stroke_paint {
599            stroke_paint = Box::new(Some(ss));
600        };
601        let stroke = Stroke {
602            width: self.stroke_width,
603            miter_limit: self.miter_limit,
604            line_cap: self.line_cap,
605            line_join: self.line_join,
606            dash: self.stroke_dash,
607        };
608        ShapeData::new(
609            self.points,
610            fill_paint,
611            stroke,
612            stroke_paint,
613            self.shape,
614            self.fillrule,
615            self.transform,
616        )
617        .draw(canvas);
618    }
619}