Classes/ColorMath.ps1
|
<#
.SYNOPSIS The color arithmetic of gradients and color forms: the spaces a gradient blends in, its easing curves and stop places, HSL and the 256 colors. .DESCRIPTION Written so Windows PowerShell 5.1, PowerShell 7 and PWRSWriteColorEX give the same results to the last bit: the constants are the bit patterns of their doubles rather than decimal numbers a parser may round either way, and the arithmetic is +, -, *, / and square roots in a fixed order, which IEEE 754 rounds the same everywhere. Sine, cosine, arctangent and powers of two are series written here from those operations, since [Math]'s may differ in the last bit between .NET versions. A class rather than functions because a gradient computes a color for every character, and a function call costs more than that arithmetic. .NOTES Author: Mark Newton License: MIT Requires: PowerShell 5.1 or later #> class ColorMath { # The linear value of each sRGB channel value 0-255: ((c / 255 + 0.055) / 1.055) ^ 2.4, or # c / 255 / 12.92 at the dark end. It rises, so a linear value finds its nearest channel # value by comparing with its neighbors. static [double[]] $SrgbToLinear = [ColorMath]::LinearTable() # Bjorn Ottosson's OKLab matrices: linear sRGB to LMS, the cube roots of LMS to OKLab, OKLab # to the cube roots of LMS, and LMS to linear sRGB static [double] $L1 = [BitConverter]::Int64BitsToDouble(0x3FDA61D629F2E197) static [double] $L2 = [BitConverter]::Int64BitsToDouble(0x3FE129A2D9E60E32) static [double] $L3 = [BitConverter]::Int64BitsToDouble(0x3FAA572112081026) static [double] $M1 = [BitConverter]::Int64BitsToDouble(0x3FCB1FA76156A7C5) static [double] $M2 = [BitConverter]::Int64BitsToDouble(0x3FE5C84A69936914) static [double] $M3 = [BitConverter]::Int64BitsToDouble(0x3FBB7E5DF0497455) static [double] $S1 = [BitConverter]::Int64BitsToDouble(0x3FB69AFD7A044C17) static [double] $S2 = [BitConverter]::Int64BitsToDouble(0x3FD207AE728A2F45) static [double] $S3 = [BitConverter]::Int64BitsToDouble(0x3FE428C9177A5EDB) static [double] $LL = [BitConverter]::Int64BitsToDouble(0x3FCAF02A3FE8A4FA) static [double] $LM = [BitConverter]::Int64BitsToDouble(0x3FE9655120032AAD) static [double] $LS = -[BitConverter]::Int64BitsToDouble(0x3F70ADD9BD572B38) static [double] $AL = [BitConverter]::Int64BitsToDouble(0x3FFFA5E1BFFFDE12) static [double] $AM = -[BitConverter]::Int64BitsToDouble(0x40036DC1BFFE5D3E) static [double] $AS = [BitConverter]::Int64BitsToDouble(0x3FDCD686FFF371A5) static [double] $BL = [BitConverter]::Int64BitsToDouble(0x3F9A869680B729E0) static [double] $BM = [BitConverter]::Int64BitsToDouble(0x3FE90C776001F502) static [double] $BS = -[BitConverter]::Int64BitsToDouble(0x3FE9E0AC0001353D) static [double] $LA = [BitConverter]::Int64BitsToDouble(0x3FD95D9920068C8A) static [double] $LB = [BitConverter]::Int64BitsToDouble(0x3FCB9F751FFA8CC8) static [double] $MA = -[BitConverter]::Int64BitsToDouble(0x3FBB06117FEEC881) static [double] $MB = -[BitConverter]::Int64BitsToDouble(0x3FB058BF3FE39E34) static [double] $SA = -[BitConverter]::Int64BitsToDouble(0x3FB6E86F5FDF38B5) static [double] $SB = -[BitConverter]::Int64BitsToDouble(0x3FF4A9ECBFFEAA8D) static [double] $R1 = [BitConverter]::Int64BitsToDouble(0x40104E955DC3D73A) static [double] $R2 = -[BitConverter]::Int64BitsToDouble(0x400A76317EA9DE73) static [double] $R3 = [BitConverter]::Int64BitsToDouble(0x3FCD906C3222FFEF) static [double] $G1 = -[BitConverter]::Int64BitsToDouble(0x3FF44B85A62C2AFF) static [double] $G2 = [BitConverter]::Int64BitsToDouble(0x4004E0C87D01BF65) static [double] $G3 = -[BitConverter]::Int64BitsToDouble(0x3FD5D82D4F5D4F2A) static [double] $B1 = -[BitConverter]::Int64BitsToDouble(0x3F712FEA56E00671) static [double] $B2 = -[BitConverter]::Int64BitsToDouble(0x3FE68267C131178D) static [double] $B3 = [BitConverter]::Int64BitsToDouble(0x3FFB5263CAEF6BCD) hidden static [double[]] LinearTable() { $bits = [long[]]@( 0x0000000000000000, 0x3F33E45677C176F7, 0x3F43E45677C176F7, 0x3F4DD681B3A23272, 0x3F53E45677C176F7, 0x3F58DD6C15B1D4B4, 0x3F5DD681B3A23272, 0x3F6167CBA8C94818, 0x3F63E45677C176F7, 0x3F6660E146B9A5D5, 0x3F68DD6C15B1D4B4, 0x3F6B6A31B5259C99, 0x3F6E1E31D70C99DD, 0x3F707C38BF8583A9, 0x3F71FCC2BEED6421, 0x3F7390FFAF95E279, 0x3F753936CC7BC928, 0x3F76F5ADDB50C915, 0x3F78C6A94031B561, 0x3F7AAC6C0FB97351, 0x3F7CA7381F9F602B, 0x3F7EB74E160978D0, 0x3F806E76BBDA92B8, 0x3F818C2A5A8A8044, 0x3F82B4E09B3F0AE3, 0x3F83E8B7B3BDE965, 0x3F8527CD60AF8B85, 0x3F86723EEA8D3709, 0x3F87C8292A3DB6B3, 0x3F8929A88D67B521, 0x3F8A96D91A8016BD, 0x3F8C0FD67499FAB6, 0x3F8D94BBDEFD740E, 0x3F8F25A44089883F, 0x3F9061551372C694, 0x3F9135F3E4C2CCE2, 0x3F9210BB8642B172, 0x3F92F1B8C1AE46BD, 0x3F93D8F839B79C0B, 0x3F94C6866B3E9FA4, 0x3F95BA6FAE794313, 0x3F96B4C0380D2DEE, 0x3F97B5841A1BF3AC, 0x3F98BCC74542ADDB, 0x3F99CA95898DC8B5, 0x3F9ADEFA9761C020, 0x3F9BFA0200597BD9, 0x3F9D1BB7381AEC1F, 0x3F9E442595227BCA, 0x3F9F73585185E1B5, 0x3FA054AD45D76878, 0x3FA0F31BA386FF26, 0x3FA194FCB663747B, 0x3FA23A55E62A662A, 0x3FA2E32C8E148D11, 0x3FA38F85FD21EACF, 0x3FA43F67766310FF, 0x3FA4F2D6313FA8D0, 0x3FA5A9D759BA5ED0, 0x3FA6647010B254EE, 0x3FA722A56C2239EE, 0x3FA7E47C775D2427, 0x3FA8A9FA33494B07, 0x3FA973239698B9CC, 0x3FAA3FFD8E001389, 0x3FAB108CFC6B7FBC, 0x3FABE4D6BB31D522, 0x3FACBCDF9A4616F2, 0x3FAD98AC60675833, 0x3FAE7841CB4F16DF, 0x3FAF5BA48FDE2048, 0x3FB0216CAD240765, 0x3FB096F2671EB815, 0x3FB10E65C38A5192, 0x3FB187C90BF8BCE2, 0x3FB2031E85F5D6DA, 0x3FB28068731A1952, 0x3FB2FFA9111CB94B, 0x3FB380E299E53F92, 0x3FB40417439CA10F, 0x3FB4894940BDDBFB, 0x3FB5107AC0261E59, 0x3FB599ADED247AAC, 0x3FB624E4EF892ED4, 0x3FB6B221EBB4817E, 0x3FB7416702A539D1, 0x3FB7D2B65206B527, 0x3FB86611F43E9E6A, 0x3FB8FB7C007A4A70, 0x3FB992F68ABBBC89, 0x3FBA2C83A3E6566D, 0x3FBAC82559CB3644, 0x3FBB65DDB7354604, 0x3FBC05AEC3F4FE5E, 0x3FBCA79A84EBE030, 0x3FBD4BA2FC17A6A5, 0x3FBDF1CA289D34B8, 0x3FBE9A1206D34003, 0x3FBF447C904CBB4E, 0x3FBFF10BBBE302C2, 0x3FC04FE0BEDFE5F1, 0x3FC0A84FE3B36D8F, 0x3FC101D443DFC06F, 0x3FC15C6ED58EEFDF, 0x3FC1B8208DA5FEF0, 0x3FC214EA5FC9514A, 0x3FC272CD3E610123, 0x3FC2D1CA1A9D1CFB, 0x3FC331E1E479CDF5, 0x3FC393158AC3674E, 0x3FC3F565FB1A5FD5, 0x3FC458D421F735DF, 0x3FC4BD60EAAE3E73, 0x3FC5230D3F736034, 0x3FC589DA095DBAA1, 0x3FC5F1C8306B3A3C, 0x3FC65AD89B841A2B, 0x3FC6C50C307E53BF, 0x3FC73063D420FC80, 0x3FC79CE06A279303, 0x3FC80A82D5453B5D, 0x3FC8794BF727EB3F, 0x3FC8E93CB07B8679, 0x3FC95A55E0ECEC0B, 0x3FC9CC98672CF47E, 0x3FCA400520F3619C, 0x3FCAB49CEB01C003, 0x3FCB2A60A1263B0A, 0x3FCBA1511E3E632D, 0x3FCC196F3C39E76F, 0x3FCC92BBD41D41FE, 0x3FCD0D37BE045851, 0x3FCD88E3D1250F68, 0x3FCE05C0E3D1D3E0, 0x3FCE83CFCB7C16F0, 0x3FCF03115CB6BFD3, 0x3FCF83866B38924D, 0x3FD00297E4EF4553, 0x3FD044072557177A, 0x3FD086115F6BEB3A, 0x3FD0C8B6FB5C735E, 0x3FD10BF860EF039A, 0x3FD14FD5F782A5A6, 0x3FD1945026102997, 0x3FD1D967532B31B1, 0x3FD21F1BE50339E7, 0x3FD2656E41649AE3, 0x3FD2AC5ECDB988F8, 0x3FD2F3EDEF0B0ED8, 0x3FD33C1C0A020438, 0x3FD384E982E800B1, 0x3FD3CE56BDA84A81, 0x3FD418641DD0C1BC, 0x3FD463120692C7AF, 0x3FD4AE60DAC4229D, 0x3FD4FA50FCDFDE15, 0x3FD546E2CF0727A9, 0x3FD59416B3022858, 0x3FD5E1ED0A40DAAB, 0x3FD6306635DBDD7B, 0x3FD67F82969543A2, 0x3FD6CF428CD96079, 0x3FD71FA678BF915D, 0x3FD770AEBA0B042A, 0x3FD7C25BB02B7AC5, 0x3FD814ADBA3E0BD9, 0x3FD867A5370DE0B1, 0x3FD8BB428514F067, 0x3FD90F86027CB84E, 0x3FD964700D1EF1B1, 0x3FD9BA0102864521, 0x3FDA10393FEEFAFD, 0x3FDA67192247A9BE, 0x3FDABEA10631E195, 0x3FDB16D14802D5CA, 0x3FDB6FAA43C403BB, 0x3FDBC92C5533D785, 0x3FDC2357D7C64E5D, 0x3FDC7E2D26A596DE, 0x3FDCD9AC9CB2AEF2, 0x3FDD35D69485FFC5, 0x3FDD92AB686FF782, 0x3FDDF02B7279A10D, 0x3FDE4E570C6539C5, 0x3FDEAD2E8FAEC526, 0x3FDF0CB2558C9EA4, 0x3FDF6CE2B6F00983, 0x3FDFCDC00C85BEC2, 0x3FE017A5575B3CB2, 0x3FE048C17AD3C04B, 0x3FE07A349C9D9837, 0x3FE0ABFEE888C050, 0x3FE0DE208A4444C8, 0x3FE11099AD5E83EB, 0x3FE1436A7D456EEF, 0x3FE176932546CA12, 0x3FE1AA13D0906BDA, 0x3FE1DDECAA307B85, 0x3FE2121DDD15AECE, 0x3FE246A7940F86D1, 0x3FE27B89F9CE8C4B, 0x3FE2B0C538E48B07, 0x3FE2E6597BC4CCA0, 0x3FE31C46ECC4528D, 0x3FE3528DB61A0F73, 0x3FE3892E01DF1FCC, 0x3FE3C027FA0F01EB, 0x3FE3F77BC887CD3B, 0x3FE42F29970A68F8, 0x3FE467318F3AC22D, 0x3FE49F93DAA00113, 0x3FE4D850A2A4BDE1, 0x3FE51168109734E5, 0x3FE54ADA4DA97A1B, 0x3FE584A782F1AC23, 0x3FE5BECFD96A2698, 0x3FE5F95379F1B3ED, 0x3FE634328D4BBE97, 0x3FE66F6D3C2081CF, 0x3FE6AB03AEFD39AA, 0x3FE6E6F60E5452B1, 0x3FE72344827D98F6, 0x3FE75FEF33B6669B, 0x3FE79CF64A21D1E2, 0x3FE7DA59EDC8DAB0, 0x3FE8181A469A9787, 0x3FE856377C6C6224, 0x3FE894B1B6FA0377, 0x3FE8D3891DE5DF49, 0x3FE912BDD8B91F45, 0x3FE952500EE3DDA5, 0x3FE9923FE7BD4F67, 0x3FE9D28D8A83EDFC, 0x3FEA13391E5DA09F, 0x3FEA5442CA57E52E, 0x3FEA95AAB567F88F, 0x3FEAD771066AFEC2, 0x3FEB1995E4262A69, 0x3FEB5C197546E3F8, 0x3FEB9EFBE062F086, 0x3FEBE23D4BF8981B, 0x3FEC25DDDE6ECBBB, 0x3FEC69DDBE154AF1, 0x3FECAE3D1124C90B, 0x3FECF2FBFDBF11F1, 0x3FED381AA9EF2E82, 0x3FED7D993BA988D4, 0x3FEDC377D8CC0FD5, 0x3FEE09B6A71E5AA6, 0x3FEE5055CC51CBB4, 0x3FEE97556E01B351, 0x3FEEDEB5B1B37216, 0x3FEF2676BCD69ADE, 0x3FEF6E98B4C51466, 0x3FEFB71BBEC33AB2, 0x3FF0000000000000 ) $table = [double[]]::new(256) for ($i = 0; $i -lt 256; $i++) { $table[$i] = [BitConverter]::Int64BitsToDouble($bits[$i]) } return $table } # The cube root of a number from 0 to 1, by 40 Newton steps from 1. [Math]::Pow and Math.Cbrt # may differ in the last bit between .NET versions and libraries; Newton's steps use only *, # + and /, which do not. From 1 the steps fall to the root, and 40 of them reach it from any # LMS value a color gives. static [double] CubeRoot([double] $Value) { if ($Value -le 0.0) { return 0.0 } $root = 1.0 for ($step = 0; $step -lt 40; $step++) { $root = (2.0 * $root + $Value / ($root * $root)) / 3.0 } return $root } # pi, pi/2 and pi/4 static [double] $Pi = [BitConverter]::Int64BitsToDouble(0x400921FB54442D18) static [double] $HalfPi = [BitConverter]::Int64BitsToDouble(0x3FF921FB54442D18) static [double] $QuarterPi = [BitConverter]::Int64BitsToDouble(0x3FE921FB54442D18) # 2/pi, and pi/2 split into a part whose low bits are zero and the rest, so that k x pi/2 # comes off a number without losing the bits beyond a double's pi/2 static [double] $TwoOverPi = [BitConverter]::Int64BitsToDouble(0x3FE45F306DC9C883) static [double] $HalfPiHigh = [BitConverter]::Int64BitsToDouble(0x3FF921FB54400000) static [double] $HalfPiLow = [BitConverter]::Int64BitsToDouble(0x3DD0B4611A626331) # The natural logarithm of 2 static [double] $Ln2 = [BitConverter]::Int64BitsToDouble(0x3FE62E42FEFA39EF) # 1/n! for n from 0 to 18 static [double[]] $InverseFactorials = @( [BitConverter]::Int64BitsToDouble(0x3FF0000000000000), [BitConverter]::Int64BitsToDouble(0x3FF0000000000000), [BitConverter]::Int64BitsToDouble(0x3FE0000000000000), [BitConverter]::Int64BitsToDouble(0x3FC5555555555555), [BitConverter]::Int64BitsToDouble(0x3FA5555555555555), [BitConverter]::Int64BitsToDouble(0x3F81111111111111), [BitConverter]::Int64BitsToDouble(0x3F56C16C16C16C17), [BitConverter]::Int64BitsToDouble(0x3F2A01A01A01A01A), [BitConverter]::Int64BitsToDouble(0x3EFA01A01A01A01A), [BitConverter]::Int64BitsToDouble(0x3EC71DE3A556C734), [BitConverter]::Int64BitsToDouble(0x3E927E4FB7789F5C), [BitConverter]::Int64BitsToDouble(0x3E5AE64567F544E4), [BitConverter]::Int64BitsToDouble(0x3E21EED8EFF8D898), [BitConverter]::Int64BitsToDouble(0x3DE6124613A86D09), [BitConverter]::Int64BitsToDouble(0x3DA93974A8C07C9D), [BitConverter]::Int64BitsToDouble(0x3D6AE7F3E733B81F), [BitConverter]::Int64BitsToDouble(0x3D2AE7F3E733B81F), [BitConverter]::Int64BitsToDouble(0x3CE952C77030AD4A), [BitConverter]::Int64BitsToDouble(0x3CA6827863B97D97) ) # The arctangent series' coefficients (-1)^k / (2k + 1) for k from 0 to 13 static [double[]] $AtanSeries = @( [BitConverter]::Int64BitsToDouble(0x3FF0000000000000), -[BitConverter]::Int64BitsToDouble(0x3FD5555555555555), [BitConverter]::Int64BitsToDouble(0x3FC999999999999A), -[BitConverter]::Int64BitsToDouble(0x3FC2492492492492), [BitConverter]::Int64BitsToDouble(0x3FBC71C71C71C71C), -[BitConverter]::Int64BitsToDouble(0x3FB745D1745D1746), [BitConverter]::Int64BitsToDouble(0x3FB3B13B13B13B14), -[BitConverter]::Int64BitsToDouble(0x3FB1111111111111), [BitConverter]::Int64BitsToDouble(0x3FAE1E1E1E1E1E1E), -[BitConverter]::Int64BitsToDouble(0x3FAAF286BCA1AF28), [BitConverter]::Int64BitsToDouble(0x3FA8618618618618), -[BitConverter]::Int64BitsToDouble(0x3FA642C8590B2164), [BitConverter]::Int64BitsToDouble(0x3FA47AE147AE147B), -[BitConverter]::Int64BitsToDouble(0x3FA2F684BDA12F68) ) # The sine of a number from -pi/4 to pi/4: its Taylor series to the 17th power, in Horner # form on the square. [Math]::Sin and the other library functions may differ in the last bit # between .NET versions and platforms; this uses only *, + and -. hidden static [double] SineNearZero([double] $R) { $z = $R * $R $factorials = [ColorMath]::InverseFactorials $sum = $factorials[17] for ($n = 15; $n -ge 3; $n -= 2) { if ($n % 4 -eq 3) { $sum = $sum * $z - $factorials[$n] } else { $sum = $sum * $z + $factorials[$n] } } return $R * ($sum * $z + 1.0) } # The cosine of a number from -pi/4 to pi/4: its Taylor series to the 18th power, in Horner # form on the square hidden static [double] CosineNearZero([double] $R) { $z = $R * $R $factorials = [ColorMath]::InverseFactorials $sum = -$factorials[18] for ($n = 16; $n -ge 2; $n -= 2) { if ($n % 4 -eq 2) { $sum = $sum * $z - $factorials[$n] } else { $sum = $sum * $z + $factorials[$n] } } return $sum * $z + 1.0 } # A number as k x pi/2 + r, r from -pi/4 to pi/4: k's last two bits, which pick the quarter # turn, and r hidden static [double[]] QuarterTurns([double] $X) { $k = [Math]::Round($X * [ColorMath]::TwoOverPi, [MidpointRounding]::ToEven) $r = ($X - $k * [ColorMath]::HalfPiHigh) - $k * [ColorMath]::HalfPiLow $quarter = (([long]$k % 4) + 4) % 4 return [double[]]@($quarter, $r) } # The sine of X in radians static [double] Sin([double] $X) { $turns = [ColorMath]::QuarterTurns($X) switch ([int]$turns[0]) { 0 { return [ColorMath]::SineNearZero($turns[1]) } 1 { return [ColorMath]::CosineNearZero($turns[1]) } 2 { return -[ColorMath]::SineNearZero($turns[1]) } } return -[ColorMath]::CosineNearZero($turns[1]) } # The cosine of X in radians static [double] Cos([double] $X) { $turns = [ColorMath]::QuarterTurns($X) switch ([int]$turns[0]) { 0 { return [ColorMath]::CosineNearZero($turns[1]) } 1 { return -[ColorMath]::SineNearZero($turns[1]) } 2 { return -[ColorMath]::CosineNearZero($turns[1]) } } return [ColorMath]::SineNearZero($turns[1]) } # The arctangent of a number from 0 to 1: taken from pi/4 above 1/2, then halved once with # atan u = 2 atan(u / (1 + sqrt(1 + u^2))), so the series runs on no more than 0.24. # [Math]::Sqrt is exact: IEEE 754 rounds a square root correctly everywhere. hidden static [double] ArctangentToOne([double] $Z) { $base = 0.0 $u = $Z if ($Z -gt 0.5) { $base = [ColorMath]::QuarterPi $u = ($Z - 1.0) / ($Z + 1.0) } $half = $u / (1.0 + [Math]::Sqrt(1.0 + $u * $u)) $w = $half * $half $series = [ColorMath]::AtanSeries $sum = $series[13] for ($k = 12; $k -ge 0; $k--) { $sum = $sum * $w + $series[$k] } return $base + 2.0 * ($half * $sum) } # The angle of the point (X, Y) in radians, from -pi to pi; 0 for the origin static [double] Atan2([double] $Y, [double] $X) { $ax = [Math]::Abs($X) $ay = [Math]::Abs($Y) if ($ax -eq 0.0 -and $ay -eq 0.0) { return 0.0 } if ($ay -le $ax) { $angle = [ColorMath]::ArctangentToOne($ay / $ax) } else { $angle = [ColorMath]::HalfPi - [ColorMath]::ArctangentToOne($ax / $ay) } if ($X -lt 0.0) { $angle = [ColorMath]::Pi - $angle } if ($Y -lt 0.0) { return -$angle } return $angle } # 2 to the power X: 2^floor(X) by halving or doubling, times e^(f ln 2) for the fraction f by # its Taylor series to the 18th power static [double] Exp2([double] $X) { $whole = [Math]::Floor($X) $w = ($X - $whole) * [ColorMath]::Ln2 $factorials = [ColorMath]::InverseFactorials $sum = $factorials[18] for ($k = 17; $k -ge 0; $k--) { $sum = $sum * $w + $factorials[$k] } $power = [long]$whole while ($power -gt 0) { $sum *= 2.0 $power-- } while ($power -lt 0) { $sum *= 0.5 $power++ } return $sum } # The OKLab coordinates L, a and b of an sRGB color given as three channel values 0-255 static [double[]] ToOkLab([int[]] $Rgb) { $table = [ColorMath]::SrgbToLinear $red = $table[$Rgb[0]] $green = $table[$Rgb[1]] $blue = $table[$Rgb[2]] $l = [ColorMath]::CubeRoot([ColorMath]::L1 * $red + [ColorMath]::L2 * $green + [ColorMath]::L3 * $blue) $m = [ColorMath]::CubeRoot([ColorMath]::M1 * $red + [ColorMath]::M2 * $green + [ColorMath]::M3 * $blue) $s = [ColorMath]::CubeRoot([ColorMath]::S1 * $red + [ColorMath]::S2 * $green + [ColorMath]::S3 * $blue) return [double[]]@( ([ColorMath]::LL * $l + [ColorMath]::LM * $m + [ColorMath]::LS * $s), ([ColorMath]::AL * $l + [ColorMath]::AM * $m + [ColorMath]::AS * $s), ([ColorMath]::BL * $l + [ColorMath]::BM * $m + [ColorMath]::BS * $s) ) } # The sRGB channel values of a hue in degrees and a saturation and lightness from 0 to 100: # C = (1 - |2L - 1|) * S, X = C * (1 - |(H / 60) mod 2 - 1|), m = L - C / 2, each channel # rounded half to even after multiplying by 255 static [int[]] FromHsl([double] $Hue, [double] $Saturation, [double] $Lightness) { $h = $Hue - 360.0 * [Math]::Floor($Hue / 360.0) $s = [Math]::Min(100.0, [Math]::Max(0.0, $Saturation)) / 100.0 $l = [Math]::Min(100.0, [Math]::Max(0.0, $Lightness)) / 100.0 return [ColorMath]::HslToRgb($h, $s, $l) } # The sRGB channel values of a hue in degrees from 0 to 360 and a saturation and lightness from # 0 to 1, by FromHsl's formula static [int[]] HslToRgb([double] $h, [double] $s, [double] $l) { $chroma = (1.0 - [Math]::Abs(2.0 * $l - 1.0)) * $s $sector = $h / 60.0 $x = $chroma * (1.0 - [Math]::Abs(($sector - 2.0 * [Math]::Floor($sector / 2.0)) - 1.0)) $offset = $l - $chroma / 2.0 $parts = switch ([int][Math]::Floor($sector)) { 0 { $chroma, $x, 0.0 } 1 { $x, $chroma, 0.0 } 2 { 0.0, $chroma, $x } 3 { 0.0, $x, $chroma } 4 { $x, 0.0, $chroma } default { $chroma, 0.0, $x } } return [int[]]@( [int][Math]::Round(($parts[0] + $offset) * 255.0, [MidpointRounding]::ToEven), [int][Math]::Round(($parts[1] + $offset) * 255.0, [MidpointRounding]::ToEven), [int][Math]::Round(($parts[2] + $offset) * 255.0, [MidpointRounding]::ToEven) ) } # The OKLab coordinates of the 256-color codes 16-255, at index code - 16: the 6x6x6 cube, # whose channel steps are 0, 95, 135, 175, 215 and 255, then the 24 grays, 8 to 238 by 10 static [double[][]] $Ansi8Labs = [ColorMath]::Ansi8LabTable() # The indexes of Ansi8Labs in order of lightness, the lower index first of two as light, and # the lightness at each, for NearestAnsi8 to search outward from a color's own lightness static [int[]] $Ansi8Order = [ColorMath]::Ansi8LightnessOrder() static [double[]] $Ansi8Lightness = [ColorMath]::Ansi8LightnessTable() hidden static [double[][]] Ansi8LabTable() { $steps = [int[]]@(0, 95, 135, 175, 215, 255) $labs = [double[][]]::new(240) for ($i = 0; $i -lt 216; $i++) { $red = $steps[[int][Math]::Floor($i / 36)] $green = $steps[[int][Math]::Floor($i / 6) % 6] $blue = $steps[$i % 6] $labs[$i] = [ColorMath]::ToOkLab([int[]]@($red, $green, $blue)) } for ($i = 0; $i -lt 24; $i++) { $gray = 8 + 10 * $i $labs[216 + $i] = [ColorMath]::ToOkLab([int[]]@($gray, $gray, $gray)) } return $labs } hidden static [int[]] Ansi8LightnessOrder() { $labs = [ColorMath]::Ansi8Labs $order = [int[]]::new(240) for ($i = 0; $i -lt 240; $i++) { $place = $i while ($place -gt 0 -and $labs[$order[$place - 1]][0] -gt $labs[$i][0]) { $order[$place] = $order[$place - 1] $place-- } $order[$place] = $i } return $order } hidden static [double[]] Ansi8LightnessTable() { $lightness = [double[]]::new(240) for ($i = 0; $i -lt 240; $i++) { $lightness[$i] = [ColorMath]::Ansi8Labs[[ColorMath]::Ansi8Order[$i]][0] } return $lightness } # The weight of the difference in lightness in NearestAnsi8's distance: 0.6, which keeps a # dark saturated color on a code of its own hue rather than the gray of its lightness. static [double] $LightnessWeight = [BitConverter]::Int64BitsToDouble(0x3FE3333333333333) # The 256-color code nearest an sRGB color by OKLab distance, of codes 16-255, the lower code # of two as near. Codes 0-15 are left out, since the terminal's scheme sets their colors. The # distance is (LightnessWeight x the difference in L) squared, plus the squared differences # in a and b, added in that order. The search runs outward from the color's lightness and # stops on each side at a code whose lightness term alone is more than the distance of the # nearest code found. static [int] NearestAnsi8([int] $Red, [int] $Green, [int] $Blue) { $lab = [ColorMath]::ToOkLab([int[]]@($Red, $Green, $Blue)) $labs = [ColorMath]::Ansi8Labs $order = [ColorMath]::Ansi8Order $lightness = [ColorMath]::Ansi8Lightness $weight = [ColorMath]::LightnessWeight # The first code in lightness order at least as light as the color $low = 0 $high = 240 while ($low -lt $high) { $middle = ($low + $high) -shr 1 if ($lightness[$middle] -lt $lab[0]) { $low = $middle + 1 } else { $high = $middle } } $bestCode = 256 $bestDistance = [double]::MaxValue for ($i = $low; $i -lt 240; $i++) { $dl = ($lightness[$i] - $lab[0]) * $weight $dlSquared = $dl * $dl if ($dlSquared -gt $bestDistance) { break } $entry = $labs[$order[$i]] $da = $entry[1] - $lab[1] $db = $entry[2] - $lab[2] $distance = $dlSquared + $da * $da + $db * $db $code = $order[$i] + 16 if ($distance -lt $bestDistance -or ($distance -eq $bestDistance -and $code -lt $bestCode)) { $bestDistance = $distance $bestCode = $code } } for ($i = $low - 1; $i -ge 0; $i--) { $dl = ($lightness[$i] - $lab[0]) * $weight $dlSquared = $dl * $dl if ($dlSquared -gt $bestDistance) { break } $entry = $labs[$order[$i]] $da = $entry[1] - $lab[1] $db = $entry[2] - $lab[2] $distance = $dlSquared + $da * $da + $db * $db $code = $order[$i] + 16 if ($distance -lt $bestDistance -or ($distance -eq $bestDistance -and $code -lt $bestCode)) { $bestDistance = $distance $bestCode = $code } } return $bestCode } # The sRGB color of a 256-color code 16-255: the 6x6x6 cube, whose channel steps are 0, 95, # 135, 175, 215 and 255, then the 24 grays, 8 to 238 by 10 static [int[]] Ansi8Rgb([int] $Code) { if ($Code -ge 232) { $gray = 8 + 10 * ($Code - 232) return [int[]]@($gray, $gray, $gray) } $steps = [int[]]@(0, 95, 135, 175, 215, 255) $index = $Code - 16 return [int[]]@($steps[[int][Math]::Floor($index / 36)], $steps[[int][Math]::Floor($index / 6) % 6], $steps[$index % 6]) } # The relative luminance weights of red, green and blue, the 0.05 the contrast ratio adds to a # luminance, and white's luminance plus that 0.05 static [double] $LuminanceRed = [BitConverter]::Int64BitsToDouble(0x3FCB367A0F9096BC) static [double] $LuminanceGreen = [BitConverter]::Int64BitsToDouble(0x3FE6E2EB1C432CA5) static [double] $LuminanceBlue = [BitConverter]::Int64BitsToDouble(0x3FB27BB2FEC56D5D) static [double] $ContrastOffset = [BitConverter]::Int64BitsToDouble(0x3FA999999999999A) static [double] $WhiteContrast = [BitConverter]::Int64BitsToDouble(0x3FF0CCCCCCCCCCCD) # Whether black text on an sRGB color has a higher contrast ratio than white text: (Y + 0.05) # / 0.05 against 1.05 / (Y + 0.05), Y the color's relative luminance, compared as (Y + 0.05) # squared against 0.05 x 1.05 static [bool] BlackTextReads([int] $Red, [int] $Green, [int] $Blue) { $table = [ColorMath]::SrgbToLinear $luminance = [ColorMath]::LuminanceRed * $table[$Red] + [ColorMath]::LuminanceGreen * $table[$Green] + [ColorMath]::LuminanceBlue * $table[$Blue] $shifted = $luminance + [ColorMath]::ContrastOffset return $shifted * $shifted -gt [ColorMath]::ContrastOffset * [ColorMath]::WhiteContrast } # The relative luminance of an sRGB color, channels 0-255: its linear red, green and blue # times their weights, added in that order static [double] RelativeLuminance([int] $Red, [int] $Green, [int] $Blue) { $table = [ColorMath]::SrgbToLinear return [ColorMath]::LuminanceRed * $table[$Red] + [ColorMath]::LuminanceGreen * $table[$Green] + [ColorMath]::LuminanceBlue * $table[$Blue] } # The contrast ratio of two sRGB colors as WCAG 2 defines it, from 1 to 21: (L1 + 0.05) / # (L2 + 0.05), L1 the higher relative luminance and L2 the lower static [double] ContrastRatio([int[]] $First, [int[]] $Second) { $one = [ColorMath]::RelativeLuminance($First[0], $First[1], $First[2]) + [ColorMath]::ContrastOffset $two = [ColorMath]::RelativeLuminance($Second[0], $Second[1], $Second[2]) + [ColorMath]::ContrastOffset if ($one -ge $two) { return $one / $two } return $two / $one } # One color for each of Steps characters, blended evenly between the waypoints, which are RGB # arrays: RGB arrays, or 256-color codes with Ansi8. With at least as many steps as waypoints, # waypoint w is the step nearest w * (Steps - 1) / (waypoints - 1), a half rounded up, so # every waypoint is drawn, and the steps between two waypoints fall between them at a ratio # from 0 to 1. With fewer steps than waypoints, the steps are spread evenly over the whole # gradient. With OkLab the blend is in OKLab, else each channel on its own, rounded half to # even. The OKLab conversion is written out in the loop because a method call costs more than # the arithmetic of a step. static [object[]] Blend([object[]] $Waypoints, [int] $Steps, [bool] $Ansi8, [bool] $OkLab) { $points = [int[][]]::new($Waypoints.Count) $labs = [double[][]]::new($Waypoints.Count) for ($w = 0; $w -lt $Waypoints.Count; $w++) { $points[$w] = [int[]]$Waypoints[$w] if ($OkLab) { $labs[$w] = [ColorMath]::ToOkLab($points[$w]) } } $table = [ColorMath]::SrgbToLinear $linear = [double[]]::new(3) $out = [object[]]::new($Steps) $segmentCount = $Waypoints.Count - 1 # Each waypoint's step, when every waypoint can have one $anchors = $null if ($segmentCount -gt 1 -and $Steps -ge $Waypoints.Count) { $anchors = [int[]]::new($Waypoints.Count) for ($w = 0; $w -lt $Waypoints.Count; $w++) { $anchors[$w] = [int][Math]::Floor((2.0 * $w * ($Steps - 1) + $segmentCount) / (2.0 * $segmentCount)) } } $segmentIndex = 0 for ($step = 0; $step -lt $Steps; $step++) { if ($Steps -eq 1) { $segmentIndex = 0 $ratio = 0.0 } elseif ($segmentCount -eq 1) { $segmentIndex = 0 $ratio = [double]$step / ($Steps - 1) } elseif ($null -ne $anchors) { while ($step -gt $anchors[$segmentIndex + 1]) { $segmentIndex++ } $ratio = [double]($step - $anchors[$segmentIndex]) / ($anchors[$segmentIndex + 1] - $anchors[$segmentIndex]) } else { $position = [double]$step / ($Steps - 1) * $segmentCount $segmentIndex = [Math]::Min([int][Math]::Floor($position), $segmentCount - 1) $ratio = $position - $segmentIndex } $start = $points[$segmentIndex] $end = $points[$segmentIndex + 1] $rgb = [int[]]::new(3) if ($ratio -eq 0.0) { $rgb[0] = $start[0] $rgb[1] = $start[1] $rgb[2] = $start[2] } elseif ($ratio -eq 1.0) { $rgb[0] = $end[0] $rgb[1] = $end[1] $rgb[2] = $end[2] } elseif ($OkLab) { # The OKLab coordinates between the two waypoints, to LMS and on to linear sRGB $startLab = $labs[$segmentIndex] $endLab = $labs[$segmentIndex + 1] $lightness = $startLab[0] + ($endLab[0] - $startLab[0]) * $ratio $greenRed = $startLab[1] + ($endLab[1] - $startLab[1]) * $ratio $blueYellow = $startLab[2] + ($endLab[2] - $startLab[2]) * $ratio $lRoot = $lightness + [ColorMath]::LA * $greenRed + [ColorMath]::LB * $blueYellow $mRoot = $lightness + [ColorMath]::MA * $greenRed + [ColorMath]::MB * $blueYellow $sRoot = $lightness + [ColorMath]::SA * $greenRed + [ColorMath]::SB * $blueYellow $l = $lRoot * $lRoot * $lRoot $m = $mRoot * $mRoot * $mRoot $s = $sRoot * $sRoot * $sRoot $linear[0] = [ColorMath]::R1 * $l + [ColorMath]::R2 * $m + [ColorMath]::R3 * $s $linear[1] = [ColorMath]::G1 * $l + [ColorMath]::G2 * $m + [ColorMath]::G3 * $s $linear[2] = [ColorMath]::B1 * $l + [ColorMath]::B2 * $m + [ColorMath]::B3 * $s # Each linear value to the channel value whose linear value is nearest, the lower # of two as near; below 0 is 0 and above 1 is 255. The sRGB curve gives a first # guess, which may be off where [Math]::Pow differs between platforms; the # table then decides, so the result does not depend on [Math]::Pow. for ($channel = 0; $channel -lt 3; $channel++) { $value = $linear[$channel] if ($value -le 0.0) { $rgb[$channel] = 0 } elseif ($value -ge 1.0) { $rgb[$channel] = 255 } else { $low = [int][Math]::Floor((1.055 * [Math]::Pow($value, 1.0 / 2.4) - 0.055) * 255.0) if ($low -lt 0) { $low = 0 } elseif ($low -gt 254) { $low = 254 } while ($low -lt 254 -and $table[$low + 1] -le $value) { $low++ } while ($low -gt 0 -and $table[$low] -gt $value) { $low-- } if ($table[$low + 1] - $value -lt $value - $table[$low]) { $rgb[$channel] = $low + 1 } else { $rgb[$channel] = $low } } } } else { $rgb[0] = [int][Math]::Round($start[0] + ($end[0] - $start[0]) * $ratio, [MidpointRounding]::ToEven) $rgb[1] = [int][Math]::Round($start[1] + ($end[1] - $start[1]) * $ratio, [MidpointRounding]::ToEven) $rgb[2] = [int][Math]::Round($start[2] + ($end[2] - $start[2]) * $ratio, [MidpointRounding]::ToEven) } if (-not $Ansi8) { $out[$step] = $rgb continue } # The nearest 256-color code by OKLab distance, as Convert-RGBToANSI8 returns it $out[$step] = [ColorMath]::NearestAnsi8($rgb[0], $rgb[1], $rgb[2]) } return $out } # The sRGB channel value 0-255 whose linear value is nearest the one given, the lower of two # as near, by binary search of SrgbToLinear; below 0 is 0 and above 1 is 255 static [int] FromLinear([double] $Value) { if ($Value -le 0.0) { return 0 } if ($Value -ge 1.0) { return 255 } $table = [ColorMath]::SrgbToLinear $low = 0 $high = 255 while ($high - $low -gt 1) { $middle = ($low + $high) -shr 1 if ($table[$middle] -le $Value) { $low = $middle } else { $high = $middle } } if ($table[$high] - $Value -lt $Value - $table[$low]) { return $high } return $low } # The sRGB channel values 0-255 of OKLab coordinates, out-of-range channels clamped static [int[]] FromOkLab([double[]] $Lab) { $lRoot = $Lab[0] + [ColorMath]::LA * $Lab[1] + [ColorMath]::LB * $Lab[2] $mRoot = $Lab[0] + [ColorMath]::MA * $Lab[1] + [ColorMath]::MB * $Lab[2] $sRoot = $Lab[0] + [ColorMath]::SA * $Lab[1] + [ColorMath]::SB * $Lab[2] $l = $lRoot * $lRoot * $lRoot $m = $mRoot * $mRoot * $mRoot $s = $sRoot * $sRoot * $sRoot return [int[]]@( [ColorMath]::FromLinear([ColorMath]::R1 * $l + [ColorMath]::R2 * $m + [ColorMath]::R3 * $s), [ColorMath]::FromLinear([ColorMath]::G1 * $l + [ColorMath]::G2 * $m + [ColorMath]::G3 * $s), [ColorMath]::FromLinear([ColorMath]::B1 * $l + [ColorMath]::B2 * $m + [ColorMath]::B3 * $s) ) } # The names -GradientSpace, -GradientHue, -GradientEasing, -GradientRepeat and # -GradientDirection take, in order; the methods below take each option as its index in its list static [string[]] $SpaceNames = @('OKLab', 'RGB', 'LinearRGB', 'OKLCH', 'HSL', 'HSV', 'Lab', 'LCh') static [string[]] $HueNames = @('Shorter', 'Longer', 'Increasing', 'Decreasing') static [string[]] $EasingNames = @( 'Linear', 'InSine', 'OutSine', 'InOutSine', 'InQuad', 'OutQuad', 'InOutQuad', 'InCubic', 'OutCubic', 'InOutCubic', 'InQuart', 'OutQuart', 'InOutQuart', 'InQuint', 'OutQuint', 'InOutQuint', 'InExpo', 'OutExpo', 'InOutExpo', 'InCirc', 'OutCirc', 'InOutCirc', 'InBack', 'OutBack', 'InOutBack', 'InElastic', 'OutElastic', 'InOutElastic', 'InBounce', 'OutBounce', 'InOutBounce' ) static [string[]] $RepeatNames = @('None', 'Repeat', 'Mirror') static [string[]] $DirectionNames = @('Forward', 'Reverse', 'CenterOut', 'EdgesIn') # The index of a name in a list of option names, compared without regard to case; 0, each # list's default, for a name not in it static [int] OptionIndex([string[]] $Names, [string] $Name) { for ($i = 0; $i -lt $Names.Count; $i++) { if ($Names[$i] -eq $Name) { return $i } } return 0 } # A gradient's options as GradientRgb takes them: the indexes of the space, hue path, easing, # repeat and direction, then the bands, 0 for a smooth blend static [int[]] GradientOptions([string] $Space, [string] $Hue, [string] $Easing, [string] $Repeat, [string] $Direction, [int] $Steps) { return [int[]]@( [ColorMath]::OptionIndex([ColorMath]::SpaceNames, $Space), [ColorMath]::OptionIndex([ColorMath]::HueNames, $Hue), [ColorMath]::OptionIndex([ColorMath]::EasingNames, $Easing), [ColorMath]::OptionIndex([ColorMath]::RepeatNames, $Repeat), [ColorMath]::OptionIndex([ColorMath]::DirectionNames, $Direction), $Steps ) } # Degrees in a radian and radians in a degree static [double] $DegreesPerRadian = [BitConverter]::Int64BitsToDouble(0x404CA5DC1A63C1F8) static [double] $RadiansPerDegree = [BitConverter]::Int64BitsToDouble(0x3F91DF46A2529D39) # Below these chromas a color counts as gray, with no hue: 2^-20 in OKLab and 2^-10 in CIELAB, # above the rounding left in an sRGB gray and below the chroma of any color one step from one static [double] $OkLchHueless = 1.0 / 1048576.0 static [double] $LchHueless = 1.0 / 1024.0 # CSS Color 4's matrices, row by row: linear sRGB to CIE XYZ with D65 white and back, and the # Bradford adaptation from D65 to D50 and back static [double[]] $ToXyz = @( [BitConverter]::Int64BitsToDouble(0x3FDA649C610130B1), [BitConverter]::Int64BitsToDouble(0x3FD6E2A96CCDCB17), [BitConverter]::Int64BitsToDouble(0x3FC719FE95DEFF92), [BitConverter]::Int64BitsToDouble(0x3FCB37C144093A36), [BitConverter]::Int64BitsToDouble(0x3FE6E2A96CCDCB17), [BitConverter]::Int64BitsToDouble(0x3FB27B32117F32DB), [BitConverter]::Int64BitsToDouble(0x3F93CB7548C0E484), [BitConverter]::Int64BitsToDouble(0x3FBE838C9112641F), [BitConverter]::Int64BitsToDouble(0x3FEE6AC26776AE5E) ) static [double[]] $FromXyz = @( [BitConverter]::Int64BitsToDouble(0x4009ED81A61E643A), -[BitConverter]::Int64BitsToDouble(0x3FF8991F1A515886), -[BitConverter]::Int64BitsToDouble(0x3FDFE93D1B3918E5), -[BitConverter]::Int64BitsToDouble(0x3FEF040B3AF5D706), [BitConverter]::Int64BitsToDouble(0x3FFE03F67FB55A13), [BitConverter]::Int64BitsToDouble(0x3FA546B459182D75), [BitConverter]::Int64BitsToDouble(0x3FAC7B8BB9F1E66D), -[BitConverter]::Int64BitsToDouble(0x3FCA1BEABFC8688F), [BitConverter]::Int64BitsToDouble(0x3FF0E95AF667A0D1) ) static [double[]] $D65ToD50 = @( [BitConverter]::Int64BitsToDouble(0x3FF0C45207B7C1D9), [BitConverter]::Int64BitsToDouble(0x3F977F626B1E2A0E), -[BitConverter]::Int64BitsToDouble(0x3FA9B2CCFC72D22E), [BitConverter]::Int64BitsToDouble(0x3F9E56C096E0CD1E), [BitConverter]::Int64BitsToDouble(0x3FEFB1A389F7B189), -[BitConverter]::Int64BitsToDouble(0x3F917BCB426228F0), -[BitConverter]::Int64BitsToDouble(0x3F82EE03EA54107A), [BitConverter]::Int64BitsToDouble(0x3F8ED54198AFD11C), [BitConverter]::Int64BitsToDouble(0x3FE80F5AA72DFEC3) ) static [double[]] $D50ToD65 = @( [BitConverter]::Int64BitsToDouble(0x3FEE933CFF2FA1D7), -[BitConverter]::Int64BitsToDouble(0x3F97A71F125F4EBC), [BitConverter]::Int64BitsToDouble(0x3FB031C1FCB7565E), -[BitConverter]::Int64BitsToDouble(0x3F9D0CF2F724D576), [BitConverter]::Int64BitsToDouble(0x3FF028F0EF3DEA4D), [BitConverter]::Int64BitsToDouble(0x3F958BE3371EB644), [BitConverter]::Int64BitsToDouble(0x3F8938171900D5F0), -[BitConverter]::Int64BitsToDouble(0x3F94FFF50C39F78C), [BitConverter]::Int64BitsToDouble(0x3FF5492DC80EC342) ) # D50 white's X and Z, Y being 1: 0.3457 / 0.3585 and (1 - 0.3457 - 0.3585) / 0.3585, as CSS # writes them static [double] $WhiteX = [BitConverter]::Int64BitsToDouble(0x3FEEDB829B3E0DDB) static [double] $WhiteZ = [BitConverter]::Int64BitsToDouble(0x3FEA6741C471F7DC) # CIELAB's 216/24389 and 24389/27 static [double] $LabEpsilon = [BitConverter]::Int64BitsToDouble(0x3F822354D28F7CD6) static [double] $LabKappa = [BitConverter]::Int64BitsToDouble(0x408C3A5ED097B426) # A matrix of nine numbers, row by row, times a column, each row's products added left to right hidden static [double[]] Multiply([double[]] $Matrix, [double[]] $Column) { return [double[]]@( ($Matrix[0] * $Column[0] + $Matrix[1] * $Column[1] + $Matrix[2] * $Column[2]), ($Matrix[3] * $Column[0] + $Matrix[4] * $Column[1] + $Matrix[5] * $Column[2]), ($Matrix[6] * $Column[0] + $Matrix[7] * $Column[1] + $Matrix[8] * $Column[2]) ) } # The CIELAB coordinates L, a and b of an sRGB color, with D50 white, as CSS lab() defines them static [double[]] ToLab([int[]] $Rgb) { $table = [ColorMath]::SrgbToLinear $xyz = [ColorMath]::Multiply([ColorMath]::ToXyz, [double[]]@($table[$Rgb[0]], $table[$Rgb[1]], $table[$Rgb[2]])) $d50 = [ColorMath]::Multiply([ColorMath]::D65ToD50, $xyz) $scaled = [double[]]@(($d50[0] / [ColorMath]::WhiteX), $d50[1], ($d50[2] / [ColorMath]::WhiteZ)) $f = [double[]]::new(3) for ($k = 0; $k -lt 3; $k++) { if ($scaled[$k] -gt [ColorMath]::LabEpsilon) { $f[$k] = [ColorMath]::CubeRoot($scaled[$k]) } else { $f[$k] = ([ColorMath]::LabKappa * $scaled[$k] + 16.0) / 116.0 } } return [double[]]@((116.0 * $f[1] - 16.0), (500.0 * ($f[0] - $f[1])), (200.0 * ($f[1] - $f[2]))) } # The sRGB channel values 0-255 of CIELAB coordinates with D50 white, out-of-range channels # clamped static [int[]] FromLab([double[]] $Lab) { $epsilon = [ColorMath]::LabEpsilon $kappa = [ColorMath]::LabKappa $fy = ($Lab[0] + 16.0) / 116.0 $fx = $Lab[1] / 500.0 + $fy $fz = $fy - $Lab[2] / 200.0 if ($fx * $fx * $fx -gt $epsilon) { $x = $fx * $fx * $fx } else { $x = (116.0 * $fx - 16.0) / $kappa } if ($Lab[0] -gt $kappa * $epsilon) { $y = $fy * $fy * $fy } else { $y = $Lab[0] / $kappa } if ($fz * $fz * $fz -gt $epsilon) { $z = $fz * $fz * $fz } else { $z = (116.0 * $fz - 16.0) / $kappa } $d65 = [ColorMath]::Multiply([ColorMath]::D50ToD65, [double[]]@(($x * [ColorMath]::WhiteX), $y, ($z * [ColorMath]::WhiteZ))) $linear = [ColorMath]::Multiply([ColorMath]::FromXyz, $d65) return [int[]]@([ColorMath]::FromLinear($linear[0]), [ColorMath]::FromLinear($linear[1]), [ColorMath]::FromLinear($linear[2])) } # Lightness, chroma, hue in degrees and 1 for no hue, from a lightness and two opponent axes; # a chroma below HuelessBelow has no hue hidden static [double[]] Polar([double[]] $Lab, [double] $HuelessBelow) { $chroma = [Math]::Sqrt($Lab[1] * $Lab[1] + $Lab[2] * $Lab[2]) $hue = [ColorMath]::Atan2($Lab[2], $Lab[1]) * [ColorMath]::DegreesPerRadian if ($hue -lt 0.0) { $hue += 360.0 } $hueless = 0.0 if ($chroma -lt $HuelessBelow) { $hueless = 1.0 } return [double[]]@($Lab[0], $chroma, $hue, $hueless) } # A lightness and two opponent axes from lightness, chroma and hue in degrees hidden static [double[]] FromPolar([double] $Lightness, [double] $Chroma, [double] $Hue) { $radians = $Hue * [ColorMath]::RadiansPerDegree return [double[]]@($Lightness, ($Chroma * [ColorMath]::Cos($radians)), ($Chroma * [ColorMath]::Sin($radians))) } # The hue in degrees of channels from 0 to 1 whose largest is Max, Range above the smallest hidden static [double] SextantHue([double] $R, [double] $G, [double] $B, [double] $Max, [double] $Range) { if ($Max -eq $R) { $wrap = 0.0 if ($G -lt $B) { $wrap = 6.0 } $sextant = ($G - $B) / $Range + $wrap } elseif ($Max -eq $G) { $sextant = ($B - $R) / $Range + 2.0 } else { $sextant = ($R - $G) / $Range + 4.0 } return $sextant * 60.0 } # HSL saturation, lightness and hue of an sRGB color, then 1 for a gray, which has no hue hidden static [double[]] ToHsl([int[]] $Rgb) { $r = $Rgb[0] / 255.0 $g = $Rgb[1] / 255.0 $b = $Rgb[2] / 255.0 $max = [Math]::Max([Math]::Max($r, $g), $b) $min = [Math]::Min([Math]::Min($r, $g), $b) $lightness = ($max + $min) / 2.0 $range = $max - $min if ($range -eq 0.0) { return [double[]]@(0.0, $lightness, 0.0, 1.0) } $saturation = $range / (1.0 - [Math]::Abs(2.0 * $lightness - 1.0)) return [double[]]@($saturation, $lightness, [ColorMath]::SextantHue($r, $g, $b, $max, $range), 0.0) } # HSV saturation, value and hue of an sRGB color, then 1 for a gray, which has no hue hidden static [double[]] ToHsv([int[]] $Rgb) { $r = $Rgb[0] / 255.0 $g = $Rgb[1] / 255.0 $b = $Rgb[2] / 255.0 $max = [Math]::Max([Math]::Max($r, $g), $b) $min = [Math]::Min([Math]::Min($r, $g), $b) $range = $max - $min if ($range -eq 0.0) { return [double[]]@(0.0, $max, 0.0, 1.0) } return [double[]]@(($range / $max), $max, [ColorMath]::SextantHue($r, $g, $b, $max, $range), 0.0) } # The sRGB channel values of a hue in degrees from 0 to 360 and a saturation and value from 0 # to 1: C = V * S, X = C * (1 - |(H / 60) mod 2 - 1|), m = V - C, each channel rounded half to # even after multiplying by 255 static [int[]] HsvToRgb([double] $H, [double] $S, [double] $V) { $chroma = $V * $S $sector = $H / 60.0 $x = $chroma * (1.0 - [Math]::Abs(($sector - 2.0 * [Math]::Floor($sector / 2.0)) - 1.0)) $offset = $V - $chroma $parts = switch ([int][Math]::Floor($sector)) { 0 { $chroma, $x, 0.0 } 1 { $x, $chroma, 0.0 } 2 { 0.0, $chroma, $x } 3 { 0.0, $x, $chroma } 4 { $x, 0.0, $chroma } default { $chroma, 0.0, $x } } return [int[]]@( [int][Math]::Round(($parts[0] + $offset) * 255.0, [MidpointRounding]::ToEven), [int][Math]::Round(($parts[1] + $offset) * 255.0, [MidpointRounding]::ToEven), [int][Math]::Round(($parts[2] + $offset) * 255.0, [MidpointRounding]::ToEven) ) } # A color's coordinates in a space, by its index in SpaceNames: three numbers, then 1 for a # color with no hue and 0 for one with. The round spaces keep their hue last, in degrees from 0 # to 360: OKLCH and LCh hold lightness, chroma and hue; HSL saturation, lightness and hue; HSV # saturation, value and hue, the saturations and lightness and value from 0 to 1. static [double[]] ToCoordinates([int[]] $Rgb, [int] $Space) { switch ($Space) { 1 { return [double[]]@($Rgb[0], $Rgb[1], $Rgb[2], 0.0) } 2 { $table = [ColorMath]::SrgbToLinear return [double[]]@($table[$Rgb[0]], $table[$Rgb[1]], $table[$Rgb[2]], 0.0) } 3 { return [ColorMath]::Polar([ColorMath]::ToOkLab($Rgb), [ColorMath]::OkLchHueless) } 4 { return [ColorMath]::ToHsl($Rgb) } 5 { return [ColorMath]::ToHsv($Rgb) } 6 { $lab = [ColorMath]::ToLab($Rgb) return [double[]]@($lab[0], $lab[1], $lab[2], 0.0) } 7 { return [ColorMath]::Polar([ColorMath]::ToLab($Rgb), [ColorMath]::LchHueless) } } $lab = [ColorMath]::ToOkLab($Rgb) return [double[]]@($lab[0], $lab[1], $lab[2], 0.0) } # The two hues a blend runs between, after CSS Color 4's fix-up for the path, by its index in # HueNames: a color with no hue takes the other's, and one hue moves a turn so the blend goes # the chosen way round hidden static [double[]] HueEnds([double[]] $A, [double[]] $B, [int] $Path) { $h1 = $A[2] $h2 = $B[2] if ($A[3] -ne 0.0 -and $B[3] -ne 0.0) { $h1 = 0.0 $h2 = 0.0 } elseif ($A[3] -ne 0.0) { $h1 = $B[2] } elseif ($B[3] -ne 0.0) { $h2 = $A[2] } $difference = $h2 - $h1 switch ($Path) { 0 { if ($difference -gt 180.0) { $h1 += 360.0 } elseif ($difference -lt -180.0) { $h2 += 360.0 } } 1 { if ($difference -gt 0.0 -and $difference -lt 180.0) { $h1 += 360.0 } elseif ($difference -gt -180.0 -and $difference -le 0.0) { $h2 += 360.0 } } 2 { if ($difference -lt 0.0) { $h2 += 360.0 } } 3 { if ($difference -gt 0.0) { $h1 += 360.0 } } } return [double[]]@($h1, $h2) } # The color at Ratio along the blend from one color to another, given both colors and their # coordinates in the space, by its index in SpaceNames, with the hue path by its index in # HueNames: exactly the first at 0 and the second at 1 static [int[]] BlendColors([int[]] $From, [int[]] $To, [double[]] $A, [double[]] $B, [double] $Ratio, [int] $Space, [int] $Path) { if ($Ratio -eq 0.0) { return [int[]]$From.Clone() } if ($Ratio -eq 1.0) { return [int[]]$To.Clone() } $mix0 = $A[0] + ($B[0] - $A[0]) * $Ratio $mix1 = $A[1] + ($B[1] - $A[1]) * $Ratio $mix2 = $A[2] + ($B[2] - $A[2]) * $Ratio switch ($Space) { 0 { return [ColorMath]::FromOkLab([double[]]@($mix0, $mix1, $mix2)) } 1 { return [int[]]@( [int][Math]::Round($mix0, [MidpointRounding]::ToEven), [int][Math]::Round($mix1, [MidpointRounding]::ToEven), [int][Math]::Round($mix2, [MidpointRounding]::ToEven) ) } 2 { return [int[]]@([ColorMath]::FromLinear($mix0), [ColorMath]::FromLinear($mix1), [ColorMath]::FromLinear($mix2)) } 6 { return [ColorMath]::FromLab([double[]]@($mix0, $mix1, $mix2)) } } $ends = [ColorMath]::HueEnds($A, $B, $Path) $turned = $ends[0] + ($ends[1] - $ends[0]) * $Ratio $hue = $turned - 360.0 * [Math]::Floor($turned / 360.0) switch ($Space) { 3 { return [ColorMath]::FromOkLab([ColorMath]::FromPolar($mix0, $mix1, $hue)) } 7 { return [ColorMath]::FromLab([ColorMath]::FromPolar($mix0, $mix1, $hue)) } 4 { return [ColorMath]::HslToRgb($hue, $mix0, $mix1) } } return [ColorMath]::HsvToRgb($hue, $mix0, $mix1) } # Back's overshoot and the factor InOutBack scales it by: 1.70158 and 1.525 static [double] $Back = [BitConverter]::Int64BitsToDouble(0x3FFB39ABF3387161) static [double] $BackInOutScale = [BitConverter]::Int64BitsToDouble(0x3FF8666666666666) # The value at X from 0 to 1 of an easing curve, by its index in EasingNames, as easings.net # writes the curves. Back and Elastic overshoot below 0 and above 1. static [double] Ease([int] $Easing, [double] $X) { $overshoot = [ColorMath]::Back switch ($Easing) { 0 { return $X } 1 { return 1.0 - [ColorMath]::Cos(($X * [ColorMath]::Pi) / 2.0) } 2 { return [ColorMath]::Sin(($X * [ColorMath]::Pi) / 2.0) } 3 { return -([ColorMath]::Cos([ColorMath]::Pi * $X) - 1.0) / 2.0 } 4 { return $X * $X } 5 { $rest = 1.0 - $X return 1.0 - $rest * $rest } 6 { if ($X -lt 0.5) { return 2.0 * $X * $X } $rest = -2.0 * $X + 2.0 return 1.0 - $rest * $rest / 2.0 } 7 { return $X * $X * $X } 8 { $rest = 1.0 - $X return 1.0 - $rest * $rest * $rest } 9 { if ($X -lt 0.5) { return 4.0 * $X * $X * $X } $rest = -2.0 * $X + 2.0 return 1.0 - $rest * $rest * $rest / 2.0 } 10 { return $X * $X * $X * $X } 11 { $rest = 1.0 - $X return 1.0 - $rest * $rest * $rest * $rest } 12 { if ($X -lt 0.5) { return 8.0 * $X * $X * $X * $X } $rest = -2.0 * $X + 2.0 return 1.0 - $rest * $rest * $rest * $rest / 2.0 } 13 { return $X * $X * $X * $X * $X } 14 { $rest = 1.0 - $X return 1.0 - $rest * $rest * $rest * $rest * $rest } 15 { if ($X -lt 0.5) { return 16.0 * $X * $X * $X * $X * $X } $rest = -2.0 * $X + 2.0 return 1.0 - $rest * $rest * $rest * $rest * $rest / 2.0 } 16 { if ($X -eq 0.0) { return 0.0 } return [ColorMath]::Exp2(10.0 * $X - 10.0) } 17 { if ($X -eq 1.0) { return 1.0 } return 1.0 - [ColorMath]::Exp2(-10.0 * $X) } 18 { if ($X -eq 0.0) { return 0.0 } if ($X -eq 1.0) { return 1.0 } if ($X -lt 0.5) { return [ColorMath]::Exp2(20.0 * $X - 10.0) / 2.0 } return (2.0 - [ColorMath]::Exp2(-20.0 * $X + 10.0)) / 2.0 } 19 { return 1.0 - [Math]::Sqrt(1.0 - $X * $X) } 20 { $rest = $X - 1.0 return [Math]::Sqrt(1.0 - $rest * $rest) } 21 { if ($X -lt 0.5) { $twice = 2.0 * $X return (1.0 - [Math]::Sqrt(1.0 - $twice * $twice)) / 2.0 } $rest = -2.0 * $X + 2.0 return ([Math]::Sqrt(1.0 - $rest * $rest) + 1.0) / 2.0 } 22 { return ($overshoot + 1.0) * $X * $X * $X - $overshoot * $X * $X } 23 { $rest = $X - 1.0 return 1.0 + ($overshoot + 1.0) * $rest * $rest * $rest + $overshoot * $rest * $rest } 24 { $scaled = $overshoot * [ColorMath]::BackInOutScale if ($X -lt 0.5) { $twice = 2.0 * $X return ($twice * $twice * (($scaled + 1.0) * 2.0 * $X - $scaled)) / 2.0 } $rest = 2.0 * $X - 2.0 return ($rest * $rest * (($scaled + 1.0) * ($X * 2.0 - 2.0) + $scaled) + 2.0) / 2.0 } 25 { if ($X -eq 0.0) { return 0.0 } if ($X -eq 1.0) { return 1.0 } $period = (2.0 * [ColorMath]::Pi) / 3.0 return -[ColorMath]::Exp2(10.0 * $X - 10.0) * [ColorMath]::Sin(($X * 10.0 - 10.75) * $period) } 26 { if ($X -eq 0.0) { return 0.0 } if ($X -eq 1.0) { return 1.0 } $period = (2.0 * [ColorMath]::Pi) / 3.0 return [ColorMath]::Exp2(-10.0 * $X) * [ColorMath]::Sin(($X * 10.0 - 0.75) * $period) + 1.0 } 27 { if ($X -eq 0.0) { return 0.0 } if ($X -eq 1.0) { return 1.0 } $period = (2.0 * [ColorMath]::Pi) / 4.5 if ($X -lt 0.5) { return -([ColorMath]::Exp2(20.0 * $X - 10.0) * [ColorMath]::Sin((20.0 * $X - 11.125) * $period)) / 2.0 } return ([ColorMath]::Exp2(-20.0 * $X + 10.0) * [ColorMath]::Sin((20.0 * $X - 11.125) * $period)) / 2.0 + 1.0 } 28 { return 1.0 - [ColorMath]::OutBounce(1.0 - $X) } 29 { return [ColorMath]::OutBounce($X) } 30 { if ($X -lt 0.5) { return (1.0 - [ColorMath]::OutBounce(1.0 - 2.0 * $X)) / 2.0 } return (1.0 + [ColorMath]::OutBounce(2.0 * $X - 1.0)) / 2.0 } } return $X } # easings.net's easeOutBounce: four parabolas, each lower bounce a quarter as high hidden static [double] OutBounce([double] $X) { if ($X -lt 1.0 / 2.75) { return 7.5625 * $X * $X } if ($X -lt 2.0 / 2.75) { $rest = $X - 1.5 / 2.75 return 7.5625 * $rest * $rest + 0.75 } if ($X -lt 2.5 / 2.75) { $rest = $X - 2.25 / 2.75 return 7.5625 * $rest * $rest + 0.9375 } $rest = $X - 2.625 / 2.75 return 7.5625 * $rest * $rest + 0.984375 } # A gradient color with its place after an '@', as a percent: 'Gold@40' is Gold at 0.4. The # place is the digits over 10 to the power of the decimals plus 2, in one division; above 100 # is 1. Returns the color and the place, or $null for a text with no place. static [object[]] SplitPlace([string] $Text) { $at = $Text.LastIndexOf('@') if ($at -lt 0) { return $null } $place = $Text.Substring($at + 1).Trim() if ($place.EndsWith('%')) { $place = $place.Substring(0, $place.Length - 1).TrimEnd() } $whole = $place $fraction = '' $dot = $place.IndexOf('.') if ($dot -ge 0) { $whole = $place.Substring(0, $dot) $fraction = $place.Substring($dot + 1) } $digits = $whole + $fraction if ($digits.Length -eq 0 -or $digits.Length -gt 15) { return $null } $mantissa = [long]0 foreach ($digit in $digits.ToCharArray()) { $code = [int]$digit if ($code -lt 48 -or $code -gt 57) { return $null } $mantissa = $mantissa * 10 + ($code - 48) } $scale = 100.0 for ($k = 0; $k -lt $fraction.Length; $k++) { $scale *= 10.0 } return [object[]]@($Text.Substring(0, $at).TrimEnd(), [Math]::Min(1.0, [double]$mantissa / $scale)) } # Each stop's place from 0 to 1 as CSS places color stops, given each stop's place or $null: # the first at 0 and the last at 1 unless placed, a place before an earlier stop's moved up to # it, and the stops with none spread evenly between the placed ones around them static [double[]] ResolvePlaces([object[]] $Given) { $count = $Given.Count $last = $count - 1 $known = [object[]]::new($count) for ($k = 0; $k -lt $count; $k++) { if ($null -ne $Given[$k]) { $known[$k] = [Math]::Min(1.0, [Math]::Max(0.0, [double]$Given[$k])) } } if ($null -eq $known[0]) { $known[0] = 0.0 } if ($null -eq $known[$last]) { $known[$last] = 1.0 } $highest = 0.0 for ($k = 0; $k -lt $count; $k++) { if ($null -eq $known[$k]) { continue } if ($known[$k] -lt $highest) { $known[$k] = $highest } else { $highest = $known[$k] } } $places = [double[]]::new($count) $k = 0 while ($k -le $last) { if ($null -ne $known[$k]) { $places[$k] = $known[$k] $k++ continue } $before = $k - 1 $after = $k while ($null -eq $known[$after]) { $after++ } $from = $places[$before] $to = [double]$known[$after] $span = [double]($after - $before) for ($i = $k; $i -lt $after; $i++) { $places[$i] = $from + ($to - $from) * (($i - $before) / $span) } $k = $after } return $places } # The segment a place falls in among stops' places, and its ratio from the segment's first # stop to its last: the first stop's color at or before the first place, the last's at or # after the last, and the later stop's where two share a place static [double[]] Locate([double[]] $Places, [double] $Place) { $last = $Places.Count - 1 if ($Place -le $Places[0]) { return [double[]]@(0.0, 0.0) } if ($Place -ge $Places[$last]) { return [double[]]@(($last - 1), 1.0) } $segment = 0 while ($segment + 1 -lt $last -and $Places[$segment + 1] -le $Place) { $segment++ } return [double[]]@($segment, (($Place - $Places[$segment]) / ($Places[$segment + 1] - $Places[$segment]))) } # A place carried back into the span from the first stop's place to the last's by the repeat, # by its index in RepeatNames: unchanged inside the span or with no repeat static [double] RepeatPlace([double] $Place, [double] $First, [double] $Last, [int] $Repeat) { $span = $Last - $First if ($Repeat -eq 0 -or $span -le 0.0 -or ($Place -ge $First -and $Place -le $Last)) { return $Place } $turns = ($Place - $First) / $span if ($Repeat -eq 2) { $within = $turns - 2.0 * [Math]::Floor($turns / 2.0) if ($within -gt 1.0) { $within = 2.0 - $within } } else { $within = $turns - [Math]::Floor($turns) } return $First + $within * $span } # A place moved to the place of its band when the span from the first stop's place to the # last's is cut into Bands: band k of n at k / (n - 1) of the span, so the first band is the # first stop and the last band the last; a single band is the first stop, and 0 bands leave # the place as it is static [double] BandPlace([double] $Place, [double] $First, [double] $Last, [int] $Bands) { $span = $Last - $First if ($Bands -eq 0 -or $span -le 0.0) { return $Place } if ($Bands -eq 1) { return $First } $fraction = [Math]::Min(1.0, [Math]::Max(0.0, ($Place - $First) / $span)) $band = [Math]::Min([int][Math]::Floor($fraction * $Bands), $Bands - 1) return $First + ($band / ($Bands - 1)) * $span } # The place along the gradient of a place from 0 to 1 across the text, by the direction's # index in DirectionNames static [double] DirectionPlace([double] $Across, [int] $Direction) { switch ($Direction) { 1 { return 1.0 - $Across } 2 { return [Math]::Abs(2.0 * $Across - 1.0) } 3 { return 1.0 - [Math]::Abs(2.0 * $Across - 1.0) } } return $Across } # The place along the gradient a place across the text maps to, through the direction, the # easing curve, the repeat and the bands, in that order, with Options as GradientOptions # returns them static [double] Shape([double] $Across, [double[]] $Places, [int[]] $Options) { $first = $Places[0] $last = $Places[$Places.Count - 1] $eased = [ColorMath]::Ease($Options[2], [ColorMath]::DirectionPlace($Across, $Options[4])) return [ColorMath]::BandPlace([ColorMath]::RepeatPlace($eased, $first, $last, $Options[3]), $first, $last, $Options[5]) } # A stop's place moved to the nearest place a character takes: the characters' places are # Offset + Interval x j over Denominator, from Lowest to Highest, and a half rounds up. Evenly # spaced stops, stop EvenIndex of Count, are placed in integers, exactly; EvenIndex is -1 for # stops that are not. static [int] Snapped([double] $Place, [int] $EvenIndex, [int] $Count, [int] $Denominator, [int] $Interval, [int] $Offset, [int] $Lowest, [int] $Highest) { if ($EvenIndex -ge 0) { # j = floor((index x denominator / spans - offset) / interval + 1/2) in integers; the # sum is never below 0, since an offset of 1 comes with an interval of 2 $spans = $Count - 1 $sum = 2 * $EvenIndex * $Denominator + $Interval * $spans - 2 * $Offset * $spans $divisor = 2 * $Interval * $spans $numerator = $Offset + $Interval * (($sum - $sum % $divisor) / $divisor) } else { $j = [Math]::Max(0.0, [Math]::Floor(($Place * $Denominator - $Offset) / $Interval + 0.5)) $numerator = $Offset + $Interval * [int]$j } return [Math]::Min($Highest, [Math]::Max($Lowest, $numerator)) } # The numerator over Denominator of character I's place along the gradient, by the # direction's index in DirectionNames hidden static [int] CharacterNumerator([int] $I, [int] $Denominator, [int] $Direction) { switch ($Direction) { 1 { return $Denominator - $I } 2 { return [Math]::Abs(2 * $I - $Denominator) } 3 { return $Denominator - [Math]::Abs(2 * $I - $Denominator) } } return $I } # Each of Count stops' place, or $null, from a list of places that may be shorter or $null hidden static [object[]] StopPlaces([object[]] $Places, [int] $Count) { $given = [object[]]::new($Count) if ($null -ne $Places) { for ($k = 0; $k -lt [Math]::Min($Places.Count, $Count); $k++) { $given[$k] = $Places[$k] } } return $given } # The color at At, a segment and the ratio along it as Locate returns them hidden static [int[]] BlendAt([int[][]] $Rgbs, [double[][]] $Coordinates, [double[]] $At, [int] $Space, [int] $Hue) { $segment = [int]$At[0] return [ColorMath]::BlendColors($Rgbs[$segment], $Rgbs[$segment + 1], $Coordinates[$segment], $Coordinates[$segment + 1], $At[1], $Space, $Hue) } # The colors of a gradient across Count characters, as RGB arrays: Stops are RGB arrays, Places # each stop's place from 0 to 1 or $null, and Options as GradientOptions returns them. With # evenly spaced stops and no option but a space of OKLab or RGB, the colors are Blend's. static [object[]] GradientRgb([object[]] $Stops, [object[]] $Places, [int] $Count, [int[]] $Options) { if ($Count -le 0 -or $Stops.Count -eq 0) { return [object[]]@() } $rgbs = [int[][]]::new($Stops.Count) for ($k = 0; $k -lt $Stops.Count; $k++) { $rgbs[$k] = [int[]]$Stops[$k] } $out = [object[]]::new($Count) if ($Stops.Count -eq 1) { for ($i = 0; $i -lt $Count; $i++) { $out[$i] = [int[]]$rgbs[0].Clone() } return $out } $resolved = [ColorMath]::ResolvePlaces([ColorMath]::StopPlaces($Places, $Stops.Count)) $spans = $Stops.Count - 1 $even = $true for ($k = 0; $k -le $spans; $k++) { if ($resolved[$k] -ne $k / $spans) { $even = $false } } $space = $Options[0] $hue = $Options[1] if ($even -and $space -le 1 -and $Options[2] -eq 0 -and $Options[3] -eq 0 -and $Options[4] -eq 0 -and $Options[5] -eq 0) { return [ColorMath]::Blend($rgbs, $Count, $false, $space -eq 0) } $coordinates = [double[][]]::new($Stops.Count) for ($k = 0; $k -lt $Stops.Count; $k++) { $coordinates[$k] = [ColorMath]::ToCoordinates($rgbs[$k], $space) } if ($Count -eq 1) { $out[0] = [ColorMath]::BlendAt($rgbs, $coordinates, [ColorMath]::Locate($resolved, [ColorMath]::Shape(0.0, $resolved, $Options)), $space, $hue) return $out } # Each character's place is a numerator over Count - 1, after the direction. The places # the characters take are every numerator from 0 to Count - 1, or, from the middle out or # the ends in, every other one. $denominator = $Count - 1 $direction = $Options[4] $interval = 1 $offset = 0 $lowest = 0 $highest = $denominator if ($direction -eq 2) { $interval = 2 $offset = $denominator % 2 $lowest = $denominator % 2 } elseif ($direction -eq 3) { $interval = 2 $highest = $denominator - $denominator % 2 } $width = $highest - $lowest if (($width - $width % $interval) / $interval + 1 -lt $Stops.Count) { for ($i = 0; $i -lt $Count; $i++) { $across = [ColorMath]::CharacterNumerator($i, $denominator, $direction) / $denominator $out[$i] = [ColorMath]::BlendAt($rgbs, $coordinates, [ColorMath]::Locate($resolved, [ColorMath]::Shape($across, $resolved, $Options)), $space, $hue) } return $out } # With a place for every stop, each stop moves to the nearest character's place $numerators = [int[]]::new($Stops.Count) for ($k = 0; $k -lt $Stops.Count; $k++) { $evenIndex = -1 if ($even) { $evenIndex = $k } $numerators[$k] = [ColorMath]::Snapped($resolved[$k], $evenIndex, $Stops.Count, $denominator, $interval, $offset, $lowest, $highest) } $wholeSpan = $numerators[0] -eq 0 -and $numerators[$spans] -eq $denominator if ($Options[2] -eq 0 -and $Options[5] -eq 0 -and ($Options[3] -eq 0 -or $wholeSpan)) { $stopNumerators = [double[]]::new($Stops.Count) for ($k = 0; $k -lt $Stops.Count; $k++) { $stopNumerators[$k] = $numerators[$k] } for ($i = 0; $i -lt $Count; $i++) { $at = [ColorMath]::Locate($stopNumerators, [ColorMath]::CharacterNumerator($i, $denominator, $direction)) $out[$i] = [ColorMath]::BlendAt($rgbs, $coordinates, $at, $space, $hue) } return $out } $snappedPlaces = [double[]]::new($Stops.Count) for ($k = 0; $k -lt $Stops.Count; $k++) { $snappedPlaces[$k] = $numerators[$k] / $denominator } for ($i = 0; $i -lt $Count; $i++) { $across = [ColorMath]::CharacterNumerator($i, $denominator, $direction) / $denominator $out[$i] = [ColorMath]::BlendAt($rgbs, $coordinates, [ColorMath]::Locate($snappedPlaces, [ColorMath]::Shape($across, $snappedPlaces, $Options)), $space, $hue) } return $out } # The color at a place from 0 to 1 along a gradient, for a single color that moves along it; # stops keep their exact places, since there are no characters to move them to. A place # outside 0 to 1 is the nearer end, and NaN is 0. static [int[]] GradientRgbAt([object[]] $Stops, [object[]] $Places, [double] $Place, [int[]] $Options) { if ($Stops.Count -eq 0) { return [int[]]@(128, 128, 128) } if ($Stops.Count -eq 1) { return [int[]]([int[]]$Stops[0]).Clone() } $along = 0.0 if (-not [double]::IsNaN($Place)) { $along = [Math]::Min(1.0, [Math]::Max(0.0, $Place)) } $resolved = [ColorMath]::ResolvePlaces([ColorMath]::StopPlaces($Places, $Stops.Count)) $at = [ColorMath]::Locate($resolved, [ColorMath]::Shape($along, $resolved, $Options)) $segment = [int]$at[0] $from = [int[]]$Stops[$segment] $to = [int[]]$Stops[$segment + 1] return [ColorMath]::BlendColors($from, $to, [ColorMath]::ToCoordinates($from, $Options[0]), [ColorMath]::ToCoordinates($to, $Options[0]), $at[1], $Options[0], $Options[1]) } } |