blob: ce036b488572081974f7fa73826b44cd77d2e8ef [file] [log] [blame]
/*
* 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 <cmath>
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> slide)
: fSlide(std::move(slide)) {
SkASSERT(fSlide);
}
std::unique_ptr<sksg::Animator> makeForwardingAnimator() {
// Trivial sksg::Animator -> skottie::Animation tick adapter
class ForwardingAnimator final : public sksg::Animator {
public:
explicit ForwardingAnimator(sk_sp<SlideAdapter> adapter)
: fAdapter(std::move(adapter)) {}
protected:
void onTick(float t) override {
fAdapter->tick(SkScalarRoundToInt(t));
}
private:
sk_sp<SlideAdapter> fAdapter;
};
return skstd::make_unique<ForwardingAnimator>(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 override {
fSlide->draw(canvas);
}
private:
void tick(SkMSec t) {
fSlide->animate(SkAnimTimer(0, t * 1e6, SkAnimTimer::kRunning_State));
this->invalidate();
}
const sk_sp<Slide> fSlide;
using INHERITED = sksg::RenderNode;
};
SkMatrix SlideMatrix(const sk_sp<Slide>& 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<Slide> fSlide;
sk_sp<sksg::Transform> 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);
SkTSwap(fM0, fM1);
SkTSwap(fOpacity0, fOpacity1);
fTimeBase = 0;
fState = State::kUnfocusing;
}
bool onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state, uint32_t modifiers) {
SkASSERT(fTarget);
// TODO: no SkRect::contains(SkPoint) ?!
if (x < fRect.fLeft || x > fRect.fRight || y < fRect.fTop || y > fRect.fBottom) {
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<sksg::RenderNode> fShade;
sk_sp<sksg::PaintNode> 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<sk_sp<Slide>, true>&& slides, int columns)
: fSlides(std::move(slides))
, fColumns(columns) {
fName = name;
}
static sk_sp<sksg::RenderNode> 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<SlideAdapter>(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<FocusController>(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() * (fSlides.count() / 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<int>(x / fCellSize.width()),
row = static_cast<int>(y / fCellSize.height()),
idx = row * fColumns + col;
return idx <= fRecs.count() ? &fRecs[idx] : nullptr;
}