File Coverage

lib/Graphics/Toolkit/Color/SetCalculator.pm
Criterion Covered Total %
statement 144 144 100.0
branch 29 36 80.5
condition 5 5 100.0
subroutine 7 7 100.0
pod 0 4 0.0
total 185 196 94.3


line stmt bran cond sub pod time code
1              
2             # color value operation generating color sets
3              
4             package Graphics::Toolkit::Color::SetCalculator;
5 6     6   254242 use v5.12;
  6         41  
6 6     6   23 use warnings;
  6         18  
  6         258  
7 6     6   2539 use Graphics::Toolkit::Color::Calculator;
  6         18  
  6         8713  
8              
9             ########################################################################
10             sub complement { # -- :start_values, @target_delta, +steps, +tilt, +skew, :space --> @:values
11 14     14 0 440 my ($start_color, $target_delta, $steps, $tilt, $skew, $color_space) = @_;
12 14 50       50 return unless ref $color_space eq 'Graphics::Toolkit::Color::Space';
13 14 50       47 return 'need a cylindrical color space from the HSL family as color space' unless $color_space->family eq 'HSL';
14 14         42 my $axis_position = {
15             h => $color_space->pos_from_axis_role('hue'),
16             s => $color_space->pos_from_axis_role('saturation'),
17             l => $color_space->pos_from_axis_role('lightness'),
18             };
19 14         39 my $hue_half_max = $color_space->shape->axis_value_max( $axis_position->{'h'} ) / 2;
20              
21 14         48 my $start_tuple = $start_color->shaped( $color_space->name );
22 14         32 $start_tuple = $color_space->rotate( $start_tuple );
23 14         34 my $target_values = [@$start_tuple]; # target = THE complement + usr changes
24 14         31 $target_values->[$axis_position->{'h'}] += $hue_half_max;
25 14   100     89 $target_delta->[$_] //= 0 for 0 .. 2;
26 14         42 $target_values->[$_] += $target_delta->[$_] for 0 .. 2;
27 14         38 $target_values = $color_space->clamp( $target_values );
28              
29 14         49 $target_delta->[$axis_position->{'s'}] = $target_values->[$axis_position->{'s'}] - $start_tuple->[$axis_position->{'s'}];
30 14         38 $target_delta->[$axis_position->{'l'}] = $target_values->[$axis_position->{'l'}] - $start_tuple->[$axis_position->{'l'}];
31              
32 14         18 my $result_count = int abs $steps;
33 14         23 my $scaling_exponent = abs($tilt) + 1;
34 14         40 my @hue_pos_normal = map {($_ * 2 / $result_count) ** $scaling_exponent} 1 .. ($result_count - 1) / 2;
  10         44  
35 14 100       34 @hue_pos_normal = map {1 - $_} reverse @hue_pos_normal if $tilt > 0; # reverse tilt effect if tilt negative
  4         11  
36              
37 14         39 my $hue_target_delta = $hue_half_max + $target_delta->[$axis_position->{'h'}]; # real value size of half complement circle
38 14         21 my @result = ();
39 14         31 for my $hue_position (@hue_pos_normal){
40 10         13 my $tuple = [];
41 10         43 $tuple->[$axis_position->{'h'}] = $start_tuple->[$axis_position->{'h'}] + ($hue_target_delta * $hue_position);
42 10         26 $tuple->[$axis_position->{'s'}] = $start_tuple->[$axis_position->{'s'}] + ($target_delta->[$axis_position->{'s'}] * $hue_position);
43 10         33 $tuple->[$axis_position->{'l'}] = $start_tuple->[$axis_position->{'l'}] + ($target_delta->[$axis_position->{'l'}] * $hue_position);
44 10 100       39 $tuple->[$axis_position->{'l'}] -= ($hue_position <= 0.5) ? ($skew * $hue_position * 2) : ($skew * (2 - ( $hue_position * 2)));
45 10         23 push @result, Graphics::Toolkit::Color::Values->new_from_tuple( $tuple, $color_space->name );
46             }
47 14 100 100     66 push @result, Graphics::Toolkit::Color::Values->new_from_tuple( $target_values, $color_space->name)
48             if $result_count == 1 or not $result_count % 2;
49 14         28 $hue_target_delta = $hue_half_max - $target_delta->[$axis_position->{'h'}];
50 14         29 @hue_pos_normal = map {1 - $_} reverse @hue_pos_normal;
  10         21  
51 14         20 for my $hue_position (@hue_pos_normal){
52 10         13 my $tuple = [];
53 10         26 $tuple->[$axis_position->{'h'}] = $target_values->[$axis_position->{'h'}] + ($hue_target_delta * $hue_position);
54 10         28 $tuple->[$axis_position->{'s'}] = $target_values->[$axis_position->{'s'}] - ($target_delta->[$axis_position->{'s'}] * $hue_position);
55 10         23 $tuple->[$axis_position->{'l'}] = $target_values->[$axis_position->{'l'}] - ($target_delta->[$axis_position->{'l'}] * $hue_position);
56 10 100       30 $tuple->[$axis_position->{'l'}] += ($hue_position <= 0.5) ? ($skew * $hue_position * 2) : ($skew * (2 - ( $hue_position * 2)));
57 10         17 push @result, Graphics::Toolkit::Color::Values->new_from_tuple( $tuple, $color_space->name );
58             }
59 14 100       31 push @result, $start_color if $result_count > 1;
60 14         102 return @result;
61             }
62              
63             ########################################################################
64             sub analogous { # :start_values, :next_values -- +steps, +tilt, :space --> @:values
65 3     3 0 9 my ($start_color, $next_color, $steps, $tilt, $color_space) = @_;
66 3         3 $steps = int $steps;
67 3 50       8 return $start_color if $steps == 1;
68 3 50       5 return $start_color, $next_color if $steps == 2;
69 3         7 my @result = ($start_color, $next_color);
70 3         28 my $start_tuple = $start_color->normalized( $color_space->name );
71 3         7 my $next_tuple = $next_color->normalized( $color_space->name );
72 3         13 my $delta_tuple = $color_space->delta( $start_tuple, $next_tuple );
73 3         7 for my $color_nr (3 .. $steps){
74 4         9 for my $axis_nr ($color_space->basis->axis_iterator){
75 12         17 $delta_tuple->[$axis_nr] *= 1 + $tilt;
76 12         15 $next_tuple->[$axis_nr] += $delta_tuple->[$axis_nr];
77             }
78 4         9 my $next_color = $start_color->new_from_tuple( $next_tuple, $color_space->name, 'normal', 'raw' );
79 4 100       12 last unless $next_color->is_in_gamut( $color_space->name );
80 3         7 push @result, $next_color;
81             }
82 3         17 return @result;
83             }
84              
85             ########################################################################
86             sub gradient { # @:color_values -- +steps, +tilt, :space --> @:values
87 15     15 0 1758 my ($colors, $steps, $tilt, $color_space) = @_;
88 15         34 my $scaling_exponent = abs($tilt) + 1; # tilt = exponential scaling
89 15         34 my $segment_count = @$colors - 1;
90 15         47 my @percent_in_gradient = map {(($_-1) / ($steps-1)) ** $scaling_exponent} 2 .. $steps - 1;
  55         137  
91 15 100       47 @percent_in_gradient = map {1 - $_} reverse @percent_in_gradient if $tilt < 0;
  1         5  
92 15         34 my @result = ($colors->[0]);
93 15         39 for my $step_nr (2 .. $steps - 1){
94 55         85 my $percent_in_gradient = $percent_in_gradient[$step_nr-2];
95 55         77 my $current_segment_nr = int ($percent_in_gradient * $segment_count);
96 55         89 my $percent_in_segment = $segment_count * ($percent_in_gradient - ($current_segment_nr / $segment_count));
97 55         170 push @result, Graphics::Toolkit::Color::Calculator::mix(
98             $colors->[$current_segment_nr], [ $colors->[$current_segment_nr+1] ], $percent_in_segment, $color_space );
99             }
100 15 50       46 push @result, pop @$colors if $steps > 1;
101 15         115 return @result;
102             }
103              
104             ########################################################################
105             my $adj_len_at_45_deg = sqrt(2) / 2;
106              
107             sub cluster { # :center_values, @+|+radius +min_distance -- :space --> @:values
108 16     16 0 4841 my ($center_color, $cluster_radius, $color_distance, $color_space) = @_;
109 16         66 my $center_tuple = $center_color->shaped( $color_space->name );
110 16         33 my $center_x = $center_tuple->[0];
111 16         24 my $center_y = $center_tuple->[1];
112 16         53 my $center_z = $center_tuple->[2];
113 16         18 my @result_values;
114 16 100       43 if (ref $cluster_radius) { # cuboid shaped cluster
115 8         20 my $colors_in_direction = int $cluster_radius->[0] / $color_distance;
116 8         16 my $corner_value = $center_tuple->[0] - ($colors_in_direction * $color_distance);
117 8         23 @result_values = map {[$corner_value + ($_ * $color_distance)]} 0 .. 2 * $colors_in_direction;
  18         33  
118 8         23 for my $axis_index (1 .. $color_space->axis_count - 1){
119 17         41 my $colors_in_direction = int $cluster_radius->[$axis_index] / $color_distance;
120 17         22 my $corner_value = $center_tuple->[$axis_index] - ($colors_in_direction * $color_distance);
121             @result_values = map {
122 17         21 my @good_values = @$_[0 .. $axis_index-1];
  77         114  
123 77         94 map {[@good_values, ($corner_value + ($_ * $color_distance))]} 0 .. 2 * $colors_in_direction;
  285         557  
124             } @result_values;
125             }
126             } else { # ball shaped cluster (FCC)
127 8         24 my $layer_distance = sqrt( 2 * $color_distance * $color_distance ) / 2;
128 8         21 for my $layer_nr (0 .. $cluster_radius / $layer_distance){
129 16         20 my $layer_height = $layer_nr * $layer_distance;
130 16         22 my $layer_z_up = $center_z + $layer_height;
131 16         17 my $layer_z_dn = $center_z - $layer_height;
132 16         37 my $layer_radius = sqrt( ($cluster_radius**2) - ($layer_height**2) );
133 16         20 my $radius_in_colors = $layer_radius / $color_distance;
134 16 100       46 if ($layer_nr % 2){ # odd layer of cuboctahedral packing
135 7         12 my $contour_cursor = int ($radius_in_colors - 0.5);
136 7         10 my $grid_row_count = ($radius_in_colors * $adj_len_at_45_deg) - .5;
137 7 50       16 next if $grid_row_count < 0;
138 7         12 my @grid = ();
139 7         13 for my $x_index (0 .. $grid_row_count){
140 7 100       24 $contour_cursor-- if sqrt( (($contour_cursor+.5)**2) + (($x_index+.5)**2) ) > $radius_in_colors;
141 7         12 $grid[$x_index] = $contour_cursor;
142 7         14 $grid[$contour_cursor] = $x_index;
143             }
144 7         16 for my $x_index (0 .. $#grid){
145 8         14 my $delta_x = (0.5 + $x_index) * $color_distance;
146 8         14 my ($x1, $x2) = ($center_x + $delta_x, $center_x - $delta_x);
147 8         14 for my $y_index (0 .. $grid[$x_index]){
148 9         11 my $delta_y = (0.5 + $y_index) * $color_distance;
149 9         11 my ($y1, $y2) = ($center_y + $delta_y, $center_y - $delta_y);
150 9         59 push @result_values,
151             [$x1, $y1, $layer_z_up], [$x2, $y1, $layer_z_up],
152             [$x1, $y2, $layer_z_up], [$x2, $y2, $layer_z_up],
153             [$x1, $y1, $layer_z_dn], [$x2, $y1, $layer_z_dn],
154             [$x1, $y2, $layer_z_dn], [$x2, $y2, $layer_z_dn];
155             }
156             }
157             } else { # even layer of cuboctahedral packing
158 9         12 my $grid_row_count = int $radius_in_colors;
159 9         18 my @grid = ($grid_row_count);
160 9         11 $grid[$grid_row_count] = 0;
161 9         13 my $contour_cursor = $grid_row_count;
162 9         20 for my $x_index (1 .. $layer_radius * $adj_len_at_45_deg / $color_distance){
163 2 50       7 $contour_cursor-- if sqrt(($contour_cursor**2) + ($x_index**2)) > $radius_in_colors;
164 2         3 $grid[$x_index] = $contour_cursor;
165 2         3 $grid[$contour_cursor] = $x_index;
166             }
167 9         22 my @layer_values = map {[$center_x + ($_ * $color_distance), $center_y, $layer_z_up]}
  27         59  
168             -$grid_row_count .. $grid_row_count;
169 9         20 for my $y_index (1 .. $grid_row_count){
170 9         15 my $delta_y = $y_index * $color_distance;
171 9         18 my ($y1, $y2) = ($center_y + $delta_y, $center_y - $delta_y);
172 9         19 for my $x_index (-$grid[$y_index] .. $grid[$y_index]){
173 11         14 my $x = $center_x + ($x_index * $color_distance);
174 11         32 push @layer_values, [$x, $y1, $layer_z_up], [$x, $y2, $layer_z_up];
175             }
176             }
177 9 100       25 if ($layer_nr > 0){
178 1         5 push @result_values, [$_->[0], $_->[1], $layer_z_dn] for @layer_values;
179             }
180 9         20 push @result_values, @layer_values;
181             }
182             }
183             }
184             # check for linear space borders and constraints
185 281         568 return map { Graphics::Toolkit::Color::Values->new_from_tuple( $_, $color_space->name )}
186 16         31 grep { $color_space->is_in_linear_bounds($_) } @result_values;
  352         539  
187             }
188              
189             1;