├── .gitignore
├── AUTHORS.md
├── LICENSE
├── README.md
├── demo
├── main.py
└── media
│ ├── drugs.pex
│ ├── fire.pex
│ ├── jellyfish.pex
│ ├── particle.pex
│ ├── particle.png
│ └── sun.pex
├── kivyparticle
├── __init__.py
├── engine.py
└── utils.py
├── run-tests.py
└── test
├── __init__.py
├── config.pex
├── engine.py
└── media
├── config.pex
└── particle.png
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | TODO
3 | .vscode/launch.json
4 |
--------------------------------------------------------------------------------
/AUTHORS.md:
--------------------------------------------------------------------------------
1 | Kivy-Particle was originally written by Alexis Couronne and greatly inspired by [Starling Extension Particle System](https://github.com/PrimaryFeather/Starling-Extension-Particle-System)
2 |
3 | ##Development Lead
4 |
5 | * Alexis Couronne <[alexis.couronne@gmail](mailto:alexis.couronne@gmail)>
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Alexis Couronne
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #Kivy Particle
2 |
3 | Kivy Particle is an implementation of [Starling Extension Particle System](https://github.com/PrimaryFeather/Starling-Extension-Particle-System) for Kivy Python framework.
4 |
5 | (Work in Progress)
6 |
7 | * 2022-02-25
8 | * update to run by Python 3.9
9 | * maxint replaced by maxsize
10 | * minor fixes on App path and on media files path
11 | * gitignore updated: included vscode directory
12 |
--------------------------------------------------------------------------------
/demo/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import sys
4 | from os.path import abspath, dirname, join
5 |
6 | DIR_APP = join(
7 | dirname(__file__),
8 | '..'
9 | )
10 |
11 | sys.path.insert(
12 | 0,
13 | abspath(DIR_APP)
14 | )
15 |
16 |
17 | import kivy
18 | kivy.require('1.5.1')
19 |
20 | from kivy.app import App
21 | from kivy.uix.boxlayout import BoxLayout
22 | from kivy.uix.gridlayout import GridLayout
23 | from kivy.uix.button import Button
24 | from kivy.uix.widget import Widget
25 |
26 | from kivyparticle import ParticleSystem
27 |
28 |
29 | class DemoParticle(Widget):
30 | def __init__(self, **kwargs):
31 | super(DemoParticle, self).__init__(**kwargs)
32 | self.sun = ParticleSystem(abspath(join(DIR_APP,'demo/media/sun.pex')))
33 | self.drugs = ParticleSystem(abspath(join(DIR_APP,'demo/media/drugs.pex')))
34 | self.jellyfish = ParticleSystem(abspath(join(DIR_APP,'demo/media/jellyfish.pex')))
35 | self.fire = ParticleSystem(abspath(join(DIR_APP,'demo/media/fire.pex')))
36 |
37 | self.current = None
38 | self._show(self.sun)
39 |
40 | def on_touch_down(self, touch):
41 | self.current.emitter_x = float(touch.x)
42 | self.current.emitter_y = float(touch.y)
43 |
44 | def on_touch_move(self, touch):
45 | self.current.emitter_x = float(touch.x)
46 | self.current.emitter_y = float(touch.y)
47 |
48 | def show_sun(self, b):
49 | self._show(self.sun)
50 |
51 | def show_drugs(self, b):
52 | self._show(self.drugs)
53 |
54 | def show_jellyfish(self, b):
55 | self._show(self.jellyfish)
56 |
57 | def show_fire(self, b):
58 | self._show(self.fire)
59 |
60 | def _show(self, system):
61 | if self.current:
62 | self.remove_widget(self.current)
63 | self.current.stop(True)
64 | self.current = system
65 |
66 | self.current.emitter_x = 300.0
67 | self.current.emitter_y = 300.0
68 | self.add_widget(self.current)
69 | self.current.start()
70 |
71 |
72 | class DemoParticleApp(App):
73 | def build(self):
74 | root = GridLayout(cols=2)
75 | paint = DemoParticle(size_hint_x=None, width=600)
76 | root.add_widget(paint)
77 | buttons = BoxLayout(orientation='vertical')
78 | root.add_widget(buttons)
79 |
80 | sun = Button(text='Sun')
81 | sun.bind(on_press=paint.show_sun)
82 | buttons.add_widget(sun)
83 |
84 | drugs = Button(text='Drugs')
85 | drugs.bind(on_press=paint.show_drugs)
86 | buttons.add_widget(drugs)
87 |
88 | jellyfish = Button(text='JellyFish')
89 | jellyfish.bind(on_press=paint.show_jellyfish)
90 | buttons.add_widget(jellyfish)
91 |
92 | fire = Button(text='Fire')
93 | fire.bind(on_press=paint.show_fire)
94 | buttons.add_widget(fire)
95 |
96 | return root
97 |
98 |
99 | if __name__ == '__main__':
100 | DemoParticleApp().run()
101 |
--------------------------------------------------------------------------------
/demo/media/drugs.pex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/demo/media/fire.pex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/demo/media/jellyfish.pex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/demo/media/particle.pex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/demo/media/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skitoo/kivy-particle/0e287e93da86d48e00f59820f83b2a11a057bee4/demo/media/particle.png
--------------------------------------------------------------------------------
/demo/media/sun.pex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/kivyparticle/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import kivy
4 | kivy.require('1.4.1')
5 |
6 | __title__ = 'kivyparticle'
7 | __version__ = '0.1'
8 | __author__ = 'Alexis Couronne'
9 | __all__ = ['ParticleSystem', 'EMITTER_TYPE_GRAVITY', 'EMITTER_TYPE_RADIAL']
10 |
11 | from .engine import ParticleSystem, EMITTER_TYPE_GRAVITY, EMITTER_TYPE_RADIAL
12 |
--------------------------------------------------------------------------------
/kivyparticle/engine.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # 2022-02-25 - Python 3 remove maxint - replaced by maxsize - https://stackoverflow.com/a/13795777
4 |
5 | from kivy.uix.widget import Widget
6 | from kivy.clock import Clock
7 | from kivy.graphics import Color, Callback, Rotate, PushMatrix, PopMatrix, Translate, Quad
8 | from kivy.graphics.opengl import glBlendFunc, GL_SRC_ALPHA, GL_ONE, GL_ZERO, GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_ONE_MINUS_SRC_ALPHA, GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_DST_COLOR, GL_ONE_MINUS_DST_COLOR
9 | from kivy.core.image import Image
10 | from kivy.logger import Logger
11 | from xml.dom.minidom import parse as parse_xml
12 | from .utils import random_variance, random_color_variance
13 | from kivy.properties import NumericProperty, BooleanProperty, ListProperty, StringProperty, ObjectProperty
14 |
15 | import sys
16 | import os
17 | import math
18 |
19 | __all__ = ['EMITTER_TYPE_GRAVITY', 'EMITTER_TYPE_RADIAL', 'Particle', 'ParticleSystem']
20 |
21 |
22 | EMITTER_TYPE_GRAVITY = 0
23 | EMITTER_TYPE_RADIAL = 1
24 |
25 | BLEND_FUNC = {0: GL_ZERO,
26 | 1: GL_ONE,
27 | 0x300: GL_SRC_COLOR,
28 | 0x301: GL_ONE_MINUS_SRC_COLOR,
29 | 0x302: GL_SRC_ALPHA,
30 | 0x303: GL_ONE_MINUS_SRC_ALPHA,
31 | 0x304: GL_DST_ALPHA,
32 | 0x305: GL_ONE_MINUS_DST_ALPHA,
33 | 0x306: GL_DST_COLOR,
34 | 0x307: GL_ONE_MINUS_DST_COLOR
35 | }
36 |
37 |
38 | class Particle(object):
39 | x, y, rotation, current_time = -256, -256, 0, 0
40 | scale, total_time = 1.0, 0.
41 | color = [1.0, 1.0, 1.0, 1.0]
42 | color_delta = [0.0, 0.0, 0.0, 0.0]
43 | start_x, start_y, velocity_x, velocity_y = 0, 0, 0, 0
44 | radial_acceleration, tangent_acceleration = 0, 0
45 | emit_radius, emit_radius_delta = 0, 0
46 | emit_rotation, emit_rotation_delta = 0, 0
47 | rotation_delta, scale_delta = 0, 0
48 |
49 |
50 | class ParticleSystem(Widget):
51 | max_num_particles = NumericProperty(200)
52 | life_span = NumericProperty(2)
53 | texture = ObjectProperty(None)
54 | texture_path = StringProperty(None)
55 | life_span_variance = NumericProperty(0)
56 | start_size = NumericProperty(16)
57 | start_size_variance = NumericProperty(0)
58 | end_size = NumericProperty(16)
59 | end_size_variance = NumericProperty(0)
60 | emit_angle = NumericProperty(0)
61 | emit_angle_variance = NumericProperty(0)
62 | start_rotation = NumericProperty(0)
63 | start_rotation_variance = NumericProperty(0)
64 | end_rotation = NumericProperty(0)
65 | end_rotation_variance = NumericProperty(0)
66 | emitter_x_variance = NumericProperty(100)
67 | emitter_y_variance = NumericProperty(100)
68 | gravity_x = NumericProperty(0)
69 | gravity_y = NumericProperty(0)
70 | speed = NumericProperty(0)
71 | speed_variance = NumericProperty(0)
72 | radial_acceleration = NumericProperty(100)
73 | radial_acceleration_variance = NumericProperty(0)
74 | tangential_acceleration = NumericProperty(0)
75 | tangential_acceleration_variance = NumericProperty(0)
76 | max_radius = NumericProperty(100)
77 | max_radius_variance = NumericProperty(0)
78 | min_radius = NumericProperty(50)
79 | rotate_per_second = NumericProperty(0)
80 | rotate_per_second_variance = NumericProperty(0)
81 | start_color = ListProperty([1., 1., 1., 1.])
82 | start_color_variance = ListProperty([1., 1., 1., 1.])
83 | end_color = ListProperty([1., 1., 1., 1.])
84 | end_color_variance = ListProperty([1., 1., 1., 1.])
85 | blend_factor_source = NumericProperty(770)
86 | blend_factor_dest = NumericProperty(1)
87 | emitter_type = NumericProperty(0)
88 |
89 | update_interval = NumericProperty(1. / 30.)
90 | _is_paused = BooleanProperty(False)
91 |
92 | def __init__(self, config, **kwargs):
93 | super(ParticleSystem, self).__init__(**kwargs)
94 | self.capacity = 0
95 | self.particles = list()
96 | self.particles_dict = dict()
97 | self.emission_time = 0.0
98 | self.frame_time = 0.0
99 | self.num_particles = 0
100 |
101 | if config is not None:
102 | self._parse_config(config)
103 | self.emission_rate = self.max_num_particles / self.life_span
104 | self.initial_capacity = self.max_num_particles
105 | self.max_capacity = self.max_num_particles
106 | self._raise_capacity(self.initial_capacity)
107 |
108 | with self.canvas.before:
109 | Callback(self._set_blend_func)
110 | with self.canvas.after:
111 | Callback(self._reset_blend_func)
112 |
113 | Clock.schedule_once(self._update, self.update_interval)
114 |
115 | def start(self, duration=sys.maxsize):
116 | if self.emission_rate != 0:
117 | self.emission_time = duration
118 |
119 | def stop(self, clear=False):
120 | self.emission_time = 0.0
121 | if clear:
122 | self.num_particles = 0
123 | self.particles_dict = dict()
124 | self.canvas.clear()
125 |
126 | def on_max_num_particles(self, instance, value):
127 | self.max_capacity = value
128 | if self.capacity < value:
129 | self._raise_capacity(self.max_capacity - self.capacity)
130 | elif self.capacity > value:
131 | self._lower_capacity(self.capacity - self.max_capacity)
132 | self.emission_rate = self.max_num_particles / self.life_span
133 |
134 | def on_texture(self, instance, value):
135 | for p in self.particles:
136 | try:
137 | self.particles_dict[p]['rect'].texture = self.texture
138 | except KeyError:
139 | # if particle isn't initialized yet, you can't change its texture.
140 | pass
141 |
142 | def on_life_span(self, instance, value):
143 | self.emission_rate = self.max_num_particles / value
144 |
145 | def _set_blend_func(self, instruction):
146 | #glBlendFunc(self.blend_factor_source, self.blend_factor_dest)
147 | #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
148 | glBlendFunc(GL_SRC_ALPHA, GL_ONE)
149 |
150 | def _reset_blend_func(self, instruction):
151 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
152 |
153 | def _parse_config(self, config):
154 | self._config = parse_xml(config)
155 |
156 | texture_path = self._parse_data('texture', 'name')
157 | config_dir_path = os.path.dirname(os.path.abspath(config))
158 | path = os.path.join(config_dir_path, texture_path)
159 | if os.path.exists(path):
160 | self.texture_path = path
161 | else:
162 | self.texture_path = texture_path
163 |
164 | self.texture = Image(self.texture_path).texture
165 | self.emitter_x = float(self._parse_data('sourcePosition', 'x'))
166 | self.emitter_y = float(self._parse_data('sourcePosition', 'y'))
167 | self.emitter_x_variance = float(self._parse_data('sourcePositionVariance', 'x'))
168 | self.emitter_y_variance = float(self._parse_data('sourcePositionVariance', 'y'))
169 | self.gravity_x = float(self._parse_data('gravity', 'x'))
170 | self.gravity_y = float(self._parse_data('gravity', 'y'))
171 | self.emitter_type = int(self._parse_data('emitterType'))
172 | self.max_num_particles = int(self._parse_data('maxParticles'))
173 | self.life_span = max(0.01, float(self._parse_data('particleLifeSpan')))
174 | self.life_span_variance = float(self._parse_data('particleLifespanVariance'))
175 | self.start_size = float(self._parse_data('startParticleSize'))
176 | self.start_size_variance = float(self._parse_data('startParticleSizeVariance'))
177 | self.end_size = float(self._parse_data('finishParticleSize'))
178 | self.end_size_variance = float(self._parse_data('FinishParticleSizeVariance'))
179 | self.emit_angle = math.radians(float(self._parse_data('angle')))
180 | self.emit_angle_variance = math.radians(float(self._parse_data('angleVariance')))
181 | self.start_rotation = math.radians(float(self._parse_data('rotationStart')))
182 | self.start_rotation_variance = math.radians(float(self._parse_data('rotationStartVariance')))
183 | self.end_rotation = math.radians(float(self._parse_data('rotationEnd')))
184 | self.end_rotation_variance = math.radians(float(self._parse_data('rotationEndVariance')))
185 | self.speed = float(self._parse_data('speed'))
186 | self.speed_variance = float(self._parse_data('speedVariance'))
187 | self.radial_acceleration = float(self._parse_data('radialAcceleration'))
188 | self.radial_acceleration_variance = float(self._parse_data('radialAccelVariance'))
189 | self.tangential_acceleration = float(self._parse_data('tangentialAcceleration'))
190 | self.tangential_acceleration_variance = float(self._parse_data('tangentialAccelVariance'))
191 | self.max_radius = float(self._parse_data('maxRadius'))
192 | self.max_radius_variance = float(self._parse_data('maxRadiusVariance'))
193 | self.min_radius = float(self._parse_data('minRadius'))
194 | self.rotate_per_second = math.radians(float(self._parse_data('rotatePerSecond')))
195 | self.rotate_per_second_variance = math.radians(float(self._parse_data('rotatePerSecondVariance')))
196 | self.start_color = self._parse_color('startColor')
197 | self.start_color_variance = self._parse_color('startColorVariance')
198 | self.end_color = self._parse_color('finishColor')
199 | self.end_color_variance = self._parse_color('finishColorVariance')
200 | self.blend_factor_source = self._parse_blend('blendFuncSource')
201 | self.blend_factor_dest = self._parse_blend('blendFuncDestination')
202 |
203 | def _parse_data(self, name, attribute='value'):
204 | return self._config.getElementsByTagName(name)[0].getAttribute(attribute)
205 |
206 | def _parse_color(self, name):
207 | return [float(self._parse_data(name, 'red')), float(self._parse_data(name, 'green')), float(self._parse_data(name, 'blue')), float(self._parse_data(name, 'alpha'))]
208 |
209 | def _parse_blend(self, name):
210 | value = int(self._parse_data(name))
211 | return BLEND_FUNC[value]
212 |
213 | def pause(self):
214 | self._is_paused = True
215 |
216 | def resume(self):
217 | self._is_paused = False
218 | Clock.schedule_once(self._update, self.update_interval)
219 |
220 | def _update(self, dt):
221 | self._advance_time(dt)
222 | self._render()
223 | if not self._is_paused:
224 | Clock.schedule_once(self._update, self.update_interval)
225 |
226 | def _create_particle(self):
227 | return Particle()
228 |
229 | def _init_particle(self, particle):
230 | life_span = random_variance(self.life_span, self.life_span_variance)
231 | if life_span <= 0.0:
232 | return
233 |
234 | particle.current_time = 0.0
235 | particle.total_time = life_span
236 |
237 | particle.x = random_variance(self.emitter_x, self.emitter_x_variance)
238 | particle.y = random_variance(self.emitter_y, self.emitter_y_variance)
239 | particle.start_x = self.emitter_x
240 | particle.start_y = self.emitter_y
241 |
242 | angle = random_variance(self.emit_angle, self.emit_angle_variance)
243 | speed = random_variance(self.speed, self.speed_variance)
244 | particle.velocity_x = speed * math.cos(angle)
245 | particle.velocity_y = speed * math.sin(angle)
246 |
247 | particle.emit_radius = random_variance(self.max_radius, self.max_radius_variance)
248 | particle.emit_radius_delta = (self.max_radius - self.min_radius) / life_span
249 |
250 | particle.emit_rotation = random_variance(self.emit_angle, self.emit_angle_variance)
251 | particle.emit_rotation_delta = random_variance(self.rotate_per_second, self.rotate_per_second_variance)
252 |
253 | particle.radial_acceleration = random_variance(self.radial_acceleration, self.radial_acceleration_variance)
254 | particle.tangent_acceleration = random_variance(self.tangential_acceleration, self.tangential_acceleration_variance)
255 |
256 | start_size = random_variance(self.start_size, self.start_size_variance)
257 | end_size = random_variance(self.end_size, self.end_size_variance)
258 |
259 | start_size = max(0.1, start_size)
260 | end_size = max(0.1, end_size)
261 |
262 | particle.scale = start_size / self.texture.width
263 | particle.scale_delta = ((end_size - start_size) / life_span) / self.texture.width
264 |
265 | # colors
266 | start_color = random_color_variance(self.start_color, self.start_color_variance)
267 | end_color = random_color_variance(self.end_color, self.end_color_variance)
268 |
269 | particle.color_delta = [(end_color[i] - start_color[i]) / life_span for i in range(4)]
270 | particle.color = start_color
271 |
272 | # rotation
273 | start_rotation = random_variance(self.start_rotation, self.start_rotation_variance)
274 | end_rotation = random_variance(self.end_rotation, self.end_rotation_variance)
275 | particle.rotation = start_rotation
276 | particle.rotation_delta = (end_rotation - start_rotation) / life_span
277 |
278 | def _advance_particle(self, particle, passed_time):
279 | passed_time = min(passed_time, particle.total_time - particle.current_time)
280 | particle.current_time += passed_time
281 |
282 | if self.emitter_type == EMITTER_TYPE_RADIAL:
283 | particle.emit_rotation += particle.emit_rotation_delta * passed_time
284 | particle.emit_radius -= particle.emit_radius_delta * passed_time
285 | particle.x = self.emitter_x - math.cos(particle.emit_rotation) * particle.emit_radius
286 | particle.y = self.emitter_y - math.sin(particle.emit_rotation) * particle.emit_radius
287 |
288 | if particle.emit_radius < self.min_radius:
289 | particle.current_time = particle.total_time
290 |
291 | else:
292 | distance_x = particle.x - particle.start_x
293 | distance_y = particle.y - particle.start_y
294 | distance_scalar = math.sqrt(distance_x * distance_x + distance_y * distance_y)
295 | if distance_scalar < 0.01:
296 | distance_scalar = 0.01
297 |
298 | radial_x = distance_x / distance_scalar
299 | radial_y = distance_y / distance_scalar
300 | tangential_x = radial_x
301 | tangential_y = radial_y
302 |
303 | radial_x *= particle.radial_acceleration
304 | radial_y *= particle.radial_acceleration
305 |
306 | new_y = tangential_x
307 | tangential_x = -tangential_y * particle.tangent_acceleration
308 | tangential_y = new_y * particle.tangent_acceleration
309 |
310 | particle.velocity_x += passed_time * (self.gravity_x + radial_x + tangential_x)
311 | particle.velocity_y += passed_time * (self.gravity_y + radial_y + tangential_y)
312 |
313 | particle.x += particle.velocity_x * passed_time
314 | particle.y += particle.velocity_y * passed_time
315 |
316 | particle.scale += particle.scale_delta * passed_time
317 | particle.rotation += particle.rotation_delta * passed_time
318 |
319 | particle.color = [particle.color[i] + particle.color_delta[i] * passed_time for i in range(4)]
320 |
321 | def _raise_capacity(self, by_amount):
322 | old_capacity = self.capacity
323 | new_capacity = min(self.max_capacity, self.capacity + by_amount)
324 |
325 | for i in range(int(new_capacity - old_capacity)):
326 | self.particles.append(self._create_particle())
327 |
328 | self.num_particles = int(new_capacity)
329 | self.capacity = new_capacity
330 |
331 | def _lower_capacity(self, by_amount):
332 | old_capacity = self.capacity
333 | new_capacity = max(0, self.capacity - by_amount)
334 |
335 | for i in range(int(old_capacity - new_capacity)):
336 | try:
337 | self.canvas.remove(self.particles_dict[self.particles.pop()]['rect'])
338 | except:
339 | pass
340 |
341 | self.num_particles = int(new_capacity)
342 | self.capacity = new_capacity
343 |
344 | def _advance_time(self, passed_time):
345 | particle_index = 0
346 |
347 | # advance existing particles
348 | while particle_index < self.num_particles:
349 | particle = self.particles[particle_index]
350 | if particle.current_time < particle.total_time:
351 | self._advance_particle(particle, passed_time)
352 | particle_index += 1
353 | else:
354 | if particle_index != self.num_particles - 1:
355 | next_particle = self.particles[self.num_particles - 1]
356 | self.particles[self.num_particles - 1] = particle
357 | self.particles[particle_index] = next_particle
358 | self.num_particles -= 1
359 | if self.num_particles == 0:
360 | Logger.debug('Particle: COMPLETE')
361 |
362 | # create and advance new particles
363 | if self.emission_time > 0:
364 | time_between_particles = 1.0 / self.emission_rate
365 | self.frame_time += passed_time
366 |
367 | while self.frame_time > 0:
368 | if self.num_particles < self.max_capacity:
369 | if self.num_particles == self.capacity:
370 | self._raise_capacity(self.capacity)
371 |
372 | particle = self.particles[self.num_particles]
373 | self.num_particles += 1
374 | self._init_particle(particle)
375 | self._advance_particle(particle, self.frame_time)
376 |
377 | self.frame_time -= time_between_particles
378 |
379 | if self.emission_time != sys.maxsize:
380 | self.emission_time = max(0.0, self.emission_time - passed_time)
381 |
382 | def _render(self):
383 | if self.num_particles == 0:
384 | return
385 | for i in range(self.num_particles):
386 | particle = self.particles[i]
387 | size = (self.texture.size[0] * particle.scale, self.texture.size[1] * particle.scale)
388 | if particle not in self.particles_dict:
389 | self.particles_dict[particle] = dict()
390 | color = particle.color[:]
391 | with self.canvas:
392 | self.particles_dict[particle]['color'] = Color(color[0], color[1], color[2], color[3])
393 | PushMatrix()
394 | self.particles_dict[particle]['translate'] = Translate()
395 | self.particles_dict[particle]['rotate'] = Rotate()
396 | self.particles_dict[particle]['rotate'].set(particle.rotation, 0, 0, 1)
397 | self.particles_dict[particle]['rect'] = Quad(texture=self.texture, points=(-size[0] * 0.5, -size[1] * 0.5, size[0] * 0.5, -size[1] * 0.5, size[0] * 0.5, size[1] * 0.5, -size[0] * 0.5, size[1] * 0.5))
398 | self.particles_dict[particle]['translate'].xy = (particle.x, particle.y)
399 | PopMatrix()
400 | else:
401 | self.particles_dict[particle]['rotate'].angle = particle.rotation
402 | self.particles_dict[particle]['translate'].xy = (particle.x, particle.y)
403 | self.particles_dict[particle]['color'].rgba = particle.color
404 | self.particles_dict[particle]['rect'].points = (-size[0] * 0.5, -size[1] * 0.5, size[0] * 0.5, -size[1] * 0.5, size[0] * 0.5, size[1] * 0.5, -size[0] * 0.5, size[1] * 0.5)
405 |
--------------------------------------------------------------------------------
/kivyparticle/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import random
4 |
5 | __all__ = ['random_variance', 'random_color_variance']
6 |
7 |
8 | def random_variance(base, variance):
9 | return base + variance * (random.random() * 2.0 - 1.0)
10 |
11 |
12 | def random_color_variance(base, variance):
13 | return [min(max(0.0, (random_variance(base[i], variance[i]))), 1.0) for i in range(4)]
14 |
--------------------------------------------------------------------------------
/run-tests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from test import main
5 | import sys
6 | import os
7 |
8 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
9 |
10 |
11 | if __name__ == '__main__':
12 | main()
13 |
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import unittest
4 | from .engine import TestParticleSystem
5 |
6 | __all__ = ['main']
7 |
8 |
9 | def main():
10 | suite = unittest.TestSuite()
11 | suite.addTest(unittest.makeSuite(TestParticleSystem))
12 | unittest.TextTestRunner(verbosity=2).run(suite)
13 |
--------------------------------------------------------------------------------
/test/config.pex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/test/engine.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from kivyparticle import ParticleSystem
4 | from kivy.graphics.opengl import GL_ONE, GL_SRC_ALPHA
5 | import unittest
6 | import math
7 |
8 |
9 | class TestParticleSystem(unittest.TestCase):
10 | def setUp(self):
11 | self.s = ParticleSystem('test/media/config.pex')
12 |
13 | def test_config(self):
14 | self.assertEquals((32, 32), self.s.texture.size)
15 | self.assertEquals(160.55, self.s.emitter_x)
16 | self.assertEquals(428.95, self.s.emitter_y)
17 | self.assertEquals(104.41, self.s.emitter_x_variance)
18 | self.assertEquals(0.00, self.s.emitter_y_variance)
19 | self.assertEquals(0.00, self.s.gravity_x)
20 | self.assertEquals(0.00, self.s.gravity_y)
21 | self.assertEquals(0, self.s.emitter_type)
22 | self.assertEquals(300, self.s.max_num_particles)
23 | self.assertEquals(2.0, self.s.life_span)
24 | self.assertEquals(1.9, self.s.life_span_variance)
25 | self.assertEquals(70.0, self.s.start_size)
26 | self.assertEquals(49.53, self.s.start_size_variance)
27 | self.assertEquals(10.0, self.s.end_size)
28 | self.assertEquals(0.0, self.s.end_size_variance)
29 | self.assertEquals(math.radians(270.37), self.s.emit_angle)
30 | self.assertEquals(math.radians(15.0), self.s.emit_angle_variance)
31 | self.assertEquals(0.0, self.s.start_rotation)
32 | self.assertEquals(0.0, self.s.start_rotation_variance)
33 | self.assertEquals(0.0, self.s.end_rotation)
34 | self.assertEquals(0.0, self.s.end_rotation_variance)
35 | self.assertEquals(90.0, self.s.speed)
36 | self.assertEquals(30.0, self.s.speed_variance)
37 | self.assertEquals(0.0, self.s.radial_acceleration)
38 | self.assertEquals(0.0, self.s.radial_acceleration_variance)
39 | self.assertEquals(0.0, self.s.tangential_acceleration)
40 | self.assertEquals(0.0, self.s.tangential_acceleration_variance)
41 | self.assertEquals(100.0, self.s.max_radius)
42 | self.assertEquals(0.0, self.s.max_radius_variance)
43 | self.assertEquals(0.0, self.s.min_radius)
44 | self.assertEquals(0.0, self.s.rotate_per_second)
45 | self.assertEquals(0.0, self.s.rotate_per_second_variance)
46 | self.assertEquals([1.0, 0.31, 0.0, 0.62], self.s.start_color)
47 | self.assertEquals([0.0, 0.0, 0.0, 0.0], self.s.start_color_variance)
48 | self.assertEquals([1.0, 0.31, 0.0, 0.0], self.s.end_color)
49 | self.assertEquals([0.0, 0.0, 0.0, 0.0], self.s.end_color_variance)
50 | self.assertEquals(GL_SRC_ALPHA, self.s.blend_factor_source)
51 | self.assertEquals(GL_ONE, self.s.blend_factor_dest)
52 |
--------------------------------------------------------------------------------
/test/media/config.pex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/test/media/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skitoo/kivy-particle/0e287e93da86d48e00f59820f83b2a11a057bee4/test/media/particle.png
--------------------------------------------------------------------------------