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> |
9 | QT_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 | |
181 | QQuickParticleEmitter::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 | |
214 | QQuickParticleEmitter::~QQuickParticleEmitter() |
215 | { |
216 | if (m_defaultExtruder) |
217 | delete m_defaultExtruder; |
218 | } |
219 | |
220 | bool QQuickParticleEmitter::isEmitConnected() |
221 | { |
222 | IS_SIGNAL_CONNECTED(this, QQuickParticleEmitter, emitParticles, (const QJSValue &)); |
223 | } |
224 | |
225 | void 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 | |
235 | void 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 | |
244 | void QQuickParticleEmitter::setEnabled(bool arg) |
245 | { |
246 | if (m_enabled != arg) { |
247 | m_enabled = arg; |
248 | emit enabledChanged(arg); |
249 | } |
250 | } |
251 | |
252 | |
253 | QQuickParticleExtruder* 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 | |
262 | void QQuickParticleEmitter::pulse(int milliseconds) |
263 | { |
264 | if (!m_enabled) |
265 | m_pulseLeft = milliseconds; |
266 | } |
267 | |
268 | void QQuickParticleEmitter::burst(int num) |
269 | { |
270 | m_burstQueue << qMakePair(value1&: num, value2: QPointF(x(), y())); |
271 | } |
272 | |
273 | void QQuickParticleEmitter::burst(int num, qreal x, qreal y) |
274 | { |
275 | m_burstQueue << qMakePair(value1&: num, value2: QPointF(x, y)); |
276 | } |
277 | |
278 | void 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 | |
299 | void 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 | |
307 | void QQuickParticleEmitter::reset() |
308 | { |
309 | m_reset_last = true; |
310 | } |
311 | |
312 | void 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 | |
474 | QT_END_NAMESPACE |
475 | |
476 | #include "moc_qquickparticleemitter_p.cpp" |
477 | |