Repository navigation
Expand file tree
/
Copy pathGradientComponent.h
More file actions
327 lines (279 loc) · 13 KB
/
Copy pathGradientComponent.h
File metadata and controls
327 lines (279 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#pragma once
#include <deki/providers/Buffer.h>
#include <stdint.h>
#include "deki-rendering/RendererComponent.h"
#include <deki/Color.h>
namespace Deki2D
{
enum class GradientType : uint8_t
{
Linear = 0, // Along a direction set by the angle
Radial = 1, // Out from the centre
Conical = 2 // Around the centre
};
enum class GradientTileMode : uint8_t
{
None = 0, // Drawn once
Horizontal = 1,
Vertical = 2,
Both = 3,
Mirror = 4 // Every other tile is reversed
};
enum class GradientDitherMode : uint8_t
{
None = 0,
Ordered2x2 = 1, // 2x2 Bayer matrix (coarse pattern)
Ordered4x4 = 2, // 4x4 Bayer matrix
Ordered8x8 = 3, // 8x8 Bayer matrix
Ordered16x16 = 4 // 16x16 Bayer matrix (finest pattern)
};
/// One colour stop of a gradient.
struct GradientStop
{
float position; // Along the gradient, 0 to 1
Deki::Color color;
GradientStop(float pos = 0.0f, const Deki::Color& col = Deki::Color::Black)
: position(pos),
color(col)
{
}
// From separate 0-255 channels
GradientStop(float pos, uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = 255)
: position(pos),
color(red, green, blue, alpha)
{
}
};
/// Draws a procedural gradient: linear, radial or conical, with optional
/// tiling, and dithering to hide banding on RGB565 displays. The gradient is
/// baked into a pixel buffer and re-baked only when an input changes.
DEKI_CATEGORY("2D")
DEKI_DESCRIPTION("Draws a procedural gradient: linear, radial or conical.")
class GradientComponent : public DekiRendering::RendererComponent
{
public:
DEKI_EXPORT
DEKI_TOOLTIP("Linear runs the colours along the angle below. Radial runs them out from the centre point.")
GradientType gradientType;
DEKI_EXPORT
DEKI_TOOLTIP("What happens past the last stop: hold the end colour, repeat the ramp, or mirror it back.")
GradientTileMode tileMode;
DEKI_EXPORT
DEKI_TOOLTIP(
"Dithering hides the banding a smooth ramp shows on a 16-bit display, by trading it for a fine speckle.")
GradientDitherMode ditherMode;
// Size of one Bayer cell, in art pixels. 1 (the default) dithers pixel by
// pixel; 2, 4, 8 or 16 make chunkier blocks for a retro look. Other values
// snap down to a power of two (3 becomes 2, 7 becomes 4).
DEKI_EXPORT
DEKI_TOOLTIP("Size of the dither pattern in art pixels. 1 dithers pixel by pixel; 2, 4, 8 or 16 make chunkier "
"blocks for a retro look.")
uint8_t ditherScale;
DEKI_EXPORT
DEKI_TOOLTIP("Width of the gradient in meters.")
DEKI_UNIT(Distance)
float width;
DEKI_EXPORT
DEKI_TOOLTIP("Height of the gradient in meters.")
DEKI_UNIT(Distance)
float height;
DEKI_EXPORT
DEKI_TOOLTIP("Direction a linear gradient runs, in radians. 0 runs left to right.")
DEKI_UNIT(Angle)
float angle; // Radians (0 = horizontal, pi/2 = vertical); the inspector shows degrees
DEKI_EXPORT
DEKI_TOOLTIP("Centre of a radial gradient across the width, 0 to 1. 0.5 is the middle.")
float centerX; // Radial and conical, 0 to 1
DEKI_EXPORT
DEKI_TOOLTIP("Centre of a radial gradient down the height, 0 to 1.")
float centerY; // Radial and conical, 0 to 1
DEKI_EXPORT
DEKI_TOOLTIP("How far a radial gradient reaches before the last stop, relative to the size.")
float radius; // Radial, 0 to 1
// At most 4 stops, to keep the component small
static constexpr uint8_t kMaxStops = 4;
GradientStop stops[kMaxStops];
DEKI_EXPORT
DEKI_TOOLTIP("How many of the four colour stops are used. The rest are ignored.")
uint8_t stopCount;
// Each stop as separate properties, for the inspector and serialization.
// Stop 1 is always shown: a gradient needs at least one stop.
DEKI_EXPORT
DEKI_TOOLTIP("Where the first stop sits along the ramp, 0 to 1.")
float stop1Position;
DEKI_EXPORT
DEKI_TOOLTIP("Colour at the first stop.")
Deki::Color stop1Color;
DEKI_EXPORT
DEKI_TOOLTIP("Where the second stop sits along the ramp, 0 to 1.")
DEKI_VISIBLE_WHEN(stopCount, 2)
float stop2Position;
DEKI_EXPORT
DEKI_TOOLTIP("Colour at the second stop.")
DEKI_VISIBLE_WHEN(stopCount, 2)
Deki::Color stop2Color;
DEKI_EXPORT
DEKI_TOOLTIP("Where the third stop sits, 0 to 1. Used when the stop count is 3 or more.")
DEKI_VISIBLE_WHEN(stopCount, 3)
float stop3Position;
DEKI_EXPORT
DEKI_TOOLTIP("Colour at the third stop.")
DEKI_VISIBLE_WHEN(stopCount, 3)
Deki::Color stop3Color;
DEKI_EXPORT
DEKI_TOOLTIP("Where the fourth stop sits, 0 to 1. Used when the stop count is 4.")
DEKI_VISIBLE_WHEN(stopCount, 4)
float stop4Position;
DEKI_EXPORT
DEKI_TOOLTIP("Colour at the fourth stop.")
DEKI_VISIBLE_WHEN(stopCount, 4)
Deki::Color stop4Color;
// Tile size in world meters; 0 uses the component's width or height
DEKI_EXPORT
DEKI_TOOLTIP("Width of one repeat when the tile mode repeats or mirrors.")
DEKI_UNIT(Distance)
float tileWidth;
DEKI_EXPORT
DEKI_TOOLTIP("Height of one repeat when the tile mode repeats or mirrors.")
DEKI_UNIT(Distance)
float tileHeight;
GradientComponent(float w = 0.0f, float h = 0.0f);
~GradientComponent();
void SetGradientType(GradientType type);
/// Makes this a linear gradient at `angleRadians` (0 runs left to right,
/// pi/2 top to bottom).
void SetLinearGradient(float angleRadians = 0.0f);
/// Makes this a radial gradient. Centre and radius are 0 to 1, relative to
/// the size, and are clamped to that range.
void SetRadialGradient(float centerX = 0.5f, float centerY = 0.5f, float radius = 0.5f);
/// Adds a colour stop at `position` (0 to 1) and keeps the stops sorted.
/// Does nothing when all four stops are in use.
void AddColorStop(float position, uint8_t r, uint8_t g, uint8_t b);
void ClearColorStops();
/// Copies the stopN properties into stops[]. Runs before every bake.
void SyncStopsFromProperties();
/// Replaces the stops with two: the start colour at 0 and the end colour
/// at 1.
void SetSimpleGradient(uint8_t startR, uint8_t startG, uint8_t startB, uint8_t endR, uint8_t endG, uint8_t endB);
/// Sets the tiling mode and tile size in world meters (0 uses the
/// component's width or height).
void SetTiling(GradientTileMode mode, float tileW = 0.0f, float tileH = 0.0f);
void SetDithering(GradientDitherMode mode);
/// Sets the size, in world meters.
void SetArea(float w, float h);
/// Bakes the gradient into an RGB565 buffer of outW x outH pixels
/// (outW * outH * 2 bytes).
///
/// The gradient is laid out on its art grid (artW x artH: its size at the
/// project's pixels per meter); each output pixel takes the art pixel it
/// covers, colour and dither alike: the Bayer pattern is laid out on the
/// art grid too, in cells `ditherArt` art pixels wide. Every art pixel is
/// one colour, so a bake as big as the art or bigger is the device's
/// picture magnified, and RenderContent bakes it at art size and lets
/// QuadBlit magnify it: the pattern never changes as the editor zooms in,
/// it only grows. (Laid out in output pixels instead, its cells were
/// rounded to whole output pixels while the art pixels were not, and the
/// two grids drifted apart at every zoom between whole numbers.)
///
/// A bake smaller than the art (drawn shrunk) cannot show every art
/// pixel, so the pattern is laid out on the output grid instead, in cells
/// `ditherArt` art pixels wide rounded to whole output pixels: a regular
/// stipple rather than art pixels dropped unevenly.
///
/// Returns false, with nothing drawn, when there is nothing to draw or no
/// room for its table of columns (one float per art column).
bool RenderToBuffer(uint8_t* buffer, int32_t outW, int32_t outH, int32_t artW, int32_t artH, int32_t ditherArt);
/// Whether every art row of an artW x artH bake is one colour
/// (`sameAcross`) or every art column is (`sameDown`). Worked out from
/// the positions, not the angle, so it holds pixel for pixel; only an
/// undithered linear gradient can be either. RenderContent then bakes one
/// column or one row and lets QuadBlit stretch it.
void FindStraightAxes(int32_t artW, int32_t artH, bool& sameAcross, bool& sameDown);
// Drawn through QuadBlit.
// Culling extents in meters: the box the content is baked into.
bool GetContentExtents(float& outWidth, float& outHeight) const override
{
if (width <= 0.0f || height <= 0.0f)
{
return false;
}
outWidth = width;
outHeight = height;
return true;
}
/// Changes whenever the bake is filled again in place.
bool GetContentVersion(uint32_t& outVersion) const override
{
outVersion = m_BakeVersion;
return true;
}
bool RenderContent(const Deki::Object* owner, QuadBlit::Source& outSource, float& outPivotX, float& outPivotY,
uint8_t& outTintR, uint8_t& outTintG, uint8_t& outTintB, uint8_t& outTintA) override;
private:
/// The position along the gradient (0 to 1) of a point given in 0 to 1
/// coordinates across the area.
float CalculateGradientPosition(float normX, float normY) const;
/// The colour at `position` (0 to 1), blended between the two stops
/// around it.
void InterpolateColor(float position, uint8_t* r, uint8_t* g, uint8_t* b) const;
/// The Bayer threshold at (x, y) for the current ditherMode, in [0, 1).
float SampleBayerThreshold(int32_t x, int32_t y) const;
/// Pixelorama-style stipple: picks one of the two stops around `position`,
/// by whether the position between them reaches `threshold`. Every output
/// pixel is exactly one of the stop colours.
void PickStopByThreshold(float position, float threshold, uint8_t* r, uint8_t* g, uint8_t* b) const;
uint16_t ConvertToRGB565(uint8_t r, uint8_t g, uint8_t b) const;
/// Writes one pixel, skipping it when it is outside the buffer.
void RenderPixel(int32_t x, int32_t y, uint16_t color, uint8_t* renderBuffer, int screenWidth,
int screenHeight) const;
// Baked pixels for the current properties, which RenderContent hands to
// QuadBlit. Keyed by a hash of every input, so the gradient is rasterised
// (with trig per pixel) only when something changed.
Deki::Buffer<uint8_t> m_Baked;
uint32_t m_BakeVersion = 0; // GetContentVersion and Source::revision: QuadBlit::NextRevision() each bake
uint64_t m_BakeKey = 0;
// The bake's size in pixels: the drawn size, or 1 across or down for a
// gradient that changes along one axis only (FindStraightAxes).
int32_t m_BakeWidth = 0;
int32_t m_BakeHeight = 0;
// A bake the device could not make. Retrying it every frame achieves
// nothing and floods the log, so it is attempted once and only
// reconsidered when an input changes.
bool m_BakeFailed = false;
uint64_t m_BakeFailedKey = 0;
uint64_t ComputeBakeKey(int32_t widthPx, int32_t heightPx) const;
// Sets up what a bake reads: the stops from their properties, the angle's
// cos/sin and the linear projection's range.
void PrepareBake();
// A tile's size in art pixels, or the art's when the tile size is 0.
void TileSizes(int32_t artW, int32_t artH, int32_t& tileW, int32_t& tileH) const;
// Where art column or row `i` sits across the gradient, 0 to 1, tiles
// included (`full` art pixels in all, `tile` to a tile).
float NormAlong(int32_t i, int32_t full, int32_t tile, bool tiled) const;
// What the bake turned out to be, checked on its pixels after each bake:
// every row one colour (a vertical gradient), or every row the same (a
// horizontal one). QuadBlit then fills or repeats rows instead of reading
// the whole bake out of PSRAM every frame.
bool m_UniformRows = false;
bool m_UniformColumns = false;
// With uniform rows, each row's colour in internal memory
// (QuadBlit::Source::rowColors), when there is room for it.
Deki::Buffer<uint16_t> m_RowColors;
void CheckBakeUniformity(int32_t widthPx, int32_t heightPx);
// Drops QuadBlit's scaled copies of the bake, before it changes or goes.
void ForgetBake();
// Linear-gradient trig, computed once per bake instead of once per pixel,
// and where the projection on the angle runs over the unit square: from
// m_LinearLow, m_LinearRange long. m_LinearInverse is 1 / m_LinearRange
// when that is exact (the range a power of two), else 0.
float m_CosAngle = 1.0f;
float m_SinAngle = 0.0f;
float m_LinearLow = 0.0f;
float m_LinearRange = 1.0f;
float m_LinearInverse = 1.0f;
// Copies stops[] back into the stopN properties after a change from code,
// so SyncStopsFromProperties(), which runs before every bake, does not
// overwrite AddColorStop()/SetSimpleGradient() with stale values.
void WriteStopsToProperties();
};
} // namespace Deki2D