6
\$\begingroup\$

Inspired in this color gradient generator, this is a gradient generator in perl6. I wonder if the code could be simplified or improved. It converts the hex colour to base10, calculates the gradient colours, and then converts the colour to base16.

my $initial_color = '#FF0000';
my $final_color   = '#00FF00';
my $gradient      = 10;

my @initial   = ($initial_color ~~ /\#(..)(..)(..)/).list.map: { .Str.parse-base(16) };
my @final     = ($final_color   ~~ /\#(..)(..)(..)/).list.map: { .Str.parse-base(16) };
my @range     = @final Z- @initial; 
my @increment = @range.map: { $_ / $gradient };

my @color;
@color.push: @initial;
for (1..$gradient) -> $i {
  @color.push: @(@color[$i-1]) Z+ @increment;
}

for (0..$gradient) -> $i {
  @color[$i] = '#' ~ ( @(@color[$i]).map: { .base(16,0).fmt('%02s') } ).join;
}

my $fh = open '/tmp/delete.html', :w;
for (0..$gradient) -> $i {
  $fh.print: "<span style='background-color:@color[$i]'>&nbsp;</span>"; 
}
$fh.print: '&nbsp';
$fh.close;

run <elinks -dump -dump-color-mode 3 /tmp/borrem.html>;
\$\endgroup\$
5
\$\begingroup\$

I'm going to limit my initial answer to not include modules, but I'll also cover a module that can help out some here too later.

my $initial_color = '#FF0000';
my $final_color   = '#00FF00';

While there's nothing wrong with using underscores, the tendency in Perl 6 has been to move towards hyphens, which would give us $initial-color and $final-color.

my @initial   = ($initial_color ~~ /\#(..)(..)(..)/).list.map: { .Str.parse-base(16) };

This is a bit of personal preference, I'd write this as

my @initial = .map: *.Str.parse-base(16) with $initial-color ~~ /\#(..)(..)(..)/;

Since it allows you to get rid of most of the parentheses/braces. Note that in any case, you don't need the .list because your Match is already an Iterable. But this is of course a pure matter of taste (some might prefer parentheses around the .map to emphasize it's separate from the with postfix.

You can also prestore the regex, particularly if you plan on using it more often:

my $hex-format = /\#(..)(..)(..)/;
my @initial = .map: *.Str.parse-base(16) with $initial-color ~~ $hex-format;

Your next two lines have two different techniques for applying math operators across lists:

my @range     = @final Z- @initial; 
my @increment = @range.map: { $_ / $gradient };

You could use a zip operator for both ( @range Z/ ($gradient xx 3) ), or you could opt for a hyper meta operator:

my @range     = @final «-» @initial;
my @increment = @range «/» $gradient

Now for generating the colors themselves, you had

my @color;
@color.push: @initial;
for (1..$gradient) -> $i {
  @color.push: @(@color[$i-1]) Z+ @increment;
}

for (0..$gradient) -> $i {
  @color[$i] = '#' ~ ( @(@color[$i]).map: { .base(16,0).fmt('%02s') } ).join;
}

First the easy things. If I see a variable @color, I assume it is a single entity with multiple constituent entities (one color, three values — R, G, B). But in this case, it's a list of colors, so the variable name is best used in plural. Also, you don't need parentheses around the list for the for loop. Just for 0..$gradient -> $i works.

Your first for loop uses the previous item to determine the next one, so you have a push outside the loop. There's a few ways to solve this. If you have to do an initial step like that, I like to put it in the loop using FIRST (or once, both are functionally equivalent here).

my @color;
for 1..$gradient -> $i {
  FIRST @color.push(@initial);
  @color.push: @(@color[$i-1]) Z+ @increment;
}

But we can actually rewrite this to calculate the value without relying upon the previous entry:

my @color;
for 0..$gradient -> $step {
  @color.push: @initial «+» @increment «*» $step;
}

This could be even further reduced into a one liner, but I don't think that's necessary nor desirable here. What's great here about the hyper operators is that they read like scalar arithmetic ($initial + $increment * $step) so readability is maintained.

Your second loop can also be simplified quite a bit by just looping on the values. I think it makes most sense to just put them into a new array

my @html-colors;
for @colors -> @color {
  @html-colors.push: '#' ~ @color.map( *.base(16,0).fmt: '%02s' ).join;
}

Anytime, though, you have a .map that has another method chain in its code block, you have a candidate for a hyper method call ».

my @html-colors;
for @colors -> @color {
  @html-colors.push: '#' ~ @color».base(16,0)».fmt('%02s').join;
}

Basically, ».foo means take every element and .foo it, and it returns a list of the values, which can be ».bar on every element. Note that .join doesn't have the hyperoperator, because we want to act on the resulting list.

But you could also loop in a rw manner (notice the <->) with

for @colors <-> @color {
  @colors = '#' ~ @color».base(16,0)».fmt('%02s').join;
}

Because now both loops though are 0 .. $gradient, it might make sense to merge them:

my @colors
for 0..$gradient -> $step {
  my @intermediate = @initial »+» @increment »*» $step;
  @colors.push: '#' ~ @intermediate».base(16,0)».fmt('%02s').join;
}

Lastly when you print, remember you can just loop on the color items:

for @colors -> $color {
  $fh.print: "<span style='background-color:$color'>&nbsp;</span>"; 
}

Which also allows a one liner using the topic variable $_ (and in this case, absent doing anything else, makes more sense to me):

$fh.print: "<span style='background-color:$_'>&nbsp;</span>" for @colors;

Since it's effectively the same loop, this is, of course, a candidate for putting back in the main loop, but there's something to be said for separating the calculations from the final output.

Rewritten, my version would be:

my $initial-color = '#FF0000';
my $final-color   = '#00FF00';
my $gradient      = 10;

my $hex-format = /\#(..)(..)(..)/;

my @initial = .map: *.Str.parse-base(16) with $initial-color ~~ $hex-format;
my @final   = .map: *.Str.parse-base(16) with   $final-color ~~ $hex-format;

my @increment = (@final »-» @initial) »/» $gradient;

my @colors;

for 0..$gradient -> $step {
  my @intermediate = @initial »+» @increment »*» $step;
  @colors.push: '#' ~ @intermediate».base(16,0)».fmt('%02s').join;
}

my $fh = open '/tmp/delete.html', :w;
$fh.print: "<span style='background-color:$_'>&nbsp;</span>" for @colors; 
$fh.print: '&nbsp';
$fh.close;
\$\endgroup\$
1
\$\begingroup\$

Some ideas to improvement.

If I need List of Str I prefer comb to match.

my @initial   = $initial_color.comb(/<xdigit> ** 2/)».parse-base(16);
#or
my @final     = $final_color.substr(1).comb(2)».parse-base(16);

You can use the sequence operator ... to make sequence of colors.

my @color =  @initial, { @^p Z+ @increment } ...  * ~~ @final;

Method fmt works with list and can convert to hex numbers.

@color .= map: '#' ~ *.fmt: '%02X', q{} ;

One more, I use fmt on List to make HTML color gradient.

my $html-color-gradient = @color.fmt( q{<span style='background-color:%s'>&nbsp;</span>}, q{}) ~ '&nbsp';

Last two steps in one by method tree.

my $html-color-gradient = @color.tree(
      *.fmt( q{<span style='background-color:%s'>&nbsp;</span>}, q{}) ~ '&nbsp',
      '#' ~ *.fmt( '%02X', q{} ),
  )

You could use spurt instead of open, print, close.

spurt '/tmp/delete.html', $html-color-gradient;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.