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

source code of qtdeclarative/src/particles/qquickparticleemitter.cpp