File Coverage

blib/lib/SVG/ChristmasTree.pm
Criterion Covered Total %
statement 101 101 100.0
branch n/a
condition n/a
subroutine 20 20 100.0
pod 2 5 40.0
total 123 126 97.6


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             SVG::ChristmasTree - Perl extension to draw Christmas trees with SVG
4              
5             =head1 DESCRIPTION
6              
7             Perl extension to draw Christmas trees with SVG
8              
9             =head1 SYNOPSIS
10              
11             # Default tree
12             my $tree = SVG::ChristmasTree->new;
13             print $tree->as_xml;
14              
15             # Or change things
16             my $tree = SVG::ChristmasTree->new({
17             layers => 5,
18             leaf_colour => 'rgb(0,255,0)',
19             pot_colour => 'rgb(0,0,255)',
20             star_color => 'rgb(255,0,0)',
21             });
22             print $tree->as_xml;
23              
24             =cut
25              
26             package SVG::ChristmasTree;
27              
28 1     1   78263 use strict;
  1         1  
  1         27  
29 1     1   3 use warnings;
  1         2  
  1         38  
30              
31 1     1   1566 use Moose;
  1         597811  
  1         12  
32 1     1   11070 use namespace::autoclean;
  1         11780  
  1         4  
33 1     1   1091 use SVG;
  1         23679  
  1         8  
34 1     1   1760 use Math::Trig qw[deg2rad tan];
  1         19525  
  1         204  
35              
36             with 'MooseX::Getopt';
37              
38             our $VERSION = '0.0.7';
39              
40             # Constants that we haven't made into attributes yet
41             use constant {
42 1         2179 TREE_WIDTH => 600, # Width of the bottom tree layer
43             TOP_ANGLE => 90, # Angle at the top of the tree triangles
44             LAYER_SIZE_RATIO => (5/6), # How much smaller each layer gets
45             LAYER_STACKING => 0.5, # How far up a layer triangle does the next one start
46             POT_TOP_WIDTH => 300, # Width of the top of the pot
47             POT_BOT_WIDTH => 200, # Width of the bottom of the pot
48             TRUNK_WIDTH => 100, # Width of the trunk
49             BAUBLE_RADIUS => 20, # Radius of a bauble
50 1     1   11 };
  1         2  
