Рисование цветового кольца для выбора оттенка на C#.

Данная статья является переводом Saveen Reddy “ Drawing a Color Hue Wheel with C# ”.

Вы уже видели такой интерфейс раньше. Это – кольцо с плавным переходом между всеми цветами радуги.

Ниже – пример из приложения для рисования MyPaint.

И еще один – из Corel Painter 12.

Обратите внимание на различия в расположении цветов и направлении перехода цвета.

Вчера я планировал написать новую статью в блог о цветах RGB и понял, что мне необходимо создать такой интерфейс. И вместо того, чтобы использовать один из уже существующих, я подумал, что будет хорошей тренировкой создать такое самостоятельно. Итак, ниже – то, что у меня получилось при помощи небольшой собственной программы.

Исходный код доступен на CodePlex странице проекта Vizibl: найдите проект под названием DemoDrawColorWheelBitmap в папке Demos. Ниже я объясню как это работает. Данный код не особо отлажен и не оптимизирован, есть масса других способов реализации такого функционала. Я предлагаю использовать этот код для целей обучения, иллюстрирующих основу концепции. (На самом деле я думаю, что это может быть хорошим вопросом на интервью для соискателя на позицию в Майкрософт).

Основа функционала, представленного ниже, состоит в отрисовке круга. Это работает следующим образом: создается битмап для отображения круга определенной ширины, заданной внутренним и внешним радиусами. Цикл проходит по всем координатам этого битмапа. Если координата находится внутри круга, вычисляется значение цвета на основе координаты и смещения. Если же координата не попадает в круг, оставляется белый пиксель.

Вычисление цвета на основе координаты требует нахождения угла между центром круга и текущей координатой. Данный угол находится в интервале между –Π и Π. Оттенок вычисляется из угла нормализацией его значения на интервал от 0.0 до 1.0.

  string output_filename = "D:\\\\colorwheel.png";
  
  int padding = 10;
  int inner_radius = 200;
  int outer_radius = inner_radius + 50;
  
  int bmp_width = (2 * outer_radius) + (2 * padding);
  int bmp_height = bmp_width;
  
  var center = new System.Drawing.Point(bmp_width / 2, bmp_height / 2);
  var c = System.Drawing.Color.Red;
  
  using (var bmp = new System.Drawing.Bitmap(bmp_width, bmp_height))
  {
      using (var g = System.Drawing.Graphics.FromImage(bmp))
      {
          g.FillRectangle(System.Drawing.Brushes.White, 0, 0, bmp.Width, bmp.Height);
      }
      for (int y = 0; y < bmp_width; y++)
      {
          int dy = (center.Y - y);
  
          for (int x = 0; x < bmp_width; x++)
          {
              int dx = (center.X - x);
  
              double dist = System.Math.Sqrt(dx * dx + dy * dy);
  
  
              if (dist >= inner_radius && dist <= outer_radius)
              {
                  double theta = System.Math.Atan2(dy, dx);
                  // theta can go from -pi to pi
  
                  double hue = (theta + System.Math.PI) / (2 * System.Math.PI);
  
                  double dr, dg, db;
                  const double sat = 1.0;
                  const double val = 1.0;
                  HSVToRGB(hue, sat, val, out dr, out dg, out db);
                  c = System.Drawing.Color.FromArgb((int)(dr * 255), (int)(dg * 255), (int)(db * 255));
  
                  bmp.SetPixel(x, y, c);
              }
          }
      }
      bmp.Save(output_filename);
  

Затем значение RGB получается из оттенка, насыщенности и константы, где насыщенность равна 1.0 и константа тоже равна 1.0.

Код, приведенный ниже, конвертирует HSV в RGB. Заметьте, что все значения для ввода и вывода находятся в интервале от 0 до 1.0.

  public static void HSVToRGB(double H, double S, double V, out double R, out double G, out double B)
  {
      if (H == 1.0)
      {
          H = 0.0;
      }
  
      double step = 1.0/6.0;
      double vh = H/step;
  
      int i = (int) System.Math.Floor(vh);
  
      double f = vh - i;
      double p = V*(1.0 - S);
      double q = V*(1.0 - (S*f));
      double t = V*(1.0 - (S*(1.0 - f)));
  
      switch (i)
      {
          case 0:
              {
                  R = V;
                  G = t;
                  B = p;
                  break;
              }
          case 1:
              {
                  R = q;
                  G = V;
                  B = p;
                  break;
              }
          case 2:
              {
                  R = p;
                  G = V;
                  B = t;
                  break;
              }
          case 3:
              {
                  R = p;
                  G = q;
                  B = V;
                  break;
              }
          case 4:
              {
                  R = t;
                  G = p;
                  B = V;
                  break;
              }
          case 5:
              {
                  R = V;
                  G = p;
                  B = q;
                  break;
              }
          default: 
              {
                  // not possible - if we get here it is an internal error
                  throw new ArgumentException();
              }
      }
  }