Skip to content

Commit 2ee0183

Browse files
committed
🔨 Step 2 of improving the RGB to HSV conversion.
Still incorrect but implements the simplifications of the algorithm to reduce the number of operations required.
1 parent 90c5c74 commit 2ee0183

1 file changed

Lines changed: 64 additions & 54 deletions

File tree

src/Exo/Core/Exo.Core/ColorFormats/HsvColor.cs

Lines changed: 64 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -69,88 +69,120 @@ public RgbColor ToRgb()
6969

7070
public static HsvColor FromRgb(RgbColor rgb)
7171
{
72+
// Max is always directly brightness, other values are scaled relative to brightness and need to be rescaled to 255 in a way that round-trips.
73+
// Min is inverse saturation. (i.e. if 0, S = 255 and if B, S = 0)
74+
// Med is the hue offset. Positive or negative, relative to which components are Max and Med.
7275
byte min;
76+
byte med;
7377
byte max;
74-
uint h;
78+
bool isPositiveOffset = false;
7579
uint baseHue;
76-
byte componentA;
77-
byte componentB;
7880
if (rgb.R >= rgb.G)
7981
{
8082
if (rgb.R >= rgb.B)
8183
{
8284
max = rgb.R;
83-
componentA = rgb.G;
84-
componentB = rgb.B;
85-
if (rgb.G >= rgb.B)
85+
if (isPositiveOffset = rgb.G >= rgb.B)
8686
{
8787
min = rgb.B;
88-
if (max == min)
89-
{
90-
h = 0;
91-
goto ReturnColor;
92-
}
88+
// Obvious special case when R = G = B, we have a grayscale.
89+
if (max == min) return new(0, 0, max);
90+
med = rgb.G;
9391
baseHue = 0;
9492
}
9593
else
9694
{
9795
min = rgb.G;
96+
med = rgb.B;
9897
baseHue = 1530;
9998
}
10099
goto ComputeHue;
101100
}
102101
else
103102
{
104103
min = rgb.G;
104+
med = rgb.R;
105+
isPositiveOffset = true;
105106
goto BaseHue1020;
106107
}
107108
}
108109
else if (rgb.G >= rgb.B)
109110
{
110111
max = rgb.G;
111112
baseHue = 510;
112-
componentA = rgb.B;
113-
componentB = rgb.R;
114-
min = rgb.B >= rgb.R ? rgb.R : rgb.B;
113+
if (isPositiveOffset = rgb.B >= rgb.R)
114+
{
115+
min = rgb.R;
116+
med = rgb.B;
117+
}
118+
else
119+
{
120+
min = rgb.B;
121+
med = rgb.R;
122+
}
115123
goto ComputeHue;
116124
}
117125
else
118126
{
119127
min = rgb.R;
128+
med = rgb.G;
120129
goto BaseHue1020;
121130
}
122131
BaseHue1020:;
123132
max = rgb.B;
124133
baseHue = 1020;
125-
componentA = rgb.R;
126-
componentB = rgb.G;
127-
goto ComputeHue;
128134
ComputeHue:;
129-
h = ComputeHue(baseHue, componentA - componentB, (uint)(max - min));
130-
ReturnColor:;
131-
return new((ushort)h, ComputeSaturation(min, max), max);
135+
// Special case when the hue is "pure", we need only a single division.
136+
if (min == med)
137+
{
138+
return new((ushort)baseHue, (byte)~ReversibleDivision(min, max), max);
139+
}
140+
// For now, to deal with the annoying integer division stuff, we'll deconstruct the RGB color one HSV component at a time.
141+
// First, we rescale all three components before computing the rest. (This means that for all intents and purposes max is now 255)
142+
// It does require 3 extra divisions, which is all but great, but at least it will make the computations perfect.
143+
// (At the very best one of those divisions should simply go away, because one component is the max)
144+
min = (byte)ReversibleDivision(min, max);
145+
med = (byte)ReversibleDivision(med, max);
146+
// Then, undo the effect from the saturation.
147+
med = ReverseSaturation(med, min);
148+
return new((ushort)(isPositiveOffset ? baseHue + med : baseHue - med), (byte)~min, max);
132149
}
133150

134-
private static uint ComputeHue(uint baseHue, int componentOffset, uint amplitude)
135-
=> componentOffset >= 0 ?
136-
baseHue + ReversibleDivision((uint)componentOffset, amplitude) :
137-
baseHue - ReversibleDivision((uint)-componentOffset, amplitude);
138-
139-
private static byte ComputeSaturation(byte minimumComponent, byte brightness)
151+
// TODO: Similar logic to the reversible division. Hopefully possible to make it suck less.
152+
private static byte ReverseSaturation(byte component, byte minimum)
140153
{
141-
// We are looking to find the S value so that C = B * ~S / 255
142-
// 255 * C = B * ~S
143-
// ~S = 255 * C / B
144-
// S = ~(255 * C / B)
145-
if (brightness == 0) return 0;
146-
return (byte)ReversibleDivision2(minimumComponent, brightness);
154+
// C = (c * S + 255 * ~S) / 255
155+
// 255 * C = c * S + 255 * ~S
156+
// 255 * (C - ~S) = c * S
157+
// c = 255 * (C - ~S) / S
158+
uint saturation = (byte)~minimum;
159+
uint result = 255 * (uint)(component - minimum) / saturation;
160+
uint complement = 255U * minimum;
161+
uint c = (result * saturation + complement) / 255;
162+
if (c == component) return (byte)result;
163+
if (c < component)
164+
{
165+
result++;
166+
if ((result * saturation + complement) / 255 == component) return (byte)result;
167+
result++;
168+
if ((result * saturation + complement) / 255 == component) return (byte)result;
169+
}
170+
else
171+
{
172+
result--;
173+
if ((result * saturation + complement) / 255 == component) return (byte)result;
174+
result--;
175+
if ((result * saturation + complement) / 255 == component) return (byte)result;
176+
}
177+
throw new InvalidOperationException();
147178
}
148179

149180
// TODO: Make this suck less.
150181
// It is probably possible to do better than this. I do hope that it is possible.
151182
// Anyway, it will do for now.
152183
private static uint ReversibleDivision(uint a, uint b)
153184
{
185+
if (b == 255) return a;
154186
uint result = 255 * a / b;
155187
uint aa = b * result / 255;
156188
if (aa == a) return result;
@@ -171,28 +203,6 @@ private static uint ReversibleDivision(uint a, uint b)
171203
throw new InvalidOperationException();
172204
}
173205

174-
private static uint ReversibleDivision2(uint a, uint b)
175-
{
176-
uint result = 255 * (b - a) / b;
177-
uint aa = b * (byte)~result / 255;
178-
if (aa == a) return result;
179-
if (aa < a)
180-
{
181-
result--;
182-
if (b * (byte)~result / 255 == a) return result;
183-
result--;
184-
if (b * (byte)~result / 255 == a) return result;
185-
}
186-
else
187-
{
188-
result++;
189-
if (b * (byte)~result / 255 == a) return result;
190-
result++;
191-
if (b * (byte)~result / 255 == a) return result;
192-
}
193-
throw new InvalidOperationException();
194-
}
195-
196206
public static ushort GetScaledHue(float hue)
197207
{
198208
if (hue >= 360) hue = hue % 360;

0 commit comments

Comments
 (0)