FTXUI  5.0.0
C++ functional terminal UI.
linear_gradient.cpp
Go to the documentation of this file.
1 // Copyright 2023 Arthur Sonzogni. All rights reserved.
2 // Use of this source code is governed by the MIT license that can be found in
3 // the LICENSE file.
4 #include <algorithm> // for max, min, sort, copy
5 #include <cmath> // for fmod, cos, sin
6 #include <cstddef> // for size_t
7 #include <ftxui/dom/linear_gradient.hpp> // for LinearGradient::Stop, LinearGradient
8 #include <memory> // for allocator_traits<>::value_type, make_shared
9 #include <optional> // for optional, operator!=, operator<
10 #include <utility> // for move
11 #include <vector> // for vector
12 
13 #include "ftxui/dom/elements.hpp" // for Element, Decorator, bgcolor, color
14 #include "ftxui/dom/node_decorator.hpp" // for NodeDecorator
15 #include "ftxui/screen/box.hpp" // for Box
16 #include "ftxui/screen/color.hpp" // for Color, Color::Default, Color::Blue
17 #include "ftxui/screen/screen.hpp" // for Pixel, Screen
18 
19 namespace ftxui {
20 namespace {
21 
22 struct LinearGradientNormalized {
23  float angle = 0.F;
24  std::vector<Color> colors;
25  std::vector<float> positions; // Sorted.
26 };
27 
28 // Convert a LinearGradient to a normalized version.
29 LinearGradientNormalized Normalize(LinearGradient gradient) {
30  // Handle gradient of size 0.
31  if (gradient.stops.empty()) {
32  return LinearGradientNormalized{
33  0.F,
35  {0.F, 1.F},
36  };
37  }
38 
39  // Fill in the two extent, if not provided.
40  if (!gradient.stops.front().position) {
41  gradient.stops.front().position = 0.F;
42  }
43  if (!gradient.stops.back().position) {
44  gradient.stops.back().position = 1.F;
45  }
46 
47  // Fill in the blank, by interpolating positions.
48  size_t last_checkpoint = 0;
49  for (size_t i = 1; i < gradient.stops.size(); ++i) {
50  if (!gradient.stops[i].position) {
51  continue;
52  }
53 
54  if (i - last_checkpoint >= 2) {
55  const float min = gradient.stops[i].position.value(); // NOLINT
56  const float max =
57  gradient.stops[last_checkpoint].position.value(); // NOLINT
58  for (size_t j = last_checkpoint + 1; j < i; ++j) {
59  gradient.stops[j].position = min + (max - min) *
60  float(j - last_checkpoint) /
61  float(i - last_checkpoint);
62  }
63  }
64 
65  last_checkpoint = i;
66  }
67 
68  // Sort the stops by position.
69  std::sort(
70  gradient.stops.begin(), gradient.stops.end(),
71  [](const auto& a, const auto& b) { return a.position < b.position; });
72 
73  // If we don't being with zero, add a stop at zero.
74  if (gradient.stops.front().position != 0) {
75  gradient.stops.insert(gradient.stops.begin(),
76  {gradient.stops.front().color, 0.F});
77  }
78  // If we don't end with one, add a stop at one.
79  if (gradient.stops.back().position != 1) {
80  gradient.stops.push_back({gradient.stops.back().color, 1.F});
81  }
82 
83  // Normalize the angle.
84  LinearGradientNormalized normalized;
85  const float modulo = 360.F;
86  normalized.angle =
87  std::fmod(std::fmod(gradient.angle, modulo) + modulo, modulo);
88  for (auto& stop : gradient.stops) {
89  normalized.colors.push_back(stop.color);
90  // NOLINTNEXTLINE
91  normalized.positions.push_back(stop.position.value());
92  }
93  return normalized;
94 }
95 
96 Color Interpolate(const LinearGradientNormalized& gradient, float t) {
97  // Find the right color in the gradient's stops.
98  size_t i = 1;
99  while (true) {
100  if (i > gradient.positions.size()) {
101  const float half = 0.5F;
102  return Color::Interpolate(half, gradient.colors.back(),
103  gradient.colors.back());
104  }
105  if (t <= gradient.positions[i]) {
106  break;
107  }
108  ++i;
109  }
110 
111  const float t0 = gradient.positions[i - 1];
112  const float t1 = gradient.positions[i - 0];
113  const float tt = (t - t0) / (t1 - t0);
114 
115  const Color& c0 = gradient.colors[i - 1];
116  const Color& c1 = gradient.colors[i - 0];
117  const Color& cc = Color::Interpolate(tt, c0, c1);
118 
119  return cc;
120 }
121 
122 class LinearGradientColor : public NodeDecorator {
123  public:
124  explicit LinearGradientColor(Element child,
125  const LinearGradient& gradient,
126  bool background_color)
127  : NodeDecorator(std::move(child)),
128  gradient_(Normalize(gradient)),
129  background_color_{background_color} {}
130 
131  private:
132  void Render(Screen& screen) override {
133  const float degtorad = 0.01745329251F;
134  const float dx = std::cos(gradient_.angle * degtorad);
135  const float dy = std::sin(gradient_.angle * degtorad);
136 
137  // Project every corner to get the extent of the gradient.
138  const float p1 = float(box_.x_min) * dx + float(box_.y_min) * dy;
139  const float p2 = float(box_.x_min) * dx + float(box_.y_max) * dy;
140  const float p3 = float(box_.x_max) * dx + float(box_.y_min) * dy;
141  const float p4 = float(box_.x_max) * dx + float(box_.y_max) * dy;
142  const float min = std::min({p1, p2, p3, p4});
143  const float max = std::max({p1, p2, p3, p4});
144 
145  // Renormalize the projection to [0, 1] using the extent and projective
146  // geometry.
147  const float dX = dx / (max - min);
148  const float dY = dy / (max - min);
149  const float dZ = -min / (max - min);
150 
151  // Project every pixel to get the color.
152  if (background_color_) {
153  for (int y = box_.y_min; y <= box_.y_max; ++y) {
154  for (int x = box_.x_min; x <= box_.x_max; ++x) {
155  const float t = float(x) * dX + float(y) * dY + dZ;
156  screen.PixelAt(x, y).background_color = Interpolate(gradient_, t);
157  }
158  }
159  } else {
160  for (int y = box_.y_min; y <= box_.y_max; ++y) {
161  for (int x = box_.x_min; x <= box_.x_max; ++x) {
162  const float t = float(x) * dX + float(y) * dY + dZ;
163  screen.PixelAt(x, y).foreground_color = Interpolate(gradient_, t);
164  }
165  }
166  }
167 
168  NodeDecorator::Render(screen);
169  }
170 
171  LinearGradientNormalized gradient_;
172  bool background_color_;
173 };
174 
175 } // namespace
176 
177 /// @brief Build the "empty" gradient. This is often followed by calls to
178 /// LinearGradient::Angle() and LinearGradient::Stop().
179 /// Example:
180 /// ```cpp
181 /// auto gradient =
182 /// LinearGradient()
183 /// .Angle(45)
184 /// .Stop(Color::Red, 0.0)
185 /// .Stop(Color::Green, 0.5)
186 /// .Stop(Color::Blue, 1.0);;
187 /// ```
188 /// @ingroup dom
190 
191 /// @brief Build a gradient with two colors.
192 /// @param begin The color at the beginning of the gradient.
193 /// @param end The color at the end of the gradient.
194 /// @ingroup dom
196  : LinearGradient(0, begin, end) {}
197 
198 /// @brief Build a gradient with two colors and an angle.
199 /// @param a The angle of the gradient.
200 /// @param begin The color at the beginning of the gradient.
201 /// @param end The color at the end of the gradient.
202 /// @ingroup dom
203 LinearGradient::LinearGradient(float a, Color begin, Color end) : angle(a) {
204  stops.push_back({begin, {}});
205  stops.push_back({end, {}});
206 }
207 
208 /// @brief Set the angle of the gradient.
209 /// @param a The angle of the gradient.
210 /// @return The gradient.
211 /// @ingroup dom
213  angle = a;
214  return *this;
215 }
216 
217 /// @brief Add a color stop to the gradient.
218 /// @param c The color of the stop.
219 /// @param p The position of the stop.
220 /// @return The gradient.
222  stops.push_back({c, p});
223  return *this;
224 }
225 
226 /// @brief Add a color stop to the gradient.
227 /// @param c The color of the stop.
228 /// @return The gradient.
229 /// @ingroup dom
230 /// @note The position of the stop is interpolated from nearby stops.
232  stops.push_back({c, {}});
233  return *this;
234 }
235 
236 /// @brief Set the foreground color of an element with linear-gradient effect.
237 /// @param gradient The gradient effect to be applied on the output element.
238 /// @param child The input element.
239 /// @return The output element colored.
240 /// @ingroup dom
241 ///
242 /// ### Example
243 ///
244 /// ```cpp
245 /// color(LinearGradient{0, {Color::Red, Color::Blue}}, text("Hello"))
246 /// ```
247 Element color(const LinearGradient& gradient, Element child) {
248  return std::make_shared<LinearGradientColor>(std::move(child), gradient,
249  /*background_color*/ false);
250 }
251 
252 /// @brief Set the background color of an element with linear-gradient effect.
253 /// @param gradient The gradient effect to be applied on the output element.
254 /// @param child The input element.
255 /// @return The output element colored.
256 /// @ingroup dom
257 ///
258 /// ### Example
259 ///
260 /// ```cpp
261 /// bgcolor(LinearGradient{0, {Color::Red, Color::Blue}}, text("Hello"))
262 /// ```
263 Element bgcolor(const LinearGradient& gradient, Element child) {
264  return std::make_shared<LinearGradientColor>(std::move(child), gradient,
265  /*background_color*/ true);
266 }
267 
268 /// @brief Decorate using a linear-gradient effect on the foreground color.
269 /// @param gradient The gradient effect to be applied on the output element.
270 /// @return The Decorator applying the color.
271 /// @ingroup dom
272 ///
273 /// ### Example
274 ///
275 /// ```cpp
276 /// text("Hello") | color(LinearGradient{0, {Color::Red, Color::Blue}})
277 /// ```
278 Decorator color(const LinearGradient& gradient) {
279  return
280  [gradient](Element child) { return color(gradient, std::move(child)); };
281 }
282 
283 /// @brief Decorate using a linear-gradient effect on the background color.
284 /// @param gradient The gradient effect to be applied on the output element.
285 /// @return The Decorator applying the color.
286 /// @ingroup dom
287 ///
288 /// ### Example
289 ///
290 /// ```cpp
291 /// text("Hello") | color(LinearGradient{0, {Color::Red, Color::Blue}})
292 /// ```
293 Decorator bgcolor(const LinearGradient& gradient) {
294  return
295  [gradient](Element child) { return bgcolor(gradient, std::move(child)); };
296 }
297 
298 } // namespace ftxui
A class representing terminal colors.
Definition: color.hpp:20
static Color Interpolate(float t, const Color &a, const Color &b)
Definition: color.cpp:175
virtual void Render(Screen &screen)
Display an element on a ftxui::Screen.
Definition: node.cpp:32
Decorator bgcolor(Color)
Decorate using a background color.
Definition: color.cpp:105
std::function< Element(Element)> Decorator
Definition: elements.hpp:24
std::shared_ptr< Node > Element
Definition: elements.hpp:22
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
Definition: node.cpp:47
Decorator color(Color)
Decorate using a foreground color.
Definition: color.cpp:91
A class representing the settings for linear-gradient color effect.
LinearGradient & Stop(Color color, float position)
Add a color stop to the gradient.
LinearGradient & Angle(float angle)
Set the angle of the gradient.
LinearGradient()
Build the "empty" gradient. This is often followed by calls to LinearGradient::Angle() and LinearGrad...
std::vector< Stop > stops