@@ -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