File Coverage

lib/Graphics/Toolkit/Color.pm
Criterion Covered Total %
statement 264 298 88.5
branch 166 218 76.1
condition 81 116 69.8
subroutine 30 37 81.0
pod 21 26 80.7
total 562 695 80.8


line stmt bran cond sub pod time code
1              
2             # public user level API: doc summary, help msg and arg cleaning
3              
4             package Graphics::Toolkit::Color;
5             our $VERSION = '2.22';
6 5     5   381810 use v5.12;
  5         17  
7 5     5   25 use warnings;
  5         8  
  5         356  
8 5     5   1710 use Graphics::Toolkit::Color::Error qw/error/;
  5         11  
  5         298  
9 5     5   1178 use Graphics::Toolkit::Color::Space::Util qw/is_nr/;
  5         9  
  5         268  
10 5     5   2232 use Graphics::Toolkit::Color::SetCalculator;
  5         18  
  5         192  
11              
12             ## import export, error handling #######################################
13 5     5   32 use Exporter;
  5         5  
  5         20169  
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw/color is_in_gamut/;
16              
17             sub import {
18 10     10   3505 my ($class, @args) = @_;
19 10         14 my @export_symbols;
20 10   100     72 push @export_symbols, shift @args while @args and lc $args[0] ne 'error';
21 10         40 Graphics::Toolkit::Color::Error::change_mode( $args[1] );
22 10         10147 $class->Exporter::export_to_level(1, $class, @export_symbols);
23             }
24              
25             ## constructor #########################################################
26             my $POD_link = ' Type "perldoc Graphics::Toolkit::Color" for more help.';
27             sub new {
28 75     75 0 5931 my ($pkg, @args) = @_;
29 75         107 my $help = 'method "new" accepts the arguments: "color" (color definition), "raw", '.
30             '"range" and "in" (space name). "color" is the required and default argument.'.$POD_link;
31 75         92 my ($color_def, $space_name, $range_def, $is_raw);
32 75 100 100     377 if (@args > 0 and not @args % 2){
33 32         80 my %h = @args;
34 32 100       83 return error('got an argument twice') if keys(%h) * 2 < int(@args);
35 31   100     161 ($color_def, $space_name, $range_def, $is_raw) = ($h{'color'}, $h{'in'}, $h{'range'}, $h{'raw'} // 0);
36             }
37 74 100       181 $color_def = _color_def_into_scalar( @args ) unless defined $color_def;
38 74 100       124 return error($help) unless defined $color_def;
39 73         128 my $self = _new_from_scalar_def( $color_def, $space_name, $range_def, $is_raw );
40 73 100       383 return (ref $self) ? $self : error($self);
41             }
42             sub color {
43 25     25 0 613204 my $self = _new_from_scalar_def( _color_def_into_scalar( @_ ) );
44 25 100       87 return (ref $self) ? $self : error($self);
45             }
46             sub _color_def_into_scalar {
47 100     100   190 my (@args) = @_;
48 100 50 66     429 return if @args < 1 or @args > 8 or @args == 7;
      66        
49 97 100       264 return $args[0] if @args == 1; # pass names
50 39 100       143 return [@args] if @args <= 5; # lists and named lists --> array and named array
51 17         44 return {@args}; # hashes without curly braces --> hash
52             }
53             sub _new_from_scalar_def {
54 148     148   257 my ($color_def, $space_name, $range_def, $is_raw) = @_;
55 148 100       299 return $color_def if ref $color_def eq __PACKAGE__;
56 137         500 return _new_from_value_obj( Graphics::Toolkit::Color::Values->new_from_any_input( $color_def, $space_name, $range_def, $is_raw ) );
57             }
58             sub _new_from_value_obj {
59 315     315   406 my ($value_obj) = @_;
60 315 100       584 return $value_obj unless ref $value_obj eq 'Graphics::Toolkit::Color::Values';
61 265         1247 return bless {values => $value_obj};
62             }
63 254 50   254 0 1277 sub values_object { $_[0]->{'values'} if ref $_[0] eq __PACKAGE__}
64              
65             sub is_in_gamut {
66 13     13 1 401 my ($self, $space_name, $named_arg) = @_;
67 13 100       40 return is_in_gamut_sub (@_) if ref $self ne __PACKAGE__;
68 5         6 my $help = 'The method "is_in_gamut" accepts one optional, positional argument, a color space name, '.
69             'which defaults to the space the color was defined in'.$POD_link;
70 5 100 100     19 $space_name = $named_arg if defined $space_name and $space_name eq 'in' and defined $named_arg;
      66        
71 5         11 my $space = Graphics::Toolkit::Color::Space::Hub::get_space( $space_name );
72 5 50 66     15 return error($help) if defined $space_name and not ref $space;
73 5 100       8 $self->values_object->is_in_gamut( (ref $space) ? $space->name : undef );
74             }
75             sub is_in_gamut_sub {
76 8     8 0 12 my (@color) = @_;
77 8         15 my $values = Graphics::Toolkit::Color::Values->new_from_any_input(
78             _color_def_into_scalar( @_ ), undef, undef, 1
79             );
80 8 100       18 return error($values.$POD_link) unless ref $values;
81 7         19 $values->is_in_gamut( );
82             }
83              
84             ########################################################################
85             sub _split_named_args {
86 191     191   408 my ($raw_args, $only_parameter, $required_parameter, $optional_parameter, $parameter_alias) = @_;
87 191 50 66     601 @$raw_args = %{$raw_args->[0]} if @$raw_args == 1 and ref $raw_args->[0] eq 'HASH' and not
  0   0     0  
      33        
88             (defined $only_parameter and $only_parameter eq 'to' and ref _new_from_scalar_def( $raw_args ) );
89              
90 191 100 100     473 if (@$raw_args == 1 and defined $only_parameter and $only_parameter){
      66        
91 35 50       69 return "The one default argument can not cover multiple, required parameter !" if @$required_parameter > 1;
92 35 50 66     99 return "The default argument does not cover the required argument!"
93             if @$required_parameter and $required_parameter->[0] ne $only_parameter;
94              
95 35         118 my %defaults = %$optional_parameter;
96 35         69 delete $defaults{$only_parameter};
97 35         181 return {$only_parameter => $raw_args->[0], %defaults};
98             }
99 156         186 my %clean_arg;
100 156 100       398 if (@$raw_args % 2) {
101 2 50 33     15 return (defined $only_parameter and $only_parameter)
102             ? "Got odd number of arguments, please use key value pairs as arguments or one default argument !\n"
103             : "Got odd number of values, please use key value pairs as arguments !\n"
104             }
105 154         1787 my %arg_hash = @$raw_args;
106 154         293 for my $parameter_name (@$required_parameter){
107 83 100 100     464 if (ref $parameter_alias eq 'HASH' and exists $parameter_alias->{ $parameter_name }
      100        
108             and exists $arg_hash{ $parameter_alias->{$parameter_name} }){
109 25         106 $arg_hash{ $parameter_name } = delete $arg_hash{ $parameter_alias->{$parameter_name} };
110             }
111 83 100       219 return "Argument '$parameter_name' is missing!\n" unless exists $arg_hash{$parameter_name};
112 75         214 $clean_arg{ $parameter_name } = delete $arg_hash{ $parameter_name };
113             }
114 146         375 for my $parameter_name (keys %$optional_parameter){
115 538 100 100     814 if (ref $parameter_alias eq 'HASH' and exists $parameter_alias->{ $parameter_name }
      100        
116             and exists $arg_hash{ $parameter_alias->{$parameter_name} }){
117 10         28 $arg_hash{ $parameter_name } = delete $arg_hash{ $parameter_alias->{$parameter_name} };
118             }
119             $clean_arg{ $parameter_name } = exists $arg_hash{$parameter_name}
120             ? delete $arg_hash{ $parameter_name }
121 538 100       1029 : $optional_parameter->{ $parameter_name };
122             }
123 146 100       322 return "Inserted unknown argument(s): ".(join ',', keys %arg_hash)."\n" if %arg_hash;
124 139         310 return \%clean_arg;
125             }
126              
127             ### getter #############################################################
128             my $default_space_name = Graphics::Toolkit::Color::Space::Hub::default_space_name();
129             sub values {
130 70     70 1 20040 my ($self, @args) = @_;
131 70         435 my $arg = _split_named_args( \@args, 'in', [],
132             { in => $default_space_name, as => 'list', raw => 0,
133             precision => undef, range => undef, suffix => undef } );
134 70         144 my $help = 'The method "values" returns numeric color values and accepts six named, optional arguments: '.
135             '"in" (color space name - default arg), "as" (color definition format), "raw", "range", "precision" and "suffix"!';
136 70 50       113 return error($arg.$help.$POD_link) unless ref $arg;
137 70         135 my @result = $self->values_object->formatted( @$arg{qw/in as suffix range precision raw/} );
138 70 50       144 return error(${$result[0]}.$help.$POD_link) if ref $result[0] eq 'SCALAR';
  0         0  
139 70 100       408 return wantarray ? @result : $result[0];
140             }
141              
142             sub name {
143 37     37 1 7162 my ($self, @args) = @_;
144 37 100       163 return $self->values_object->name unless @args;
145 2         13 my $arg = _split_named_args( \@args, 'from', [], {from => 'default', all => 0, full => 0, distance => 0}, {distance => 'd'});
146 2         5 my $help = 'The method "name" returns one, several or no (empty) color name strings and accepts four named, optional arguments: '.
147             '"from" (scheme name - default arg), "all" (color names), "full" (name) and "distance" (or "d")!';
148 2 50       4 return error($arg.$help.$POD_link) unless ref $arg;
149 2         6 Graphics::Toolkit::Color::Name::from_values( $self->values_object->shaped, @$arg{qw/from all full distance/});
150             }
151              
152             sub closest_name {
153 11     11 1 6613 my ($self, @args) = @_;
154 11         51 my $arg = _split_named_args( \@args, 'from', [], {from => 'default', all => 0, full => 0});
155 11         21 my $help = 'The method "closest_name" returns one or several (in an ARRAY) color name strings and in list context also '.
156             'the numeric distance and accepts three named, optional arguments: '.
157             '"from" (scheme name - default arg), "all" (color names) and "full" (name)!';
158 11 50       17 return error($arg.$help.$POD_link) unless ref $arg;
159             my ($name, $distance) = Graphics::Toolkit::Color::Name::closest_from_values(
160 11         23 $self->values_object->shaped, @$arg{qw/from all full/});
161 11 100       59 return wantarray ? ($name, $distance) : $name;
162             }
163              
164             sub distance {
165 10     10 1 1293 my ($self, @args) = @_;
166 10         54 my $arg = _split_named_args( \@args, 'to', ['to'], {in => $default_space_name, only => undef, range => undef}, {only => 'select'});
167 10         22 my $help = 'The method "distance" returns one numeric distance value and accepts four named arguments, the first being default '.
168             'and required: "to" (definition of second color), "in" (color space name), "only" (axis selection) and "range"!';
169 10 100       25 return error($arg.$help.$POD_link) unless ref $arg;
170 7         17 my $target_color = _new_from_scalar_def( $arg->{'to'} );
171 7 50       8 return error("target color definition: $arg->{to} is ill formed".$help.$POD_link) unless ref $target_color;
172 7         16 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
173 7 50       12 return error($color_space.$help.$POD_link) unless ref $color_space;
174 7 100       10 if (defined $arg->{'only'}){
175 3 100       10 if (not ref $arg->{'only'}){
    50          
176             return error($arg->{'only'}." is not an axis name or role in color space: ".$color_space->name.$help.$POD_link)
177 2 50       6 unless $color_space->is_axis_role( $arg->{'only'} );
178             } elsif (ref $arg->{'only'} eq 'ARRAY'){
179 1         2 for my $axis_name (@{$arg->{'only'}}) {
  1         2  
180 2 50       4 return error( $axis_name." is not an axis name or role in color space: ".$color_space->name.$help.$POD_link)
181             unless $color_space->is_axis_role( $axis_name );
182             }
183 0         0 } else { return error('The "only" argument needs one axis name or an ARRAY with several axis names from '.
184             'the same color space!'.$help.$POD_link) }
185             }
186 7         14 my $range_def = $color_space->shape->try_check_range_definition( $arg->{'range'} );
187 7 50       11 return error($range_def.$help.$POD_link) unless ref $range_def;
188             Graphics::Toolkit::Color::Space::Hub::distance(
189 7         15 $self->values_object->normalized, $target_color->values_object->normalized, $color_space->name, $arg->{'only'}, $range_def );
190             }
191              
192             ## single color creation methods #######################################
193             # --- lightweight designer API ---
194             my $design_default = 'OKHSL';
195             sub lighten {
196 0     0 1 0 my ($self, @args) = @_;
197 0         0 my $arg = _split_named_args( \@args, 'by', ['by'], {in => $design_default});
198 0 0       0 return "The only argument or named argument 'by' has to be a number between 0 and 1!" unless ref $arg;
199 0         0 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::lighten( $self->values_object, $arg->{'by'}, $arg->{'in'} ) );
200             }
201             sub darken {
202 0     0 1 0 my ($self, @args) = @_;
203 0         0 my $arg = _split_named_args( \@args, 'by', ['by'], {in => $design_default});
204 0 0       0 return "The only argument or named argument 'by' has to be a number between 0 and 1!" unless ref $arg;
205 0         0 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::darken( $self->values_object, $arg->{'by'}, $arg->{'in'} ) );
206             }
207             sub saturate {
208 0     0 1 0 my ($self, @args) = @_;
209 0         0 my $arg = _split_named_args( \@args, 'by', ['by'], {in => $design_default});
210 0 0       0 return "The only argument or named argument 'by' has to be a number between 0 and 1!" unless ref $arg;
211 0         0 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::saturate( $self->values_object, $arg->{'by'}, $arg->{'in'} ) );
212             }
213             sub desaturate {
214 0     0 1 0 my ($self, @args) = @_;
215 0         0 my $arg = _split_named_args( \@args, 'by', ['by'], {in => $design_default});
216 0 0       0 return "The only argument or named argument 'by' has to be a number between 0 and 1!" unless ref $arg;
217 0         0 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::desaturate( $self->values_object, $arg->{'by'}, $arg->{'in'} ) );
218             }
219             sub tint {
220 0     0 1 0 my ($self, @args) = @_;
221 0         0 my $arg = _split_named_args( \@args, 'by', ['by'], {in => $design_default});
222 0 0       0 return "The only argument or named argument 'by' has to be a number between 0 and 1!" unless ref $arg;
223 0         0 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::tint( $self->values_object, $arg->{'by'}, $arg->{'in'} ) );
224             }
225             sub tone {
226 0     0 1 0 my ($self, @args) = @_;
227 0         0 my $arg = _split_named_args( \@args, 'by', ['by'], {in => $design_default});
228 0 0       0 return "The only argument or named argument 'by' has to be a number between 0 and 1!" unless ref $arg;
229 0         0 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::tone( $self->values_object, $arg->{'by'}, $arg->{'in'} ) );
230             }
231             sub shade {
232 0     0 1 0 my ($self, @args) = @_;
233 0         0 my $arg = _split_named_args( \@args, 'by', ['by'], {in => $design_default});
234 0 0       0 return "The only argument or named argument 'by' has to be a number between 0 and 1!" unless ref $arg;
235 0         0 _new_from_value_obj( Graphics::Toolkit::Color::Calculator::shade( $self->values_object, $arg->{'by'}, $arg->{'in'} ) );
236             }
237              
238             # --- low level complex API ---
239              
240 4     4 0 1263 sub apply { tone_curve(@_) }
241             sub tone_curve {
242 4     4 1 9 my ($self, @args) = @_;
243 4         32 my $arg = _split_named_args( \@args, undef, ['gamma'], {in => 'LinearRGB'} );
244 4         7 my $help = 'The method "tone_curve" returns a GTC object with gamma corrected values and accepts two named arguments, '.
245             'the first being required: "gamma", "in" (color space name - default LinearRGB)!';
246 4 50       8 return error($arg.$help.$POD_link) unless ref $arg;
247 4         10 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
248 4 50       7 return error($color_space.$help.$POD_link) unless ref $color_space;
249 4         9 my $result = Graphics::Toolkit::Color::Calculator::apply_gamma( $self->values_object, $arg->{'gamma'}, $color_space );
250 4 50       8 return error($result.$help.$POD_link) unless ref $result;
251 4         8 return _new_from_value_obj( $result );
252             }
253              
254             sub set_value {
255 10     10 1 2561 my ($self, @args) = @_;
256 10 50 33     41 @args = %{$args[0]} if @args == 1 and ref $args[0] eq 'HASH';
  0         0  
257 10         19 my $help = 'The method "set_value" returns a GTC object with some values replaced. Arguments are selected axis '.
258             'names of target space and optionally "in" for color space disambiguation!';
259 10 100 66     65 return error($help.$POD_link) if @args % 2 or not @args or @args > 10;
      66        
260 9         30 my $partial_color = { @args };
261 9         18 my $space_name = delete $partial_color->{'in'};
262 9         26 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $space_name );
263 9 50 33     36 return error($color_space.$help.$POD_link) if defined $color_space and not ref $color_space;
264 9         26 my $result = Graphics::Toolkit::Color::Calculator::set_value( $self->values_object, $partial_color, $space_name );
265 9 100       27 return error($result.' '.$help.$POD_link) unless ref $result;
266 7         16 return _new_from_value_obj( $result );
267             }
268             sub add_value {
269 10     10 1 1313 my ($self, @args) = @_;
270 10 50 33     37 @args = %{$args[0]} if @args == 1 and ref $args[0] eq 'HASH';
  0         0  
271 10         21 my $help = 'The method "add_value" returns a GTC object with some values different. Arguments are selected axis '.
272             'names of target space and optionally "in" for color space disambiguation!';
273 10 100 66     96 return error($help.$POD_link) if @args % 2 or not @args or @args > 10;
      66        
274 9         40 my $partial_color = { @args };
275 9         21 my $space_name = delete $partial_color->{'in'};
276 9         33 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $space_name );
277 9 50 33     39 return error($color_space.$help.$POD_link) if defined $color_space and not ref $color_space;
278 9         27 my $result = Graphics::Toolkit::Color::Calculator::add_value( $self->values_object, $partial_color, $space_name );
279 9 100       25 return error($result.' '.$help.$POD_link) unless ref $result;
280 6         16 return _new_from_value_obj( $result );
281             }
282              
283             sub mix {
284 24     24 1 5688 my ($self, @args) = @_;
285 24         185 my $arg = _split_named_args( \@args, 'to', ['to'], {in => 'OKLAB', by => undef}, {by => 'amount'});
286 24         70 my $help = 'The method "mix" returns a GTC object, which is a blend between given colors. Arguments are: '.
287             '"to" (other color[s]-required and default), "by" (mix amounts) and "in"(color space name, default OKLAB)!';
288 24 100       63 return error($arg.' '.$help.$POD_link) unless ref $arg;
289 22         77 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( delete $arg->{'in'} );
290 22 100       56 return error($color_space.' '.$help.$POD_link) unless ref $color_space;
291 21         78 my $second_color = _new_from_scalar_def($arg->{'to'});
292 21 100       46 if (ref $second_color){ $arg->{'to'} = [$second_color->values_object] }
  10         24  
293             else {
294 11 100       33 if (ref $arg->{'to'} ne 'ARRAY'){
295 2         8 return error("Target color definition (argument 'to'): '$arg->{to}' is ill formed. $second_color. ".$POD_link);
296             } else {
297 9         16 my @to = ();
298 9         11 for my $color_def (@{$arg->{'to'}}){
  9         19  
299 15 100       31 if (ref $color_def eq __PACKAGE__) { push @to, $color_def->values_object }
  9         20  
300             else {
301 6         24 $second_color = Graphics::Toolkit::Color::Values->new_from_any_input( $color_def );
302 6 50       14 return error("target color definition (argument 'to'). '$color_def' is ill formed: $second_color. ".$POD_link)
303             unless ref $second_color;
304 6         19 push @to, $second_color;
305             }
306             }
307 9         28 $arg->{'to'} = \@to;
308             }
309             }
310             # backward compatibility: 'by' > 1 is read as percent (0 .. 100) and mapped to 0 .. 1
311 19 100       48 if (defined $arg->{'by'}){
312 9 100 66     24 if (ref $arg->{'by'} eq 'ARRAY') {
    100          
313 5 100 66     8 for (@{$arg->{'by'}}) { $_ /= 100 if is_nr($_) and $_ > 1 }
  5         10  
  8         15  
314 2         6 } elsif (is_nr($arg->{'by'}) and $arg->{'by'} > 1) { $arg->{'by'} /= 100 }
315             }
316 19         44 my $result = Graphics::Toolkit::Color::Calculator::mix( $self->values_object, $arg->{'to'}, $arg->{'by'}, $color_space );
317 19 100       64 return error($result.' '.$help.$POD_link) unless ref $result;
318 17         45 return _new_from_value_obj( $result );
319             }
320              
321             sub invert {
322 16     16 1 1416 my ($self, @args) = @_;
323 16         67 my $arg = _split_named_args( \@args, 'only', [], {in => undef, only => undef});
324 16         36 my $help = 'The method "invert" returns a GTC object with inverted ($max - $_) values. Optional arguments are: '.
325             '"only" (axis selection, default is all) and "in" (color space name)!';
326 16 100 66     80 return error($arg.$help.$POD_link) unless ref $arg and (not ref $arg->{'only'} or ref $arg->{'only'} eq 'ARRAY');
      100        
327 15         62 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
328 15 50 66     58 return error($color_space.$help.$POD_link) if defined $arg->{'in'} and not ref $color_space;
329 15 100       33 $arg->{'in'} = $color_space if defined $arg->{'in'};
330 15         23 my $default_space = Graphics::Toolkit::Color::Space::Hub::get_space( 'OKHSL' );
331 15         35 my $result = Graphics::Toolkit::Color::Calculator::invert( $self->values_object, $arg->{'only'}, $arg->{'in'}, $default_space );
332 15 100       42 return error($result.$help.$POD_link) unless ref $result;
333 14         33 return _new_from_value_obj( $result );
334             }
335              
336             ## color set creation methods ##########################################
337             sub complement {
338 13     13 1 632 my ($self, @args) = @_;
339 13         83 my $arg = _split_named_args( \@args, 'steps', [], {steps => 1, tilt => 0, skew => 0, target => {}, in => $design_default});
340 13         30 my $help = 'The method "complement" returns a list of GTC objects with complementary colors. Optional arguments are: '.
341             '"steps" (color count, default 1 - default argument), "in" (color space name, default "OKHSL", "tilt", "skew" and "target")!';
342 13 100       27 return error($arg.$help.$POD_link) unless ref $arg;
343 12 100       30 return error('Optional argument "steps" has to be a number ! '.$help.$POD_link) unless is_nr($arg->{'steps'});
344 10 50       26 return error('Optional argument "steps" is zero or negative, no complement colors will be computed! '.$help.$POD_link) if $arg->{'steps'} < 1;
345 10 100       18 return error('Optional argument "tilt" has to be a number! '.$help.$POD_link) unless is_nr($arg->{'tilt'});
346 9 100       18 return error('Optional argument "skew" has to be a number! '.$help.$POD_link) unless is_nr($arg->{'skew'});
347 8 100       25 return error('Optional argument "target" has to be a HASH ref! '.$help.$POD_link) if ref $arg->{'target'} ne 'HASH';
348 7         9 my ($target_delta, $space_name);
349 7 100       6 if (keys %{$arg->{'target'}}){
  7         25  
350 2         10 ($target_delta, $space_name) = Graphics::Toolkit::Color::Space::Hub::deformat_search_partial_hash( $arg->{'target'}, 'HSL' );
351 2 100       8 return error('Optional argument "target" got HASH keys that do not fit HSL roles ("h","s","l")! '.$help.$POD_link) unless ref $target_delta;
352 5         6 } else { $target_delta = [] }
353 6         16 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
354 6 50       12 return error($color_space.$help.$POD_link) unless ref $color_space;
355 6 50       17 return error("Need a cylindrical space from the HSL family! ".$help.$POD_link) unless $color_space->family eq 'HSL';
356              
357 6         21 my @result = Graphics::Toolkit::Color::SetCalculator::complement( $self->values_object, $target_delta, @$arg{qw/steps tilt skew/}, $color_space );
358 6 50       17 return error($result[0].$help.$POD_link) unless ref $result[0];
359 6         8 map {_new_from_value_obj( $_ )} @result;
  13         28  
360             }
361              
362             sub analogous {
363 11     11 1 4117 my ($self, @args) = @_;
364 11         54 my $arg = _split_named_args( \@args, 'to', ['to'], {steps => 4, tilt => 0, in => $design_default});
365 11         24 my $help = 'The method "analogous" returns a list of GTC objects with analogous colors. Arguments are: "to" (next color - default arg. and required), '.
366             '"steps" (max. color count, default 4), "in" (color space name, default "OKHSL" and "tilt"!';
367 11 100       35 return error($arg.$help.$POD_link) unless ref $arg;
368 8         22 my $next_color = _new_from_scalar_def( $arg->{'to'} );
369 8 50       15 if (ref $next_color) { $arg->{'to'} = $next_color->values_object }
  8         18  
370 0         0 else { return error('Argument "to" contains malformed color definition! '.$next_color.$POD_link) }
371 8 100       17 return error('Optional argument "steps" has to be a number ! '.$help.$POD_link) unless is_nr($arg->{'steps'});
372 6 50 33     14 return error('Optional argument "steps" has to be a number greater equal two! '.$help.$POD_link) unless is_nr($arg->{'steps'}) and $arg->{'steps'} >= 2;
373 6 100       12 return error('Optional argument "tilt" has to be a number! '.$help.$POD_link) unless is_nr($arg->{'tilt'});
374 4         11 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
375 4 100       13 return error($color_space.$help.$POD_link) unless ref $color_space;
376            
377 3         8 my @result = Graphics::Toolkit::Color::SetCalculator::analogous( $self->values_object, $arg->{'to'}, @$arg{qw/steps tilt/}, $color_space);
378 3 50       8 return error($result[0].$help.$POD_link) unless ref $result[0];
379 3         4 map {_new_from_value_obj( $_ )} @result;
  9         13  
380             }
381              
382             sub gradient {
383 10     10 1 4187 my ($self, @args) = @_;
384 10         66 my $arg = _split_named_args( \@args, 'to', ['to'], {steps => 10, tilt => 0, in => 'OKLAB'});
385 10         21 my $help = 'The method "gradient" returns a list of GTC objects with a gradual transition between colors. Arguments are: '.
386             '"to" (next color - default arg. and required), "steps" (color count, default 10), "in" (color space name, default "OKLAB" and "tilt")!';
387 10 100       28 return error($arg.$help.$POD_link) unless ref $arg;
388 9         36 my @colors = ($self->values_object);
389 9         30 my $target_color = _new_from_scalar_def( $arg->{'to'} );
390 9 100       22 if (ref $target_color) {
391 6         14 push @colors, $target_color->values_object }
392             else {
393 3 100 66     18 return error('Argument "to" contains malformed color definition! '.$help.$POD_link) if ref $arg->{'to'} ne 'ARRAY' or not @{$arg->{'to'}};
  2         8  
394 2         3 for my $color_def (@{$arg->{'to'}}){
  2         5  
395 5         28 my $target_color = _new_from_scalar_def( $color_def );
396 5 100       18 return error('Argument "to" contains malformed color definition: '.$color_def.'! '.$help.$POD_link) unless ref $target_color;
397 4         11 push @colors, $target_color->values_object;
398             }
399             }
400 7 50 33     22 return error('Argument "steps" has to be a number greater equel two! '.$help.$POD_link) unless is_nr($arg->{'steps'}) and $arg->{'steps'} >= 2;
401 7         15 $arg->{'steps'} = int $arg->{'steps'};
402 7 50       16 return error('Argument "tilt" has to be a number! '.$help.$POD_link) unless is_nr($arg->{'tilt'});
403 7         22 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
404 7 50       15 return error($color_space.$help.$POD_link) unless ref $color_space;
405            
406 7         30 my @result = Graphics::Toolkit::Color::SetCalculator::gradient( \@colors, @$arg{qw/steps tilt/}, $color_space);
407 7 50       18 return error($result[0].$help.$POD_link) unless ref $result[0];
408 7         16 map {_new_from_value_obj( $_ )} @result;
  53         63  
409             }
410              
411             sub cluster {
412 20     20 1 9756 my ($self, @args) = @_;
413 20         172 my $arg = _split_named_args( \@args, undef, ['radius', 'minimal_distance'], {in => 'OKLAB'}, {radius => 'r', minimal_distance => 'min_d'});
414 20         103 my $help = 'The method "cluster" returns a list of GTC objects with similar but distinct colors. The arguments are: '.
415             '"radius" (max. distance from center, alias "r", required), "minimal_distance" (between colors, required) and "in" (color space name, default "OKLAB")!';
416 20 100       84 return error($arg.$help.$POD_link) unless ref $arg;
417 14         55 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
418 14 100       53 return error($color_space.$help.$POD_link) unless ref $color_space;
419             return error('Argument "radius" has to be a non-negative number or an ARRAY of numbers that holds for each space axis a radius value. '.$help.$POD_link)
420 13 100 100     46 unless (is_nr($arg->{'radius'}) and $arg->{'radius'} >= 0) or $color_space->is_number_tuple( $arg->{'radius'} );
      100        
421             return error('Argument "minimal_distance" (or "min_d") has to be a number greater zero! '.$help.$POD_link)
422 9 100 100     30 unless is_nr($arg->{'minimal_distance'}) and $arg->{'minimal_distance'} > 0;
423             return error('Ball shaped cluster works only in spaces with three dimensions! '.$help.$POD_link)
424 7 100 100     25 if $color_space->axis_count > 3 and not ref $arg->{'radius'};
425              
426 6         27 my @result = Graphics::Toolkit::Color::SetCalculator::cluster( $self->values_object, @$arg{qw/radius minimal_distance/}, $color_space);
427 6 50       20 return error($result[0].$help.$POD_link) unless ref $result[0];
428 6         9 map {_new_from_value_obj( $_ )} @result;
  55         58  
429             }
430              
431             1;
432              
433             __END__