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