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 | #undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses |
5 | |
6 | #include "qquickparticleemitter_p.h" |
7 | |
8 | #include <private/qqmlglobal_p.h> |
9 | #include <private/qquickv4particledata_p.h> |
10 | |
11 | #include <QtCore/qrandom.h> |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | /*! |
16 | \qmltype Emitter |
17 | //! \nativetype QQuickParticleEmitter |
18 | \inqmlmodule QtQuick.Particles |
19 | \brief Emits logical particles. |
20 | \ingroup qtquick-particles |
21 | |
22 | This element emits logical particles into the ParticleSystem, with the |
23 | given starting attributes. |
24 | |
25 | Note that logical particles are not |
26 | automatically rendered, you will need to have one or more |
27 | ParticlePainter elements visualizing them. |
28 | |
29 | Note that the given starting attributes can be modified at any point |
30 | in the particle's lifetime by any Affector element in the same |
31 | ParticleSystem. This includes attributes like lifespan. |
32 | */ |
33 | |
34 | /*! |
35 | \qmlproperty ParticleSystem QtQuick.Particles::Emitter::system |
36 | |
37 | This is the Particle system that the Emitter will emit into. |
38 | This can be omitted if the Emitter is a direct child of the ParticleSystem |
39 | */ |
40 | /*! |
41 | \qmlproperty string QtQuick.Particles::Emitter::group |
42 | |
43 | This is the logical particle group which it will emit into. |
44 | |
45 | Default value is "" (empty string). |
46 | */ |
47 | /*! |
48 | \qmlproperty Shape QtQuick.Particles::Emitter::shape |
49 | |
50 | This shape is applied with the size of the Emitter. Particles will be emitted |
51 | randomly from any area covered by the shape. |
52 | |
53 | The default shape is a filled in rectangle, which corresponds to the full bounding |
54 | box of the Emitter. |
55 | */ |
56 | /*! |
57 | \qmlproperty bool QtQuick.Particles::Emitter::enabled |
58 | |
59 | If set to false, the emitter will cease emissions until it is set to true. |
60 | |
61 | Default value is true. |
62 | */ |
63 | /*! |
64 | \qmlproperty real QtQuick.Particles::Emitter::emitRate |
65 | |
66 | Number of particles emitted per second. |
67 | |
68 | Default value is 10 particles per second. |
69 | */ |
70 | /*! |
71 | \qmlproperty int QtQuick.Particles::Emitter::lifeSpan |
72 | |
73 | The time in milliseconds each emitted particle should last for. |
74 | |
75 | If you do not want particles to automatically die after a time, for example if |
76 | you wish to dispose of them manually, set lifeSpan to Emitter.InfiniteLife. |
77 | |
78 | lifeSpans greater than or equal to 600000 (10 minutes) will be treated as infinite. |
79 | Particles with lifeSpans less than or equal to 0 will start out dead. |
80 | |
81 | Default value is 1000 (one second). |
82 | */ |
83 | /*! |
84 | \qmlproperty int QtQuick.Particles::Emitter::lifeSpanVariation |
85 | |
86 | Particle lifespans will vary by up to this much in either direction. |
87 | |
88 | Default value is 0. |
89 | */ |
90 | |
91 | /*! |
92 | \qmlproperty int QtQuick.Particles::Emitter::maximumEmitted |
93 | |
94 | The maximum number of particles at a time that this emitter will have alive. |
95 | |
96 | This can be set as a performance optimization (when using burst and pulse) or |
97 | to stagger emissions. |
98 | |
99 | If this is set to a number below zero, then there is no maximum limit on the number |
100 | of particles this emitter can have alive. |
101 | |
102 | The default value is -1. |
103 | */ |
104 | /*! |
105 | \qmlproperty int QtQuick.Particles::Emitter::startTime |
106 | |
107 | If this value is set when the emitter is loaded, then it will emit particles from the |
108 | past, up to startTime milliseconds ago. These will simulate as if they were emitted then, |
109 | but will not have any affectors applied to them. Affectors will take effect from the present time. |
110 | */ |
111 | /*! |
112 | \qmlproperty real QtQuick.Particles::Emitter::size |
113 | |
114 | The size in pixels of the particles at the start of their life. |
115 | |
116 | Default value is 16. |
117 | */ |
118 | /*! |
119 | \qmlproperty real QtQuick.Particles::Emitter::endSize |
120 | |
121 | The size in pixels of the particles at the end of their life. Size will |
122 | be linearly interpolated during the life of the particle from this value and |
123 | size. If endSize is -1, then the size of the particle will remain constant at |
124 | the starting size. |
125 | |
126 | Default value is -1. |
127 | */ |
128 | /*! |
129 | \qmlproperty real QtQuick.Particles::Emitter::sizeVariation |
130 | |
131 | The size of a particle can vary by this much up or down from size/endSize. The same |
132 | random addition is made to both size and endSize for a single particle. |
133 | |
134 | Default value is 0. |
135 | */ |
136 | /*! |
137 | \qmlproperty StochasticDirection QtQuick.Particles::Emitter::velocity |
138 | |
139 | The starting velocity of the particles emitted. |
140 | */ |
141 | /*! |
142 | \qmlproperty StochasticDirection QtQuick.Particles::Emitter::acceleration |
143 | |
144 | The starting acceleraton of the particles emitted. |
145 | */ |
146 | /*! |
147 | \qmlproperty qreal QtQuick.Particles::Emitter::velocityFromMovement |
148 | |
149 | If this value is non-zero, then any movement of the emitter will provide additional |
150 | starting velocity to the particles based on the movement. The additional vector will be the |
151 | same angle as the emitter's movement, with a magnitude that is the magnitude of the emitters |
152 | movement multiplied by velocityFromMovement. |
153 | |
154 | Default value is 0. |
155 | */ |
156 | |
157 | /*! |
158 | \qmlsignal QtQuick.Particles::Emitter::emitParticles(Array particles) |
159 | |
160 | This signal is emitted when particles are emitted. \a particles is a JavaScript |
161 | array of Particle objects. You can modify particle attributes directly within the handler. |
162 | |
163 | \note JavaScript is slower to execute, so it is not recommended to use this in |
164 | high-volume particle systems. |
165 | */ |
166 | |
167 | /*! \qmlmethod QtQuick.Particles::Emitter::burst(int count) |
168 | |
169 | Emits a number of particles, specified by \a count, from this emitter immediately. |
170 | */ |
171 | |
172 | /*! \qmlmethod QtQuick.Particles::Emitter::burst(int count, int x, int y) |
173 | |
174 | Emits a number of particles, specified by \a count, from this emitter immediately. |
175 | The particles are emitted as if the Emitter was positioned at (\a {x}, \a {y}) but |
176 | all other properties are the same. |
177 | */ |
178 | |
179 | /*! \qmlmethod QtQuick.Particles::Emitter::pulse(int duration) |
180 | |
181 | If the emitter is not enabled, enables it for a specified \a duration |
182 | (in milliseconds) and then switches it back off. |
183 | */ |
184 | |
185 | QQuickParticleEmitter::QQuickParticleEmitter(QQuickItem *parent) : |
186 | QQuickItem(parent) |
187 | , m_particlesPerSecond(10) |
188 | , m_particleDuration(1000) |
189 | , m_particleDurationVariation(0) |
190 | , m_enabled(true) |
191 | , m_system(nullptr) |
192 | , m_extruder(nullptr) |
193 | , m_defaultExtruder(nullptr) |
194 | , m_velocity(&m_nullVector) |
195 | , m_acceleration(&m_nullVector) |
196 | , m_particleSize(16) |
197 | , m_particleEndSize(-1) |
198 | , m_particleSizeVariation(0) |
199 | , m_startTime(0) |
200 | , m_overwrite(true) |
201 | , m_pulseLeft(0) |
202 | , m_maxParticleCount(-1) |
203 | , m_velocity_from_movement(0) |
204 | , m_reset_last(true) |
205 | , m_last_timestamp(-1) |
206 | , m_last_emission(0) |
207 | , m_groupIdNeedRecalculation(false) |
208 | , m_groupId(QQuickParticleGroupData::DefaultGroupID) |
209 | |
210 | { |
211 | //TODO: Reset velocity/acc back to null vector? Or allow null pointer? |
212 | connect(sender: this, signal: &QQuickParticleEmitter::particlesPerSecondChanged, |
213 | context: this, slot: &QQuickParticleEmitter::particleCountChanged); |
214 | connect(sender: this, signal: &QQuickParticleEmitter::particleDurationChanged, |
215 | context: this, slot: &QQuickParticleEmitter::particleCountChanged); |
216 | } |
217 | |
218 | QQuickParticleEmitter::~QQuickParticleEmitter() |
219 | { |
220 | if (m_defaultExtruder) |
221 | delete m_defaultExtruder; |
222 | } |
223 | |
224 | bool QQuickParticleEmitter::isEmitConnected() |
225 | { |
226 | IS_SIGNAL_CONNECTED( |
227 | this, QQuickParticleEmitter, emitParticles, (const QList<QQuickV4ParticleData> &)); |
228 | } |
229 | |
230 | void QQuickParticleEmitter::reclaculateGroupId() const |
231 | { |
232 | if (!m_system) { |
233 | m_groupId = QQuickParticleGroupData::InvalidID; |
234 | return; |
235 | } |
236 | m_groupId = m_system->groupIds.value(key: group(), defaultValue: QQuickParticleGroupData::InvalidID); |
237 | m_groupIdNeedRecalculation = m_groupId == QQuickParticleGroupData::InvalidID; |
238 | } |
239 | |
240 | void QQuickParticleEmitter::componentComplete() |
241 | { |
242 | if (!m_system && qobject_cast<QQuickParticleSystem*>(object: parentItem())) |
243 | setSystem(qobject_cast<QQuickParticleSystem*>(object: parentItem())); |
244 | if (m_system) |
245 | m_system->finishRegisteringParticleEmitter(e: this); |
246 | QQuickItem::componentComplete(); |
247 | } |
248 | |
249 | void QQuickParticleEmitter::setEnabled(bool arg) |
250 | { |
251 | if (m_enabled != arg) { |
252 | m_enabled = arg; |
253 | emit enabledChanged(arg); |
254 | } |
255 | } |
256 | |
257 | |
258 | QQuickParticleExtruder* QQuickParticleEmitter::effectiveExtruder() |
259 | { |
260 | if (m_extruder) |
261 | return m_extruder; |
262 | if (!m_defaultExtruder) |
263 | m_defaultExtruder = new QQuickParticleExtruder; |
264 | return m_defaultExtruder; |
265 | } |
266 | |
267 | void QQuickParticleEmitter::pulse(int milliseconds) |
268 | { |
269 | if (!m_enabled) |
270 | m_pulseLeft = milliseconds; |
271 | } |
272 | |
273 | void QQuickParticleEmitter::burst(int num) |
274 | { |
275 | m_burstQueue << qMakePair(value1&: num, value2: QPointF(x(), y())); |
276 | } |
277 | |
278 | void QQuickParticleEmitter::burst(int num, qreal x, qreal y) |
279 | { |
280 | m_burstQueue << qMakePair(value1&: num, value2: QPointF(x, y)); |
281 | } |
282 | |
283 | void QQuickParticleEmitter::setMaxParticleCount(int arg) |
284 | { |
285 | if (m_maxParticleCount != arg) { |
286 | if (arg < 0 && m_maxParticleCount >= 0){ |
287 | connect(sender: this, signal: &QQuickParticleEmitter::particlesPerSecondChanged, |
288 | context: this, slot: &QQuickParticleEmitter::particleCountChanged); |
289 | connect(sender: this, signal: &QQuickParticleEmitter::particleDurationChanged, |
290 | context: this, slot: &QQuickParticleEmitter::particleCountChanged); |
291 | } else if (arg >= 0 && m_maxParticleCount < 0){ |
292 | disconnect(sender: this, signal: &QQuickParticleEmitter::particlesPerSecondChanged, |
293 | receiver: this, slot: &QQuickParticleEmitter::particleCountChanged); |
294 | disconnect(sender: this, signal: &QQuickParticleEmitter::particleDurationChanged, |
295 | receiver: this, slot: &QQuickParticleEmitter::velocityFromMovementChanged); |
296 | } |
297 | m_overwrite = arg < 0; |
298 | m_maxParticleCount = arg; |
299 | emit maximumEmittedChanged(arg); |
300 | emit particleCountChanged(); |
301 | } |
302 | } |
303 | |
304 | void QQuickParticleEmitter::setVelocityFromMovement(qreal t) |
305 | { |
306 | if (t == m_velocity_from_movement) |
307 | return; |
308 | m_velocity_from_movement = t; |
309 | emit velocityFromMovementChanged(); |
310 | } |
311 | |
312 | void QQuickParticleEmitter::reset() |
313 | { |
314 | m_reset_last = true; |
315 | } |
316 | |
317 | void QQuickParticleEmitter::emitWindow(int timeStamp) |
318 | { |
319 | if (m_system == nullptr) |
320 | return; |
321 | if ((!m_enabled || m_particlesPerSecond <= 0)&& !m_pulseLeft && m_burstQueue.isEmpty()){ |
322 | m_reset_last = true; |
323 | return; |
324 | } |
325 | |
326 | if (m_reset_last) { |
327 | m_last_emitter = m_last_last_emitter = QPointF(x(), y()); |
328 | if (m_last_timestamp == -1) |
329 | m_last_timestamp = (timeStamp - m_startTime)/1000.; |
330 | else |
331 | m_last_timestamp = timeStamp/1000.; |
332 | m_last_emission = m_last_timestamp; |
333 | m_reset_last = false; |
334 | m_emitCap = -1; |
335 | } |
336 | |
337 | if (m_pulseLeft){ |
338 | m_pulseLeft -= timeStamp - m_last_timestamp * 1000.; |
339 | if (m_pulseLeft < 0){ |
340 | if (!m_enabled) |
341 | timeStamp += m_pulseLeft; |
342 | m_pulseLeft = 0; |
343 | } |
344 | } |
345 | qreal time = timeStamp / 1000.; |
346 | qreal particleRatio = 1. / m_particlesPerSecond; |
347 | qreal pt = m_last_emission; |
348 | qreal maxLife = (m_particleDuration + m_particleDurationVariation)/1000.0; |
349 | if (pt + maxLife < time)//We missed so much, that we should skip emiting particles that are dead by now |
350 | pt = time - maxLife; |
351 | |
352 | qreal opt = pt; // original particle time |
353 | qreal dt = time - m_last_timestamp; // timestamp delta... |
354 | if (!dt) |
355 | dt = 0.000001; |
356 | |
357 | // emitter difference since last... |
358 | qreal dex = (x() - m_last_emitter.x()); |
359 | qreal dey = (y() - m_last_emitter.y()); |
360 | |
361 | qreal ax = (m_last_last_emitter.x() + m_last_emitter.x()) / 2; |
362 | qreal bx = m_last_emitter.x(); |
363 | qreal cx = (x() + m_last_emitter.x()) / 2; |
364 | qreal ay = (m_last_last_emitter.y() + m_last_emitter.y()) / 2; |
365 | qreal by = m_last_emitter.y(); |
366 | qreal cy = (y() + m_last_emitter.y()) / 2; |
367 | |
368 | qreal sizeAtEnd = m_particleEndSize >= 0 ? m_particleEndSize : m_particleSize; |
369 | qreal emitter_x_offset = m_last_emitter.x() - x(); |
370 | qreal emitter_y_offset = m_last_emitter.y() - y(); |
371 | if (!m_burstQueue.isEmpty() && !m_pulseLeft && !m_enabled)//'outside time' emissions only |
372 | pt = time; |
373 | |
374 | QList<QQuickParticleData*> toEmit; |
375 | |
376 | while ((pt < time && m_emitCap) || !m_burstQueue.isEmpty()) { |
377 | //int pos = m_last_particle % m_particle_count; |
378 | QQuickParticleData* datum = m_system->newDatum(groupId: m_system->groupIds[m_group], respectLimits: !m_overwrite); |
379 | if (datum){//actually emit(otherwise we've been asked to skip this one) |
380 | qreal t = 1 - (pt - opt) / dt; |
381 | qreal vx = |
382 | - 2 * ax * (1 - t) |
383 | + 2 * bx * (1 - 2 * t) |
384 | + 2 * cx * t; |
385 | qreal vy = |
386 | - 2 * ay * (1 - t) |
387 | + 2 * by * (1 - 2 * t) |
388 | + 2 * cy * t; |
389 | |
390 | |
391 | // Particle timestamp |
392 | datum->t = pt; |
393 | datum->lifeSpan = |
394 | (m_particleDuration |
395 | + (QRandomGenerator::global()->bounded(highest: (m_particleDurationVariation*2) + 1) - m_particleDurationVariation)) |
396 | / 1000.0; |
397 | |
398 | if (datum->lifeSpan >= m_system->maxLife){ |
399 | datum->lifeSpan = m_system->maxLife; |
400 | if (m_emitCap == -1) |
401 | m_emitCap = particleCount(); |
402 | m_emitCap--;//emitCap keeps us from reemitting 'infinite' particles after their life. Unless you reset the emitter. |
403 | } |
404 | |
405 | // Particle position |
406 | QRectF boundsRect; |
407 | if (!m_burstQueue.isEmpty()){ |
408 | boundsRect = QRectF(m_burstQueue.first().second.x() - x(), m_burstQueue.first().second.y() - y(), |
409 | width(), height()); |
410 | } else { |
411 | boundsRect = QRectF(emitter_x_offset + dex * (pt - opt) / dt, emitter_y_offset + dey * (pt - opt) / dt |
412 | , width(), height()); |
413 | } |
414 | QPointF newPos = effectiveExtruder()->extrude(boundsRect); |
415 | datum->x = newPos.x(); |
416 | datum->y = newPos.y(); |
417 | |
418 | // Particle velocity |
419 | const QPointF &velocity = m_velocity->sample(from: newPos); |
420 | datum->vx = velocity.x() |
421 | + m_velocity_from_movement * vx; |
422 | datum->vy = velocity.y() |
423 | + m_velocity_from_movement * vy; |
424 | |
425 | // Particle acceleration |
426 | const QPointF &accel = m_acceleration->sample(from: newPos); |
427 | datum->ax = accel.x(); |
428 | datum->ay = accel.y(); |
429 | |
430 | // Particle size |
431 | float sizeVariation = -m_particleSizeVariation |
432 | + QRandomGenerator::global()->bounded(highest: m_particleSizeVariation * 2); |
433 | |
434 | float size = qMax(a: (qreal)0.0 , b: m_particleSize + sizeVariation); |
435 | float endSize = qMax(a: (qreal)0.0 , b: sizeAtEnd + sizeVariation); |
436 | |
437 | datum->size = size;// * float(m_emitting); |
438 | datum->endSize = endSize;// * float(m_emitting); |
439 | |
440 | toEmit << datum; |
441 | } |
442 | if (m_burstQueue.isEmpty()){ |
443 | pt += particleRatio; |
444 | }else{ |
445 | m_burstQueue.first().first--; |
446 | if (m_burstQueue.first().first <= 0) |
447 | m_burstQueue.pop_front(); |
448 | } |
449 | } |
450 | |
451 | foreach (QQuickParticleData* d, toEmit) |
452 | m_system->emitParticle(p: d, particleEmitter: this); |
453 | |
454 | if (isEmitConnected()) { |
455 | //Done after emitParticle so that the Painter::load is done first, this allows you to customize its static variables |
456 | //We then don't need to request another reload, because the first reload isn't scheduled until we get back to the render thread |
457 | |
458 | QList<QQuickV4ParticleData> particles; |
459 | particles.reserve(asize: toEmit.size()); |
460 | for (QQuickParticleData *particle : std::as_const(t&: toEmit)) |
461 | particles.push_back(t: particle->v4Value(particleSystem: m_system)); |
462 | |
463 | emit emitParticles(particles);//A chance for arbitrary JS changes |
464 | } |
465 | |
466 | m_last_emission = pt; |
467 | |
468 | m_last_last_emitter = m_last_emitter; |
469 | m_last_emitter = QPointF(x(), y()); |
470 | m_last_timestamp = time; |
471 | } |
472 | |
473 | |
474 | QT_END_NAMESPACE |
475 | |
476 | #include "moc_qquickparticleemitter_p.cpp" |
477 | |