osb 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|