1 /*
2 * Copyright 2016 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 "include/core/SkCanvas.h"
9 #include "include/core/SkPathBuilder.h"
10 #include "include/core/SkRRect.h"
11 #include "include/private/SkTPin.h"
12 #include "include/utils/SkRandom.h"
13 #include "samplecode/Sample.h"
14 #include "tools/timer/TimeUtils.h"
15
16 #include "modules/sksg/include/SkSGDraw.h"
17 #include "modules/sksg/include/SkSGGroup.h"
18 #include "modules/sksg/include/SkSGInvalidationController.h"
19 #include "modules/sksg/include/SkSGPaint.h"
20 #include "modules/sksg/include/SkSGPath.h"
21 #include "modules/sksg/include/SkSGRect.h"
22 #include "modules/sksg/include/SkSGScene.h"
23 #include "modules/sksg/include/SkSGTransform.h"
24
25 namespace {
26
27 static const SkRect kBounds = SkRect::MakeLTRB(0.1f, 0.1f, 0.9f, 0.9f);
28 static const SkSize kPaddleSize = SkSize::Make(0.03f, 0.1f);
29 static const SkScalar kBallSize = 0.04f;
30 static const SkScalar kShadowOpacity = 0.40f;
31 static const SkScalar kShadowParallax = 0.04f;
32 static const SkScalar kBackgroundStroke = 0.01f;
33 static const uint32_t kBackgroundDashCount = 20;
34
35 static const SkScalar kBallSpeedMax = 0.0020f;
36 static const SkScalar kBallSpeedMin = 0.0005f;
37 static const SkScalar kBallSpeedFuzz = 0.0002f;
38
39 static const SkScalar kTimeScaleMin = 0.0f;
40 static const SkScalar kTimeScaleMax = 5.0f;
41
42 // Box the value within [min, max), by applying infinite reflection on the interval endpoints.
box_reflect(SkScalar v,SkScalar min,SkScalar max)43 SkScalar box_reflect(SkScalar v, SkScalar min, SkScalar max) {
44 const SkScalar intervalLen = max - min;
45 SkASSERT(intervalLen > 0);
46
47 // f(v) is periodic in 2 * intervalLen: one normal progression + one reflection
48 const SkScalar P = intervalLen * 2;
49 // relative to P origin
50 const SkScalar vP = v - min;
51 // map to [0, P)
52 const SkScalar vMod = (vP < 0) ? P - SkScalarMod(-vP, P) : SkScalarMod(vP, P);
53 // reflect if needed, to map to [0, intervalLen)
54 const SkScalar vInterval = vMod < intervalLen ? vMod : P - vMod;
55 // finally, reposition relative to min
56 return vInterval + min;
57 }
58
59 // Compute <t, y> for the trajectory intersection with the next vertical edge.
find_yintercept(const SkPoint & pos,const SkVector & spd,const SkRect & box)60 std::tuple<SkScalar, SkScalar> find_yintercept(const SkPoint& pos, const SkVector& spd,
61 const SkRect& box) {
62 const SkScalar edge = spd.fX > 0 ? box.fRight : box.fLeft;
63 const SkScalar t = (edge - pos.fX) / spd.fX;
64 SkASSERT(t >= 0);
65 const SkScalar dY = t * spd.fY;
66
67 return std::make_tuple(t, box_reflect(pos.fY + dY, box.fTop, box.fBottom));
68 }
69
update_pos(const sk_sp<sksg::RRect> & rr,const SkPoint & pos)70 void update_pos(const sk_sp<sksg::RRect>& rr, const SkPoint& pos) {
71 // TODO: position setters on RRect?
72
73 const auto r = rr->getRRect().rect();
74 const auto offsetX = pos.x() - r.x(),
75 offsetY = pos.y() - r.y();
76 rr->setRRect(rr->getRRect().makeOffset(offsetX, offsetY));
77 }
78
79 } // namespace
80
81 class PongView final : public Sample {
82 public:
83 PongView() = default;
84
85 protected:
onOnceBeforeDraw()86 void onOnceBeforeDraw() override {
87 const SkRect fieldBounds = kBounds.makeOutset(kBallSize / 2, kBallSize / 2);
88 const SkRRect ball = SkRRect::MakeOval(SkRect::MakeWH(kBallSize, kBallSize));
89 const SkRRect paddle = SkRRect::MakeRectXY(SkRect::MakeWH(kPaddleSize.width(),
90 kPaddleSize.height()),
91 kPaddleSize.width() / 2,
92 kPaddleSize.width() / 2);
93 fBall.initialize(ball,
94 SkPoint::Make(kBounds.centerX(), kBounds.centerY()),
95 SkVector::Make(fRand.nextRangeScalar(kBallSpeedMin, kBallSpeedMax),
96 fRand.nextRangeScalar(kBallSpeedMin, kBallSpeedMax)));
97 fPaddle0.initialize(paddle,
98 SkPoint::Make(fieldBounds.left() - kPaddleSize.width() / 2,
99 fieldBounds.centerY()),
100 SkVector::Make(0, 0));
101 fPaddle1.initialize(paddle,
102 SkPoint::Make(fieldBounds.right() + kPaddleSize.width() / 2,
103 fieldBounds.centerY()),
104 SkVector::Make(0, 0));
105
106 // Background decoration.
107 SkPathBuilder bgPath;
108 bgPath.moveTo(kBounds.left() , fieldBounds.top())
109 .lineTo(kBounds.right(), fieldBounds.top())
110 .moveTo(kBounds.left() , fieldBounds.bottom())
111 .lineTo(kBounds.right(), fieldBounds.bottom());
112 // TODO: stroke-dash support would come in handy right about now.
113 for (uint32_t i = 0; i < kBackgroundDashCount; ++i) {
114 bgPath.moveTo(kBounds.centerX(),
115 kBounds.top() + (i + 0.25f) * kBounds.height() / kBackgroundDashCount)
116 .lineTo(kBounds.centerX(),
117 kBounds.top() + (i + 0.75f) * kBounds.height() / kBackgroundDashCount);
118 }
119
120 auto bg_path = sksg::Path::Make(bgPath.detach());
121 auto bg_paint = sksg::Color::Make(SK_ColorBLACK);
122 bg_paint->setStyle(SkPaint::kStroke_Style);
123 bg_paint->setStrokeWidth(kBackgroundStroke);
124
125 auto ball_paint = sksg::Color::Make(SK_ColorGREEN),
126 paddle0_paint = sksg::Color::Make(SK_ColorBLUE),
127 paddle1_paint = sksg::Color::Make(SK_ColorRED),
128 shadow_paint = sksg::Color::Make(SK_ColorBLACK);
129 ball_paint->setAntiAlias(true);
130 paddle0_paint->setAntiAlias(true);
131 paddle1_paint->setAntiAlias(true);
132 shadow_paint->setAntiAlias(true);
133 shadow_paint->setOpacity(kShadowOpacity);
134
135 // Build the scene graph.
136 auto group = sksg::Group::Make();
137 group->addChild(sksg::Draw::Make(std::move(bg_path), std::move(bg_paint)));
138 group->addChild(sksg::Draw::Make(fPaddle0.shadowNode, shadow_paint));
139 group->addChild(sksg::Draw::Make(fPaddle1.shadowNode, shadow_paint));
140 group->addChild(sksg::Draw::Make(fBall.shadowNode, shadow_paint));
141 group->addChild(sksg::Draw::Make(fPaddle0.objectNode, paddle0_paint));
142 group->addChild(sksg::Draw::Make(fPaddle1.objectNode, paddle1_paint));
143 group->addChild(sksg::Draw::Make(fBall.objectNode, ball_paint));
144
145 // Handle everything in a normalized 1x1 space.
146 fContentMatrix = sksg::Matrix<SkMatrix>::Make(
147 SkMatrix::RectToRect(SkRect::MakeWH(1, 1),
148 SkRect::MakeIWH(this->width(), this->height())));
149 auto root = sksg::TransformEffect::Make(std::move(group), fContentMatrix);
150 fScene = sksg::Scene::Make(std::move(root));
151
152 // Off we go.
153 this->updatePaddleStrategy();
154 }
155
name()156 SkString name() override { return SkString("SGPong"); }
157
onChar(SkUnichar uni)158 bool onChar(SkUnichar uni) override {
159 switch (uni) {
160 case '[':
161 fTimeScale = SkTPin(fTimeScale - 0.1f, kTimeScaleMin, kTimeScaleMax);
162 return true;
163 case ']':
164 fTimeScale = SkTPin(fTimeScale + 0.1f, kTimeScaleMin, kTimeScaleMax);
165 return true;
166 case 'I':
167 fShowInval = !fShowInval;
168 return true;
169 default:
170 break;
171 }
172 return false;
173 }
174
onSizeChange()175 void onSizeChange() override {
176 if (fContentMatrix) {
177 fContentMatrix->setMatrix(SkMatrix::RectToRect(SkRect::MakeWH(1, 1),
178 SkRect::MakeIWH(this->width(),
179 this->height())));
180 }
181
182 this->INHERITED::onSizeChange();
183 }
184
onDrawContent(SkCanvas * canvas)185 void onDrawContent(SkCanvas* canvas) override {
186 sksg::InvalidationController ic;
187 fScene->render(canvas);
188
189 if (fShowInval) {
190 SkPaint fill, stroke;
191 fill.setAntiAlias(true);
192 fill.setColor(0x40ff0000);
193 stroke.setAntiAlias(true);
194 stroke.setColor(0xffff0000);
195 stroke.setStyle(SkPaint::kStroke_Style);
196
197 for (const auto& r : ic) {
198 canvas->drawRect(r, fill);
199 canvas->drawRect(r, stroke);
200 }
201 }
202 }
203
onAnimate(double nanos)204 bool onAnimate(double nanos) override {
205 // onAnimate may fire before the first draw.
206 if (fScene) {
207 SkScalar dt = (TimeUtils::NanosToMSec(nanos) - fLastTick) * fTimeScale;
208 fLastTick = TimeUtils::NanosToMSec(nanos);
209
210 fPaddle0.posTick(dt);
211 fPaddle1.posTick(dt);
212 fBall.posTick(dt);
213
214 this->enforceConstraints();
215
216 fPaddle0.updateDom();
217 fPaddle1.updateDom();
218 fBall.updateDom();
219 }
220 return true;
221 }
222
223 private:
224 struct Object {
initializePongView::Object225 void initialize(const SkRRect& rrect, const SkPoint& p, const SkVector& s) {
226 objectNode = sksg::RRect::Make(rrect);
227 shadowNode = sksg::RRect::Make(rrect);
228
229 pos = p;
230 spd = s;
231 size = SkSize::Make(rrect.width(), rrect.height());
232 }
233
posTickPongView::Object234 void posTick(SkScalar dt) {
235 pos += spd * dt;
236 }
237
updateDomPongView::Object238 void updateDom() {
239 const SkPoint corner = pos - SkPoint::Make(size.width() / 2, size.height() / 2);
240 update_pos(objectNode, corner);
241
242 // Simulate parallax shadow for a centered light source.
243 SkPoint shadowOffset = pos - SkPoint::Make(kBounds.centerX(), kBounds.centerY());
244 shadowOffset.scale(kShadowParallax);
245 const SkPoint shadowCorner = corner + shadowOffset;
246
247 update_pos(shadowNode, shadowCorner);
248 }
249
250 sk_sp<sksg::RRect> objectNode,
251 shadowNode;
252 SkPoint pos;
253 SkVector spd;
254 SkSize size;
255 };
256
enforceConstraints()257 void enforceConstraints() {
258 // Perfect vertical reflection.
259 if (fBall.pos.fY < kBounds.fTop || fBall.pos.fY >= kBounds.fBottom) {
260 fBall.spd.fY = -fBall.spd.fY;
261 fBall.pos.fY = box_reflect(fBall.pos.fY, kBounds.fTop, kBounds.fBottom);
262 }
263
264 // Horizontal bounce - introduces a speed fuzz.
265 if (fBall.pos.fX < kBounds.fLeft || fBall.pos.fX >= kBounds.fRight) {
266 fBall.spd.fX = this->fuzzBallSpeed(-fBall.spd.fX);
267 fBall.spd.fY = this->fuzzBallSpeed(fBall.spd.fY);
268 fBall.pos.fX = box_reflect(fBall.pos.fX, kBounds.fLeft, kBounds.fRight);
269 this->updatePaddleStrategy();
270 }
271 }
272
fuzzBallSpeed(SkScalar spd)273 SkScalar fuzzBallSpeed(SkScalar spd) {
274 // The speed limits are absolute values.
275 const SkScalar sign = spd >= 0 ? 1.0f : -1.0f;
276 const SkScalar fuzzed = fabs(spd) + fRand.nextRangeScalar(-kBallSpeedFuzz, kBallSpeedFuzz);
277
278 return sign * SkTPin(fuzzed, kBallSpeedMin, kBallSpeedMax);
279 }
280
updatePaddleStrategy()281 void updatePaddleStrategy() {
282 Object* pitcher = fBall.spd.fX > 0 ? &fPaddle0 : &fPaddle1;
283 Object* catcher = fBall.spd.fX > 0 ? &fPaddle1 : &fPaddle0;
284
285 SkScalar t, yIntercept;
286 std::tie(t, yIntercept) = find_yintercept(fBall.pos, fBall.spd, kBounds);
287
288 // The pitcher aims for a neutral/centered position.
289 pitcher->spd.fY = (kBounds.centerY() - pitcher->pos.fY) / t;
290
291 // The catcher goes for the ball. Duh.
292 catcher->spd.fY = (yIntercept - catcher->pos.fY) / t;
293 }
294
295 std::unique_ptr<sksg::Scene> fScene;
296 sk_sp<sksg::Matrix<SkMatrix>> fContentMatrix;
297 Object fPaddle0, fPaddle1, fBall;
298 SkRandom fRand;
299
300 SkMSec fLastTick = 0;
301 SkScalar fTimeScale = 1.0f;
302 bool fShowInval = false;
303
304 using INHERITED = Sample;
305 };
306
307 DEF_SAMPLE( return new PongView(); )
308