| /* |
| * Copyright 2019 Google LLC |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "SkParticleEffect.h" |
| |
| #include "SkCanvas.h" |
| #include "SkColorData.h" |
| #include "SkPaint.h" |
| #include "SkParticleAffector.h" |
| #include "SkParticleDrawable.h" |
| #include "SkReflected.h" |
| #include "SkRSXform.h" |
| |
| void SkParticleEffectParams::visitFields(SkFieldVisitor* v) { |
| v->visit("MaxCount", fMaxCount); |
| v->visit("Duration", fEffectDuration); |
| v->visit("Rate", fRate); |
| v->visit("Life", fLifetime); |
| |
| v->visit("Drawable", fDrawable); |
| |
| v->visit("Spawn", fSpawnAffectors); |
| v->visit("Update", fUpdateAffectors); |
| } |
| |
| SkParticleEffect::SkParticleEffect(sk_sp<SkParticleEffectParams> params, const SkRandom& random) |
| : fParams(std::move(params)) |
| , fRandom(random) |
| , fLooping(false) |
| , fSpawnTime(-1.0) |
| , fCount(0) |
| , fLastTime(-1.0) |
| , fSpawnRemainder(0.0f) { |
| this->setCapacity(fParams->fMaxCount); |
| } |
| |
| void SkParticleEffect::start(double now, bool looping) { |
| fCount = 0; |
| fLastTime = fSpawnTime = now; |
| fSpawnRemainder = 0.0f; |
| fLooping = looping; |
| } |
| |
| void SkParticleEffect::update(double now) { |
| if (!this->isAlive() || !fParams->fDrawable) { |
| return; |
| } |
| |
| float deltaTime = static_cast<float>(now - fLastTime); |
| if (deltaTime <= 0.0f) { |
| return; |
| } |
| fLastTime = now; |
| |
| // Handle user edits to fMaxCount |
| if (fParams->fMaxCount != fCapacity) { |
| this->setCapacity(fParams->fMaxCount); |
| } |
| |
| float effectAge = static_cast<float>((now - fSpawnTime) / fParams->fEffectDuration); |
| effectAge = fLooping ? fmodf(effectAge, 1.0f) : SkTPin(effectAge, 0.0f, 1.0f); |
| |
| SkParticleUpdateParams updateParams; |
| updateParams.fDeltaTime = deltaTime; |
| updateParams.fEffectAge = effectAge; |
| |
| // During spawn, values that refer to kAge_Source get the *effect* age |
| updateParams.fAgeSource = SkParticleValue::kEffectAge_Source; |
| |
| // Advance age for existing particles, and remove any that have reached their end of life |
| for (int i = 0; i < fCount; ++i) { |
| fParticles[i].fAge += fParticles[i].fInvLifetime * deltaTime; |
| if (fParticles[i].fAge > 1.0f) { |
| // NOTE: This is fast, but doesn't preserve drawing order. Could be a problem... |
| fParticles[i] = fParticles[fCount - 1]; |
| fStableRandoms[i] = fStableRandoms[fCount - 1]; |
| --i; |
| --fCount; |
| } |
| } |
| |
| // Spawn new particles |
| float desired = fParams->fRate * deltaTime + fSpawnRemainder; |
| int numToSpawn = sk_float_round2int(desired); |
| fSpawnRemainder = desired - numToSpawn; |
| numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount); |
| if (numToSpawn) { |
| const int spawnBase = fCount; |
| |
| for (int i = 0; i < numToSpawn; ++i) { |
| // Mutate our SkRandom so each particle definitely gets a different generator |
| fRandom.nextU(); |
| fParticles[fCount].fAge = 0.0f; |
| fParticles[fCount].fPose.fPosition = { 0.0f, 0.0f }; |
| fParticles[fCount].fPose.fHeading = { 0.0f, -1.0f }; |
| fParticles[fCount].fPose.fScale = 1.0f; |
| fParticles[fCount].fVelocity.fLinear = { 0.0f, 0.0f }; |
| fParticles[fCount].fVelocity.fAngular = 0.0f; |
| fParticles[fCount].fColor = { 1.0f, 1.0f, 1.0f, 1.0f }; |
| fParticles[fCount].fFrame = 0.0f; |
| fParticles[fCount].fRandom = fRandom; |
| fCount++; |
| } |
| |
| // Apply spawn affectors |
| for (auto affector : fParams->fSpawnAffectors) { |
| if (affector) { |
| affector->apply(updateParams, fParticles + spawnBase, numToSpawn); |
| } |
| } |
| |
| // Now stash copies of the random generators and compute particle lifetimes |
| // (so the curve can refer to spawn-computed source values) |
| for (int i = spawnBase; i < fCount; ++i) { |
| fParticles[i].fInvLifetime = |
| sk_ieee_float_divide(1.0f, fParams->fLifetime.eval(updateParams, fParticles[i])); |
| fStableRandoms[i] = fParticles[i].fRandom; |
| } |
| } |
| |
| // Restore all stable random generators so update affectors get consistent behavior each frame |
| for (int i = 0; i < fCount; ++i) { |
| fParticles[i].fRandom = fStableRandoms[i]; |
| } |
| |
| // During update, values that refer to kAge_Source get the *particle* age |
| updateParams.fAgeSource = SkParticleValue::kParticleAge_Source; |
| |
| // Apply update rules |
| for (auto affector : fParams->fUpdateAffectors) { |
| if (affector) { |
| affector->apply(updateParams, fParticles, fCount); |
| } |
| } |
| |
| // Do fixed-function update work (integration of position and orientation) |
| for (int i = 0; i < fCount; ++i) { |
| fParticles[i].fPose.fPosition += fParticles[i].fVelocity.fLinear * deltaTime; |
| |
| SkScalar c, s = SkScalarSinCos(fParticles[i].fVelocity.fAngular * deltaTime, &c); |
| SkVector oldHeading = fParticles[i].fPose.fHeading; |
| fParticles[i].fPose.fHeading = { oldHeading.fX * c - oldHeading.fY * s, |
| oldHeading.fX * s + oldHeading.fY * c }; |
| } |
| |
| // Mark effect as dead if we've reached the end (and are not looping) |
| if (!fLooping && (now - fSpawnTime) > fParams->fEffectDuration) { |
| fSpawnTime = -1.0; |
| } |
| } |
| |
| void SkParticleEffect::draw(SkCanvas* canvas) { |
| if (this->isAlive() && fParams->fDrawable) { |
| SkPaint paint; |
| paint.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality); |
| fParams->fDrawable->draw(canvas, fParticles.get(), fCount, &paint); |
| } |
| } |
| |
| void SkParticleEffect::setCapacity(int capacity) { |
| fParticles.realloc(capacity); |
| fStableRandoms.realloc(capacity); |
| |
| fCapacity = capacity; |
| fCount = SkTMin(fCount, fCapacity); |
| } |
| |
| constexpr SkFieldVisitor::EnumStringMapping gValueSourceMapping[] = { |
| { SkParticleValue::kAge_Source, "Age" }, |
| { SkParticleValue::kRandom_Source, "Random" }, |
| { SkParticleValue::kParticleAge_Source, "ParticleAge" }, |
| { SkParticleValue::kEffectAge_Source, "EffectAge" }, |
| { SkParticleValue::kPositionX_Source, "PositionX" }, |
| { SkParticleValue::kPositionY_Source, "PositionY" }, |
| { SkParticleValue::kHeadingX_Source, "HeadingX" }, |
| { SkParticleValue::kHeadingY_Source, "HeadingY" }, |
| { SkParticleValue::kScale_Source, "Scale" }, |
| { SkParticleValue::kVelocityX_Source, "VelocityX" }, |
| { SkParticleValue::kVelocityY_Source, "VelocityY" }, |
| { SkParticleValue::kRotation_Source, "Rotation" }, |
| { SkParticleValue::kColorR_Source, "ColorR" }, |
| { SkParticleValue::kColorG_Source, "ColorG" }, |
| { SkParticleValue::kColorB_Source, "ColorB" }, |
| { SkParticleValue::kColorA_Source, "ColorA" }, |
| { SkParticleValue::kSpriteFrame_Source, "SpriteFrame" }, |
| }; |
| |
| constexpr SkFieldVisitor::EnumStringMapping gValueTileModeMapping[] = { |
| { SkParticleValue::kClamp_TileMode, "Clamp" }, |
| { SkParticleValue::kRepeat_TileMode, "Repeat" }, |
| { SkParticleValue::kMirror_TileMode, "Mirror" }, |
| }; |
| |
| static bool source_needs_frame(int source) { |
| switch (source) { |
| case SkParticleValue::kHeadingX_Source: |
| case SkParticleValue::kHeadingY_Source: |
| case SkParticleValue::kVelocityX_Source: |
| case SkParticleValue::kVelocityY_Source: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| void SkParticleValue::visitFields(SkFieldVisitor* v) { |
| v->visit("Source", fSource, gValueSourceMapping, SK_ARRAY_COUNT(gValueSourceMapping)); |
| if (source_needs_frame(fSource)) { |
| v->visit("Frame", fFrame, gParticleFrameMapping, SK_ARRAY_COUNT(gParticleFrameMapping)); |
| } |
| v->visit("TileMode", fTileMode, gValueTileModeMapping, SK_ARRAY_COUNT(gValueTileModeMapping)); |
| v->visit("Left", fLeft); |
| v->visit("Right", fRight); |
| |
| // Re-compute cached evaluation parameters |
| fScale = sk_float_isfinite(1.0f / (fRight - fLeft)) ? 1.0f / (fRight - fLeft) : 0; |
| fBias = -fLeft * fScale; |
| } |
| |
| float SkParticleValue::getSourceValue(const SkParticleUpdateParams& params, |
| SkParticleState& ps) const { |
| switch ((kAge_Source == fSource) ? params.fAgeSource : fSource) { |
| // Do all the simple (non-frame-dependent) sources first: |
| case kRandom_Source: return ps.fRandom.nextF(); |
| case kParticleAge_Source: return ps.fAge; |
| case kEffectAge_Source: return params.fEffectAge; |
| |
| case kPositionX_Source: return ps.fPose.fPosition.fX; |
| case kPositionY_Source: return ps.fPose.fPosition.fY; |
| case kScale_Source: return ps.fPose.fScale; |
| case kRotation_Source: return ps.fVelocity.fAngular; |
| |
| case kColorR_Source: return ps.fColor.fR; |
| case kColorG_Source: return ps.fColor.fG; |
| case kColorB_Source: return ps.fColor.fB; |
| case kColorA_Source: return ps.fColor.fA; |
| case kSpriteFrame_Source: return ps.fFrame; |
| } |
| |
| SkASSERT(source_needs_frame(fSource)); |
| SkVector frameUp = ps.getFrameHeading(static_cast<SkParticleFrame>(fFrame)); |
| SkVector frameRight = { -frameUp.fY, frameUp.fX }; |
| |
| switch (fSource) { |
| case kHeadingX_Source: return ps.fPose.fHeading.dot(frameRight); |
| case kHeadingY_Source: return ps.fPose.fHeading.dot(frameUp); |
| case kVelocityX_Source: return ps.fVelocity.fLinear.dot(frameRight); |
| case kVelocityY_Source: return ps.fVelocity.fLinear.dot(frameUp); |
| } |
| |
| SkDEBUGFAIL("Unreachable"); |
| return 0.0f; |
| } |
| |
| float SkParticleValue::eval(const SkParticleUpdateParams& params, SkParticleState& ps) const { |
| float v = this->getSourceValue(params, ps); |
| v = (v * fScale) + fBias; |
| |
| switch (fTileMode) { |
| case kClamp_TileMode: |
| v = SkTPin(v, 0.0f, 1.0f); |
| break; |
| case kRepeat_TileMode: |
| v = sk_float_mod(v, 1.0f); |
| if (v < 0) { |
| v += 1.0f; |
| } |
| break; |
| case kMirror_TileMode: |
| v = sk_float_mod(v, 2.0f); |
| if (v < 0) { |
| v += 2.0f; |
| } |
| v = 1.0f - sk_float_abs(v - 1.0f); |
| break; |
| } |
| |
| return v; |
| } |