]> git.ktnx.net Git - mobile-ledger.git/blobdiff - tools/gen-styles
no need for gravity, only one edge is constrained anyway
[mobile-ledger.git] / tools / gen-styles
index d158d8bf370e227e5b9894c9a39b8c02558006dd..64cebf27e9e8ab4d72652f6ecd735d2baa20f391 100644 (file)
 #!/usr/bin/perl
 
-use strict; use warnings; use utf8;
+use strict; use warnings; use utf8::all;
 use autodie;
-use Math::Trig;
-use File::Basename qw(basename dirname);
-use File::Temp qw(tempfile);
+use Carp;
+
+use Type::Tiny;
+use Types::Standard qw(StrictNum);
+my $colorValue = Type::Tiny->new(
+    parent     => StrictNum,
+    constraint => '($_ >= 0) and ($_ <= 1)'
+);
+
+package Color::HSL;
+use Moo;
+use namespace::clean;
+
+has h => ( is => 'ro', isa => $colorValue );
+has s => ( is => 'ro', isa => $colorValue );
+has l => ( is => 'ro', isa => $colorValue );
+
+package Color::sRGB;
+use Moo;
+use Types::Standard qw(StrictNum);
+use List::MoreUtils qw(minmax);
+
+use namespace::clean;
+
+use overload '""' => 'hexTuple';
+
+has r => ( is => 'ro', isa => $colorValue );
+has g => ( is => 'ro', isa => $colorValue );
+has b => ( is => 'ro', isa => $colorValue );
 
 sub hexTuple {
-       my ($r, $g, $b) = @_;
-       return sprintf('%02x%02x%02x', int(255*$r+0.5), int(255*$g+0.5), int(255*$b+0.5));
+    my $self = shift;
+    return sprintf( '%02x%02x%02x',
+        int( $self->r * 255 + 0.5 ),
+        int( $self->g * 255 + 0.5 ),
+        int( $self->b * 255 + 0.5 ) );
 }
-sub hsvHex {
-       my ($hue, $sat, $val ) = @_;
-       my $h = int($hue * 6);
-       my $f = $hue * 6 - $h;
-       my $p = $val * (1 - $sat);
-       my $q = $val * ( 1 - $f * $sat);
-       my $t = $val * ( 1 - (1-$f) * $sat);
 
-       return hexTuple($val, $t, $p) if $h == 0 or $h == 6;
-       return hexTuple($q, $val, $p) if $h == 1;
-       return hexTuple($p, $val, $t) if $h == 2;
-       return hexTuple($p, $q, $val) if $h == 3;
-       return hexTuple($t, $p, $val) if $h == 4;
-       return hexTuple($val, $p, $q) if $h == 5;
+# https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+sub _norm {
+    return $_[0] / 12.92 if $_[0] <= 0.03928;
+    return ( ( $_[0] + 0.055 ) / 1.055 )**2.4;
+}
 
-       die $h;
+sub relativeLuminance {
+    my $self = shift;
+
+    return 0.2126 * _norm( $self->r ) + 0.7152 * _norm( $self->g )
+        + 0.0722 * _norm( $self->b );
 }
 
