1 /* 2 * Copyright 2017 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 "SkShadowUtils.h" 9 #include "SkCanvas.h" 10 #include "SkColorFilter.h" 11 #include "SkPath.h" 12 #include "SkRandom.h" 13 #include "SkResourceCache.h" 14 #include "SkShadowTessellator.h" 15 #include "SkString.h" 16 #include "SkTLazy.h" 17 #include "SkVertices.h" 18 #if SK_SUPPORT_GPU 19 #include "GrShape.h" 20 #include "effects/GrBlurredEdgeFragmentProcessor.h" 21 #endif 22 #include "../../src/effects/shadows/SkAmbientShadowMaskFilter.h" 23 #include "../../src/effects/shadows/SkSpotShadowMaskFilter.h" 24 25 /** 26 * Gaussian color filter -- produces a Gaussian ramp based on the color's B value, 27 * then blends with the color's G value. 28 * Final result is black with alpha of Gaussian(B)*G. 29 * The assumption is that the original color's alpha is 1. 30 */ 31 class SK_API SkGaussianColorFilter : public SkColorFilter { 32 public: 33 static sk_sp<SkColorFilter> Make() { 34 return sk_sp<SkColorFilter>(new SkGaussianColorFilter); 35 } 36 37 void filterSpan(const SkPMColor src[], int count, SkPMColor dst[]) const override; 38 39 #if SK_SUPPORT_GPU 40 sk_sp<GrFragmentProcessor> asFragmentProcessor(GrContext*, SkColorSpace*) const override; 41 #endif 42 43 SK_TO_STRING_OVERRIDE() 44 SK_DECLARE_PUBLIC_FLATTENABLE_DESERIALIZATION_PROCS(SkGaussianColorFilter) 45 46 protected: 47 void flatten(SkWriteBuffer&) const override {} 48 49 private: 50 SkGaussianColorFilter() : INHERITED() {} 51 52 typedef SkColorFilter INHERITED; 53 }; 54 55 void SkGaussianColorFilter::filterSpan(const SkPMColor src[], int count, SkPMColor dst[]) const { 56 for (int i = 0; i < count; ++i) { 57 SkPMColor c = src[i]; 58 59 SkScalar factor = SK_Scalar1 - SkGetPackedB32(c) / 255.f; 60 factor = SkScalarExp(-factor * factor * 4) - 0.018f; 61 62 SkScalar a = factor * SkGetPackedG32(c); 63 dst[i] = SkPackARGB32(a, a, a, a); 64 } 65 } 66 67 sk_sp<SkFlattenable> SkGaussianColorFilter::CreateProc(SkReadBuffer&) { 68 return Make(); 69 } 70 71 #ifndef SK_IGNORE_TO_STRING 72 void SkGaussianColorFilter::toString(SkString* str) const { 73 str->append("SkGaussianColorFilter "); 74 } 75 #endif 76 77 #if SK_SUPPORT_GPU 78 79 sk_sp<GrFragmentProcessor> SkGaussianColorFilter::asFragmentProcessor(GrContext*, 80 SkColorSpace*) const { 81 return GrBlurredEdgeFP::Make(GrBlurredEdgeFP::kGaussian_Mode); 82 } 83 #endif 84 85 /////////////////////////////////////////////////////////////////////////////////////////////////// 86 87 namespace { 88 89 uint64_t resource_cache_shared_id() { 90 return 0x2020776f64616873llu; // 'shadow ' 91 } 92 93 /** Factory for an ambient shadow mesh with particular shadow properties. */ 94 struct AmbientVerticesFactory { 95 SkScalar fOccluderHeight = SK_ScalarNaN; // NaN so that isCompatible will fail until init'ed. 96 SkScalar fAmbientAlpha; 97 bool fTransparent; 98 99 bool isCompatible(const AmbientVerticesFactory& that, SkVector* translate) const { 100 if (fOccluderHeight != that.fOccluderHeight || fAmbientAlpha != that.fAmbientAlpha || 101 fTransparent != that.fTransparent) { 102 return false; 103 } 104 translate->set(0, 0); 105 return true; 106 } 107 108 sk_sp<SkVertices> makeVertices(const SkPath& path, const SkMatrix& ctm) const { 109 SkScalar z = fOccluderHeight; 110 return SkShadowTessellator::MakeAmbient(path, ctm, 111 [z](SkScalar, SkScalar) { return z; }, 112 fAmbientAlpha, fTransparent); 113 } 114 }; 115 116 /** Factory for an spot shadow mesh with particular shadow properties. */ 117 struct SpotVerticesFactory { 118 enum class OccluderType { 119 // The umbra cannot be dropped out because the occluder is not opaque. 120 kTransparent, 121 // The umbra can be dropped where it is occluded. 122 kOpaque, 123 // It is known that the entire umbra is occluded. 124 kOpaqueCoversUmbra 125 }; 126 127 SkVector fOffset; 128 SkScalar fOccluderHeight = SK_ScalarNaN; // NaN so that isCompatible will fail until init'ed. 129 SkPoint3 fDevLightPos; 130 SkScalar fLightRadius; 131 SkScalar fSpotAlpha; 132 OccluderType fOccluderType; 133 134 bool isCompatible(const SpotVerticesFactory& that, SkVector* translate) const { 135 if (fOccluderHeight != that.fOccluderHeight || fDevLightPos.fZ != that.fDevLightPos.fZ || 136 fLightRadius != that.fLightRadius || fSpotAlpha != that.fSpotAlpha || 137 fOccluderType != that.fOccluderType) { 138 return false; 139 } 140 switch (fOccluderType) { 141 case OccluderType::kTransparent: 142 case OccluderType::kOpaqueCoversUmbra: 143 // 'this' and 'that' will either both have no umbra removed or both have all the 144 // umbra removed. 145 *translate = that.fOffset - fOffset; 146 return true; 147 case OccluderType::kOpaque: 148 // In this case we partially remove the umbra differently for 'this' and 'that' 149 // if the offsets don't match. 150 if (fOffset == that.fOffset) { 151 translate->set(0, 0); 152 return true; 153 } 154 return false; 155 } 156 SkFAIL("Uninitialized occluder type?"); 157 return false; 158 } 159 160 sk_sp<SkVertices> makeVertices(const SkPath& path, const SkMatrix& ctm) const { 161 bool transparent = OccluderType::kTransparent == fOccluderType; 162 SkScalar z = fOccluderHeight; 163 return SkShadowTessellator::MakeSpot(path, ctm, 164 [z](SkScalar, SkScalar) -> SkScalar { return z; }, 165 fDevLightPos, fLightRadius, 166 fSpotAlpha, transparent); 167 } 168 }; 169 170 /** 171 * This manages a set of tessellations for a given shape in the cache. Because SkResourceCache 172 * records are immutable this is not itself a Rec. When we need to update it we return this on 173 * the FindVisitor and let the cache destory the Rec. We'll update the tessellations and then add 174 * a new Rec with an adjusted size for any deletions/additions. 175 */ 176 class CachedTessellations : public SkRefCnt { 177 public: 178 size_t size() const { return fAmbientSet.size() + fSpotSet.size(); } 179 180 sk_sp<SkVertices> find(const AmbientVerticesFactory& ambient, const SkMatrix& matrix, 181 SkVector* translate) const { 182 return fAmbientSet.find(ambient, matrix, translate); 183 } 184 185 sk_sp<SkVertices> add(const SkPath& devPath, const AmbientVerticesFactory& ambient, 186 const SkMatrix& matrix) { 187 return fAmbientSet.add(devPath, ambient, matrix); 188 } 189 190 sk_sp<SkVertices> find(const SpotVerticesFactory& spot, const SkMatrix& matrix, 191 SkVector* translate) const { 192 return fSpotSet.find(spot, matrix, translate); 193 } 194 195 sk_sp<SkVertices> add(const SkPath& devPath, const SpotVerticesFactory& spot, 196 const SkMatrix& matrix) { 197 return fSpotSet.add(devPath, spot, matrix); 198 } 199 200 private: 201 template <typename FACTORY, int MAX_ENTRIES> 202 class Set { 203 public: 204 size_t size() const { return fSize; } 205 206 sk_sp<SkVertices> find(const FACTORY& factory, const SkMatrix& matrix, 207 SkVector* translate) const { 208 for (int i = 0; i < MAX_ENTRIES; ++i) { 209 if (fEntries[i].fFactory.isCompatible(factory, translate)) { 210 const SkMatrix& m = fEntries[i].fMatrix; 211 if (matrix.hasPerspective() || m.hasPerspective()) { 212 if (matrix != fEntries[i].fMatrix) { 213 continue; 214 } 215 } else if (matrix.getScaleX() != m.getScaleX() || 216 matrix.getSkewX() != m.getSkewX() || 217 matrix.getScaleY() != m.getScaleY() || 218 matrix.getSkewY() != m.getSkewY()) { 219 continue; 220 } 221 *translate += SkVector{matrix.getTranslateX() - m.getTranslateX(), 222 matrix.getTranslateY() - m.getTranslateY()}; 223 return fEntries[i].fVertices; 224 } 225 } 226 return nullptr; 227 } 228 229 sk_sp<SkVertices> add(const SkPath& path, const FACTORY& factory, const SkMatrix& matrix) { 230 sk_sp<SkVertices> vertices = factory.makeVertices(path, matrix); 231 if (!vertices) { 232 return nullptr; 233 } 234 int i; 235 if (fCount < MAX_ENTRIES) { 236 i = fCount++; 237 } else { 238 i = gRandom.nextULessThan(MAX_ENTRIES); 239 fSize -= fEntries[i].fVertices->approximateSize(); 240 } 241 fEntries[i].fFactory = factory; 242 fEntries[i].fVertices = vertices; 243 fEntries[i].fMatrix = matrix; 244 fSize += vertices->approximateSize(); 245 return vertices; 246 } 247 248 private: 249 struct Entry { 250 FACTORY fFactory; 251 sk_sp<SkVertices> fVertices; 252 SkMatrix fMatrix; 253 }; 254 Entry fEntries[MAX_ENTRIES]; 255 int fCount = 0; 256 size_t fSize = 0; 257 }; 258 259 Set<AmbientVerticesFactory, 4> fAmbientSet; 260 Set<SpotVerticesFactory, 4> fSpotSet; 261 262 static SkRandom gRandom; 263 }; 264 265 SkRandom CachedTessellations::gRandom; 266 267 /** 268 * A record of shadow vertices stored in SkResourceCache of CachedTessellations for a particular 269 * path. The key represents the path's geometry and not any shadow params. 270 */ 271 class CachedTessellationsRec : public SkResourceCache::Rec { 272 public: 273 CachedTessellationsRec(const SkResourceCache::Key& key, 274 sk_sp<CachedTessellations> tessellations) 275 : fTessellations(std::move(tessellations)) { 276 fKey.reset(new uint8_t[key.size()]); 277 memcpy(fKey.get(), &key, key.size()); 278 } 279 280 const Key& getKey() const override { 281 return *reinterpret_cast<SkResourceCache::Key*>(fKey.get()); 282 } 283 284 size_t bytesUsed() const override { return fTessellations->size(); } 285 286 const char* getCategory() const override { return "tessellated shadow masks"; } 287 288 sk_sp<CachedTessellations> refTessellations() const { return fTessellations; } 289 290 template <typename FACTORY> 291 sk_sp<SkVertices> find(const FACTORY& factory, const SkMatrix& matrix, 292 SkVector* translate) const { 293 return fTessellations->find(factory, matrix, translate); 294 } 295 296 private: 297 std::unique_ptr<uint8_t[]> fKey; 298 sk_sp<CachedTessellations> fTessellations; 299 }; 300 301 /** 302 * Used by FindVisitor to determine whether a cache entry can be reused and if so returns the 303 * vertices and a translation vector. If the CachedTessellations does not contain a suitable 304 * mesh then we inform SkResourceCache to destroy the Rec and we return the CachedTessellations 305 * to the caller. The caller will update it and reinsert it back into the cache. 306 */ 307 template <typename FACTORY> 308 struct FindContext { 309 FindContext(const SkMatrix* viewMatrix, const FACTORY* factory) 310 : fViewMatrix(viewMatrix), fFactory(factory) {} 311 const SkMatrix* const fViewMatrix; 312 // If this is valid after Find is called then we found the vertices and they should be drawn 313 // with fTranslate applied. 314 sk_sp<SkVertices> fVertices; 315 SkVector fTranslate = {0, 0}; 316 317 // If this is valid after Find then the caller should add the vertices to the tessellation set 318 // and create a new CachedTessellationsRec and insert it into SkResourceCache. 319 sk_sp<CachedTessellations> fTessellationsOnFailure; 320 321 const FACTORY* fFactory; 322 }; 323 324 /** 325 * Function called by SkResourceCache when a matching cache key is found. The FACTORY and matrix of 326 * the FindContext are used to determine if the vertices are reusable. If so the vertices and 327 * necessary translation vector are set on the FindContext. 328 */ 329 template <typename FACTORY> 330 bool FindVisitor(const SkResourceCache::Rec& baseRec, void* ctx) { 331 FindContext<FACTORY>* findContext = (FindContext<FACTORY>*)ctx; 332 const CachedTessellationsRec& rec = static_cast<const CachedTessellationsRec&>(baseRec); 333 findContext->fVertices = 334 rec.find(*findContext->fFactory, *findContext->fViewMatrix, &findContext->fTranslate); 335 if (findContext->fVertices) { 336 return true; 337 } 338 // We ref the tessellations and let the cache destroy the Rec. Once the tessellations have been 339 // manipulated we will add a new Rec. 340 findContext->fTessellationsOnFailure = rec.refTessellations(); 341 return false; 342 } 343 344 class ShadowedPath { 345 public: 346 ShadowedPath(const SkPath* path, const SkMatrix* viewMatrix) 347 : fPath(path) 348 , fViewMatrix(viewMatrix) 349 #if SK_SUPPORT_GPU 350 , fShapeForKey(*path, GrStyle::SimpleFill()) 351 #endif 352 {} 353 354 const SkPath& path() const { return *fPath; } 355 const SkMatrix& viewMatrix() const { return *fViewMatrix; } 356 #if SK_SUPPORT_GPU 357 /** Negative means the vertices should not be cached for this path. */ 358 int keyBytes() const { return fShapeForKey.unstyledKeySize() * sizeof(uint32_t); } 359 void writeKey(void* key) const { 360 fShapeForKey.writeUnstyledKey(reinterpret_cast<uint32_t*>(key)); 361 } 362 bool isRRect(SkRRect* rrect) { return fShapeForKey.asRRect(rrect, nullptr, nullptr, nullptr); } 363 #else 364 int keyBytes() const { return -1; } 365 void writeKey(void* key) const { SkFAIL("Should never be called"); } 366 bool isRRect(SkRRect* rrect) { return false; } 367 #endif 368 369 private: 370 const SkPath* fPath; 371 const SkMatrix* fViewMatrix; 372 #if SK_SUPPORT_GPU 373 GrShape fShapeForKey; 374 #endif 375 }; 376 377 // This creates a domain of keys in SkResourceCache used by this file. 378 static void* kNamespace; 379 380 /** 381 * Draws a shadow to 'canvas'. The vertices used to draw the shadow are created by 'factory' unless 382 * they are first found in SkResourceCache. 383 */ 384 template <typename FACTORY> 385 void draw_shadow(const FACTORY& factory, SkCanvas* canvas, ShadowedPath& path, SkColor color, 386 SkResourceCache* cache) { 387 FindContext<FACTORY> context(&path.viewMatrix(), &factory); 388 389 SkResourceCache::Key* key = nullptr; 390 SkAutoSTArray<32 * 4, uint8_t> keyStorage; 391 int keyDataBytes = path.keyBytes(); 392 if (keyDataBytes >= 0) { 393 keyStorage.reset(keyDataBytes + sizeof(SkResourceCache::Key)); 394 key = new (keyStorage.begin()) SkResourceCache::Key(); 395 path.writeKey((uint32_t*)(keyStorage.begin() + sizeof(*key))); 396 key->init(&kNamespace, resource_cache_shared_id(), keyDataBytes); 397 if (cache) { 398 cache->find(*key, FindVisitor<FACTORY>, &context); 399 } else { 400 SkResourceCache::Find(*key, FindVisitor<FACTORY>, &context); 401 } 402 } 403 404 sk_sp<SkVertices> vertices; 405 const SkVector* translate; 406 static constexpr SkVector kZeroTranslate = {0, 0}; 407 bool foundInCache = SkToBool(context.fVertices); 408 if (foundInCache) { 409 vertices = std::move(context.fVertices); 410 translate = &context.fTranslate; 411 } else { 412 // TODO: handle transforming the path as part of the tessellator 413 if (key) { 414 // Update or initialize a tessellation set and add it to the cache. 415 sk_sp<CachedTessellations> tessellations; 416 if (context.fTessellationsOnFailure) { 417 tessellations = std::move(context.fTessellationsOnFailure); 418 } else { 419 tessellations.reset(new CachedTessellations()); 420 } 421 vertices = tessellations->add(path.path(), factory, path.viewMatrix()); 422 if (!vertices) { 423 return; 424 } 425 auto rec = new CachedTessellationsRec(*key, std::move(tessellations)); 426 if (cache) { 427 cache->add(rec); 428 } else { 429 SkResourceCache::Add(rec); 430 } 431 } else { 432 vertices = factory.makeVertices(path.path(), path.viewMatrix()); 433 if (!vertices) { 434 return; 435 } 436 } 437 translate = &kZeroTranslate; 438 } 439 440 SkPaint paint; 441 // Run the vertex color through a GaussianColorFilter and then modulate the grayscale result of 442 // that against our 'color' param. 443 paint.setColorFilter(SkColorFilter::MakeComposeFilter( 444 SkColorFilter::MakeModeFilter(color, SkBlendMode::kModulate), 445 SkGaussianColorFilter::Make())); 446 if (translate->fX || translate->fY) { 447 canvas->save(); 448 canvas->translate(translate->fX, translate->fY); 449 } 450 canvas->drawVertices(vertices, SkBlendMode::kModulate, paint); 451 if (translate->fX || translate->fY) { 452 canvas->restore(); 453 } 454 } 455 } 456 457 // Draw an offset spot shadow and outlining ambient shadow for the given path. 458 void SkShadowUtils::DrawShadow(SkCanvas* canvas, const SkPath& path, SkScalar occluderHeight, 459 const SkPoint3& devLightPos, SkScalar lightRadius, 460 SkScalar ambientAlpha, SkScalar spotAlpha, SkColor color, 461 uint32_t flags, SkResourceCache* cache) { 462 SkAutoCanvasRestore acr(canvas, true); 463 SkMatrix viewMatrix = canvas->getTotalMatrix(); 464 465 // try circular fast path 466 SkRect rect; 467 if (viewMatrix.isSimilarity() && 468 path.isOval(&rect) && rect.width() == rect.height()) { 469 SkPaint newPaint; 470 newPaint.setColor(color); 471 if (ambientAlpha > 0) { 472 newPaint.setMaskFilter(SkAmbientShadowMaskFilter::Make(occluderHeight, ambientAlpha, 473 flags)); 474 canvas->drawPath(path, newPaint); 475 } 476 if (spotAlpha > 0) { 477 newPaint.setMaskFilter(SkSpotShadowMaskFilter::Make(occluderHeight, devLightPos, 478 lightRadius, spotAlpha, flags)); 479 canvas->drawPath(path, newPaint); 480 } 481 return; 482 } 483 484 canvas->resetMatrix(); 485 486 ShadowedPath shadowedPath(&path, &viewMatrix); 487 488 bool transparent = SkToBool(flags & SkShadowFlags::kTransparentOccluder_ShadowFlag); 489 490 if (ambientAlpha > 0) { 491 ambientAlpha = SkTMin(ambientAlpha, 1.f); 492 AmbientVerticesFactory factory; 493 factory.fOccluderHeight = occluderHeight; 494 factory.fAmbientAlpha = ambientAlpha; 495 factory.fTransparent = transparent; 496 497 draw_shadow(factory, canvas, shadowedPath, color, cache); 498 } 499 500 if (spotAlpha > 0) { 501 spotAlpha = SkTMin(spotAlpha, 1.f); 502 SpotVerticesFactory factory; 503 float zRatio = SkTPin(occluderHeight / (devLightPos.fZ - occluderHeight), 0.0f, 0.95f); 504 SkScalar radius = lightRadius * zRatio; 505 506 // Compute the scale and translation for the spot shadow. 507 SkScalar scale = devLightPos.fZ / (devLightPos.fZ - occluderHeight); 508 509 SkPoint center = SkPoint::Make(path.getBounds().centerX(), path.getBounds().centerY()); 510 viewMatrix.mapPoints(¢er, 1); 511 factory.fOffset = SkVector::Make(zRatio * (center.fX - devLightPos.fX), 512 zRatio * (center.fY - devLightPos.fY)); 513 factory.fOccluderHeight = occluderHeight; 514 factory.fDevLightPos = devLightPos; 515 factory.fLightRadius = lightRadius; 516 factory.fSpotAlpha = spotAlpha; 517 518 SkRRect rrect; 519 if (transparent) { 520 factory.fOccluderType = SpotVerticesFactory::OccluderType::kTransparent; 521 } else { 522 factory.fOccluderType = SpotVerticesFactory::OccluderType::kOpaque; 523 if (shadowedPath.isRRect(&rrect)) { 524 SkRRect devRRect; 525 if (rrect.transform(viewMatrix, &devRRect)) { 526 SkScalar s = 1.f - scale; 527 SkScalar w = devRRect.width(); 528 SkScalar h = devRRect.height(); 529 SkScalar hw = w / 2.f; 530 SkScalar hh = h / 2.f; 531 SkScalar umbraInsetX = s * hw + radius; 532 SkScalar umbraInsetY = s * hh + radius; 533 // The umbra is inset by radius along the diagonal, so adjust for that. 534 SkScalar d = 1.f / SkScalarSqrt(hw * hw + hh * hh); 535 umbraInsetX *= hw * d; 536 umbraInsetY *= hh * d; 537 if (umbraInsetX > hw || umbraInsetY > hh) { 538 // There is no umbra to occlude. 539 factory.fOccluderType = SpotVerticesFactory::OccluderType::kTransparent; 540 } else if (fabsf(factory.fOffset.fX) < umbraInsetX && 541 fabsf(factory.fOffset.fY) < umbraInsetY) { 542 factory.fOccluderType = 543 SpotVerticesFactory::OccluderType::kOpaqueCoversUmbra; 544 } else if (factory.fOffset.fX > w - umbraInsetX || 545 factory.fOffset.fY > h - umbraInsetY) { 546 // There umbra is fully exposed, there is nothing to omit. 547 factory.fOccluderType = SpotVerticesFactory::OccluderType::kTransparent; 548 } 549 } 550 } 551 } 552 if (factory.fOccluderType == SpotVerticesFactory::OccluderType::kOpaque) { 553 factory.fOccluderType = SpotVerticesFactory::OccluderType::kTransparent; 554 } 555 draw_shadow(factory, canvas, shadowedPath, color, cache); 556 } 557 } 558 559 // Draw an offset spot shadow and outlining ambient shadow for the given path, 560 // without caching and using a function based on local position to compute the height. 561 void SkShadowUtils::DrawUncachedShadow(SkCanvas* canvas, const SkPath& path, 562 std::function<SkScalar(SkScalar, SkScalar)> heightFunc, 563 const SkPoint3& lightPos, SkScalar lightRadius, 564 SkScalar ambientAlpha, SkScalar spotAlpha, SkColor color, 565 uint32_t flags) { 566 SkAutoCanvasRestore acr(canvas, true); 567 SkMatrix viewMatrix = canvas->getTotalMatrix(); 568 canvas->resetMatrix(); 569 570 bool transparent = SkToBool(flags & SkShadowFlags::kTransparentOccluder_ShadowFlag); 571 572 if (ambientAlpha > 0) { 573 ambientAlpha = SkTMin(ambientAlpha, 1.f); 574 sk_sp<SkVertices> vertices = SkShadowTessellator::MakeAmbient(path, viewMatrix, 575 heightFunc, ambientAlpha, 576 transparent); 577 SkPaint paint; 578 // Run the vertex color through a GaussianColorFilter and then modulate the grayscale 579 // result of that against our 'color' param. 580 paint.setColorFilter(SkColorFilter::MakeComposeFilter( 581 SkColorFilter::MakeModeFilter(color, SkBlendMode::kModulate), 582 SkGaussianColorFilter::Make())); 583 canvas->drawVertices(vertices, SkBlendMode::kModulate, paint); 584 } 585 586 if (spotAlpha > 0) { 587 spotAlpha = SkTMin(spotAlpha, 1.f); 588 sk_sp<SkVertices> vertices = SkShadowTessellator::MakeSpot(path, viewMatrix, heightFunc, 589 lightPos, lightRadius, 590 spotAlpha, transparent); 591 SkPaint paint; 592 // Run the vertex color through a GaussianColorFilter and then modulate the grayscale 593 // result of that against our 'color' param. 594 paint.setColorFilter(SkColorFilter::MakeComposeFilter( 595 SkColorFilter::MakeModeFilter(color, SkBlendMode::kModulate), 596 SkGaussianColorFilter::Make())); 597 canvas->drawVertices(vertices, SkBlendMode::kModulate, paint); 598 } 599 } 600