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