File Coverage

blib/lib/Algorithm/Line/Bresenham.pm
Criterion Covered Total %
statement 153 181 84.5
branch 34 52 65.3
condition 7 15 46.6
subroutine 13 13 100.0
pod 6 6 100.0
total 213 267 79.7


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Algorithm::Line::Bresenham - simple pixellated line-drawing algorithm
4              
5             =head1 SYNOPSIS
6              
7             use Algorithm::Line::Bresenham qw/line/;
8             my @points = line(3,3 => 5,0);
9             # returns the list: [3,3], [4,2], [4,1], [5,0]
10             my @points = circle(30,30,5);
11             # returns the points to draw a circle centered at 30,30, radius 5
12              
13             =head1 DESCRIPTION
14              
15             Bresenham is one of the canonical line drawing algorithms for pixellated grids.
16             Given a start and an end-point, Bresenham calculates which points on the grid
17             need to be filled to generate the line between them.
18              
19             Googling for 'Bresenham', and 'line drawing algorithms' gives some good
20             overview. The code here are adapted from various sources, mainly from
21             C code at https://gist.github.com/bert/1085538
22              
23             =head1 FUNCTIONS
24              
25             =cut
26              
27              
28             package Algorithm::Line::Bresenham;
29 1     1   57506 use strict; use warnings;
  1     1   3  
  1         24  
  1         4  
  1         1  
  1         35  
30             our $VERSION = 0.13;
31 1     1   5 use base 'Exporter';
  1         1  
  1         170  
