1

I'm working on something that will programatically apply a dark theme to an HTML/CSS file. Part of the process is inverting all the detected colors, and then rotating the hue by 180 degrees. e.g.:


  1. Parse color
    • #00DD00
  2. Invert
    • #00DD00 → #FF22FF
  3. Hue-rotate 180∘
    • #FF22FF → #FFA190

  1. Parse color
    • #CC1100
  2. Invert
    • #CC1100 → #33EEFF
  3. Hue-rotate 180∘
    • #33EEFF → #FFA190

Steps 1 & 2 were easy, but Step 3 is tripping me up because I don't know how to go about it.

Here's what I have for Step 2:

$parsedColor = '#123456'  # this would normally be automatically parsed from a HTML file


# New StringBuilder object that starts with #
$invertedColor = [Text.StringBuilder]'#'


# Split into groups of 2 characters, ignoring # and empty:
$parsedColor -split '([^#][^#])' -ne '#' -ne '' | &{process{

  # Append value to StringBuilder:
  $invertedColor.Append(

    # Format as hexidecimal string after flipping the bit
    '{0:X}' -f ([byte]"0x$_" -bxor 0xFF)

  )

# Suppress .Append noise
}} | Out-Null


# Output StringBuilder as String
$invertedColor -as [string]
5
  • How are you calculating your "hue-rotate" reference values (e.g. #33EEFF -> #FF4433)? If you look at the reference implementation at drafts.fxtf.org/filter-effects/#feColorMatrixElement where it says "For type="hueRotate"", the formula appears to give #33EEFF -> #FFA190, which is also backed up by this runable answer: stackoverflow.com/questions/19187905/… which says "rgb(51,238,255) -> rgb(255,161,144)"
    – mclayton
    Commented Aug 14, 2023 at 21:19
  • 1
    @mclayton I was using mdigi.tools/change-color-hue/#33eeff but apparently what it considers 180 degrees, hue-rotate considers to be 90 degrees. I've now adjusted my question to reflect that.
    – Maybe
    Commented Aug 15, 2023 at 22:24
  • 1
    You might need to update the title of the 2 x "step 3"'s as well? Currently says "Hue-rotate *180∘" :-)...
    – mclayton
    Commented Aug 15, 2023 at 22:36
  • @mclayton Disregard what I said before, 90deg actually comes out to #FFA8FF; the way hue-rotate calculates the new color is completely different from how MDiGi (as well as Hue adjustment filters that can be found in programs like Photoshop) calculates things. Basically, it's based on HSL rather than RGB, which is why it's so different. I've got my own set of functions for this Hex -> RGB -> HSL -> Hex process, and I'll post them in a supplemental answer soon... maybe today or tomorrow.
    – Maybe
    Commented Aug 17, 2023 at 1:36
  • @mclayton Took me a bit, but I finished it: stackoverflow.com/a/76974490
    – Maybe
    Commented Aug 25, 2023 at 5:36

2 Answers 2

2

Based on the reference for the css hue-rotate algorithm at https://drafts.fxtf.org/filter-effects/#feColorMatrixElement and some other references on this site at ...

... a function for applying hue-rotate in PowerShell would be something like this:

function Invoke-HueRotate
{
    param( [uint] $Color, [decimal] $Degrees )

    # split color into r, g and b channels
    $r = ($Color -shr 16) -band 0xFF;
    $g = ($Color -shr  8) -band 0xFF;
    $b = ($Color -shr  0) -band 0xFF;

    # convert degrees to radians and evaluate cos / sin
    $rad = $Degrees * [Math]::PI / 180;
    $cos = [Math]::Cos($rad);
    $sin = [Math]::Sin($rad);

    # long-hand matrix definition
    # see https://drafts.fxtf.org/filter-effects/#feColorMatrixElement
    # where it says 'For type="hueRotate"'
    $a00 = 0.213 + ( 0.787 * $cos) + (-0.213 * $sin);
    $a01 = 0.715 + (-0.715 * $cos) + (-0.715 * $sin);
    $a02 = 0.072 + (-0.072 * $cos) + ( 0.928 * $sin);
    $a10 = 0.213 + (-0.213 * $cos) + ( 0.143 * $sin);
    $a11 = 0.715 + ( 0.285 * $cos) + ( 0.140 * $sin);
    $a12 = 0.072 + (-0.072 * $cos) + (-0.283 * $sin);
    $a20 = 0.213 + (-0.213 * $cos) + (-0.787 * $sin);
    $a21 = 0.715 + (-0.715 * $cos) + ( 0.715 * $sin);
    $a22 = 0.072 + ( 0.928 * $cos) + ( 0.072 * $sin);

    # long-hand matrix multiplication
    $r2 = ($a00 * $r) + ($a01 * $g) + ($a02 * $b);
    $g2 = ($a10 * $r) + ($a11 * $g) + ($a12 * $b);
    $b2 = ($a20 * $r) + ($a21 * $g) + ($a22 * $b);

    # re-combine r, g and b channels
    $result = [uint](
        ([Math]::Clamp([int]$r2, 0, 255) -shl 16) `
        -bor
        ([Math]::Clamp([int]$g2, 0, 255) -shl  8) `
        -bor
        ([Math]::Clamp([int]$b2, 0, 255) -shl  0) `
    );

    return $result;

}

(I've avoided any matrix libraries to keep it self-contained, but you could simplify it down if you don't mind taking a dependency on something that can do the math cleaner.)

If you add the following utility methods as well:

function Invoke-InvertColor
{
    param( [uint] $Color )
    return $Color -bxor 0xFFFFFF;
}

function ConvertFrom-HtmlColor
{
    param( [string] $Value )
    return [System.Convert]::ToUInt32($Value.Substring(1), 16);
}

function ConvertTo-HtmlColor
{
    param( [uint] $Value )
    return "#" + $Value.ToString("X2").PadLeft(6, "0");
}

you could use it as follows:

$color = "#CC1100";

$original = ConvertFrom-HtmlColor -Value $color;
ConvertTo-HtmlColor $original;
#CC1100

$inverted = Invoke-InvertColor -Color $original;
ConvertTo-HtmlColor $inverted;
#33EEFF

$rotated = Invoke-HueRotate -Color $inverted -Degrees 180;
ConvertTo-HtmlColor $rotated;
#FFA190

Note that this gives a different result to your example for

         Invert           Hue-rotate 180∘
#CC1100    ->     #33EEFF        ->       #FF4433

but the result seems to tally up with the formulas in the first link in this answer, and gets the same result as the implementations in the other links, so you might need to elaborate on what algorithm / implementation you're using for hue-rotate if you're after a different result...


Update

Additional verification of the function result:

<style>
div {
    background-color: #33EEFF;
    width: 250px;
    height: 250px;
    filter: hue-rotate(180deg);
}
</style>

<div>
</div>

gives this:

enter image description here

and if you use the color dropper in your favourite image editor it'll tell you the peach color is #FFA190 which matches the result from the PowerShell code above...

2
  • Awesome, thank you. I clarified the issue regarding the hue rotation in the comments of my question above. I have to ask though, what matrix libraries for PowerShell are you referring to, and what would it look like if I used them?
    – Maybe
    Commented Aug 15, 2023 at 22:28
  • 1
    Glad it helped. Re matrix libraries, I'm sure there's some out there but I made a conscious decision to not even look :-). There's also various matrix classes in the base class library for dotnet that might be suitable, but it was just easier to hand-crank the calculations for a standalone, one-off function rather than take additional dependencies...
    – mclayton
    Commented Aug 15, 2023 at 22:34
1

So, when I first asked this question, I didn't realize CSS's hue-rotate filter was completely different from how Photoshop's Hue/Saturation/Lightness adjustment works. After doing some digging and with taking cues from mclayton, I came up with a set of functions that works for what I had originally intended.

function Invoke-InvertHexColor {
  param( $Color )
  [uint]$c = '0x'+ "$Color".TrimStart('#')
  return '#{0:X6}' -f ($c -bxor 0xFFFFFF)
}
function ConvertFrom-HexToRGB {
  param( [string]$Value )

  [int]$Red, [int]$Green, [int]$Blue =
    $Value -split '([^#][^#])' -ne '#' -ne '' | &{process{
      [Convert]::ToInt32($_,16)
    }}

  return [ordered]@{
    Red   = $Red
    Green = $Green
    Blue  = $Blue
  }
}
function ConvertTo-HexFromRGB {
  param( [int]$Red , [int]$Green , [int]$Blue )

  $hexColor = [Text.StringBuilder]'#'
  $Red, $Green, $Blue | &{process{
    [void] $hexColor.Append( '{0:X2}' -f [int]$_ )
  }}

  return $hexColor.ToString()
}
function Get-HueRotate {
  [OutputType([decimal])]
  param( [decimal]$Hue , [int]$Degrees )

  [decimal]$h = ($Hue + $Degrees) % 360

  return @(
    $h        # $h -ge 0
    $h + 360  # $h -lt 0
  )[
    $h -lt 0
  ]
}
function ConvertFrom-RGBtoHSL {
  param( [int]$Red , [int]$Green , [int]$Blue )

  [decimal]$r = $Red / 255
  [decimal]$g = $Green / 255
  [decimal]$b = $Blue / 255

  [decimal]$max = [Linq.Enumerable]::Max( [decimal[]]($r,$g,$b) )
  [decimal]$min = [Linq.Enumerable]::Min( [decimal[]]($r,$g,$b) )

  [decimal]$l = ($max + $min) / 2
  [decimal]$s = 0
  [decimal]$h = 0

  if ($max -ne $min) {
    $s =
      if ($l -lt 0.5) {
        ($max - $min) / ($max + $min)
      }
      else {
        ($max - $min) / (2 - $max - $min)
      }

    $h =
      switch ($max) {
        $r { (  ($g - $b) / ($max - $min) + @(0,6)[$g -lt $b]  ) / 6 ;break}
        $g { (  ($b - $r) / ($max - $min) + 2                  ) / 6 ;break}
        $b { (  ($r - $g) / ($max - $min) + 4                  ) / 6 ;break}
      }
  }

  return [ordered]@{
    Hue        = [Math]::Round($h * 360)
    Saturation = $s
    Lightness  = $l
  }
}
function ConvertTo-RGBfromHSL {
  param( [decimal]$Hue , [decimal]$Saturation , [decimal]$Lightness )

  [decimal]$h = $Hue / 360
  [decimal]$s = $Saturation
  [decimal]$l = $Lightness

  [decimal]$r = [decimal]$g = [decimal]$b = $l

  $Vertices2RGB = {
    param( [decimal]$m , [decimal]$n , [decimal]$o )

    if ($o -lt 0)   { $o++ }
    if ($o -gt 1)   { $o-- }

    switch ($o) {
      {$_ -lt 1/6} { return $m + ($n - $m) * 6 * $o         }
      {$_ -lt 1/2} { return $n                              }
      {$_ -lt 2/3} { return $m + ($n - $m) * (2/3 - $o) * 6 }
      default      { return $m                              }
    }
  }

  if ($s -ne 0) {
    [decimal]$v2 =
      if ($l -lt 0.5) {
        $l * (1 + $s)
      } else {
        $l + $s - ($l * $s)
      }

    [decimal]$v1 = (2 * $l) - $v2

    $r = & $Vertices2RGB $v1 $v2 ($h + 1/3)
    $g = & $Vertices2RGB $v1 $v2 $h
    $b = & $Vertices2RGB $v1 $v2 ($h - 1/3)
  }

  return [ordered]@{
    Red   = [Math]::Round($r * 255)
    Green = [Math]::Round($g * 255)
    Blue  = [Math]::Round($b * 255)
  }
}
$hexColor = Invoke-InvertHexColor '#CC1100'
$RGB = ConvertFrom-HexToRGB $hexColor
$HSL = ConvertFrom-RGBtoHSL @RGB
$HSL.Hue = Get-HueRotate $HSL.Hue 180
$RGB = ConvertTo-RGBfromHSL @HSL
$hexColor = ConvertTo-HexFromRGB @RGB

# $hexColor should be '#FF4433'
1
  • Nice work - looks good :-)…
    – mclayton
    Commented Aug 25, 2023 at 7:54

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