com.wallstop-studios.unity-helpers 2.0.0-rc41 → 2.0.0-rc43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,5 @@
1
1
  namespace UnityHelpers.Core.Extension
2
2
  {
3
- using System;
4
3
  using System.Collections.Generic;
5
4
  using System.ComponentModel;
6
5
  using System.Linq;
@@ -19,6 +18,8 @@
19
18
  // https://sharpsnippets.wordpress.com/2014/03/11/c-extension-complementary-color/
20
19
  public static class ColorExtensions
21
20
  {
21
+ private static readonly Dictionary<Vector3Int, int> ColorBucketCache = new();
22
+
22
23
  public static Color GetAverageColor(
23
24
  this Sprite sprite,
24
25
  ColorAveragingMethod method = ColorAveragingMethod.LAB,
@@ -36,9 +37,9 @@
36
37
  {
37
38
  return GetAverageColor(
38
39
  sprites
39
- .Where(Objects.NotNull)
40
+ .Where(value => value != null)
40
41
  .Select(sprite => sprite.texture)
41
- .Where(Objects.NotNull)
42
+ .Where(value => value != null)
42
43
  .SelectMany(texture =>
43
44
  {
44
45
  texture.MakeReadable();
@@ -56,48 +57,105 @@
56
57
  float alphaCutoff = 0.01f
57
58
  )
58
59
  {
59
- switch (method)
60
+ return method switch
60
61
  {
61
- case ColorAveragingMethod.LAB:
62
- {
63
- return AverageInLABSpace(pixels, alphaCutoff);
64
- }
65
- case ColorAveragingMethod.HSV:
62
+ ColorAveragingMethod.LAB => AverageInLABSpace(pixels, alphaCutoff),
63
+ ColorAveragingMethod.HSV => AverageInHSVSpace(pixels, alphaCutoff),
64
+ ColorAveragingMethod.Weighted => WeightedRGBAverage(pixels, alphaCutoff),
65
+ ColorAveragingMethod.Dominant => GetDominantColor(pixels, alphaCutoff),
66
+ _ => throw new InvalidEnumArgumentException(
67
+ nameof(method),
68
+ (int)method,
69
+ typeof(ColorAveragingMethod)
70
+ ),
71
+ };
72
+ }
73
+
74
+ // CIE L*a*b* space averaging - most perceptually accurate
75
+ private static Color AverageInLABSpace(IEnumerable<Color> pixels, float alphaCutoff)
76
+ {
77
+ double l = 0;
78
+ double a = 0;
79
+ double b = 0;
80
+ int count = 0;
81
+ switch (pixels)
82
+ {
83
+ case List<Color> colorList:
66
84
  {
67
- return AverageInHSVSpace(pixels, alphaCutoff);
85
+ foreach (Color pixel in colorList)
86
+ {
87
+ if (pixel.a <= alphaCutoff)
88
+ {
89
+ continue;
90
+ }
91
+
92
+ LABColor lab = RGBToLAB(pixel);
93
+ l += lab.l;
94
+ a += lab.a;
95
+ b += lab.b;
96
+ ++count;
97
+ }
98
+
99
+ break;
68
100
  }
69
- case ColorAveragingMethod.Weighted:
101
+ case Color[] colorArray:
70
102
  {
71
- return WeightedRGBAverage(pixels, alphaCutoff);
103
+ foreach (Color pixel in colorArray)
104
+ {
105
+ if (pixel.a <= alphaCutoff)
106
+ {
107
+ continue;
108
+ }
109
+
110
+ LABColor lab = RGBToLAB(pixel);
111
+ l += lab.l;
112
+ a += lab.a;
113
+ b += lab.b;
114
+ ++count;
115
+ }
116
+
117
+ break;
72
118
  }
73
- case ColorAveragingMethod.Dominant:
119
+ case HashSet<Color> colorSet:
74
120
  {
75
- return GetDominantColor(pixels, alphaCutoff);
121
+ foreach (Color pixel in colorSet)
122
+ {
123
+ if (pixel.a <= alphaCutoff)
124
+ {
125
+ continue;
126
+ }
127
+
128
+ LABColor lab = RGBToLAB(pixel);
129
+ l += lab.l;
130
+ a += lab.a;
131
+ b += lab.b;
132
+ ++count;
133
+ }
134
+
135
+ break;
76
136
  }
77
137
  default:
78
138
  {
79
- throw new InvalidEnumArgumentException(
80
- nameof(method),
81
- (int)method,
82
- typeof(ColorAveragingMethod)
83
- );
139
+ foreach (Color pixel in pixels)
140
+ {
141
+ if (pixel.a <= alphaCutoff)
142
+ {
143
+ continue;
144
+ }
145
+
146
+ LABColor lab = RGBToLAB(pixel);
147
+ l += lab.l;
148
+ a += lab.a;
149
+ b += lab.b;
150
+ ++count;
151
+ }
152
+
153
+ break;
84
154
  }
85
155
  }
86
- }
87
-
88
- // CIE L*a*b* space averaging - most perceptually accurate
89
- private static Color AverageInLABSpace(IEnumerable<Color> pixels, float alphaCutoff)
90
- {
91
- List<LABColor> labValues = pixels
92
- .Where(pixel => pixel.a > alphaCutoff)
93
- .Select(RGBToLAB)
94
- .ToList();
95
-
96
- double avgL = labValues.Average(lab => lab.l);
97
- double avgA = labValues.Average(lab => lab.a);
98
- double avgB = labValues.Average(lab => lab.b);
99
156
 
100
- return LABToRGB(avgL, avgA, avgB);
157
+ count = Mathf.Max(count, 1);
158
+ return LABToRGB(l / count, a / count, b / count);
101
159
  }
102
160
 
103
161
  // HSV space averaging - good for preserving vibrant colors
@@ -108,31 +166,109 @@
108
166
  float avgV = 0f;
109
167
  int count = 0;
110
168
 
111
- foreach (Color pixel in pixels.Where(pixel => pixel.a > alphaCutoff))
169
+ switch (pixels)
112
170
  {
113
- Color.RGBToHSV(pixel, out float h, out float s, out float v);
171
+ case List<Color> pixelList:
172
+ {
173
+ foreach (Color pixel in pixelList)
174
+ {
175
+ if (pixel.a <= alphaCutoff)
176
+ {
177
+ continue;
178
+ }
114
179
 
115
- // Handle hue wrapping around 360 degrees
116
- float hRad = h * 2f * Mathf.PI;
117
- avgH += Mathf.Cos(hRad);
118
- avgH += Mathf.Sin(hRad);
180
+ Color.RGBToHSV(pixel, out float h, out float s, out float v);
119
181
 
120
- avgS += s;
121
- avgV += v;
122
- count++;
123
- }
182
+ // Handle hue wrapping around 360 degrees
183
+ float hRad = h * 2f * Mathf.PI;
184
+ avgH += Mathf.Cos(hRad);
185
+ avgH += Mathf.Sin(hRad);
124
186
 
187
+ avgS += s;
188
+ avgV += v;
189
+ ++count;
190
+ }
191
+
192
+ break;
193
+ }
194
+ case Color[] pixelArray:
195
+ {
196
+ foreach (Color pixel in pixelArray)
197
+ {
198
+ if (pixel.a <= alphaCutoff)
199
+ {
200
+ continue;
201
+ }
202
+
203
+ Color.RGBToHSV(pixel, out float h, out float s, out float v);
204
+
205
+ // Handle hue wrapping around 360 degrees
206
+ float hRad = h * 2f * Mathf.PI;
207
+ avgH += Mathf.Cos(hRad);
208
+ avgH += Mathf.Sin(hRad);
209
+
210
+ avgS += s;
211
+ avgV += v;
212
+ ++count;
213
+ }
214
+
215
+ break;
216
+ }
217
+ case HashSet<Color> pixelSet:
218
+ {
219
+ foreach (Color pixel in pixelSet)
220
+ {
221
+ if (pixel.a <= alphaCutoff)
222
+ {
223
+ continue;
224
+ }
225
+
226
+ Color.RGBToHSV(pixel, out float h, out float s, out float v);
227
+
228
+ // Handle hue wrapping around 360 degrees
229
+ float hRad = h * 2f * Mathf.PI;
230
+ avgH += Mathf.Cos(hRad);
231
+ avgH += Mathf.Sin(hRad);
232
+
233
+ avgS += s;
234
+ avgV += v;
235
+ ++count;
236
+ }
237
+
238
+ break;
239
+ }
240
+ default:
241
+ {
242
+ foreach (Color pixel in pixels)
243
+ {
244
+ if (pixel.a <= alphaCutoff)
245
+ {
246
+ continue;
247
+ }
248
+
249
+ Color.RGBToHSV(pixel, out float h, out float s, out float v);
250
+
251
+ // Handle hue wrapping around 360 degrees
252
+ float hRad = h * 2f * Mathf.PI;
253
+ avgH += Mathf.Cos(hRad);
254
+ avgH += Mathf.Sin(hRad);
255
+
256
+ avgS += s;
257
+ avgV += v;
258
+ ++count;
259
+ }
260
+
261
+ break;
262
+ }
263
+ }
264
+ count = Mathf.Max(count, 1);
125
265
  avgH = Mathf.Atan2(avgH / count, avgH / count) / (2f * Mathf.PI);
266
+
126
267
  if (avgH < 0)
127
268
  {
128
269
  avgH += 1f;
129
270
  }
130
271
 
131
- if (count <= 0)
132
- {
133
- count = 1;
134
- }
135
-
136
272
  avgS /= count;
137
273
  avgV /= count;
138
274
 
@@ -153,14 +289,81 @@
153
289
  b = 0f,
154
290
  a = 0f;
155
291
 
156
- foreach (Color pixel in pixels.Where(pixel => pixel.a > alphaCutoff))
292
+ switch (pixels)
157
293
  {
158
- float weight = pixel.r * rWeight + pixel.g * gWeight + pixel.b * bWeight;
159
- r += pixel.r * weight;
160
- g += pixel.g * weight;
161
- b += pixel.b * weight;
162
- a += pixel.a * weight;
163
- totalWeight += weight;
294
+ case List<Color> colorList:
295
+ {
296
+ foreach (Color pixel in colorList)
297
+ {
298
+ if (pixel.a <= alphaCutoff)
299
+ {
300
+ continue;
301
+ }
302
+ float weight = pixel.r * rWeight + pixel.g * gWeight + pixel.b * bWeight;
303
+ r += pixel.r * weight;
304
+ g += pixel.g * weight;
305
+ b += pixel.b * weight;
306
+ a += pixel.a * weight;
307
+ totalWeight += weight;
308
+ }
309
+
310
+ break;
311
+ }
312
+ case Color[] colorArray:
313
+ {
314
+ foreach (Color pixel in colorArray)
315
+ {
316
+ if (pixel.a <= alphaCutoff)
317
+ {
318
+ continue;
319
+ }
320
+ float weight = pixel.r * rWeight + pixel.g * gWeight + pixel.b * bWeight;
321
+ r += pixel.r * weight;
322
+ g += pixel.g * weight;
323
+ b += pixel.b * weight;
324
+ a += pixel.a * weight;
325
+ totalWeight += weight;
326
+ }
327
+
328
+ break;
329
+ }
330
+ case HashSet<Color> colorSet:
331
+ {
332
+ foreach (Color pixel in colorSet)
333
+ {
334
+ if (pixel.a <= alphaCutoff)
335
+ {
336
+ continue;
337
+ }
338
+ float weight = pixel.r * rWeight + pixel.g * gWeight + pixel.b * bWeight;
339
+ r += pixel.r * weight;
340
+ g += pixel.g * weight;
341
+ b += pixel.b * weight;
342
+ a += pixel.a * weight;
343
+ totalWeight += weight;
344
+ }
345
+
346
+ break;
347
+ }
348
+ default:
349
+ {
350
+ foreach (Color pixel in pixels)
351
+ {
352
+ if (pixel.a <= alphaCutoff)
353
+ {
354
+ continue;
355
+ }
356
+
357
+ float weight = pixel.r * rWeight + pixel.g * gWeight + pixel.b * bWeight;
358
+ r += pixel.r * weight;
359
+ g += pixel.g * weight;
360
+ b += pixel.b * weight;
361
+ a += pixel.a * weight;
362
+ totalWeight += weight;
363
+ }
364
+
365
+ break;
366
+ }
164
367
  }
165
368
 
166
369
  if (totalWeight > 0f)
@@ -177,25 +380,112 @@
177
380
  // Find dominant color using simple clustering
178
381
  private static Color GetDominantColor(IEnumerable<Color> pixels, float alphaCutoff)
179
382
  {
180
- Dictionary<Vector3Int, int> colorBuckets = new();
383
+ ColorBucketCache.Clear();
181
384
  const int bucketSize = 32; // Adjust for different precision
182
385
 
183
- foreach (Color pixel in pixels.Where(pixel => pixel.a > alphaCutoff))
386
+ switch (pixels)
184
387
  {
185
- Vector3Int bucket = new(
186
- Mathf.RoundToInt(pixel.r * 255 / bucketSize),
187
- Mathf.RoundToInt(pixel.g * 255 / bucketSize),
188
- Mathf.RoundToInt(pixel.b * 255 / bucketSize)
189
- );
190
-
191
- colorBuckets.TryAdd(bucket, 0);
192
- colorBuckets[bucket]++;
388
+ case List<Color> colorList:
389
+ {
390
+ foreach (Color pixel in colorList)
391
+ {
392
+ if (pixel.a <= alphaCutoff)
393
+ {
394
+ continue;
395
+ }
396
+
397
+ Vector3Int bucket = new(
398
+ Mathf.RoundToInt(pixel.r * 255 / bucketSize),
399
+ Mathf.RoundToInt(pixel.g * 255 / bucketSize),
400
+ Mathf.RoundToInt(pixel.b * 255 / bucketSize)
401
+ );
402
+
403
+ ColorBucketCache.AddOrUpdate(bucket, _ => 0, (_, value) => value + 1);
404
+ }
405
+
406
+ break;
407
+ }
408
+ case Color[] colorArray:
409
+ {
410
+ foreach (Color pixel in colorArray)
411
+ {
412
+ if (pixel.a <= alphaCutoff)
413
+ {
414
+ continue;
415
+ }
416
+
417
+ Vector3Int bucket = new(
418
+ Mathf.RoundToInt(pixel.r * 255 / bucketSize),
419
+ Mathf.RoundToInt(pixel.g * 255 / bucketSize),
420
+ Mathf.RoundToInt(pixel.b * 255 / bucketSize)
421
+ );
422
+
423
+ ColorBucketCache.AddOrUpdate(bucket, _ => 0, (_, value) => value + 1);
424
+ }
425
+
426
+ break;
427
+ }
428
+ case HashSet<Color> colorSet:
429
+ {
430
+ foreach (Color pixel in colorSet)
431
+ {
432
+ if (pixel.a <= alphaCutoff)
433
+ {
434
+ continue;
435
+ }
436
+
437
+ Vector3Int bucket = new(
438
+ Mathf.RoundToInt(pixel.r * 255 / bucketSize),
439
+ Mathf.RoundToInt(pixel.g * 255 / bucketSize),
440
+ Mathf.RoundToInt(pixel.b * 255 / bucketSize)
441
+ );
442
+
443
+ ColorBucketCache.AddOrUpdate(bucket, _ => 0, (_, value) => value + 1);
444
+ }
445
+
446
+ break;
447
+ }
448
+ default:
449
+ {
450
+ foreach (Color pixel in pixels)
451
+ {
452
+ if (pixel.a <= alphaCutoff)
453
+ {
454
+ continue;
455
+ }
456
+
457
+ Vector3Int bucket = new(
458
+ Mathf.RoundToInt(pixel.r * 255 / bucketSize),
459
+ Mathf.RoundToInt(pixel.g * 255 / bucketSize),
460
+ Mathf.RoundToInt(pixel.b * 255 / bucketSize)
461
+ );
462
+
463
+ ColorBucketCache.AddOrUpdate(bucket, _ => 0, (_, value) => value + 1);
464
+ }
465
+
466
+ break;
467
+ }
193
468
  }
194
469
 
195
- Vector3Int dominantBucket = colorBuckets
196
- .OrderByDescending(kvp => kvp.Value)
197
- .First()
198
- .Key;
470
+ KeyValuePair<Vector3Int, int>? largest = null;
471
+ if (0 < ColorBucketCache.Count)
472
+ {
473
+ foreach (KeyValuePair<Vector3Int, int> bucketEntry in ColorBucketCache)
474
+ {
475
+ largest ??= bucketEntry;
476
+ if (largest.Value.Value < bucketEntry.Value)
477
+ {
478
+ largest = bucketEntry;
479
+ }
480
+ }
481
+ }
482
+
483
+ if (largest == null)
484
+ {
485
+ return default;
486
+ }
487
+
488
+ Vector3Int dominantBucket = largest.Value.Key;
199
489
  return new Color(
200
490
  dominantBucket.x * bucketSize / 255f,
201
491
  dominantBucket.y * bucketSize / 255f,
@@ -276,15 +566,21 @@
276
566
  )
277
567
  {
278
568
  Color inputColor = source;
279
- //if RGB values are close to each other by a diff less than 10%, then if RGB values are lighter side, decrease the blue by 50% (eventually it will increase in conversion below), if RBB values are on darker side, decrease yellow by about 50% (it will increase in conversion)
569
+ /*
570
+ If RGB values are close to each other by a diff less than 10%, then if RGB values are lighter side,
571
+ decrease the blue by 50% (eventually it will increase in conversion below), if RBB values are on the
572
+ darker side, decrease yellow by about 50% (it will increase in conversion)
573
+ */
280
574
  float avgColorValue = (source.r + source.g + source.b) / 3;
281
- float rDiff = Math.Abs(source.r - avgColorValue);
282
- float gDiff = Math.Abs(source.g - avgColorValue);
283
- float bDiff = Math.Abs(source.b - avgColorValue);
575
+ float rDiff = Mathf.Abs(source.r - avgColorValue);
576
+ float gDiff = Mathf.Abs(source.g - avgColorValue);
577
+ float bDiff = Mathf.Abs(source.b - avgColorValue);
284
578
  const float greyDelta = 20 / 255f;
285
- if (rDiff < greyDelta && gDiff < greyDelta && bDiff < greyDelta) //The color is a shade of gray
579
+ //The color is a shade of gray
580
+ if (rDiff < greyDelta && gDiff < greyDelta && bDiff < greyDelta)
286
581
  {
287
- if (avgColorValue < 123 / 255f) //color is dark
582
+ // Color is dark
583
+ if (avgColorValue < 123 / 255f)
288
584
  {
289
585
  inputColor.b = 220 / 255f;
290
586
  inputColor.g = 230 / 255f;
@@ -302,18 +598,18 @@
302
598
  {
303
599
  if (variance != 0)
304
600
  {
305
- variance = Math.Abs(variance);
601
+ variance = Mathf.Abs(variance);
306
602
 
307
- float minR = Clamp(inputColor.r - variance);
308
- float maxR = Clamp(inputColor.r + variance);
603
+ float minR = Mathf.Clamp01(inputColor.r - variance);
604
+ float maxR = Mathf.Clamp01(inputColor.r + variance);
309
605
  inputColor.r = random.NextFloat(minR, maxR);
310
606
 
311
- float minG = Clamp(inputColor.g - variance);
312
- float maxG = Clamp(inputColor.g + variance);
607
+ float minG = Mathf.Clamp01(inputColor.g - variance);
608
+ float maxG = Mathf.Clamp01(inputColor.g + variance);
313
609
  inputColor.g = random.NextFloat(minG, maxG);
314
610
 
315
- float minB = Clamp(inputColor.b - variance);
316
- float maxB = Clamp(inputColor.b + variance);
611
+ float minB = Mathf.Clamp01(inputColor.b - variance);
612
+ float maxB = Mathf.Clamp01(inputColor.b + variance);
317
613
  inputColor.b = random.NextFloat(minB, maxB);
318
614
  }
319
615
  else
@@ -329,10 +625,5 @@
329
625
  Color result = Color.HSVToRGB(h, s, v);
330
626
  return result;
331
627
  }
332
-
333
- private static float Clamp(float value)
334
- {
335
- return Math.Clamp(value, 0, 1);
336
- }
337
628
  }
338
629
  }