X-Git-Url: https://git.ktnx.net/?p=mobile-ledger.git;a=blobdiff_plain;f=tools%2Fgen-styles;h=64cebf27e9e8ab4d72652f6ecd735d2baa20f391;hp=1c2c4c8524af8796d0c35ce33214857f70a3a069;hb=23ec8777f698b472c52c194ef7218fc1e8a67f83;hpb=da3450cd594a3f9ad6f0a76b2b2fc219949a6695 diff --git a/tools/gen-styles b/tools/gen-styles index 1c2c4c85..64cebf27 100644 --- a/tools/gen-styles +++ b/tools/gen-styles @@ -1,10 +1,164 @@ #!/usr/bin/perl -use strict; use warnings; use utf8; +use strict; use warnings; use utf8::all; use autodie; +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 $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 ) ); +} + +# 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; +} + +sub relativeLuminance { + my $self = shift; + + return 0.2126 * _norm( $self->r ) + 0.7152 * _norm( $self->g ) + + 0.0722 * _norm( $self->b ); +} + +sub BLACK { + shift->new( r => 0, g => 0, b => 0 ); +} + +sub WHITE { + shift->new( r => 1, g => 1, b => 1 ); +} + +my @hexDigit = split //, '0123456789abcdef'; +my %hexValue = + map( ( lc( $hexDigit[$_] ) => $_, uc( $hexDigit[$_] ) => $_ ), 0 .. 15 ); + +sub fromHexTriplet { + my ( $class, $triplet ) = @_; + + my @d = $triplet =~ /^#?(.)(.)(.)(.)(.)(.)$/ + or die "'$triplet' is not a valid colour triplet"; + + 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 + ); +} + +# 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 ); + + my $C = $M - $m; + + 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 contrastWith { + my ( $self, $ref ) = @_; + + my $myL = $self->relativeLuminance; + my $refL = $ref->relativeLuminance; + + my $ratio = ( $myL + 0.05 ) / ( $refL + 0.05 ); + $ratio = 1 / $ratio if $ratio < 1; + return $ratio; +} + +package MAIN; + use Math::Trig; use File::Basename qw(basename dirname); use File::Temp qw(tempfile); +use Getopt::Long; + +my $opt_night; + +GetOptions( + 'night!' => \$opt_night, +) or exit 1; + +my $DEFAULT_HUE = 261.2245; sub hexTuple { my ($r, $g, $b) = @_; @@ -28,188 +182,162 @@ sub hsvHex { die $h; } -# 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; - - die $h; -} - -my @hexDigit = split //, '0123456789abcdef'; -my %hexValue = map( - (lc($hexDigit[$_]) => $_, uc($hexDigit[$_]) => $_ ), - 0..15 ); - -sub min { - my $min = shift; - - for (@_) { $min = $_ if $_ < $min } - - return $min; + my ( $h, $s, $l ) = @_; + return Color::sRGB->fromHSL( + Color::HSL->new( { h => $h / 360.0, s => $s, l => $l } ) )->hexTuple; } -sub max { - my $max = shift; - - for (@_) { $max = $_ if $_ > $max } - - return $max; -} +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 ); -sub hexToRGB { - my $hexTriplet = shift; +my $baseTheme = "AppTheme"; - my @d = $hexTriplet =~ /^#?(.)(.)(.)(.)(.)(.)/; +use constant STEP_DEGREES => 5; - return (16 * $hexValue{$d[0]} + $hexValue{$d[1]}, - 16 * $hexValue{$d[2]} + $hexValue{$d[3]}, - 16 * $hexValue{$d[4]} + $hexValue{$d[5]}); +sub outputThemes { + 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 hexToHSL { - my $hexTriplet = shift; - - my ($r,$g,$b) = hexToRGB($hexTriplet); - warn "$hexTriplet -> $r:$g:$b"; - - for ($r, $g, $b ) { $_ = $_ / 255.0 } - - my $M = max($r, $g, $b); - my $m = min($r, $g, $b); - my $C = $M - $m; - - 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 $H = 60 * $h; - my $L = ($M + $m) / 2; - - my $S = ( $L <= 0.5 ) ? $C/(2*$L) : $C / (2-2*$L); - - return( $H, $S, $L ); -} - -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; - } - } -} -warn sprintf( 's%1.3f, l%1.3f -> %s', - @$best ); - -my $baseTheme = "AppTheme.NoActionBar"; - -# # hsb -# for( my $hue = 0; $hue < 360; $hue += 15 ) { -# printf "\n"; -# } +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; + } + } -# HSL -sub outputThemes { - my $out = shift; - $out->print(hslStyleForHue($baseColorHue)); - for( my $hue = 0; $hue < 360; $hue += 15 ) { - $out->print(hslStyleForHue($hue, $baseTheme)); - } + 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 $blueL = 0.665; - my $yellowL = 0.350; - - my $y = $hue - 60; - $y += 360 if $y < 0; - my $q = cos(deg2rad(abs($y-180)/2.0)); - my $l1 = $yellowL + ($blueL - $yellowL) * $q; - my $l2 = 0.150 + 0.350 * $q; - my $l3 = 0.950; - my $l4 = 0.980; + my $baseIndent = shift // ''; + 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 "\n"; + + $result .= sprintf "$indent\n", $hue, + $S, $l1 if 0; + $result .= sprintf "$indent#%s\n", + 'colorPrimary', hslHex( $hue, $S, $l1 ); + $result .= sprintf "$indent#00%s\n", + 'colorPrimaryTransparent', hslHex( $hue, $S, $l1 ); + $result .= sprintf "$indent#%s\n", + 'colorSecondary', hslHex( $hue, $S, $l1 ); + $result .= sprintf "$indent#%s\n", + 'colorPrimaryDark', hslHex( $hue, $S*0.8, $l2 ); + $result .= sprintf "$indent#%s\n", + 'table_row_dark_bg', hslHex( $hue, $S, $l3 ); + $result .= sprintf "$indent#%s\n", + 'table_row_light_bg', hslHex( $hue, $S, $l4 ); + $result .= "$baseIndent\n"; return $result; } @@ -220,17 +348,20 @@ if ($xml) { my $start_marker = ''; my $end_marker = ''; 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'; while (<$in>) { if ( $state eq 'waiting-for-start-marker' ) { print $fh $_; - $state = 'skipping-styles' if /^\s*\Q$start_marker\E/; + $state = 'skipping-styles', $base_indent = $1 + if /^(\s*)\Q$start_marker\E/; next; } if ( $state eq 'skipping-styles' ) { next unless /^\s*\Q$end_marker\E/; - outputThemes($fh); + outputThemes($fh, $base_indent); print $fh $_; $state = 'copying-the-rest'; next;