-# https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
-sub hslHex {
-       my ($hue, $sat, $lig ) = @_;
-       $hue = $hue / 360.0;
-       my $h = ($hue * 6.0);
-       my $c = (1 - abs(2.0*$lig - 1)) * $sat;
-       my $h_mod_2 = $h - 2.0*int($h/2);
-       my $x = $c * (1 - abs($h_mod_2 - 1));
-       my ($r, $g, $b);
-       my $m = $lig - $c / 2.0;
-
-       return hexTuple($c + $m, $x + $m,  0 + $m) if $h < 1 or $h == 6;
-       return hexTuple($x + $m, $c + $m,  0 + $m) if $h < 2;
-       return hexTuple( 0 + $m, $c + $m, $x + $m) if $h < 3;
-       return hexTuple( 0 + $m, $x + $m, $c + $m) if $h < 4;
-       return hexTuple($x + $m,  0 + $m, $c + $m) if $h < 5;
-       return hexTuple($c + $m,  0 + $m, $x + $m) if $h < 6;
+sub BLACK {
+    shift->new( r => 0, g => 0, b => 0 );
+}
 
-       die $h;
+sub WHITE {
+    shift->new( r => 1, g => 1, b => 1 );
 }
 
 my @hexDigit = split //, '0123456789abcdef';
-my %hexValue = map(
-               (lc($hexDigit[$_]) => $_, uc($hexDigit[$_]) => $_ ),
-               0..15 );
+my %hexValue =
+    map( ( lc( $hexDigit[$_] ) => $_, uc( $hexDigit[$_] ) => $_ ), 0 .. 15 );
 
-sub min {
-       my $min = shift;
+sub fromHexTriplet {
+    my ( $class, $triplet ) = @_;
 
-       for (@_) { $min = $_ if $_ < $min }
+    my @d = $triplet =~ /^#?(.)(.)(.)(.)(.)(.)$/
+        or die "'$triplet' is not a valid colour triplet";
 
-       return $min;
+    return $class->new(
+        r => ( 16 * $hexValue{ $d[0] } + $hexValue{ $d[1] } ) / 255.0,
+        g => ( 16 * $hexValue{ $d[2] } + $hexValue{ $d[3] } ) / 255.0,
+        b => ( 16 * $hexValue{ $d[4] } + $hexValue{ $d[5] } ) / 255.0
+    );
 }
 
-sub max {
-       my $max = shift;
+# https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
+sub fromHSL {
+    my ( $class, $hsl ) = @_;
+    my $hue = $hsl->h;
+    my $sat = $hsl->s;
+    my $lig = $hsl->l;
+
+    my $h       = ( $hue * 6.0 );
+    my $c       = ( 1 - abs( 2.0 * $lig - 1 ) ) * $sat;
+    my $h_mod_2 = $h - 2.0 * int( $h / 2 );
+    my $x       = $c * ( 1 - abs( $h_mod_2 - 1 ) );
+    my ( $r, $g, $b );
+    my $m = $lig - $c / 2.0;
+
+    return $class->new( r => $c + $m, g => $x + $m, b => 0 + $m )
+        if $h < 1 or $h == 6;
+    return $class->new( r => $x + $m, g => $c + $m, b => 0 + $m )  if $h < 2;
+    return $class->new( r => 0 + $m,  g => $c + $m, b => $x + $m ) if $h < 3;
+    return $class->new( r => 0 + $m,  g => $x + $m, b => $c + $m ) if $h < 4;
+    return $class->new( r => $x + $m, g => 0 + $m,  b => $c + $m ) if $h < 5;
+    return $class->new( r => $c + $m, g => 0 + $m,  b => $x + $m ) if $h < 6;
+
+    die $h;
+}
+
+sub toHSL {
+    my $self = shift;
+
+    my ( $m, $M ) = minmax( $self->r, $self->g, $self->b );
 
-       for (@_) { $max = $_ if $_ > $max }
+    my $C = $M - $m;
 
-       return $max;
+    my $h;
+    if ( $C == 0 ) {
+        $h = 0;
+    }
+    elsif ( $self->r == $M ) {
+        $h = ( $self->g - $self->b ) / $C;
+        $h -= 6 * int( $h / 6.0 );
+    }
+    elsif ( $self->g == $M ) {
+        $h = ( $self->b - $self->r ) / $C + 2;
+    }
+    elsif ( $self->b == $M ) {
+        $h = ( $self->r - $self->g ) / $C + 4;
+    }
+    else { die "$C, $M, $self"; }
+
+    my $H = 60 * $h;
+    my $L = ( $M + $m ) / 2;
+
+    my $S = ( $L <= 0.5 ) ? $C / ( 2 * $L ) : $C / ( 2 - 2 * $L );
+
+    return Color::HSL->new( h => $H/360.0, s => $S, l => $L );
 }
 
-sub hexToRGB {
-       my $hexTriplet = shift;
+sub contrastWith {
+    my ( $self, $ref ) = @_;
 
-       my @d = $hexTriplet =~ /^#?(.)(.)(.)(.)(.)(.)/;
+    my $myL = $self->relativeLuminance;
+    my $refL = $ref->relativeLuminance;
 
-       return (16 * $hexValue{$d[0]} + $hexValue{$d[1]},
-               16 * $hexValue{$d[2]} + $hexValue{$d[3]},
-               16 * $hexValue{$d[4]} + $hexValue{$d[5]});
+    my $ratio = ( $myL + 0.05 ) / ( $refL + 0.05 );
+    $ratio = 1 / $ratio if $ratio < 1;
+    return $ratio;
 }
 
-sub hexToHSL {
-       my $hexTriplet = shift;
+package MAIN;
 
-       my ($r,$g,$b) = hexToRGB($hexTriplet);
-       warn "$hexTriplet -> $r:$g:$b";
+use Math::Trig;
+use File::Basename qw(basename dirname);
+use File::Temp qw(tempfile);
+use Getopt::Long;
 
-       for ($r, $g, $b ) { $_ = $_ / 255.0 }
+my $opt_night;
 
-       my $M = max($r, $g, $b);
-       my $m = min($r, $g, $b);
-       my $C = $M - $m;
+GetOptions(
+    'night!'    => \$opt_night,
+) or exit 1;
 
-       my $h;
-       if ($C == 0) {
-               $h = 0;
-       }
-       elsif ( $r == $M ) {
-               $h = ($g-$b)/$C;
-               $h -= 6*int($h/6.0);
-       }
-       elsif ( $g == $M ) {
-               $h = ($b-$r)/$C + 2;
-       }
-       elsif ( $b == $M ) {
-               $h = ($r-$g)/$C + 4;
-       }
-       else { die "$C, $M, $r, $g, $b"; }
+my $DEFAULT_HUE = 261.2245;
 
-       my $H = 60 * $h;
-       my $L = ($M + $m) / 2;
+sub hexTuple {
+       my ($r, $g, $b) = @_;
+       return sprintf('%02x%02x%02x', int(255*$r+0.5), int(255*$g+0.5), int(255*$b+0.5));
+}
+sub hsvHex {
+       my ($hue, $sat, $val ) = @_;
+       my $h = int($hue * 6);
+       my $f = $hue * 6 - $h;
+       my $p = $val * (1 - $sat);
+       my $q = $val * ( 1 - $f * $sat);
+       my $t = $val * ( 1 - (1-$f) * $sat);
 
-       my $S = ( $L <= 0.5 ) ? $C/(2*$L) : $C / (2-2*$L);
+       return hexTuple($val, $t, $p) if $h == 0 or $h == 6;
+       return hexTuple($q, $val, $p) if $h == 1;
+       return hexTuple($p, $val, $t) if $h == 2;
+       return hexTuple($p, $q, $val) if $h == 3;
+       return hexTuple($t, $p, $val) if $h == 4;
+       return hexTuple($val, $p, $q) if $h == 5;
 
-       return( $H, $S, $L );
+       die $h;
 }
 
-my $baseColorHSV = [ hexToHSL('#935ff2') ];
-my $baseColorHue = $baseColorHSV->[0];
-warn sprintf( 'H:%1.4f S:%1.4f V:%1.4f', @$baseColorHSV );
-warn sprintf( 'H:%1.4f S:%1.4f L:%1.4f', hexToHSL('#3e148c') );
-my @target = hexToRGB('#935ff2');
-my ($best, $min_dist);
-for (my $s = 0.50; $s < 0.90; $s += 0.001) {
-       for ( my $l = 0.50; $l <= 0.80; $l += 0.001 ) {
-               my $hexColor = hslHex($baseColorHue, $s, $l);
-               my ($r,$g,$b) = hexToRGB( $hexColor );
-               my $dist = abs($r-$target[0])
-                        + abs($g-$target[1])
-                        + abs($b-$target[2]);
-               if (not defined($best) or $dist < $min_dist) {
-                       $best = [ $s, $l, $hexColor ];
-                       $min_dist = $dist;
-               }
-       }
+sub hslHex {
+    my ( $h, $s, $l ) = @_;
+    return Color::sRGB->fromHSL(
+        Color::HSL->new( { h => $h / 360.0, s => $s, l => $l } ) )->hexTuple;
 }
-warn sprintf( 's%1.3f, l%1.3f -> %s',
-       @$best );
-
-my $baseTheme = "AppTheme.NoActionBar";
-
-use constant STEP_DEGREES => 15;
-
-# # hsb
-# for( my $hue = 0; $hue < 360; $hue += STEP_DEGREES ) {
-#      printf "<style name=\"%s.%d\" parent=\"%s\">\n",
-#              $baseTheme, $hue, $baseTheme;
-#      printf "  <item name=\"colorPrimary\">#%s</item>\n",
-#                      hsvHex($hue/360.0, 0.61, 0.95);
-#      printf "  <item name=\"colorPrimaryDark\">#%s</item>\n",
-#                      hsvHex($hue/360.0, 0.86, 0.55);
-#      printf "  <item name=\"colorAccent\">#%s</item>\n",
-#                      hsvHex(($hue-4)/360.0, 0.72, 0.82);
-#      printf "  <item name=\"drawer_background\">#ffffffff</item>\n";
-#      printf "  <item name=\"table_row_dark_bg\">#28%s</item>\n",
-#                      hsvHex($hue/360.0, 0.65, 0.83);
-#      printf "  <item name=\"table_row_light_bg\">#28%s</item>\n",
-#                      hsvHex($hue/360.0, 0.20, 1.00);
-#      printf "  <item name=\"header_border\">#80%s</item>\n",
-#                      hsvHex(($hue+6)/360.0, 0.86, 0.55);
-#      printf "</style>\n";
+
+warn sprintf("%s: %2.1f\n", 'white', Color::sRGB->WHITE->relativeLuminance);
+warn sprintf("%s: %2.1f\n", 'black', Color::sRGB->BLACK->relativeLuminance);
+warn sprintf( "%s: %2.1f\n",
+    '50% gray',
+    Color::sRGB->new( r => 0.5, g => 0.5, b => 0.5 )->relativeLuminance );
+
+my $baseColor = '#935ff2';
+my $baseColorRGB = Color::sRGB->fromHexTriplet($baseColor);
+my $baseColorHSL = $baseColorRGB->toHSL;
+my $baseColorHue = $baseColorHSL->h;
+warn sprintf(
+    '%s → H:%1.4f S:%1.4f L:%1.4f (luminance: %1.4f; cW: %1.4f, cB: %1.4f)',
+    $baseColor,
+    360 * $baseColorHSL->h,
+    $baseColorHSL->s,
+    $baseColorHSL->l,
+    $baseColorRGB->relativeLuminance,
+    $baseColorRGB->contrastWith( Color::sRGB->WHITE ),
+    $baseColorRGB->contrastWith( Color::sRGB->BLACK ),
+);
+# # find best saturation/lightness for the desired color
+# # test if the above is correct
+# my ($best, $min_dist);
+# for (my $s = 0.50; $s < 0.90; $s += 0.001) {
+#     for ( my $l = 0.50; $l <= 0.80; $l += 0.001 ) {
+#         my $color = Color::sRGB->fromHSL(
+#             Color::HSL->new( h => $baseColorHue, s => $s, l => $l ) );
+#         my $dist =
+#               abs( $color->r - $baseColorRGB->r )
+#             + abs( $color->g - $baseColorRGB->g )
+#             + abs( $color->b - $baseColorRGB->b );
+#         if ( not defined($best) or $dist < $min_dist ) {
+#             $best     = [ $s, $l, $color ];
+#             $min_dist = $dist;
+#         }
+#     }
 # }
+# warn sprintf( 's%1.3f, l%1.3f → %s', @$best );
+
+my $baseTheme = "AppTheme";
+
+use constant STEP_DEGREES => 5;
 
-# HSL
 sub outputThemes {
-       my $out = shift;
-       my $baseIndent = shift;
-       $out->print(hslStyleForHue($baseColorHue, undef, $baseIndent));
-       for( my $hue = 0; $hue < 360; $hue += STEP_DEGREES ) {
-               $out->print("\n");
-               $out->print(hslStyleForHue($hue, $baseTheme, $baseIndent));
-       }
+    my $out        = shift;
+    my $baseIndent = shift;
+    $out->print("\n");
+    $out->print(
+        hslStyleForHue( $DEFAULT_HUE, $baseTheme, $baseIndent, 'default' ) );
+    for ( my $hue = 0; $hue < 360; $hue += STEP_DEGREES ) {
+        $out->print("\n");
+        $out->print( hslStyleForHue( $hue, $baseTheme, $baseIndent ) );
+    }
+}
+
+sub bestLightnessForHue {
+    my ( $h, $s ) = @_;
+    my $targetContrast = $opt_night ? 5.16 : 4.07;
+    my $white = $opt_night ? Color::sRGB->BLACK : Color::sRGB->WHITE;
+    my $bestLightness;
+    my $bestContrast;
+    for ( my $l = 0; $l < 1; $l += 0.002 ) {
+        my $contrast = Color::sRGB->fromHSL(
+            Color::HSL->new( { h => $h, s => $s, l => $l } ) )
+            ->contrastWith($white);
+
+        if ( defined $bestLightness ) {
+            if (abs( $contrast - $targetContrast ) <
+                abs( $bestContrast - $targetContrast ) )
+            {
+                $bestLightness = $l;
+                $bestContrast  = $contrast;
+            }
+        }
+        else {
+            $bestLightness = $l;
+            $bestContrast = $contrast;
+        }
+    }
+
+    warn sprintf(
+        "Found best lightness for hue %1.4f: %1.4f (contrast %1.4f)\n",
+        360 * $h, $bestLightness, $bestContrast );
+    return $bestLightness;
 }
 
 sub hslStyleForHue {
        my $hue = shift;
        my $base = shift;
        my $baseIndent = shift // '';
-
-       my $blueL = 0.665;
-       my $yellowL = 0.350;
-
-       my $blueL2 = 0.350;
-       my $yellowL2 = 0.500;
-
-       # $y == 0 for yellow
-       my $y = $hue - 60;
-       $y += 360 if $y < 0;
-       # $q == 0 for yellow, 1 for blue
-       my $q = cos(deg2rad(abs($y-180)/2.0));
-       my $l1 = $yellowL + ($blueL - $yellowL) * $q;
-       my $l2 = 0.250 + 0.250 * $q;
-       my $l3 = 0.950;
-       my $l4 = 0.980;
+        my $subTheme = shift // sprintf('%03d', $hue);
+
+       my %lQ = (
+               0   => 0.550,   # red
+               60  => 0.250,   # yellow
+               120 => 0.290,   # green
+               180 => 0.300,   # cyan
+               240 => 0.680,   # blue
+               300 => 0.450,   # magenta
+       );
+       $lQ{360} = $lQ{0};
+
+       my ($x0, $x1, $y0, $y1);
+       $x0 = (int( $hue / 60 ) * 60) % 360;
+       $x1 = $x0 + 60;
+       $y0 = $lQ{$x0};
+       $y1 = $lQ{$x1};
+
+        my $S = 0.8497;
+
+       # linear interpolation
+        #my $l1 = $y0 + 1.0 * ( $hue - $x0 ) * ( $y1 - $y0 ) / ( $x1 - $x0 );
+        my $l1 = bestLightnessForHue( $hue / 360.0, $S );
+        #$l1 += ( 1 - $l1 ) * 0.20 if $opt_night;
+
+        #my $l2 = $opt_night ? ( $l1 + ( 1 - $l1 ) * 0.15 ) : $l1 * 0.85;
+        my $l2 = $l1 * 0.80;
+        my $l3 = $opt_night ? 0.150                        : 0.950;
+        my $l4 = $opt_night ? 0.100                        : 0.980;
 
        my $result = "";
        my $indent = "$baseIndent    ";
 
        if ($base) {
-               $result .= sprintf "$baseIndent<style name=\"%s.%d\" parent=\"%s\">\n",
-                        $baseTheme, $hue, $baseTheme;
+               $result .= sprintf "$baseIndent<style name=\"%s.%s\" parent=\"%s\">\n",
+                        $baseTheme, $subTheme, $baseTheme;
         }
         else {
                 $result .= sprintf "$baseIndent<style name=\"%s\">\n",
                         $baseTheme;
-                $result .= "$indent<item name=\"windowActionBar\">false</item>\n";
-                $result .= "$indent<item name=\"windowNoTitle\">true</item>\n";
-                $result .= "$indent<item name=\"textColor\">#8a000000</item>\n";
+#                $result .= "$indent<item name=\"windowActionBar\">false</item>\n";
+#                $result .= "$indent<item name=\"windowNoTitle\">true</item>\n";
+#                $result .= "$indent<item name=\"textColor\">#757575</item>\n";
         }
-        my $S = 0.845;
-        $result .= sprintf "$indent<item name=\"colorPrimary\">#%s</item>\n",
-                hslHex($hue, $S, $l1);
-        $result .= sprintf "$indent<item name=\"colorPrimaryTransparent\">#00%s</item>\n",
-                hslHex($hue, $S, $l1);
-        $result .= sprintf "$indent<item name=\"colorAccent\">#%s</item>\n",
-                hslHex($hue, $S, $l2);
-        $result .= "$indent<item name=\"drawer_background\">#ffffffff</item>\n";
-        $result .= sprintf "$indent<item name=\"table_row_dark_bg\">#%s</item>\n",
-                hslHex($hue, $S, $l3);
-        $result .= sprintf "$indent<item name=\"table_row_light_bg\">#%s</item>\n",
-                hslHex($hue, $S, $l4);
+
+        $result .= sprintf "$indent<!-- h: %1.4f s:%1.4f l:%1.4f -->\n", $hue,
+            $S, $l1 if 0;
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'colorPrimary', hslHex( $hue, $S, $l1 );
+        $result .= sprintf "$indent<item name=\"%s\">#00%s</item>\n",
+            'colorPrimaryTransparent', hslHex( $hue, $S, $l1 );
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'colorSecondary', hslHex( $hue, $S, $l1 );
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'colorPrimaryDark', hslHex( $hue, $S*0.8, $l2 );
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'table_row_dark_bg', hslHex( $hue, $S, $l3 );
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'table_row_light_bg', hslHex( $hue, $S, $l4 );
         $result .= "$baseIndent</style>\n";
 
         return $result;
@@ -231,6 +348,7 @@ if ($xml) {
        my $start_marker = '<!-- theme list start -->';
        my $end_marker = '<!-- theme list end -->';
        my ($fh, $filename) = tempfile(basename($0).'.XXXXXXXX', DIR => dirname($xml));
+        $fh->binmode(':utf8');
        open(my $in, '<', $xml);
        my $base_indent = '';
        my $state = 'waiting-for-start-marker';