ruby-sfml 3.0.0.5 → 3.0.0.6
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 +4 -4
- data/CHANGELOG.md +76 -0
- data/README.md +1 -0
- data/lib/sfml/app.rb +54 -3
- data/lib/sfml/audio/music.rb +3 -3
- data/lib/sfml/audio/sound.rb +2 -2
- data/lib/sfml/audio/sound_buffer.rb +6 -6
- data/lib/sfml/audio/sound_buffer_recorder.rb +3 -3
- data/lib/sfml/audio/sound_recorder.rb +3 -3
- data/lib/sfml/audio/sound_stream.rb +1 -1
- data/lib/sfml/graphics/animation.rb +120 -0
- data/lib/sfml/graphics/circle_shape.rb +1 -1
- data/lib/sfml/graphics/convex_shape.rb +1 -1
- data/lib/sfml/graphics/font.rb +4 -4
- data/lib/sfml/graphics/image.rb +8 -8
- data/lib/sfml/graphics/particle_system.rb +165 -0
- data/lib/sfml/graphics/rectangle_shape.rb +1 -1
- data/lib/sfml/graphics/render_texture.rb +2 -2
- data/lib/sfml/graphics/render_window.rb +26 -2
- data/lib/sfml/graphics/shader.rb +3 -3
- data/lib/sfml/graphics/shape.rb +1 -1
- data/lib/sfml/graphics/shape_inspectable.rb +1 -1
- data/lib/sfml/graphics/sprite.rb +2 -2
- data/lib/sfml/graphics/sprite_sheet.rb +100 -0
- data/lib/sfml/graphics/text.rb +2 -2
- data/lib/sfml/graphics/texture.rb +7 -7
- data/lib/sfml/graphics/texture_atlas.rb +126 -0
- data/lib/sfml/graphics/transformable_object.rb +2 -2
- data/lib/sfml/graphics/vertex_array.rb +2 -2
- data/lib/sfml/graphics/vertex_buffer.rb +2 -2
- data/lib/sfml/graphics/view.rb +3 -3
- data/lib/sfml/input_actions.rb +105 -0
- data/lib/sfml/network/ftp.rb +1 -1
- data/lib/sfml/network/http.rb +3 -3
- data/lib/sfml/network/packet.rb +2 -2
- data/lib/sfml/network/socket_selector.rb +1 -1
- data/lib/sfml/network/tcp_listener.rb +1 -1
- data/lib/sfml/network/tcp_socket.rb +1 -1
- data/lib/sfml/network/udp_socket.rb +1 -1
- data/lib/sfml/scene.rb +4 -0
- data/lib/sfml/system/vector2.rb +75 -0
- data/lib/sfml/system/vector3.rb +59 -0
- data/lib/sfml/version.rb +1 -1
- data/lib/sfml/window/context.rb +1 -1
- data/lib/sfml/window/cursor.rb +2 -2
- data/lib/sfml/window/window.rb +2 -2
- data/lib/sfml.rb +44 -0
- metadata +6 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module SFML
|
|
2
|
+
# A pool-backed particle emitter built on top of `SFML::VertexArray`.
|
|
3
|
+
# Each particle is two triangles (a quad) so a single CSFML draw
|
|
4
|
+
# call ships thousands at once.
|
|
5
|
+
#
|
|
6
|
+
# class Sparks < SFML::ParticleSystem
|
|
7
|
+
# def emit_one
|
|
8
|
+
# angle = rand(0.0..2 * Math::PI)
|
|
9
|
+
# speed = rand(80.0..200.0)
|
|
10
|
+
# spawn(
|
|
11
|
+
# position: @origin,
|
|
12
|
+
# velocity: SFML::Vector2.new(Math.cos(angle), Math.sin(angle)) * speed,
|
|
13
|
+
# lifetime: 0.8,
|
|
14
|
+
# color: SFML::Color.new(255, 200, 50),
|
|
15
|
+
# size: 4,
|
|
16
|
+
# )
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# sparks = Sparks.new(max: 500)
|
|
21
|
+
#
|
|
22
|
+
# def update(dt)
|
|
23
|
+
# 5.times { sparks.emit_one } if mouse_down?
|
|
24
|
+
# sparks.update(dt)
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def draw
|
|
28
|
+
# window.draw(sparks)
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# Particles fade linearly from full alpha at spawn to zero alpha
|
|
32
|
+
# at `lifetime`. Override `#update_particle(p, dt)` to apply
|
|
33
|
+
# gravity, drag, custom colour curves, etc.
|
|
34
|
+
#
|
|
35
|
+
# Acceleration: pass `gravity:` to the constructor for the common
|
|
36
|
+
# case (px/s² added to velocity every frame). For richer behaviour,
|
|
37
|
+
# subclass and override.
|
|
38
|
+
class ParticleSystem
|
|
39
|
+
# A single particle. Plain Struct so allocations stay cheap;
|
|
40
|
+
# mutated in-place from `#update`.
|
|
41
|
+
Particle = Struct.new(
|
|
42
|
+
:x, :y,
|
|
43
|
+
:vx, :vy,
|
|
44
|
+
:age, :lifetime,
|
|
45
|
+
:r, :g, :b, :a,
|
|
46
|
+
:size,
|
|
47
|
+
) do
|
|
48
|
+
def alive? = age < lifetime
|
|
49
|
+
def normalized_age = age / lifetime
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
DEFAULT_GRAVITY = [0.0, 0.0].freeze
|
|
53
|
+
|
|
54
|
+
def initialize(max: 1000, gravity: DEFAULT_GRAVITY, texture: nil)
|
|
55
|
+
@max = Integer(max)
|
|
56
|
+
@gravity_x = Float(gravity[0])
|
|
57
|
+
@gravity_y = Float(gravity[1])
|
|
58
|
+
@texture = texture
|
|
59
|
+
@particles = []
|
|
60
|
+
@vertex_array = VertexArray.new(:triangles)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
attr_reader :particles, :texture
|
|
64
|
+
def size = @particles.size
|
|
65
|
+
def empty? = @particles.empty?
|
|
66
|
+
def full? = @particles.size >= @max
|
|
67
|
+
|
|
68
|
+
# Spawn a new particle. Silently dropped if the pool is full
|
|
69
|
+
# (better than reallocating; users can size up `max:`).
|
|
70
|
+
#
|
|
71
|
+
# @param position [Vector2, Array] world coords
|
|
72
|
+
# @param velocity [Vector2, Array] px/s
|
|
73
|
+
# @param lifetime [Numeric] seconds
|
|
74
|
+
# @param color [SFML::Color]
|
|
75
|
+
# @param size [Numeric] half-side of the quad in pixels
|
|
76
|
+
def spawn(position:, velocity: [0, 0], lifetime: 1.0, color: Color.white, size: 4)
|
|
77
|
+
return if full?
|
|
78
|
+
|
|
79
|
+
px, py = _xy(position)
|
|
80
|
+
vx, vy = _xy(velocity)
|
|
81
|
+
|
|
82
|
+
@particles << Particle.new(
|
|
83
|
+
Float(px), Float(py),
|
|
84
|
+
Float(vx), Float(vy),
|
|
85
|
+
0.0, Float(lifetime),
|
|
86
|
+
color.r, color.g, color.b, color.a,
|
|
87
|
+
Float(size),
|
|
88
|
+
)
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Drop every live particle.
|
|
93
|
+
def clear
|
|
94
|
+
@particles.clear
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Advance all particles by `dt` and remove dead ones.
|
|
99
|
+
def update(dt)
|
|
100
|
+
seconds = dt.is_a?(Time) ? dt.as_seconds : Float(dt)
|
|
101
|
+
gx, gy = @gravity_x, @gravity_y
|
|
102
|
+
|
|
103
|
+
@particles.each do |p|
|
|
104
|
+
next unless p.alive?
|
|
105
|
+
|
|
106
|
+
# Gravity + integration. Subclasses can override
|
|
107
|
+
# update_particle to layer additional forces / drag.
|
|
108
|
+
p.vx += gx * seconds
|
|
109
|
+
p.vy += gy * seconds
|
|
110
|
+
p.x += p.vx * seconds
|
|
111
|
+
p.y += p.vy * seconds
|
|
112
|
+
p.age += seconds
|
|
113
|
+
update_particle(p, seconds)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
@particles.delete_if { |p| !p.alive? }
|
|
117
|
+
|
|
118
|
+
self
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Hook: tweak a particle's state. Default does nothing; override
|
|
122
|
+
# for drag, attractors, colour curves over lifetime, etc.
|
|
123
|
+
def update_particle(_particle, _dt); end
|
|
124
|
+
|
|
125
|
+
# Drawable interface — rebuilds the VertexArray and forwards.
|
|
126
|
+
def draw_on(target, states_ptr = nil)
|
|
127
|
+
_rebuild_vertex_array
|
|
128
|
+
@vertex_array.draw_on(target, states_ptr)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def _xy(value)
|
|
134
|
+
case value
|
|
135
|
+
when Vector2 then [value.x, value.y]
|
|
136
|
+
when Array then value
|
|
137
|
+
else raise ArgumentError, "expected Vector2 or [x, y]; got #{value.class}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Pack live particles into the vertex array as two triangles each.
|
|
142
|
+
# We rebuild from scratch every frame — for ~1000 particles this
|
|
143
|
+
# is ~6000 vertex writes per frame, which is well within budget.
|
|
144
|
+
def _rebuild_vertex_array
|
|
145
|
+
@vertex_array.clear
|
|
146
|
+
@particles.each do |p|
|
|
147
|
+
next unless p.alive?
|
|
148
|
+
|
|
149
|
+
s = p.size
|
|
150
|
+
# Fade alpha linearly from full → zero across lifetime.
|
|
151
|
+
alpha = (p.a * (1.0 - p.normalized_age)).round.clamp(0, 255)
|
|
152
|
+
col = Color.new(p.r, p.g, p.b, alpha)
|
|
153
|
+
|
|
154
|
+
# Two triangles forming a quad centred on (x, y).
|
|
155
|
+
tl = Vertex.new([p.x - s, p.y - s], color: col)
|
|
156
|
+
tr = Vertex.new([p.x + s, p.y - s], color: col)
|
|
157
|
+
bl = Vertex.new([p.x - s, p.y + s], color: col)
|
|
158
|
+
br = Vertex.new([p.x + s, p.y + s], color: col)
|
|
159
|
+
|
|
160
|
+
@vertex_array << tl << tr << br
|
|
161
|
+
@vertex_array << tl << br << bl
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -14,7 +14,7 @@ module SFML
|
|
|
14
14
|
|
|
15
15
|
def initialize(size:, **opts)
|
|
16
16
|
ptr = C::Graphics.sfRectangleShape_create
|
|
17
|
-
raise
|
|
17
|
+
raise GraphicsError, "sfRectangleShape_create returned NULL" if ptr.null?
|
|
18
18
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfRectangleShape_destroy))
|
|
19
19
|
|
|
20
20
|
self.size = size
|
|
@@ -27,7 +27,7 @@ module SFML
|
|
|
27
27
|
size[:y] = Integer(height)
|
|
28
28
|
|
|
29
29
|
ptr = C::Graphics.sfRenderTexture_create(size, nil)
|
|
30
|
-
raise
|
|
30
|
+
raise GraphicsError, "sfRenderTexture_create returned NULL" if ptr.null?
|
|
31
31
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfRenderTexture_destroy))
|
|
32
32
|
|
|
33
33
|
self.smooth = smooth
|
|
@@ -96,7 +96,7 @@ module SFML
|
|
|
96
96
|
def texture
|
|
97
97
|
@texture ||= begin
|
|
98
98
|
ptr = C::Graphics.sfRenderTexture_getTexture(@handle)
|
|
99
|
-
raise
|
|
99
|
+
raise GraphicsError, "sfRenderTexture_getTexture returned NULL" if ptr.null?
|
|
100
100
|
# Borrowed — RenderTexture owns the underlying sf::Texture, so
|
|
101
101
|
# we wrap with a raw pointer (no AutoPointer / no destructor).
|
|
102
102
|
# Sprite.new(@texture) will still get a valid handle through it.
|
|
@@ -49,7 +49,7 @@ module SFML
|
|
|
49
49
|
C::Window::State[state],
|
|
50
50
|
ctx_ptr,
|
|
51
51
|
)
|
|
52
|
-
raise
|
|
52
|
+
raise WindowError, "sfRenderWindow_create returned NULL" if ptr.null?
|
|
53
53
|
|
|
54
54
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfRenderWindow_destroy))
|
|
55
55
|
@event_buffer = C::Window::Event.new
|
|
@@ -132,6 +132,30 @@ module SFML
|
|
|
132
132
|
Vector2.new(v[:x], v[:y])
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
# Capture the current back-buffer to disk. The format is inferred
|
|
136
|
+
# from the file extension (png / jpg / bmp / tga supported by
|
|
137
|
+
# CSFML's stb_image-based saver). Returns the path.
|
|
138
|
+
#
|
|
139
|
+
# window.screenshot("screenshot.png")
|
|
140
|
+
#
|
|
141
|
+
# `format:` lets you save under a different name than the
|
|
142
|
+
# extension. For an in-memory copy, use `#capture_image` instead.
|
|
143
|
+
def screenshot(path, format: nil)
|
|
144
|
+
capture_image.save(path.to_s)
|
|
145
|
+
path
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Read the current back-buffer into a fresh CPU-side SFML::Image.
|
|
149
|
+
# Useful for processing/encoding the frame yourself (sending it
|
|
150
|
+
# over a socket, encoding as JPEG memory bytes, comparing
|
|
151
|
+
# against a reference, etc.).
|
|
152
|
+
def capture_image
|
|
153
|
+
w, h = size.x, size.y
|
|
154
|
+
tex = Texture.create(w, h)
|
|
155
|
+
tex.update_from_render_window(self)
|
|
156
|
+
tex.to_image
|
|
157
|
+
end
|
|
158
|
+
|
|
135
159
|
# Replace the window's title-bar / taskbar icon with the pixels from
|
|
136
160
|
# the given SFML::Image. The OS scales it as needed; 32×32 RGBA
|
|
137
161
|
# is the typical sweet spot.
|
|
@@ -247,7 +271,7 @@ module SFML
|
|
|
247
271
|
def self.from_handle(handle)
|
|
248
272
|
ptr = handle.is_a?(FFI::Pointer) ? handle : FFI::Pointer.new(:void, Integer(handle))
|
|
249
273
|
raw = C::Graphics.sfRenderWindow_createFromHandle(ptr, nil)
|
|
250
|
-
raise
|
|
274
|
+
raise WindowError, "sfRenderWindow_createFromHandle returned NULL" if raw.null?
|
|
251
275
|
|
|
252
276
|
win = allocate
|
|
253
277
|
win.instance_variable_set(:@handle,
|
data/lib/sfml/graphics/shader.rb
CHANGED
|
@@ -56,7 +56,7 @@ module SFML
|
|
|
56
56
|
ptr = C::Graphics.sfShader_createFromFile(
|
|
57
57
|
vertex&.to_s, geometry&.to_s, fragment&.to_s,
|
|
58
58
|
)
|
|
59
|
-
raise
|
|
59
|
+
raise ShaderError, "sfShader_createFromFile failed (compile error or missing file?)" if ptr.null?
|
|
60
60
|
_wrap(ptr)
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -64,7 +64,7 @@ module SFML
|
|
|
64
64
|
def self.from_source(vertex: nil, geometry: nil, fragment: nil)
|
|
65
65
|
_check_at_least_one(vertex, geometry, fragment)
|
|
66
66
|
ptr = C::Graphics.sfShader_createFromMemory(vertex, geometry, fragment)
|
|
67
|
-
raise
|
|
67
|
+
raise ShaderError, "sfShader_createFromMemory failed (GLSL compile error?)" if ptr.null?
|
|
68
68
|
_wrap(ptr)
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -76,7 +76,7 @@ module SFML
|
|
|
76
76
|
streams = [vertex, geometry, fragment].map { |io| io && SFML::InputStream.new(io) }
|
|
77
77
|
ptrs = streams.map { |s| s ? s.to_ptr : nil }
|
|
78
78
|
ptr = C::Graphics.sfShader_createFromStream(*ptrs)
|
|
79
|
-
raise
|
|
79
|
+
raise ShaderError, "sfShader_createFromStream failed (GLSL compile error?)" if ptr.null?
|
|
80
80
|
_wrap(ptr)
|
|
81
81
|
end
|
|
82
82
|
|
data/lib/sfml/graphics/shape.rb
CHANGED
|
@@ -47,7 +47,7 @@ module SFML
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
ptr = C::Graphics.sfShape_create(@get_point_count_cb, @get_point_cb, nil)
|
|
50
|
-
raise
|
|
50
|
+
raise GraphicsError, "sfShape_create returned NULL" if ptr.null?
|
|
51
51
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfShape_destroy))
|
|
52
52
|
|
|
53
53
|
self.fill_color = opts[:fill_color] if opts.key?(:fill_color)
|
|
@@ -78,7 +78,7 @@ module SFML
|
|
|
78
78
|
# cheap to alias); transform/colour state is independent.
|
|
79
79
|
def dup
|
|
80
80
|
ptr = _csfml(:copy, @handle)
|
|
81
|
-
raise
|
|
81
|
+
raise GraphicsError, "#{self.class.name}#dup returned NULL" if ptr.null?
|
|
82
82
|
|
|
83
83
|
destroy_fn = C::Graphics.method(:"#{self.class::CSFML_PREFIX}_destroy")
|
|
84
84
|
copy = self.class.allocate
|
data/lib/sfml/graphics/sprite.rb
CHANGED
|
@@ -15,7 +15,7 @@ module SFML
|
|
|
15
15
|
raise ArgumentError, "Sprite requires a SFML::Texture" unless texture.is_a?(Texture)
|
|
16
16
|
|
|
17
17
|
ptr = C::Graphics.sfSprite_create(texture.handle)
|
|
18
|
-
raise
|
|
18
|
+
raise GraphicsError, "sfSprite_create returned NULL" if ptr.null?
|
|
19
19
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfSprite_destroy))
|
|
20
20
|
@texture = texture # keep alive for GC
|
|
21
21
|
|
|
@@ -92,7 +92,7 @@ module SFML
|
|
|
92
92
|
# objects), independent transform / colour state.
|
|
93
93
|
def dup
|
|
94
94
|
ptr = C::Graphics.sfSprite_copy(@handle)
|
|
95
|
-
raise
|
|
95
|
+
raise GraphicsError, "sfSprite_copy returned NULL" if ptr.null?
|
|
96
96
|
|
|
97
97
|
copy = self.class.allocate
|
|
98
98
|
copy.instance_variable_set(:@handle,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module SFML
|
|
2
|
+
# A grid-based slice of a texture. Where `TextureAtlas` loads an
|
|
3
|
+
# already-packed sheet with named frames, `SpriteSheet` slices a
|
|
4
|
+
# uniformly-gridded image into numbered frames at load time.
|
|
5
|
+
#
|
|
6
|
+
# Use a SpriteSheet when your art is a regular grid (run cycles
|
|
7
|
+
# baked at 32×32 cells); use a TextureAtlas when frames are
|
|
8
|
+
# densely packed at irregular sizes.
|
|
9
|
+
#
|
|
10
|
+
# sheet = SFML::SpriteSheet.load("hero.png", frame_size: [32, 32])
|
|
11
|
+
#
|
|
12
|
+
# sheet.frame_count # 8 if the image is 256×32
|
|
13
|
+
# sheet.region(0) # SFML::Rect of the first cell
|
|
14
|
+
# sheet.sprite(0) # ready-to-draw SFML::Sprite of the first cell
|
|
15
|
+
#
|
|
16
|
+
# `frame_size` can be a `[w, h]` pair (one number for both if
|
|
17
|
+
# `frame_size: 32`). `padding` and `margin` let you skip pixels
|
|
18
|
+
# between cells and around the edge respectively — common when
|
|
19
|
+
# the source image has separators or a 1px outline to prevent
|
|
20
|
+
# bleed.
|
|
21
|
+
class SpriteSheet
|
|
22
|
+
def self.load(path, frame_size:, padding: 0, margin: 0, smooth: true)
|
|
23
|
+
new(texture: Texture.load(path, smooth: smooth),
|
|
24
|
+
frame_size: frame_size, padding: padding, margin: margin)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(texture:, frame_size:, padding: 0, margin: 0)
|
|
28
|
+
@texture = texture
|
|
29
|
+
fw, fh = _pair(frame_size)
|
|
30
|
+
pw, ph = _pair(padding)
|
|
31
|
+
mw, mh = _pair(margin)
|
|
32
|
+
|
|
33
|
+
tex_w, tex_h = texture.size.x, texture.size.y
|
|
34
|
+
@cols = (tex_w - 2 * mw + pw) / (fw + pw)
|
|
35
|
+
@rows = (tex_h - 2 * mh + ph) / (fh + ph)
|
|
36
|
+
@frame_w, @frame_h = fw, fh
|
|
37
|
+
|
|
38
|
+
# Pre-compute every rect once. Cheap (a few hundred at most),
|
|
39
|
+
# and #region becomes a flat array lookup.
|
|
40
|
+
@regions = Array.new(@cols * @rows) do |i|
|
|
41
|
+
col = i % @cols
|
|
42
|
+
row = i / @cols
|
|
43
|
+
x = mw + col * (fw + pw)
|
|
44
|
+
y = mh + row * (fh + ph)
|
|
45
|
+
Rect.new([x, y], [fw, fh])
|
|
46
|
+
end.freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :texture, :cols, :rows, :frame_w, :frame_h
|
|
50
|
+
|
|
51
|
+
def frame_count = @regions.size
|
|
52
|
+
|
|
53
|
+
# The pixel Rect for cell `index` (0 = top-left, increases
|
|
54
|
+
# row-major). Negative indexes wrap (e.g. -1 = last).
|
|
55
|
+
def region(index)
|
|
56
|
+
i = Integer(index) % frame_count
|
|
57
|
+
@regions[i]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `[col, row]` accessor for callers who prefer 2D coords.
|
|
61
|
+
def region_at(col, row)
|
|
62
|
+
raise IndexError, "col #{col} out of range (cols: #{@cols})" if col < 0 || col >= @cols
|
|
63
|
+
raise IndexError, "row #{row} out of range (rows: #{@rows})" if row < 0 || row >= @rows
|
|
64
|
+
@regions[row * @cols + col]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Fresh Sprite pointing at cell `index`. Combine with `#sprite=`
|
|
68
|
+
# patterns or feed into `SFML::Animation`.
|
|
69
|
+
def sprite(index, **opts)
|
|
70
|
+
Sprite.new(@texture, **opts).tap { |s| s.texture_rect = region(index) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# An Animation looping through `frame_indexes` (defaults to all
|
|
74
|
+
# frames in row-major order).
|
|
75
|
+
def animation(frame_indexes: nil, fps: 12, loop: true)
|
|
76
|
+
indexes = frame_indexes || (0...frame_count).to_a
|
|
77
|
+
Animation.new(
|
|
78
|
+
self,
|
|
79
|
+
frames: indexes.map { |i| region(i) },
|
|
80
|
+
fps: fps,
|
|
81
|
+
loop: loop,
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_s = "#<SpriteSheet #{@cols}×#{@rows} (#{@frame_w}×#{@frame_h}px cells)>"
|
|
86
|
+
alias inspect to_s
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Accept 32 OR [32, 32] OR Vector2[32, 32].
|
|
91
|
+
def _pair(value)
|
|
92
|
+
case value
|
|
93
|
+
when Numeric then [Integer(value), Integer(value)]
|
|
94
|
+
when Vector2 then [Integer(value.x), Integer(value.y)]
|
|
95
|
+
when Array then value.map { |n| Integer(n) }
|
|
96
|
+
else raise ArgumentError, "expected Integer, [w, h], or Vector2; got #{value.class}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/sfml/graphics/text.rb
CHANGED
|
@@ -27,7 +27,7 @@ module SFML
|
|
|
27
27
|
raise ArgumentError, "Text requires a SFML::Font" unless font.is_a?(Font)
|
|
28
28
|
|
|
29
29
|
ptr = C::Graphics.sfText_create(font.handle)
|
|
30
|
-
raise
|
|
30
|
+
raise GraphicsError, "sfText_create returned NULL" if ptr.null?
|
|
31
31
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfText_destroy))
|
|
32
32
|
@font = font # keep alive for GC
|
|
33
33
|
|
|
@@ -161,7 +161,7 @@ module SFML
|
|
|
161
161
|
# (Fonts are shareable; each owns its own glyph atlas).
|
|
162
162
|
def dup
|
|
163
163
|
ptr = C::Graphics.sfText_copy(@handle)
|
|
164
|
-
raise
|
|
164
|
+
raise GraphicsError, "sfText_copy returned NULL" if ptr.null?
|
|
165
165
|
|
|
166
166
|
copy = self.class.allocate
|
|
167
167
|
copy.instance_variable_set(:@handle, FFI::AutoPointer.new(ptr, C::Graphics.method(:sfText_destroy)))
|
|
@@ -16,7 +16,7 @@ module SFML
|
|
|
16
16
|
ptr = srgb \
|
|
17
17
|
? C::Graphics.sfTexture_createSrgbFromFile(path.to_s, nil) \
|
|
18
18
|
: C::Graphics.sfTexture_createFromFile(path.to_s, nil)
|
|
19
|
-
raise
|
|
19
|
+
raise LoadError, "Could not load texture from #{path.inspect}" if ptr.null?
|
|
20
20
|
|
|
21
21
|
tex = allocate
|
|
22
22
|
tex.send(:_take_ownership, ptr)
|
|
@@ -33,7 +33,7 @@ module SFML
|
|
|
33
33
|
size = C::System::Vector2u.new
|
|
34
34
|
size[:x] = Integer(width); size[:y] = Integer(height)
|
|
35
35
|
ptr = srgb ? C::Graphics.sfTexture_createSrgb(size) : C::Graphics.sfTexture_create(size)
|
|
36
|
-
raise
|
|
36
|
+
raise GraphicsError, "sfTexture_create returned NULL — out of GPU memory?" if ptr.null?
|
|
37
37
|
|
|
38
38
|
tex = allocate
|
|
39
39
|
tex.send(:_take_ownership, ptr)
|
|
@@ -51,7 +51,7 @@ module SFML
|
|
|
51
51
|
ptr = srgb \
|
|
52
52
|
? C::Graphics.sfTexture_createSrgbFromMemory(buf, bytes.bytesize, nil) \
|
|
53
53
|
: C::Graphics.sfTexture_createFromMemory(buf, bytes.bytesize, nil)
|
|
54
|
-
raise
|
|
54
|
+
raise LoadError, "sfTexture_createFromMemory returned NULL — unsupported format?" if ptr.null?
|
|
55
55
|
|
|
56
56
|
tex = allocate
|
|
57
57
|
tex.send(:_take_ownership, ptr)
|
|
@@ -69,7 +69,7 @@ module SFML
|
|
|
69
69
|
ptr = srgb \
|
|
70
70
|
? C::Graphics.sfTexture_createSrgbFromStream(stream.to_ptr, nil) \
|
|
71
71
|
: C::Graphics.sfTexture_createFromStream(stream.to_ptr, nil)
|
|
72
|
-
raise
|
|
72
|
+
raise LoadError, "sfTexture_createFromStream returned NULL — unsupported format?" if ptr.null?
|
|
73
73
|
|
|
74
74
|
tex = allocate
|
|
75
75
|
tex.send(:_take_ownership, ptr)
|
|
@@ -86,7 +86,7 @@ module SFML
|
|
|
86
86
|
ptr = srgb \
|
|
87
87
|
? C::Graphics.sfTexture_createSrgbFromImage(image.handle, nil) \
|
|
88
88
|
: C::Graphics.sfTexture_createFromImage(image.handle, nil)
|
|
89
|
-
raise
|
|
89
|
+
raise LoadError, "sfTexture_createFromImage returned NULL" if ptr.null?
|
|
90
90
|
|
|
91
91
|
tex = allocate
|
|
92
92
|
tex.send(:_take_ownership, ptr)
|
|
@@ -110,7 +110,7 @@ module SFML
|
|
|
110
110
|
# — useful for screenshots or post-processing inspection.
|
|
111
111
|
def to_image
|
|
112
112
|
ptr = C::Graphics.sfTexture_copyToImage(@handle)
|
|
113
|
-
raise
|
|
113
|
+
raise GraphicsError, "sfTexture_copyToImage returned NULL" if ptr.null?
|
|
114
114
|
img = Image.allocate
|
|
115
115
|
img.send(:_take_ownership, ptr)
|
|
116
116
|
img
|
|
@@ -164,7 +164,7 @@ module SFML
|
|
|
164
164
|
# Deep copy. The returned texture has its own GPU memory.
|
|
165
165
|
def dup
|
|
166
166
|
ptr = C::Graphics.sfTexture_copy(@handle)
|
|
167
|
-
raise
|
|
167
|
+
raise GraphicsError, "sfTexture_copy returned NULL" if ptr.null?
|
|
168
168
|
|
|
169
169
|
tex = self.class.allocate
|
|
170
170
|
tex.send(:_take_ownership, ptr)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module SFML
|
|
4
|
+
# A sprite-sheet plus a frame-name → rectangle index. Loads the
|
|
5
|
+
# JSON descriptors that Aseprite and TexturePacker export — both
|
|
6
|
+
# use the same `frames + meta` shape, just with a few field
|
|
7
|
+
# differences we paper over.
|
|
8
|
+
#
|
|
9
|
+
# atlas = SFML::TextureAtlas.load("hero.json")
|
|
10
|
+
#
|
|
11
|
+
# atlas.texture #=> SFML::Texture
|
|
12
|
+
# atlas.region("hero-walk-0") #=> SFML::Rect
|
|
13
|
+
# atlas.frame_names #=> [...]
|
|
14
|
+
# atlas.sprite("hero-walk-0") #=> ready-to-draw SFML::Sprite
|
|
15
|
+
# atlas.duration("hero-walk-0") #=> Integer ms (Aseprite only)
|
|
16
|
+
#
|
|
17
|
+
# The image path inside the JSON is resolved relative to the JSON
|
|
18
|
+
# file's directory. Override with `image: "path/to/img.png"`.
|
|
19
|
+
#
|
|
20
|
+
# Frame lookups are tolerant of the `.png` extension — Aseprite
|
|
21
|
+
# exports `walk-0.png`, but you can write `atlas.region("walk-0")`.
|
|
22
|
+
class TextureAtlas
|
|
23
|
+
IMAGE_EXTS = /\.(?:png|jpg|jpeg|bmp|tga|gif)\z/i
|
|
24
|
+
|
|
25
|
+
# @param json_path [String] path to the atlas JSON
|
|
26
|
+
# @param image [String, nil] explicit image path (overrides
|
|
27
|
+
# the path embedded in the JSON's `meta.image` field)
|
|
28
|
+
# @return [TextureAtlas]
|
|
29
|
+
def self.load(json_path, image: nil, smooth: true)
|
|
30
|
+
data = JSON.parse(File.read(json_path))
|
|
31
|
+
image_path = image || File.expand_path(data.dig("meta", "image") || "", File.dirname(json_path))
|
|
32
|
+
raise LoadError, "TextureAtlas: image not found at #{image_path}" unless File.file?(image_path)
|
|
33
|
+
|
|
34
|
+
texture = Texture.load(image_path, smooth: smooth)
|
|
35
|
+
regions, durations = _parse_frames(data["frames"])
|
|
36
|
+
new(texture: texture, regions: regions, durations: durations, source: json_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Build directly from a Texture + a frame Hash — useful when
|
|
40
|
+
# generating atlases procedurally or when the descriptor lives
|
|
41
|
+
# in a different format you've already parsed.
|
|
42
|
+
def initialize(texture:, regions:, durations: {}, source: nil)
|
|
43
|
+
@texture = texture
|
|
44
|
+
@regions = regions.transform_keys { |k| _normalize(k) }.freeze
|
|
45
|
+
@durations = durations.transform_keys { |k| _normalize(k) }.freeze
|
|
46
|
+
@source = source
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :texture, :source
|
|
50
|
+
|
|
51
|
+
def frame_names = @regions.keys
|
|
52
|
+
|
|
53
|
+
# @return [SFML::Rect] the pixel rect for frame `name`.
|
|
54
|
+
# @raise [SFML::LoadError] if the frame isn't in this atlas.
|
|
55
|
+
def region(name)
|
|
56
|
+
@regions[_normalize(name)] or
|
|
57
|
+
raise LoadError, "TextureAtlas: no frame named #{name.inspect} " \
|
|
58
|
+
"(have: #{@regions.keys.first(5).inspect}...)"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Build a fresh Sprite bound to the atlas texture with its
|
|
62
|
+
# texture_rect set to this frame.
|
|
63
|
+
def sprite(name, **opts)
|
|
64
|
+
Sprite.new(@texture, **opts).tap { |s| s.texture_rect = region(name) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Build an Animation from a list of frame names. When `fps:` is
|
|
68
|
+
# nil and Aseprite-style `duration` data is present, the per-frame
|
|
69
|
+
# durations are used directly — letting artists set timing in
|
|
70
|
+
# Aseprite without re-tuning in code. Pass an explicit `fps:` to
|
|
71
|
+
# override.
|
|
72
|
+
def animation(frame_names, fps: nil, loop: true)
|
|
73
|
+
rects = frame_names.map { |n| region(n) }
|
|
74
|
+
if fps.nil? && frame_names.first && duration(frame_names.first) > 0
|
|
75
|
+
# Use Aseprite's per-frame ms timings — collapse to average
|
|
76
|
+
# fps for the Animation; per-frame durations would need a
|
|
77
|
+
# variable-rate Animation we don't ship yet.
|
|
78
|
+
avg_ms = frame_names.map { |n| duration(n) }.sum / frame_names.size.to_f
|
|
79
|
+
fps = 1000.0 / avg_ms if avg_ms > 0
|
|
80
|
+
end
|
|
81
|
+
Animation.new(self, frames: rects, fps: fps || 12, loop: loop)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Integer milliseconds per frame, as exported by Aseprite. Returns
|
|
85
|
+
# 0 if the source didn't include durations (TexturePacker etc.).
|
|
86
|
+
def duration(name) = @durations[_normalize(name)] || 0
|
|
87
|
+
|
|
88
|
+
# The internal frame-name → Rect hash. Useful for filtering:
|
|
89
|
+
#
|
|
90
|
+
# walk_frames = atlas.regions.select { |k, _| k.start_with?("walk-") }
|
|
91
|
+
def regions = @regions
|
|
92
|
+
|
|
93
|
+
def to_s = "#<TextureAtlas #{@regions.size} frames#{@source ? " from #{File.basename(@source)}" : ""}>"
|
|
94
|
+
alias inspect to_s
|
|
95
|
+
|
|
96
|
+
# @!visibility private
|
|
97
|
+
def self._parse_frames(raw)
|
|
98
|
+
regions = {}
|
|
99
|
+
durations = {}
|
|
100
|
+
|
|
101
|
+
entries =
|
|
102
|
+
case raw
|
|
103
|
+
when Hash then raw.map { |name, entry| [name, entry] }
|
|
104
|
+
when Array then raw.map { |entry| [entry.fetch("filename"), entry] }
|
|
105
|
+
else raise LoadError, "TextureAtlas: unexpected `frames` shape: #{raw.class}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
entries.each do |name, entry|
|
|
109
|
+
rect = entry.fetch("frame")
|
|
110
|
+
regions[name] = Rect.new([rect["x"], rect["y"]], [rect["w"], rect["h"]])
|
|
111
|
+
durations[name] = entry["duration"] if entry["duration"]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
[regions, durations]
|
|
115
|
+
end
|
|
116
|
+
private_class_method :_parse_frames
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Frame names are stored stripped of any image extension so
|
|
121
|
+
# callers can use either `walk-0` or `walk-0.png`.
|
|
122
|
+
def _normalize(name)
|
|
123
|
+
name.to_s.sub(IMAGE_EXTS, "")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -16,7 +16,7 @@ module SFML
|
|
|
16
16
|
|
|
17
17
|
def initialize(**opts)
|
|
18
18
|
ptr = C::Graphics.sfTransformable_create
|
|
19
|
-
raise
|
|
19
|
+
raise GraphicsError, "sfTransformable_create returned NULL" if ptr.null?
|
|
20
20
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfTransformable_destroy))
|
|
21
21
|
|
|
22
22
|
self.position = opts[:position] if opts.key?(:position)
|
|
@@ -35,7 +35,7 @@ module SFML
|
|
|
35
35
|
|
|
36
36
|
def dup
|
|
37
37
|
ptr = C::Graphics.sfTransformable_copy(@handle)
|
|
38
|
-
raise
|
|
38
|
+
raise GraphicsError, "sfTransformable_copy returned NULL" if ptr.null?
|
|
39
39
|
copy = self.class.allocate
|
|
40
40
|
copy.instance_variable_set(:@handle,
|
|
41
41
|
FFI::AutoPointer.new(ptr, C::Graphics.method(:sfTransformable_destroy)))
|
|
@@ -25,7 +25,7 @@ module SFML
|
|
|
25
25
|
|
|
26
26
|
def initialize(primitive_type = :points, vertices = nil)
|
|
27
27
|
ptr = C::Graphics.sfVertexArray_create
|
|
28
|
-
raise
|
|
28
|
+
raise GraphicsError, "sfVertexArray_create returned NULL" if ptr.null?
|
|
29
29
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfVertexArray_destroy))
|
|
30
30
|
|
|
31
31
|
self.primitive_type = primitive_type
|
|
@@ -113,7 +113,7 @@ module SFML
|
|
|
113
113
|
# don't affect the other.
|
|
114
114
|
def dup
|
|
115
115
|
ptr = C::Graphics.sfVertexArray_copy(@handle)
|
|
116
|
-
raise
|
|
116
|
+
raise GraphicsError, "sfVertexArray_copy returned NULL" if ptr.null?
|
|
117
117
|
copy = self.class.allocate
|
|
118
118
|
copy.instance_variable_set(:@handle,
|
|
119
119
|
FFI::AutoPointer.new(ptr, C::Graphics.method(:sfVertexArray_destroy)))
|
|
@@ -49,7 +49,7 @@ module SFML
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
ptr = C::Graphics.sfVertexBuffer_create(n, ptype, uidx)
|
|
52
|
-
raise
|
|
52
|
+
raise GraphicsError, "sfVertexBuffer_create returned NULL " \
|
|
53
53
|
"(VBOs unavailable on this GPU?)" if ptr.null?
|
|
54
54
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfVertexBuffer_destroy))
|
|
55
55
|
|
|
@@ -101,7 +101,7 @@ module SFML
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
ok = C::Graphics.sfVertexBuffer_update(@handle, buf, n, Integer(offset))
|
|
104
|
-
raise
|
|
104
|
+
raise GraphicsError, "sfVertexBuffer_update failed " \
|
|
105
105
|
"(buffer too small or driver rejected the upload?)" unless ok
|
|
106
106
|
self
|
|
107
107
|
end
|