32             our @EXPORT_OK = qw/line circle ellipse_rect quad_bezier polyline/;
33              
34              
35             =head2 C
36              
37             line ($from_x, $from_y => $to_x, $to_y);
38              
39             Generates a list of all the intermediate points. This is returned as a list
40             of array references. Previous versions used to include a callback parameter
41             as a CODE ref to act on each point in turn. This version omits that for
42             performance reasons
43              
44             =cut
45              
46             sub line { # ported from https://gist.github.com/bert/1085538
47 1     1   461 use integer;
  1         13  
  1         4  
48 6     6 1 87 my ($x0, $y0, $x1, $y1,$callback)=@_;
49 1     1   39 use integer;
  1         2  
  1         3  
50 6         11 my $dx = abs ($x1 - $x0);
51 6 100       12 my $sx = $x0 < $x1 ? 1 : -1;
52 6         8 my $dy = -abs ($y1 - $y0);
53 6 100       12 my $sy = $y0 < $y1 ? 1 : -1;
54 6         9 my $err = $dx + $dy;
55 6         7 my $e2; #/* error value e_xy */
56             my @points;
57            
58 6         9 while(1){ #/* loop */
59 21         32 push @points,[$x0,$y0];
60 21 100 100     48 last if ($x0 == $x1 && $y0 == $y1);
61 15         18 $e2 = 2 * $err;
62 15 100       26 if ($e2 >= $dy) { $err += $dy; $x0 += $sx; } #/* e_xy+e_x > 0 */
  13         14  
  13         15  
63 15 100       21 if ($e2 <= $dx) { $err += $dx; $y0 += $sy; } #/* e_xy+e_y < 0 */
  10         11  
  10         12  
64             }
65 6         23 return @points;
66             }
67              
68              
69             =head2 C
70              
71             my @points = circle ($x, $y, $radius)
72              
73             Returns the points to draw a circle centered on C<$x,$y> with
74             radius C<$radius>
75              
76             =cut
77              
78             sub circle { # ported from https://gist.github.com/bert/1085538
79 1     1 1 22 my ($xm, $ym, $r)=@_;
80 1     1   139 use integer;
  1         2  
  1         4  
81 1         3 my $x = -$r;
82 1         2 my $y = 0;
83 1         3 my $err = 2-2*$r; #/* II. Quadrant */
84 1         1 my @points;
85 1         2 do {
86 1         3 push @points,[$xm-$x, $ym+$y];# /* I. Quadrant */
87 1         3 push @points,[$xm-$y, $ym-$x];# /* II. Quadrant */
88 1         3 push @points,[$xm+$x, $ym-$y];# /* III. Quadrant */
89 1         2 push @points,[$xm+$y, $ym+$x];# /* IV. Quadrant */
90 1         2 $r = $err;
91 1 50       4 $err += ++$x*2+1 if ($r > $x); #/* e_xy+e_x > 0 */
92 1 50       5 $err += ++$y*2+1 if ($r <= $y); #/* e_xy+e_y < 0 */
93             } while ($x < 0);
94 1         5 return @points;
95             }
96              
97             =head2 C
98              
99             my @points = ellipse_rect ($x0, $y0, $x1, $y1)
100              
101             Returns the points to an ellipse bound within a rectangle defined by
102             the two coordinate pairs passed.
103              
104             =cut
105              
106              
107             sub ellipse_rect{ # ported from https://gist.github.com/bert/1085538
108 1     1   125 use integer;
  1         2  
  1         2  
109 1     1 1 4 my ($x0, $y0, $x1, $y1)=@_;
110 1         2 my $a = abs ($x1 - $x0);
111 1         3 my $b = abs ($y1 - $y0);
112 1         3 my $b1 = $b & 1; #/* values of diameter */
113 1         3 my $dx = 4 * (1 - $a) * $b * $b;
114 1         3 my $dy = 4 * ($b1 + 1) * $a * $a; #/* error increment */
115 1         2 my $err = $dx + $dy + $b1 * $a * $a;
116 1         1 my $e2; #/* error of 1.step */
117              
118 1 50       3 if ($x0 > $x1) { $x0 = $x1; $x1 += $a; } #/* if called with swapped points */
  0         0  
  0         0  
119 1 50       13 $y0 = $y1 if ($y0 >$y1);# /* .. exchange them */
120 1         3 $y0 += ($b + 1) / 2;
121 1         2 $y1 = $y0-$b1; #/* starting pixel */
122 1         2 $a *= 8 * $a; $b1 = 8 * $b * $b;
  1         2  
123 1         1 my @points;
124             do
125 1         2 {
126 3         6 push @points,[$x1, $y0];# /* I. Quadrant */
127 3         6 push @points,[$x0, $y0];# /* II. Quadrant */
128 3         5 push @points,[$x0, $y1];# /* III. Quadrant */
129 3         6 push @points,[$x1, $y1];# /* IV. Quadrant */
130 3         4 $e2 = 2 * $err;
131 3 50       4 if ($e2 >= $dx)
132             {
133 3         3 $x0++;
134 3         8 $x1--;
135 3         6 $err += $dx += $b1; # does this translate into perl
136             } #/* x step */
137 3 50       10 if ($e2 <= $dy)
138             {
139 0         0 $y0++;
140 0         0 $y1--;
141 0         0 $err += $dy += $a;
142             } #/* y step */
143             } while ($x0 <= $x1);
144 1         4 while ($y0-$y1 < $b)
145             { #/* too early stop of flat ellipses a=1 */
146 0         0 push @points,[$x0-1, $y0]; #/* -> finish tip of ellipse */
147 0         0 push @points,[$x1+1, $y0++];
148 0         0 push @points,[$x0-1, $y1];
149 0         0 push @points,[$x1+1, $y1--];
150             }
151            
152 1         6 return @points;
153             }
154              
155             =head2 C
156              
157             my @points = basic_bezier ($x0, $y0, $x1, $y1, $x2, $y2)
158              
159             This is not usefull on its own. Iteturns the points to segment of a
160             bezier curve without a gradient sign change. It is a companion to
161             the C function that splits a bezier into segments
162             with each gradient direction and these segments are computed in
163             C
164              
165             =cut
166              
167              
168             sub basic_bezier{ # without gradient changes adapted from https://gist.github.com/bert/1085538
169 2     2 1 5 my ($x0, $y0, $x1, $y1, $x2, $y2)=@_;
170 2 50       6 my $sx = $x0 < $x2 ? 1 : -1;
171 2 100       5 my $sy = $y0 < $y2 ? 1 : -1; #/* step direction */
172 2         5 my $cur = $sx * $sy *(($x0 - $x1) * ($y2 - $y1) - ($x2 - $x1) * ($y0 - $y1)); #/* curvature */
173 2         4 my $x = $x0 - 2 * $x1 + $x2;
174 2         3 my $y = $y0 - 2 * $y1 +$y2;
175 2         3 my $xy = 2 * $x * $y * $sx * $sy;
176             # /* compute error increments of P0 */
177 2         6 my $dx = (1 - 2 * abs ($x0 - $x1)) * $y * $y + abs ($y0 - $y1) * $xy - 2 * $cur * abs ($y0 - $y2);
178 2         6 my $dy = (1 - 2 * abs ($y0 - $y1)) * $x * $x + abs ($x0 - $x1) * $xy + 2 * $cur * abs ($x0 - $x2);
179             #/* compute error increments of P2 */
180 2         6 my $ex = (1 - 2 * abs ($x2 - $x1)) * $y * $y + abs ($y2 - $y1) * $xy + 2 * $cur * abs ($y0 - $y2);
181 2         6 my $ey = (1 - 2 * abs ($y2 - $y1)) * $x * $x + abs ($x2 - $x1) * $xy - 2 * $cur * abs ($x0 - $x2);
182             # /* sign of gradient must not change */
183 2 50 33     10 warn "gradient change detected" unless (($x0 - $x1) * ($x2 - $x1) <= 0 && ($y0 - $y1) * ($y2 - $y1) <= 0);
184 2 50       4 if ($cur == 0)
185             { #/* straight line */
186 0         0 return line ($x0, $y0, $x2, $y2);
187            
188             }
189 2         2 $x *= 2 * $x;
190 2         4 $y *= 2 * $y;
191 2 100       4 if ($cur < 0)
192             { #/* negated curvature */
193 1         2 $x = -$x;
194 1         2 $dx = -$dx;
195 1         1 $ex = -$ex;
196 1         1 $xy = -$xy;
197 1         2 $y = -$y;
198 1         1 $dy = -$dy;
199 1         1 $ey = -$ey;
200             }
201             #/* algorithm fails for almost straight line, check error values */
202 2 50 33     22 if ($dx >= -$y || $dy <= -$x || $ex <= -$y || $ey >= -$x)
      33        
      33        
203             {
204 0         0 return (line ($x0, $y0, $x1, $y1), line ($x1, $y1, $x2, $y2)); #/* simple approximation */
205             }
206 2         3 $dx -= $xy;
207 2         7 $ex = $dx + $dy;
208 2         3 $dy -= $xy; #/* error of 1.step */
209 2         2 my @points;
210 2         3 while(1)
211             { #/* plot curve */
212 7         14 push @points,[$x0, $y0];
213 7         9 $ey = 2 * $ex - $dy; #/* save value for test of y step */
214 7 50       12 if (2 * $ex >= $dx)
215             { #/* x step */
216 7 100       12 last if ($x0 == $x2);
217 5         6 $x0 += $sx;
218 5         5 $dy -= $xy;
219 5         5 $ex += $dx += $y;
220             }
221 5 100       8 if ($ey <= 0)
222             { #/* y step */
223 2 50       4 last if ($y0 == $y2);
224 2         3 $y0 += $sy;
225 2         2 $dx -= $xy;
226 2         3 $ex += $dy += $x;
227             }
228             }
229 2         6 return @points;
230             }
231              
232              
233             =head2 C
234              
235             my @points = quad_bezier ($x0, $y0, $x1, $y1, $x2, $y2)
236              
237             Draws a Bezier curve from C<($x0,$y0)> to C<($x2,$y2)> using control
238             point C<($x1,$y1)>
239              
240             =cut
241              
242              
243             sub quad_bezier{ # adapted from http://members.chello.at/easyfilter/bresenham.html
244 1     1 1 4 my ($x0, $y0, $x1, $y1, $x2, $y2)=@_;# /* plot any quadratic Bezier curve */
245 1         2 my $x = $x0-$x1;
246 1         3 my $y = $y0-$y1;
247 1         3 my $t = $x0-2*$x1+$x2;
248 1         2 my $r;
249             my @points;
250 1 50       4 if ($x*($x2-$x1) > 0) { #/* horizontal cut at P4? */
251 0 0       0 if ($y*($y2-$y1) > 0){ #/* vertical cut at P6 too? */
252 0 0       0 if (abs(($y0-2*$y1+$y2)/$t*$x) > abs($y)) { #/* which first? */
253 0         0 $x0 = $x2; $x2 = $x+$x1; $y0 = $y2; $y2 = $y+$y1;# /* swap points */
  0         0  
  0         0  
  0         0  
254             } #/* now horizontal cut at P4 comes first */
255             }
256 0         0 $t = ($x0-$x1)/$t;
257 0         0 $r = (1-$t)*((1-$t)*$y0+2.0*$t*$y1)+$t*$t*$y2;# /* By(t=P4) */
258 0         0 $t = ($x0*$x2-$x1*$x1)*$t/($x0-$x1); #/* gradient dP4/dx=0 */
259 0         0 $x = int($t+0.5); $y = int($r+0.5);
  0         0  
260 0         0 $r = ($y1-$y0)*($t-$x0)/($x1-$x0)+$y0; #/* intersect P3 | P0 P1 */
261 0         0 push @points, basic_bezier($x0,$y0, $x,int($r+0.5), $x,$y);
262 0         0 $r = ($y1-$y2)*($t-$x2)/($x1-$x2)+$y2; #/* intersect P4 | P1 P2 */
263 0         0 $x0 = $x1 = $x; $y0 = $y; $y1 = int($r+0.5);# /* P0 = P4, P1 = P8 */
  0         0  
  0         0  
264             }
265 1 50       6 if (($y0-$y1)*($y2-$y1) > 0) { #/* vertical cut at P6? */
266 1         2 $t = $y0-2*$y1+$y2; $t = ($y0-$y1)/$t;
  1         3  
267 1         14 $r = (1-$t)*((1-$t)*$x0+2.0*$t*$x1)+$t*$t*$x2; # /* Bx(t=P6) */
268 1         3 $t = ($y0*$y2-$y1*$y1)*$t/($y0-$y1); #/* gradient dP6/dy=0 */
269 1         3 $x = int($r+0.5); $y = int($t+0.5);
  1         2  
270 1         3 $r = ($x1-$x0)*($t-$y0)/($y1-$y0)+$x0; #/* intersect P6 | P0 P1 */
271 1         5 push @points, basic_bezier($x0,$y0, int($r+0.5),$y, $x,$y);
272 1         3 $r = ($x1-$x2)*($t-$y2)/($y1-$y2)+$x2; #/* intersect P7 | P1 P2 */
273 1         2 $x0 = $x; $x1 = int($r+0.5); $y0 = $y1 = $y;# /* P0 = P6, P1 = P7 */
  1         2  
  1         1  
274             }
275 1         3 push @points, basic_bezier($x0,$y0, $x1,$y1, $x2,$y2); #/* remaining part */
276 1         12 return @points;
277             }
278              
279             =head2 C
280              
281             my @points = polyline ($x0, $y0, $x1, $y1, $x2, $y2)
282              
283             Draws a polyline between points served as a list of x,y pairs
284              
285             =cut
286              
287             sub polyline{
288 1     1 1 2 my @vertices;
289 1         7 push @vertices,[shift,shift] while (@_>1);
290 1         2 my @points;
291 1         4 foreach my $vertex(0..(@vertices-2)){
292 2         2 push @points,line(@{$vertices[$vertex]},@{$vertices[$vertex+1]});
  2         4  
  2         4  
293 2 100       7 pop @points if ($vertex < (@vertices-2)); # remove duplicated points
294             }
295 1         12 return @points;
296             }
297              
298             1;
299             __END__