puzzle_paths/
lib.rs

1//! The Jigsaw Puzzle library creates SVG paths which can be used to cut out puzzle pieces from a
2//! given rectangular image. It provides three public functions:
3//!
4//! - [`build_jigsaw_template`] returns the paths from a given number of pieces in a column and a
5//! row. This is the function you normally want to use
6//! - [`generate_columns_rows_numbers`] returns an ideal distribution of pieces on the x- and y-axes
7//! for a given total number of pieces
8//! - [`round`] is a util function which approximately rounds a f32 value to two decimal places
9
10use std::f32;
11use std::vec;
12
13/// Provides all information on how to cut out the jigsaw puzzle pieces from an image
14#[derive(Clone, Debug)]
15pub struct JigsawTemplate {
16    /// SVG path for every jigsaw puzzle piece
17    pub svg_paths: Vec<String>,
18    /// The dimensions (width, length) in pixel
19    pub piece_dimensions: (f32, f32),
20    /// The number of pieces in the x- and the y-axis
21    pub number_of_pieces: (usize, usize),
22}
23
24/// A segment of an indented puzzle piece edge. A segment is described by a cubic Bézier curve,
25/// which includes a starting point, an end point and two control points. Three segments make up a
26/// piece's edge.
27#[derive(Clone, Debug)]
28struct IndentationSegment {
29    /// Starting point of the segment
30    pub starting_point: (f32, f32),
31    /// End point of the segment
32    pub end_point: (f32, f32),
33    /// The cubic Bézier curve's first control point
34    pub control_point_1: (f32, f32),
35    /// The cubic Bézier curve's second control point
36    pub control_point_2: (f32, f32),
37}
38
39impl IndentationSegment {
40    /// Return segment as SVG path. If reverse = true, return path from right to left for
41    /// horizontal edges and bottom to top for vertical edges.
42    pub fn as_path(&self, reverse: bool) -> String {
43        if reverse {
44            format!(
45                "C {},{} {},{} {},{}",
46                &self.control_point_2.0,
47                &self.control_point_2.1,
48                &self.control_point_1.0,
49                &self.control_point_1.1,
50                &self.starting_point.0,
51                &self.starting_point.1
52            )
53        } else {
54            format!(
55                "C {},{} {},{} {},{}",
56                &self.control_point_1.0,
57                &self.control_point_1.1,
58                &self.control_point_2.0,
59                &self.control_point_2.1,
60                &self.end_point.0,
61                &self.end_point.1
62            )
63        }
64    }
65}
66
67#[derive(Clone, Debug)]
68/// An indented puzzle piece edge. An edge is decribe via three distinct cubic Bézier curves (the
69/// "segments")
70struct IndentedEdge {
71    /// Describes the left half for a horizontal edge, the upper half for a vertical edge
72    pub first_segment: IndentationSegment,
73    /// Describes the form of the tab
74    pub middle_segment: IndentationSegment,
75    /// Describes the right half for a horizontal edge, the lower half for a vertical edge
76    pub last_segment: IndentationSegment,
77}
78
79impl IndentedEdge {
80    /// Creates a new indented edge
81    pub fn new(
82        starting_point: (f32, f32),
83        end_point: (f32, f32),
84        generator: &mut EdgeContourGenerator,
85    ) -> Self {
86        generator.create(starting_point, end_point)
87    }
88    /// Returns edge as SVG path. If reverse = true, returns path from right to left for horizontal
89    /// edges and bottom to top for vertical edges
90    pub fn as_path(&self, reverse: bool) -> String {
91        if reverse {
92            format!(
93                "{}  {}  {}",
94                &self.last_segment.as_path(reverse),
95                &self.middle_segment.as_path(reverse),
96                &self.first_segment.as_path(reverse)
97            )
98        } else {
99            format!(
100                "{}  {}  {}",
101                &self.first_segment.as_path(reverse),
102                &self.middle_segment.as_path(reverse),
103                &self.last_segment.as_path(reverse)
104            )
105        }
106    }
107}
108
109/// Provides the means to generate [`IndentedEdge`]s
110struct EdgeContourGenerator {
111    /// The baseline width of a puzzle piece
112    piece_width: f32,
113    /// The baseline height of a puzzle piece
114    piece_height: f32,
115    /// The tab size factor
116    tab_size: f32,
117    /// The "jitter" factor. A bigger number makes the puzzle pieces more asymmetrical
118    jitter: f32,
119    /// Seed for random values. Increased by 1 after each iteration.
120    seed: usize,
121    /// Flipped tab
122    flipped: bool,
123    /// Random value based on the seed and the predefined jitter value.
124    a: f32,
125    b: f32,
126    c: f32,
127    d: f32,
128    e: f32,
129}
130
131impl EdgeContourGenerator {
132    /// Creates a new [`EdgeContourGenerator`] instance after making sure that the optionally
133    /// provided `tab_size`, `jitter` and `seed` values are in the allowed ranges
134    pub fn new(
135        piece_width: f32,
136        piece_height: f32,
137        tab_size: Option<f32>,
138        jitter: Option<f32>,
139        seed: Option<usize>,
140    ) -> EdgeContourGenerator {
141        let tab_size = tab_size.unwrap_or(20.0) / 200.0;
142        assert!((0.05..=0.15).contains(&tab_size));
143        let jitter = jitter.unwrap_or(0.0) / 100.0;
144        assert!((0.0..=0.13).contains(&jitter));
145        let seed = seed.unwrap_or(0);
146        let e = Self::uniform(-jitter, jitter, seed + 1);
147        let (seed, flipped, a, b, c, d, e) = Self::dice(e, false, seed + 2, jitter);
148        EdgeContourGenerator {
149            piece_width,
150            piece_height,
151            tab_size,
152            jitter,
153            seed,
154            flipped,
155            a,
156            b,
157            c,
158            d,
159            e,
160        }
161    }
162
163    /// Normalises the seed value on a scale between 0 and 1
164    fn normalise(seed: usize) -> f32 {
165        let x = f32::sin(seed as f32) * 10000.0;
166        x - f32::floor(x)
167    }
168
169    /// Returns a statistically evenly distributed value between a `min` and a `max` value
170    fn uniform(min: f32, max: f32, seed: usize) -> f32 {
171        min + Self::normalise(seed) * (max - min)
172    }
173
174    /// Returns `true` if the given value is greater than 0.5 after being normalised on a scale
175    /// between 0.0 and 1.0. I.e. the chances should be approximately 50% for the result to be
176    /// `true`.
177    fn rbool(seed: usize) -> bool {
178        Self::normalise(seed) > 0.5
179    }
180
181    /// Recomputes the factors influencing the form of the edge
182    fn dice(
183        e: f32,
184        flipped: bool,
185        seed: usize,
186        jitter: f32,
187    ) -> (usize, bool, f32, f32, f32, f32, f32) {
188        let new_flipped = Self::rbool(seed);
189        let a = if new_flipped == flipped { -e } else { e };
190        let b = Self::uniform(-jitter, jitter, seed + 2);
191        let c = Self::uniform(-jitter, jitter, seed + 3);
192        let d = Self::uniform(-jitter, jitter, seed + 4);
193        let e = Self::uniform(-jitter, jitter, seed + 5);
194        (seed + 6, new_flipped, a, b, c, d, e)
195    }
196
197    /// Computes the position of a point on an axis along the piece's edge
198    fn longitudinal_position(coeff: f32, offset: f32, length: f32) -> f32 {
199        round(offset + coeff * length)
200    }
201
202    /// Computes the position of a point on an axis transverse to the piece's edge
203    fn transverse_position(coeff: f32, offset: f32, length: f32, flipped: bool) -> f32 {
204        round(offset + coeff * length * if flipped { -1.0 } else { 1.0 })
205    }
206
207    /// Gets the coordinates of a point in a cubic Bézier curve relative to a starting point, the
208    /// length and the direction of the edge (horizontal, vertical) and finally two coefficients
209    /// which designate the offset of the respective points on the longitudinal (`l_coeff`) and the
210    /// transverse (`t_coeff`) axes.
211    fn coords(
212        &self,
213        l_coeff: f32,
214        t_coeff: f32,
215        starting_point: (f32, f32),
216        vertical: bool,
217    ) -> (f32, f32) {
218        let pos_1 = Self::longitudinal_position(
219            l_coeff,
220            if vertical {
221                starting_point.1
222            } else {
223                starting_point.0
224            },
225            if vertical {
226                self.piece_height
227            } else {
228                self.piece_width
229            },
230        );
231        let pos_2 = Self::transverse_position(
232            t_coeff,
233            if vertical {
234                starting_point.0
235            } else {
236                starting_point.1
237            },
238            if vertical {
239                self.piece_width
240            } else {
241                self.piece_height
242            },
243            self.flipped,
244        );
245        if vertical {
246            (pos_2, pos_1)
247        } else {
248            (pos_1, pos_2)
249        }
250    }
251
252    /// Coordinates of the first segment's end point
253    fn ep1(&self, starting_point: (f32, f32), vertical: bool) -> (f32, f32) {
254        self.coords(
255            0.5 - self.tab_size + self.b,
256            self.tab_size + self.c,
257            starting_point,
258            vertical,
259        )
260    }
261
262    /// Coordinates of the first segment's first control point
263    fn cp1_1(&self, starting_point: (f32, f32), vertical: bool) -> (f32, f32) {
264        self.coords(0.2, self.a, starting_point, vertical)
265    }
266
267    /// Coordinates of the first segment's second control point
268    fn cp1_2(&self, starting_point: (f32, f32), vertical: bool) -> (f32, f32) {
269        self.coords(
270            0.5 + self.b + self.d,
271            -self.tab_size + self.c,
272            starting_point,
273            vertical,
274        )
275    }
276
277    /// Coordinates of the second segment's end point
278    fn ep2(&self, starting_point: (f32, f32), vertical: bool) -> (f32, f32) {
279        self.coords(
280            0.5 + self.tab_size + self.b,
281            self.tab_size + self.c,
282            starting_point,
283            vertical,
284        )
285    }
286
287    /// Coordinates of the second segment's first control point
288    fn cp2_1(&self, starting_point: (f32, f32), vertical: bool) -> (f32, f32) {
289        self.coords(
290            0.5 - 2.0 * self.tab_size + self.b - self.d,
291            3.0 * self.tab_size + self.c,
292            starting_point,
293            vertical,
294        )
295    }
296
297    /// Coordinates of the second segment's second control point
298    fn cp2_2(&self, starting_point: (f32, f32), vertical: bool) -> (f32, f32) {
299        self.coords(
300            0.5 + 2.0 * self.tab_size + self.b - self.d,
301            3.0 * self.tab_size + self.c,
302            starting_point,
303            vertical,
304        )
305    }
306
307    /// Coordinates of the third segment's first control point
308    fn cp3_1(&self, starting_point: (f32, f32), vertical: bool) -> (f32, f32) {
309        self.coords(
310            0.5 + self.b + self.d,
311            -self.tab_size + self.b + self.d,
312            starting_point,
313            vertical,
314        )
315    }
316
317    /// Coordinates of the third segment's second control point
318    fn cp3_2(&self, starting_point: (f32, f32), vertical: bool) -> (f32, f32) {
319        self.coords(0.8, self.e, starting_point, vertical)
320    }
321
322    /// Returns a new [`IndentedEdge`] from a given starting and end point
323    pub fn create(&mut self, starting_point: (f32, f32), end_point: (f32, f32)) -> IndentedEdge {
324        let vertical = (end_point.0 - starting_point.0).abs() < 1.0;
325        let first_segment = IndentationSegment {
326            starting_point,
327            end_point: self.ep1(starting_point, vertical),
328            control_point_1: self.cp1_1(starting_point, vertical),
329            control_point_2: self.cp1_2(starting_point, vertical),
330        };
331        let middle_segment = IndentationSegment {
332            starting_point: self.ep1(starting_point, vertical),
333            end_point: self.ep2(starting_point, vertical),
334            control_point_1: self.cp2_1(starting_point, vertical),
335            control_point_2: self.cp2_2(starting_point, vertical),
336        };
337        let last_segment = IndentationSegment {
338            starting_point: self.ep2(starting_point, vertical),
339            end_point,
340            control_point_1: self.cp3_1(starting_point, vertical),
341            control_point_2: self.cp3_2(starting_point, vertical),
342        };
343        let indented_edge = IndentedEdge {
344            first_segment,
345            middle_segment,
346            last_segment,
347        };
348        (
349            self.seed,
350            self.flipped,
351            self.a,
352            self.b,
353            self.c,
354            self.d,
355            self.e,
356        ) = Self::dice(self.e, false, self.seed + 2, self.jitter);
357        indented_edge
358    }
359}
360
361#[derive(Clone, Debug)]
362/// A puzzle piece edge which is at the same time a part of the puzzle's border and therefore forms
363/// a straight line
364struct StraightEdge {
365    pub starting_point: (f32, f32),
366    pub end_point: (f32, f32),
367}
368
369impl StraightEdge {
370    /// Returns edge as SVG path. If reverse = true, returns path from right to left for horizontal
371    /// edges and bottom to top for vertical edges
372    pub fn as_path(&self, reverse: bool) -> String {
373        if reverse {
374            format!("L {},{}", self.starting_point.0, self.starting_point.1)
375        } else {
376            format!("L {},{}", self.end_point.0, self.end_point.1)
377        }
378    }
379}
380
381#[derive(Clone, Debug)]
382/// A border of a puzzle piece. Can be either an `StraightEdge` (no adjacent other piece) or an
383/// `IndentedEdge`
384enum Edge {
385    IndentedEdge(IndentedEdge),
386    StraightEdge(StraightEdge),
387}
388
389impl Edge {
390    /// Returns edge as SVG path. If reverse = true, returns path from right to left for horizontal
391    /// edges and bottom to top for vertical edges
392    pub fn as_path(&self, reverse: bool) -> String {
393        match self {
394            Edge::IndentedEdge(ie) => ie.as_path(reverse),
395            Edge::StraightEdge(oe) => oe.as_path(reverse),
396        }
397    }
398}
399
400/// Divides the axis into `pieces` of equal length. Returns the starting point of each piece,
401/// i.e. the x coordinate on the left of the piece for horizontal lines, and the y coordinate on
402/// the top of the piece for vertical lines, and the length of the piece.
403fn divide_axis(length: f32, piece_num: usize) -> (Vec<f32>, f32) {
404    let piece_length = round(length / piece_num as f32);
405    (
406        (0..piece_num)
407            .map(|s| round(s as f32 * piece_length))
408            .collect::<Vec<f32>>(),
409        piece_length,
410    )
411}
412
413/// Rounds a given rational number to two decimal places
414pub fn round(x: f32) -> f32 {
415    (x * 100.0).round() / 100.0
416}
417
418/// Takes the for edges of a "piece" and creates an SVG path around it. The path always starts at
419/// the upper left corner and proceeds clockwise.
420fn puzzle_piece(
421    starting_point: (f32, f32),
422    top_edge: &Edge,
423    right_edge: &Edge,
424    bottom_edge: &Edge,
425    left_edge: &Edge,
426) -> String {
427    format!(
428        "M {},{}  {}  {}  {}  {}  Z",
429        starting_point.0,
430        starting_point.1,
431        top_edge.as_path(false),
432        right_edge.as_path(false),
433        bottom_edge.as_path(true),
434        left_edge.as_path(true)
435    )
436}
437
438/// Returns the indices of the top, right, bottom and left edge from a given `position` of the
439/// piece in a one-dimensional list of all pieces in the jigsaw puzzle. The returned indices are
440/// used to get the SVG paths for the edges from two lists of all vertical and horizontal edges.
441fn get_border_indices(position: usize, number_of_columns: usize) -> (usize, usize, usize, usize) {
442    let row_ind = position / number_of_columns;
443    (
444        position,
445        position + 1 + row_ind,
446        position + number_of_columns,
447        position + row_ind,
448    )
449}
450
451/// Returns the position of a given segment's end
452fn end_point_pos(ind: usize, segments: &Vec<f32>, fallback: f32) -> f32 {
453    if ind < (segments.len() - 1) {
454        segments[ind + 1]
455    } else {
456        fallback
457    }
458}
459
460/// Returns all divisor pairs for a given number
461fn find_divisors(num: usize) -> Vec<(usize, usize)> {
462    let mut i = 1;
463    let mut divisor_pairs = vec![];
464    loop {
465        if i * i > num {
466            break;
467        } else if num % i == 0 {
468            divisor_pairs.push((i, num / i));
469        }
470        i += 1;
471    }
472    let mut mirrored = divisor_pairs
473        .iter()
474        .filter(|(a, b)| a != b)
475        .map(|(a, b)| (*b, *a))
476        .collect::<Vec<(usize, usize)>>();
477    mirrored.reverse();
478    divisor_pairs.append(&mut mirrored);
479    divisor_pairs
480}
481
482/// Returns the visually most appealing piece aspect ratio, i.e. a square one (equal width and
483/// height) or, if that's not possible , a "landscape" format as square as possible.
484fn optimal_aspect_ratio(
485    possible_dimensions: Vec<(usize, usize)>,
486    image_width: f32,
487    image_height: f32,
488) -> (usize, usize) {
489    let mut width_height_diff = std::f32::MAX;
490    let mut number_of_pieces = *possible_dimensions
491        .first()
492        .expect("No possible dimensions found. This error should never happen!");
493    for (x, y) in possible_dimensions {
494        let width = image_width / x as f32;
495        let height = image_height / y as f32;
496        let new_width_height_diff = (width - height).abs();
497        if new_width_height_diff < 1. {
498            return (x, y);
499        }
500        if width_height_diff >= new_width_height_diff {
501            width_height_diff = new_width_height_diff;
502            number_of_pieces = (x, y);
503        } else {
504            return number_of_pieces;
505        }
506    }
507    number_of_pieces
508}
509
510/// Returns the visually most appealing numbers of pieces in one column and one row based on a
511/// given number of pieces
512pub fn generate_columns_rows_numbers(
513    image_width: f32,
514    image_height: f32,
515    number_of_pieces: usize,
516) -> (usize, usize) {
517    let divisor_pairs = find_divisors(number_of_pieces);
518    optimal_aspect_ratio(divisor_pairs, image_width, image_height)
519}
520
521/// Returns information on how to cut jigsaw puzzle pieces from an image of a given width and
522/// height and the number of pieces in a row and a column as an optional the tab size, a "jitter"
523/// factor and a initial seed value.
524///
525/// The `tab_size` argument defines the size of the pieces' tabs. It can be any number from `10.0` to `30.0` and defaults to `20.0`
526///
527/// `jitter` can be a number between 0.0 and 13.0. The bigger the number, the more asymmetrical are
528/// the puzzle pieces. Defaults to `0.0` (symmetrical).
529///
530/// `seed` provides the initial "randomness" when creating the contours of the puzzle pieces. Same
531/// seed values for images with same dimensions and same number of pieces lead to same SVG paths.
532pub fn build_jigsaw_template(
533    image_width: f32,
534    image_height: f32,
535    pieces_in_column: usize,
536    pieces_in_row: usize,
537    tab_size: Option<f32>,
538    jitter: Option<f32>,
539    seed: Option<usize>,
540) -> JigsawTemplate {
541    let (starting_points_x, piece_width) = divide_axis(image_width, pieces_in_column);
542    let (starting_points_y, piece_height) = divide_axis(image_height, pieces_in_row);
543    let mut contour_gen =
544        EdgeContourGenerator::new(piece_width, piece_height, tab_size, jitter, seed);
545    let mut vertical_edges = vec![];
546    let mut horizontal_edges = vec![];
547    let mut top_border = true;
548    for index_y in 0..starting_points_y.len() {
549        let mut left_border = true;
550        for index_x in 0..starting_points_x.len() {
551            horizontal_edges.push(if top_border {
552                Edge::StraightEdge(StraightEdge {
553                    starting_point: (starting_points_x[index_x], 0.0),
554                    end_point: (end_point_pos(index_x, &starting_points_x, image_width), 0.0),
555                })
556            } else {
557                Edge::IndentedEdge(IndentedEdge::new(
558                    (starting_points_x[index_x], starting_points_y[index_y]),
559                    (
560                        end_point_pos(index_x, &starting_points_x, image_width),
561                        starting_points_y[index_y],
562                    ),
563                    &mut contour_gen,
564                ))
565            });
566            vertical_edges.push(if left_border {
567                Edge::StraightEdge(StraightEdge {
568                    starting_point: (0.0, starting_points_y[index_y]),
569                    end_point: (
570                        0.0,
571                        end_point_pos(index_y, &starting_points_y, image_height),
572                    ),
573                })
574            } else {
575                Edge::IndentedEdge(IndentedEdge::new(
576                    (starting_points_x[index_x], starting_points_y[index_y]),
577                    (
578                        starting_points_x[index_x],
579                        end_point_pos(index_y, &starting_points_y, image_height),
580                    ),
581                    &mut contour_gen,
582                ))
583            });
584            left_border = false;
585        }
586        top_border = false;
587        // Draw right outer edge
588        vertical_edges.push(Edge::StraightEdge(StraightEdge {
589            starting_point: (image_width, starting_points_y[index_y]),
590            end_point: (
591                image_width,
592                end_point_pos(index_y, &starting_points_y, image_height),
593            ),
594        }));
595    }
596
597    // Draw bottom outer edges
598    for index_x in 0..starting_points_x.len() {
599        horizontal_edges.push(Edge::StraightEdge(StraightEdge {
600            starting_point: (starting_points_x[index_x], image_height),
601            end_point: (
602                end_point_pos(index_x, &starting_points_x, image_width),
603                image_height,
604            ),
605        }))
606    }
607    let mut svg_paths = vec![];
608    let mut i = 0;
609    for y in starting_points_y.iter() {
610        for x in starting_points_x.iter() {
611            let (top_index, right_index, bottom_index, left_index) =
612                get_border_indices(i, pieces_in_column);
613            svg_paths.push(puzzle_piece(
614                (*x, *y),
615                &horizontal_edges[top_index],
616                &vertical_edges[right_index],
617                &horizontal_edges[bottom_index],
618                &vertical_edges[left_index],
619            ));
620            i += 1;
621        }
622    }
623    JigsawTemplate {
624        svg_paths,
625        piece_dimensions: (piece_width, piece_height),
626        number_of_pieces: (pieces_in_column, pieces_in_row),
627    }
628}
629
630#[cfg(test)]
631mod tests {
632    use super::*;
633
634    #[test]
635    fn test_divide_axis() {
636        let res = divide_axis(1000.0, 4);
637        assert!(res.0.len() == 4);
638        assert!(res.1 > 249.0 && res.1 < 251.0);
639    }
640
641    #[test]
642    fn test_divisor_pairs() {
643        let given_number = 1;
644        assert_eq!(find_divisors(given_number), vec![(1, 1),]);
645
646        let given_number = 24;
647        assert_eq!(
648            find_divisors(given_number),
649            vec![
650                (1, 24),
651                (2, 12),
652                (3, 8),
653                (4, 6),
654                (6, 4),
655                (8, 3),
656                (12, 2),
657                (24, 1),
658            ]
659        );
660
661        let given_number = 9;
662        assert_eq!(find_divisors(given_number), vec![(1, 9), (3, 3), (9, 1),])
663    }
664
665    #[test]
666    fn test_optimal_aspect_ratio() {
667        let image_width: f32 = 1024.;
668        let image_height: f32 = 768.;
669        let possible_aspect_ratios = vec![(1, 25), (5, 5), (25, 1)];
670        assert_eq!(
671            optimal_aspect_ratio(possible_aspect_ratios, image_width, image_height),
672            (5, 5)
673        );
674
675        let image_width: f32 = 666.;
676        let image_height: f32 = 666.;
677        let possible_aspect_ratios = vec![
678            (1, 24),
679            (2, 12),
680            (3, 8),
681            (4, 6),
682            (6, 4),
683            (8, 3),
684            (12, 2),
685            (24, 1),
686        ];
687        assert_eq!(
688            optimal_aspect_ratio(possible_aspect_ratios, image_width, image_height),
689            (6, 4)
690        );
691    }
692}