osb 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/osb/animation.rb +58 -0
- data/lib/osb/assert.rb +131 -0
- data/lib/osb/background.rb +14 -0
- data/lib/osb/color.rb +98 -0
- data/lib/osb/commandable.rb +373 -0
- data/lib/osb/enums/easing.rb +42 -0
- data/lib/osb/enums/layer.rb +12 -0
- data/lib/osb/enums/origin.rb +31 -0
- data/lib/osb/integer.rb +19 -0
- data/lib/osb/math.rb +12 -0
- data/lib/osb/numeric.rb +7 -0
- data/lib/osb/sample.rb +34 -0
- data/lib/osb/sprite.rb +44 -0
- data/lib/osb/storyboard.rb +232 -0
- data/lib/osb/vector2.rb +84 -0
- data/lib/osb/video.rb +17 -0
- data/lib/osb.rb +22 -0
- metadata +59 -0
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,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
|
data/lib/osb/integer.rb
ADDED
@@ -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
|
data/lib/osb/numeric.rb
ADDED
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
|
data/lib/osb/vector2.rb
ADDED
@@ -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: []
|