/* * Copyright 2018 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "SlideDir.h" #include "SkAnimTimer.h" #include "SkCanvas.h" #include "SkCubicMap.h" #include "SkMakeUnique.h" #include "SkSGColor.h" #include "SkSGDraw.h" #include "SkSGGroup.h" #include "SkSGPlane.h" #include "SkSGRect.h" #include "SkSGRenderNode.h" #include "SkSGScene.h" #include "SkSGText.h" #include "SkSGTransform.h" #include "SkTypeface.h" #include #include namespace { static constexpr float kAspectRatio = 1.5f; static constexpr float kLabelSize = 12.0f; static constexpr SkSize kPadding = { 12.0f , 24.0f }; static constexpr float kFocusDuration = 500; static constexpr SkSize kFocusInset = { 100.0f, 100.0f }; static constexpr SkPoint kFocusCtrl0 = { 0.3f, 1.0f }; static constexpr SkPoint kFocusCtrl1 = { 0.0f, 1.0f }; static constexpr SkColor kFocusShade = 0xa0000000; // TODO: better unfocus binding? static constexpr SkUnichar kUnfocusKey = ' '; class SlideAdapter final : public sksg::RenderNode { public: explicit SlideAdapter(sk_sp slide) : fSlide(std::move(slide)) { SkASSERT(fSlide); } std::unique_ptr makeForwardingAnimator() { // Trivial sksg::Animator -> skottie::Animation tick adapter class ForwardingAnimator final : public sksg::Animator { public: explicit ForwardingAnimator(sk_sp adapter) : fAdapter(std::move(adapter)) {} protected: void onTick(float t) override { fAdapter->tick(SkScalarRoundToInt(t)); } private: sk_sp fAdapter; }; return skstd::make_unique(sk_ref_sp(this)); } protected: SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override { const auto isize = fSlide->getDimensions(); return SkRect::MakeIWH(isize.width(), isize.height()); } void onRender(SkCanvas* canvas, const RenderContext* ctx) const override { SkAutoCanvasRestore acr(canvas, true); canvas->clipRect(SkRect::Make(fSlide->getDimensions()), true); // TODO: commit the context? fSlide->draw(canvas); } private: void tick(SkMSec t) { fSlide->animate(SkAnimTimer(0, t * 1e6, SkAnimTimer::kRunning_State)); this->invalidate(); } const sk_sp fSlide; using INHERITED = sksg::RenderNode; }; SkMatrix SlideMatrix(const sk_sp& slide, const SkRect& dst) { const auto slideSize = slide->getDimensions(); return SkMatrix::MakeRectToRect(SkRect::MakeIWH(slideSize.width(), slideSize.height()), dst, SkMatrix::kCenter_ScaleToFit); } } // namespace struct SlideDir::Rec { sk_sp fSlide; sk_sp fTransform; SkRect fRect; }; class SlideDir::FocusController final : public sksg::Animator { public: FocusController(const SlideDir* dir, const SkRect& focusRect) : fDir(dir) , fRect(focusRect) , fTarget(nullptr) , fState(State::kIdle) { fMap.setPts(kFocusCtrl1, kFocusCtrl0); fShadePaint = sksg::Color::Make(kFocusShade); fShade = sksg::Draw::Make(sksg::Plane::Make(), fShadePaint); } bool hasFocus() const { return fState == State::kFocused; } void startFocus(const Rec* target) { if (fState != State::kIdle) return; fTarget = target; // Move the shade & slide to front. fDir->fRoot->removeChild(fTarget->fTransform); fDir->fRoot->addChild(fShade); fDir->fRoot->addChild(fTarget->fTransform); fM0 = SlideMatrix(fTarget->fSlide, fTarget->fRect); fM1 = SlideMatrix(fTarget->fSlide, fRect); fOpacity0 = 0; fOpacity1 = 1; fTimeBase = 0; fState = State::kFocusing; // Push initial state to the scene graph. this->onTick(fTimeBase); } void startUnfocus() { SkASSERT(fTarget); using std::swap; swap(fM0, fM1); swap(fOpacity0, fOpacity1); fTimeBase = 0; fState = State::kUnfocusing; } bool onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state, uint32_t modifiers) { SkASSERT(fTarget); if (!fRect.contains(x, y)) { this->startUnfocus(); return true; } // Map coords to slide space. const auto xform = SkMatrix::MakeRectToRect(fRect, SkRect::MakeSize(fDir->fWinSize), SkMatrix::kCenter_ScaleToFit); const auto pt = xform.mapXY(x, y); return fTarget->fSlide->onMouse(pt.x(), pt.y(), state, modifiers); } bool onChar(SkUnichar c) { SkASSERT(fTarget); return fTarget->fSlide->onChar(c); } protected: void onTick(float t) { if (!this->isAnimating()) return; if (!fTimeBase) { fTimeBase = t; } const auto rel_t = (t - fTimeBase) / kFocusDuration, map_t = SkTPin(fMap.computeYFromX(rel_t), 0.0f, 1.0f); SkMatrix m; for (int i = 0; i < 9; ++i) { m[i] = fM0[i] + map_t * (fM1[i] - fM0[i]); } SkASSERT(fTarget); fTarget->fTransform->getMatrix()->setMatrix(m); const auto shadeOpacity = fOpacity0 + map_t * (fOpacity1 - fOpacity0); fShadePaint->setOpacity(shadeOpacity); if (rel_t < 1) return; switch (fState) { case State::kFocusing: fState = State::kFocused; break; case State::kUnfocusing: fState = State::kIdle; fDir->fRoot->removeChild(fShade); break; case State::kIdle: case State::kFocused: SkASSERT(false); break; } } private: enum class State { kIdle, kFocusing, kUnfocusing, kFocused, }; bool isAnimating() const { return fState == State::kFocusing || fState == State::kUnfocusing; } const SlideDir* fDir; const SkRect fRect; const Rec* fTarget; SkCubicMap fMap; sk_sp fShade; sk_sp fShadePaint; SkMatrix fM0 = SkMatrix::I(), fM1 = SkMatrix::I(); float fOpacity0 = 0, fOpacity1 = 1, fTimeBase = 0; State fState = State::kIdle; using INHERITED = sksg::Animator; }; SlideDir::SlideDir(const SkString& name, SkTArray>&& slides, int columns) : fSlides(std::move(slides)) , fColumns(columns) { fName = name; } static sk_sp MakeLabel(const SkString& txt, const SkPoint& pos, const SkMatrix& dstXform) { const auto size = kLabelSize / std::sqrt(dstXform.getScaleX() * dstXform.getScaleY()); auto text = sksg::Text::Make(nullptr, txt); text->setFlags(SkPaint::kAntiAlias_Flag); text->setSize(size); text->setAlign(SkPaint::kCenter_Align); text->setPosition(pos + SkPoint::Make(0, size)); return sksg::Draw::Make(std::move(text), sksg::Color::Make(SK_ColorBLACK)); } void SlideDir::load(SkScalar winWidth, SkScalar winHeight) { // Build a global scene using transformed animation fragments: // // [Group(root)] // [Transform] // [Group] // [AnimationWrapper] // [Draw] // [Text] // [Color] // [Transform] // [Group] // [AnimationWrapper] // [Draw] // [Text] // [Color] // ... // fWinSize = SkSize::Make(winWidth, winHeight); const auto cellWidth = winWidth / fColumns; fCellSize = SkSize::Make(cellWidth, cellWidth / kAspectRatio); sksg::AnimatorList sceneAnimators; fRoot = sksg::Group::Make(); for (int i = 0; i < fSlides.count(); ++i) { const auto& slide = fSlides[i]; slide->load(winWidth, winHeight); const auto slideSize = slide->getDimensions(); const auto cell = SkRect::MakeXYWH(fCellSize.width() * (i % fColumns), fCellSize.height() * (i / fColumns), fCellSize.width(), fCellSize.height()), slideRect = cell.makeInset(kPadding.width(), kPadding.height()); auto slideMatrix = SlideMatrix(slide, slideRect); auto adapter = sk_make_sp(slide); auto slideGrp = sksg::Group::Make(); slideGrp->addChild(sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeIWH(slideSize.width(), slideSize.height())), sksg::Color::Make(0xfff0f0f0))); slideGrp->addChild(adapter); slideGrp->addChild(MakeLabel(slide->getName(), SkPoint::Make(slideSize.width() / 2, slideSize.height()), slideMatrix)); auto slideTransform = sksg::Transform::Make(std::move(slideGrp), slideMatrix); sceneAnimators.push_back(adapter->makeForwardingAnimator()); fRoot->addChild(slideTransform); fRecs.push_back({ slide, slideTransform, slideRect }); } fScene = sksg::Scene::Make(fRoot, std::move(sceneAnimators)); const auto focusRect = SkRect::MakeSize(fWinSize).makeInset(kFocusInset.width(), kFocusInset.height()); fFocusController = skstd::make_unique(this, focusRect); } void SlideDir::unload() { for (const auto& slide : fSlides) { slide->unload(); } fRecs.reset(); fScene.reset(); fFocusController.reset(); fRoot.reset(); fTimeBase = 0; } SkISize SlideDir::getDimensions() const { return SkSize::Make(fWinSize.width(), fCellSize.height() * (1 + (fSlides.count() - 1) / fColumns)).toCeil(); } void SlideDir::draw(SkCanvas* canvas) { fScene->render(canvas); } bool SlideDir::animate(const SkAnimTimer& timer) { if (fTimeBase == 0) { // Reset the animation time. fTimeBase = timer.msec(); } const auto t = timer.msec() - fTimeBase; fScene->animate(t); fFocusController->tick(t); return true; } bool SlideDir::onChar(SkUnichar c) { if (fFocusController->hasFocus()) { if (c == kUnfocusKey) { fFocusController->startUnfocus(); return true; } return fFocusController->onChar(c); } return false; } bool SlideDir::onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state, uint32_t modifiers) { if (state == sk_app::Window::kMove_InputState || modifiers) return false; if (fFocusController->hasFocus()) { return fFocusController->onMouse(x, y, state, modifiers); } const auto* cell = this->findCell(x, y); if (!cell) return false; static constexpr SkScalar kClickMoveTolerance = 4; switch (state) { case sk_app::Window::kDown_InputState: fTrackingCell = cell; fTrackingPos = SkPoint::Make(x, y); break; case sk_app::Window::kUp_InputState: if (cell == fTrackingCell && SkPoint::Distance(fTrackingPos, SkPoint::Make(x, y)) < kClickMoveTolerance) { fFocusController->startFocus(cell); } break; default: break; } return false; } const SlideDir::Rec* SlideDir::findCell(float x, float y) const { // TODO: use SG hit testing instead of layout info? const auto size = this->getDimensions(); if (x < 0 || y < 0 || x >= size.width() || y >= size.height()) { return nullptr; } const int col = static_cast(x / fCellSize.width()), row = static_cast(y / fCellSize.height()), idx = row * fColumns + col; return idx < fRecs.count() ? &fRecs[idx] : nullptr; }