osb 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dae3bb6b24e591176075dcfaeb67dc5e74c625c668e44dd91e8f35b660973222
4
+ data.tar.gz: 30a7304fe4b0c47a81d9a55ea0cc91d5e60775ab06f01aa85e4d76c46b241c0c
5
+ SHA512:
6
+ metadata.gz: 7fd1cc3edbf6bc0995eb04951ac640ca7d583130e36e46b8b9aa801cbbdbd040cbcd41f1b1ea2eff477361f9af39f7775487b80b65332737df0a7055830c877e
7
+ data.tar.gz: 3d4a452f5fdc3002e544d5ffc32542fa850f792dc01e19da19a32ae506da120fccfdf49d68c2c6d25534320d9406bdf575a67d63c1b1904a98878f26be9c1424
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ # A moving image.
5
+ class Animation
6
+ # @api private
7
+ attr_reader :commands, :layer
8
+ include Commandable
9
+
10
+ # @param [String] layer the layer the object appears on.
11
+ # @param [String] origin where on the image should osu! consider that image's origin (coordinate) to be.
12
+ # @param [String] file_path filename of the image.
13
+ # @param [Vector2, nil] initial_position where the object should be by default.
14
+ # @param [Integer] frame_count how many frames the animation has.
15
+ # @param [Integer] frame_delay how many milliseconds should be in between each frame.
16
+ # @param [Boolean] repeat if the animation should loop or not.
17
+ def initialize(
18
+ layer: Osb::Layer::Background,
19
+ origin: Osb::Origin::Center,
20
+ file_path:,
21
+ initial_position: nil,
22
+ frame_count:,
23
+ frame_delay:,
24
+ repeat: false
25
+ )
26
+ Internal.assert_type!(layer, String, "layer")
27
+ Internal.assert_value!(layer, Osb::Layer::ALL, "layer")
28
+
29
+ Internal.assert_type!(origin, String, "origin")
30
+ Internal.assert_value!(origin, Osb::Origin::ALL, "origin")
31
+
32
+ Internal.assert_type!(file_path, String, "file_path")
33
+ Internal.assert_file_name_ext!(file_path, %w[png jpg jpeg])
34
+ if initial_position
35
+ Internal.assert_type!(initial_position, Vector2, "initial_position")
36
+ end
37
+
38
+ Internal.assert_type!(frame_count, Integer, "frame_count")
39
+ Internal.assert_type!(frame_delay, Integer, "frame_delay")
40
+ Internal.assert_type!(repeat, Internal::Boolean, "repeat")
41
+
42
+ @layer = layer
43
+
44
+ first_command = "Animation,#{layer},#{origin},\"#{file_path}\""
45
+ if initial_position
46
+ first_command += ",#{initial_position.x},#{initial_position.y}"
47
+ else
48
+ first_command += ",,"
49
+ end
50
+ first_command += ",#{frame_count}"
51
+ first_command += ",#{frame_delay}"
52
+ looptype = repeat ? "LoopForever" : "LoopOnce"
53
+ first_command += ",#{type}" if repeat
54
+ # @type [Array<String>]
55
+ @commands = [first_command]
56
+ end
57
+ end
58
+ end
data/lib/osb/assert.rb ADDED
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ # @api private
5
+ class TypeError < StandardError
6
+ end
7
+
8
+ # @api private
9
+ class InvalidValueError < StandardError
10
+ end
11
+
12
+ # @api private
13
+ module Internal
14
+ # @api private
15
+ Boolean = [TrueClass, FalseClass]
16
+
17
+ # @api private
18
+ class TypedArray
19
+ # @param [Class] type
20
+ def initialize(type)
21
+ @type = type
22
+ end
23
+
24
+ def name
25
+ "Array<#{@type.name}>"
26
+ end
27
+
28
+ def valid?(object)
29
+ object.is_a?(Array) && object.all? { |value| value.is_a?(@type) }
30
+ end
31
+ end
32
+
33
+ # @api private
34
+ # @type [Hash{Class => Hash{Class => Object}}]
35
+ T = { Array => { Numeric => TypedArray.new(Numeric) } }
36
+
37
+ # Check if supplied argument is correctly typed.
38
+ # @param [Object] arg
39
+ # @param [BasicObject, Array, TypedArray] possible_types
40
+ # @param [String] param_name
41
+ # @return [void]
42
+ # @api private
43
+ def self.assert_type!(arg, possible_types, param_name)
44
+ if possible_types.is_a?(Array)
45
+ valid =
46
+ possible_types.any? do |type|
47
+ type.is_a?(TypedArray) ? type.valid?(arg) : arg.is_a?(type)
48
+ end
49
+ unless valid
50
+ accepted_types = possible_types.map { |type| type.name }.join(" or ")
51
+
52
+ raise TypeError,
53
+ "Parameter #{param_name} expects type #{accepted_types}, " +
54
+ "got type #{arg.class.name} instead."
55
+ end
56
+ elsif possible_types.is_a?(TypedArray)
57
+ valid = possible_types.valid?(arg)
58
+ unless valid
59
+ raise TypeError,
60
+ "Parameter #{param_name} expects type Array<#{possible_types.type.name}>, " +
61
+ "got type #{arg.class.name} instead."
62
+ end
63
+ else
64
+ type = possible_types
65
+ unless arg.is_a?(type)
66
+ raise TypeError,
67
+ "Parameter #{param_name} expects type #{type.name}, " +
68
+ "got type #{arg.class.name} instead."
69
+ end
70
+ end
71
+ end
72
+
73
+ # Ensures the supplied argument is correct.
74
+ # @param [Object] arg
75
+ # @param [BasicObject, Array, Range] possible_values
76
+ # @param [String] param_name
77
+ # @return [void]
78
+ # @api private
79
+ def self.assert_value!(arg, possible_values, param_name)
80
+ val =
81
+ if arg.is_a?(String) && arg.empty?
82
+ "an empty string"
83
+ else
84
+ arg
85
+ end
86
+
87
+ if possible_values.is_a?(Array)
88
+ valid = possible_values.any? { |value| arg == value }
89
+ unless valid
90
+ accepted_values = possible_values.join(" or ")
91
+
92
+ raise InvalidValueError,
93
+ "Parameter #{param_name} expects #{accepted_values}, " +
94
+ "got #{val} instead."
95
+ end
96
+ elsif possible_values.is_a?(Range)
97
+ valid = arg >= possible_values.min && arg <= possible_values.max
98
+ unless valid
99
+ raise InvalidValueError,
100
+ "Parameter #{param_name} expects value within " +
101
+ "#{possible_values.min} to #{possible_values.max}, " +
102
+ "got #{val} instead."
103
+ end
104
+ else
105
+ unless arg == possible_values
106
+ raise InvalidValueError,
107
+ "Parameter #{param_name} expects #{possible_values}, " +
108
+ "got #{val} instead."
109
+ end
110
+ end
111
+ end
112
+
113
+ # Ensure the file name extenstion is correct.
114
+ # @param [String] file_name
115
+ # @param [String, Array<String>] exts
116
+ # @api private
117
+ def self.assert_file_name_ext!(file_name, exts)
118
+ if exts.is_a?(Array)
119
+ exts_ = exts.join("|")
120
+ exts__ = exts.join(" or ")
121
+ unless /[\w\s\d]+\.(#{exts_})/.match(file_name)
122
+ raise InvalidValueError, "File name must end with #{exts__}"
123
+ end
124
+ else
125
+ unless /[\w\s\d]+\.#{exts}/.match(file_name)
126
+ raise InvalidValueError, "File name must end with #{exts}"
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,14 @@
1
+ module Osb
2
+ # Beatmap's background.
3
+ class Background
4
+ # @api private
5
+ attr_reader :command
6
+
7
+ # @param [String] file_name location of the background image relative to the beatmap directory.
8
+ def initialize(file_path:)
9
+ Internal.assert_type!(file_path, String, "file_path")
10
+ Internal.assert_file_name_ext!(file_path, %w[png jpg jpeg])
11
+ @command = "0,0,\"#{file_path}\""
12
+ end
13
+ end
14
+ end
data/lib/osb/color.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ # Represents an RGB color.
5
+ class Color
6
+ attr_accessor :r, :g, :b
7
+ # @attribute [rw] r
8
+ # @return Red value.
9
+ # @attribute [rw] g
10
+ # @return Green value.
11
+ # @attribute [rw] b
12
+ # @return Blue value.
13
+
14
+ # @param [Integer] r red value
15
+ # @param [Integer] g green value
16
+ # @param [Integer] b blue value
17
+ def initialize(r, g, b)
18
+ Internal.assert_type!(r, Integer, "r")
19
+ Internal.assert_value!(r, 0..255, "r")
20
+
21
+ Internal.assert_type!(g, Integer, "g")
22
+ Internal.assert_value!(g, 0..255, "g")
23
+
24
+ Internal.assert_type!(b, Integer, "b")
25
+ Internal.assert_value!(b, 0..255, "b")
26
+
27
+ @r = r
28
+ @g = g
29
+ @b = b
30
+ end
31
+
32
+ # Returns whether 2 colors are not equal.
33
+ # @param [Color] color
34
+ def !=(color)
35
+ Internal.assert_type!(color, Color, "color")
36
+
37
+ color.r != self.r && color.g != self.g && color.b != self.b
38
+ end
39
+
40
+ # Converts an HSL color value to RGB.
41
+ # @param [Integer] h
42
+ # @param [Integer] s
43
+ # @param [Integer] l
44
+ # @return [Color]
45
+ def self.from_hsl(h, s, l)
46
+ Internal.assert_type!(h, Integer, "h")
47
+ Internal.assert_type!(s, Integer, "s")
48
+ Internal.assert_type!(l, Integer, "l")
49
+
50
+ h = h / 360.0
51
+ s = s / 100.0
52
+ l = l / 100.0
53
+
54
+ r = 0.0
55
+ g = 0.0
56
+ b = 0.0
57
+
58
+ if s == 0.0
59
+ r = l.to_f
60
+ g = l.to_f
61
+ b = l.to_f
62
+ else
63
+ q = l < 0.5 ? l * (1 + s) : l + s - l * s
64
+ p = 2 * l - q
65
+ r = hue_to_rgb(p, q, h + 1 / 3.0)
66
+ g = hue_to_rgb(p, q, h)
67
+ b = hue_to_rgb(p, q, h - 1 / 3.0)
68
+ end
69
+
70
+ Color.new((r * 255).round, (g * 255).round, (b * 255).round)
71
+ end
72
+
73
+ # @api private
74
+ # @param [Float] p
75
+ # @param [Float] q
76
+ # @param [Float] t_
77
+ # @return [Float]
78
+ def self.hue_to_rgb(p, q, t_)
79
+ t = t_ + 1 if t_ < 0
80
+ t = t_ - 1 if t_ > 1
81
+ return(p + (q - p) * 6 * t) if t < 1 / 6.0
82
+ return q if t < 1 / 2.0
83
+ return(p + (q - p) * (2 / 3.0 - t) * 6) if t < 2 / 3.0
84
+ return p
85
+ end
86
+
87
+ # Create a Color object from hex string.
88
+ # @param [String] hex
89
+ # @return [Color]
90
+ def self.from_hex(hex)
91
+ Internal.assert_type!(hex, String, "hex")
92
+
93
+ hex.gsub!("#", "")
94
+ components = hex.scan(/.{2}/)
95
+ components.collect { |component| component.to_i(16) }
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,373 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ module Internal
5
+ # @param [Integer] time
6
+ # @return [void]
7
+ # @api private
8
+ def self.raise_if_invalid_start_time!(time)
9
+ Internal.assert_type!(time, Integer, "start_time")
10
+ end
11
+
12
+ # @param [Integer] time
13
+ # @return [void]
14
+ # @api private
15
+ def self.raise_if_invalid_end_time!(time)
16
+ Internal.assert_type!(time, Integer, "end_time")
17
+ end
18
+
19
+ # @param [Integer] easing
20
+ # @return [void]
21
+ # @api private
22
+ def self.raise_if_invalid_easing!(easing)
23
+ Internal.assert_type!(easing, Integer, "easing")
24
+ Internal.assert_value!(easing, Easing::ALL, "easing")
25
+ end
26
+ end
27
+
28
+ module Commandable
29
+ private def tab_level
30
+ @is_in_trigger ? 2 : 1
31
+ end
32
+
33
+ private def raise_if_trigger_called!
34
+ if @trigger_called
35
+ raise RuntimeError, "Do not add commands after #trigger is called."
36
+ end
37
+ end
38
+
39
+ # Change the opacity of the object (how transparent it is).
40
+ # @param [Integer] start_time
41
+ # @param [Integer] end_time
42
+ # @param [Integer] easing
43
+ # @param [Numeric] start_opacity
44
+ # @param [Numeric] end_opacity
45
+ def fade(
46
+ start_time:,
47
+ end_time: start_time,
48
+ easing: Easing::Linear,
49
+ start_opacity:,
50
+ end_opacity: start_opacity
51
+ )
52
+ self.raise_if_trigger_called!
53
+ Internal.raise_if_invalid_start_time!(start_time)
54
+ Internal.raise_if_invalid_end_time!(end_time)
55
+ Internal.raise_if_invalid_easing!(easing)
56
+ Internal.assert_type!(start_opacity, Numeric, "start_opacity")
57
+ Internal.assert_type!(end_opacity, Numeric, "end_opacity")
58
+ Internal.assert_value!(start_opacity, 0..1, "start_opacity")
59
+ Internal.assert_value!(end_opacity, 0..1, "end_opacity")
60
+
61
+ end_time = "" if start_time == end_time
62
+ tabs = " " * self.tab_level
63
+ command = "#{tabs}F,#{start_time},#{end_time},#{start_opacity}"
64
+ command += ",#{end_opacity}" if end_opacity != start_opacity
65
+ @commands << command
66
+ end
67
+
68
+ # Move the object to a new position in the play area.
69
+ # @param [Integer] start_time
70
+ # @param [Integer] end_time
71
+ # @param [Integer] easing
72
+ # @param [Osb::Vector2, Array<Numeric>] start_position
73
+ # @param [Osb::Vector2, Array<Numeric>] end_position
74
+ def move(
75
+ start_time:,
76
+ end_time: start_time,
77
+ easing: Easing::Linear,
78
+ start_position:,
79
+ end_position: start_position
80
+ )
81
+ self.raise_if_trigger_called!
82
+ Internal.raise_if_invalid_start_time!(start_time)
83
+ Internal.raise_if_invalid_end_time!(end_time)
84
+ Internal.raise_if_invalid_easing!(easing)
85
+ Internal.assert_type!(
86
+ start_position,
87
+ [Osb::Vector2, T[Array][Numeric]],
88
+ "start_position"
89
+ )
90
+ Internal.assert_type!(
91
+ end_position,
92
+ [Osb::Vector2, T[Array][Numeric]],
93
+ "end_position"
94
+ )
95
+ if start_position.is_a?(Array)
96
+ start_position = Osb::Vector2.new(start_position)
97
+ end
98
+ end_position = Osb::Vector2.new(end_position) if end_position.is_a?(Array)
99
+ end_time = "" if start_time == end_time
100
+ tabs = " " * self.tab_level
101
+ command =
102
+ "#{tabs}M,#{start_time},#{end_time},#{start_position.x},#{start_position.y}"
103
+ command += ",#{end_position.x},#{end_position.y}" if end_position !=
104
+ start_position
105
+ @commands << command
106
+ end
107
+
108
+ # Move the object along the x axis.
109
+ # @param [Integer] start_time
110
+ # @param [Integer] end_time
111
+ # @param [Integer] easing
112
+ # @param [Numeric] start_x
113
+ # @param [Numeric] end_x
114
+ def move_x(
115
+ start_time:,
116
+ end_time: start_time,
117
+ easing: Easing::Linear,
118
+ start_x:,
119
+ end_x: start_x
120
+ )
121
+ self.raise_if_trigger_called!
122
+ Internal.raise_if_invalid_start_time!(start_time)
123
+ Internal.raise_if_invalid_end_time!(end_time)
124
+ Internal.raise_if_invalid_easing!(easing)
125
+ Internal.assert_type!(start_x, Numeric, "start_x")
126
+ Internal.assert_type!(end_x, Numeric, "end_x")
127
+
128
+ end_time = "" if start_time == end_time
129
+ tabs = " " * self.tab_level
130
+ command = "#{tabs}MX,#{start_time},#{end_time},#{start_x}"
131
+ command += ",#{end_x}" if end_x
132
+ @commands << command
133
+ end
134
+
135
+ # Move the object along the y axis.
136
+ # @param [Integer] start_time
137
+ # @param [Integer] end_time
138
+ # @param [Integer] easing
139
+ # @param [Numeric] start_y
140
+ # @param [Numeric] end_y
141
+ def move_y(
142
+ start_time:,
143
+ end_time: start_time,
144
+ easing: Easing::Linear,
145
+ start_y:,
146
+ end_y: start_y
147
+ )
148
+ self.raise_if_trigger_called!
149
+ Internal.raise_if_invalid_start_time!(start_time)
150
+ Internal.raise_if_invalid_end_time!(end_time) if end_time
151
+ Internal.raise_if_invalid_easing!(easing)
152
+ Internal.assert_type!(start_y, Numeric, "start_y")
153
+ Internal.assert_type!(end_y, Numeric, "end_y")
154
+
155
+ end_time = "" if start_time == end_time
156
+ tabs = " " * self.tab_level
157
+ command = "#{tabs}MY,#{start_time},#{end_time},#{start_y}"
158
+ command += ",#{end_y}" if end_y != start_y
159
+ @commands << command
160
+ end
161
+
162
+ # Change the size of the object relative to its original size. Will scale
163
+ # seperatedly if given +Osb::Vector2+s or +Array<Numeric>+s. The scaling is
164
+ # affected by the object's origin
165
+ # @param [Integer] start_time
166
+ # @param [Integer] end_time
167
+ # @param [Integer] easing
168
+ # @param [Numeric, Osb::Vector2, Array<Numeric>] start_scale
169
+ # @param [Numeric, Osb::Vector2, Array<Numeric>] end_scale
170
+ def scale(
171
+ start_time:,
172
+ end_time: start_time,
173
+ easing: Easing::Linear,
174
+ start_scale:,
175
+ end_scale: start_scale
176
+ )
177
+ self.raise_if_trigger_called!
178
+ Internal.raise_if_invalid_start_time!(start_time)
179
+ Internal.raise_if_invalid_end_time!(end_time) if end_time
180
+ Internal.raise_if_invalid_easing!(easing)
181
+ Internal.assert_type!(
182
+ start_scale,
183
+ [Numeric, T[Array][Numeric], Osb::Vector2],
184
+ "start_scale"
185
+ )
186
+ Internal.assert_type!(
187
+ end_scale,
188
+ [Numeric, T[Array][Numeric], Osb::Vector2],
189
+ "end_scale"
190
+ )
191
+
192
+ end_time = "" if start_time == end_time
193
+ tabs = " " * self.tab_level
194
+
195
+ if start_scale.is_a?(Numeric)
196
+ unless end_scale.is_a?(Numeric)
197
+ raise InvalidValueError,
198
+ "start_scale and end_scale must be either both Numeric values or Vector2-like values."
199
+ end
200
+ command = "#{tabs}S,#{start_time},#{end_time},#{start_scale}"
201
+ command += ",#{end_scale}" if end_scale != start_scale
202
+ @commands << command
203
+ else
204
+ if end_scale.is_a?(Numeric)
205
+ raise InvalidValueError,
206
+ "start_scale and end_scale must be either both Numeric values or Vector2-like values."
207
+ end
208
+
209
+ start_scale = Osb::Vector2.new(start_scale) if start_scale.is_a?(Array)
210
+
211
+ end_scale = Osb::Vector2.new(end_scale) if end_scale.is_a?(Array)
212
+
213
+ command =
214
+ "#{tabs}V,#{start_time},#{end_time},#{start_scale.x},#{start_scale.y}"
215
+ command += ",#{end_scale.x},#{end_scale.y}" if end_scale
216
+ @commands << command
217
+ end
218
+ end
219
+
220
+ # Rotate the object around its origin.
221
+ # @param [Integer] start_time
222
+ # @param [Integer] end_time
223
+ # @param [Integer] easing
224
+ # @param [Float] start_angle start angle in radians.
225
+ # @param [Float] end_angle end angle in radians.
226
+ def rotate(
227
+ start_time:,
228
+ end_time: start_time,
229
+ easing: Easing::Linear,
230
+ start_angle:,
231
+ end_angle: start_angle
232
+ )
233
+ self.raise_if_trigger_called!
234
+ Internal.raise_if_invalid_start_time!(start_time)
235
+ Internal.raise_if_invalid_end_time!(end_time)
236
+ Internal.raise_if_invalid_easing!(easing)
237
+ Internal.assert_type!(start_angle, Numeric, "start_angle")
238
+ Internal.assert_type!(end_angle, Numeric, "end_angle")
239
+
240
+ end_time = "" if start_time == end_time
241
+ tabs = " " * self.tab_level
242
+ command = "#{tabs}R,#{start_time},#{end_time},#{start_angle}"
243
+ command += ",#{end_angle}" if end_angle != start_angle
244
+ @commands << command
245
+ end
246
+
247
+ # The virtual light source colour on the object. The colours of the pixels on the object are determined subtractively.
248
+ # @param [Integer] start_time
249
+ # @param [Integer] end_time
250
+ # @param [Integer] easing
251
+ # @param [Osb::Color] start_color
252
+ # @param [Osb::Color] end_color
253
+ def color(
254
+ start_time:,
255
+ end_time: start_time,
256
+ easing: Easing::Linear,
257
+ start_color:,
258
+ end_color: start_color
259
+ )
260
+ self.raise_if_trigger_called!
261
+ Internal.raise_if_invalid_start_time!(start_time)
262
+ Internal.raise_if_invalid_end_time!(end_time)
263
+ Internal.raise_if_invalid_easing!(easing)
264
+ Internal.assert_type!(start_color, Osb::Color, "start_color")
265
+ Internal.assert_type!(end_color, Osb::Color, "end_color")
266
+
267
+ end_time = "" if start_time == end_time
268
+ tabs = " " * self.tab_level
269
+ command =
270
+ "#{tabs}C,#{start_time},#{end_time},#{start_color.r},#{start_color.g},#{start_color.b}"
271
+ if end_color != start_color
272
+ command += ",#{end_color.r},#{end_color.g},#{end_color.b}"
273
+ end
274
+ @commands << command
275
+ end
276
+
277
+ # Flip the object horizontally or vertically.
278
+ # @param [Integer] start_time
279
+ # @param [Integer] end_time
280
+ # @param [Boolean] horizontally
281
+ # @param [Boolean] vertically
282
+ def flip(start_time:, end_time:, horizontally: true, vertically: false)
283
+ self.raise_if_trigger_called!
284
+ Internal.raise_if_invalid_start_time!(start_time)
285
+ Internal.raise_if_invalid_end_time!(end_time)
286
+ Internal.assert_type!(horizontally, Internal::Boolean, "horizontally")
287
+ Internal.assert_type!(vertically, Internal::Boolean, "vertically")
288
+
289
+ if horizontally && vertically
290
+ raise InvalidValueError,
291
+ "Cannot flip an object both horizontally and vertically."
292
+ end
293
+ if !horizontally && !vertically
294
+ raise InvalidValueError, "Specify a direction to flip."
295
+ end
296
+
297
+ direction = horizontally ? "H" : "V"
298
+ end_time = "" if start_time == end_time
299
+ tabs = " " * self.tab_level
300
+ command = "#{tabs}P,#{start_time},#{end_time},#{direction}"
301
+ @commands << command
302
+ end
303
+
304
+ # Use additive-color blending instead of alpha-blending.
305
+ # @param [Integer] start_time
306
+ # @param [Integer] end_time
307
+ def additive_color_blending(start_time:, end_time:)
308
+ self.raise_if_trigger_called!
309
+ Internal.raise_if_invalid_start_time!(start_time)
310
+ Internal.raise_if_invalid_end_time!(end_time)
311
+
312
+ tabs = " " * self.tab_level
313
+ command = "#{tabs}P,#{start_time},#{end_time},A"
314
+ @commands << command
315
+ end
316
+
317
+ # Add a group of commands on a specific condition.
318
+ # `#trigger` can only be called on an empty object declaration (no commands).
319
+ # Pass a block to this method call to specify which commands to run if
320
+ # the condition is met.
321
+ #
322
+ # @example
323
+ # img.trigger(on: "Passing", start_time: 0, end_time: 1000) do
324
+ # img.fade(start_time: 0, start_opacity: 0.5)
325
+ # end
326
+ #
327
+ # In addition to the "implicit" player feedback via the separate
328
+ # Pass/Fail layers, you can use one of several Trigger conditions
329
+ # to cause a series of events to happen whenever that condition is
330
+ # fulfilled within a certain time period.
331
+ # Note that `start_time` and `end_time` of any commands called inside
332
+ # the block become relative to the `start_time` and `end_time` of the
333
+ # `#trigger` command.
334
+ #
335
+ # While osu! supports trigger on hitsounds playing, we decided to not
336
+ # include it in because it is unreliable/unpredictable.
337
+ #
338
+ # @param [String] on indicates the trigger condition. It can be "Failing" or "Passing".
339
+ # @param [Integer] start_time the timestamp at which the trigger becomes valid.
340
+ # @param [Integer] end_time the timestamp at which the trigger stops being valid.
341
+ def trigger(on:, start_time:, end_time:)
342
+ self.raise_if_trigger_called!
343
+ Internal.raise_if_invalid_start_time!(start_time)
344
+ Internal.raise_if_invalid_end_time!(end_time)
345
+ Internal.assert_type!(on, String, "on")
346
+ Internal.assert_value!(on, %w[Passing Failing], "on")
347
+
348
+ unless block_given?
349
+ raise InvalidValueError, "Do not use an empty trigger."
350
+ end
351
+
352
+ if @commands.size > 1
353
+ raise RuntimeError, "Do not call #trigger after any other commands."
354
+ end
355
+
356
+ if @is_in_trigger
357
+ raise RuntimeError,
358
+ "Do not call #trigger inside another #trigger block."
359
+ end
360
+
361
+ command = " T,#{on},#{start_time},#{end_time}"
362
+ @commands << command
363
+
364
+ @is_in_trigger = true
365
+ yield self
366
+ unless @commands.size > 1
367
+ raise InvalidValueError, "Do not use an empty trigger."
368
+ end
369
+ @is_in_trigger = false
370
+ @trigger_called = true
371
+ end
372
+ end
373
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ module Easing
5
+ Linear = 0
6
+ Out = 1
7
+ In = 2
8
+ InQuad = 3
9
+ OutQuad = 4
10
+ InOutQuad = 5
11
+ InCubic = 6
12
+ OutCubic = 7
13
+ InOutCubic = 8
14
+ InQuart = 9
15
+ OutQuart = 10
16
+ InOutQuart = 11
17
+ InQuint = 12
18
+ OutQuint = 13
19
+ InOutQuint = 14
20
+ InSine = 15
21
+ OutSine = 16
22
+ InOutSine = 17
23
+ InExpo = 18
24
+ OutExpo = 19
25
+ InOutExpo = 20
26
+ InCirc = 21
27
+ OutCirc = 22
28
+ InOutCirc = 23
29
+ InElastic = 24
30
+ OutElastic = 25
31
+ OutElasticHalf = 26
32
+ OutElasticQuarter = 27
33
+ InOutElastic = 28
34
+ InBack = 29
35
+ OutBack = 30
36
+ InOutBack = 31
37
+ InBounce = 32
38
+ OutBounce = 33
39
+ InOutBounce = 34
40
+ ALL = 0..34
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ module Layer
5
+ Background = "Background"
6
+ Fail = "Fail"
7
+ Pass = "Pass"
8
+ Foreground = "Foreground"
9
+ Overlay = "Overlay"
10
+ ALL = %w[Background Fail Pass Foreground Overlay]
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ module Origin
5
+ TopLeft = "TopLeft"
6
+ TopCentre = "TopCentre"
7
+ TopCenter = "TopCentre"
8
+ TopRight = "TopRight"
9
+ CentreLeft = "CentreLeft"
10
+ CenterLeft = "CentreLeft"
11
+ Centre = "Centre"
12
+ Center = "Centre"
13
+ CentreRight = "CentreRight"
14
+ CenterRight = "CentreRight"
15
+ BottomLeft = "BottomLeft"
16
+ BottomCentre = "BottomCentre"
17
+ BottomCenter = "BottomCentre"
18
+ BottomRight = "BottomRight"
19
+ ALL = %w[
20
+ TopLeft
21
+ TopCentre
22
+ TopRight
23
+ CentreLeft
24
+ Centre
25
+ CentreRight
26
+ BottomLeft
27
+ BottomCentre
28
+ BottomRight
29
+ ]
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ class Integer
2
+ # Does nothing. Just a way to tell people it's represented in milliseconds.
3
+ # @return [Integer]
4
+ def ms
5
+ self
6
+ end
7
+
8
+ # Convert from seconds to milliseconds.
9
+ # @return [Integer]
10
+ def second
11
+ self * 1000
12
+ end
13
+
14
+ # Convert from percentage to float.
15
+ # @return [Float]
16
+ def percent
17
+ self / 100.0
18
+ end
19
+ end
data/lib/osb/math.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Math
4
+ # Returns whether 2 real numbers are equal within tolerance.
5
+ # @param [Numeric] x
6
+ # @param [Numeric] y
7
+ # @param [Numeric] tolerance
8
+ # @return [Boolean]
9
+ def self.fuzzy_equal(x, y, tolerance = 1e-8)
10
+ (x - y).abs < tolerance
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ class Numeric
2
+ # The degrees method is used to convert from degrees to radians.
3
+ # @return [Float]
4
+ def degrees
5
+ self * Math::PI / 180
6
+ end
7
+ end
data/lib/osb/sample.rb ADDED
@@ -0,0 +1,34 @@
1
+ module Osb
2
+ class Sample
3
+ # @api private
4
+ attr_reader :command
5
+
6
+ # @param [Integer] time the timestamp that the sound should start playing.
7
+ # @param [String] layer the layer you want the sound to be on.
8
+ # @param [String] file_path filename of the audio.
9
+ # @param [Integer] volume indicate the relative loudness of the sound.
10
+ def initialize(time:, layer:, file_path:, volume: 100)
11
+ Internal.assert_type!(layer, String, "layer")
12
+ Internal.assert_value!(layer, Layer::ALL, "layer")
13
+ Internal.assert_type!(file_path, String, "file_path")
14
+
15
+ layer_ =
16
+ case layer
17
+ when Osb::Layer::Background
18
+ 0
19
+ when Osb::Layer::Foreground
20
+ 1
21
+ when Osb::Layer::Fail
22
+ 2
23
+ when Osb::Layer::Pass
24
+ 3
25
+ else
26
+ raise InvalidValueError,
27
+ "An audio sample can only exists in one of these layers: " +
28
+ "Background, Foreground, Fail or Pass."
29
+ end
30
+
31
+ @command = "Sample,#{time},#{layer_},\"#{file_path}}\",#{volume}"
32
+ end
33
+ end
34
+ end
data/lib/osb/sprite.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ # A still image.
5
+ class Sprite
6
+ # @api private
7
+ attr_reader :commands, :layer
8
+ include Commandable
9
+
10
+ # @param [String] layer the layer the object appears on.
11
+ # @param [String] origin where on the image should osu! consider that image's origin (coordinate) to be.
12
+ # @param [String] file_path filename of the image.
13
+ # @param [Osb::Vector2, nil] initial_position where the object should be by default.
14
+ def initialize(
15
+ layer: Layer::Background,
16
+ origin: Origin::Center,
17
+ file_path:,
18
+ initial_position: nil
19
+ )
20
+ Internal.assert_type!(layer, String, "layer")
21
+ Internal.assert_value!(layer, Layer::ALL, "layer")
22
+ Internal.assert_type!(origin, String, "origin")
23
+ Internal.assert_value!(origin, Origin::ALL, "origin")
24
+ Internal.assert_type!(file_path, String, "file_path")
25
+ Internal.assert_file_name_ext!(file_path, %w[png jpg jpeg])
26
+ if initial_position
27
+ Internal.assert_type!(
28
+ initial_position,
29
+ Osb::Vector2,
30
+ "initial_position"
31
+ )
32
+ end
33
+
34
+ @layer = layer
35
+
36
+ first_command = "Sprite,#{layer},#{origin},\"#{file_path}\""
37
+ if initial_position
38
+ first_command += ",#{initial_position.x},#{initial_position.y}"
39
+ end
40
+ # @type [Array<String>]
41
+ @commands = [first_command]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ # @api private
5
+ module Internal
6
+ # @api private
7
+ class LayerManager
8
+ attr_reader :background,
9
+ :foreground,
10
+ :fail,
11
+ :pass,
12
+ :overlay,
13
+ :samples,
14
+ :bg_and_video
15
+
16
+ def initialize
17
+ # @type [Array<Osb::Sprite, Osb::Animation>]
18
+ @background = []
19
+ # @type [Array<Osb::Sprite, Osb::Animation>]
20
+ @foreground = []
21
+ # @type [Array<Osb::Sprite, Osb::Animation>]
22
+ @fail = []
23
+ # @type [Array<Osb::Sprite, Osb::Animation>]
24
+ @pass = []
25
+ # @type [Array<Osb::Sprite, Osb::Animation>]
26
+ @overlay = []
27
+ # @type [Array<Osb::Background, Osb::Video>]
28
+ @bg_and_video = []
29
+ # @type [Array<Osb::Sample>]
30
+ @samples = []
31
+ end
32
+
33
+ # @param [Osb::Sprite, Osb::Animation, Osb::Sample, Osb::Background, Osb::Video] object
34
+ def add(object)
35
+ case object
36
+ when Osb::Sprite, Osb::Animation
37
+ case object.layer
38
+ when Layer::Background
39
+ @background << object
40
+ when Layer::Foreground
41
+ @foreground << object
42
+ when Layer::Fail
43
+ @fail << object
44
+ when Layer::Pass
45
+ @pass << object
46
+ when Layer::Overlay
47
+ @overlay << object
48
+ end
49
+ when Osb::Sample
50
+ @samples << object
51
+ when Osb::Background, Osb::Video
52
+ @bg_and_video << object
53
+ end
54
+ end
55
+
56
+ # @param [Osb::Group] group
57
+ def concat(group)
58
+ @background.concat(group.layers.background)
59
+ @foreground.concat(group.layers.foreground)
60
+ @fail.concat(group.layers.fail)
61
+ @pass.concat(group.layers.pass)
62
+ @overlay.concat(group.layers.overlay)
63
+ @samples.concat(group.layers.samples)
64
+ end
65
+ end
66
+ end
67
+
68
+ # When designing storyboard, we often want to group storyboard elements
69
+ # that are used in a similar context (eg. a scene), so this class' purpose
70
+ # is only to act as a container. You can add the elements directly to the
71
+ # +Osb::Storyboard+ object, but we recommend you to split the project into multiple
72
+ # +Osb::Group+ so it will be easier to manage.
73
+ class Group
74
+ # @api private
75
+ attr_reader :layers
76
+
77
+ def initialize
78
+ @layers = Internal::LayerManager.new
79
+ end
80
+
81
+ # Add an +Osb::Sprite+, +Osb::Animation+, +Osb::Sample+ or +Osb::Group+ to
82
+ # this group.
83
+ # @param [Osb::Group, Osb::Sprite, Osb::Animation, Osb::Sample] object
84
+ # @return [self]
85
+ def add(object)
86
+ Internal.assert_type!(
87
+ object,
88
+ [
89
+ Osb::Group,
90
+ Osb::Sprite,
91
+ Osb::Animation,
92
+ Osb::Sample,
93
+ Osb::Video,
94
+ Osb::Background
95
+ ],
96
+ "object"
97
+ )
98
+
99
+ case object
100
+ when Osb::Sprite, Osb::Animation, Osb::Sample
101
+ @layers.add(object)
102
+ when Osb::Group
103
+ @layers.concat(object)
104
+ end
105
+
106
+ return self
107
+ end
108
+
109
+ # Add an +Osb::Sprite+, +Osb::Animation+, +Osb::Sample+ or +Osb::Group+ to
110
+ # this group. Alias for +#add+.
111
+ # @param [Osb::Group, Osb::Sprite, Osb::Animation, Osb::Sample] object
112
+ # @return [self]
113
+ def <<(object)
114
+ self.add(object)
115
+ end
116
+ end
117
+
118
+ # Represent a osu! storyboard. Each sprite or animation can be added directly
119
+ # to the storyboard instance, or through an intermediate group. A group can
120
+ # have multiple nested groups in itself.
121
+ class Storyboard
122
+ # @api private
123
+ attr_reader :layers
124
+
125
+ def initialize
126
+ @layers = Internal::LayerManager.new
127
+ end
128
+
129
+ # Add an +Osb::Sprite+, +Osb::Animation+, +Osb::Sample+, +Osb::Video+,
130
+ # +Osb::Background+ or +Osb::Group+ to this storyboard.
131
+ # @param [Osb::Group, Osb::Sprite, Osb::Animation, Osb::Sample, Osb::Video,
132
+ # Osb::Background] object
133
+ # @return [self]
134
+ def add(object)
135
+ Internal.assert_type!(
136
+ object,
137
+ [
138
+ Osb::Group,
139
+ Osb::Sprite,
140
+ Osb::Animation,
141
+ Osb::Video,
142
+ Osb::Background,
143
+ Osb::Sample
144
+ ],
145
+ "object"
146
+ )
147
+
148
+ case object
149
+ when Osb::Sprite, Osb::Animation, Osb::Sample, Osb::Video, Osb::Background
150
+ @layers.add(object)
151
+ when Osb::Group
152
+ @layers.concat(object)
153
+ end
154
+
155
+ return self
156
+ end
157
+
158
+ # Add an +Osb::Sprite+, +Osb::Animation+, +Osb::Sample+, +Osb::Video+,
159
+ # +Osb::Background+ or +Osb::Group+ to this storyboard. Alias for +#add+.
160
+ # @param [Osb::Group, Osb::Sprite, Osb::Animation, Osb::Sample, Osb::Video,
161
+ # Osb::Background] object
162
+ # @return [self]
163
+ def <<(object)
164
+ self.add(object)
165
+ end
166
+
167
+ # Returns the storyboard string.
168
+ # @return [String]
169
+ def to_s
170
+ bg_and_video_layer =
171
+ @layers.bg_and_video.map { |object| object.command }.join("\n")
172
+ background_layer =
173
+ @layers
174
+ .background
175
+ .map { |object| object.commands.join("\n") }
176
+ .join("\n")
177
+ fail_layer =
178
+ @layers.fail.map { |object| object.commands.join("\n") }.join("\n")
179
+ pass_layer =
180
+ @layers.pass.map { |object| object.commands.join("\n") }.join("\n")
181
+ foreground_layer =
182
+ @layers
183
+ .foreground
184
+ .map { |object| object.commands.join("\n") }
185
+ .join("\n")
186
+ overlay_layer =
187
+ @layers.overlay.map { |object| object.commands.join("\n") }.join("\n")
188
+ samples_layer = @layers.samples.map { |object| object.command }.join("\n")
189
+
190
+ osb_string = "[Events]\n"
191
+ osb_string +=
192
+ "//Background and Video events\n" + bg_and_video_layer + "\n" +
193
+ "//Storyboard Layer 0 (Background)\n" + background_layer + "\n" +
194
+ "//Storyboard Layer 1 (Fail)\n" + fail_layer + "\n" +
195
+ "//Storyboard Layer 2 (Pass)\n" + pass_layer + "\n" +
196
+ "//Storyboard Layer 3 (Foreground)\n" + foreground_layer + "\n" +
197
+ "//Storyboard Layer 4 (Overlay)\n" + overlay_layer + "\n" +
198
+ "//Storyboard Sound Samples\n" + samples_layer + "\n"
199
+ end
200
+
201
+ # Generate an osb or osu file for this storyboard.
202
+ # @param [String] out_path path to .osb or .osu file
203
+ # @return [void]
204
+ def generate(out_path)
205
+ Internal.assert_file_name_ext!(out_path, %w[osb osu])
206
+
207
+ case File.extname(out_path)
208
+ when ".osu"
209
+ unless File.exist?(out_path)
210
+ raise InvalidValueError, "Cannot find osu file."
211
+ end
212
+
213
+ out_osu_file = ""
214
+ File
215
+ .readlines(out_path)
216
+ .each do |line|
217
+ if line.match(/[Events]/)
218
+ out_osu_file += self.to_s
219
+ else
220
+ out_osu_file += line
221
+ end
222
+ end
223
+
224
+ File.new(out_path, "w").write(out_osu_file)
225
+ when ".osb"
226
+ File.new(out_path, "w").write(self.to_s)
227
+ else
228
+ raise InvalidValueError, "Not osu or osb file." # should not be here
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osb
4
+ # Represents a 2d point or vector.
5
+ class Vector2
6
+ attr_accessor :x, :y
7
+ # @!attribute [rw] x
8
+ # @return x coordinate of this vector
9
+ # @!attribute [rw] y
10
+ # @return y coordinate of this vector
11
+
12
+ # @param [Numeric, Array<Numeric>] x
13
+ # x coordinate of this +Vector2+, or an +Array+ of 2 numbers.
14
+ # @param [Numeric] y y coordinate of this +Vector2+
15
+ def initialize(x = 0, y = 0)
16
+ Internal.assert_type!(x, [Numeric, Internal::T[Array][Numeric]], "x")
17
+ Internal.assert_type!(y, Numeric, "y")
18
+
19
+ if x.is_a?(Array)
20
+ raise InvalidValueError, "Must be an Array of 2 Numeric values." if x.size != 2
21
+ @x = x[0]
22
+ @y = x[1]
23
+ else
24
+ @x = x
25
+ @y = y
26
+ end
27
+ end
28
+
29
+ # Add another +Vector2+ to this one.
30
+ # @param [Vector2] vector
31
+ # @return [Vector2]
32
+ def +(vector)
33
+ Internal.assert_type!(vector, Vector2, "vector")
34
+ Vector2.new(self.x + vector.x, self.y + vector.y)
35
+ end
36
+
37
+ # Subtract another +Vector2+ from this one.
38
+ # @param [Vector2] vector
39
+ # @return [Vector2]
40
+ def -(vector)
41
+ Internal.assert_type!(vector, Vector2, "vector")
42
+ Vector2.new(self.x - vector.x, self.y - vector.y)
43
+ end
44
+
45
+ # Returns whether two +Vector2+ are equal within tolerance
46
+ # @param [Vector2] vector
47
+ # @return [Boolean]
48
+ def ==(vector)
49
+ Internal.assert_type!(vector, Vector2, "vector")
50
+ Math.fuzzy_equal(self.x, vector.x) && Math.fuzzy_equal(self.y, vector.y)
51
+ end
52
+
53
+ # Returns whether two +Vector2+ are not equal within tolerance
54
+ # @param [Vector2] vector
55
+ # @return [Boolean]
56
+ def !=(vector)
57
+ !(self == vector)
58
+ end
59
+
60
+ # Makes a copy of this +Vector2+.
61
+ # @return [Vector2]
62
+ def clone
63
+ Vector2.new(self.x, self.y)
64
+ end
65
+
66
+ # Retrieves the coordinates in an Array.
67
+ # @return [Array(Float, Float)]
68
+ def to_a
69
+ [self.x, self.y]
70
+ end
71
+
72
+ # Returns a string representation of this +Vector2+.
73
+ # @return [String]
74
+ def to_s
75
+ self.to_a.to_s
76
+ end
77
+
78
+ # Returns the length of this +Vector2+.
79
+ # @return [Float]
80
+ def length
81
+ Math.sqrt(self.x**2 + self.y**2)
82
+ end
83
+ end
84
+ end
data/lib/osb/video.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Osb
2
+ # A video.
3
+ class Video
4
+ # @api private
5
+ attr_reader :commands, :layer
6
+
7
+ # @param [String] file_path location of the background image relative to the beatmap directory.
8
+ # @param [Integer] start_time when the video starts.
9
+ def initialize(file_path:, start_time:)
10
+ Internal.assert_type!(file_path, String, "file_path")
11
+ Internal.assert_file_name_ext!(file_path, %w[png jpg jpeg])
12
+ Internal.assert_type!(start_time, Integer, "start_time")
13
+
14
+ @command = "1,#{start_time},\"#{file_path}\""
15
+ end
16
+ end
17
+ end
data/lib/osb.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "osb/integer"
4
+ require_relative "osb/numeric"
5
+ require_relative "osb/assert"
6
+ require_relative "osb/math"
7
+ require_relative "osb/vector2"
8
+ require_relative "osb/color"
9
+ require_relative "osb/enums/layer"
10
+ require_relative "osb/enums/easing"
11
+ require_relative "osb/enums/origin"
12
+ require_relative "osb/commandable"
13
+ require_relative "osb/animation"
14
+ require_relative "osb/sprite"
15
+ require_relative "osb/sample"
16
+ require_relative "osb/video"
17
+ require_relative "osb/background"
18
+ require_relative "osb/storyboard"
19
+
20
+ module Osb
21
+ VERSION = "1.0.3"
22
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: osb
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Dinh Vu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-08-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A simple framework for building osu! storyboard.
14
+ email: dinhvu2509@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/osb.rb
20
+ - lib/osb/animation.rb
21
+ - lib/osb/assert.rb
22
+ - lib/osb/background.rb
23
+ - lib/osb/color.rb
24
+ - lib/osb/commandable.rb
25
+ - lib/osb/enums/easing.rb
26
+ - lib/osb/enums/layer.rb
27
+ - lib/osb/enums/origin.rb
28
+ - lib/osb/integer.rb
29
+ - lib/osb/math.rb
30
+ - lib/osb/numeric.rb
31
+ - lib/osb/sample.rb
32
+ - lib/osb/sprite.rb
33
+ - lib/osb/storyboard.rb
34
+ - lib/osb/vector2.rb
35
+ - lib/osb/video.rb
36
+ homepage: https://github.com/nanachi-code/osb-ruby
37
+ licenses:
38
+ - MIT
39
+ metadata: {}
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.1.6
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: osu! storyboard framework
59
+ test_files: []