File Coverage

lib/Graphics/Toolkit/Color.pm
Criterion Covered Total %
statement 226 233 97.0
branch 146 172 84.8
condition 91 122 74.5
subroutine 24 24 100.0
pod 15 15 100.0
total 502 566 88.6


line stmt bran cond sub pod time code
1              
2             # public user level API: docs, help and arg cleaning
3              
4             package Graphics::Toolkit::Color;
5             our $VERSION = '2.11';
6              
7 4     4   433718 use v5.12;
  4         13  
8 4     4   22 use warnings;
  4         5  
  4         251  
9 4     4   24 use Exporter 'import';
  4         9  
  4         183  
10 4     4   1247 use Graphics::Toolkit::Color::Space::Util qw/is_nr/;
  4         8  
  4         263  
11 4     4   1944 use Graphics::Toolkit::Color::SetCalculator;
  4         10  
  4         14101  
12              
13             my $default_space_name = Graphics::Toolkit::Color::Space::Hub::default_space_name();
14             our @EXPORT_OK = qw/color is_in_gamut/;
15              
16             ## constructor #########################################################
17              
18 24     24 1 744206 sub color { Graphics::Toolkit::Color->new ( @_ ) }
19              
20             sub new {
21 97     97 1 4835 my ($pkg, @args) = @_;
22 97         171 my $help = <
23             constructor new of Graphics::Toolkit::Color object needs either:
24             1. a color name: 'red' or 'SVG:red'
25             2. RGB hex string '#FF0000' or '#f00'
26             3. RGB list or ARRAY ref: ( 255, 0, 0 ) or ( [255, 0, 0] )
27             4. named list or named ARRAY: ( 'HSL', 255, 0, 0 ) or ( ['HSL', 255, 0, 0 ])
28             which works even nested: 'HSL' => [ 255, 0, 0 ] or ['HSL' => [ 255, 0, 0 ]]
29             5. string: new( 'HSL: 0, 100, 50' ) or new( 'ncol(r0, 0%, 0%)' )
30             6. HASH or HASH ref with values from RGB or any other space:
31             (r => 255, g => 0, b => 0) or { hue => 0, saturation => 100, lightness => 50 }
32             7. or use the key 'color' with any SCALAR color definition in order to add
33             the option 'raw' and/or 'range'
34             EOH
35 97         205 my ($color_def, $range_def, $raw) = _compact_color_def_into_scalar( @args );
36 97 100       258 return $help unless defined $color_def;
37 86         188 my $self = _new_from_scalar_def( $color_def, $range_def, $raw );
38 86 100       574 return (ref $self) ? $self : $help;
39             }
40             sub _compact_color_def_into_scalar {
41 109     109   183 my (@args) = @_;
42 109 100       268 return unless @args;
43 106 100 100     473 if (not(@args % 2) and ($args[0] eq 'range' or $args[0] eq 'color' or $args[0] eq 'raw')){
      100        
44 8 50 33     38 if (@args == 2 and $args[0] eq 'color'){ shift @args } # ->new (color => ...) is allowed
  0 50       0  
45             elsif (@args > 2) {
46 8         26 my %h = @args;
47 8 100       50 return (delete( $h{'color'} ), delete( $h{'range'} ), delete( $h{'raw'} )) if @args == (scalar keys %h) * 2; # prevent double key use
48             }
49 0         0 else { return }
50             }
51 99         282 my $first_arg_is_color_space = Graphics::Toolkit::Color::Space::Hub::is_space_name( $args[0] );
52 99 100 100     354 @args = ([ $args[0], @{$args[1]} ]) if @args == 2 and $first_arg_is_color_space and ref $args[1] eq 'ARRAY';
  3   66     9  
53 99 100 100     420 @args = ([ @args ]) if @args == 3 or (@args > 3 and $first_arg_is_color_space);
      100        
54 99 100 100     418 @args = ({ @args }) if @args == 6 or @args == 8;
55 99 100       352 return (@args == 1) ? $args[0] : undef;
56             }
57             sub _new_from_scalar_def { # color defs of method arguments
58 145     145   261 my ($color_def, $range_def, $raw) = @_;
59 145 100       367 return $color_def if ref $color_def eq __PACKAGE__;
60 125         478 return _new_from_value_obj( Graphics::Toolkit::Color::Values->new_from_any_input( $color_def, $range_def, $raw ) );
61             }
62             sub _new_from_value_obj {
63 299     299   466 my ($value_obj) = @_;
64 299 100       685 return $value_obj unless ref $value_obj eq 'Graphics::Toolkit::Color::Values';
65 252         1310 return bless {values => $value_obj};
66             }
67              
68             ########################################################################
69             sub _split_named_args {
70 173     173   435 my ($raw_args, $only_parameter, $required_parameter, $optional_parameter, $parameter_alias) = @_;
71 173 50 66     696 @$raw_args = %{$raw_args->[0]} if @$raw_args == 1 and ref $raw_args->[0] eq 'HASH' and not
  0   0     0  
      33        
72             (defined $only_parameter and $only_parameter eq 'to' and ref _new_from_scalar_def( $raw_args ) );
73              
74 173 100 100     557 if (@$raw_args == 1 and defined $only_parameter and $only_parameter){
      66        
75 34 50       85 return "The one default argument can not cover multiple, required parameter !" if @$required_parameter > 1;
76 34 50 66     107 return "The default argument does not cover the required argument!"
77             if @$required_parameter and $required_parameter->[0] ne $only_parameter;
78              
79 34         196 my %defaults = %$optional_parameter;
80 34         98 delete $defaults{$only_parameter};
81 34         217 return {$only_parameter => $raw_args->[0], %defaults};
82             }
83 139         192 my %clean_arg;
84 139 100       313 if (@$raw_args % 2) {
85 2 50 33     12 return (defined $only_parameter and $only_parameter)
86             ? "Got odd number of values, please use key value pairs as arguments or one default argument !\n"
87             : "Got odd number of values, please use key value pairs as arguments !\n"
88             }
89 137         324 my %arg_hash = @$raw_args;
90 137         278 for my $parameter_name (@$required_parameter){
91 71 100 100     322 if (ref $parameter_alias eq 'HASH' and exists $parameter_alias->{ $parameter_name }
      100        
92             and exists $arg_hash{ $parameter_alias->{$parameter_name} }){
93 10         23 $arg_hash{ $parameter_name } = delete $arg_hash{ $parameter_alias->{$parameter_name} };
94             }
95 71 100       187 return "Argument '$parameter_name' is missing\n" unless exists $arg_hash{$parameter_name};
96 65         219 $clean_arg{ $parameter_name } = delete $arg_hash{ $parameter_name };
97             }
98 131         385 for my $parameter_name (keys %$optional_parameter){
99 485 50 100     781 if (ref $parameter_alias eq 'HASH' and exists $parameter_alias->{ $parameter_name }
      66        
100             and exists $arg_hash{ $parameter_alias->{$parameter_name} }){
101 0         0 $arg_hash{ $parameter_name } = delete $arg_hash{ $parameter_alias->{$parameter_name} };
102             }
103             $clean_arg{ $parameter_name } = exists $arg_hash{$parameter_name}
104             ? delete $arg_hash{ $parameter_name }
105 485 100       1141 : $optional_parameter->{ $parameter_name };
106             }
107 131 100       370 return "Inserted unknown argument(s): ".(join ',', keys %arg_hash)."\n" if %arg_hash;
108 124         285 return \%clean_arg;
109             }
110              
111             ### getter #############################################################
112             sub values {
113 65     65 1 17247 my ($self, @args) = @_;
114 65         568 my $arg = _split_named_args( \@args, 'in', [],
115             { in => $default_space_name, as => 'list', raw => 0,
116             precision => undef, range => undef, suffix => undef } );
117 65         257 my $help = <
118             GTC method 'values' accepts either no arguments, one color space name or four optional, named args:
119             values ( ...
120             in => 'HSL', # color space name, defaults to "$default_space_name"
121             as => 'css_string', # output format name, default is "list"
122             range => 1, # value range (SCALAR or ARRAY), default set by space def
123             precision => 3, # value precision (SCALAR or ARRAY), default set by space
124             suffix => '%', # value suffix (SCALAR or ARRAY), default set by color space
125             raw => 1, # no value clamping, rounding and scaling only by arg request
126              
127             EOH
128 65 100       159 return $arg.$help unless ref $arg;
129 64         320 $self->{'values'}->formatted( @$arg{qw/in as suffix range precision raw/} );
130             }
131              
132             sub name {
133 36     36 1 8063 my ($self, @args) = @_;
134 36 100       234 return $self->{'values'}->name unless @args;
135 2         12 my $arg = _split_named_args( \@args, 'from', [], {from => 'default', all => 0, full => 0, distance => 0});
136 2         5 my $help = <
137             GTC method 'name' accepts three optional, named arguments:
138             name ( ...
139             'CSS', # color naming scheme works as only positional argument
140             from => 'CSS', # same scheme (defaults to internal: X + CSS + PantoneReport)
141             from => ['SVG', 'X'], # more color naming schemes at once, without duplicates
142             all => 1, # returns list of all names with the object's RGB values (defaults 0)
143             full => 1, # adds color scheme name to the color name. 'SVG:red' (defaults 0)
144             distance => 3, # color names from within distance of 3 (defaults 0)
145             EOH
146 2         9 return Graphics::Toolkit::Color::Name::from_values( $self->{'values'}->shaped, @$arg{qw/from all full distance/});
147             }
148              
149             sub closest_name {
150 11     11 1 9901 my ($self, @args) = @_;
151 11         78 my $arg = _split_named_args( \@args, 'from', [], {from => 'default', all => 0, full => 0});
152 11         29 my $help = <
153             GTC method 'closest_name' accepts three optional, named arguments:
154             closest_name ( ...
155             'CSS', # color naming scheme works as only positional argument
156             from => 'CSS', # same scheme (defaults to internal: X + CSS + PantoneReport)
157             from => ['SVG', 'X'], # more color naming schemes at once, without duplicates
158             all => 1, # returns list of all names with the object's RGB values (defaults 0)
159             full => 1, # adds color scheme name to the color name. 'SVG:red' (defaults 0)
160             EOH
161             my ($name, $distance) = Graphics::Toolkit::Color::Name::closest_from_values(
162 11         101 $self->{'values'}->shaped, @$arg{qw/from all full/});
163 11 100       73 return wantarray ? ($name, $distance) : $name;
164             }
165              
166             sub distance {
167 9     9 1 1738 my ($self, @args) = @_;
168 9         59 my $arg = _split_named_args( \@args, 'to', ['to'], {in => $default_space_name, select => undef, range => undef},
169             {select => 'only'});
170 9         32 my $help = <
171             GTC method 'distance' computes the Euclidean distance between two colors (points)
172             in a color space. It accepts as arguments either a scalar color definition or
173             four named arguments, only the first being required:
174             distance ( ...
175             to => 'green', # color object or color definition (required)
176             in => 'HSL', # color space name, defaults to "$default_space_name"
177             select => 'red', # axis name or names (ARRAY ref), default is none
178             only => 'red', # argument alias name to select
179             range => 2**16, # value range definition, defaults come from color space def
180             EOH
181 9 100       26 return $arg.$help unless ref $arg;
182 7         23 my $target_color = _new_from_scalar_def( $arg->{'to'} );
183 7 50       13 return "target color definition: $arg->{to} is ill formed" unless ref $target_color;
184 7         44 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
185 7 50       16 return "$color_space\n".$help unless ref $color_space;
186 7 100       16 if (defined $arg->{'select'}){
187 3 100       12 if (not ref $arg->{'select'}){
    50          
188             return $arg->{'select'}." is not an axis name in color space: ".$color_space->name
189 2 50       10 unless $color_space->is_axis_name( $arg->{'select'} );
190             } elsif (ref $arg->{'select'} eq 'ARRAY'){
191 1         2 for my $axis_name (@{$arg->{'select'}}) {
  1         2  
192 2 50       6 return "$axis_name is not an axis name in color space: ".$color_space->name
193             unless $color_space->is_axis_name( $axis_name );
194             }
195 0         0 } else { return "The 'select' argument needs one axis name or an ARRAY with several axis names".
196             " from the same color space!" }
197             }
198 7         24 my $range_def = $color_space->shape->try_check_range_definition( $arg->{'range'} );
199 7 50       14 return $range_def unless ref $range_def;
200             Graphics::Toolkit::Color::Space::Hub::distance(
201 7         47 $self->{'values'}->normalized, $target_color->{'values'}->normalized, $color_space->name ,$arg->{'select'}, $range_def );
202             }
203              
204              
205             sub is_in_gamut {
206 15     15 1 408 my ($self, @args) = @_;
207 15         26 my $help = <
208             GTC method 'is_in_gamut' returns a perlish pseudo boolean (0/1),
209             telling you if a color is inside the gamut (range) of a color space or not.
210             It accepts any color definition 'new' would. And like 'new' you have to give
211             the color as value of the argument 'color', if you want to add information
212             about the color. If no color definition is provided, the method will operate
213             upon the current color, held by the object.
214             Unlike 'new', the argument 'raw' defaults here to true (1).
215             is_in_gamut ( ...
216             color => [12,1000,5], # color definition
217             range => 2**16, # observe these value ranges while reading the color definition
218             in => 'HSL', # check if color is in gamut of that space
219             # if no space name is provided, the color will be checked
220             # against the boundaries of the space the color was defined in
221             raw => 0, # clamp values to boundaries of the space, the color was defined in,
222             # before converting it into the space you check against
223             EOH
224 15 100       54 unshift @args, $self unless ref $self eq __PACKAGE__;
225 15 50 66     44 return $help if not ref $self and not @args;
226 15         38 my ($color_def, $space_name, $range_def, $raw);
227 15 100 100     96 if (not @args % 2 and @args and ($args[0] eq 'color' or $args[0] eq 'range' or $args[0] eq 'in' or $args[0] eq 'raw')){
      66        
      100        
228 3         12 my %args = @args;
229 3         8 $color_def = delete $args{'color'};
230 3         6 $range_def = delete $args{'range'};
231 3         6 $raw = delete $args{'raw'};
232 3         7 $space_name = delete $args{'in'};
233 3 50 66     13 return "Got no color definition!\n\n".$help unless defined $color_def or ref $self;
234             } else {
235 12         36 $color_def = _compact_color_def_into_scalar(@args);
236 12 50 66     56 return "Got no valid color definition!\n\n".$help if @args and not defined $color_def;
237             }
238             my $values = (defined $color_def)
239             ? Graphics::Toolkit::Color::Values->new_from_any_input( $color_def, $range_def, $raw // 1 )
240 15 100 50     98 : $self->{'values'};
241 15 50       44 return $values unless ref $values;
242 15         54 $values->is_in_gamut( $space_name );
243             }
244            
245             ## single color creation methods #######################################
246             sub apply {
247 3     3 1 818 my ($self, @args) = @_;
248 3         14 my $arg = _split_named_args( \@args, undef, ['gamma'], {in => $default_space_name} );
249 3         7 my $help = <
250             GTC method 'apply' accepts one named argument with a numeric value:
251             apply ( ...
252             gamma => 2.2, # reverse is with 1 / 2.2
253             gamma => {r=> 1, g=> 2, b=> 1.2}, # custom gamma per axis
254             in => 'OKLAB', # compute in oklab space
255             EOH
256 3 50       22 return $arg.$help unless ref $arg;
257 3         10 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
258 3 50       5 return "$color_space\n".$help unless ref $color_space;
259 3         29 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::apply_gamma( $self->{'values'}, $arg->{'gamma'}, $color_space ) );
260             }
261              
262             sub set_value {
263 10     10 1 1369 my ($self, @args) = @_;
264 10 50 33     26 @args = %{$args[0]} if @args == 1 and ref $args[0] eq 'HASH';
  0         0  
265 10         15 my $help = <
266             GTC method 'set_value' needs a value HASH (not a ref) whose keys are axis names or
267             short names from one color space. If the chosen axis name(s) is/are ambiguous,
268             you might add the "in" argument:
269             set_value( green => 20 ) or set( g => 20 ) or
270             set_value( hue => 240, in => 'HWB' )
271             EOH
272 10 100 66     56 return $help if @args % 2 or not @args or @args > 10;
      66        
273 9         23 my $partial_color = { @args };
274 9         12 my $space_name = delete $partial_color->{'in'};
275 9         25 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $space_name );
276 9 50       39 return "$color_space\n".$help unless ref $color_space;
277 9         25 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::set_value( $self->{'values'}, $partial_color, $space_name ) );
278             }
279              
280             sub add_value {
281 10     10 1 887 my ($self, @args) = @_;
282 10 50 33     31 @args = %{$args[0]} if @args == 1 and ref $args[0] eq 'HASH';
  0         0  
283 10         19 my $help = <
284             GTC method 'add_value' needs a value HASH (not a ref) whose keys are axis names or
285             short names from one color space. If the chosen axis name(s) is/are ambiguous,
286             you might add the "in" argument:
287             add_value( blue => -10 ) or set( b => -10 )
288             add_value( hue => 100 , in => 'HWB' )
289             EOH
290 10 100 66     55 return $help if @args % 2 or not @args or @args > 10;
      66        
291 9         23 my $partial_color = { @args };
292 9         16 my $space_name = delete $partial_color->{'in'};
293 9         22 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $space_name );
294 9 50       17 return "$color_space\n".$help unless ref $color_space;
295 9         29 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::add_value( $self->{'values'}, $partial_color, $space_name ) );
296             }
297              
298             sub mix {
299 24     24 1 4545 my ($self, @args) = @_;
300 24         133 my $arg = _split_named_args( \@args, 'to', ['to'], {in => $default_space_name, amount => -1});
301 24         76 my $help = <
302             GTC method 'mix' accepts three named arguments, only the first being required:
303             mix ( ...
304             to => ['HSL', 240, 100, 50], # scalar color definition or ARRAY ref thereof
305             amount => 20, # percentage value or ARRAY ref thereof, default is 50
306             in => 'HSL', # color space name, defaults to "$default_space_name"
307             Please note that ARRAY for amount makes only sense if to got also an ARRAY.
308             Both ARRAY have to have the same length. 'amount' refers to the color(s) picked with 'to'.
309             It is possible to give to an ARRAY and amount a SCALAR.
310             EOH
311 24 100       91 return $arg.$help unless ref $arg;
312 22         71 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( delete $arg->{'in'} );
313 22 100       53 return "$color_space\n".$help unless ref $color_space;
314 21         48 my $recipe = _new_from_scalar_def( $arg->{'to'} );
315 21 100       40 if (ref $recipe){
316 10 100       33 return "argument 'amount' has to be a scalar value if only one color is mixed !\n".$help if ref $arg->{'amount'};
317 9 100       25 $arg->{'amount'} = 50 if $arg->{'amount'} < 0;
318 9 100       21 $arg->{'amount'} = 100 if $arg->{'amount'} > 100;
319 9         35 $recipe = [{color => $recipe->{'values'}, percent => $arg->{'amount'}}];
320 9 100       64 push @$recipe, {color => $self->{'values'}, percent => 100 - $arg->{'amount'} } if $arg->{'amount'} < 100;
321              
322             } else {
323 11 100       31 if (ref $arg->{'to'} ne 'ARRAY'){
324 2         22 return "target color definition (argument 'to'): '$arg->{to}' is ill formed. It has to be one color definition or an ARRAY of them.";
325             } else {
326             return "Argument 'amount' has to be an ARRAY of same length as argument 'to' (color definitions)!\n".$help
327 9 100 66     48 if ref $arg->{'to'} eq 'ARRAY' and ref $arg->{'amount'} eq 'ARRAY' and @{$arg->{'amount'}} != @{$arg->{'to'}};
  4   100     9  
  4         18  
328 8         13 my $color_count = 1 + @{$arg->{'to'}};
  8         16  
329 8 100       16 unless (ref $arg->{'amount'}){
330             $arg->{'amount'} = ($arg->{'amount'} < 0)
331             ? [(100/$color_count) x $color_count]
332 5 50       28 : [($arg->{'amount'}) x $color_count];
333             }
334 8         14 $recipe = [];
335 8         13 my $amount_sum = 0;
336 8         13 for my $color_nr (0 .. $#{$arg->{'to'}}){
  8         24  
337 14         25 my $color_def = $arg->{'to'}[$color_nr];
338 14         18 my $color = _new_from_scalar_def( $color_def );
339 14 50       32 return "target color nr. $color_nr definition: '$color_def' is ill formed" unless ref $color;
340 14         54 push @$recipe, { color => $color->{'values'}, percent => $arg->{'amount'}[$color_nr] };
341 14         37 $amount_sum += $arg->{'amount'}[$color_nr];
342             }
343 8 100       31 push @$recipe, {color => $self->{'values'}, percent => 100 - $amount_sum } if $amount_sum < 100;
344 8 100       20 if ($amount_sum > 100){
345 1         7 $_->{'percent'} = ($_->{'percent'} / $amount_sum * 100) for @$recipe;
346             }
347             }
348             }
349 17         59 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::mix( $self->{'values'}, $recipe, $color_space ) );
350             }
351              
352             sub invert {
353 15     15 1 567 my ($self, @args) = @_;
354 15         132 my $arg = _split_named_args( \@args, 'in', [], {in => $default_space_name, only => undef});
355 15         62 my $help = <
356             GTC method 'invert' accepts one optional argument, which can be positional or named:
357             invert ( ...
358             in => 'HSL', # color space name, defaults to "$default_space_name"
359             only => 'Saturation', # inverts only second value of the tuple
360             only => [qw/s l/], # axis name or names have to match selected space
361             EOH
362 15 100 66     98 return $arg.$help unless ref $arg and (not ref $arg->{'only'} or ref $arg->{'only'} eq 'ARRAY');
      100        
363 14         55 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
364 14 100       50 return "$color_space\n".$help unless ref $color_space;
365 13         60 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::invert( $self->{'values'}, $arg->{'only'}, $color_space ) );
366             }
367              
368             ## color set creation methods ##########################################
369             sub complement {
370 12     12 1 544 my ($self, @args) = @_;
371 12         78 my $arg = _split_named_args( \@args, 'steps', [], {steps => 1, tilt => 0, target => {}});
372 12         42 my $help = <
373             GTC method 'complement' is computed in HSL and has two named, optional arguments:
374             complement ( ...
375             steps => 20, # count of produced colors, default is 1
376             tilt => 10, # default is 0
377             target => {h => 10, s => 20, l => 3}, # sub-keys are independent, default to 0
378             EOH
379 12 100       37 return $arg.$help unless ref $arg;
380 11 100       49 return "Optional argument 'steps' has to be a number !\n".$help unless is_nr($arg->{'steps'});
381 9 50       30 return "Optional argument 'steps' is zero, no complement colors will be computed !\n".$help unless $arg->{'steps'};
382 9 100       22 return "Optional argument 'tilt' has to be a number !\n".$help unless is_nr($arg->{'tilt'});
383 8 100       41 return "Optional argument 'target' has to be a HASH ref !\n".$help if ref $arg->{'target'} ne 'HASH';
384 7         15 my ($target_values, $space_name);
385 7 100       10 if (keys %{$arg->{'target'}}){
  7         36  
386 2         12 ($target_values, $space_name) = Graphics::Toolkit::Color::Space::Hub::deformat_partial_hash( $arg->{'target'}, 'HSL' );
387 2 100       18 return "Optional argument 'target' got HASH keys that do not fit HSL space (use 'h','s','l') !\n".$help
388             unless ref $target_values;
389 5         11 } else { $target_values = [] }
390 13         70 map {_new_from_value_obj( $_ )}
391 6         38 Graphics::Toolkit::Color::SetCalculator::complement( $self->{'values'}, @$arg{qw/steps tilt/}, $target_values );
392             }
393              
394             sub gradient {
395 14     14 1 537 my ($self, @args) = @_;
396 14         184 my $arg = _split_named_args( \@args, 'to', ['to'], {steps => 10, tilt => 0, in => $default_space_name});
397 14         63 my $help = <
398             GTC method 'gradient' accepts four named arguments, only the first is required:
399             gradient ( ...
400             to => 'blue', # scalar color definition or ARRAY ref thereof
401             steps => 20, # count of produced colors, defaults to 10
402             tilt => 1, # dynamics of color change, defaults to 0
403             in => 'HSL', # color space name, defaults to "$default_space_name"
404             EOH
405 14 100       65 return $arg.$help unless ref $arg;
406 12         37 my @colors = ($self->{'values'});
407 12         45 my $target_color = _new_from_scalar_def( $arg->{'to'} );
408 12 100       38 if (ref $target_color) {
409 9         31 push @colors, $target_color->{'values'} }
410             else {
411 3 100 66     51 return "Argument 'to' contains malformed color definition!\n".$help if ref $arg->{'to'} ne 'ARRAY' or not @{$arg->{'to'}};
  2         11  
412 2         5 for my $color_def (@{$arg->{'to'}}){
  2         7  
413 5         14 my $target_color = _new_from_scalar_def( $color_def );
414 5 100       41 return "Argument 'to' contains malformed color definition: $color_def !\n".$help unless ref $target_color;
415 4         21 push @colors, $target_color->{'values'};
416             }
417             }
418             return "Argument 'steps' has to be a number greater zero !\n".$help
419 10 100 66     44 unless is_nr($arg->{'steps'}) and $arg->{'steps'} > 0;
420 9         33 $arg->{'steps'} = int $arg->{'steps'};
421 9 100       25 return "Argument 'tilt' has to be a number !\n".$help unless is_nr($arg->{'tilt'});
422 8         37 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
423 8 100       41 return "$color_space\n".$help unless ref $color_space;
424 53         103 map {_new_from_value_obj( $_ )}
425 7         62 Graphics::Toolkit::Color::SetCalculator::gradient( \@colors, @$arg{qw/steps tilt/}, $color_space);
426             }
427              
428             sub cluster {
429 18     18 1 1102 my ($self, @args) = @_;
430 18         108 my $arg = _split_named_args( \@args, undef, ['radius', 'minimal_distance'], {in => $default_space_name},
431             {radius => 'r', minimal_distance => 'min_d'} );
432 18         67 my $help = <
433             GTC method 'cluster' accepts three named arguments, the first two being required:
434             cluster ( ...
435             radius => 3 # ball shaped cluster with cuboctahedral packing or
436             r => [10, 5, 3] # cuboid shaped cluster with cubical packing
437             minimal_distance => 0.5 # minimal distance between colors in cluster
438             min_d => 0.5 # short alias for minimal distance
439             in => 'HSL' # color space name, defaults to "$default_space_name"
440             EOH
441 18 100       81 return $arg.$help unless ref $arg;
442 12         34 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
443 12 100       36 return "$color_space\n".$help unless ref $color_space;
444             return "Argument 'radius' has to be a number or an ARRAY of numbers".$help
445 11 100 100     32 unless is_nr($arg->{'radius'}) or $color_space->is_number_tuple( $arg->{'radius'} );
446             return "Argument 'minimal_distance' (or 'min_d') has to be a number greater zero !\n".$help
447 8 100 100     22 unless is_nr($arg->{'minimal_distance'}) and $arg->{'minimal_distance'} > 0;
448             return "Ball shaped cluster works only in spaces with three dimensions !\n".$help
449 6 100 100     17 if $color_space->axis_count > 3 and not ref $arg->{'radius'};
450 57         58 map {_new_from_value_obj( $_ )}
451 5         28 Graphics::Toolkit::Color::SetCalculator::cluster( $self->{'values'}, @$arg{qw/radius minimal_distance/}, $color_space);
452             }
453              
454             1;
455              
456             __END__