Skip to content

ChooseTextColorDemo

Repository source: ChooseTextColorDemo

Question

If you have a question about this example, please use the VTK Discourse Forum

Code

ChooseTextColorDemo.cxx

#include <vtkActor2D.h>
#include <vtkCamera.h>
#include <vtkCellArray.h>
#include <vtkCoordinate.h>
#include <vtkMath.h>
#include <vtkMinimalStandardRandomSequence.h>
#include <vtkNamedColors.h>
#include <vtkNew.h>
#include <vtkPoints.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper2D.h>
#include <vtkPolyLine.h>
#include <vtkProperty.h>
#include <vtkProperty2D.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
#include <vtkTextActor.h>
#include <vtkTextProperty.h>
#include <vtkTextRepresentation.h>
#include <vtkTextWidget.h>

#include <array>
#include <string>
#include <vector>

namespace {
// Given a color, find a contrasting color. If the given color is "light",
// use the lightColor otherwise use the darkColor.
void ChooseContrastingColor(double* rgbIn, double* rgbOut,
                            double threshold = .5,
                            const std::string& lightColor = "white",
                            const std::string& darkColor = "black");

void RandomHSV(double hsv[3], double const& min_r, double const& max_r,
               vtkMinimalStandardRandomSequence* rng);

/** Specify the rules for drawing the borders around a viewport.
 *
 * Here the borders for the viewports are defined in the order
 *  [top, left, bottom, right].
 *
 * Names for adjacent sides reflect anticlockwise ordering.
 */
static constexpr struct ViewportBorderSpecifier
{
  std::array<bool, 4> t{true, false, false, false};
  std::array<bool, 4> l{false, true, false, false};
  std::array<bool, 4> b{false, false, true, false};
  std::array<bool, 4> r{false, false, false, true};
  std::array<bool, 4> lb{false, true, true, false};
  std::array<bool, 4> lbr{false, true, true, true};
  std::array<bool, 4> tlb{true, true, true, false};
  std::array<bool, 4> tlbr{true, true, true, true};
  std::array<bool, 4> rtl{true, true, false, true};
  std::array<bool, 4> tl{true, true, false, false};
} viewportBorderSpecifier;

/** Draw a border around a viewport.
 *
 * @param renderer: The renderer corresponding to the viewport.
 * @param sides: An array of boolean corresponding to [top, left, bottom, right]
 * @param border_color: The color of the border.
 * @param border_width: The width of the border.
 * @return The border actor.
 */
vtkNew<vtkActor2D> DrawViewportBorder(std::array<bool, 4> const& sides,
                                      std::string const& border_color,
                                      unsigned int const& border_width);

typedef std::map<std::string, std::array<double, 2>> TTextPosition;
typedef std::map<std::string, TTextPosition> TTextPositions;

/** Get viewport positioning information for a vector of names.
 *
 * Note: You must include vtkSystemIncludes.h to get these defines:
 *  VTK_TEXT_LEFT 0, VTK_TEXT_CENTERED 1, VTK_TEXT_RIGHT 2,
 *  VTK_TEXT_BOTTOM 0, VTK_TEXT_TOP 2
 *
 *  @param names - The vector of names.
 *  @param justification - Horizontal justification of the text.
 *  @param vertical_justification - Vertical justification of the text.
 *  @param width - Width of the bounding_box of the text in screen coordinates.
 *  @param height - Height of the bounding_box of the text in screen
 *                  coordinates.
 *  @return A map of positioning information for exch name in names.
 */
TTextPositions
GetTextPositions(std::vector<std::string> const& names,
                 int const justification = VTK_TEXT_LEFT,
                 int const vertical_justification = VTK_TEXT_BOTTOM,
                 double const width = 0.96, double const height = 0.1);

} // namespace

