microsoft / PowerToys

Windows system utilities to maximize productivity
MIT License
109.75k stars 6.46k forks source link

Color picker float format is too imprecise (2 decimal places are not enough) #31370

Open morriscurtis opened 7 months ago

morriscurtis commented 7 months ago

Microsoft PowerToys version

0.78.0

Installation method

WinGet

Running as admin

No

Area(s) with issue?

ColorPicker

Steps to reproduce

Create a custom float color format in the color picker, eg: image As you can see in the example string, the precision is only 2 decimal places. When you multiply this value with 255 to get back to the byte value, it should be the same value as shown in the byte color format. Let's say you pick this shade of red: image If you multiply the float values with 255, you do not get the same color: 0.93 255 = 237.15 // Red channel is correct, but... 0.16 255 = 40.8 // Blue channel is only correct if you round, but wrong if you floor the number! 0.22 * 255 = 56.1 // Green channel is wrong, doesn't matter whether you round or floor the number!

✔️ Expected Behavior

The float color format should be able to represent all 256 shades of a color channel exactly.

❌ Actual Behavior

The picked color is in many cases not exactly represented by the 2 decimal points of the current float color format. The range from 0.00 to 1.00 contains 101 possible values, while the range from 0 to 255 contains 256 values. It is impossible to uniquely map 256 values into just 101. The range from 0.000 to 1.000 contains 1001 values, so there an exact mapping is possible.

Other Software

No response

morriscurtis commented 7 months ago

I'm assuming the current implementation is something like this:

var floatValue = Math.Round(byteValue * 100f / 255f) / 100f;

Calculating the correct float values is not a trivial thing (simply changing the factors above from 100f to 1000f won't suffice). There would still be rounding errors.

One simple way of implementing this would be a lookup table. Here is an example implementation of a lookup table for 3 decimal places:

// Generate the lookup table
var lookupTable = new float[256];
var lastNewByteValue = -1;
for (int i = 0; i <= 1000; ++i)
{
    var value = i * 0.001f;
    var byteValue = (int)(value * 255f);
    if (byteValue != lastNewByteValue)
    {
        lookupTable[byteValue] = value;
        lastNewByteValue = byteValue;
    }
}

// Print the lookup table
for (int i = 0; i < 256; ++i)
{
    Console.WriteLine($"{i:000} => {lookupTable[i]:0.000}");
}
Console.ReadKey(true);

The float values in this lookup table can safely by multiplied by 255 and rounded down to yield the exact byte value of the color. The code basically iterates through all 1001 possible floats and saves the first float that can be safely rounded down into the lookup table.