particlefx2d 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +21 -0
  5. data/.ruby-version +1 -0
  6. data/.yardopts +1 -0
  7. data/CHANGELOG.md +25 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/Gemfile +8 -0
  10. data/Gemfile.lock +67 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +174 -0
  13. data/Rakefile +19 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +8 -0
  16. data/example/images/fx_blue_swirling_smoke.png +0 -0
  17. data/example/images/fx_square_burst.png +0 -0
  18. data/example/images/fx_white_red_smoke.png +0 -0
  19. data/example/ruby2d/fx_blue_swirling_smoke.rb +39 -0
  20. data/example/ruby2d/fx_square_burst.rb +41 -0
  21. data/example/ruby2d/fx_white_red_smoke.rb +37 -0
  22. data/lib/particlefx2d/emitter.rb +163 -0
  23. data/lib/particlefx2d/images/particle.png +0 -0
  24. data/lib/particlefx2d/particle.rb +173 -0
  25. data/lib/particlefx2d/private/color.rb +88 -0
  26. data/lib/particlefx2d/private/vector2.rb +85 -0
  27. data/lib/particlefx2d/renderer.rb +34 -0
  28. data/lib/particlefx2d/renderer_factory.rb +37 -0
  29. data/lib/particlefx2d/ruby2d/canvas_renderer_factory.rb +63 -0
  30. data/lib/particlefx2d/ruby2d/particle_circle.rb +38 -0
  31. data/lib/particlefx2d/ruby2d/particle_image.rb +45 -0
  32. data/lib/particlefx2d/ruby2d/shape_renderer.rb +52 -0
  33. data/lib/particlefx2d/ruby2d/shape_renderer_factory.rb +30 -0
  34. data/lib/particlefx2d/version.rb +6 -0
  35. data/lib/particlefx2d.rb +9 -0
  36. data/lib/particlefx_ruby2d.rb +14 -0
  37. data/particlefx2d.gemspec +32 -0
  38. data/sig/particlefx2d.rbs +4 -0
  39. data/spec/spec_helper.rb +15 -0
  40. data/spec/unit/particlefx2d_spec.rb +7 -0
  41. metadata +170 -0
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'particle'
4
+
5
+ module ParticleFX2D
6
+ #
7
+ # A particle effect emitter
8
+ #
9
+ class Emitter
10
+ attr_accessor :emission_rate, :particle_config
11
+
12
+ #
13
+ # Create a new particle emitter
14
+ #
15
+ # @param opts Options to configure the emitter as follows:
16
+ # - +quantity+ Number of total particles in the emitter's pool
17
+ # - +emission_rate+ Number of particles to emit per second
18
+ # - +particle_config+ Options are used to configure each particle when it is emitted, which are as follows:
19
+ # - +x+ Initial x-axis position of emission
20
+ # - +x_range+ optional range from which a random value is chosen to add to the initial +x+; default is 0
21
+ # - +y+ Initial y-axis position of emission
22
+ # - +y_range+ optional range from which a random value is chosen to add to the initial +y+; default is 0
23
+ # - +start_color+ optional initial colour of the emitted particle. See `Particle` for default
24
+ # - +end_color+ optional end colour of the particle by the end of its life. See `Particle` for default
25
+ # - +start_scale+ optional initial size scale factor of the emitted particle relative to its +size+; default is 1
26
+ # - +end_scale+ optional end size scale factor of the particle by the end of its life; default is 1
27
+ # - +angle+ optional angle at which the particle is emitted, in degrees. See `Particle` for default
28
+ # - +angle_range+ optional range from which a random value is chosen to add to the +angle+; default is 0
29
+ # - +speed+ optional speed at which the particle is emitted, in pixels/s. See `Particle` for default
30
+ # - +speed_range+ optional range from which a random value is chosen to add to the +speed+; default is 0
31
+ # - +size+ optional size of the particle when emitted, in pixels. See `Particle` for default
32
+ # - +size_range+ optional range from which a random value is chosen to add to the +size+; default is 0
33
+ # - +gravity_x+ optional linear acceleration in pixels/second squared along the x axis, default is 0
34
+ # - +gravity_y+ optional linear acceleration in pixels/second squared along the y axis, default is 0
35
+ # - +radial_acceleration+ optional radial accelation in pixel/seconds squared, default is 0
36
+ # - +tangential_acceleration+ optional tangential accelation in pixel/seconds squared, default is 0
37
+ # - +life_time+ of each particle in seconds. See `Particle` for default
38
+ # - +life_time_range+ optional range from which a random value is chosen to add to the +life_time+; default is 0
39
+ #
40
+ def initialize(opts = {})
41
+ @quantity = (opts[:quantity] || 128).to_i
42
+ @emission_rate = opts[:emission_rate].to_f
43
+ @emission = 0
44
+ @particle_config = opts[:particle_config]
45
+ @renderer_factory = opts[:renderer_factory]
46
+ setup_particle_pool
47
+ end
48
+
49
+ #
50
+ # Update the particle effect emission, called for each frame of the animation cycle.
51
+ #
52
+ # @param [Float] frame_time in seconds (essentially, 1 / fps)
53
+ #
54
+ def update(frame_time)
55
+ @renderer_factory.on_update_start
56
+ emit_particles frame_time
57
+ @active.each do |p|
58
+ p.update frame_time
59
+ free_particle p unless p.alive?
60
+ end
61
+ @renderer_factory.on_update_end
62
+ end
63
+
64
+ #
65
+ # Retrieve statistics about the emitter's current status
66
+ #
67
+ # @return A hash with the following properties:
68
+ # - +quantity+ The total number of particles in the emitter
69
+ # - +emission_rate+ The emission rate (particles per second)
70
+ # - +active+ The number of active particles
71
+ # - +unused+ The number of inactive particles in the pool
72
+ #
73
+ def stats
74
+ {
75
+ quantity: @quantity,
76
+ emission_rate: @emission_rate,
77
+ active: @active.count,
78
+ unused: @pool.count
79
+ }
80
+ end
81
+
82
+ private
83
+
84
+ # Initialize the particle pool; call once per emitter.
85
+ def setup_particle_pool
86
+ @active = []
87
+ @pool = []
88
+ @quantity.times do
89
+ p = Particle.new
90
+ renderer = @renderer_factory.renderer_for(p)
91
+ p.renderer! renderer
92
+ free_particle p
93
+ end
94
+ end
95
+
96
+ # For each update frame emit as many particles as specified by the emission rate.
97
+ # May emit less that the rate if there aren't enough particles in the pool
98
+ def emit_particles(frame_time)
99
+ return unless @emission_rate.positive?
100
+
101
+ rate = @emission_rate * frame_time
102
+ @emission += rate
103
+ while @active.count < @quantity && @emission > rate
104
+ new_particle
105
+ @emission -= 1
106
+ end
107
+ end
108
+
109
+ # Generate a value for a particle configuration range-based property.
110
+ #
111
+ # @return nil if no config with +prop_name+ is found
112
+ def config_range_value(prop_name, prop_range_name)
113
+ return nil unless @particle_config[prop_name.to_sym]
114
+
115
+ prop_range = @particle_config[prop_range_name.to_sym]
116
+ @particle_config[prop_name.to_sym] + (prop_range ? rand(prop_range) : 0)
117
+ end
118
+
119
+ # Retrieve a value for a particle configuration value-only property.
120
+ #
121
+ # @return nil if no config with +prop_name+ is found
122
+ def config_value(prop_name, prop_alt_name = nil)
123
+ @particle_config[prop_name.to_sym] || (@particle_config[prop_alt_name.to_sym] if prop_alt_name)
124
+ end
125
+
126
+ # Reset a particle based on the emitter's particle config.
127
+ # This is done before emitting a particle from the particle pool
128
+ def reset_particle(particle)
129
+ particle.reset! x: config_range_value(:x, :x_range),
130
+ y: config_range_value(:y, :y_range),
131
+ color: config_value(:start_color, :start_colour),
132
+ end_color: config_value(:end_color, :end_colour),
133
+ scale: config_value(:start_scale), end_scale: config_value(:end_scale),
134
+ gravity_x: config_value(:gravity_x), gravity_y: config_value(:gravity_y),
135
+ radial_acceleration: config_range_value(:radial_acceleration, :radial_acceleration_range),
136
+ tangential_acceleration: config_range_value(:tangential_acceleration, :tangential_acceleration),
137
+ size: config_range_value(:size, :size_range),
138
+ speed: config_range_value(:speed, :speed_range),
139
+ angle: config_range_value(:angle, :angle_range),
140
+ life_time: config_range_value(:life_time, :life_time_range)
141
+ end
142
+
143
+ # Retrieve an unused particle from the pool, reset it,
144
+ # and show it's associated shape/peer
145
+ def new_particle
146
+ p = @pool.pop
147
+ return unless p
148
+
149
+ @active << p
150
+ reset_particle p
151
+ p.renderer&.show_particle(p)
152
+ end
153
+
154
+ # Return a particle back to the unused pool,
155
+ # and hide it's associated shape/peer
156
+ def free_particle(particle)
157
+ ix = @active.find_index particle
158
+ @active.delete_at ix unless ix.nil?
159
+ @pool.push particle
160
+ particle.renderer&.hide_particle(p)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'private/vector2'
4
+ require_relative 'private/color'
5
+
6
+ module ParticleFX2D
7
+ #
8
+ # A single 2D particle.
9
+ #
10
+ class Particle
11
+ attr_reader :x, :y, :color, :end_color, :velocity, :speed, :angle, :life, :size, :renderer,
12
+ :gravity_x, :gravity_y, :radial_accel, :tangent_accel, :scale, :end_scale
13
+
14
+ # @!visibility private
15
+ # Create a new particle
16
+ #
17
+ # @param opts Options describing the particle. See #reset! for alll the properties.
18
+ #
19
+ def initialize(opts = {})
20
+ reset!(opts)
21
+ end
22
+
23
+ #
24
+ # Used by the _Emitter_ when re-using particles from the particle pool.
25
+ #
26
+ # @param opts Options describing the particle as follows:
27
+ # - +x+ defaults to 0
28
+ # - +y+ defaults to 0
29
+ # - +color+ or +colour+ array of particle's color components [r, g, b, a]; default is _[0, 1.0, 0, 1.0]_ (green)
30
+ # - +end_color+ or +end_colour+ array of particle's end color [r, g, b, a]; default is _[1.0, 0, 0, 1.0]_ (red)
31
+ # - +angle+ in degrees; default is 0
32
+ # - +speed+ in pixels per second; default is 100
33
+ # - +life_time+ in seconds; default is 100.0
34
+ # - +size+ in pixels; default is 5
35
+ # - +scale+ relative to size, default is 1
36
+ # - +end_scale+ particle's end scale relative to size; default is +scale+
37
+ # - +gravity_x+ in pixels/second squared along the x axis, default is 0
38
+ # - +gravity_y+ in pixels/second squared along the y axis, default is 0
39
+ # - +radial_acceleration+ in pixel/seconds squared, default is 0
40
+ # - +tangential_acceleration+ in pixel/seconds squared, default is 0
41
+ #
42
+ def reset!(opts = {})
43
+ @initial_life = @life = value_from(opts, :life_time, default: 100).to_f
44
+ @initial_size = @size = value_from(opts, :size, default: 5)
45
+ @gravity = Private::Vector2.new value_from(opts, :gravity_x, default: 0),
46
+ value_from(opts, :gravity_y, default: 0)
47
+ # following may depend on initial life and size
48
+ reset_forces opts
49
+ reset_scale_from opts
50
+ reset_position_from opts
51
+ reset_color_from opts
52
+ reset_velocity_from opts
53
+ end
54
+
55
+ #
56
+ # Set the rendering peer for the particle.
57
+ #
58
+ # @param renderer The renderer must implement following methods:
59
+ # - +show_particle(p)+ The specified particle is visible.
60
+ # - +hide_particle(p)+ The specified particle is no longer visible.
61
+ # - +draw_particle(p)+ Render the specified particle; this method is called only if a particle is visible.
62
+ #
63
+ def renderer!(renderer)
64
+ @renderer = renderer
65
+ end
66
+
67
+ #
68
+ # Returns true if the particle is considered alive
69
+ #
70
+ def alive?
71
+ @life.positive?
72
+ end
73
+
74
+ #
75
+ # Used by the _Emitter_ to update the particle by +frame_time+ seconds.
76
+ #
77
+ # @param [Float] frame_time in seconds
78
+ #
79
+ def update(frame_time)
80
+ @life -= frame_time
81
+ return unless alive?
82
+
83
+ @scale += @delta_scale * frame_time
84
+ @size = @initial_size * @scale
85
+ @color.add!(@delta_color, each_times: frame_time)
86
+ update_forces
87
+ update_motion frame_time
88
+ @renderer&.draw_particle self
89
+ end
90
+
91
+ private
92
+
93
+ # Calculates the number of pixels the particle should move
94
+ # based on its velocity and force of acceleration. Called per axis.
95
+ def accelerated_velocity(velocity, force_per_frame)
96
+ velocity + (force_per_frame.abs2 / 2)
97
+ end
98
+
99
+ # Update the force of acceleration based on configured gravity, radial accel and
100
+ # tangential accel if applicable.
101
+ def update_forces
102
+ @forces.copy! @gravity
103
+ return @forces if (@radial_accel.zero? && @tangent_accel.zero?) || (@x == @initial_x && @y == @initial_y)
104
+
105
+ @radial.set!(@x, @y)
106
+ .subtract!(@initial_x, @initial_y)
107
+ .normalize!
108
+ @tangential.copy!(@radial)
109
+ .cross!
110
+ .times!(@tangent_accel)
111
+ @forces.add_vector!(@radial.times!(@radial_accel))
112
+ .add_vector!(@tangential)
113
+ end
114
+
115
+ # Update the motion for a frame of animation based on the updated forces and velocity
116
+ def update_motion(frame_time)
117
+ @forces.times!(frame_time)
118
+ @x += accelerated_velocity @velocity.x * frame_time, @forces.x
119
+ @y += accelerated_velocity @velocity.y * frame_time, @forces.y
120
+ @velocity.add_vector! @forces
121
+ end
122
+
123
+ # Initialize position from configuration options
124
+ def reset_position_from(opts)
125
+ @initial_x = @x = value_from(opts, :x, default: 0)
126
+ @initial_y = @y = value_from(opts, :y, default: 0)
127
+ end
128
+
129
+ # Initialize scale factors from configuration options
130
+ def reset_scale_from(opts)
131
+ @initial_scale = @scale = value_from(opts, :scale, default: 1).to_f
132
+ @end_scale = value_from(opts, :end_scale, default: @initial_scale).to_f
133
+ @delta_scale = (@end_scale - @initial_scale) / @initial_life
134
+ end
135
+
136
+ # Initialize colours from configuration options
137
+ def reset_color_from(opts)
138
+ @initial_color = Private::Color.new(value_from(opts, :color, alt_name: :colour) || [0, 1.0, 0, 1.0])
139
+ @end_color = value_from(opts, :end_color, alt_name: :end_colour, default: @initial_color)
140
+ @color = Private::Color.new(@initial_color)
141
+ @delta_color = Private::Color.new(@end_color).subtract!(@initial_color).divide_by!(@initial_life)
142
+ end
143
+
144
+ # Convenient method to extract value for a particle configuration property
145
+ # if present.
146
+ def value_from(opts, name, default: nil, alt_name: nil)
147
+ value = opts[name.to_sym]
148
+ value ||= opts[alt_name.to_sym] unless alt_name.nil?
149
+ value ||= default
150
+ value
151
+ end
152
+
153
+ # Initialize velocity from configuration options
154
+ def reset_velocity_from(opts)
155
+ @angle = value_from(opts, :angle, default: 0).to_f
156
+ @angle_in_radians = nil
157
+ @speed = value_from(opts, :speed, default: 100).to_f
158
+ @velocity ||= Private::Vector2.new
159
+ angle_rad = @angle * Math::PI / 180
160
+ @velocity.set! @speed * Math.cos(angle_rad),
161
+ -@speed * Math.sin(angle_rad)
162
+ end
163
+
164
+ # Initialize forces from configuration options
165
+ def reset_forces(opts)
166
+ @radial ||= Private::Vector2.new
167
+ @tangential ||= Private::Vector2.new
168
+ @forces ||= Private::Vector2.new
169
+ @radial_accel = value_from(opts, :radial_acceleration, default: 0)
170
+ @tangent_accel = value_from(opts, :tangential_acceleration, default: 0)
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # 2D Particle Effects
5
+ #
6
+ module ParticleFX2D
7
+ # @!visibility private
8
+ module Private
9
+ # @!visibility private
10
+ #
11
+ # RGBA colour representation by its four components, for internal use only.
12
+ #
13
+ class Color
14
+ attr_accessor :r, :g, :b, :a
15
+
16
+ def initialize(other)
17
+ case other
18
+ when Array
19
+ @r = other[0].to_f
20
+ @g = other[1].to_f
21
+ @b = other[2].to_f
22
+ @a = other[3].to_f
23
+ else
24
+ @r = other.r
25
+ @g = other.g
26
+ @b = other.b
27
+ @a = other.a
28
+ end
29
+ end
30
+
31
+ alias opacity a
32
+ alias opacity= a=
33
+
34
+ def to_a
35
+ [@r, @g, @b, @a]
36
+ end
37
+
38
+ def subtract!(other)
39
+ case other
40
+ when Numeric
41
+ @r -= other
42
+ @g -= other
43
+ @b -= other
44
+ @a -= other
45
+ else
46
+ @r -= other.r
47
+ @g -= other.g
48
+ @b -= other.b
49
+ @a -= other.a
50
+ end
51
+ self
52
+ end
53
+
54
+ def add!(other, each_times: 1)
55
+ case other
56
+ when Numeric
57
+ value = other * each_times
58
+ @r += value
59
+ @g += value
60
+ @b += value
61
+ @a += value
62
+ else
63
+ @r += other.r * each_times
64
+ @g += other.g * each_times
65
+ @b += other.b * each_times
66
+ @a += other.a * each_times
67
+ end
68
+ self
69
+ end
70
+
71
+ def divide_by!(other)
72
+ case other
73
+ when Numeric
74
+ @r /= other
75
+ @g /= other
76
+ @b /= other
77
+ @a /= other
78
+ else
79
+ @r /= other.r
80
+ @g /= other.g
81
+ @b /= other.b
82
+ @a /= other.a
83
+ end
84
+ self
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ParticleFX2D
4
+ # @!visibility private
5
+ module Private
6
+ # @!visibility private
7
+ #
8
+ # A 2D vector, for internal use only
9
+ #
10
+ class Vector2
11
+ attr_reader :x, :y
12
+
13
+ def self.copy(vector)
14
+ Vector.new(vector.x, vector.y)
15
+ end
16
+
17
+ def initialize(x = 0, y = 0)
18
+ @x = x
19
+ @y = y
20
+ end
21
+
22
+ def set!(x, y)
23
+ @x = x
24
+ @y = y
25
+ self
26
+ end
27
+
28
+ def copy!(other)
29
+ @x = other.x
30
+ @y = other.y
31
+ self
32
+ end
33
+
34
+ def add!(x, y)
35
+ @x += x
36
+ @y += y
37
+ self
38
+ end
39
+
40
+ def subtract!(x, y)
41
+ @x -= x
42
+ @y -= y
43
+ self
44
+ end
45
+
46
+ def times!(value)
47
+ @x *= value
48
+ @y *= value
49
+ self
50
+ end
51
+
52
+ def divide_by!(value)
53
+ @x /= value
54
+ @y /= value
55
+ self
56
+ end
57
+
58
+ def add_vector!(other)
59
+ @x += other.x
60
+ @y += other.y
61
+ self
62
+ end
63
+
64
+ def minus_vector!(other)
65
+ @x -= other.x
66
+ @y -= other.y
67
+ self
68
+ end
69
+
70
+ def magnitude
71
+ Math.sqrt(@x.abs2 + @y.abs2)
72
+ end
73
+
74
+ def cross!
75
+ set! @y, -@x
76
+ end
77
+
78
+ def normalize!
79
+ mag = magnitude
80
+ mag = Float::INFINITY if mag.zero?
81
+ divide_by! mag
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ParticleFX2D
4
+ # Defines a particle renderer. Depending on the graphics system used to
5
+ # render the particle effects, you can implement either one single renderer per
6
+ # emitter that does the drawing implement a renderer per particle.
7
+ module Renderer
8
+ # Factory method to provide a renderer for a particle. Called once per particle.
9
+ # The particle system does not care if the factory returns a shared renderer for all the
10
+ # particles or if it returns one per particle. Each particle will be associated with the
11
+ # renderer.
12
+ def self.for(_particle)
13
+ raise StandardError('unimplemented')
14
+ end
15
+
16
+ # instance methods per Renderer
17
+
18
+ # Notifies the renderer that a particle is visible.
19
+ def show_particle(_particle)
20
+ raise StandardError('unimplemented')
21
+ end
22
+
23
+ # Notifies the renderer that a particle is hidden.
24
+ def hide_particle(_particle)
25
+ raise StandardError('unimplemented')
26
+ end
27
+
28
+ # Requests the render to draw the particle (or update the particle's rendering
29
+ # peer with the particle's visual attributes.)
30
+ def draw_particle(_particle)
31
+ raise StandardError('unimplemented')
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'renderer'
4
+
5
+ module ParticleFX2D
6
+ # Defines a RendererFactory. Each emitter should have it's own
7
+ # factory which is called once per particle to create a Renderer.
8
+ # It is possible for a factory to
9
+ # * EITHER return the same Renderer for all particles and handle the drawing for the particles;
10
+ # * OR return a renderer per particle.
11
+ module RendererFactory
12
+ # Return a particle renderer.
13
+ #
14
+ # @return [Renderer] for each particle
15
+ def renderer_for(_particle)
16
+ raise StandardError('unimplemented')
17
+ end
18
+
19
+ #
20
+ # Called once per update cycle by the emitter to let the factory know that
21
+ # all the particles are about to be updated and redrawn.
22
+ # A shared renderer factory can use this to clear the underlying surface, for example.
23
+ # A per-particle renderer factory can ignore this method as by default it does nothing.
24
+ def on_update_start
25
+ # Does nothing by default.
26
+ end
27
+
28
+ #
29
+ # Called once per update cycle by the emitter to let the factory know that
30
+ # an update cycle is complete.
31
+ # A shared renderer factory can use this to commit the changes made, for example.
32
+ # A per-particle renderer factory can ignore this method as by default it does nothing.
33
+ def on_update_end
34
+ # Does nothing by default.
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../renderer_factory'
4
+
5
+ module ParticleFX2D
6
+ module Ruby2D
7
+ # A shared surface renderer that uses Ruby2D's _Canvas_ to draw into. This is both
8
+ # a factory and the renderer/
9
+ #
10
+ # This approach uses the same renderer for all particles in an emitter.
11
+ class CanvasRendererFactory
12
+ include ParticleFX2D::RendererFactory
13
+ include ParticleFX2D::Renderer
14
+
15
+ # ----- Factory
16
+
17
+ # Instantiate a shape renderer factory.
18
+ #
19
+ # @param [Ruby2D::Canvas] canvas The partice effects will be drawn into this canvas. Disable auto-update for
20
+ # the Canvas so that each attempt to draw into it will not cause a texture update.
21
+ def initialize(canvas)
22
+ @canvas = canvas
23
+ end
24
+
25
+ # Return a particle renderer.
26
+ #
27
+ # @return [Renderer] for each particle
28
+ def renderer_for(_particle)
29
+ self
30
+ end
31
+
32
+ # Clear the canvas before the next update cycle
33
+ def on_update_start
34
+ @canvas.draw_rectangle(x: 0, y: 0,
35
+ width: @canvas.width, height: @canvas.height,
36
+ color: [0, 0, 0, 0])
37
+ end
38
+
39
+ # Update the canvas at the end of the next update cycle
40
+ def on_update_end
41
+ @canvas.update
42
+ end
43
+
44
+ # ----- Renderer
45
+
46
+ # Show the particle. Used when a particle is activated.
47
+ def show_particle(_particle); end
48
+
49
+ # Hide the particle. Used when a particle is deactivated.
50
+ def hide_particle(_particle); end
51
+
52
+ # Updates the shape's properties; no explicit drawing needed.
53
+ def draw_particle(particle)
54
+ size = particle.size
55
+ x = particle.x - (size / 2)
56
+ y = particle.y - (size / 2)
57
+ @canvas.draw_rectangle(x: x, y: y,
58
+ width: size, height: size,
59
+ color: particle.color.to_a)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'shape_renderer'
4
+
5
+ module ParticleFX2D
6
+ module Ruby2D
7
+ #
8
+ # A particle shape that is based on the Ruby2D _Circle_ shape.
9
+ class ParticleCircle < ::Ruby2D::Circle
10
+ include ShapeRenderer
11
+
12
+ # Called by the emitter for each particle that it manages. Creates an instance
13
+ # of _ParticleCircle_ intialized with the particle's position, size and colour.
14
+ #
15
+ # @param [ParticleFX2D::Particle] particle
16
+ #
17
+ def self.for(particle)
18
+ s = ParticleCircle.new x: particle.x, y: particle.y,
19
+ radius: particle.size / 2
20
+ s.color! particle.color
21
+ s.remove
22
+ s
23
+ end
24
+
25
+ # Sets the circle's position using the incoming centre coordinates.
26
+ def center!(centre_x, centre_y)
27
+ self.x = centre_x
28
+ self.y = centre_y
29
+ end
30
+
31
+ # Sets the radius using the particle size and calls #super
32
+ def draw_particle(particle)
33
+ self.radius = particle.size / 2
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end