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