using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace CodeImp.DoomBuilder.ColorPicker { public sealed class ColorWheel : IDisposable { // These resources should be disposed // of when you're done with them. private Graphics g; private Region colorRegion; private Region brightnessRegion; private Bitmap colorImage; public delegate void ColorChangedEventHandler(object sender, ColorChangedEventArgs e); public ColorChangedEventHandler ColorChanged; // Keep track of the current mouse state. private enum MouseState { MouseUp, ClickOnColor, DragInColor, ClickOnBrightness, DragInBrightness, ClickOutsideRegion, DragOutsideRegion, } private MouseState currentState = MouseState.MouseUp; // The code needs to convert back and forth between // degrees and radians. There are 2*PI radians in a // full circle, and 360 degrees. This constant allows // you to convert back and forth. private const float DEGREES_PER_RADIAN = 180.0f / (float)Math.PI; // COLOR_COUNT represents the number of distinct colors // used to create the circular gradient. Its value // is somewhat arbitrary -- change this to 6, for // example, to see what happens. 1536 (6 * 256) seems // a good compromise -- it's enough to get a full // range of colors, but it doesn't overwhelm the processor // attempting to generate the image. The color wheel // contains 6 sections, and each section displays // 256 colors. Seems like a reasonable compromise. private const int COLOR_COUNT = 6 * 256; private Point centerPoint; private int radius; private Rectangle colorRectangle; private Rectangle brightnessRectangle; private int brightnessX; private float brightnessScaling; // selectedColor is the actual value selected by the user. fullColor is the same color, // with its brightness set to 255. private Color selectedColor = Color.White; private Color fullColor; private ColorHandler.RGB RGB; private ColorHandler.HSV HSV; // Locations for the two "pointers" on the form. private Point colorPoint; private Point brightnessPoint; private int brightness; private int brightnessMin; private int brightnessMax; public Color Color { get { return selectedColor; } } public ColorWheel(Rectangle colorRectangle, Rectangle brightnessRectangle) { // Caller must provide locations for color wheel // (colorRectangle), brightness "strip" (brightnessRectangle) // and location to display selected color (selectedColorRectangle). using(GraphicsPath path = new GraphicsPath()) { // Store away locations for later use. this.colorRectangle = colorRectangle; this.brightnessRectangle = brightnessRectangle; //this.selectedColorRectangle = selectedColorRectangle; // Calculate the center of the circle. // Start with the location, then offset // the point by the radius. // Use the smaller of the width and height of // the colorRectangle value. this.radius = Math.Min(colorRectangle.Width, colorRectangle.Height) / 2; this.centerPoint = colorRectangle.Location; this.centerPoint.Offset(radius, radius); // Start the pointer in the center. this.colorPoint = this.centerPoint; // Create a region corresponding to the color circle. // Code uses this later to determine if a specified // point is within the region, using the IsVisible // method. path.AddEllipse(colorRectangle); colorRegion = new Region(path); // set { the range for the brightness selector. this.brightnessMin = this.brightnessRectangle.Top; this.brightnessMax = this.brightnessRectangle.Bottom; // Create a region corresponding to the // brightness rectangle, with a little extra // "breathing room". path.AddRectangle(new Rectangle(brightnessRectangle.Left, brightnessRectangle.Top - 10, brightnessRectangle.Width + 10, brightnessRectangle.Height + 20)); // Create region corresponding to brightness // rectangle. Later code uses this to // determine if a specified point is within // the region, using the IsVisible method. brightnessRegion = new Region(path); // Set the location for the brightness indicator "marker". // Also calculate the scaling factor, scaling the height // to be between 0 and 255. brightnessX = brightnessRectangle.Left + brightnessRectangle.Width; brightnessScaling = 255.0f / (brightnessMax - brightnessMin); // Calculate the location of the brightness // pointer. Assume it's at the highest position. brightnessPoint = new Point(brightnessX, brightnessMax); // Create the bitmap that contains the circular gradient. CreateGradient(); } } private void OnColorChanged(ColorHandler.RGB RGB, ColorHandler.HSV HSV) { ColorChangedEventArgs e = new ColorChangedEventArgs(RGB, HSV); ColorChanged(this, e); } void IDisposable.Dispose() { // Dispose of graphic resources if(colorImage != null) colorImage.Dispose(); if(colorRegion != null) colorRegion.Dispose(); if(brightnessRegion != null) brightnessRegion.Dispose(); if(g != null) g.Dispose(); } public void SetMouseUp() { // Indicate that the user has // released the mouse. currentState = MouseState.MouseUp; } public void Draw(Graphics g, ColorHandler.RGB RGB) { // Given RGB values, calculate HSV and then update the screen. this.g = g; this.HSV = ColorHandler.RGBtoHSV(RGB); CalcCoordsAndUpdate(this.HSV); UpdateDisplay(); } public void Draw(Graphics g, Point mousePoint) { // You've moved the mouse. // Now update the screen to match. // Keep track of the previous color pointer point, // so you can put the mouse there in case the // user has clicked outside the circle. Point newColorPoint = colorPoint; Point newBrightnessPoint = brightnessPoint; // Store this away for later use. this.g = g; if(currentState == MouseState.MouseUp) { if(!mousePoint.IsEmpty) { if(colorRegion.IsVisible(mousePoint)) { // Is the mouse point within the color circle? // If so, you just clicked on the color wheel. currentState = MouseState.ClickOnColor; } else if(brightnessRegion.IsVisible(mousePoint)) { // Is the mouse point within the brightness area? // You clicked on the brightness area. currentState = MouseState.ClickOnBrightness; } else { // Clicked outside the color and the brightness // regions. In that case, just put the // pointers back where they were. currentState = MouseState.ClickOutsideRegion; } } } switch(currentState) { case MouseState.ClickOnBrightness: case MouseState.DragInBrightness: // Calculate new color information // based on the brightness, which may have changed. Point newPoint = mousePoint; if(newPoint.Y < brightnessMin) { newPoint.Y = brightnessMin; } else if(newPoint.Y > brightnessMax) { newPoint.Y = brightnessMax; } newBrightnessPoint = new Point(brightnessX, newPoint.Y); brightness = (int)((brightnessMax - newPoint.Y) * brightnessScaling); HSV.value = brightness; RGB = ColorHandler.HSVtoRGB(HSV); break; case MouseState.ClickOnColor: case MouseState.DragInColor: // Calculate new color information // based on selected color, which may have changed. newColorPoint = mousePoint; // Calculate x and y distance from the center, // and then calculate the angle corresponding to the // new location. Point delta = new Point(mousePoint.X - centerPoint.X, mousePoint.Y - centerPoint.Y); int degrees = CalcDegrees(delta); // Calculate distance from the center to the new point // as a fraction of the radius. Use your old friend, // the Pythagorean theorem, to calculate this value. float distance = (float)Math.Sqrt(delta.X * delta.X + delta.Y * delta.Y) / radius; if(currentState == MouseState.DragInColor) { if(distance > 1) { // Mouse is down, and outside the circle, but you // were previously dragging in the color circle. // What to do? // In that case, move the point to the edge of the // circle at the correct angle. distance = 1; newColorPoint = GetPoint(degrees, radius, centerPoint); } } // Calculate the new HSV and RGB values. HSV.Hue = degrees * 255 / 360; HSV.Saturation = (int)(distance * 255); HSV.value = brightness; RGB = ColorHandler.HSVtoRGB(HSV); fullColor = ColorHandler.HSVtoColor(HSV.Hue, HSV.Saturation, 255); break; } selectedColor = ColorHandler.HSVtoColor(HSV); // Raise an event back to the parent form, // so the form can update any UI it's using // to display selected color values. OnColorChanged(RGB, HSV); // On the way out, set the new state. switch(currentState) { case MouseState.ClickOnBrightness: currentState = MouseState.DragInBrightness; break; case MouseState.ClickOnColor: currentState = MouseState.DragInColor; break; case MouseState.ClickOutsideRegion: currentState = MouseState.DragOutsideRegion; break; } // Store away the current points for next time. colorPoint = newColorPoint; brightnessPoint = newBrightnessPoint; // Draw the gradients and points. UpdateDisplay(); } private Point CalcBrightnessPoint(int brightness) { // Take the value for brightness (0 to 255), scale to the // scaling used in the brightness bar, then add the value // to the bottom of the bar. return the correct point at which // to display the brightness pointer. return new Point(brightnessX, (int)(brightnessMax - brightness / brightnessScaling)); } private void UpdateDisplay() { // Update the gradients, and place the pointers correctly based on colors and // brightness. CreateGradient(); // Draw the saved color wheel image. g.DrawImage(colorImage, colorRectangle); // Draw the "brightness" rectangle. DrawLinearGradient(fullColor); // Draw the two pointers. DrawColorPointer(colorPoint); DrawBrightnessPointer(brightnessPoint); } private void CalcCoordsAndUpdate(ColorHandler.HSV HSV) { // Convert color to real-world coordinates and then calculate // the various points. HSV.Hue represents the degrees (0 to 360), // HSV.Saturation represents the radius. // This procedure doesn't draw anything--it simply // updates class-level variables. The UpdateDisplay // procedure uses these values to update the screen. // Given the angle (HSV.Hue), and distance from // the center (HSV.Saturation), and the center, // calculate the point corresponding to // the selected color, on the color wheel. colorPoint = GetPoint((float)HSV.Hue / 255 * 360, (float)HSV.Saturation / 255 * radius, centerPoint); // Given the brightness (HSV.value), calculate the // point corresponding to the brightness indicator. brightnessPoint = CalcBrightnessPoint(HSV.value); // Store information about the selected color. brightness = HSV.value; selectedColor = ColorHandler.HSVtoColor(HSV); RGB = ColorHandler.HSVtoRGB(HSV); // The full color is the same as HSV, except that the // brightness is set to full (255). This is the top-most // color in the brightness gradient. fullColor = ColorHandler.HSVtoColor(HSV.Hue, HSV.Saturation, 255); } private void DrawLinearGradient(Color TopColor) { // Given the top color, draw a linear gradient // ranging from black to the top color. Use the // brightness rectangle as the area to fill. using(LinearGradientBrush lgb = new LinearGradientBrush(brightnessRectangle, TopColor, Color.Black, LinearGradientMode.Vertical)) { g.FillRectangle(lgb, brightnessRectangle); } } private static int CalcDegrees(Point pt) { int degrees; if(pt.X == 0) { // The point is on the y-axis. Determine whether it's above or below the x-axis, and return the // corresponding angle. Note that the orientation of the y-coordinate is backwards. That is, // A positive Y value indicates a point BELOW the x-axis. degrees = pt.Y > 0 ? 270 : 90; } else { // This value needs to be multiplied by -1 because the y-coordinate // is opposite from the normal direction here. // That is, a y-coordinate that's "higher" on the form has a lower y-value, in this coordinate // system. So everything's off by a factor of -1 when performing the ratio calculations. degrees = (int)(-Math.Atan((float)pt.Y / pt.X) * DEGREES_PER_RADIAN); // If the x-coordinate of the selected point is to the left of the center of the circle, you // need to add 180 degrees to the angle. ArcTan only gives you a value on the right-hand side // of the circle. if(pt.X < 0) degrees += 180; // Ensure that the return value is between 0 and 360. degrees = (degrees + 360) % 360; } return degrees; } private void CreateGradient() { // Create a new PathGradientBrush, supplying an array of points created by calling // the GetPoints method. using(PathGradientBrush pgb = new PathGradientBrush(GetPoints(radius, new Point(radius, radius)))) { // Set the various properties. Note the SurroundColors property, which contains an array of points, // in a one-to-one relationship with the points that created the gradient. pgb.CenterColor = Color.White; pgb.CenterPoint = new PointF(radius, radius); pgb.SurroundColors = GetColors(); // Create a new bitmap containing the color wheel gradient, so the // code only needs to do all this work once. Later code uses the bitmap // rather than recreating the gradient. colorImage = new Bitmap(colorRectangle.Width, colorRectangle.Height, PixelFormat.Format32bppArgb); using(Graphics newGraphics = Graphics.FromImage(colorImage)) { newGraphics.FillEllipse(pgb, 0, 0, colorRectangle.Width, colorRectangle.Height); } } } private Color[] GetColors() { // Create an array of COLOR_COUNT colors, looping through all the hues between 0 and 255, broken // into COLOR_COUNT intervals. HSV is particularly well-suited for this, because the only value // that changes as you create colors is the Hue. Color[] Colors = new Color[COLOR_COUNT]; for(int i = 0; i < COLOR_COUNT; i++) Colors[i] = ColorHandler.HSVtoColor((int)((float)(i * 255) / COLOR_COUNT), 255, HSV.value); return Colors; } private static Point[] GetPoints(float radius, Point centerPoint) { // Generate the array of points that describe the locations of the COLOR_COUNT colors to be // displayed on the color wheel. Point[] Points = new Point[COLOR_COUNT]; for(int i = 0; i < COLOR_COUNT; i++) Points[i] = GetPoint((float)(i * 360) / COLOR_COUNT, radius, centerPoint); return Points; } private static Point GetPoint(float degrees, float radius, Point centerPoint) { // Given the center of a circle and its radius, along // with the angle corresponding to the point, find the coordinates. // In other words, conver t from polar to rectangular coordinates. float radians = degrees / DEGREES_PER_RADIAN; return new Point((int)(centerPoint.X + Math.Floor(radius * Math.Cos(radians))), (int)(centerPoint.Y - Math.Floor(radius * Math.Sin(radians)))); } private void DrawColorPointer(Point pt) { // Given a point, draw the color selector. The constant SIZE represents half // the width -- the square will be twice this value in width and height. const int SIZE = 3; g.DrawRectangle(Pens.Black, pt.X - SIZE, pt.Y - SIZE, SIZE * 2, SIZE * 2); } private void DrawBrightnessPointer(Point pt) { // Draw a triangle for the brightness indicator that "points" at the provided point. const int HEIGHT = 10; const int WIDTH = 7; Point[] Points = new Point[3]; Points[0] = pt; Points[1] = new Point(pt.X + WIDTH, pt.Y + HEIGHT / 2); Points[2] = new Point(pt.X + WIDTH, pt.Y - HEIGHT / 2); g.FillPolygon(Brushes.Black, Points); } } }