1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qquicktrailemitter_p.h" |
5 | #include <private/qqmlengine_p.h> |
6 | #include <private/qqmlglobal_p.h> |
7 | #include <private/qjsvalue_p.h> |
8 | #include <QRandomGenerator> |
9 | #include <cmath> |
10 | QT_BEGIN_NAMESPACE |
11 | |
12 | /*! |
13 | \qmltype TrailEmitter |
14 | \instantiates QQuickTrailEmitter |
15 | \inqmlmodule QtQuick.Particles |
16 | \inherits QQuickParticleEmitter |
17 | \brief Emits logical particles from other logical particles. |
18 | \ingroup qtquick-particles |
19 | |
20 | This element emits logical particles into the ParticleSystem, with the |
21 | starting positions based on those of other logical particles. |
22 | */ |
23 | QQuickTrailEmitter::QQuickTrailEmitter(QQuickItem *parent) : |
24 | QQuickParticleEmitter(parent) |
25 | , m_particlesPerParticlePerSecond(0) |
26 | , m_lastTimeStamp(0) |
27 | , m_emitterXVariation(0) |
28 | , m_emitterYVariation(0) |
29 | , m_followCount(0) |
30 | , m_emissionExtruder(nullptr) |
31 | , m_defaultEmissionExtruder(new QQuickParticleExtruder(this)) |
32 | { |
33 | //TODO: If followed increased their size |
34 | connect(sender: this, SIGNAL(followChanged(QString)), |
35 | receiver: this, SLOT(recalcParticlesPerSecond())); |
36 | connect(sender: this, SIGNAL(particleDurationChanged(int)), |
37 | receiver: this, SLOT(recalcParticlesPerSecond())); |
38 | connect(sender: this, SIGNAL(particlesPerParticlePerSecondChanged(int)), |
39 | receiver: this, SLOT(recalcParticlesPerSecond())); |
40 | } |
41 | |
42 | /*! |
43 | \qmlproperty string QtQuick.Particles::TrailEmitter::follow |
44 | |
45 | The type of logical particle which this is emitting from. |
46 | */ |
47 | /*! |
48 | \qmlproperty qreal QtQuick.Particles::TrailEmitter::velocityFromMovement |
49 | |
50 | If this value is non-zero, then any movement of the emitter will provide additional |
51 | starting velocity to the particles based on the movement. The additional vector will be the |
52 | same angle as the emitter's movement, with a magnitude that is the magnitude of the emitters |
53 | movement multiplied by velocityFromMovement. |
54 | |
55 | Default value is 0. |
56 | */ |
57 | /*! |
58 | \qmlproperty Shape QtQuick.Particles::TrailEmitter::emitShape |
59 | |
60 | As the area of a TrailEmitter is the area it follows, a separate shape can be provided |
61 | to be the shape it emits out of. This shape has width and height specified by emitWidth |
62 | and emitHeight, and is centered on the followed particle's position. |
63 | |
64 | The default shape is a filled Rectangle. |
65 | */ |
66 | /*! |
67 | \qmlproperty real QtQuick.Particles::TrailEmitter::emitWidth |
68 | |
69 | The width in pixels the emitShape is scaled to. If set to TrailEmitter.ParticleSize, |
70 | the width will be the current size of the particle being followed. |
71 | |
72 | Default is 0. |
73 | */ |
74 | /*! |
75 | \qmlproperty real QtQuick.Particles::TrailEmitter::emitHeight |
76 | |
77 | The height in pixels the emitShape is scaled to. If set to TrailEmitter.ParticleSize, |
78 | the height will be the current size of the particle being followed. |
79 | |
80 | Default is 0. |
81 | */ |
82 | /*! |
83 | \qmlproperty real QtQuick.Particles::TrailEmitter::emitRatePerParticle |
84 | */ |
85 | /*! |
86 | \qmlsignal QtQuick.Particles::TrailEmitter::emitFollowParticles(Array particles, Particle followed) |
87 | |
88 | This signal is emitted when particles are emitted from the \a followed particle. \a particles contains an array of particle objects which can be directly manipulated. |
89 | |
90 | If you use this signal handler, emitParticles will not be emitted. |
91 | */ |
92 | |
93 | bool QQuickTrailEmitter::isEmitFollowConnected() |
94 | { |
95 | IS_SIGNAL_CONNECTED(this, QQuickTrailEmitter, emitFollowParticles, |
96 | (const QJSValue &, const QJSValue &)); |
97 | } |
98 | |
99 | void QQuickTrailEmitter::recalcParticlesPerSecond(){ |
100 | if (!m_system) |
101 | return; |
102 | m_followCount = m_system->groupData[m_system->groupIds[m_follow]]->size(); |
103 | if (!m_followCount){ |
104 | setParticlesPerSecond(1);//XXX: Fix this horrendous hack, needed so they aren't turned off from start (causes crashes - test that when gone you don't crash with 0 PPPS) |
105 | }else{ |
106 | setParticlesPerSecond(m_particlesPerParticlePerSecond * m_followCount); |
107 | m_lastEmission.resize(size: m_followCount); |
108 | m_lastEmission.fill(t: m_lastTimeStamp); |
109 | } |
110 | } |
111 | |
112 | void QQuickTrailEmitter::reset() |
113 | { |
114 | m_followCount = 0; |
115 | } |
116 | |
117 | void QQuickTrailEmitter::emitWindow(int timeStamp) |
118 | { |
119 | if (m_system == nullptr) |
120 | return; |
121 | if (!m_enabled && !m_pulseLeft && m_burstQueue.isEmpty()) |
122 | return; |
123 | if (m_followCount != m_system->groupData[m_system->groupIds[m_follow]]->size()){ |
124 | qreal oldPPS = m_particlesPerSecond; |
125 | recalcParticlesPerSecond(); |
126 | if (m_particlesPerSecond != oldPPS) |
127 | return;//system may need to update |
128 | } |
129 | |
130 | if (m_pulseLeft){ |
131 | m_pulseLeft -= timeStamp - m_lastTimeStamp * 1000.; |
132 | if (m_pulseLeft < 0){ |
133 | timeStamp += m_pulseLeft; |
134 | m_pulseLeft = 0; |
135 | } |
136 | } |
137 | |
138 | //TODO: Implement startTime and velocityFromMovement |
139 | qreal time = timeStamp / 1000.; |
140 | qreal particleRatio = 1. / m_particlesPerParticlePerSecond; |
141 | qreal pt; |
142 | qreal maxLife = (m_particleDuration + m_particleDurationVariation)/1000.0; |
143 | |
144 | //Have to map it into this system, because particlesystem automaps it back |
145 | QPointF offset = m_system->mapFromItem(item: this, point: QPointF(0, 0)); |
146 | qreal sizeAtEnd = m_particleEndSize >= 0 ? m_particleEndSize : m_particleSize; |
147 | |
148 | int gId = m_system->groupIds[m_follow]; |
149 | int gId2 = groupId(); |
150 | for (int i=0; i<m_system->groupData[gId]->data.size(); i++) { |
151 | QQuickParticleData *d = m_system->groupData[gId]->data[i]; |
152 | if (!d->stillAlive(system: m_system)){ |
153 | m_lastEmission[i] = time; //Should only start emitting when it returns to life |
154 | continue; |
155 | } |
156 | pt = m_lastEmission[i]; |
157 | if (pt < d->t) |
158 | pt = d->t; |
159 | if (pt + maxLife < time)//We missed so much, that we should skip emiting particles that are dead by now |
160 | pt = time - maxLife; |
161 | |
162 | if ((width() || height()) && !effectiveExtruder()->contains(bounds: QRectF(offset.x(), offset.y(), width(), height()), |
163 | point: QPointF(d->curX(particleSystem: m_system), d->curY(particleSystem: m_system)))) { |
164 | m_lastEmission[d->index] = time;//jump over this time period without emitting, because it's outside |
165 | continue; |
166 | } |
167 | |
168 | QList<QQuickParticleData*> toEmit; |
169 | |
170 | while (pt < time || !m_burstQueue.isEmpty()){ |
171 | QQuickParticleData* datum = m_system->newDatum(groupId: gId2, respectLimits: !m_overwrite); |
172 | if (datum){//else, skip this emission |
173 | // Particle timestamp |
174 | datum->t = pt; |
175 | datum->lifeSpan = |
176 | (m_particleDuration |
177 | + (QRandomGenerator::global()->bounded(highest: (m_particleDurationVariation*2) + 1) - m_particleDurationVariation)) |
178 | / 1000.0; |
179 | |
180 | // Particle position |
181 | // Note that burst location doesn't get used for follow emitter |
182 | qreal followT = pt - d->t; |
183 | qreal followT2 = followT * followT * 0.5; |
184 | qreal eW = m_emitterXVariation < 0 ? d->curSize(particleSystem: m_system) : m_emitterXVariation; |
185 | qreal eH = m_emitterYVariation < 0 ? d->curSize(particleSystem: m_system) : m_emitterYVariation; |
186 | //Subtract offset, because PS expects this in emitter coordinates |
187 | QRectF boundsRect(d->x - offset.x() + d->vx * followT + d->ax * followT2 - eW/2, |
188 | d->y - offset.y() + d->vy * followT + d->ay * followT2 - eH/2, |
189 | eW, eH); |
190 | |
191 | QQuickParticleExtruder* effectiveEmissionExtruder = m_emissionExtruder ? m_emissionExtruder : m_defaultEmissionExtruder; |
192 | const QPointF &newPos = effectiveEmissionExtruder->extrude(boundsRect); |
193 | datum->x = newPos.x(); |
194 | datum->y = newPos.y(); |
195 | |
196 | // Particle velocity |
197 | const QPointF &velocity = m_velocity->sample(from: newPos); |
198 | datum->vx = velocity.x() |
199 | + m_velocity_from_movement * d->vx; |
200 | datum->vy = velocity.y() |
201 | + m_velocity_from_movement * d->vy; |
202 | |
203 | // Particle acceleration |
204 | const QPointF &accel = m_acceleration->sample(from: newPos); |
205 | datum->ax = accel.x(); |
206 | datum->ay = accel.y(); |
207 | |
208 | // Particle size |
209 | float sizeVariation = -m_particleSizeVariation |
210 | + QRandomGenerator::global()->generateDouble() * m_particleSizeVariation * 2; |
211 | |
212 | float size = qMax(a: (qreal)0.0, b: m_particleSize + sizeVariation); |
213 | float endSize = qMax(a: (qreal)0.0, b: sizeAtEnd + sizeVariation); |
214 | |
215 | datum->size = size * float(m_enabled); |
216 | datum->endSize = endSize * float(m_enabled); |
217 | |
218 | toEmit << datum; |
219 | |
220 | m_system->emitParticle(p: datum, particleEmitter: this); |
221 | } |
222 | if (!m_burstQueue.isEmpty()){ |
223 | m_burstQueue.first().first--; |
224 | if (m_burstQueue.first().first <= 0) |
225 | m_burstQueue.pop_front(); |
226 | }else{ |
227 | pt += particleRatio; |
228 | } |
229 | } |
230 | |
231 | foreach (QQuickParticleData* d, toEmit) |
232 | m_system->emitParticle(p: d, particleEmitter: this); |
233 | |
234 | if (isEmitConnected() || isEmitFollowConnected()) { |
235 | QQmlEngine *qmlEngine = ::qmlEngine(this); |
236 | QV4::ExecutionEngine *v4 = qmlEngine->handle(); |
237 | |
238 | QV4::Scope scope(v4); |
239 | QV4::ScopedArrayObject array(scope, v4->newArrayObject(count: toEmit.size())); |
240 | QV4::ScopedValue v(scope); |
241 | for (int i=0; i<toEmit.size(); i++) |
242 | array->put(idx: i, v: (v = toEmit[i]->v4Value(particleSystem: m_system))); |
243 | |
244 | QJSValue particles; |
245 | QJSValuePrivate::setValue(jsval: &particles, v: array); |
246 | if (isEmitFollowConnected()) { |
247 | //A chance for many arbitrary JS changes |
248 | emit emitFollowParticles( |
249 | particles, followed: QJSValuePrivate::fromReturnedValue(d: d->v4Value(particleSystem: m_system))); |
250 | } else if (isEmitConnected()) { |
251 | emit emitParticles(particles);//A chance for arbitrary JS changes |
252 | } |
253 | } |
254 | m_lastEmission[d->index] = pt; |
255 | } |
256 | |
257 | m_lastTimeStamp = time; |
258 | } |
259 | QT_END_NAMESPACE |
260 | |
261 | #include "moc_qquicktrailemitter_p.cpp" |
262 | |