1 /*
2 * Copyright 2018 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 "SlideDir.h"
9
10 #include "SkAnimTimer.h"
11 #include "SkCanvas.h"
12 #include "SkCubicMap.h"
13 #include "SkMakeUnique.h"
14 #include "SkSGColor.h"
15 #include "SkSGDraw.h"
16 #include "SkSGGroup.h"
17 #include "SkSGPlane.h"
18 #include "SkSGRect.h"
19 #include "SkSGRenderNode.h"
20 #include "SkSGScene.h"
21 #include "SkSGText.h"
22 #include "SkSGTransform.h"
23 #include "SkTypeface.h"
24
25 #include <cmath>
26 #include <utility>
27
28 namespace {
29
30 static constexpr float kAspectRatio = 1.5f;
31 static constexpr float kLabelSize = 12.0f;
32 static constexpr SkSize kPadding = { 12.0f , 24.0f };
33
34 static constexpr float kFocusDuration = 500;
35 static constexpr SkSize kFocusInset = { 100.0f, 100.0f };
36 static constexpr SkPoint kFocusCtrl0 = { 0.3f, 1.0f };
37 static constexpr SkPoint kFocusCtrl1 = { 0.0f, 1.0f };
38 static constexpr SkColor kFocusShade = 0xa0000000;
39
40 // TODO: better unfocus binding?
41 static constexpr SkUnichar kUnfocusKey = ' ';
42
43 class SlideAdapter final : public sksg::RenderNode {
44 public:
SlideAdapter(sk_sp<Slide> slide)45 explicit SlideAdapter(sk_sp<Slide> slide)
46 : fSlide(std::move(slide)) {
47 SkASSERT(fSlide);
48 }
49
makeForwardingAnimator()50 std::unique_ptr<sksg::Animator> makeForwardingAnimator() {
51 // Trivial sksg::Animator -> skottie::Animation tick adapter
52 class ForwardingAnimator final : public sksg::Animator {
53 public:
54 explicit ForwardingAnimator(sk_sp<SlideAdapter> adapter)
55 : fAdapter(std::move(adapter)) {}
56
57 protected:
58 void onTick(float t) override {
59 fAdapter->tick(SkScalarRoundToInt(t));
60 }
61
62 private:
63 sk_sp<SlideAdapter> fAdapter;
64 };
65
66 return skstd::make_unique<ForwardingAnimator>(sk_ref_sp(this));
67 }
68
69 protected:
onRevalidate(sksg::InvalidationController * ic,const SkMatrix & ctm)70 SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
71 const auto isize = fSlide->getDimensions();
72 return SkRect::MakeIWH(isize.width(), isize.height());
73 }
74
onRender(SkCanvas * canvas,const RenderContext * ctx) const75 void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
76 SkAutoCanvasRestore acr(canvas, true);
77 canvas->clipRect(SkRect::Make(fSlide->getDimensions()), true);
78
79 // TODO: commit the context?
80 fSlide->draw(canvas);
81 }
82
83 private:
tick(SkMSec t)84 void tick(SkMSec t) {
85 fSlide->animate(SkAnimTimer(t * 1e6));
86 this->invalidate();
87 }
88
89 const sk_sp<Slide> fSlide;
90
91 using INHERITED = sksg::RenderNode;
92 };
93
SlideMatrix(const sk_sp<Slide> & slide,const SkRect & dst)94 SkMatrix SlideMatrix(const sk_sp<Slide>& slide, const SkRect& dst) {
95 const auto slideSize = slide->getDimensions();
96 return SkMatrix::MakeRectToRect(SkRect::MakeIWH(slideSize.width(), slideSize.height()),
97 dst,
98 SkMatrix::kCenter_ScaleToFit);
99 }
100
101 } // namespace
102
103 struct SlideDir::Rec {
104 sk_sp<Slide> fSlide;
105 sk_sp<sksg::RenderNode> fSlideRoot;
106 sk_sp<sksg::Matrix<SkMatrix>> fMatrix;
107 SkRect fRect;
108 };
109
110 class SlideDir::FocusController final : public sksg::Animator {
111 public:
FocusController(const SlideDir * dir,const SkRect & focusRect)112 FocusController(const SlideDir* dir, const SkRect& focusRect)
113 : fDir(dir)
114 , fRect(focusRect)
115 , fTarget(nullptr)
116 , fState(State::kIdle) {
117 fMap.setPts(kFocusCtrl1, kFocusCtrl0);
118
119 fShadePaint = sksg::Color::Make(kFocusShade);
120 fShade = sksg::Draw::Make(sksg::Plane::Make(), fShadePaint);
121 }
122
hasFocus() const123 bool hasFocus() const { return fState == State::kFocused; }
124
startFocus(const Rec * target)125 void startFocus(const Rec* target) {
126 if (fState != State::kIdle)
127 return;
128
129 fTarget = target;
130
131 // Move the shade & slide to front.
132 fDir->fRoot->removeChild(fTarget->fSlideRoot);
133 fDir->fRoot->addChild(fShade);
134 fDir->fRoot->addChild(fTarget->fSlideRoot);
135
136 fM0 = SlideMatrix(fTarget->fSlide, fTarget->fRect);
137 fM1 = SlideMatrix(fTarget->fSlide, fRect);
138
139 fOpacity0 = 0;
140 fOpacity1 = 1;
141
142 fTimeBase = 0;
143 fState = State::kFocusing;
144
145 // Push initial state to the scene graph.
146 this->onTick(fTimeBase);
147 }
148
startUnfocus()149 void startUnfocus() {
150 SkASSERT(fTarget);
151
152 using std::swap;
153 swap(fM0, fM1);
154 swap(fOpacity0, fOpacity1);
155
156 fTimeBase = 0;
157 fState = State::kUnfocusing;
158 }
159
onMouse(SkScalar x,SkScalar y,sk_app::Window::InputState state,uint32_t modifiers)160 bool onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state, uint32_t modifiers) {
161 SkASSERT(fTarget);
162
163 if (!fRect.contains(x, y)) {
164 this->startUnfocus();
165 return true;
166 }
167
168 // Map coords to slide space.
169 const auto xform = SkMatrix::MakeRectToRect(fRect,
170 SkRect::MakeSize(fDir->fWinSize),
171 SkMatrix::kCenter_ScaleToFit);
172 const auto pt = xform.mapXY(x, y);
173
174 return fTarget->fSlide->onMouse(pt.x(), pt.y(), state, modifiers);
175 }
176
onChar(SkUnichar c)177 bool onChar(SkUnichar c) {
178 SkASSERT(fTarget);
179
180 return fTarget->fSlide->onChar(c);
181 }
182
183 protected:
onTick(float t)184 void onTick(float t) {
185 if (!this->isAnimating())
186 return;
187
188 if (!fTimeBase) {
189 fTimeBase = t;
190 }
191
192 const auto rel_t = (t - fTimeBase) / kFocusDuration,
193 map_t = SkTPin(fMap.computeYFromX(rel_t), 0.0f, 1.0f);
194
195 SkMatrix m;
196 for (int i = 0; i < 9; ++i) {
197 m[i] = fM0[i] + map_t * (fM1[i] - fM0[i]);
198 }
199
200 SkASSERT(fTarget);
201 fTarget->fMatrix->setMatrix(m);
202
203 const auto shadeOpacity = fOpacity0 + map_t * (fOpacity1 - fOpacity0);
204 fShadePaint->setOpacity(shadeOpacity);
205
206 if (rel_t < 1)
207 return;
208
209 switch (fState) {
210 case State::kFocusing:
211 fState = State::kFocused;
212 break;
213 case State::kUnfocusing:
214 fState = State::kIdle;
215 fDir->fRoot->removeChild(fShade);
216 break;
217
218 case State::kIdle:
219 case State::kFocused:
220 SkASSERT(false);
221 break;
222 }
223 }
224
225 private:
226 enum class State {
227 kIdle,
228 kFocusing,
229 kUnfocusing,
230 kFocused,
231 };
232
isAnimating() const233 bool isAnimating() const { return fState == State::kFocusing || fState == State::kUnfocusing; }
234
235 const SlideDir* fDir;
236 const SkRect fRect;
237 const Rec* fTarget;
238
239 SkCubicMap fMap;
240 sk_sp<sksg::RenderNode> fShade;
241 sk_sp<sksg::PaintNode> fShadePaint;
242
243 SkMatrix fM0 = SkMatrix::I(),
244 fM1 = SkMatrix::I();
245 float fOpacity0 = 0,
246 fOpacity1 = 1,
247 fTimeBase = 0;
248 State fState = State::kIdle;
249
250 using INHERITED = sksg::Animator;
251 };
252
SlideDir(const SkString & name,SkTArray<sk_sp<Slide>> && slides,int columns)253 SlideDir::SlideDir(const SkString& name, SkTArray<sk_sp<Slide>>&& slides, int columns)
254 : fSlides(std::move(slides))
255 , fColumns(columns) {
256 fName = name;
257 }
258
MakeLabel(const SkString & txt,const SkPoint & pos,const SkMatrix & dstXform)259 static sk_sp<sksg::RenderNode> MakeLabel(const SkString& txt,
260 const SkPoint& pos,
261 const SkMatrix& dstXform) {
262 const auto size = kLabelSize / std::sqrt(dstXform.getScaleX() * dstXform.getScaleY());
263 auto text = sksg::Text::Make(nullptr, txt);
264 text->setEdging(SkFont::Edging::kAntiAlias);
265 text->setSize(size);
266 text->setAlign(SkTextUtils::kCenter_Align);
267 text->setPosition(pos + SkPoint::Make(0, size));
268
269 return sksg::Draw::Make(std::move(text), sksg::Color::Make(SK_ColorBLACK));
270 }
271
load(SkScalar winWidth,SkScalar winHeight)272 void SlideDir::load(SkScalar winWidth, SkScalar winHeight) {
273 // Build a global scene using transformed animation fragments:
274 //
275 // [Group(root)]
276 // [Transform]
277 // [Group]
278 // [AnimationWrapper]
279 // [Draw]
280 // [Text]
281 // [Color]
282 // [Transform]
283 // [Group]
284 // [AnimationWrapper]
285 // [Draw]
286 // [Text]
287 // [Color]
288 // ...
289 //
290
291 fWinSize = SkSize::Make(winWidth, winHeight);
292 const auto cellWidth = winWidth / fColumns;
293 fCellSize = SkSize::Make(cellWidth, cellWidth / kAspectRatio);
294
295 sksg::AnimatorList sceneAnimators;
296 fRoot = sksg::Group::Make();
297
298 for (int i = 0; i < fSlides.count(); ++i) {
299 const auto& slide = fSlides[i];
300 slide->load(winWidth, winHeight);
301
302 const auto slideSize = slide->getDimensions();
303 const auto cell = SkRect::MakeXYWH(fCellSize.width() * (i % fColumns),
304 fCellSize.height() * (i / fColumns),
305 fCellSize.width(),
306 fCellSize.height()),
307 slideRect = cell.makeInset(kPadding.width(), kPadding.height());
308
309 auto slideMatrix = sksg::Matrix<SkMatrix>::Make(SlideMatrix(slide, slideRect));
310 auto adapter = sk_make_sp<SlideAdapter>(slide);
311 auto slideGrp = sksg::Group::Make();
312 slideGrp->addChild(sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeIWH(slideSize.width(),
313 slideSize.height())),
314 sksg::Color::Make(0xfff0f0f0)));
315 slideGrp->addChild(adapter);
316 slideGrp->addChild(MakeLabel(slide->getName(),
317 SkPoint::Make(slideSize.width() / 2, slideSize.height()),
318 slideMatrix->getMatrix()));
319 auto slideRoot = sksg::TransformEffect::Make(std::move(slideGrp), slideMatrix);
320
321 sceneAnimators.push_back(adapter->makeForwardingAnimator());
322
323 fRoot->addChild(slideRoot);
324 fRecs.push_back({ slide, slideRoot, slideMatrix, slideRect });
325 }
326
327 fScene = sksg::Scene::Make(fRoot, std::move(sceneAnimators));
328
329 const auto focusRect = SkRect::MakeSize(fWinSize).makeInset(kFocusInset.width(),
330 kFocusInset.height());
331 fFocusController = skstd::make_unique<FocusController>(this, focusRect);
332 }
333
unload()334 void SlideDir::unload() {
335 for (const auto& slide : fSlides) {
336 slide->unload();
337 }
338
339 fRecs.reset();
340 fScene.reset();
341 fFocusController.reset();
342 fRoot.reset();
343 fTimeBase = 0;
344 }
345
getDimensions() const346 SkISize SlideDir::getDimensions() const {
347 return SkSize::Make(fWinSize.width(),
348 fCellSize.height() * (1 + (fSlides.count() - 1) / fColumns)).toCeil();
349 }
350
draw(SkCanvas * canvas)351 void SlideDir::draw(SkCanvas* canvas) {
352 fScene->render(canvas);
353 }
354
animate(const SkAnimTimer & timer)355 bool SlideDir::animate(const SkAnimTimer& timer) {
356 if (fTimeBase == 0) {
357 // Reset the animation time.
358 fTimeBase = timer.msec();
359 }
360
361 const auto t = timer.msec() - fTimeBase;
362 fScene->animate(t);
363 fFocusController->tick(t);
364
365 return true;
366 }
367
onChar(SkUnichar c)368 bool SlideDir::onChar(SkUnichar c) {
369 if (fFocusController->hasFocus()) {
370 if (c == kUnfocusKey) {
371 fFocusController->startUnfocus();
372 return true;
373 }
374 return fFocusController->onChar(c);
375 }
376
377 return false;
378 }
379
onMouse(SkScalar x,SkScalar y,sk_app::Window::InputState state,uint32_t modifiers)380 bool SlideDir::onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state,
381 uint32_t modifiers) {
382 if (state == sk_app::Window::kMove_InputState || modifiers)
383 return false;
384
385 if (fFocusController->hasFocus()) {
386 return fFocusController->onMouse(x, y, state, modifiers);
387 }
388
389 const auto* cell = this->findCell(x, y);
390 if (!cell)
391 return false;
392
393 static constexpr SkScalar kClickMoveTolerance = 4;
394
395 switch (state) {
396 case sk_app::Window::kDown_InputState:
397 fTrackingCell = cell;
398 fTrackingPos = SkPoint::Make(x, y);
399 break;
400 case sk_app::Window::kUp_InputState:
401 if (cell == fTrackingCell &&
402 SkPoint::Distance(fTrackingPos, SkPoint::Make(x, y)) < kClickMoveTolerance) {
403 fFocusController->startFocus(cell);
404 }
405 break;
406 default:
407 break;
408 }
409
410 return false;
411 }
412
findCell(float x,float y) const413 const SlideDir::Rec* SlideDir::findCell(float x, float y) const {
414 // TODO: use SG hit testing instead of layout info?
415 const auto size = this->getDimensions();
416 if (x < 0 || y < 0 || x >= size.width() || y >= size.height()) {
417 return nullptr;
418 }
419
420 const int col = static_cast<int>(x / fCellSize.width()),
421 row = static_cast<int>(y / fCellSize.height()),
422 idx = row * fColumns + col;
423
424 return idx < fRecs.count() ? &fRecs[idx] : nullptr;
425 }
426