I am currently trying to modify color picker control. Everything seems to be working as expected. However I would like to have possibility to set "selectedColor" on initialization. So that flow would be as follows:
Currently picker is taking in account only coordinates of pointer X and Y. This means if I will provide previously selected color for Color picker control it will not be able to place pointer in a right place, because it is waiting for X, Y coordinates and not color. I have got a work-around where I save all needed parameters into string (Color Hex code, as well as X and Y coordinates). It is working, however this is adding additional complexity for combining strings and then parsing them inside ViewModels.
I have been getting familiar with possibility to read pixels, searching for needed color and getting it's coordinates. Here are some problems:
if (this.PickedColor.ToSKColor() == pixelColor
&& this.PickedColor.ToSKColor() != Color.FromHex("#00000000").ToSKColor()
&& this.PickedColor.ToSKColor() != Color.FromHex("#FFFFFFFF").ToSKColor())
{
//this.SelectedPoint = new Point(x, y);
Debug.WriteLine(String.Format("Color: {0} | Coordinate: {1} {2}", pixelColor, x, y));
}
Here is OnPaintSurface method of Color Picker control (you can see at the bottom method this.GetPixelCoordinates(bitmap); that is commented out):
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
SKImageInfo skImageInfo = e.Info;
SKSurface skSurface = e.Surface;
this.SKCanvas = skSurface.Canvas;
int skCanvasWidth = skImageInfo.Width;
int skCanvasHeight = skImageInfo.Height;
this.SKCanvas.Clear();
// Draw gradient rainbow Color spectrum
using (SKPaint paint = new SKPaint())
{
paint.IsAntialias = true;
System.Collections.Generic.List<SKColor> colors = new System.Collections.Generic.List<SKColor>();
this.ColorList.ForEach((color) => { colors.Add(Color.FromHex(color).ToSKColor()); });
// create the gradient shader between Colors
using (SKShader shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
this.ColorListDirection == ColorListDirection.Horizontal ?
new SKPoint(skCanvasWidth, 0) : new SKPoint(0, skCanvasHeight),
colors.ToArray(),
null,
SKShaderTileMode.Clamp))
{
paint.Shader = shader;
this.SKCanvas.DrawPaint(paint);
}
}
// Draw darker gradient spectrum
using (SKPaint paint = new SKPaint())
{
paint.IsAntialias = true;
// Initiate the darkened primary color list
SKColor[] colors = GetGradientOrder();
// create the gradient shader
using (SKShader shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
this.ColorListDirection == ColorListDirection.Horizontal ?
new SKPoint(0, skCanvasHeight) : new SKPoint(skCanvasWidth, 0),
colors,
null,
SKShaderTileMode.Clamp))
{
paint.Shader = shader;
this.SKCanvas.DrawPaint(paint);
}
}
// Picking the Pixel Color values on the Touch Point
// Represent the color of the current Touch point
SKColor touchPointColor;
// Efficient and fast
// https://forums.xamarin.com/discussion/92899/read-a-pixel-info-from-a-canvas
// create the 1x1 bitmap (auto allocates the pixel buffer)
using (SKBitmap bitmap = new SKBitmap(skImageInfo))
{
// get the pixel buffer for the bitmap
IntPtr dstpixels = bitmap.GetPixels();
// read the surface into the bitmap
skSurface.ReadPixels(skImageInfo,
dstpixels,
skImageInfo.RowBytes,
(int)this.SelectedPoint.X,
(int)this.SelectedPoint.Y);
// access the color
touchPointColor = bitmap.GetPixel(0, 0);
//this.GetPixelCoordinates(bitmap);
//bitmap.SetPixel(50, 50, this.PickedColor.ToSKColor());
}
Here is GetPixelCoordinates method:
private void GetPixelCoordinates(SKBitmap bitmap)
{
if (bitmap == null)
{
return;
}
for (int x = 0; x < bitmap.Width; x++)
{
for (int y = 0; y < bitmap.Height; y++)
{
SKColor pixelColor = bitmap.GetPixel(x, y);
if (this.PickedColor.ToSKColor() == pixelColor
&& this.PickedColor.ToSKColor() != Color.FromHex("#00000000").ToSKColor()
&& this.PickedColor.ToSKColor() != Color.FromHex("#FFFFFFFF").ToSKColor())
{
//this.SelectedPoint = new Point(x, y);
Debug.WriteLine(String.Format("Color: {0} | Coordinate: {1} {2}", pixelColor, x, y));
}
}
}
}
Here is PickedColor property:
public static readonly BindableProperty PickedColorProperty
= BindableProperty.Create(
propertyName: nameof(PickedColor),
returnType: typeof(Color),
declaringType: typeof(ColorPicker),
defaultValue: Color.Green,
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: OnColorChanged);
private static void OnColorChanged(BindableObject bindable, object oldValue, object newValue)
{
ColorPicker control = (ColorPicker)bindable;
control.PickedColor = (Color)newValue;
}
/// <summary>
/// Set the Color Spectrum Gradient Style
/// </summary>
public GradientColorStyle GradientColorStyle
{
get { return (GradientColorStyle)GetValue(GradientColorStyleProperty); }
set { SetValue(GradientColorStyleProperty, value); }
}
public static readonly BindableProperty ColorListProperty
= BindableProperty.Create(
propertyName: nameof(ColorList),
returnType: typeof(string[]),
declaringType: typeof(ColorPicker),
defaultValue: new string[]
{
new Color(255, 0, 0).ToHex(), // Red
new Color(255, 255, 0).ToHex(), // Yellow
new Color(0, 255, 0).ToHex(), // Green (Lime)
new Color(0, 255, 255).ToHex(), // Aqua
new Color(0, 0, 255).ToHex(), // Blue
new Color(255, 0, 255).ToHex(), // Fuchsia
new Color(255, 0, 0).ToHex(), // Red
},
defaultBindingMode: BindingMode.OneTime, null);
My question is: Is generating string with parameters the only way to go (Color Hex code, as well as X and Y coordinates)? Is there some possibility to place pointer on control initialization by provided color in some efficient way without constant loop iteration and freeze of UI?
Color palette:
StackOverflow is designed for each post to ask and answer one coding question.
(To ask a second question, please make a new post. Include in that post the essential details needed to understand that question. Link back to this question, so you don't have to repeat the information that gives additional background/context.)
Your primary question can be stated as:
Given: A color palette [see picture] generated by [see
OnPaintSurface
code, starting at// Draw gradient rainbow Color spectrum
. How calculate (x,y) coordinates that correspond to a given color?
First, an observation. That 2D palette gives 2 of 3 color axes. You'll need a separate "saturation" slider, to allow picking of any color.
The palette you show is an approximation to an "HSV" color model.
In wiki HSL-HSV models, click on diagram at right. Your palette looks like the rectangle labeled S(HSV) = 1.
Hue + Saturation + Value.
Your ColorList should have fully Saturated colors at max Value.
Going down the screen, the palette reduces Value to near zero.
This is the beginning of an answer to that.
What is needed, is a mathematical formula that corresponds to what is drawn.
Lets look at how that rectangular image was generated.
Rename the color lists so easier to work with.
Store as fields, so can use them later. Use the original Colors
from which the SkColors
were generated, for easier manipulation.
private List<Color> saturatedColors;
private List<Color> darkenedColors;
private int nColors => saturatedColors.Count;
private int maxX, maxY; // From your UI rectangle.
The top row (y=0) has saturatedColors
, evenly spaced across x.
The bottom row (y=maxY) has darkenedColors
, evenly spaced across x.
The pixel colors are linearly interpolated from top row to bottom row.
Goal is to find pixel closest to a given color, "Color goalColor
".
Consider each tall, thin rectangle whose corners are two topColors and the corresponding two bottomColors. Goal is to find which rectangle contains goalColor, then find the pixel within that rectangle that is closest to goalColor.
The trickiest part is "comparing" colors, to decide when a color is "between" two colors. This is hard to do in RGB; convert colors to HSV to match the palette you are using. See Greg's answer - ColorToHSV.
Its easier if you make an HSV class:
using System;
using System.Collections.Generic;
using System.Linq;
// OR could use System.Drawing.Color.
using Color = Xamarin.Forms.Color;
...
public class HSV
{
#region --- static ---
public static HSV FromColor(Color color)
{
ColorToHSV(color, out double hue, out double saturation, out double value);
return new HSV(hue, saturation, value);
}
public static List<HSV> FromColors(IEnumerable<Color> colors)
{
return colors.Select(color => FromColor(color)).ToList();
}
const double Epsilon = 0.000001;
// returns Tuple<int colorIndex, double wgtB>.
public static Tuple<int, double> FindHueInColors(IList<HSV> colors, double goalHue)
{
int colorIndex;
double wgtB = 0;
// "- 1": because each iteration needs colors[colorIndex+1].
for (colorIndex = 0; colorIndex < colors.Count - 1; colorIndex++)
{
wgtB = colors[colorIndex].WgtFromHue(colors[colorIndex + 1], goalHue);
// Epsilon compensates for possible round-off error in WgtFromHue.
// To ensure the color is considered within one of the ranges.
if (wgtB >= 0 - Epsilon && wgtB < 1)
break;
}
return new Tuple<int, double>(colorIndex, wgtB);
}
// From https://stackoverflow.com/a/1626175/199364.
public static void ColorToHSV(Color color, out double hue, out double saturation, out double value)
{
int max = Math.Max(color.R, Math.Max(color.G, color.B));
int min = Math.Min(color.R, Math.Min(color.G, color.B));
hue = color.GetHue();
saturation = (max == 0) ? 0 : 1d - (1d * min / max);
value = max / 255d;
}
// From https://stackoverflow.com/a/1626175/199364.
public static Color ColorFromHSV(double hue, double saturation, double value)
{
int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6;
double f = hue / 60 - Math.Floor(hue / 60);
value = value * 255;
int v = Convert.ToInt32(value);
int p = Convert.ToInt32(value * (1 - saturation));
int q = Convert.ToInt32(value * (1 - f * saturation));
int t = Convert.ToInt32(value * (1 - (1 - f) * saturation));
if (hi == 0)
return Color.FromArgb(255, v, t, p);
else if (hi == 1)
return Color.FromArgb(255, q, v, p);
else if (hi == 2)
return Color.FromArgb(255, p, v, t);
else if (hi == 3)
return Color.FromArgb(255, p, q, v);
else if (hi == 4)
return Color.FromArgb(255, t, p, v);
else
return Color.FromArgb(255, v, p, q);
}
#endregion
public double H { get; set; }
public double S { get; set; }
public double V { get; set; }
// c'tors
public HSV()
{
}
public HSV(double h, double s, double v)
{
H = h;
S = s;
V = v;
}
public Color ToColor()
{
return ColorFromHSV(H, S, V);
}
public HSV Lerp(HSV b, double wgtB)
{
return new HSV(
MathExt.Lerp(H, b.H, wgtB),
MathExt.Lerp(S, b.S, wgtB),
MathExt.Lerp(V, b.V, wgtB));
}
// Returns "wgtB", such that goalHue = Lerp(H, b.H, wgtB).
// If a and b have same S and V, then this is a measure of
// how far to move along segment (a, b), to reach goalHue.
public double WgtFromHue(HSV b, double goalHue)
{
return MathExt.Lerp(H, b.H, goalHue);
}
// Returns "wgtB", such that goalValue = Lerp(V, b.V, wgtB).
public double WgtFromValue(HSV b, double goalValue)
{
return MathExt.Lerp(V, b.V, goalValue);
}
}
public static class MathExt
{
public static double Lerp(double a, double b, double wgtB)
{
return a + (wgtB * (b - a));
}
// Converse of Lerp:
// returns "wgtB", such that
// result == lerp(a, b, wgtB)
public static double WgtFromResult(double a, double b, double result)
{
double denominator = b - a;
if (Math.Abs(denominator) < 0.00000001)
{
if (Math.Abs(result - a) < 0.00000001)
// Any value is "valid"; return the average.
return 0.5;
// Unsolvable - no weight can return this result.
return double.NaN;
}
double wgtB = (result - a) / denominator;
return wgtB;
}
}
Usage:
public static class Tests {
public static void TestFindHueInColors(List<Color> saturatedColors, Color goalColor)
{
List<HSV> hsvColors = HSV.FromColors(saturatedColors);
HSV goalHSV = HSV.FromColor(goalColor);
var hueAt = HSV.FindHueInColors(hsvColors, goalHSV.H);
int colorIndex = hueAt.Item1;
double wgtB = hueAt.Item2;
// ...
}
}
This is the essence of the approach. From colorIndex
, nColors
, wgtB
, and maxX
, it is possible to calculate x
. I recommend writing several test cases, to figure out how to do so.
Calculating y
is much simpler. Should be possible using goalHSV.V
and maxY
.
As you can see, this is not trivial to code.
The most important points:
goalHSV
is between.If the above math is too intense, then draw a box with just one of those rectangles. That is, make the gradient with only TWO colors in the top list. Experiment with trying to locate a color pixel within that rectangle.
IMPORTANT: There might not be a pixel that is EXACTLY the color you are starting with. Find which pixel is closest to that color.
If you aren't sure you have the "best" pixel, then read a few nearby pixels, decide which is "closest". That is, which has the smallest var error = (r2-r1)*(r2-r1) + (g2-g1)*(g2-g1) + (b2-b1)*(b2-b1);
.