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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/.ruby-version +1 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +25 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +174 -0
- data/Rakefile +19 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/example/images/fx_blue_swirling_smoke.png +0 -0
- data/example/images/fx_square_burst.png +0 -0
- data/example/images/fx_white_red_smoke.png +0 -0
- data/example/ruby2d/fx_blue_swirling_smoke.rb +39 -0
- data/example/ruby2d/fx_square_burst.rb +41 -0
- data/example/ruby2d/fx_white_red_smoke.rb +37 -0
- data/lib/particlefx2d/emitter.rb +163 -0
- data/lib/particlefx2d/images/particle.png +0 -0
- data/lib/particlefx2d/particle.rb +173 -0
- data/lib/particlefx2d/private/color.rb +88 -0
- data/lib/particlefx2d/private/vector2.rb +85 -0
- data/lib/particlefx2d/renderer.rb +34 -0
- data/lib/particlefx2d/renderer_factory.rb +37 -0
- data/lib/particlefx2d/ruby2d/canvas_renderer_factory.rb +63 -0
- data/lib/particlefx2d/ruby2d/particle_circle.rb +38 -0
- data/lib/particlefx2d/ruby2d/particle_image.rb +45 -0
- data/lib/particlefx2d/ruby2d/shape_renderer.rb +52 -0
- data/lib/particlefx2d/ruby2d/shape_renderer_factory.rb +30 -0
- data/lib/particlefx2d/version.rb +6 -0
- data/lib/particlefx2d.rb +9 -0
- data/lib/particlefx_ruby2d.rb +14 -0
- data/particlefx2d.gemspec +32 -0
- data/sig/particlefx2d.rbs +4 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/unit/particlefx2d_spec.rb +7 -0
- 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
|
Binary file
|
@@ -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
|