int main(int argc, char* argv[])
{
  // For testing
  vtkNew<vtkMinimalStandardRandomSequence> rng;
  rng->SetSeed(8775070);
  // rng->SetSeed(0);
  // rng->SetSeed(4355412);

  double threshold = 0.5;
  std::string lightColor = "white";
  std::string darkColor = "black";

  if (argc > 1)
  {
    threshold = atof(argv[1]);
  }
  if (argc > 2)
  {
    lightColor = argv[2];
  }
  if (argc > 3)
  {
    darkColor = argv[3];
  }
  // Visualize
  vtkNew<vtkNamedColors> colors;

  std::vector<std::string> titles;
  titles.push_back("Text");

  // Position text according to its length and centered in the viewport.
  auto textPositions =
      GetTextPositions(titles, VTK_TEXT_CENTERED, VTK_TEXT_CENTERED, 0.5, 0.5);

  // Setup render window.
  unsigned int xGridDimensions = 10;
  unsigned int yGridDimensions = 10;
  int rendererSize = 100;

  vtkNew<vtkRenderWindow> renderWindow;
  renderWindow->SetSize(rendererSize * xGridDimensions,
                        rendererSize * yGridDimensions);
  renderWindow->SetWindowName("ChooseTextColorDemo");
  vtkNew<vtkRenderWindowInteractor> renderWindowInteractor;
  renderWindowInteractor->SetRenderWindow(renderWindow);

  std::vector<vtkSmartPointer<vtkTextWidget>> textWidgets;
  std::vector<vtkSmartPointer<vtkRenderer>> renderers;

  auto min_r = 0.0;
  auto max_r = 1.0;

  for (auto i = 0; i < static_cast<int>(xGridDimensions * yGridDimensions); ++i)
  {
    auto name = titles[0];

    // Setup a renderer.
    vtkNew<vtkRenderer> renderer;
    double hsv[3];
    RandomHSV(hsv, min_r, max_r, rng);
    double rgb[3];
    vtkMath::HSVToRGB(hsv, rgb);
    renderer->SetBackground(rgb);

    renderers.push_back(renderer);
    renderWindow->AddRenderer(renderer);

    // Get the contrasting color.
    ChooseContrastingColor(renderers[i]->GetBackground(), rgb, threshold,
                           lightColor, darkColor);

    vtkNew<vtkTextProperty> textProperty;
    textProperty->SetColor(rgb);
    textProperty->SetJustificationToCentered();

    // Create the text actor and representation.
    vtkNew<vtkTextActor> textActor;
    textActor->SetInput(name.c_str());
    textActor->SetTextScaleModeToNone();
    textActor->SetTextProperty(textProperty);

    // Create the text representation. Used for positioning the text actor.
    vtkNew<vtkTextRepresentation> textRepresentation;
    textRepresentation->EnforceNormalizedViewportBoundsOff();
    textRepresentation->GetPositionCoordinate()->SetValue(
        textPositions[name]["p"].data());
    textRepresentation->GetPosition2Coordinate()->SetValue(
        textPositions[name]["p2"].data());

    // Create the text widget, setting the default renderer and interactor.
    vtkNew<vtkTextWidget> textWidget;
    textWidget->SetRepresentation(textRepresentation);
    textWidget->SetDefaultRenderer(renderers[i]);
    textWidget->SetInteractor(renderWindowInteractor);
    textWidget->SetTextActor(textActor);
    textWidget->SelectableOff();
    textWidget->ResizableOn();
    textWidgets.push_back(textWidget);
  }

  // Setup viewports for the renderers

  // Specify what borders will be drawn around each viewport.
  struct VP
  {
    // Viewport dimensions [xmin, ymin, xmax, ymax].
    std::array<double, 4> viewport{0.0, 0.0, 0.0, 0.0};
    // What borders will be drawn around the viewport.
    std::array<bool, 4> border{false, false, false, false};
  };
  std::map<unsigned int, VP> viewports;

  auto lastCol{false};
  auto lastRow{false};
  // auto blank = nameOrientation.size();

  for (auto row = 0; row < static_cast<int>(yGridDimensions); row++)
  {
    if (row == yGridDimensions - 1)
    {
      lastRow = true;
    }
    for (auto col = 0; col < static_cast<int>(xGridDimensions); col++)
    {
      if (col == xGridDimensions - 1)
      {
        lastCol = true;
      }
      auto index = row * xGridDimensions + col;

      // (xmin, ymin, xmax, ymax)
      std::array<double, 4> viewport = {
          static_cast<double>(col) * rendererSize /
              (xGridDimensions * rendererSize),
          static_cast<double>(yGridDimensions - (row + 1)) * rendererSize /
              (yGridDimensions * rendererSize),
          static_cast<double>(col + 1) * rendererSize /
              (xGridDimensions * rendererSize),
          static_cast<double>(yGridDimensions - row) * rendererSize /
              (yGridDimensions * rendererSize)};
      renderers[index]->SetViewport(viewport.data());

      // Decide what borders will be drawn around the viewport.
      std::array<bool, 4> border;
      if (lastRow && lastCol)
      {
        border = viewportBorderSpecifier.tlbr;
        lastRow = false;
        lastCol = false;
      }
      else if (lastCol)
      {
        border = viewportBorderSpecifier.rtl;
        lastCol = false;
      }
      else if (lastRow)
      {
        border = viewportBorderSpecifier.tlb;
      }
      else
      {
        border = viewportBorderSpecifier.tl;
      }
      viewports[index] = {viewport, border};
    }
  }
  for (size_t index = 0; index < renderers.size(); ++index)
  {
    auto borderActor = DrawViewportBorder(viewports[index].border, "White", 4);
    renderers[index]->AddViewProp(borderActor);
  }

  for (auto& tw : textWidgets)
  {
    tw->On();
  }

  renderWindow->Render();
  renderWindowInteractor->Start();

  return EXIT_SUCCESS;
}

