1 /*
2 * Copyright 2024 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "include/core/SkCanvas.h"
9 #include "include/core/SkColor.h"
10 #include "include/core/SkCubicMap.h"
11 #include "include/core/SkFont.h"
12 #include "include/core/SkPicture.h"
13 #include "include/core/SkPictureRecorder.h"
14 #include "include/core/SkPoint.h"
15 #include "include/core/SkRefCnt.h"
16 #include "include/core/SkShader.h"
17 #include "include/core/SkString.h"
18 #include "include/core/SkVertices.h"
19 #include "include/effects/SkRuntimeEffect.h"
20 #include "include/private/base/SkDebug.h"
21 #include "src/base/SkRandom.h"
22 #include "tools/fonts/FontToolUtils.h"
23 #include "tools/viewer/Slide.h"
24
25 #include <cmath>
26 #include <cstddef>
27 #include <limits>
28 #include <vector>
29
30 #include "delaunator.hpp"
31 #include "imgui.h"
32
33 namespace {
34
triangulate_pts(const std::vector<SkPoint> & pts,const std::vector<SkColor> & colors)35 sk_sp<SkVertices> triangulate_pts(const std::vector<SkPoint>& pts, const std::vector<SkColor>& colors) {
36 // put points in the format delaunator wants
37 std::vector<double> coords;
38 for (size_t i = 0; i < pts.size(); ++i) {
39 coords.push_back(pts[i].x());
40 coords.push_back(pts[i].y());
41 }
42
43 // triangulation happens here
44 delaunator::Delaunator d(coords);
45
46 // SkVertices parameters
47 std::vector<SkPoint> vertices;
48 std::vector<uint16_t> indices;
49
50 // populate vertices & colors
51 for(std::size_t i = 0; i < d.coords.size(); i+=2) {
52 vertices.push_back(SkPoint::Make(d.coords[i], d.coords[i+1]));
53 }
54
55 // populate triangle indices
56 for(std::size_t i = 0; i < d.triangles.size(); i+=3) {
57 indices.push_back(d.triangles[i]);
58 indices.push_back(d.triangles[i+1]);
59 indices.push_back(d.triangles[i+2]);
60 }
61
62 return SkVertices::MakeCopy(SkVertices::kTriangles_VertexMode, vertices.size(), vertices.data(), nullptr, colors.data(), indices.size(), indices.data());
63 }
64
makeGradientShader(int w,int h,std::vector<SkPoint> & vertices,std::vector<SkColor> & colors)65 sk_sp<SkShader> makeGradientShader(int w, int h, std::vector<SkPoint>& vertices, std::vector<SkColor>& colors) {
66 vertices.push_back(SkPoint::Make(0.f, 0.f));
67 vertices.push_back(SkPoint::Make(w, 0.f));
68 vertices.push_back(SkPoint::Make(0.f, h));
69 vertices.push_back(SkPoint::Make(w, h));
70
71 colors.push_back(SK_ColorTRANSPARENT);
72 colors.push_back(SK_ColorTRANSPARENT);
73 colors.push_back(SK_ColorTRANSPARENT);
74 colors.push_back(SK_ColorTRANSPARENT);
75
76 sk_sp<SkVertices> sk_vertices = triangulate_pts(vertices, colors);
77
78 // record with a picture
79 SkRect tile = SkRect::MakeWH(w, h);
80 SkPictureRecorder recorder;
81 SkCanvas* c = recorder.beginRecording(tile);
82
83 SkPaint p;
84 p.setColor(SK_ColorWHITE);
85 c->drawVertices(sk_vertices, SkBlendMode::kModulate, p);
86 sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture());
87
88 return picture->makeShader(SkTileMode::kDecal, SkTileMode::kDecal, SkFilterMode::kNearest);
89 }
90
91 class GradientRenderer : public SkRefCnt {
92 public:
93 virtual void draw(SkCanvas*) const = 0;
94
95 virtual sk_sp<SkShader> asShader() const = 0;
96
97 virtual void updateVertices(SkSpan<const SkPoint> vert_pos,
98 SkSpan<const SkColor4f> vert_colors) = 0;
99 };
100
101 class SkSlRenderer : public GradientRenderer {
102 public:
draw(SkCanvas * canvas) const103 void draw(SkCanvas* canvas) const override {
104 SkPaint paint;
105 paint.setShader(fShader);
106 canvas->drawRect(SkRect::MakeWH(1, 1), paint);
107 }
108
asShader() const109 sk_sp<SkShader> asShader() const override { return fShader; }
110
updateVertices(SkSpan<const SkPoint> vert_pos,SkSpan<const SkColor4f> vert_colors)111 void updateVertices(SkSpan<const SkPoint> vert_pos,
112 SkSpan<const SkColor4f> vert_colors) override {
113 SkASSERT(vert_pos.size() == vert_colors.size());
114 const auto vert_count = vert_pos.size();
115
116 if (!vert_count) {
117 return;
118 }
119
120 // Effect compilation is expensive, so we cache and only recompile when the count changes.
121 if (vert_count != fCachedCount) {
122 this->buildEffect(vert_count);
123 fCachedCount = vert_count;
124 }
125
126 SkRuntimeEffectBuilder builder(fEffect);
127 builder.uniform("u_vertcolors").set(vert_colors.data(), vert_colors.size());
128 builder.uniform("u_vertpos") .set(vert_pos.data() , vert_pos.size());
129
130 fShader = builder.makeShader();
131 }
132 virtual void buildEffect(size_t vert_count) = 0;
133
134 protected:
135 sk_sp<SkRuntimeEffect> fEffect;
136 sk_sp<SkShader> fShader;
137
138 size_t fCachedCount = 0;
139 };
140
141 class AEGradientRenderer final : public SkSlRenderer {
142 public:
buildEffect(size_t vert_count)143 void buildEffect(size_t vert_count) override {
144 static constexpr char gAEGradientSkSL[] =
145 "uniform half4 u_vertcolors[%zu];"
146 "uniform float2 u_vertpos[%zu];"
147
148 "half4 main(float2 xy) {"
149 "half4 c = half4(0);"
150 "float w_acc = 0;"
151
152 "for (int i = 0; i < %zu; ++i) {"
153 "float d = distance(xy, u_vertpos[i]);"
154 "float w = 1 / (d * d);"
155
156 "c += u_vertcolors[i] * w;"
157 "w_acc += w;"
158 "}"
159
160 "return c / w_acc;"
161 "}";
162
163 const auto res = SkRuntimeEffect::MakeForShader(
164 SkStringPrintf(gAEGradientSkSL, vert_count, vert_count, vert_count));
165 if (!res.effect) {
166 SkDEBUGF("%s\n", res.errorText.c_str());
167 }
168
169 fEffect = res.effect;
170
171 SkASSERT(fEffect);
172 }
173 };
174
175 class LinearGradientRenderer final : public SkSlRenderer {
176 public:
buildEffect(size_t vert_count)177 void buildEffect(size_t vert_count) override {
178 static constexpr char sksl[] =
179 "uniform half4 u_vertcolors[%zu];"
180 "uniform float2 u_vertpos[%zu];"
181
182 "half4 main(float2 xy) {"
183 "float v[%zu];"
184 "for (int i = 0; i < %zu; i++) {"
185 "v[i] = 1.;"
186 "}"
187
188 "for (int i = 0; i < %zu; ++i) {"
189 "for (int j = 0; j < %zu; ++j) {"
190 "vec2 delta;"
191 "delta.x = u_vertpos[j].x - u_vertpos[i].x;"
192 "delta.y = u_vertpos[j].y - u_vertpos[i].y;"
193
194 "mat3 m = mat3 ("
195 "delta.x, delta.y, 0.," // 1st column
196 "-delta.y, delta.x, 0.," // 2nd column
197 "u_vertpos[i].x, u_vertpos[i].y, 1." // 3rd column
198 ");"
199 "mat3 m_inv = inverse(m);"
200
201 "vec3 p_h = vec3(xy.x, xy.y, 1.);"
202 "vec3 u = m_inv*p_h;"
203 "float t = u.x;"
204
205 "if (t < 0) {"
206 "v[j] = 0;"
207 "} else if (t > 1) {"
208 "v[i] = 0;"
209 "} else {"
210 "v[i] *= 1-t;"
211 "v[j] *= t;"
212 "}"
213 "}"
214 "}"
215
216 "half4 c = half4(0);"
217 "float w_acc = 0;"
218 "for (int i = 0; i < %zu; i++) {"
219 "c += u_vertcolors[i] * v[i];"
220 "w_acc += v[i];"
221 "}"
222
223 "return c / w_acc;"
224 "}";
225
226 const auto res = SkRuntimeEffect::MakeForShader(
227 SkStringPrintf(sksl, vert_count, vert_count, vert_count, vert_count, vert_count, vert_count, vert_count));
228 if (!res.effect) {
229 SkDEBUGF("%s\n", res.errorText.c_str());
230 }
231
232 fEffect = res.effect;
233
234 SkASSERT(fEffect);
235 }
236 };
237
238 class IllGradientRenderer final : public SkSlRenderer {
239 public:
buildEffect(size_t vert_count)240 void buildEffect(size_t vert_count) override {
241 static constexpr char sksl[] =
242 "uniform half4 u_vertcolors[%zu];"
243 "uniform float2 u_vertpos[%zu];"
244
245 "half4 main(float2 xy) {"
246 "float d[%zu];"
247 "for (int i = 0; i < %zu; i++) {"
248 "d[i] = 0.;"
249 "}"
250
251 "for (int i = 0; i < %zu; ++i) {"
252 "for (int j = 0; j < %zu; ++j) {"
253 "vec2 delta;"
254 "delta.x = u_vertpos[j].x - u_vertpos[i].x;"
255 "delta.y = u_vertpos[j].y - u_vertpos[i].y;"
256
257 "mat3 m = mat3 ("
258 "delta.x, delta.y, 0.," // 1st column
259 "-delta.y, delta.x, 0.," // 2nd column
260 "u_vertpos[i].x, u_vertpos[i].y, 1." // 3rd column
261 ");"
262 "mat3 m_inv = inverse(m);"
263
264 "vec3 p_h = vec3(xy.x, xy.y, 1.);"
265 "vec3 u = m_inv*p_h;"
266 "float t = u.x;"
267
268 "float s = length(delta);"
269 "if (t < 0) {"
270 "d[i] += s*abs(u.y);"
271 "d[j] += s*distance(vec2(u.x, u.y), vec2(1., 0.));"
272 "} else if (t > 1) {"
273 "d[j] += s*abs(u.y);"
274 "d[i] += s*distance(vec2(u.x, u.y), vec2(0., 0.));"
275 "} else {"
276 "d[i] += s*distance(vec2(u.x, u.y), vec2(0., 0.));"
277 "d[j] += s*distance(vec2(u.x, u.y), vec2(1., 0.));"
278 "}"
279 "}"
280 "}"
281
282 "half4 c = half4(0);"
283 "float w_acc = 0;"
284 "for (int i = 0; i < %zu; i++) {"
285 "float w = 1 / (d[i] * d[i]);"
286 "c += u_vertcolors[i] * w;"
287 "w_acc += w;"
288 "}"
289
290 "return c / w_acc;"
291 "}";
292
293 const auto res = SkRuntimeEffect::MakeForShader(
294 SkStringPrintf(sksl, vert_count, vert_count, vert_count, vert_count, vert_count, vert_count, vert_count));
295 if (!res.effect) {
296 SkDEBUGF("%s\n", res.errorText.c_str());
297 }
298
299 fEffect = res.effect;
300
301 SkASSERT(fEffect);
302 }
303 };
304
305 class TriangulatedGradientRenderer final : public GradientRenderer {
306 public:
draw(SkCanvas * canvas) const307 void draw(SkCanvas* canvas) const override {
308 SkPaint paint;
309 paint.setShader(fShader);
310 canvas->drawRect(SkRect::MakeWH(1, 1), paint);
311 }
312
asShader() const313 sk_sp<SkShader> asShader() const override { return fShader; }
314
updateVertices(SkSpan<const SkPoint> vert_pos,SkSpan<const SkColor4f> vert_colors)315 void updateVertices(SkSpan<const SkPoint> vert_pos,
316 SkSpan<const SkColor4f> vert_colors) override {
317 SkASSERT(vert_pos.size() == vert_colors.size());
318 const auto vert_count = vert_pos.size();
319
320 if (!vert_count) {
321 return;
322 }
323
324 std::vector<SkPoint> pos;
325 for (auto& p : vert_pos) {
326 pos.push_back(p);
327 }
328 std::vector<SkColor> colors;
329 for (auto& c : vert_colors) {
330 colors.push_back(c.toSkColor());
331 }
332
333 fShader = makeGradientShader(1, 1, pos, colors);
334 }
335
336 private:
337 sk_sp<SkShader> fShader;
338 };
339
340 static constexpr struct RendererChoice {
341 const char* fName;
342 sk_sp<GradientRenderer>(*fFactory)();
343 } gGradientRenderers[] = {
344 {
345 "AfterEffects Gradient",
__anonc8458bdc0202() 346 []() -> sk_sp<GradientRenderer> { return sk_make_sp<AEGradientRenderer>(); }
347 },
348 {
349 "n-Linear gradient",
__anonc8458bdc0302() 350 []() -> sk_sp<GradientRenderer> { return sk_make_sp<LinearGradientRenderer>(); }
351 },
352 {
353 "Illustrator (attempt) gradient",
__anonc8458bdc0402() 354 []() -> sk_sp<GradientRenderer> { return sk_make_sp<IllGradientRenderer>(); }
355 },
356 {
357 "Triangulated gradient",
__anonc8458bdc0502() 358 []() -> sk_sp<GradientRenderer> { return sk_make_sp<TriangulatedGradientRenderer>(); }
359 }
360 };
361
lerp(float a,float b,float t)362 float lerp(float a, float b, float t) {
363 return a + (b - a)*t;
364 }
365
366 static constexpr struct VertexAnimator {
367 const char* fName;
368 void (*fAanimate)(SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t);
369 } gVertexAnimators[] = {
370 {
371 "Wigglynator",
__anonc8458bdc0602(SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) 372 [](SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) {
373 const float radius = t*0.2f/(std::sqrt(uvs.size()) - 1);
374 for (size_t i = 0; i < uvs.size(); ++i) {
375 const float phase = i*SK_FloatPI*0.31f,
376 angle = phase + t*SK_FloatPI*2;
377 pos[i] = uvs[i] + SkVector{
378 radius*std::cos(angle),
379 radius*std::sin(angle),
380 };
381 }
382 },
383 },
384 {
385 "Squircillator",
386 // Pull all vertices towards center, proportionally, such that the outer square edge
387 // is mapped to a circle for t == 1.
__anonc8458bdc0702(SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) 388 [](SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) {
389 for (size_t i = 0; i < uvs.size(); ++i) {
390 // remap to [-.5,.5]
391 const auto uv = (uvs[i] - SkPoint{0.5,0.5});
392 // can't allow len to collapse to zero, lest bad things happen at {0.5, 0.5}
393 const auto len = std::max(uv.length(), std::numeric_limits<float>::min());
394
395 // Distance from center to outer edge for the line pasing through uv.
396 const auto d = len*0.5f/std::max(std::abs(uv.fX), std::abs(uv.fY));
397 // Scale needed to pull the outer edge to the r=0.5 circle at t == 1.
398 const auto s = lerp(1, (0.5f / d), t);
399
400 pos[i] = uv*s + SkPoint{0.5, 0.5};
401 }
402 },
403 },
404 {
405 "Twirlinator",
406 // Rotate vertices proportional to their distance to center.
__anonc8458bdc0802(SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) 407 [](SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) {
408 static constexpr float kMaxRotate = SK_FloatPI*4;
409
410 for (size_t i = 0; i < uvs.size(); ++i) {
411 // remap to [-.5,.5]
412 const auto uv = (uvs[i] - SkPoint{0.5,0.5});
413 const auto angle = kMaxRotate * t * uv.length();
414
415 pos[i] = SkMatrix::RotateRad(angle).mapPoint(uv) + SkPoint{0.5, 0.5};
416 }
417 },
418 },
419 {
420 "Cylinderator",
421 // Simulate a cylinder rolling sideways across the 1x1 uv space.
__anonc8458bdc0902(SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) 422 [](SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) {
423 static constexpr float kCylRadius = .2f;
424
425 const auto cyl_pos = t;
426
427 for (size_t i = 0; i < uvs.size(); ++i) {
428 if (uvs[i].fX <= cyl_pos) {
429 pos[i] = uvs[i];
430 continue;
431 }
432
433 const auto arc_len = uvs[i].fX - cyl_pos,
434 arc_ang = arc_len/kCylRadius;
435
436 pos[i] = SkPoint{
437 cyl_pos + std::sin(arc_ang)*kCylRadius,
438 uvs[i].fY,
439 };
440 }
441 },
442 },
443 {
444 "None",
__anonc8458bdc0a02(SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) 445 [](SkSpan<const SkPoint> uvs, SkSpan<SkPoint> pos, float t) {
446 memcpy(pos.data(), uvs.data(), uvs.size() * sizeof(SkPoint));
447 },
448 },
449 };
450
451 class MeshGradientSlide final : public Slide {
452 public:
MeshGradientSlide()453 MeshGradientSlide()
454 : fCurrentRenderer(gGradientRenderers[0].fFactory())
455 , fFont(ToolUtils::DefaultPortableTypeface(), .75f)
456 , fTimeMapper({0.5f, 0}, {0.5f, 1})
457 , fVertedCountTimeMapper({0, 0.5f}, {0.5f, 1})
458 {
459 fName = "MeshGradient";
460 }
461
load(SkScalar w,SkScalar h)462 void load(SkScalar w, SkScalar h) override {
463 fSize = {w, h};
464 }
465
resize(SkScalar w,SkScalar h)466 void resize(SkScalar w, SkScalar h) override { fSize = {w, h}; }
467
draw(SkCanvas * canvas)468 void draw(SkCanvas* canvas) override {
469 SkAutoCanvasRestore acr(canvas, true);
470
471 static constexpr float kMeshViewFract = 0.85f;
472 const float mesh_size = std::min(fSize.fWidth, fSize.fHeight) * kMeshViewFract;
473
474 canvas->translate((fSize.fWidth - mesh_size) * 0.5f,
475 (fSize.fHeight - mesh_size) * 0.5f);
476 canvas->scale(mesh_size, mesh_size);
477
478 if (fShaderFill) {
479 SkPaint paint;
480 paint.setAntiAlias(true);
481 paint.setShader(fCurrentRenderer->asShader());
482 canvas->drawString("SK", 0, 0.75f, fFont, paint);
483 } else {
484 fCurrentRenderer->draw(canvas);
485 }
486
487 this->drawControls();
488 }
489
animate(double nanos)490 bool animate(double nanos) override {
491 // Mesh count animation
492 {
493 if (!fVertexCountTimeBase) {
494 fVertexCountTimeBase = nanos;
495 }
496
497 static constexpr float kSizeAdjustSeconds = 2;
498 const float t = (nanos - fVertexCountTimeBase) * 0.000000001 / kSizeAdjustSeconds;
499
500 this->updateMesh(lerp(fVertexCountFrom,
501 fVertexCountTo,
502 fVertedCountTimeMapper.computeYFromX(t)));
503 }
504
505 // Vertex animation
506 {
507 if (!fTimeBase) {
508 fTimeBase = nanos;
509 }
510
511 const float t =
512 std::abs((std::fmod((nanos - fTimeBase) * 0.000000001 * fAnimationSpeed, 2) - 1));
513
514 fCurrentAnimator->fAanimate(fVertUVs, fVertPos, fTimeMapper.computeYFromX(t));
515
516 fCurrentRenderer->updateVertices(fVertPos, fVertColors);
517 }
518
519 return true;
520 }
521
522 private:
updateMesh(size_t new_count)523 void updateMesh(size_t new_count) {
524 // These look better than rng when the count is low.
525 static constexpr struct {
526 SkPoint fUv;
527 SkColor4f fColor;
528 } gFixedVertices[] = {
529 {{ .25f, .25f}, {1, 0, 0, 1}},
530 {{ .75f, .75f}, {0, 1, 0, 1}},
531 {{ .75f, .25f}, {0, 0, 1, 1}},
532 {{ .25f, .75f}, {1, 1, 0, 1}},
533 {{ .50f, .50f}, {0, 1, 1, 1}},
534 };
535
536 SkASSERT(fVertUVs.size() == fVertPos.size());
537 SkASSERT(fVertUVs.size() == fVertColors.size());
538 const size_t current_count = fVertUVs.size();
539
540 fVertUVs.resize(new_count);
541 fVertPos.resize(new_count);
542 fVertColors.resize(new_count);
543
544 for (size_t i = current_count; i < new_count; ++i) {
545 const SkPoint uv = i < std::size(gFixedVertices)
546 ? gFixedVertices[i].fUv
547 : SkPoint{ fRNG.nextF(), fRNG.nextF() };
548 const SkColor4f color = i < std::size(gFixedVertices)
549 ? gFixedVertices[i].fColor
550 : SkColor4f{ fRNG.nextF(), fRNG.nextF(), fRNG.nextF(), 1.f };
551
552 fVertUVs[i] = fVertPos[i] = uv;
553 fVertColors[i] = color;
554 }
555
556 if (new_count < current_count) {
557 // Reset the RNG state
558 fRNG.setSeed(0);
559 static constexpr size_t kRandsPerVertex = 5;
560 const size_t rand_vertex_count =
561 std::max(new_count, std::size(gFixedVertices)) - std::size(gFixedVertices);
562 for (size_t i = 0; i < rand_vertex_count * kRandsPerVertex; ++i) { fRNG.nextF(); }
563 }
564 }
565
drawControls()566 void drawControls() {
567 ImGui::Begin("Mesh Options");
568
569 if (ImGui::BeginCombo("Gradient Type", fCurrentRendererChoice->fName)) {
570 for (const auto& renderer : gGradientRenderers) {
571 const auto is_selected = (fCurrentRendererChoice->fName == renderer.fName);
572 if (ImGui::Selectable(renderer.fName) && !is_selected) {
573 fCurrentRendererChoice = &renderer;
574 fCurrentRenderer = renderer.fFactory();
575 fCurrentRenderer->updateVertices(fVertPos, fVertColors);
576 }
577 if (is_selected) {
578 ImGui::SetItemDefaultFocus();
579 }
580 }
581 ImGui::EndCombo();
582 }
583
584 if (ImGui::BeginCombo("Animator", fCurrentAnimator->fName)) {
585 for (const auto& anim : gVertexAnimators) {
586 const auto is_selected = (fCurrentAnimator->fName == anim.fName);
587 if (ImGui::Selectable(anim.fName) && !is_selected) {
588 fCurrentAnimator = &anim;
589 fTimeBase = 0;
590 }
591 if (is_selected) {
592 ImGui::SetItemDefaultFocus();
593 }
594 }
595 ImGui::EndCombo();
596 }
597
598 if (ImGui::SliderInt("Vertex Count", &fVertexCountTo, 3, 64)) {
599 fVertexCountTimeBase = 0;
600 fVertexCountFrom = fVertUVs.size();
601 }
602
603 ImGui::SliderFloat("Speed", &fAnimationSpeed, 0.25, 4, "%.2f");
604
605 ImGui::Checkbox("Shader Fill", &fShaderFill);
606
607 ImGui::End();
608 }
609
610 SkSize fSize;
611 std::vector<SkPoint> fVertUVs;
612 std::vector<SkPoint> fVertPos; // fVertUVs after animation
613 std::vector<SkColor4f> fVertColors;
614
615 SkRandom fRNG;
616
617 sk_sp<GradientRenderer> fCurrentRenderer;
618
619 const SkFont fFont;
620
621 // Animation state
622 const SkCubicMap fTimeMapper,
623 fVertedCountTimeMapper;
624 double fTimeBase = 0,
625 fVertexCountTimeBase = 0;
626 int fVertexCountFrom = 0,
627 fVertexCountTo = 5;
628
629 // UI stuff
630 const RendererChoice* fCurrentRendererChoice = &gGradientRenderers[0];
631 const VertexAnimator* fCurrentAnimator = &gVertexAnimators[0];
632 float fAnimationSpeed = 1.f;
633 bool fShaderFill = false;
634 };
635
636 } // anonymous namespace
637
638 DEF_SLIDE(return new MeshGradientSlide{};)
639