51              
52             =head1 Methods
53              
54             =head2 $tree = SVG::ChristmasTree->new(\%args)
55              
56             Constructs and returns a new SVG::ChristmasTree object. With no arguments,
57             a default tree design is created, but it is possible to change that by
58             passing the following attributes to the method.
59              
60             =over 4
61              
62             =item width INT
63              
64             The width of the tree diagram in "pixels". The default is 1,000.
65              
66             =item layers INT
67              
68             The number of layers in the tree. The default tree has four layers.
69              
70             =item trunk_length INT
71              
72             The length of the trunk in "pixels". The default length is 100.
73              
74             =item leaf_colour STR
75              
76             The colour of the tree's leaves. This must be defined as an SVG RGB value.
77             The default value is "rgb(0,127,0)".
78              
79             =item bauble_colour STR
80              
81             The colour of the baubles that hang on the tree. This must be defined as an
82             SVG RGB value. The default value is "rgb(212,175,55)".
83              
84             =item trunk colour STR
85              
86             The colour of the tree trunk. This must be defined as an SVG RGB value. The
87             default value is "rgb(139,69,19)".
88              
89             =item pot_colour STR
90              
91             The colour of the pot. This must be defined as an SVG RGB value. The default
92             value is "rgb(191,0,0)".
93              
94             =item pot_height INT
95              
96             The height of the pot in "pixels". The default height is 200.
97              
98             =item star_colour STR
99              
100             The colour of the star. This must be defined as an SVG RGB value. The default
101             value is "rgb(212,175,55)".
102              
103             =item star_size INT
104              
105             The size of the star in "pixels". The start will be in a square of the defined
106             size.The default size is 80.
107              
108             =back
109              
110             =head2 $tree->as_xml
111              
112             Returns the SVG document as XML. You will usually want to store the returned
113             value in a variable, print it to C<STDOUT> or write it to a file.
114              
115             =cut
116              
117             has width => (
118             isa => 'Int',
119             is => 'ro',
120             default => 1_000,
121             );
122              
123             has height => (
124             isa => 'Int',
125             is => 'ro',
126             lazy_build => 1,
127             init_arg => undef,
128             );
129              
130             # Height is calculated from all the other stuff
131             sub _build_height {
132 1     1   2 my $self = shift;
133              
134             # Pot height ...
135 1         27 my $height = $self->pot_height;
136             # ... plus the trunk length ...
137 1         25 $height += $self->trunk_length;
138             # ... for most of the layers ...
139 1         21 for (0 .. $self->layers - 2) {
140             # ... add LAYER_STACKING of the height ...
141 3         81 $height += $self->triangle_heights->[$_] * LAYER_STACKING;
142             }
143             # ... add all of the last layer ...
144 1         19 $height += $self->triangle_heights->[-1];
145             # ... and (finally) half of the star
146 1         21 $height += $self->star_size / 2;
147              
148 1         20 return int($height + 0.5);
149             }
150              
151             has triangle_heights => (
152             isa => 'ArrayRef',
153             is => 'ro',
154             lazy_build => 1,
155             init_arg => undef,
156             );
157              
158             sub _build_triangle_heights {
159 1     1   1 my $self = shift;
160              
161 1         2 my @heights;
162 1         2 my $width = TREE_WIDTH;
163 1         18 for (1 .. $self->layers) {
164 4         8 push @heights, $self->_triangle_height($width, TOP_ANGLE);
165 4         61 $width *= LAYER_SIZE_RATIO;
166             }
167              
168 1         31 return \@heights;
169             }
170              
171             sub _triangle_height {
172 4     4   5 my $self = shift;
173 4         5 my ($base, $top_angle) = @_;
174              
175             # Assume $top_angle is in degrees
176 4         13 $top_angle = deg2rad($top_angle) / 2;
177             # If I remember my trig correctly...
178 4         51 return ($base / 2) / tan($top_angle);
179             }
180              
181             has svg => (
182             isa => 'SVG',
183             is => 'ro',
184             lazy_build => 1,
185             init_arg => undef,
186             );
187              
188             sub _build_svg {
189 1     1   2 my $self = shift;
190              
191 1         25 return SVG->new(
192             width => $self->width,
193             height => $self->height,
194             );
195             }
196              
197             has layers => (
198             isa => 'Int',
199             is => 'ro',
200             default => 4,
201             );
202              
203             has trunk_length => (
204             isa => 'Int',
205             is => 'ro',
206             default => 100,
207             );
208              
209             has leaf_colour => (
210             isa => 'Str',
211             is => 'ro',
212             default => 'rgb(0,127,0)',
213             );
214              
215             has bauble_colour => (
216             isa => 'Str',
217             is => 'ro',
218             default => 'rgb(212,175,55)',
219             );
220              
221             has trunk_colour => (
222             isa => 'Str',
223             is => 'ro',
224             default => 'rgb(139,69,19)',
225             );
226              
227             has pot_colour => (
228             isa => 'Str',
229             is => 'ro',
230             default => 'rgb(191,0,0)',
231             );
232              
233             has star_colour => (
234             isa => 'Str',
235             is => 'ro',
236             default => 'rgb(212,175,55)',
237             );
238              
239             has pot_height => (
240             isa => 'Int',
241             is => 'ro',
242             default => 200,
243             );
244              
245             has star_size => (
246             isa => 'Int',
247             is => 'ro',
248             default => 80,
249             );
250              
251             has triangles => (
252             isa => 'ArrayRef',
253             is => 'ro',
254             lazy_build => 1,
255             init_arg => undef,
256             );
257              
258             sub _build_triangles {
259 1     1   2 my $self = shift;
260              
261 1         2 my $width = TREE_WIDTH;
262 1         19 my $tri_bottom = $self->height - $self->pot_height - $self->trunk_length;
263              
264 1         2 my @triangles;
265 1         34 for (1 .. $self->layers) {
266 4         9 push @triangles, $self->_triangle(TOP_ANGLE, $width, $tri_bottom);
267 4         25 $width *= LAYER_SIZE_RATIO;
268 4         7 $tri_bottom -= $triangles[-1]->{h} * LAYER_STACKING;
269             }
270              
271 1         20 return \@triangles;
272             }
273              
274             sub as_xml {
275 1     1 1 3 my $self = shift;
276              
277 1         4 $self->pot;
278 1         52 $self->trunk;
279              
280 1         39 for (@{$self->triangles}) {
  1         23  
281 4         155 my $h = $self->_triangle(TOP_ANGLE, $_->{w}, $_->{b});
282 4         9 $self->bauble($self->_mid_y - ($_->{w}/2), $_->{b});
283 4         199 $self->bauble($self->_mid_y + ($_->{w}/2), $_->{b});
284             $self->_coloured_shape(
285 4         288 $_->{x}, $_->{y}, $self->leaf_colour,
286             );
287             }
288              
289 1         42 $self->star;
290              
291 1         60 return $self->svg->xmlify;
292             }
293              
294             sub pot {
295 1     1 0 2 my $self = shift;
296              
297 1         41 my $pot_top = $self->height - $self->pot_height;
298              
299 1         5 $self->_coloured_shape(
300             [ $self->_mid_y - (POT_BOT_WIDTH / 2),
301             $self->_mid_y - (POT_TOP_WIDTH / 2),
302             $self->_mid_y + (POT_TOP_WIDTH / 2),
303             $self->_mid_y + (POT_BOT_WIDTH / 2) ],
304             [ $self->height, $pot_top, $pot_top, $self->height ],
305             $self->pot_colour,
306             );
307             }
308              
309             sub trunk {
310 1     1 1 1 my $self = shift;
311              
312 1         31 my $trunk_bottom = $self->height - $self->pot_height;
313 1         18 my $trunk_top = $trunk_bottom - $self->trunk_length;
314              
315 1         4 $self->_coloured_shape(
316             [ $self->_mid_y - (TRUNK_WIDTH / 2), $self->_mid_y - (TRUNK_WIDTH / 2),
317             $self->_mid_y + (TRUNK_WIDTH / 2), $self->_mid_y + (TRUNK_WIDTH / 2) ],
318             [ $trunk_bottom, $trunk_top, $trunk_top, $trunk_bottom ],
319             $self->trunk_colour,
320             );
321             }
322              
323             sub _triangle {
324 8     8   12 my $self = shift;
325 8         11 my ($top_angle, $base, $bottom) = @_;
326              
327 8         9 my ($x, $y);
328              
329             # Assume $top_angle is in degrees
330 8         18 $top_angle = deg2rad($top_angle) / 2;
331             # If I remember my trig correctly...
332 8         59 my $height = ($base / 2) / tan($top_angle);
333              
334 8         79 $x = [ $self->_mid_y - ($base / 2), $self->_mid_y, $self->_mid_y + ($base / 2) ];
335 8         15 $y = [ $bottom, $bottom - $height, $bottom ];
336              
337             return {
338 8         30 x => $x, # array ref of x points
339             y => $y, # array ref of y points
340             h => $height, # height of the triangle
341             w => $base, # length of the base of the triangle
342             b => $bottom, # y-coord of the bottom of the triangle
343             };
344             }
345              
346             sub bauble {
347 8     8 0 11 my $self = shift;
348 8         16 my ($x, $y) = @_;
349              
350 8         198 $self->svg->circle(
351             cx => $x,
352             cy => $y + BAUBLE_RADIUS,
353             r => BAUBLE_RADIUS,
354             style => {
355             fill => $self->bauble_colour,
356             stroke => $self->bauble_colour,
357             },
358             );
359             }
360              
361             sub star {
362 1     1 0 2 my $self = shift;
363 1         2 my ($x, $y, $delta_x, $delta_y);
364              
365 1         46 $delta_x = $self->_mid_y;
366 1         3 $delta_y = 0;
367              
368             # coordinates for a polyline star centered at 0,0
369 1         3 $x = [ 0, 0.125, 0.5, 0.25, 0.375, 0, -0.375, -0.25, -0.5, -0.125, 0 ];
370 1         3 $y = [ 0, 0.375, 0.375, 0.625, 1, 0.75, 1, 0.625, 0.375, 0.375, 0 ];
371              
372             # multiple by size
373 1         4 $x = [map { $_ * $self->star_size } @$x];
  11         182  
374 1         3 $y = [map { $_ * $self->star_size } @$y];
  11         180  
375              
376             # move to the placement we want (centered, on top of tree)
377 1         2 $x = [map { $_ + $delta_x } @$x];
  11         14  
378 1         3 $y = [map { $_ + $delta_y } @$y];
  11         12  
379              
380 1         24 $self->_coloured_shape(
381             $x,
382             $y,
383             $self->star_colour,
384             );
385             }
386              
387             sub _mid_y {
388 41     41   40 my $self = shift;
389              
390 41         762 return $self->width / 2;
391             }
392              
393             sub _coloured_shape {
394 7     7   9 my $self = shift;
395 7         12 my ($x, $y, $colour) = @_;
396              
397 7         148 my $path = $self->svg->get_path(
398             x => $x,
399             y => $y,
400             -type => 'polyline',
401             -closed => 1,
402             );
403              
404 7         487 $self->svg->polyline(
405             %$path,
406             style => {
407             fill => $colour,
408             stroke => $colour,
409             },
410             );
411             }
412              
413             __PACKAGE__->meta()->make_immutable();
414              
415             =head1 AUTHOR
416              
417             Dave Cross <dave@perlhacks.com>
418              
419             =head1 COPYRIGHT AND LICENCE
420              
421             Copyright (c) 2018, Magnum Solutions Ltd. All Rights Reserved.
422              
423             This library is free software; you can redistribute it and/or modify it
424             under the same terms as Perl itself.
425              
426             =cut
427              
428             1;