namespace {
vtkNew<vtkActor2D> DrawViewportBorder(std::array<bool, 4> const& sides,
                                      std::string const& border_color,
                                      unsigned int const& border_width)
{
  vtkNew<vtkNamedColors> colors;

  // Points start at upper right and proceed anti-clockwise.
  vtkNew<vtkPoints> points;
  points->InsertPoint(0, 1, 1, 0);
  points->InsertPoint(1, 0, 1, 0);
  points->InsertPoint(2, 0, 0, 0);
  points->InsertPoint(3, 1, 0, 0);

  vtkNew<vtkCellArray> cells;

  if (sides[0])
  {
    // Top
    vtkNew<vtkPolyLine> top;
    top->GetPointIds()->SetNumberOfIds(2);
    top->GetPointIds()->SetId(0, 0);
    top->GetPointIds()->SetId(1, 1);
    cells->InsertNextCell(top);
  }
  if (sides[1])
  {
    // Left
    vtkNew<vtkPolyLine> left;
    left->GetPointIds()->SetNumberOfIds(2);
    left->GetPointIds()->SetId(0, 1);
    left->GetPointIds()->SetId(1, 2);
    cells->InsertNextCell(left);
  }
  if (sides[2])
  {
    // Bottom
    vtkNew<vtkPolyLine> bottom;
    bottom->GetPointIds()->SetNumberOfIds(2);
    bottom->GetPointIds()->SetId(0, 2);
    bottom->GetPointIds()->SetId(1, 3);
    cells->InsertNextCell(bottom);
  }
  if (sides[3])
  {
    // Right
    vtkNew<vtkPolyLine> right;
    right->GetPointIds()->SetNumberOfIds(2);
    right->GetPointIds()->SetId(0, 3);
    right->GetPointIds()->SetId(1, 0);
    cells->InsertNextCell(right);
  }

  // Now make the polydata and display it.
  vtkNew<vtkPolyData> poly;
  poly->SetPoints(points);
  poly->SetLines(cells);

  // Use normalized viewport coordinates since
  // they are independent of window size.
  vtkNew<vtkCoordinate> coordinate;
  coordinate->SetCoordinateSystemToNormalizedViewport();

  vtkNew<vtkPolyDataMapper2D> mapper;
  mapper->SetInputData(poly);
  mapper->SetTransformCoordinate(coordinate);

  vtkNew<vtkActor2D> actor;
  actor->SetMapper(mapper);
  actor->GetProperty()->SetColor(colors->GetColor3d(border_color).GetData());
  // Line width should be at least 2 to be visible at extremes.
  actor->GetProperty()->SetLineWidth(border_width);

  return actor;
}

TTextPositions GetTextPositions(std::vector<std::string> const& names,
                                int const justification,
                                int const vertical_justification,
                                double const width, double const height)
{
  // The gap between the left or right edge of the screen and the text.
  auto dx = 0.02;
  auto w = abs(width);
  if (w > 0.96)
  {
    w = 0.96;
  }

  auto y0 = 0.01;
  auto h = abs(height);
  if (h > 0.9)
  {
    h = 0.9;
  }
  auto dy = h;
  if (vertical_justification == VTK_TEXT_TOP)
  {
    y0 = 1.0 - (dy + y0);
  }
  if (vertical_justification == VTK_TEXT_CENTERED)
  {
    y0 = 0.5 - (dy / 2.0 + y0);
  }

  auto minmaxIt =
      std::minmax_element(names.begin(), names.end(),
                          [](const std::string& a, const std::string& b) {
                            return a.length() < b.length();
                          });

  // auto nameLenMin = minmaxIt.first->size();
  auto nameLenMax = minmaxIt.second->size();

  TTextPositions textPositions;
  for (const auto& k : names)
  {
    auto sz = k.size();
    auto delta_sz = w * sz / nameLenMax;
    if (delta_sz > w)
    {
      delta_sz = w;
    }

    double x0 = 0;
    if (justification == VTK_TEXT_CENTERED)
    {
      x0 = 0.5 - delta_sz / 2.0;
    }
    else if (justification == VTK_TEXT_RIGHT)
    {
      x0 = 1.0 - dx - delta_sz;
    }
    else
    {
      // Default is left justification.
      x0 = dx;
    }
    textPositions[k] = {{"p", {x0, y0}}, {"p2", {delta_sz, dy}}};
    // For testing.
    // std::cout << k << std::endl;
    // std::cout << "  p: " << textPositions[k]["p"][0] << ", "
    //           << textPositions[k]["p"][1] << std::endl;
    // std::cout << " p2: " << textPositions[k]["p2"][0] << ", "
    //           << textPositions[k]["p2"][1] << std::endl;
  }
  return textPositions;
}

void ChooseContrastingColor(double* rgbIn, double* rgbOut, double threshold,
                            const std::string& lightColor,
                            const std::string& darkColor)
{
  vtkNew<vtkNamedColors> colors;

  double hsv[3];
  // If the value is <= threshold, use a light color, otherwise use a dark
  // color.
  vtkMath::RGBToHSV(rgbIn, hsv);
  if (hsv[2] <= threshold)
  {
    colors->GetColor(lightColor.c_str(), rgbOut[0], rgbOut[1], rgbOut[2]);
  }
  else
  {
    colors->GetColor(darkColor.c_str(), rgbOut[0], rgbOut[1], rgbOut[2]);
  }
}

void RandomHSV(double hsv[3], double const& min_r, double const& max_r,
               vtkMinimalStandardRandomSequence* rng)
{
  for (auto i = 0; i < 3; ++i)
  {
    hsv[i] = rng->GetRangeValue(min_r, max_r);
    rng->Next();
  }
}

} // namespace

