Private/Color.ps1
|
# Colour helpers: parsing, blending, deterministic generation. No external # dependency, compatible with Windows PowerShell 5.1. # Colour aliases specific to the Visual Studio [Solution Colors] extension # (see SolutionColors/src/ColorCache.cs). Other names are resolved through # System.Drawing (lazily loaded), then through this fallback table. $script:TcNamedColors = @{ # Solution Colors aliases 'burgundy' = '#FF6347' # Tomato 'pumpkin' = '#FF4500' # OrangeRed 'volt' = '#9ACD32' # YellowGreen 'mint' = '#66CDAA' # MediumAquamarine 'darkbrown' = '#8B4513' # SaddleBrown 'lavender' = '#9370DB' # MediumPurple # The most common everyday names (avoids loading System.Drawing) 'red' = '#FF0000' 'green' = '#008000' 'blue' = '#0000FF' 'teal' = '#008080' 'cyan' = '#00FFFF' 'magenta' = '#FF00FF' 'yellow' = '#FFFF00' 'orange' = '#FFA500' 'purple' = '#800080' 'violet' = '#EE82EE' 'pink' = '#FFC0CB' 'brown' = '#A52A2A' 'gray' = '#808080' 'grey' = '#808080' 'black' = '#000000' 'white' = '#FFFFFF' 'gold' = '#FFD700' 'olive' = '#808000' 'navy' = '#000080' 'maroon' = '#800000' 'lime' = '#00FF00' 'indigo' = '#4B0082' 'salmon' = '#FA8072' 'tomato' = '#FF6347' 'crimson' = '#DC143C' 'orangered' = '#FF4500' 'steelblue' = '#4682B4' 'seagreen' = '#2E8B57' 'darkcyan' = '#008B8B' 'chocolate' = '#D2691E' 'none' = $null } function ConvertFrom-TcColor { <# .SYNOPSIS Converts a colour (hex or name) into an @{ R; G; B } table. Returns $null when invalid. #> [CmdletBinding()] param([string] $Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $null } $v = $Value.Trim() if ($v -match '^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$') { $hex = $Matches[1] switch ($hex.Length) { 3 { $hex = "$($hex[0])$($hex[0])$($hex[1])$($hex[1])$($hex[2])$($hex[2])" } 4 { $hex = "$($hex[0])$($hex[0])$($hex[1])$($hex[1])$($hex[2])$($hex[2])" } 8 { $hex = $hex.Substring(0, 6) } # alpha channel ignored } return @{ R = [Convert]::ToInt32($hex.Substring(0, 2), 16) G = [Convert]::ToInt32($hex.Substring(2, 2), 16) B = [Convert]::ToInt32($hex.Substring(4, 2), 16) } } $key = $v.ToLowerInvariant() if ($script:TcNamedColors.ContainsKey($key)) { $mapped = $script:TcNamedColors[$key] if ($null -eq $mapped) { return $null } # [None] = no colour return ConvertFrom-TcColor -Value $mapped } # Last resort: known .NET names (SlateBlue, MediumAquamarine, ...) try { if (-not ('System.Drawing.Color' -as [type])) { Add-Type -AssemblyName System.Drawing -ErrorAction Stop } $known = [System.Drawing.Color]::FromName($v) if ($known.IsKnownColor) { return @{ R = [int]$known.R; G = [int]$known.G; B = [int]$known.B } } } catch { Write-Verbose "TerminalColors: unresolved colour name [$v] ($($_.Exception.Message))" } return $null } function ConvertTo-TcHex { [CmdletBinding()] param([hashtable] $Rgb) if ($null -eq $Rgb) { return $null } return ('#{0:X2}{1:X2}{2:X2}' -f [int]$Rgb.R, [int]$Rgb.G, [int]$Rgb.B) } function Get-TcBlendedColor { <# .SYNOPSIS Linearly blends Base towards Color by Amount (0 = Base, 1 = Color). #> [CmdletBinding()] param( [hashtable] $Base, [hashtable] $Color, [double] $Amount ) if ($Amount -lt 0) { $Amount = 0 } if ($Amount -gt 1) { $Amount = 1 } # Round half away from zero, like the bash variant (int(x + 0.5) in awk): both # implementations give exactly the same colour for the same project. return @{ R = [int][Math]::Round($Base.R + ($Color.R - $Base.R) * $Amount, 0, [MidpointRounding]::AwayFromZero) G = [int][Math]::Round($Base.G + ($Color.G - $Base.G) * $Amount, 0, [MidpointRounding]::AwayFromZero) B = [int][Math]::Round($Base.B + ($Color.B - $Base.B) * $Amount, 0, [MidpointRounding]::AwayFromZero) } } function ConvertFrom-TcHsl { [CmdletBinding()] param( [double] $Hue, # 0..360 [double] $Saturation, # 0..1 [double] $Lightness # 0..1 ) $h = (($Hue % 360) + 360) % 360 $c = (1 - [Math]::Abs(2 * $Lightness - 1)) * $Saturation $x = $c * (1 - [Math]::Abs((($h / 60.0) % 2) - 1)) $m = $Lightness - $c / 2 switch ([int][Math]::Floor($h / 60)) { 0 { $r = $c; $g = $x; $b = 0 } 1 { $r = $x; $g = $c; $b = 0 } 2 { $r = 0; $g = $c; $b = $x } 3 { $r = 0; $g = $x; $b = $c } 4 { $r = $x; $g = 0; $b = $c } default { $r = $c; $g = 0; $b = $x } } return @{ R = [int][Math]::Round(($r + $m) * 255, 0, [MidpointRounding]::AwayFromZero) G = [int][Math]::Round(($g + $m) * 255, 0, [MidpointRounding]::AwayFromZero) B = [int][Math]::Round(($b + $m) * 255, 0, [MidpointRounding]::AwayFromZero) } } function ConvertTo-TcHsl { [CmdletBinding()] param([hashtable] $Rgb) $r = $Rgb.R / 255.0; $g = $Rgb.G / 255.0; $b = $Rgb.B / 255.0 $max = [Math]::Max($r, [Math]::Max($g, $b)) $min = [Math]::Min($r, [Math]::Min($g, $b)) $l = ($max + $min) / 2 $d = $max - $min if ($d -eq 0) { return @{ H = 0.0; S = 0.0; L = $l } } $s = $d / (1 - [Math]::Abs(2 * $l - 1)) if ($max -eq $r) { $h = 60 * ((($g - $b) / $d) % 6) } elseif ($max -eq $g) { $h = 60 * ((($b - $r) / $d) + 2) } else { $h = 60 * ((($r - $g) / $d) + 4) } if ($h -lt 0) { $h += 360 } return @{ H = [double]$h; S = [double]$s; L = [double]$l } } function Get-TcAutoColor { <# .SYNOPSIS Generates a stable, readable colour from a string (a repository name). The same name always gives the same colour, on every machine. #> [CmdletBinding()] param([string] $Seed) # FNV-1a 32-bit: deterministic, unlike String.GetHashCode(), which varies from # one process to the next. # # The arithmetic is done in Int64: the intermediate product reaches 56 bits, # which exceeds a Double's exact precision (53 bits) but stays exact in Int64. # The mask is an explicit Int64 because PowerShell parses the literal # 0xFFFFFFFF as the Int32 -1, which would mask nothing at all. $mask = [int64]4294967295 $hash = [int64]2166136261 foreach ($ch in $Seed.ToLowerInvariant().ToCharArray()) { $hash = $hash -bxor ([int64][int][char]$ch) $hash = ($hash * 16777619) -band $mask } # 24 hues, 15 degrees apart. $hue = [double](($hash % 24) * 15) $sat = 0.62 + (([int](($hash -shr 8) % 3)) * 0.09) # 0.62 / 0.71 / 0.80 $lig = 0.42 + (([int](($hash -shr 16) % 3)) * 0.05) # 0.42 / 0.47 / 0.52 return ConvertFrom-TcHsl -Hue $hue -Saturation $sat -Lightness $lig } function Get-TcColorEmoji { <# .SYNOPSIS Picks the Unicode coloured square nearest to an RGB colour. #> [CmdletBinding()] param([hashtable] $Rgb) $hsl = ConvertTo-TcHsl -Rgb $Rgb if ($hsl.S -lt 0.15) { if ($hsl.L -ge 0.5) { return [char]::ConvertFromUtf32(0x2B1C) } # white square return [char]::ConvertFromUtf32(0x2B1B) # black square } $h = $hsl.H # Brown: dark orange if ($h -ge 15 -and $h -lt 45 -and $hsl.L -lt 0.38) { return [char]::ConvertFromUtf32(0x1F7EB) } if ($h -lt 15 -or $h -ge 340) { return [char]::ConvertFromUtf32(0x1F7E5) } # red if ($h -lt 45) { return [char]::ConvertFromUtf32(0x1F7E7) } # orange if ($h -lt 70) { return [char]::ConvertFromUtf32(0x1F7E8) } # yellow if ($h -lt 175) { return [char]::ConvertFromUtf32(0x1F7E9) } # green if ($h -lt 265) { return [char]::ConvertFromUtf32(0x1F7E6) } # blue return [char]::ConvertFromUtf32(0x1F7EA) # purple } |