using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace Matrix_App { partial class ColorWheel { /// /// Erforderliche Designervariable. /// private System.ComponentModel.IContainer components = null; private static readonly int WHEEL_PIXEL_RESOLUTION = 256; private static readonly int WHEEL_BORDER_SUBDIVISIONS = 6; private static readonly float RADIUS = WHEEL_PIXEL_RESOLUTION / (float) WHEEL_BORDER_SUBDIVISIONS * 4 * 0.5f; private Bitmap wheel; /// transformed triangle vertices in screen space private PointF[] transformed = new PointF[3]; /// triangle verticies in object "local" space private PointF[] vertices = { new PointF(RADIUS * fcos(0), RADIUS * fsin(0) ), new PointF(RADIUS * fcos(120), RADIUS * fsin(120)), new PointF(RADIUS * fcos(240), RADIUS * fsin(240)) }; /// gradient between the fully saturated and brightned tone color and white (desaturated) private LinearGradientBrush whiteGradient; /// gradient between black and transparent, darkens the corner in which value = 0 private LinearGradientBrush colorGradient; /// gradient between transparent and gray private LinearGradientBrush grayGradient; // stored HSV values private float angle = 180; private float saturation = 1f; private float value = 1f; // current cursor position private Point cursor = new Point(); private Point origCursor = new Point(); public EventHandler handler; private int sRGB = 0; private Boolean entered = false; private Boolean release = true; /// /// Slider types: /// /// private enum Slider { TRIANGLE, WHEEL } private Slider selection = Slider.TRIANGLE; private System.Drawing.Drawing2D.Matrix transform; private float ctheta; #region Math-utils /// convert degrees to radians /// by mapping [0, 360] to [0, 2pi] private static float ToRadians(float degree) { // about equal to pi / 180 const float PI_OVER_180 = 0.0174532925199f; return degree * PI_OVER_180; } /// convert radians to degrees /// by mapping [0, 2pi] to [0, 360] private static float ToDegree(float radians) { // about equal to 180 / pi const float HUNDERETEIGTHY_OVER_PI = 57.2957795131f; return radians * HUNDERETEIGTHY_OVER_PI; } /// Calculates the cosine of degrees as a float private static float fcos(float degree) { return MathF.Cos(ToRadians(degree)); } /// Calculates the sine of degrees as a float private static float fsin(float degree) { return MathF.Sin(ToRadians(degree)); } /// /// Dotproduct of a and b /// /// /// /// private float dot(in PointF a, in PointF b) { return a.X * b.X + a.Y + b.Y; } /// Calculates the hypotenuse of a and b /// as sqrt(a² + b²) private static float hypot(float a, float b) { return MathF.Sqrt(a * a + b * b); } /// Divides the floor of the sum of a and b by 2 private static int floorMiddle(float a, float b) { return (int)(a + b) >> 1; } /// /// Computes the traingle area of the pointes a,b,(x | y) /// /// /// /// /// /// private float triangleArea(PointF a, PointF b, float x, float y) { return MathF.Abs((a.X - x) * (b.Y - y) - (b.X - x) * (a.Y - y)); } /// /// Tests if the point (x | y) lies inside of the transformed triangle /// /// /// /// private bool triangleIntersect(double x, double y) { int originalArea = (int)triangleArea(transformed[1], transformed[2], transformed[0].X, transformed[0].Y); int area1 = (int)triangleArea(transformed[0], transformed[1], (float)x, (float)y); int area2 = (int)triangleArea(transformed[1], transformed[2], (float)x, (float)y); int area3 = (int)triangleArea(transformed[2], transformed[0], (float)x, (float)y); return Math.Abs(area1 + area2 + area3 - originalArea) < 3; } /// /// Convert cartesian coordinates (point p) to barycentric coordinates (u,v,w) relative to triangle with verticies a,b,c /// /// /// /// /// /// /// /// private void cartesianToBarycnetric(in PointF p, in PointF a, in PointF b, in PointF c, out float u, out float v, out float w) { var v0 = subtract(b, a); var v1 = subtract(c, a); var v2 = subtract(p, a); float denom = v0.X * v1.Y - v1.X * v0.Y; v = (v2.X * v1.Y - v1.X * v2.Y) / denom; w = (v0.X * v2.Y - v2.X * v0.Y) / denom; u = 1.0f - v - w; } /// /// Clamp barycentric coordinates of toClamp to the triangle specified by a,b,c, so that for every barycenttric coordinates is: /// u + v + w == 0 and none of u,v,w is greater than 1 or less than zero. /// /// /// /// /// private void clampToBarycentricCoordinates(ref PointF toClamp, in PointF a, in PointF b, in PointF c) { cartesianToBarycnetric(toClamp, a, b, c, out float u, out float v, out float w); clampBarycentric(ref u, ref v, ref w); barycentricToCartesian(ref toClamp, a, b, c, u, v, w); } /// /// Convert barycentric coordinates u,v,w relative to triangle with verticies a,b,c to cartesian coordinates. /// /// /// /// /// /// /// /// private void barycentricToCartesian(ref PointF toClamp, PointF a, PointF b, PointF c, float u, float v, float w) { toClamp.X = a.X * u + b.X * v + c.X * w; toClamp.Y = a.Y * u + b.Y * v + c.Y * w; } /// /// Clamp x to the range [0, 1] /// /// /// private float saturate(float x) { return MathF.Max(MathF.Min(x, 1.0f), 0.0f); } /// /// Clamp the barycentric coordinates, so that /// u + v + w == 1 /// without any of u,v,w being greater than 1 or less than zero. /// /// /// /// private void clampBarycentric(ref float u, ref float v, ref float w) { u = saturate(u); v = saturate(v); w = 1 - u - v; u = saturate(u); w = saturate(w); v = 1 - u - w; v = saturate(v); w = saturate(w); u = 1 - v - w; } /// /// Linearlly interpolate between x and y by factor k /// If k == 0 then 100% of x and 0% of y /// If k == 1 then 0% of x and 100% of y /// /// /// /// /// private float lerp(float x, float y, float k) { return x * (1 - k) + y * k; } /// /// Normalize point f, so that its length is 1 /// /// private void normalize(ref PointF b) { float length = hypot(b.X, b.Y); b.X = b.X / length; b.Y = b.Y / length; } /// /// Subtract point a from b, so that the result is (x0 - x1 | y0 - y1) /// /// /// /// private PointF subtract(in PointF a, in PointF b) { var result = new PointF(); result.X = a.X - b.X; result.Y = a.Y - b.Y; return result; } /// /// Convert from HSV color space to sRGB. /// s,v are expected to be in range [0, 1]. /// h is expected to be in range [0, 1]. /// Any other values results in undefiened behavior /// /// /// /// /// private int sRGBfromHSV(float h, float s, float v) { float c = v * s; float x = c * (1.0f - Math.Abs((h / 60.0f) % 2.0f - 1.0f)); float m = v - c; float r = 0, g = 0, b = 0; if (h < 60) { r = c; g = x; b = 0; } else if (h < 120) { r = x; g = c; b = 0; } else if (h < 180) { r = 0; g = c; b = x; } else if (h < 240) { r = 0; g = x; b = c; } else if (h < 300) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } return 0xFF << 24 | (int)((r + m) * 255.0f) << 16 | (int)((g + m) * 255.0f) << 8 | (int)((b + m) * 255.0f); } /// /// Convert sRGB color space to HSV /// /// /// /// private void HSVfromRGB(byte r, byte g, byte b) { const float INV_255 = 0.00392156862745f; float R = r * INV_255; float G = g * INV_255; float B = b * INV_255; byte Cmax = Math.Max(Math.Max(r, g), b); byte Cmin = Math.Min(Math.Min(r, g), b); float delta = (Cmax - Cmin) * INV_255; if (delta < 1e-2) { angle = 0; } else if (Cmax == r) { if (G < B) { angle = 360 - MathF.Abs(60 * ((G - B) / delta)); } else { angle = 60 * ((G - B) / delta + 0f); } } else if (Cmax == g) { angle = 60 * ((B - R) / delta + 2f); } else if (Cmax == b) { angle = 60 * ((R - G) / delta + 4f); } saturation = delta / Cmax * 255.0f; if (Cmax == 0) { saturation = 0; } value = Cmax * INV_255; } #endregion protected override void OnCreateControl() { base.OnCreateControl(); BackColorChanged += (a, b) => generateWheel(); grayGradient = new LinearGradientBrush( // point A new Point((int)vertices[0].X, (int)vertices[0].Y), // point B new Point(floorMiddle(vertices[2].X, vertices[1].X), floorMiddle(vertices[2].Y, vertices[1].Y)), Color.Red, Color.Gray ); whiteGradient = new LinearGradientBrush( // point A new Point((int)vertices[2].X, (int)vertices[2].Y), // point B new Point(floorMiddle(vertices[0].X, vertices[1].X), floorMiddle(vertices[0].Y, vertices[1].Y)), Color.White, Color.Transparent ); colorGradient = new LinearGradientBrush( // point A new Point((int)vertices[1].X, (int)vertices[1].Y), // point B new Point(floorMiddle(vertices[2].X, vertices[0].X), floorMiddle(vertices[2].Y, vertices[0].Y)), Color.Black, Color.Transparent ); this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true); this.MouseMove += new System.Windows.Forms.MouseEventHandler(OnMouseMove); this.MouseDown += new System.Windows.Forms.MouseEventHandler(OnMouseDown); this.MouseUp += new System.Windows.Forms.MouseEventHandler(OnMouseUp); generateWheel(); setRGB(0, 0, 0); } private void OnMouseUp(object sender, System.Windows.Forms.MouseEventArgs e) { release = true; } private void OnMouseDown(object sender, System.Windows.Forms.MouseEventArgs e) { release = false; entered = true; } private void OnMouseMove(object sender, System.Windows.Forms.MouseEventArgs e) { double u = e.X / (double)this.Width * 2.0 - 1.0; double v = e.Y / (double)this.Height * 2.0 - 1.0; double theta = Math.Atan2(v, u); double radius = Math.Sqrt(u * u + v * v); if (entered) { selection = Slider.WHEEL; if (radius < 0.6) { selection = Slider.TRIANGLE; } entered = false; } else if (!release) { if (selection == Slider.TRIANGLE) { if (transform != null) { PointF tempCursor = new PointF(e.X, e.Y); clampToBarycentricCoordinates(ref tempCursor, transformed[0], transformed[1], transformed[2]); if (true) { cursor.X = (int)tempCursor.X; cursor.Y = (int)tempCursor.Y; origCursor.X = cursor.X; origCursor.Y = cursor.Y; ctheta = angle; cartesianToBarycnetric(cursor, transformed[0], transformed[1], transformed[2], out float bu, out float bv, out float bw); value = MathF.Round(MathF.Max(MathF.Min(1.0f - bv, 1.0f), 0.0f) * 1e2f) / 1e2f; saturation = MathF.Round(MathF.Max(MathF.Min(bu, 1.0f), 0.0f) * 1e2f) / 1e2f; } } } else { angle = ToDegree((float)theta); grayGradient = new LinearGradientBrush( new Point((int)vertices[0].X, (int)vertices[0].Y), new Point(floorMiddle(vertices[2].X, vertices[1].X), floorMiddle(vertices[2].Y, vertices[1].Y) ), Color.FromArgb(sRGBfromHSV(180 + angle, 1, 1)), Color.Gray ); PointF[] c = { new PointF(origCursor.X, origCursor.Y) }; transform.Reset(); transform.RotateAt(angle - ctheta, new Point(this.Width >> 1, this.Height >> 1)); transform.TransformPoints(c); cursor.X = (int)c[0].X; cursor.Y = (int)c[0].Y; } sRGB = sRGBfromHSV(angle + 180, saturation, value); this.Refresh(); handler.Invoke(this, null); } } /// /// Generate the color wheel "wheel" texture /// private void generateWheel() { wheel = new Bitmap(WHEEL_PIXEL_RESOLUTION, WHEEL_PIXEL_RESOLUTION); wheel.MakeTransparent(); // make transparent var g = Graphics.FromImage(wheel); // draw tone gradient by varying hue by the current angle of the pixel relative to the origin for (int x = wheel.Width; x-- > -1;) { float u = x / (float)wheel.Width * 2.0f - 1.0f; for (int y = wheel.Height; y-- > -1;) { float v = y / (float)wheel.Height * 2.0f - 1.0f; // cartesian to polar float theta = ToDegree(MathF.Atan2(v, u) + MathF.PI); int rgb = sRGBfromHSV(theta, 1.0f, 1.0f); // draw pixel g.FillRectangle(new SolidBrush(Color.FromArgb(rgb)), x, y, 1, 1); } } var step = wheel.Width / WHEEL_BORDER_SUBDIVISIONS; var strip = (int)Math.Sqrt(wheel.Width * wheel.Width + wheel.Height * wheel.Height) - wheel.Width; // cut out center circle g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; g.FillEllipse(new SolidBrush(this.BackColor), new Rectangle(step, step, step * (WHEEL_BORDER_SUBDIVISIONS - 2), step * (WHEEL_BORDER_SUBDIVISIONS - 2))); g.DrawEllipse(new Pen(new SolidBrush(this.BackColor), strip), new Rectangle(-strip / 2, -strip / 2, wheel.Width + strip, wheel.Height + strip)); g.Dispose(); } /// /// Verwendete Ressourcen bereinigen. /// /// True, wenn verwaltete Ressourcen gelöscht werden sollen; andernfalls False. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { base.OnPaint(e); var g = e.Graphics; // adjust for arbitrary component scale float scaleX = this.Width / (float) wheel.Width; float scaleY = this.Height / (float) wheel.Height; // transform local verticies to screen space g.ScaleTransform(scaleX, scaleY); g.DrawImage(wheel, new Point(0, 0)); g.ResetTransform(); g.RotateTransform(angle); float step = (wheel.Width / (float) WHEEL_BORDER_SUBDIVISIONS * (WHEEL_BORDER_SUBDIVISIONS - 3)); float off = WHEEL_PIXEL_RESOLUTION / (float)(WHEEL_BORDER_SUBDIVISIONS - 2) * scaleX; g.ScaleTransform(wheel.Width / (float) Width * 4 / 6, wheel.Width / (float) Width * 4 / 6); g.TranslateTransform(wheel.Width * scaleX * 0.5f, wheel.Width * scaleX * 0.5f, MatrixOrder.Append); transformed[0].X = vertices[0].X; transformed[0].Y = vertices[0].Y; transformed[1].X = vertices[1].X; transformed[1].Y = vertices[1].Y; transformed[2].X = vertices[2].X; transformed[2].Y = vertices[2].Y; g.Transform.TransformPoints(transformed); // draw triangle g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; g.FillPolygon(grayGradient, vertices); g.FillPolygon(whiteGradient, vertices); g.FillPolygon(colorGradient, vertices); // paint hue line g.DrawLine(new Pen(new SolidBrush(Color.Black), 2.0f), vertices[0].X, vertices[0].Y, vertices[0].X + RADIUS / 2, vertices[0].Y); // paint bordered shadowed cursor g.ResetTransform(); transform = g.Transform; g.FillEllipse(new SolidBrush(Color.FromArgb(-2013265920)), new Rectangle(cursor.X - 10, cursor.Y - 10, 22, 22)); g.FillEllipse(new SolidBrush(Color.White), new Rectangle(cursor.X - 10, cursor.Y - 10, 20, 20)); g.FillEllipse(new SolidBrush(Color.FromArgb(sRGB)), new Rectangle(cursor.X - 8, cursor.Y - 8, 16, 16)); } #region Getter-Setter public int getRGB() { return sRGB; } public int getRed() { return sRGB >> 16 & 0xFF; } public int getGreen() { return sRGB >> 8 & 0xFF; } public int getBlue() { return sRGB & 0xFF; } public float getHue() { return angle; } public float getSaturation() { return saturation; } public float getValue() { return value; } public void setHue(float hue) { angle = hue; showHSV(); } public void setSaturation(float s) { saturation = s; showHSV(); } public void setValue(float v) { value = v; showHSV(); } public void setRGB(byte red, byte green, byte blue) { HSVfromRGB(red, green, blue); showHSV(); } #endregion /// /// Show the color wheel and recompute triangle gradient /// and calculate new cursor position /// private void showHSV() { // get sRGB color sRGB = sRGBfromHSV(angle, saturation, value); // recompute correct triangle vertex transformation // to screen space System.Drawing.Drawing2D.Matrix m = new System.Drawing.Drawing2D.Matrix(); transformed[0].X = vertices[0].X; transformed[0].Y = vertices[0].Y; transformed[1].X = vertices[1].X; transformed[1].Y = vertices[1].Y; transformed[2].X = vertices[2].X; transformed[2].Y = vertices[2].Y; // adjust for custom component width float scaleX = this.Width / (float)wheel.Width; float scaleY = this.Height / (float)wheel.Height; m.Rotate(angle - 180); float step = (wheel.Width / (float)WHEEL_BORDER_SUBDIVISIONS * (WHEEL_BORDER_SUBDIVISIONS - 3)); float off = WHEEL_PIXEL_RESOLUTION / (float)(WHEEL_BORDER_SUBDIVISIONS - 2) * scaleX; m.Scale(wheel.Width / (float)Width * 4 / 6, wheel.Width / (float)Width * 4 / 6); m.Translate(wheel.Width * scaleX * 0.5f, wheel.Width * scaleX * 0.5f, MatrixOrder.Append); transformed[0].X = vertices[0].X; transformed[0].Y = vertices[0].Y; transformed[1].X = vertices[1].X; transformed[1].Y = vertices[1].Y; transformed[2].X = vertices[2].X; transformed[2].Y = vertices[2].Y; m.TransformPoints(transformed); // linearly interpolate cursor location according to saturation origCursor.X = (int)lerp(transformed[2].X, transformed[0].X, saturation); origCursor.Y = (int)lerp(transformed[2].Y, transformed[0].Y, saturation); // linearly interpolate cursor location according to value origCursor.X = (int)lerp(transformed[1].X, origCursor.X, value); origCursor.Y = (int)lerp(transformed[1].Y, origCursor.Y, value); // apply cursor location cursor.X = origCursor.X; cursor.Y = origCursor.Y; // recompute new gradient for mixing fully saturated color with gray sRGB = sRGBfromHSV(angle, saturation, value); grayGradient = new LinearGradientBrush(new Point((int)vertices[0].X, (int)vertices[0].Y), new Point((int)(vertices[2].X + vertices[1].X) / 2, (int)(vertices[2].Y + vertices[1].Y) / 2), Color.FromArgb(sRGBfromHSV(angle, 1f, 1f)), Color.Gray); // adjust angle values angle -= 180; ctheta = angle; // repaint this.Refresh(); } /// /// Erforderliche Methode für die Designerunterstützung. /// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden. /// private void InitializeComponent() { components = new System.ComponentModel.Container(); } } }