CMakeLists.txt

cmake_minimum_required(VERSION 3.12 FATAL_ERROR)

project(ChooseTextColorDemo)

find_package(VTK COMPONENTS 
  CommonColor
  CommonCore
  CommonDataModel
  InteractionStyle
  RenderingContextOpenGL2
  RenderingCore
  RenderingFreeType
  RenderingGL2PSOpenGL2
  RenderingOpenGL2
)

if (NOT VTK_FOUND)
  message(FATAL_ERROR "ChooseTextColorDemo: Unable to find the VTK build folder.")
endif()

# Prevent a "command line is too long" failure in Windows.
set(CMAKE_NINJA_FORCE_RESPONSE_FILE "ON" CACHE BOOL "Force Ninja to use response files.")
add_executable(ChooseTextColorDemo MACOSX_BUNDLE ChooseTextColorDemo.cxx )
  target_link_libraries(ChooseTextColorDemo PRIVATE ${VTK_LIBRARIES}
)
# vtk_module_autoinit is needed
vtk_module_autoinit(
  TARGETS ChooseTextColorDemo
  MODULES ${VTK_LIBRARIES}
)

Download and Build ChooseTextColorDemo

Click here to download ChooseTextColorDemo and its CMakeLists.txt file. Once the tarball ChooseTextColorDemo.tar has been downloaded and extracted,

cd ChooseTextColorDemo/build

If VTK is installed:

cmake ..

If VTK is not installed but compiled on your system, you will need to specify the path to your VTK build:

cmake -DVTK_DIR:PATH=/home/me/vtk_build ..

Build the project:

make

and run it:

./ChooseTextColorDemo

WINDOWS USERS

Be sure to add the VTK bin directory to your path. This will resolve the VTK dll's at run time.