File Coverage

lib/Graphics/Toolkit/Color.pm
Criterion Covered Total %
statement 208 222 93.6
branch 92 128 71.8
condition 29 45 64.4
subroutine 35 39 89.7
pod 10 29 34.4
total 374 463 80.7


line stmt bran cond sub pod time code
1              
2             # read only color holding object with methods for relation, mixing and transitions
3              
4             package Graphics::Toolkit::Color;
5             our $VERSION = '1.71';
6 4     4   302663 use v5.12;
  4         69  
7 4     4   21 use warnings;
  4         6  
  4         109  
8              
9 4     4   30 use Carp;
  4         6  
  4         265  
10 4     4   1801 use Graphics::Toolkit::Color::Name;
  4         11  
  4         207  
11 4     4   29 use Graphics::Toolkit::Color::Values;
  4         8  
  4         88  
12              
13 4     4   20 use Exporter 'import';
  4         6  
  4         12538  
14             our @EXPORT_OK = qw/color/;
15              
16             my $new_help = 'constructor of Graphics::Toolkit::Color object needs either:'.
17             ' 1. hash or ref (RGB, HSL or any other): ->new(r => 255, g => 0, b => 0), ->new({ h => 0, s => 100, l => 50 })'.
18             ' 2. RGB array or ref: ->new( [255, 0, 0 ]) or >new( 255, 0, 0 )'.
19             ' 3. hex form "#FF0000" or "#f00" 4. a name: "red" or "SVG:red".';
20              
21             ## constructor #########################################################
22              
23 10     10 1 656 sub color { Graphics::Toolkit::Color->new ( @_ ) }
24              
25             sub new {
26 48     48 1 14076 my ($pkg, @args) = @_;
27 48 100 66     236 @args = ([@args]) if @args == 3 or Graphics::Toolkit::Color::Space::Hub::is_space( $args[0]);
28 48 100 66     198 @args = ({ @args }) if @args == 6 or @args == 8;
29 48 100       149 return carp $new_help unless @args == 1;
30 43         94 _new_from_scalar($args[0]);
31             }
32             sub _new_from_scalar {
33 167     167   253 my ($color_def) = shift;
34 167         230 my ($value_obj, @rgb, $name, $origin);
35             # strings that are not '#112233' or 'rgb: 23,34,56'
36 167 100 100     814 if (not ref $color_def and substr($color_def, 0, 1) =~ /\w/ and $color_def !~ /,/){
    100 66        
37 15         36 $name = $color_def;
38 15         29 $origin = 'name';
39 15         29 my $i = index( $color_def, ':');
40 15 100       33 if ($i > -1 ){ # resolve pallet:name
41 1         4 my $pallet_name = substr $color_def, 0, $i;
42 1         5 my $color_name = Graphics::Toolkit::Color::Name::_clean(substr $color_def, $i+1);
43 1         5 my $module_base = 'Graphics::ColorNames';
44 1     1   252 eval "use $module_base";
  0         0  
  0         0  
  1         83  
45 1 50       35 return carp "$module_base is not installed, but it's needed to load external colors" if $@;
46 0         0 my $module = $module_base.'::'.$pallet_name;
47 0         0 eval "use $module";
48 0 0       0 return carp "$module is not installed, but needed to load color '$pallet_name:$color_name'" if $@;
49              
50 0         0 my $pallet = Graphics::ColorNames->new( $pallet_name );
51 0         0 @rgb = $pallet->rgb( $color_name );
52 0 0       0 return carp "color '$color_name' was not found, propably not part of $module" unless @rgb == 3;
53             } else { # resolve name ->
54 14         47 @rgb = Graphics::Toolkit::Color::Name::rgb_from_name( $color_def );
55 14 100       72 return carp "'$color_def' is an unknown color name, please check Graphics::Toolkit::Color::Name::all()." unless @rgb == 3;
56             }
57 13         68 $value_obj = Graphics::Toolkit::Color::Values->new( [@rgb] );
58             } elsif (ref $color_def eq __PACKAGE__) { # enables color objects to be passed as arguments
59 37         77 $name = $color_def->name;
60 37         120 $value_obj = Graphics::Toolkit::Color::Values->new( $color_def->{'values'}->string );
61             } else { # define color by numbers in any format
62 115         345 my $value_obj = Graphics::Toolkit::Color::Values->new( $color_def );
63 115 100       4960 return unless ref $value_obj;
64 105         215 return _new_from_value_obj($value_obj);
65             }
66 50         232 bless {name => $name, values => $value_obj};
67             }
68             sub _new_from_value_obj {
69 128     128   1531 my ($value_obj) = @_;
70 128 100       271 return unless ref $value_obj eq 'Graphics::Toolkit::Color::Values';
71 125         392 bless {name => scalar Graphics::Toolkit::Color::Name::name_from_rgb( $value_obj->get() ), values => $value_obj};
72             }
73              
74             ## getter ##############################################################
75              
76 81     81 1 3626 sub name { $_[0]{'name'} }
77              
78 3 100   3 0 37 sub string { $_[0]{'name'} || $_[0]->{'values'}->string }
79 24     24 0 68 sub rgb { $_[0]->values( ) }
80 10     10 0 5505 sub red {($_[0]->values( in => 'rgb'))[0] }
81 10     10 0 48 sub green {($_[0]->values( in => 'rgb'))[1] }
82 10     10 0 35 sub blue {($_[0]->values( in => 'rgb'))[2] }
83 8     8 0 22 sub rgb_hex { $_[0]->values( in => 'rgb', as => 'hex') }
84 4     4 0 16 sub rgb_hash { $_[0]->values( in => 'rgb', as => 'hash') }
85 24     24 0 58 sub hsl { $_[0]->values( in => 'hsl') }
86 9     9 0 32 sub hue {($_[0]->values( in => 'hsl'))[0] }
87 9     9 0 27 sub saturation {($_[0]->values( in => 'hsl'))[1] }
88 9     9 0 24 sub lightness {($_[0]->values( in => 'hsl'))[2] }
89 4     4 0 12 sub hsl_hash { $_[0]->values( in => 'hsl', as => 'hash') }
90              
91             sub values {
92 166     166 1 2151 my ($self) = shift;
93 166 50       620 my %args = (not @_ % 2) ? @_ :
    100          
94             (@_ == 1) ? (in => $_[0])
95             : return carp "accept three optional, named arguments: in => 'HSL', as => 'css_string', range => 16";
96 166         723 $self->{'values'}->get( $args{'in'}, $args{'as'}, $args{'range'} );
97             }
98              
99             ## measurement methods ##############################################################
100              
101 0     0 0 0 sub distance_to { distance(@_) }
102             sub distance {
103 70     70 1 150 my ($self) = shift;
104 70 50       334 my %args = (not @_ % 2) ? @_ :
    100          
105             (@_ == 1) ? (to => $_[0])
106             : return carp "accept four optional, named arguments: to => 'color or color definition', in => 'RGB', metric => 'r', range => 16";
107 70         166 my ($c2, $space_name, $select, $range) = ($args{'to'}, $args{'in'}, $args{'select'}, $args{'range'});
108 70 50       254 return carp "missing argument: color object or scalar color definition" unless defined $c2;
109 70         142 $c2 = _new_from_scalar( $c2 );
110 70 50       185 return carp "second color for distance calculation (named argument 'to') is badly defined" unless ref $c2 eq __PACKAGE__;
111 70         209 $self->{'values'}->distance( $c2->{'values'}, $space_name, $select, $range );
112             }
113              
114             ## single color creation methods #######################################
115              
116             sub _get_arg_hash {
117 32 100   32   152 my $arg = (ref $_[0] eq 'HASH') ? $_[0]
    100          
118             : (not @_ % 2) ? {@_}
119             : {} ;
120 32 100       125 return (keys %$arg) ? $arg : carp "need arguments as hash (with or without braces)";
121             }
122              
123             sub set {
124 8     8 1 1537 my ($self, @args) = @_;
125 8         18 my $arg = _get_arg_hash( @args );
126 8 100       1251 return unless ref $arg;
127 6         23 _new_from_value_obj( $self->{'values'}->set( $arg ) );
128             }
129              
130             sub add {
131 6     6 1 164 my ($self, @args) = @_;
132 6         13 my $arg = _get_arg_hash( @args );
133 6 100       550 return unless ref $arg;
134 5         17 _new_from_value_obj( $self->{'values'}->add( $arg ) );
135             }
136              
137 0     0 0 0 sub blend_with { $_[0]->blend( with => $_[1], pos => $_[2], in => 'HSL') }
138             sub blend {
139 12     12 1 1733 my ($self, @args) = @_;
140 12         26 my $arg = _get_arg_hash( @args );
141 12 50       45 return unless ref $arg;
142 12         41 my $c2 = _new_from_scalar( $arg->{'with'} );
143 12 50       28 return croak "need a second color under the key 'with' ( with => { h=>1, s=>2, l=>3 })" unless ref $c2;
144 12   66     51 my $pos = $arg->{'pos'} // $arg->{'position'} // 0.5;
      50        
145 12   100     35 my $space_name = $arg->{'in'} // 'HSL';
146 12 50       25 return carp "color space $space_name is unknown" unless Graphics::Toolkit::Color::Space::Hub::is_space( $space_name );
147 12         32 _new_from_value_obj( $self->{'values'}->blend( $c2->{'values'}, $pos, $space_name ) );
148             }
149              
150             ## color set creation methods ##########################################
151              
152              
153             # for compatibility
154 4     4 0 11 sub gradient_to { hsl_gradient_to( @_ ) }
155 0     0 0 0 sub rgb_gradient_to { $_[0]->gradient( to => $_[1], steps => $_[2], dynamic => $_[3], in => 'RGB' ) }
156 4     4 0 13 sub hsl_gradient_to { $_[0]->gradient( to => $_[1], steps => $_[2], dynamic => $_[3], in => 'HSL' ) }
157             sub gradient { # $to ~in + steps +dynamic +variance --> @_
158 6     6 1 37 my ($self, @args) = @_;
159 6         16 my $arg = _get_arg_hash( @args );
160 6 50       17 return unless ref $arg eq 'HASH';
161 6         15 my $c2 = _new_from_scalar( $arg->{'to'} );
162 6 50       42 return croak "need a second color under the key 'to' : ( to => ['HSL', 10, 20, 30])" unless ref $c2;
163 6   100     18 my $space_name = $arg->{'in'} // 'HSL';
164 6   50     13 my $steps = int(abs($arg->{'steps'} // 3));
165 6   100     15 my $power = $arg->{'dynamic'} // 0;
166 6 100       14 $power = ($power >= 0) ? $power + 1 : -(1/($power-1));
167 6 100       23 return $self if $steps == 1;
168 5         11 my $space = Graphics::Toolkit::Color::Space::Hub::get_space( $space_name );
169 5 50       13 return carp "color space $space_name is unknown" unless ref $space;
170 5         13 my @val1 = $self->{'values'}->get( $space_name, 'list', 'normal' );
171 5         13 my @val2 = $c2->{'values'}->get( $space_name, 'list', 'normal' );
172 5         14 my @delta_val = $space->delta (\@val1, \@val2 );
173 5         9 my @colors = ();
174 5         10 for my $nr (1 .. $steps-2){
175 7         31 my $pos = ($nr / ($steps-1)) ** $power;
176 7         19 my @rval = map {$val1[$_] + ($pos * $delta_val[$_])} 0 .. $space->dimensions - 1;
  21         41  
177 7         19 @rval = $space->denormalize ( \@rval );
178 7         22 push @colors, _new_from_scalar( [ $space_name, @rval ] );
179             }
180 5         53 return $self, @colors, $c2;
181             }
182              
183              
184             my $comp_help = 'set constructor "complement" accepts 4 named args: "steps" (positive int), '.
185             '"hue_tilt" or "h" (-180 .. 180), '.
186             '"saturation_tilt or "s" (-100..100) or { s => (-100..100), h => (-180..180)} and '.
187             '"lightness_tilt or "l" (-100..100) or { l => (-100..100), h => (-180..180)}';
188 3     3 0 616 sub complementary { complement(@_) }
189             sub complement { # +steps +hue_tilt +saturation_tilt +lightness_tilt --> @_
190 8     8 1 17 my ($self) = shift;
191 8 0       31 my %arg = (not @_ % 2) ? @_ :
    50          
192             (@_ == 1) ? (steps => $_[0]) : return carp $comp_help;
193 8   100     30 my $steps = int abs($arg{'steps'} // 1);
194             my $hue_tilt = (exists $arg{'h'}) ? (delete $arg{'h'}) :
195 8 100       21 (exists $arg{'hue_tilt'}) ? (delete $arg{'hue_tilt'}) : 0;
    50          
196 8 50       18 return carp $comp_help if ref $hue_tilt;
197             my $saturation_tilt = (exists $arg{'s'}) ? (delete $arg{'s'}) :
198 8 100       23 (exists $arg{'saturation_tilt'}) ? (delete $arg{'saturation_tilt'}) : 0;
    100          
199 8 50 66     18 return carp $comp_help if ref $saturation_tilt and ref $saturation_tilt ne 'HASH';
200 8         12 my $saturation_axis_offset = 0;
201 8 100       17 if (ref $saturation_tilt eq 'HASH'){
202 1         3 my ($pos_hash, $space_name) = Graphics::Toolkit::Color::Space::Hub::partial_hash_deformat( $saturation_tilt );
203 1 50 33     17 return carp $comp_help if not defined $space_name or $space_name ne 'HSL' or not exists $pos_hash->{1};
      33        
204 1 50       12 $saturation_axis_offset = $pos_hash->{0} if exists $pos_hash->{0};
205 1         3 $saturation_tilt = $pos_hash->{1};
206             }
207             my $lightness_tilt = (exists $arg{'l'}) ? (delete $arg{'l'}) :
208 8 100       19 (exists $arg{'lightness_tilt'}) ? (delete $arg{'lightness_tilt'}) : 0;
    100          
209 8 50 66     22 return carp $comp_help if ref $lightness_tilt and ref $lightness_tilt ne 'HASH';
210 8         9 my $lightness_axis_offset = 0;
211 8 100       14 if (ref $lightness_tilt eq 'HASH'){
212 1         6 my ($pos_hash, $space_name) = Graphics::Toolkit::Color::Space::Hub::partial_hash_deformat( $lightness_tilt );
213 1 50 33     13 return carp $comp_help if not defined $space_name or $space_name ne 'HSL' or not exists $pos_hash->{2};
      33        
214 1 50       4 $lightness_axis_offset = $pos_hash->{0} if exists $pos_hash->{0};
215 1         3 $lightness_tilt = $pos_hash->{2};
216             }
217              
218 8         17 my @hsl2 = my @hsl = $self->values('HSL');
219 8         35 my @hue_turn_point = ($hsl[0] + 90, $hsl[0] + 270, 800); # Dmax, Dmin and Pseudo-Inf
220 8         14 my @sat_turn_point = ($hsl[0] + 90, $hsl[0] + 270, 800);
221 8         12 my @light_turn_point = ($hsl[0] + 90, $hsl[0] + 270, 800);
222 8         12 my $sat_max_hue = $hsl[0] + 90 + $saturation_axis_offset;
223 8         21 my $sat_step = $saturation_tilt * 4 / $steps;
224 8         9 my $light_max_hue = $hsl[0] + 90 + $lightness_axis_offset;
225 8         13 my $light_step = $lightness_tilt * 4 / $steps;
226 8 100       13 if ($saturation_axis_offset){
227 1         7 $sat_max_hue -= 360 while $sat_max_hue > $hsl[0]; # putting dmax in range
228 1         4 $sat_max_hue += 360 while $sat_max_hue <= $hsl[0]; # above c1->hue
229 1         3 my $dmin_first = $sat_max_hue > $hsl[0] + 180;
230 1 50       5 @sat_turn_point = $dmin_first ? ($sat_max_hue - 180, $sat_max_hue, 800)
231             : ($sat_max_hue, $sat_max_hue + 180, 800);
232 1 50       3 $sat_step = - $sat_step if $dmin_first;
233 1 50       15 my $sat_start_delta = $dmin_first ? ((($sat_max_hue - 180 - $hsl[0]) / 90 * $saturation_tilt) - $saturation_tilt)
234             : (-(($sat_max_hue - $hsl[0]) / 90 * $saturation_tilt) + $saturation_tilt);
235 1         3 $hsl[1] += $sat_start_delta;
236 1         2 $hsl2[1] -= $sat_start_delta;
237             }
238 8 100       15 if ($lightness_axis_offset){
239 1         5 $light_max_hue -= 360 while $light_max_hue > $hsl[0];
240 1         12 $light_max_hue += 360 while $light_max_hue <= $hsl[0];
241 1         4 my $dmin_first = $light_max_hue > $hsl[0] + 180;
242 1 50       6 @light_turn_point = $dmin_first ? ($light_max_hue - 180, $light_max_hue, 800)
243             : ($light_max_hue, $light_max_hue + 180, 800);
244 1 50       3 $light_step = - $light_step if $dmin_first;
245 1 50       5 my $light_start_delta = $dmin_first ? ((($light_max_hue - 180 - $hsl[0]) / 90 * $lightness_tilt) - $lightness_tilt)
246             : (-(($light_max_hue - $hsl[0]) / 90 * $lightness_tilt) + $lightness_tilt);
247 1         2 $hsl[2] += $light_start_delta;
248 1         1 $hsl2[2] -= $light_start_delta;
249             }
250 8         26 my $c1 = _new_from_scalar( [ 'HSL', @hsl ] );
251 8         24 $hsl2[0] += 180 + $hue_tilt;
252 8         23 my $c2 = _new_from_scalar( [ 'HSL', @hsl2 ] ); # main complementary color
253 8 100       57 return $c2 if $steps < 2;
254 4 50       32 return $c1, $c2 if $steps == 2;
255              
256 4         8 my (@result) = $c1;
257 4         35 my $hue_avg_step = 360 / $steps;
258 4         12 my $hue_c2_distance = $self->distance( to => $c2, in => 'HSL', select => 'hue');
259 4         10 my $hue_avg_tight_step = $hue_c2_distance * 2 / $steps;
260 4         12 my $hue_sec_deg_delta = 8 * ($hue_avg_step - $hue_avg_tight_step) / $steps; # second degree delta
261 4 50       10 $hue_sec_deg_delta = -$hue_sec_deg_delta if $hue_tilt < 0; # if c2 on right side
262 4         6 my $hue_last_step = my $hue_ak_step = $hue_avg_step; # bar height of pseudo integral
263 4         6 my $hue_current = my $hue_current_naive = $hsl[0];
264 4         6 my $saturation_current = $hsl[1];
265 4         5 my $lightness_current = $hsl[2];
266 4         5 my $hi = my $si = my $li = 0; # index of next turn point where hue step increase gets flipped (at Dmax and Dmin)
267 4         9 for my $i (1 .. $steps - 1){
268 13         23 $hue_current_naive += $hue_avg_step;
269              
270 13 100       27 if ($hue_current_naive >= $hue_turn_point[$hi]){
271 6         15 my $bar_width = ($hue_turn_point[$hi] - $hue_current_naive + $hue_avg_step) / $hue_avg_step;
272 6         10 $hue_ak_step += $hue_sec_deg_delta * $bar_width;
273 6         11 $hue_current += ($hue_ak_step + $hue_last_step) / 2 * $bar_width;
274 6         7 $hue_last_step = $hue_ak_step;
275 6         6 $bar_width = 1 - $bar_width;
276 6         7 $hue_sec_deg_delta = -$hue_sec_deg_delta;
277 6         10 $hue_ak_step += $hue_sec_deg_delta * $bar_width;
278 6         9 $hue_current += ($hue_ak_step + $hue_last_step) / 2 * $bar_width;
279 6         7 $hi++;
280             } else {
281 7         11 $hue_ak_step += $hue_sec_deg_delta;
282 7         11 $hue_current += ($hue_ak_step + $hue_last_step) / 2;
283             }
284 13         27 $hue_last_step = $hue_ak_step;
285              
286 13 100       21 if ($hue_current_naive >= $sat_turn_point[$si]){
287 6         12 my $bar_width = ($sat_turn_point[$si] - $hue_current_naive + $hue_avg_step) / $hue_avg_step;
288 6         9 $saturation_current += $sat_step * ((2 * $bar_width) - 1);
289 6         10 $sat_step = -$sat_step;
290 6         8 $si++;
291             } else {
292 7         10 $saturation_current += $sat_step;
293             }
294              
295 13 100       18 if ($hue_current_naive >= $light_turn_point[$li]){
296 5         8 my $bar_width = ($light_turn_point[$li] - $hue_current_naive + $hue_avg_step) / $hue_avg_step;
297 5         7 $lightness_current += $light_step * ((2 * $bar_width) - 1);
298 5         7 $light_step = -$light_step;
299 5         6 $li++;
300             } else {
301 8         9 $lightness_current += $light_step;
302             }
303              
304 13         48 $result[$i] = _new_from_scalar( [ HSL => $hue_current, $saturation_current, $lightness_current ] );
305             }
306              
307 4         58 return @result;
308             }
309              
310             sub bowl {# +radius +distance|count +variance ~in @range
311 0     0 0   my ($self, @args) = @_;
312 0           my $arg = _get_arg_hash( @args );
313 0 0         return unless ref $arg eq 'HASH';
314              
315             }
316              
317             1;
318              
319             __END__