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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e15752d993b5c76445ea903b92227b45c169009bed4de15166ff5db1d1fbd372
|
|
4
|
+
data.tar.gz: bf5ae66de3f3df40c765c5aee29eef1138d1743e899e6e072e707f63ee0fcf84
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dd013a7669a09457c0f5a6915ce8ee91ac32018146d0dae6a6af6d73f5f4dba8864778827aac10e2194c553b715a18895be056f41d0dc8002cfcc99087be4664
|
|
7
|
+
data.tar.gz: 41ba407931cdf67f1493187c517a59d8b981d62ae4fd7ccf051696b9348fb03046228c651d2f0be45f0edb40b5f6a2ef417f4cda6dfd7124e04a46489bb7b258
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,82 @@ ruby-sfml's own patch level.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [3.0.0.6] — 2026-05-12
|
|
12
|
+
|
|
13
|
+
Quality-of-life release: the CSFML 3.0 surface was already covered;
|
|
14
|
+
this round adds the helpers and tooling you reach for when building
|
|
15
|
+
on top of it.
|
|
16
|
+
|
|
17
|
+
### Added — system
|
|
18
|
+
|
|
19
|
+
- **Vector2 / Vector3 math** — `#distance`, `#distance_sq`,
|
|
20
|
+
`#lerp`, `#project_on`, `#reflect`, `#clamp_length`, `#zero?`,
|
|
21
|
+
`#abs`, plus `Vector2#angle`, `#angle_to(other)`,
|
|
22
|
+
`#rotated(degrees)` / `#rotated_rad(radians)`,
|
|
23
|
+
`#perpendicular`, and `Vector3#angle_between`.
|
|
24
|
+
Both classes gain `#to_v3` / `#to_v2` for cross-dimension
|
|
25
|
+
promotion + scalar coercion (`2 * vec`).
|
|
26
|
+
|
|
27
|
+
### Added — graphics
|
|
28
|
+
|
|
29
|
+
- `RenderWindow#screenshot(path)` — capture the current
|
|
30
|
+
back-buffer to disk (PNG / JPG / BMP / TGA inferred by
|
|
31
|
+
extension).
|
|
32
|
+
- `RenderWindow#capture_image` — same capture, returns an
|
|
33
|
+
in-memory `SFML::Image` for further processing.
|
|
34
|
+
- **`SFML::SpriteSheet`** — slice a uniformly-gridded image into
|
|
35
|
+
numbered frames. `.load(path, frame_size:, padding:, margin:)`,
|
|
36
|
+
`#region(i)`, `#region_at(col, row)`, `#sprite(i)`,
|
|
37
|
+
`#animation(fps:, ...)`.
|
|
38
|
+
- **`SFML::TextureAtlas`** — load Aseprite / TexturePacker JSON
|
|
39
|
+
descriptors. `.load(json_path)`, `#region(name)`,
|
|
40
|
+
`#sprite(name)`, `#animation(names, fps:)` (auto-derives fps
|
|
41
|
+
from Aseprite per-frame durations when present).
|
|
42
|
+
- **`SFML::Animation`** — frame-based animation that drives a
|
|
43
|
+
Sprite's texture_rect over time. Loop / one-shot,
|
|
44
|
+
`#update(dt)`, `#reset`, `#done?`. Sprite-style transform
|
|
45
|
+
setters (`position=`, `rotation=`, `scale=`, `origin=`,
|
|
46
|
+
`color=`) for ergonomic use.
|
|
47
|
+
- **`SFML::ParticleSystem`** — VertexArray-backed particle pool.
|
|
48
|
+
`#spawn(position:, velocity:, lifetime:, color:, size:)`,
|
|
49
|
+
optional `gravity:`, `update_particle` subclass hook for
|
|
50
|
+
drag / attractors / colour curves.
|
|
51
|
+
|
|
52
|
+
### Added — game-loop
|
|
53
|
+
|
|
54
|
+
- **Fixed timestep** — `fixed_timestep N` class macro on
|
|
55
|
+
`SFML::App` calls `update(dt)` exactly N times per second with
|
|
56
|
+
a fixed dt (semi-implicit Euler accumulator, capped at 5
|
|
57
|
+
catch-up steps to prevent the "spiral of death"). Read
|
|
58
|
+
`interpolation_alpha` from `#draw` to smoothly render between
|
|
59
|
+
fixed updates.
|
|
60
|
+
- **Input actions DSL** — `action :jump, keys: [...],
|
|
61
|
+
scancodes: [...], mouse_buttons: [...], joy_buttons: [...]`
|
|
62
|
+
on `SFML::App` and `SFML::Scene`. Poll with `action_pressed?
|
|
63
|
+
(:name)` from `update` / `draw`; build digital axes with
|
|
64
|
+
`axis(negative:, positive:)`. Scene actions inherit from the
|
|
65
|
+
host App's actions.
|
|
66
|
+
|
|
67
|
+
### Added — errors
|
|
68
|
+
|
|
69
|
+
- Domain-specific exception hierarchy:
|
|
70
|
+
- `SFML::LoadError` — asset load failures (file, memory,
|
|
71
|
+
stream)
|
|
72
|
+
- `SFML::AudioError` — capture / OpenAL / channel-map
|
|
73
|
+
- `SFML::NetworkError` — sockets / packet framing
|
|
74
|
+
- `SFML::ShaderError` — GLSL compile / link
|
|
75
|
+
- `SFML::GraphicsError` — generic graphics-side failures
|
|
76
|
+
- `SFML::WindowError` — window / context creation
|
|
77
|
+
All inherit from `SFML::Error`, so existing
|
|
78
|
+
`rescue SFML::Error` blocks keep catching everything.
|
|
79
|
+
|
|
80
|
+
### Added — CI / tooling
|
|
81
|
+
|
|
82
|
+
- GitHub Actions release workflow (`release.yml`) — tag
|
|
83
|
+
`vX.Y.Z.W` triggers a gem build + push to RubyGems. Verifies
|
|
84
|
+
the tag matches `lib/sfml/version.rb` before publishing.
|
|
85
|
+
- CI badge in `README.md`.
|
|
86
|
+
|
|
11
87
|
## [3.0.0.5] — 2026-05-11
|
|
12
88
|
|
|
13
89
|
Round-trip release: closes every remaining CSFML 3.0 gap that's
|
data/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
Modern, idiomatic Ruby bindings for [SFML 3.x](https://www.sfml-dev.org/) via [CSFML](https://github.com/SFML/CSFML) and [Ruby FFI](https://github.com/ffi/ffi).
|
|
7
7
|
|
|
8
8
|
[](https://rubygems.org/gems/ruby-sfml)
|
|
9
|
+
[](https://github.com/sOM2H/ruby-sfml/actions/workflows/ci.yml)
|
|
9
10
|
[](https://www.rubydoc.info/gems/ruby-sfml)
|
|
10
11
|
|
|
11
12
|
> **Status:** API surface complete for SFML 3.0 — system, window, graphics (incl. stencil buffer + VBOs), audio (incl. 3D positional + custom DSP + procedural streams), network (incl. HTTP / FTP / socket selector), input (keyboard, mouse, joystick, touch, sensors), plus the higher-level `App` / `Scene` / `Assets` helpers. 410 RSpec examples, 24 runnable example folders.
|
data/lib/sfml/app.rb
CHANGED
|
@@ -41,8 +41,15 @@ module SFML
|
|
|
41
41
|
framerate vsync background
|
|
42
42
|
style fullscreen
|
|
43
43
|
antialiasing context
|
|
44
|
+
fixed_timestep
|
|
44
45
|
].freeze
|
|
45
46
|
|
|
47
|
+
# Cap on catch-up update calls per frame when running with
|
|
48
|
+
# `fixed_timestep`. Prevents the "spiral of death" where a slow
|
|
49
|
+
# frame queues so many physics steps that the next frame is
|
|
50
|
+
# even slower. After this many steps, residual time is dropped.
|
|
51
|
+
FIXED_TIMESTEP_MAX_CATCHUP = 5
|
|
52
|
+
|
|
46
53
|
class << self
|
|
47
54
|
CONFIG_KEYS.each do |key|
|
|
48
55
|
# Each macro doubles as a reader (no args) and a writer
|
|
@@ -63,6 +70,7 @@ module SFML
|
|
|
63
70
|
end
|
|
64
71
|
|
|
65
72
|
include Keybindings # provides `on_key` + `key_handlers`
|
|
73
|
+
include InputActions # provides `action` + `action_bindings`
|
|
66
74
|
|
|
67
75
|
# Set the scene class the app should switch into automatically
|
|
68
76
|
# at `setup` time. Inheritable: a subclass that doesn't set
|
|
@@ -74,6 +82,8 @@ module SFML
|
|
|
74
82
|
end
|
|
75
83
|
end
|
|
76
84
|
|
|
85
|
+
include InputQueries
|
|
86
|
+
|
|
77
87
|
attr_reader :window
|
|
78
88
|
attr_accessor :background_color
|
|
79
89
|
|
|
@@ -155,15 +165,56 @@ module SFML
|
|
|
155
165
|
def toggle_pause = (@paused = !paused?)
|
|
156
166
|
def paused? = @paused == true
|
|
157
167
|
|
|
168
|
+
# Fraction of a fixed timestep accumulated since the last
|
|
169
|
+
# `update`. In range [0.0, 1.0). Use it in `#draw` to
|
|
170
|
+
# interpolate between the previous and current world state:
|
|
171
|
+
#
|
|
172
|
+
# def draw
|
|
173
|
+
# pos = @prev_pos.lerp(@curr_pos, interpolation_alpha)
|
|
174
|
+
# ...
|
|
175
|
+
# end
|
|
176
|
+
#
|
|
177
|
+
# Only meaningful when `fixed_timestep` is set; otherwise 0.
|
|
178
|
+
attr_reader :interpolation_alpha
|
|
179
|
+
|
|
158
180
|
# The main entry point. Calls #setup once, then runs the
|
|
159
181
|
# per-frame loop until the window closes.
|
|
182
|
+
#
|
|
183
|
+
# When `fixed_timestep N` is set on the class, `update(dt)` is
|
|
184
|
+
# called exactly N times per second (with a fixed dt), and
|
|
185
|
+
# rendering runs as fast as vsync/framerate allows. This is
|
|
186
|
+
# the standard pattern for deterministic physics — without it,
|
|
187
|
+
# large-dt frames produce different results than small-dt frames.
|
|
160
188
|
def run
|
|
161
189
|
setup
|
|
162
|
-
clock
|
|
190
|
+
clock = Clock.new
|
|
191
|
+
ts = self.class.fixed_timestep
|
|
192
|
+
step_seconds = ts && (1.0 / ts)
|
|
193
|
+
dt_fixed = ts && Time.seconds(step_seconds)
|
|
194
|
+
accumulator = 0.0
|
|
195
|
+
@interpolation_alpha = 0.0
|
|
196
|
+
|
|
163
197
|
while @window.open?
|
|
164
|
-
|
|
198
|
+
frame_dt = clock.restart
|
|
165
199
|
@window.each_event { |event| _dispatch(event) }
|
|
166
|
-
|
|
200
|
+
|
|
201
|
+
if dt_fixed
|
|
202
|
+
accumulator += frame_dt.as_seconds
|
|
203
|
+
steps = 0
|
|
204
|
+
while accumulator >= step_seconds && steps < FIXED_TIMESTEP_MAX_CATCHUP
|
|
205
|
+
update(dt_fixed) unless paused?
|
|
206
|
+
accumulator -= step_seconds
|
|
207
|
+
steps += 1
|
|
208
|
+
end
|
|
209
|
+
# If we hit the catch-up cap, drop residual time — better
|
|
210
|
+
# to slightly slow the simulation than spiral into longer
|
|
211
|
+
# and longer frames.
|
|
212
|
+
accumulator = 0.0 if steps == FIXED_TIMESTEP_MAX_CATCHUP
|
|
213
|
+
@interpolation_alpha = accumulator / step_seconds
|
|
214
|
+
else
|
|
215
|
+
update(frame_dt) unless paused?
|
|
216
|
+
end
|
|
217
|
+
|
|
167
218
|
@window.clear(@background_color)
|
|
168
219
|
draw
|
|
169
220
|
@window.display
|
data/lib/sfml/audio/music.rb
CHANGED
|
@@ -7,7 +7,7 @@ module SFML
|
|
|
7
7
|
class Music
|
|
8
8
|
def self.load(path, **opts)
|
|
9
9
|
ptr = C::Audio.sfMusic_createFromFile(path.to_s)
|
|
10
|
-
raise
|
|
10
|
+
raise LoadError, "Could not load music from #{path.inspect}" if ptr.null?
|
|
11
11
|
|
|
12
12
|
_wrap(ptr, opts)
|
|
13
13
|
end
|
|
@@ -22,7 +22,7 @@ module SFML
|
|
|
22
22
|
buf = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
|
|
23
23
|
buf.write_bytes(bytes)
|
|
24
24
|
ptr = C::Audio.sfMusic_createFromMemory(buf, bytes.bytesize)
|
|
25
|
-
raise
|
|
25
|
+
raise LoadError, "sfMusic_createFromMemory returned NULL — unsupported format?" if ptr.null?
|
|
26
26
|
|
|
27
27
|
m = _wrap(ptr, opts)
|
|
28
28
|
m.instance_variable_set(:@_memory_pin, buf) # keep buffer alive
|
|
@@ -35,7 +35,7 @@ module SFML
|
|
|
35
35
|
def self.from_stream(io, **opts)
|
|
36
36
|
stream = SFML::InputStream.new(io)
|
|
37
37
|
ptr = C::Audio.sfMusic_createFromStream(stream.to_ptr)
|
|
38
|
-
raise
|
|
38
|
+
raise LoadError, "sfMusic_createFromStream returned NULL — unsupported format?" if ptr.null?
|
|
39
39
|
|
|
40
40
|
m = _wrap(ptr, opts)
|
|
41
41
|
m.instance_variable_set(:@_stream_pin, stream)
|
data/lib/sfml/audio/sound.rb
CHANGED
|
@@ -10,7 +10,7 @@ module SFML
|
|
|
10
10
|
raise ArgumentError, "Sound requires a SFML::SoundBuffer" unless buffer.is_a?(SoundBuffer)
|
|
11
11
|
|
|
12
12
|
ptr = C::Audio.sfSound_create(buffer.handle)
|
|
13
|
-
raise
|
|
13
|
+
raise AudioError, "sfSound_create returned NULL" if ptr.null?
|
|
14
14
|
@handle = FFI::AutoPointer.new(ptr, C::Audio.method(:sfSound_destroy))
|
|
15
15
|
@buffer = buffer # keep alive
|
|
16
16
|
# @looping mirrors the loop flag because SFML 3's isLooping reads
|
|
@@ -243,7 +243,7 @@ module SFML
|
|
|
243
243
|
# independent transport state (volume/pan/spatialisation/etc).
|
|
244
244
|
def dup
|
|
245
245
|
ptr = C::Audio.sfSound_copy(@handle)
|
|
246
|
-
raise
|
|
246
|
+
raise AudioError, "sfSound_copy returned NULL" if ptr.null?
|
|
247
247
|
|
|
248
248
|
copy = self.class.allocate
|
|
249
249
|
copy.instance_variable_set(:@handle,
|
|
@@ -8,7 +8,7 @@ module SFML
|
|
|
8
8
|
class SoundBuffer
|
|
9
9
|
def self.load(path)
|
|
10
10
|
ptr = C::Audio.sfSoundBuffer_createFromFile(path.to_s)
|
|
11
|
-
raise
|
|
11
|
+
raise LoadError, "Could not load sound buffer from #{path.inspect}" if ptr.null?
|
|
12
12
|
buf = allocate
|
|
13
13
|
buf.send(:_take_ownership, ptr)
|
|
14
14
|
buf
|
|
@@ -22,7 +22,7 @@ module SFML
|
|
|
22
22
|
buf_p = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
|
|
23
23
|
buf_p.write_bytes(bytes)
|
|
24
24
|
ptr = C::Audio.sfSoundBuffer_createFromMemory(buf_p, bytes.bytesize)
|
|
25
|
-
raise
|
|
25
|
+
raise LoadError, "sfSoundBuffer_createFromMemory returned NULL — unsupported format?" if ptr.null?
|
|
26
26
|
|
|
27
27
|
buf = allocate
|
|
28
28
|
buf.send(:_take_ownership, ptr)
|
|
@@ -35,7 +35,7 @@ module SFML
|
|
|
35
35
|
def self.from_stream(io)
|
|
36
36
|
stream = SFML::InputStream.new(io)
|
|
37
37
|
ptr = C::Audio.sfSoundBuffer_createFromStream(stream.to_ptr)
|
|
38
|
-
raise
|
|
38
|
+
raise LoadError, "sfSoundBuffer_createFromStream returned NULL — unsupported format?" if ptr.null?
|
|
39
39
|
|
|
40
40
|
buf = allocate
|
|
41
41
|
buf.send(:_take_ownership, ptr)
|
|
@@ -72,7 +72,7 @@ module SFML
|
|
|
72
72
|
Integer(channel_count),
|
|
73
73
|
Integer(sample_rate),
|
|
74
74
|
map_buf, map.length)
|
|
75
|
-
raise
|
|
75
|
+
raise AudioError, "sfSoundBuffer_createFromSamples returned NULL" if ptr.null?
|
|
76
76
|
|
|
77
77
|
sb = allocate
|
|
78
78
|
sb.send(:_take_ownership, ptr)
|
|
@@ -97,7 +97,7 @@ module SFML
|
|
|
97
97
|
# underlying memory block.
|
|
98
98
|
def dup
|
|
99
99
|
ptr = C::Audio.sfSoundBuffer_copy(@handle)
|
|
100
|
-
raise
|
|
100
|
+
raise AudioError, "sfSoundBuffer_copy returned NULL" if ptr.null?
|
|
101
101
|
|
|
102
102
|
copy = self.class.allocate
|
|
103
103
|
copy.send(:_take_ownership, ptr)
|
|
@@ -110,7 +110,7 @@ module SFML
|
|
|
110
110
|
# with).
|
|
111
111
|
def save(path)
|
|
112
112
|
ok = C::Audio.sfSoundBuffer_saveToFile(@handle, path.to_s)
|
|
113
|
-
raise
|
|
113
|
+
raise AudioError, "could not save SoundBuffer to #{path.inspect}" unless ok
|
|
114
114
|
path
|
|
115
115
|
end
|
|
116
116
|
|
|
@@ -14,7 +14,7 @@ module SFML
|
|
|
14
14
|
class SoundBufferRecorder
|
|
15
15
|
def initialize
|
|
16
16
|
ptr = C::Audio.sfSoundBufferRecorder_create
|
|
17
|
-
raise
|
|
17
|
+
raise AudioError, "sfSoundBufferRecorder_create returned NULL" if ptr.null?
|
|
18
18
|
@handle = FFI::AutoPointer.new(ptr, C::Audio.method(:sfSoundBufferRecorder_destroy))
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -47,7 +47,7 @@ module SFML
|
|
|
47
47
|
# outlives the recorder via the buffer's data.
|
|
48
48
|
def buffer
|
|
49
49
|
ptr = C::Audio.sfSoundBufferRecorder_getBuffer(@handle)
|
|
50
|
-
raise
|
|
50
|
+
raise AudioError, "sfSoundBufferRecorder_getBuffer returned NULL" if ptr.null?
|
|
51
51
|
# Borrowed — recorder owns the underlying sf::SoundBuffer.
|
|
52
52
|
buf = SoundBuffer.allocate
|
|
53
53
|
buf.instance_variable_set(:@handle, ptr)
|
|
@@ -62,7 +62,7 @@ module SFML
|
|
|
62
62
|
|
|
63
63
|
def device=(name)
|
|
64
64
|
ok = C::Audio.sfSoundBufferRecorder_setDevice(@handle, name.to_s)
|
|
65
|
-
raise
|
|
65
|
+
raise AudioError, "could not select recording device #{name.inspect}" unless ok
|
|
66
66
|
name
|
|
67
67
|
end
|
|
68
68
|
|
|
@@ -75,7 +75,7 @@ module SFML
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
ptr = C::Audio.sfSoundRecorder_create(@start_cb, @process_cb, @stop_cb, nil)
|
|
78
|
-
raise
|
|
78
|
+
raise AudioError, "sfSoundRecorder_create returned NULL" if ptr.null?
|
|
79
79
|
|
|
80
80
|
@handle = FFI::AutoPointer.new(ptr, C::Audio.method(:sfSoundRecorder_destroy))
|
|
81
81
|
end
|
|
@@ -104,7 +104,7 @@ module SFML
|
|
|
104
104
|
|
|
105
105
|
def start(sample_rate: 44_100)
|
|
106
106
|
C::Audio.sfSoundRecorder_start(@handle, Integer(sample_rate)) ||
|
|
107
|
-
raise(
|
|
107
|
+
raise(AudioError, "sfSoundRecorder_start failed (no input device or driver error)")
|
|
108
108
|
self
|
|
109
109
|
end
|
|
110
110
|
|
|
@@ -124,7 +124,7 @@ module SFML
|
|
|
124
124
|
|
|
125
125
|
def device=(name)
|
|
126
126
|
C::Audio.sfSoundRecorder_setDevice(@handle, name.to_s) ||
|
|
127
|
-
raise(
|
|
127
|
+
raise(AudioError, "sfSoundRecorder_setDevice failed for #{name.inspect}")
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
# The channel layout the recorder is producing, as an Array of
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module SFML
|
|
2
|
+
# A frame-based animation that drives a Sprite's `texture_rect`
|
|
3
|
+
# over time. Pair it with a `SpriteSheet` or `TextureAtlas`.
|
|
4
|
+
#
|
|
5
|
+
# sheet = SFML::SpriteSheet.load("hero.png", frame_size: [32, 32])
|
|
6
|
+
# walk = sheet.animation(fps: 12, loop: true)
|
|
7
|
+
#
|
|
8
|
+
# def update(dt)
|
|
9
|
+
# walk.update(dt)
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# def draw
|
|
13
|
+
# window.draw(walk.sprite)
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# `Animation` is a self-contained drawable — it builds its own
|
|
17
|
+
# internal `Sprite` and advances `texture_rect` on each `#update`.
|
|
18
|
+
# Use `#sprite` to access the current frame's Sprite for
|
|
19
|
+
# transform setters (`position=`, `rotation=`, etc.), or call
|
|
20
|
+
# `Animation#draw_on(target)` directly.
|
|
21
|
+
#
|
|
22
|
+
# `frames` may be:
|
|
23
|
+
#
|
|
24
|
+
# * An Array of `SFML::Rect` — used as texture_rects in order.
|
|
25
|
+
# * An Array of Integer indexes paired with `sprite_sheet:`.
|
|
26
|
+
# * Constructed implicitly via `SpriteSheet#animation` or
|
|
27
|
+
# `TextureAtlas#animation` (see those classes).
|
|
28
|
+
class Animation
|
|
29
|
+
# @param source [SpriteSheet, TextureAtlas, Texture] backing image
|
|
30
|
+
# @param frames [Array<SFML::Rect>] texture rects to cycle through
|
|
31
|
+
# @param fps [Numeric] frames per second
|
|
32
|
+
# @param loop [Boolean] restart at the end if true; pause if false
|
|
33
|
+
def initialize(source, frames:, fps: 12, loop: true)
|
|
34
|
+
raise ArgumentError, "Animation needs at least one frame" if frames.empty?
|
|
35
|
+
|
|
36
|
+
texture =
|
|
37
|
+
case source
|
|
38
|
+
when Texture then source
|
|
39
|
+
when SpriteSheet,
|
|
40
|
+
TextureAtlas then source.texture
|
|
41
|
+
else raise ArgumentError, "Animation source must be Texture / SpriteSheet / TextureAtlas"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@frames = frames
|
|
45
|
+
@frame_seconds = 1.0 / Float(fps)
|
|
46
|
+
@loop = loop
|
|
47
|
+
@sprite = Sprite.new(texture)
|
|
48
|
+
@sprite.texture_rect = @frames.first
|
|
49
|
+
@elapsed = 0.0
|
|
50
|
+
@frame_index = 0
|
|
51
|
+
@done = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_reader :sprite, :frame_index
|
|
55
|
+
|
|
56
|
+
# Returns whether the animation has reached the end (only
|
|
57
|
+
# meaningful for non-looping animations).
|
|
58
|
+
def done? = @done
|
|
59
|
+
|
|
60
|
+
def playing? = !@done
|
|
61
|
+
|
|
62
|
+
# Advance by `dt` (a `SFML::Time` or seconds Float). Updates
|
|
63
|
+
# the internal sprite's texture_rect to the current frame.
|
|
64
|
+
def update(dt)
|
|
65
|
+
return if @done
|
|
66
|
+
|
|
67
|
+
seconds = dt.is_a?(Time) ? dt.as_seconds : Float(dt)
|
|
68
|
+
@elapsed += seconds
|
|
69
|
+
|
|
70
|
+
while @elapsed >= @frame_seconds
|
|
71
|
+
@elapsed -= @frame_seconds
|
|
72
|
+
@frame_index += 1
|
|
73
|
+
if @frame_index >= @frames.size
|
|
74
|
+
if @loop
|
|
75
|
+
@frame_index = 0
|
|
76
|
+
else
|
|
77
|
+
@frame_index = @frames.size - 1
|
|
78
|
+
@done = true
|
|
79
|
+
break
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@sprite.texture_rect = @frames[@frame_index]
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Rewind to the first frame and clear the done flag.
|
|
89
|
+
def reset
|
|
90
|
+
@frame_index = 0
|
|
91
|
+
@elapsed = 0.0
|
|
92
|
+
@done = false
|
|
93
|
+
@sprite.texture_rect = @frames.first
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Total animation duration in seconds.
|
|
98
|
+
def duration = @frames.size * @frame_seconds
|
|
99
|
+
|
|
100
|
+
# Drawable interface — forwards to the internal Sprite.
|
|
101
|
+
def draw_on(target, states_ptr = nil)
|
|
102
|
+
@sprite.draw_on(target, states_ptr)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Transform passthroughs so callers can write `anim.position =`
|
|
106
|
+
# instead of `anim.sprite.position =`. The full surface of
|
|
107
|
+
# Sprite stays accessible via `#sprite` for advanced cases
|
|
108
|
+
# (color, scale_by, custom blend states, etc.).
|
|
109
|
+
def position = @sprite.position
|
|
110
|
+
def position=(v); @sprite.position = v; end
|
|
111
|
+
def rotation = @sprite.rotation
|
|
112
|
+
def rotation=(v); @sprite.rotation = v; end
|
|
113
|
+
def scale = @sprite.scale
|
|
114
|
+
def scale=(v); @sprite.scale = v; end
|
|
115
|
+
def origin = @sprite.origin
|
|
116
|
+
def origin=(v); @sprite.origin = v; end
|
|
117
|
+
def color = @sprite.color
|
|
118
|
+
def color=(v); @sprite.color = v; end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -15,7 +15,7 @@ module SFML
|
|
|
15
15
|
|
|
16
16
|
def initialize(radius: 10.0, **opts)
|
|
17
17
|
ptr = C::Graphics.sfCircleShape_create
|
|
18
|
-
raise
|
|
18
|
+
raise GraphicsError, "sfCircleShape_create returned NULL" if ptr.null?
|
|
19
19
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfCircleShape_destroy))
|
|
20
20
|
|
|
21
21
|
self.radius = radius
|
|
@@ -18,7 +18,7 @@ module SFML
|
|
|
18
18
|
|
|
19
19
|
def initialize(points: nil, **opts)
|
|
20
20
|
ptr = C::Graphics.sfConvexShape_create
|
|
21
|
-
raise
|
|
21
|
+
raise GraphicsError, "sfConvexShape_create returned NULL" if ptr.null?
|
|
22
22
|
@handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfConvexShape_destroy))
|
|
23
23
|
|
|
24
24
|
self.points = points if points
|
data/lib/sfml/graphics/font.rb
CHANGED
|
@@ -20,7 +20,7 @@ module SFML
|
|
|
20
20
|
|
|
21
21
|
def self.load(path)
|
|
22
22
|
ptr = C::Graphics.sfFont_createFromFile(path.to_s)
|
|
23
|
-
raise
|
|
23
|
+
raise LoadError, "Could not load font from #{path.inspect}" if ptr.null?
|
|
24
24
|
|
|
25
25
|
font = allocate
|
|
26
26
|
font.send(:_take_ownership, ptr)
|
|
@@ -37,7 +37,7 @@ module SFML
|
|
|
37
37
|
buf = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
|
|
38
38
|
buf.write_bytes(bytes)
|
|
39
39
|
ptr = C::Graphics.sfFont_createFromMemory(buf, bytes.bytesize)
|
|
40
|
-
raise
|
|
40
|
+
raise LoadError, "sfFont_createFromMemory returned NULL" if ptr.null?
|
|
41
41
|
|
|
42
42
|
font = allocate
|
|
43
43
|
font.send(:_take_ownership, ptr)
|
|
@@ -53,7 +53,7 @@ module SFML
|
|
|
53
53
|
def self.from_stream(io)
|
|
54
54
|
stream = SFML::InputStream.new(io)
|
|
55
55
|
ptr = C::Graphics.sfFont_createFromStream(stream.to_ptr)
|
|
56
|
-
raise
|
|
56
|
+
raise LoadError, "sfFont_createFromStream returned NULL" if ptr.null?
|
|
57
57
|
|
|
58
58
|
font = allocate
|
|
59
59
|
font.send(:_take_ownership, ptr)
|
|
@@ -147,7 +147,7 @@ module SFML
|
|
|
147
147
|
# one without affecting the other.
|
|
148
148
|
def dup
|
|
149
149
|
ptr = C::Graphics.sfFont_copy(@handle)
|
|
150
|
-
raise
|
|
150
|
+
raise GraphicsError, "sfFont_copy returned NULL" if ptr.null?
|
|
151
151
|
|
|
152
152
|
font = self.class.allocate
|
|
153
153
|
font.send(:_take_ownership, ptr)
|
data/lib/sfml/graphics/image.rb
CHANGED
|
@@ -29,13 +29,13 @@ module SFML
|
|
|
29
29
|
else
|
|
30
30
|
C::Graphics.sfImage_create(size)
|
|
31
31
|
end
|
|
32
|
-
raise
|
|
32
|
+
raise GraphicsError, "sfImage_create returned NULL" if ptr.null?
|
|
33
33
|
_take_ownership(ptr)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def self.load(path)
|
|
37
37
|
ptr = C::Graphics.sfImage_createFromFile(path.to_s)
|
|
38
|
-
raise
|
|
38
|
+
raise LoadError, "Could not load image from #{path.inspect}" if ptr.null?
|
|
39
39
|
img = allocate
|
|
40
40
|
img.send(:_take_ownership, ptr)
|
|
41
41
|
img
|
|
@@ -51,7 +51,7 @@ module SFML
|
|
|
51
51
|
buf = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
|
|
52
52
|
buf.write_bytes(bytes)
|
|
53
53
|
ptr = C::Graphics.sfImage_createFromMemory(buf, bytes.bytesize)
|
|
54
|
-
raise
|
|
54
|
+
raise LoadError, "sfImage_createFromMemory returned NULL — unsupported format?" if ptr.null?
|
|
55
55
|
|
|
56
56
|
img = allocate
|
|
57
57
|
img.send(:_take_ownership, ptr)
|
|
@@ -63,7 +63,7 @@ module SFML
|
|
|
63
63
|
def self.from_stream(io)
|
|
64
64
|
stream = SFML::InputStream.new(io)
|
|
65
65
|
ptr = C::Graphics.sfImage_createFromStream(stream.to_ptr)
|
|
66
|
-
raise
|
|
66
|
+
raise LoadError, "sfImage_createFromStream returned NULL — unsupported format?" if ptr.null?
|
|
67
67
|
|
|
68
68
|
img = allocate
|
|
69
69
|
img.send(:_take_ownership, ptr)
|
|
@@ -83,7 +83,7 @@ module SFML
|
|
|
83
83
|
size[:x] = Integer(width)
|
|
84
84
|
size[:y] = Integer(height)
|
|
85
85
|
ptr = C::Graphics.sfImage_createFromPixels(size, buf)
|
|
86
|
-
raise
|
|
86
|
+
raise LoadError, "sfImage_createFromPixels returned NULL" if ptr.null?
|
|
87
87
|
|
|
88
88
|
img = allocate
|
|
89
89
|
img.send(:_take_ownership, ptr)
|
|
@@ -151,7 +151,7 @@ module SFML
|
|
|
151
151
|
|
|
152
152
|
def save(path)
|
|
153
153
|
ok = C::Graphics.sfImage_saveToFile(@handle, path.to_s)
|
|
154
|
-
raise
|
|
154
|
+
raise LoadError, "Could not save image to #{path.inspect}" unless ok
|
|
155
155
|
path
|
|
156
156
|
end
|
|
157
157
|
|
|
@@ -164,11 +164,11 @@ module SFML
|
|
|
164
164
|
# File.binwrite("out.png", png_bytes)
|
|
165
165
|
def save_to_memory(format)
|
|
166
166
|
buffer = C::System.sfBuffer_create
|
|
167
|
-
raise
|
|
167
|
+
raise GraphicsError, "sfBuffer_create returned NULL" if buffer.null?
|
|
168
168
|
|
|
169
169
|
begin
|
|
170
170
|
ok = C::Graphics.sfImage_saveToMemory(@handle, buffer, format.to_s)
|
|
171
|
-
raise
|
|
171
|
+
raise LoadError, "Could not encode image as #{format.inspect}" unless ok
|
|
172
172
|
|
|
173
173
|
size = C::System.sfBuffer_getSize(buffer)
|
|
174
174
|
data = C::System.sfBuffer_getData(buffer)
|