blob: 517d823faa854bc666b07252a88c1272f52416fd [file] [log] [blame]
Florin Malita76a076b2018-02-15 18:40:48 -05001/*
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"
Florin Malita836f8222018-02-20 10:31:01 -050012#include "SkCubicMap.h"
Florin Malita76a076b2018-02-15 18:40:48 -050013#include "SkMakeUnique.h"
14#include "SkSGColor.h"
15#include "SkSGDraw.h"
16#include "SkSGGroup.h"
Florin Malita97b3d2b2018-02-20 11:26:15 -050017#include "SkSGPlane.h"
Florin Malita65fce9e2018-02-19 13:25:18 -050018#include "SkSGRect.h"
Florin Malita76a076b2018-02-15 18:40:48 -050019#include "SkSGRenderNode.h"
20#include "SkSGScene.h"
21#include "SkSGText.h"
22#include "SkSGTransform.h"
23#include "SkTypeface.h"
24
Florin Malita65fce9e2018-02-19 13:25:18 -050025#include <cmath>
Ben Wagnerf08d1d02018-06-18 15:11:00 -040026#include <utility>
Florin Malita65fce9e2018-02-19 13:25:18 -050027
Florin Malita76a076b2018-02-15 18:40:48 -050028namespace {
29
Florin Malita65fce9e2018-02-19 13:25:18 -050030static constexpr float kAspectRatio = 1.5f;
31static constexpr float kLabelSize = 12.0f;
32static constexpr SkSize kPadding = { 12.0f , 24.0f };
Florin Malita836f8222018-02-20 10:31:01 -050033
34static constexpr float kFocusDuration = 500;
35static constexpr SkSize kFocusInset = { 100.0f, 100.0f };
36static constexpr SkPoint kFocusCtrl0 = { 0.3f, 1.0f };
37static constexpr SkPoint kFocusCtrl1 = { 0.0f, 1.0f };
38static constexpr SkColor kFocusShade = 0xa0000000;
Florin Malita65fce9e2018-02-19 13:25:18 -050039
40// TODO: better unfocus binding?
41static constexpr SkUnichar kUnfocusKey = ' ';
Florin Malita76a076b2018-02-15 18:40:48 -050042
43class SlideAdapter final : public sksg::RenderNode {
44public:
45 explicit SlideAdapter(sk_sp<Slide> slide)
46 : fSlide(std::move(slide)) {
47 SkASSERT(fSlide);
48 }
49
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
69protected:
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
75 void onRender(SkCanvas* canvas) const override {
Florin Malita6127c4c2018-04-05 15:15:59 -040076 SkAutoCanvasRestore acr(canvas, true);
77 canvas->clipRect(SkRect::Make(fSlide->getDimensions()), true);
Florin Malita76a076b2018-02-15 18:40:48 -050078 fSlide->draw(canvas);
79 }
80
81private:
82 void tick(SkMSec t) {
83 fSlide->animate(SkAnimTimer(0, t * 1e6, SkAnimTimer::kRunning_State));
84 this->invalidate();
85 }
86
87 const sk_sp<Slide> fSlide;
88
89 using INHERITED = sksg::RenderNode;
90};
91
Florin Malita65fce9e2018-02-19 13:25:18 -050092SkMatrix SlideMatrix(const sk_sp<Slide>& slide, const SkRect& dst) {
93 const auto slideSize = slide->getDimensions();
94 return SkMatrix::MakeRectToRect(SkRect::MakeIWH(slideSize.width(), slideSize.height()),
95 dst,
96 SkMatrix::kCenter_ScaleToFit);
97}
98
Florin Malita76a076b2018-02-15 18:40:48 -050099} // namespace
100
101struct SlideDir::Rec {
Florin Malita65fce9e2018-02-19 13:25:18 -0500102 sk_sp<Slide> fSlide;
103 sk_sp<sksg::Transform> fTransform;
104 SkRect fRect;
105};
106
107class SlideDir::FocusController final : public sksg::Animator {
108public:
109 FocusController(const SlideDir* dir, const SkRect& focusRect)
110 : fDir(dir)
111 , fRect(focusRect)
112 , fTarget(nullptr)
Florin Malita836f8222018-02-20 10:31:01 -0500113 , fState(State::kIdle) {
114 fMap.setPts(kFocusCtrl1, kFocusCtrl0);
115
116 fShadePaint = sksg::Color::Make(kFocusShade);
Florin Malita97b3d2b2018-02-20 11:26:15 -0500117 fShade = sksg::Draw::Make(sksg::Plane::Make(), fShadePaint);
Florin Malita836f8222018-02-20 10:31:01 -0500118 }
Florin Malita65fce9e2018-02-19 13:25:18 -0500119
120 bool hasFocus() const { return fState == State::kFocused; }
121
122 void startFocus(const Rec* target) {
Florin Malita836f8222018-02-20 10:31:01 -0500123 if (fState != State::kIdle)
124 return;
125
Florin Malita65fce9e2018-02-19 13:25:18 -0500126 fTarget = target;
Florin Malita836f8222018-02-20 10:31:01 -0500127
128 // Move the shade & slide to front.
129 fDir->fRoot->removeChild(fTarget->fTransform);
130 fDir->fRoot->addChild(fShade);
131 fDir->fRoot->addChild(fTarget->fTransform);
132
Florin Malita65fce9e2018-02-19 13:25:18 -0500133 fM0 = SlideMatrix(fTarget->fSlide, fTarget->fRect);
134 fM1 = SlideMatrix(fTarget->fSlide, fRect);
135
Florin Malita836f8222018-02-20 10:31:01 -0500136 fOpacity0 = 0;
137 fOpacity1 = 1;
138
Florin Malita65fce9e2018-02-19 13:25:18 -0500139 fTimeBase = 0;
140 fState = State::kFocusing;
Florin Malita836f8222018-02-20 10:31:01 -0500141
142 // Push initial state to the scene graph.
143 this->onTick(fTimeBase);
Florin Malita65fce9e2018-02-19 13:25:18 -0500144 }
145
146 void startUnfocus() {
147 SkASSERT(fTarget);
148
Ben Wagnerf08d1d02018-06-18 15:11:00 -0400149 using std::swap;
150 swap(fM0, fM1);
151 swap(fOpacity0, fOpacity1);
Florin Malita65fce9e2018-02-19 13:25:18 -0500152
153 fTimeBase = 0;
154 fState = State::kUnfocusing;
155 }
156
157 bool onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state, uint32_t modifiers) {
158 SkASSERT(fTarget);
159
Florin Malitaeb420452018-02-20 11:44:43 -0500160 if (!fRect.contains(x, y)) {
Florin Malita836f8222018-02-20 10:31:01 -0500161 this->startUnfocus();
162 return true;
163 }
164
Florin Malita65fce9e2018-02-19 13:25:18 -0500165 // Map coords to slide space.
166 const auto xform = SkMatrix::MakeRectToRect(fRect,
167 SkRect::MakeSize(fDir->fWinSize),
168 SkMatrix::kCenter_ScaleToFit);
169 const auto pt = xform.mapXY(x, y);
170
171 return fTarget->fSlide->onMouse(pt.x(), pt.y(), state, modifiers);
172 }
173
174 bool onChar(SkUnichar c) {
175 SkASSERT(fTarget);
176
177 return fTarget->fSlide->onChar(c);
178 }
179
180protected:
181 void onTick(float t) {
182 if (!this->isAnimating())
183 return;
184
185 if (!fTimeBase) {
186 fTimeBase = t;
187 }
188
Florin Malita836f8222018-02-20 10:31:01 -0500189 const auto rel_t = (t - fTimeBase) / kFocusDuration,
190 map_t = SkTPin(fMap.computeYFromX(rel_t), 0.0f, 1.0f);
Florin Malita65fce9e2018-02-19 13:25:18 -0500191
192 SkMatrix m;
193 for (int i = 0; i < 9; ++i) {
Florin Malita836f8222018-02-20 10:31:01 -0500194 m[i] = fM0[i] + map_t * (fM1[i] - fM0[i]);
Florin Malita65fce9e2018-02-19 13:25:18 -0500195 }
196
197 SkASSERT(fTarget);
198 fTarget->fTransform->getMatrix()->setMatrix(m);
199
Florin Malita836f8222018-02-20 10:31:01 -0500200 const auto shadeOpacity = fOpacity0 + map_t * (fOpacity1 - fOpacity0);
201 fShadePaint->setOpacity(shadeOpacity);
202
Florin Malita65fce9e2018-02-19 13:25:18 -0500203 if (rel_t < 1)
204 return;
205
206 switch (fState) {
207 case State::kFocusing:
208 fState = State::kFocused;
209 break;
210 case State::kUnfocusing:
211 fState = State::kIdle;
Florin Malita836f8222018-02-20 10:31:01 -0500212 fDir->fRoot->removeChild(fShade);
Florin Malita65fce9e2018-02-19 13:25:18 -0500213 break;
214
215 case State::kIdle:
216 case State::kFocused:
217 SkASSERT(false);
218 break;
219 }
220 }
221
222private:
223 enum class State {
224 kIdle,
225 kFocusing,
226 kUnfocusing,
227 kFocused,
228 };
229
230 bool isAnimating() const { return fState == State::kFocusing || fState == State::kUnfocusing; }
231
Florin Malita836f8222018-02-20 10:31:01 -0500232 const SlideDir* fDir;
233 const SkRect fRect;
234 const Rec* fTarget;
235
236 SkCubicMap fMap;
237 sk_sp<sksg::RenderNode> fShade;
238 sk_sp<sksg::PaintNode> fShadePaint;
Florin Malita65fce9e2018-02-19 13:25:18 -0500239
240 SkMatrix fM0 = SkMatrix::I(),
241 fM1 = SkMatrix::I();
Florin Malita836f8222018-02-20 10:31:01 -0500242 float fOpacity0 = 0,
243 fOpacity1 = 1,
244 fTimeBase = 0;
Florin Malita65fce9e2018-02-19 13:25:18 -0500245 State fState = State::kIdle;
246
247 using INHERITED = sksg::Animator;
Florin Malita76a076b2018-02-15 18:40:48 -0500248};
249
250SlideDir::SlideDir(const SkString& name, SkTArray<sk_sp<Slide>, true>&& slides, int columns)
251 : fSlides(std::move(slides))
252 , fColumns(columns) {
253 fName = name;
254}
255
Florin Malita65fce9e2018-02-19 13:25:18 -0500256static sk_sp<sksg::RenderNode> MakeLabel(const SkString& txt,
257 const SkPoint& pos,
258 const SkMatrix& dstXform) {
259 const auto size = kLabelSize / std::sqrt(dstXform.getScaleX() * dstXform.getScaleY());
Florin Malita76a076b2018-02-15 18:40:48 -0500260 auto text = sksg::Text::Make(nullptr, txt);
261 text->setFlags(SkPaint::kAntiAlias_Flag);
Florin Malita65fce9e2018-02-19 13:25:18 -0500262 text->setSize(size);
Florin Malita76a076b2018-02-15 18:40:48 -0500263 text->setAlign(SkPaint::kCenter_Align);
Florin Malita65fce9e2018-02-19 13:25:18 -0500264 text->setPosition(pos + SkPoint::Make(0, size));
Florin Malita76a076b2018-02-15 18:40:48 -0500265
266 return sksg::Draw::Make(std::move(text), sksg::Color::Make(SK_ColorBLACK));
267}
268
269void SlideDir::load(SkScalar winWidth, SkScalar winHeight) {
270 // Build a global scene using transformed animation fragments:
271 //
272 // [Group(root)]
273 // [Transform]
274 // [Group]
275 // [AnimationWrapper]
276 // [Draw]
277 // [Text]
278 // [Color]
279 // [Transform]
280 // [Group]
281 // [AnimationWrapper]
282 // [Draw]
283 // [Text]
284 // [Color]
285 // ...
286 //
287
Florin Malita65fce9e2018-02-19 13:25:18 -0500288 fWinSize = SkSize::Make(winWidth, winHeight);
289 const auto cellWidth = winWidth / fColumns;
290 fCellSize = SkSize::Make(cellWidth, cellWidth / kAspectRatio);
Florin Malita76a076b2018-02-15 18:40:48 -0500291
292 sksg::AnimatorList sceneAnimators;
Florin Malita65fce9e2018-02-19 13:25:18 -0500293 fRoot = sksg::Group::Make();
Florin Malita76a076b2018-02-15 18:40:48 -0500294
295 for (int i = 0; i < fSlides.count(); ++i) {
296 const auto& slide = fSlides[i];
297 slide->load(winWidth, winHeight);
298
299 const auto slideSize = slide->getDimensions();
Florin Malita65fce9e2018-02-19 13:25:18 -0500300 const auto cell = SkRect::MakeXYWH(fCellSize.width() * (i % fColumns),
301 fCellSize.height() * (i / fColumns),
302 fCellSize.width(),
303 fCellSize.height()),
Florin Malita76a076b2018-02-15 18:40:48 -0500304 slideRect = cell.makeInset(kPadding.width(), kPadding.height());
305
Florin Malita65fce9e2018-02-19 13:25:18 -0500306 auto slideMatrix = SlideMatrix(slide, slideRect);
307 auto adapter = sk_make_sp<SlideAdapter>(slide);
308 auto slideGrp = sksg::Group::Make();
309 slideGrp->addChild(sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeIWH(slideSize.width(),
310 slideSize.height())),
311 sksg::Color::Make(0xfff0f0f0)));
312 slideGrp->addChild(adapter);
313 slideGrp->addChild(MakeLabel(slide->getName(),
314 SkPoint::Make(slideSize.width() / 2, slideSize.height()),
315 slideMatrix));
316 auto slideTransform = sksg::Transform::Make(std::move(slideGrp), slideMatrix);
Florin Malita76a076b2018-02-15 18:40:48 -0500317
318 sceneAnimators.push_back(adapter->makeForwardingAnimator());
Florin Malita76a076b2018-02-15 18:40:48 -0500319
Florin Malita65fce9e2018-02-19 13:25:18 -0500320 fRoot->addChild(slideTransform);
321 fRecs.push_back({ slide, slideTransform, slideRect });
Florin Malita76a076b2018-02-15 18:40:48 -0500322 }
323
Florin Malita65fce9e2018-02-19 13:25:18 -0500324 fScene = sksg::Scene::Make(fRoot, std::move(sceneAnimators));
325
326 const auto focusRect = SkRect::MakeSize(fWinSize).makeInset(kFocusInset.width(),
327 kFocusInset.height());
328 fFocusController = skstd::make_unique<FocusController>(this, focusRect);
Florin Malita76a076b2018-02-15 18:40:48 -0500329}
330
331void SlideDir::unload() {
332 for (const auto& slide : fSlides) {
333 slide->unload();
334 }
335
336 fRecs.reset();
337 fScene.reset();
Florin Malita65fce9e2018-02-19 13:25:18 -0500338 fFocusController.reset();
339 fRoot.reset();
Florin Malita76a076b2018-02-15 18:40:48 -0500340 fTimeBase = 0;
341}
342
343SkISize SlideDir::getDimensions() const {
Florin Malita60d3bfc2018-02-20 16:49:20 -0500344 return SkSize::Make(fWinSize.width(),
345 fCellSize.height() * (1 + (fSlides.count() - 1) / fColumns)).toCeil();
Florin Malita76a076b2018-02-15 18:40:48 -0500346}
347
348void SlideDir::draw(SkCanvas* canvas) {
349 fScene->render(canvas);
350}
351
352bool SlideDir::animate(const SkAnimTimer& timer) {
353 if (fTimeBase == 0) {
354 // Reset the animation time.
355 fTimeBase = timer.msec();
356 }
Florin Malita65fce9e2018-02-19 13:25:18 -0500357
358 const auto t = timer.msec() - fTimeBase;
359 fScene->animate(t);
360 fFocusController->tick(t);
Florin Malita76a076b2018-02-15 18:40:48 -0500361
362 return true;
363}
364
365bool SlideDir::onChar(SkUnichar c) {
Florin Malita65fce9e2018-02-19 13:25:18 -0500366 if (fFocusController->hasFocus()) {
367 if (c == kUnfocusKey) {
368 fFocusController->startUnfocus();
369 return true;
370 }
371 return fFocusController->onChar(c);
372 }
373
Florin Malita76a076b2018-02-15 18:40:48 -0500374 return false;
375}
376
Florin Malita65fce9e2018-02-19 13:25:18 -0500377bool SlideDir::onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state,
378 uint32_t modifiers) {
379 if (state == sk_app::Window::kMove_InputState || modifiers)
380 return false;
381
382 if (fFocusController->hasFocus()) {
383 return fFocusController->onMouse(x, y, state, modifiers);
384 }
385
386 const auto* cell = this->findCell(x, y);
387 if (!cell)
388 return false;
389
390 static constexpr SkScalar kClickMoveTolerance = 4;
391
392 switch (state) {
393 case sk_app::Window::kDown_InputState:
394 fTrackingCell = cell;
395 fTrackingPos = SkPoint::Make(x, y);
396 break;
397 case sk_app::Window::kUp_InputState:
398 if (cell == fTrackingCell &&
399 SkPoint::Distance(fTrackingPos, SkPoint::Make(x, y)) < kClickMoveTolerance) {
Florin Malita65fce9e2018-02-19 13:25:18 -0500400 fFocusController->startFocus(cell);
401 }
402 break;
403 default:
404 break;
405 }
406
Florin Malita76a076b2018-02-15 18:40:48 -0500407 return false;
408}
Florin Malita65fce9e2018-02-19 13:25:18 -0500409
410const SlideDir::Rec* SlideDir::findCell(float x, float y) const {
411 // TODO: use SG hit testing instead of layout info?
412 const auto size = this->getDimensions();
413 if (x < 0 || y < 0 || x >= size.width() || y >= size.height()) {
414 return nullptr;
415 }
416
417 const int col = static_cast<int>(x / fCellSize.width()),
418 row = static_cast<int>(y / fCellSize.height()),
419 idx = row * fColumns + col;
420
Florin Malita60d3bfc2018-02-20 16:49:20 -0500421 return idx < fRecs.count() ? &fRecs[idx] : nullptr;
Florin Malita65fce9e2018-02-19 13:25:18 -0500422}