sscharter 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -0
- data/Rakefile +8 -2
- data/lib/sscharter/chart.rb +52 -8
- data/lib/sscharter/charter/basic_events.rb +277 -0
- data/lib/sscharter/charter/beat.rb +334 -0
- data/lib/sscharter/charter/check.rb +37 -0
- data/lib/sscharter/charter/event.rb +429 -0
- data/lib/sscharter/charter/events_manip.rb +175 -0
- data/lib/sscharter/charter/group.rb +130 -0
- data/lib/sscharter/charter/metadata.rb +129 -0
- data/lib/sscharter/charter/story_events.rb +54 -0
- data/lib/sscharter/charter/tip_point.rb +208 -0
- data/lib/sscharter/charter.rb +87 -0
- data/lib/sscharter/tools/svg_path.rb +549 -0
- data/lib/sscharter/tools.rb +6 -0
- data/lib/sscharter/utils.rb +19 -2
- data/lib/sscharter/version.rb +1 -1
- data/lib/sscharter.rb +2 -1074
- data/lock/ruby-3.0.7-bundler-2.5.23-Gemfile.lock +67 -0
- data/lock/ruby-3.1.7-bundler-2.6.9-Gemfile.lock +67 -0
- data/lock/ruby-3.2.11-bundler-4.0.10-Gemfile.lock +98 -0
- data/lock/ruby-3.3.11-bundler-4.0.10-Gemfile.lock +98 -0
- data/lock/ruby-3.4.9-bundler-4.0.10-Gemfile.lock +101 -0
- data/lock/ruby-4.0.2-bundler-4.0.10-Gemfile.lock +101 -0
- data/tutorial/advanced.md +33 -0
- data/tutorial/tools.md +26 -0
- data/tutorial/tutorial.md +6 -32
- metadata +64 -16
- data/Gemfile.lock +0 -48
data/lib/sscharter.rb
CHANGED
|
@@ -3,1077 +3,5 @@
|
|
|
3
3
|
require_relative 'sscharter/version'
|
|
4
4
|
require_relative 'sscharter/utils'
|
|
5
5
|
require_relative 'sscharter/chart'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
PROJECT_DIR = File.expand_path(ENV['SSCHARTER_PROJECT_DIR'] ||= Dir.pwd)
|
|
10
|
-
|
|
11
|
-
using Sunniesnow::Utils
|
|
12
|
-
|
|
13
|
-
class OffsetError < StandardError
|
|
14
|
-
def initialize method_name
|
|
15
|
-
super "offset must be set before using #{method_name}"
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
class TipPointError < StandardError
|
|
20
|
-
def initialize *expected_state, actual_state
|
|
21
|
-
super "wrong tip point state: expected #{expected_state.join ' or '}, got #{actual_state}"
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.ensure state, *expected
|
|
25
|
-
raise self.new *expected, state unless expected.include? state
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
class BpmChangeList
|
|
31
|
-
|
|
32
|
-
class BpmChange
|
|
33
|
-
include Comparable
|
|
34
|
-
|
|
35
|
-
attr_accessor :beat, :bps
|
|
36
|
-
|
|
37
|
-
def initialize beat, bpm
|
|
38
|
-
@beat = beat
|
|
39
|
-
@bps = bpm / 60.0
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def <=> other
|
|
43
|
-
@beat <=> other.beat
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
attr_accessor :offset
|
|
48
|
-
|
|
49
|
-
def initialize offset
|
|
50
|
-
@offset = offset
|
|
51
|
-
@list = []
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def add beat, bpm
|
|
55
|
-
@list.push BpmChange.new beat, bpm
|
|
56
|
-
@list.sort!
|
|
57
|
-
self
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def time_at beat
|
|
61
|
-
index = @list.bisect(right: true) { _1.beat <=> beat }
|
|
62
|
-
raise ArgumentError, 'beat is before the first bpm change' if index < 0
|
|
63
|
-
bpm = @list[index]
|
|
64
|
-
(0...index).sum @offset + (beat - bpm.beat) / bpm.bps do |i|
|
|
65
|
-
bpm = @list[i]
|
|
66
|
-
(@list[i+1].beat - bpm.beat) / bpm.bps
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def [] index
|
|
71
|
-
@list[index]
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
class Event
|
|
76
|
-
|
|
77
|
-
TIP_POINTABLE_TYPES = %i[tap hold flick drag]
|
|
78
|
-
|
|
79
|
-
attr_accessor :beat, :offset, :duration_beats, :properties
|
|
80
|
-
attr_reader :type, :bpm_changes, :backtrace
|
|
81
|
-
|
|
82
|
-
def initialize type, beat, duration_beats = nil, bpm_changes, **properties
|
|
83
|
-
@beat = beat
|
|
84
|
-
@duration_beats = duration_beats
|
|
85
|
-
@type = type
|
|
86
|
-
@bpm_changes = bpm_changes
|
|
87
|
-
@properties = properties
|
|
88
|
-
@offset = 0.0
|
|
89
|
-
@backtrace = caller.filter { _1.sub! /^#{PROJECT_DIR}\//, '' }
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def time_at_relative_beat delta_beat
|
|
93
|
-
@offset + @bpm_changes.time_at(@beat + delta_beat)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def time
|
|
97
|
-
time_at_relative_beat 0
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def end_time
|
|
101
|
-
time_at_relative_beat @duration_beats || 0
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def [] key
|
|
105
|
-
@properties[key]
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def []= key, value
|
|
109
|
-
@properties[key] = value
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def to_sunniesnow
|
|
113
|
-
t = time
|
|
114
|
-
properties = @properties.transform_keys &:snake_to_camel
|
|
115
|
-
properties[:duration] = end_time - t if @duration_beats
|
|
116
|
-
Sunniesnow::Event.new t, @type.snake_to_camel, **properties
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def dup
|
|
120
|
-
result = super
|
|
121
|
-
result.properties = @properties.dup
|
|
122
|
-
result
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def tip_pointable?
|
|
126
|
-
TIP_POINTABLE_TYPES.include? @type
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def inspect
|
|
130
|
-
"#<#@type at #@beat#{@duration_beats && " for #@duration_beats"} offset #@offset: " +
|
|
131
|
-
@properties.map { |k, v| "#{k}=#{v.inspect}" }.join(', ') + '>'
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Implements homography
|
|
136
|
-
class Transform
|
|
137
|
-
include Math
|
|
138
|
-
attr_reader :xx, :xy, :xz, :yx, :yy, :yz, :zx, :zy, :zz, :tt, :t1
|
|
139
|
-
|
|
140
|
-
def initialize
|
|
141
|
-
@xx = @yy = @zz = 1.0
|
|
142
|
-
@xy = @xz = @yx = @yz = @zx = @zy = 0.0
|
|
143
|
-
@t1 = 0r
|
|
144
|
-
@tt = 1r
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def apply event
|
|
148
|
-
event.beat = @t1 + @tt * event.beat
|
|
149
|
-
return unless x = event[:x]
|
|
150
|
-
return unless y = event[:y]
|
|
151
|
-
rx = xx*x + xy*y + xz
|
|
152
|
-
ry = yx*x + yy*y + yz
|
|
153
|
-
d = zx*x + zy*y + zz
|
|
154
|
-
event[:x] = xp = rx / d
|
|
155
|
-
event[:y] = yp = ry / d
|
|
156
|
-
|
|
157
|
-
return event unless angle = event[:angle]
|
|
158
|
-
dx = cos angle
|
|
159
|
-
dy = sin angle
|
|
160
|
-
cross = y*dx - x*dy
|
|
161
|
-
|
|
162
|
-
cx0 = zy*xx - xy*zx
|
|
163
|
-
cxx = zz*xx - xz*zx
|
|
164
|
-
cxy = zz*xy - xz*zy
|
|
165
|
-
dxp = cx0*cross + cxx*dx + cxy*dy
|
|
166
|
-
|
|
167
|
-
cy0 = zx*yy - yx*zy
|
|
168
|
-
cyy = zz*yy - yz*zy
|
|
169
|
-
cyx = zz*yx - yz*zx
|
|
170
|
-
dyp = cy0*-cross + cyy*dy + cyx*dx
|
|
171
|
-
|
|
172
|
-
event[:angle] = atan2 dyp, dxp
|
|
173
|
-
event
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def compound_linear xx, xy, yx, yy
|
|
177
|
-
@xx, @xy, @xz, @yx, @yy, @yz = [
|
|
178
|
-
xx * @xx + xy * @yx,
|
|
179
|
-
xx * @xy + xy * @yy,
|
|
180
|
-
xx * @xz + xy * @yz,
|
|
181
|
-
yx * @xx + yy * @yx,
|
|
182
|
-
yx * @xy + yy * @yy,
|
|
183
|
-
yx * @xz + yy * @yz,
|
|
184
|
-
]
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def translate dx, dy
|
|
188
|
-
raise ArgumentError, 'dx and dy must be numbers' unless dx.is_a?(Numeric) && dy.is_a?(Numeric)
|
|
189
|
-
@xz += dx
|
|
190
|
-
@yz += dy
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def horizontal_flip
|
|
194
|
-
compound_linear -1, 0, 0, 1
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def vertical_flip
|
|
198
|
-
compound_linear 1, 0, 0, -1
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def rotate angle
|
|
202
|
-
raise ArgumentError, 'angle must be a number' unless angle.is_a? Numeric
|
|
203
|
-
warn 'Are you using degrees as angle unit instead of radians?' if angle != 0 && angle % 45 == 0
|
|
204
|
-
c = cos angle
|
|
205
|
-
s = sin angle
|
|
206
|
-
compound_linear c, -s, s, c
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def scale sx, sy = sx
|
|
210
|
-
raise ArgumentError, 'sx and sy must be numbers' unless sx.is_a?(Numeric) && sy.is_a?(Numeric)
|
|
211
|
-
compound_linear sx, 0, 0, sy
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def beat_translate delta_beat
|
|
215
|
-
raise ArgumentError, 'delta_beat must be a number' unless delta_beat.is_a? Numeric
|
|
216
|
-
warn 'Rational is recommended over Float for delta_beat' if delta_beat.is_a? Float
|
|
217
|
-
@t1 += delta_beat.to_r
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
class TipPointStart
|
|
222
|
-
|
|
223
|
-
def initialize x = 0, y = 0, relative_time = 0.0, relative: true, speed: nil,
|
|
224
|
-
relative_beat: nil, beat_speed: nil
|
|
225
|
-
@x = x
|
|
226
|
-
@y = y
|
|
227
|
-
@relative_time = relative_time
|
|
228
|
-
@relative = relative
|
|
229
|
-
@speed = speed
|
|
230
|
-
@relative_beat = relative_beat
|
|
231
|
-
@beat_speed = beat_speed
|
|
232
|
-
check
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def check
|
|
236
|
-
if !@x.is_a?(Numeric) || !@y.is_a?(Numeric)
|
|
237
|
-
raise ArgumentError, 'x and y must be numbers'
|
|
238
|
-
end
|
|
239
|
-
@x = @x.to_f
|
|
240
|
-
@y = @y.to_f
|
|
241
|
-
%i[@relative_time @speed @relative_beat @beat_speed].each do |key|
|
|
242
|
-
value = instance_variable_get key
|
|
243
|
-
raise ArgumentError, "cannot specify both #@time_key and #{key}" if @time_key && value&.!=(0)
|
|
244
|
-
@time_key = key if value&.!=(0)
|
|
245
|
-
end
|
|
246
|
-
@time_key ||= :@relative_time
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def get_start_placeholder start_event
|
|
250
|
-
raise ArgumentError, "start_event is not tip-pointable" unless start_event.tip_pointable?
|
|
251
|
-
result = Event.new :placeholder, start_event.beat, start_event.bpm_changes
|
|
252
|
-
if @relative
|
|
253
|
-
result[:x] = start_event[:x] + @x
|
|
254
|
-
result[:y] = start_event[:y] + @y
|
|
255
|
-
else
|
|
256
|
-
result[:x] = @x
|
|
257
|
-
result[:y] = @y
|
|
258
|
-
end
|
|
259
|
-
case @time_key
|
|
260
|
-
when :@relative_time
|
|
261
|
-
raise ArgumentError, "relative_time must be a number" unless @relative_time.is_a? Numeric
|
|
262
|
-
raise ArgumentError, "relative_time must be non-negative" if @relative_time < 0
|
|
263
|
-
result.offset = -@relative_time.to_f
|
|
264
|
-
when :@speed
|
|
265
|
-
raise ArgumentError, "speed must be a number" unless @speed.is_a? Numeric
|
|
266
|
-
raise ArgumentError, "speed must be positive" if @speed <= 0
|
|
267
|
-
result.offset = -Math.hypot(result[:x] - start_event[:x], result[:y] - start_event[:y]) / @speed
|
|
268
|
-
when :@relative_beat
|
|
269
|
-
raise ArgumentError, "relative_beat must be a number" unless @relative_beat.is_a? Numeric
|
|
270
|
-
raise ArgumentError, "relative_beat must be non-negative" if @relative_beat < 0
|
|
271
|
-
warn "Rational is recommended over Float for relative_beat" if @relative_beat.is_a? Float
|
|
272
|
-
result.beat -= @relative_beat.to_r
|
|
273
|
-
when :@beat_speed
|
|
274
|
-
raise ArgumentError, "beat_speed must be a number" unless @beat_speed.is_a? Numeric
|
|
275
|
-
raise ArgumentError, "beat_speed must be positive" if @beat_speed <= 0
|
|
276
|
-
delta_beat = Math.hypot(result[:x] - start_event[:x], result[:y] - start_event[:y]) / @beat_speed
|
|
277
|
-
result.beat -= delta_beat.to_r # a little weird, but fine
|
|
278
|
-
end
|
|
279
|
-
result[:tip_point] = start_event[:tip_point]
|
|
280
|
-
result
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
COLORS = {
|
|
285
|
-
easy: '#3eb9fd',
|
|
286
|
-
normal: '#f19e56',
|
|
287
|
-
hard: '#e75e74',
|
|
288
|
-
master: '#8c68f3',
|
|
289
|
-
special: '#f156ee'
|
|
290
|
-
}.freeze
|
|
291
|
-
|
|
292
|
-
DIRECTIONS = {
|
|
293
|
-
right: %i[r],
|
|
294
|
-
up_right: %i[ur ru],
|
|
295
|
-
up: %i[u],
|
|
296
|
-
up_left: %i[ul lu],
|
|
297
|
-
left: %i[l],
|
|
298
|
-
down_left: %i[dl ld],
|
|
299
|
-
down: %i[d],
|
|
300
|
-
down_right: %i[dr rd]
|
|
301
|
-
}.each_with_object({
|
|
302
|
-
right: 0.0,
|
|
303
|
-
up_right: Math::PI / 4,
|
|
304
|
-
up: Math::PI / 2,
|
|
305
|
-
up_left: Math::PI * 3 / 4,
|
|
306
|
-
left: Math::PI,
|
|
307
|
-
down_left: -Math::PI * 3 / 4,
|
|
308
|
-
down: -Math::PI / 2,
|
|
309
|
-
down_right: -Math::PI / 4
|
|
310
|
-
}) do |(direction_name, aliases), directions|
|
|
311
|
-
aliases.each { directions[_1] = directions[direction_name] }
|
|
312
|
-
end.freeze
|
|
313
|
-
|
|
314
|
-
DIRECTIONS.freeze
|
|
315
|
-
|
|
316
|
-
class << self
|
|
317
|
-
# A hash containing all the charts opened by {::open}.
|
|
318
|
-
# The keys are the names of the charts, and the values are the {Sunniesnow::Charter} objects.
|
|
319
|
-
# @return [Hash<String, Sunniesnow::Charter>]
|
|
320
|
-
attr_reader :charts
|
|
321
|
-
end
|
|
322
|
-
@charts = {}
|
|
323
|
-
|
|
324
|
-
# An array of events.
|
|
325
|
-
# @return [Array<Sunniesnow::Charter::Event>]
|
|
326
|
-
attr_reader :events
|
|
327
|
-
|
|
328
|
-
# Create a new chart or open an existing chart for editing.
|
|
329
|
-
# The +name+ is used to check whether the chart already exists.
|
|
330
|
-
# If a new chart needs to be created, it is added to {.charts}.
|
|
331
|
-
#
|
|
332
|
-
# The given +block+ will be evaluated in the context of the chart
|
|
333
|
-
# (inside the block, +self+ is the same as the return value, a {Charter} instance).
|
|
334
|
-
# This method is intended to be called at the top level of a Ruby script
|
|
335
|
-
# to open a context for writing a Sunniesnow chart using the DSL.
|
|
336
|
-
#
|
|
337
|
-
# In the examples in the documentation of other methods,
|
|
338
|
-
# it is assumed that they are run inside a block passed to this method.
|
|
339
|
-
#
|
|
340
|
-
# @param name [String] the name of the chart.
|
|
341
|
-
# @return [Sunniesnow::Charter] the chart.
|
|
342
|
-
# @example
|
|
343
|
-
# Sunniesnow::Charter.open 'master' do
|
|
344
|
-
# # write the chart here
|
|
345
|
-
# end
|
|
346
|
-
def self.open name, &block
|
|
347
|
-
result = @charts[name] ||= new name
|
|
348
|
-
result.instance_eval &block if block
|
|
349
|
-
result
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
# Create a new chart.
|
|
353
|
-
# Usually you should use {.open} instead of this method.
|
|
354
|
-
# @param name [String] the name of the chart.
|
|
355
|
-
def initialize name
|
|
356
|
-
@name = name
|
|
357
|
-
init_chart_info
|
|
358
|
-
init_state
|
|
359
|
-
init_bookmarks
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
def init_chart_info
|
|
363
|
-
@difficulty_name = ''
|
|
364
|
-
@difficulty_color = '#000000'
|
|
365
|
-
@difficulty = ''
|
|
366
|
-
@difficulty_sup = ''
|
|
367
|
-
@title = ''
|
|
368
|
-
@artist = ''
|
|
369
|
-
@charter = ''
|
|
370
|
-
@events = []
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
def init_bookmarks
|
|
374
|
-
@bookmarks = {}
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def init_state
|
|
378
|
-
@current_beat = nil
|
|
379
|
-
@bpm_changes = nil
|
|
380
|
-
@tip_point_mode_stack = [:none]
|
|
381
|
-
@current_tip_point_stack = []
|
|
382
|
-
@current_tip_point_group_stack = []
|
|
383
|
-
@tip_point_peak = 0
|
|
384
|
-
@current_duplicate = 0
|
|
385
|
-
@tip_point_start_stack = [nil]
|
|
386
|
-
@tip_point_start_to_add_stack = [nil]
|
|
387
|
-
@groups = [@events]
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
def to_sunniesnow **opts
|
|
391
|
-
result = Sunniesnow::Chart.new **opts
|
|
392
|
-
result.title = @title
|
|
393
|
-
result.artist = @artist
|
|
394
|
-
result.charter = @charter
|
|
395
|
-
result.difficulty_name = @difficulty_name
|
|
396
|
-
result.difficulty_color = @difficulty_color
|
|
397
|
-
result.difficulty = @difficulty
|
|
398
|
-
result.difficulty_sup = @difficulty_sup
|
|
399
|
-
@events.each { result.events.push _1.to_sunniesnow }
|
|
400
|
-
result
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def output_json **opts
|
|
404
|
-
to_sunniesnow(**opts).to_json
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def inspect
|
|
408
|
-
"#<Sunniesnow::Charter #@name>"
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def time_at beat = @current_beat
|
|
412
|
-
raise OffsetError.new __method__ unless @bpm_changes
|
|
413
|
-
@bpm_changes.time_at beat
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
def backup_beat
|
|
417
|
-
{current_beat: @current_beat, bpm_changes: @bpm_changes}
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def restore_beat backup
|
|
421
|
-
@current_beat = backup[:current_beat]
|
|
422
|
-
@bpm_changes = backup[:bpm_changes]
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
def backup_state
|
|
426
|
-
{
|
|
427
|
-
current_beat: @current_beat,
|
|
428
|
-
bpm_changes: @bpm_changes,
|
|
429
|
-
tip_point_mode_stack: @tip_point_mode_stack.dup,
|
|
430
|
-
current_tip_point_stack: @current_tip_point_stack.dup,
|
|
431
|
-
current_tip_point_group_stack: @current_tip_point_group_stack.dup,
|
|
432
|
-
current_duplicate: @current_duplicate,
|
|
433
|
-
tip_point_start_stack: @tip_point_start_stack.dup,
|
|
434
|
-
tip_point_start_to_add_stack: @tip_point_start_to_add_stack.dup,
|
|
435
|
-
groups: @groups.dup
|
|
436
|
-
}
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def restore_state backup
|
|
440
|
-
@current_beat = backup[:current_beat]
|
|
441
|
-
@bpm_changes = backup[:bpm_changes]
|
|
442
|
-
@tip_point_mode_stack = backup[:tip_point_mode_stack]
|
|
443
|
-
@current_tip_point_stack = backup[:current_tip_point_stack]
|
|
444
|
-
@current_tip_point_group_stack = backup[:current_tip_point_group_stack]
|
|
445
|
-
@current_duplicate = backup[:current_duplicate]
|
|
446
|
-
@tip_point_start_to_add_stack = backup[:tip_point_start_to_add_stack]
|
|
447
|
-
@groups = backup[:groups]
|
|
448
|
-
nil
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
def event type, duration_beats = nil, **properties
|
|
452
|
-
raise OffsetError.new __method__ unless @bpm_changes
|
|
453
|
-
event = Event.new type, @current_beat, duration_beats, @bpm_changes, **properties
|
|
454
|
-
@groups.each { _1.push event }
|
|
455
|
-
return event unless event.tip_pointable?
|
|
456
|
-
case @tip_point_mode_stack.last
|
|
457
|
-
when :chain
|
|
458
|
-
if @tip_point_start_to_add_stack.last
|
|
459
|
-
@current_tip_point_stack[-1] = @tip_point_peak
|
|
460
|
-
@tip_point_peak += 1
|
|
461
|
-
end
|
|
462
|
-
push_tip_point_start event
|
|
463
|
-
@tip_point_start_to_add_stack[-1] = nil
|
|
464
|
-
when :drop
|
|
465
|
-
@current_tip_point_stack[-1] = @tip_point_peak
|
|
466
|
-
@tip_point_peak += 1
|
|
467
|
-
push_tip_point_start event
|
|
468
|
-
when :none
|
|
469
|
-
# pass
|
|
470
|
-
end
|
|
471
|
-
event
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
def push_tip_point_start start_event
|
|
475
|
-
start_event[:tip_point] = @current_tip_point_stack.last.to_s
|
|
476
|
-
tip_point_start = @tip_point_start_to_add_stack.last&.get_start_placeholder start_event
|
|
477
|
-
return unless tip_point_start
|
|
478
|
-
@groups.each do |group|
|
|
479
|
-
group.push tip_point_start
|
|
480
|
-
break if group.equal?(@current_tip_point_group_stack.last) && @tip_point_mode_stack.last != :drop
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
def tip_point mode, *args, preserve_beat: true, **opts, &block
|
|
485
|
-
@tip_point_mode_stack.push mode
|
|
486
|
-
if mode == :none
|
|
487
|
-
@tip_point_start_stack.push nil
|
|
488
|
-
@tip_point_start_to_add_stack.push nil
|
|
489
|
-
@current_tip_point_stack.push nil
|
|
490
|
-
else
|
|
491
|
-
if args.empty? && opts.empty?
|
|
492
|
-
unless @tip_point_start_stack.last
|
|
493
|
-
raise TipPointError, 'cannot omit tip point arguments at top level or inside tip_point_none'
|
|
494
|
-
end
|
|
495
|
-
@tip_point_start_stack.push @tip_point_start_stack.last.dup
|
|
496
|
-
else
|
|
497
|
-
@tip_point_start_stack.push TipPointStart.new *args, **opts
|
|
498
|
-
end
|
|
499
|
-
@tip_point_start_to_add_stack.push @tip_point_start_stack.last
|
|
500
|
-
@current_tip_point_stack.push nil
|
|
501
|
-
end
|
|
502
|
-
result = group preserve_beat: do
|
|
503
|
-
@current_tip_point_group_stack.push @groups.last
|
|
504
|
-
instance_eval &block
|
|
505
|
-
end
|
|
506
|
-
@tip_point_start_stack.pop
|
|
507
|
-
@tip_point_start_to_add_stack.pop
|
|
508
|
-
@tip_point_mode_stack.pop
|
|
509
|
-
@current_tip_point_stack.pop
|
|
510
|
-
@current_tip_point_group_stack.pop
|
|
511
|
-
result
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
# @!group DSL methods
|
|
515
|
-
|
|
516
|
-
# Set the title of the music for the chart.
|
|
517
|
-
# This will be reflected in the return value of {#to_sunniesnow}.
|
|
518
|
-
# @see Sunniesnow::Chart#title
|
|
519
|
-
# @param title [String] the title of the music.
|
|
520
|
-
# @return [String] the title of the music, the same as the argument +title+.
|
|
521
|
-
# @raise [ArgumentError] if +title+ is not a String.
|
|
522
|
-
def title title
|
|
523
|
-
raise ArgumentError, 'title must be a string' unless title.is_a? String
|
|
524
|
-
@title = title
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
# Set the artist of the music for the chart.
|
|
528
|
-
# This will be reflected in the return value of {#to_sunniesnow}.
|
|
529
|
-
# @see Sunniesnow::Chart#artist
|
|
530
|
-
# @param artist [String] the artist of the music.
|
|
531
|
-
# @return [String] the artist of the music, the same as the argument +artist+.
|
|
532
|
-
# @raise [ArgumentError] if +artist+ is not a String.
|
|
533
|
-
def artist artist
|
|
534
|
-
raise ArgumentError, 'artist must be a string' unless artist.is_a? String
|
|
535
|
-
@artist = artist
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
# Set the name of the chart author for the chart.
|
|
539
|
-
# This will be reflected in the return value of {#to_sunniesnow}.
|
|
540
|
-
# @see Sunniesnow::Chart#charter
|
|
541
|
-
# @param charter [String] the name of the charter.
|
|
542
|
-
# @return [String] the name of the chart author, the same as the argument +charter+.
|
|
543
|
-
# @raise [ArgumentError] if +charter+ is not a String.
|
|
544
|
-
def charter charter
|
|
545
|
-
raise ArgumentError, 'charter must be a string' unless charter.is_a? String
|
|
546
|
-
@charter = charter
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
# Set the name of the difficulty for the chart.
|
|
550
|
-
# This will be reflected in the return value of {#to_sunniesnow}.
|
|
551
|
-
# @see Sunniesnow::Chart#difficulty_name
|
|
552
|
-
# @param difficulty_name [String] the name of the difficulty.
|
|
553
|
-
# @return [String] the name of the difficulty, the same as the argument +difficulty_name+.
|
|
554
|
-
# @raise [ArgumentError] if +difficulty_name+ is not a String.
|
|
555
|
-
def difficulty_name difficulty_name
|
|
556
|
-
raise ArgumentError, 'difficulty_name must be a string' unless difficulty_name.is_a? String
|
|
557
|
-
@difficulty_name = difficulty_name
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
# Set the color of the difficulty for the chart.
|
|
561
|
-
# This will be reflected in the return value of {#to_sunniesnow}.
|
|
562
|
-
#
|
|
563
|
-
# The argument +difficulty_color+ can be a color name (a key of {COLORS}),
|
|
564
|
-
# an RGB color in hexadecimal format (e.g. +'#8c68f3'+, +'#8CF'+),
|
|
565
|
-
# an RGB color in decimal format (e.g. +'rgb(140, 104, 243)'+),
|
|
566
|
-
# or an integer representing an RGB color (e.g. +0x8c68f3+).
|
|
567
|
-
# @see Sunniesnow::Chart#difficulty_color
|
|
568
|
-
# @param difficulty_color [Symbol, String, Integer] the color of the difficulty.
|
|
569
|
-
# @return [String] the color of the difficulty in hexadecimal format (e.g. +'#8c68f3'+).
|
|
570
|
-
# @raise [ArgumentError] if +difficulty_color+ is not a valid color format.
|
|
571
|
-
def difficulty_color difficulty_color
|
|
572
|
-
@difficulty_color = case difficulty_color
|
|
573
|
-
when Symbol
|
|
574
|
-
COLORS[difficulty_color]
|
|
575
|
-
when /^#[0-9a-fA-F]{6}$/
|
|
576
|
-
difficulty_color
|
|
577
|
-
when /^#[0-9a-fA-F]{3}$/
|
|
578
|
-
_, r, g, b = difficulty_color.chars
|
|
579
|
-
"##{r}#{r}#{g}#{g}#{b}#{b}"
|
|
580
|
-
when /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/
|
|
581
|
-
r, g, b = $1, $2, $3
|
|
582
|
-
sprintf '#%02x%02x%02x', r.to_i, g.to_i, b.to_i
|
|
583
|
-
when Integer
|
|
584
|
-
sprintf '#%06x', difficulty_color % 0x1000000
|
|
585
|
-
else
|
|
586
|
-
raise ArgumentError, 'unknown format of difficulty_color'
|
|
587
|
-
end
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
# Set the difficulty level for the chart.
|
|
591
|
-
# This will be reflected in the return value of {#to_sunniesnow}.
|
|
592
|
-
#
|
|
593
|
-
# The argument +difficulty+ should be a string representing the difficulty level.
|
|
594
|
-
# Anything other than a string will be converted to a string using +to_s+.
|
|
595
|
-
# @see Sunniesnow::Chart#difficulty
|
|
596
|
-
# @param difficulty [String] the difficulty level.
|
|
597
|
-
# @return [String] the difficulty level (converted to a string).
|
|
598
|
-
def difficulty difficulty
|
|
599
|
-
@difficulty = difficulty.to_s
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
# Set the difficulty superscript for the chart.
|
|
603
|
-
# This will be reflected in the return value of {#to_sunniesnow}.
|
|
604
|
-
#
|
|
605
|
-
# The argument +difficulty_sup+ should be a string representing the difficulty superscript.
|
|
606
|
-
# Anything other than a string will be converted to a string using +to_s+.
|
|
607
|
-
# @see Sunniesnow::Chart#difficulty_sup
|
|
608
|
-
# @param difficulty_sup [String] the difficulty superscript.
|
|
609
|
-
# @return [String] the difficulty superscript (converted to a string).
|
|
610
|
-
def difficulty_sup difficulty_sup
|
|
611
|
-
@difficulty_sup = difficulty_sup.to_s
|
|
612
|
-
end
|
|
613
|
-
|
|
614
|
-
# Set the offset.
|
|
615
|
-
# This is the time in seconds of the zeroth beat.
|
|
616
|
-
# This method must be called before any other methods that require a beat,
|
|
617
|
-
# or an {OffsetError} will be raised.
|
|
618
|
-
#
|
|
619
|
-
# After calling this method, the current beat (see {#beat} and {#beat!}) is set to zero,
|
|
620
|
-
# and a new BPM needs to be set using {#bpm}.
|
|
621
|
-
# Only after that can the time of any positive beat be calculated.
|
|
622
|
-
#
|
|
623
|
-
# Though not commonly useful, this method can be called multiple times in a chart.
|
|
624
|
-
# A new call of this method does not affect the events and BPM changes set before.
|
|
625
|
-
# Technically, each event is associated with a BPM change list (see {Event#bpm_changes}),
|
|
626
|
-
# and each call of this method creates a new BPM change list,
|
|
627
|
-
# which is used for the events set after.
|
|
628
|
-
# @param offset [Numeric] the offset in seconds.
|
|
629
|
-
# @return [BpmChangeList] the BPM changes.
|
|
630
|
-
# @see BpmChangeList
|
|
631
|
-
# @raise [ArgumentError] if +offset+ is not a number.
|
|
632
|
-
# @example
|
|
633
|
-
# offset 0.1
|
|
634
|
-
# p time_at # Outputs 0.1, which is the offset
|
|
635
|
-
# offset 0.2
|
|
636
|
-
# p time_at # Outputs 0.2, which is the updated offset by the second call
|
|
637
|
-
def offset offset
|
|
638
|
-
raise ArgumentError, 'offset must be a number' unless offset.is_a? Numeric
|
|
639
|
-
@current_beat = 0r
|
|
640
|
-
@bpm_changes = BpmChangeList.new offset.to_f
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
# Set the BPM starting at the current beat.
|
|
644
|
-
# This method must be called after {#offset}.
|
|
645
|
-
# The method can be called multiple times,
|
|
646
|
-
# which is useful when the music changes its tempo from time to time.
|
|
647
|
-
#
|
|
648
|
-
# Internally, this simply calls {BpmChangeList#add} on the BPM changes created by {#offset}.
|
|
649
|
-
# @param bpm [Numeric] the BPM.
|
|
650
|
-
# @raise [OffsetError] if {#offset} has not been called.
|
|
651
|
-
# @return [BpmChangeList] the BPM changes.
|
|
652
|
-
def bpm bpm
|
|
653
|
-
raise OffsetError.new __method__ unless @bpm_changes
|
|
654
|
-
@bpm_changes.add @current_beat, bpm
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
# Increments the current beat by the given delta set by +delta_beat+.
|
|
658
|
-
# It is recommended that +delta_beat+ be a Rational or an Integer for accuracy.
|
|
659
|
-
# Float will be converted to Rational, and a warning will be issued
|
|
660
|
-
# when a Float is used.
|
|
661
|
-
#
|
|
662
|
-
# This method is also useful for inspecting the current beat.
|
|
663
|
-
# If the method is called without an argument, it simply returns the current beat.
|
|
664
|
-
# For this purpose, this method is equivalent to {#beat!}.
|
|
665
|
-
#
|
|
666
|
-
# This method must be called after {#offset}.
|
|
667
|
-
# @param delta_beat [Rational, Integer] the delta to increment the current beat by.
|
|
668
|
-
# @raise [OffsetError] if {#offset} has not been called.
|
|
669
|
-
# @return [Rational] the new current beat.
|
|
670
|
-
# @see #beat!
|
|
671
|
-
# @example Increment the current beat and inspect it
|
|
672
|
-
# offset 0.1; bpm 120
|
|
673
|
-
# p b # Outputs 0, this is the initial value
|
|
674
|
-
# p b 1 # Outputs 1, because it is incremented by 1 when it was 0
|
|
675
|
-
# p b 1/2r # Outputs 3/2, because it is incremented by 3/2 when it was 1
|
|
676
|
-
# p time_at # Outputs 0.85, which is offset + 60s / BPM * beat
|
|
677
|
-
# @example Time the notes
|
|
678
|
-
# offset 0.1; bpm 120
|
|
679
|
-
# t 0, 0; b 1
|
|
680
|
-
# t 50, 0; b 1
|
|
681
|
-
# # Now there are two tap notes, one at beat 0, and the other at beat 1
|
|
682
|
-
def beat delta_beat = 0
|
|
683
|
-
raise OffsetError.new __method__ unless @current_beat
|
|
684
|
-
case delta_beat
|
|
685
|
-
when Integer, Rational
|
|
686
|
-
@current_beat += delta_beat.to_r
|
|
687
|
-
when Float
|
|
688
|
-
warn 'float beat is not recommended'
|
|
689
|
-
@current_beat += delta_beat.to_r
|
|
690
|
-
else
|
|
691
|
-
raise ArgumentError, 'invalid delta_beat'
|
|
692
|
-
end
|
|
693
|
-
end
|
|
694
|
-
alias b beat
|
|
695
|
-
|
|
696
|
-
# Sets the current beat to the given value.
|
|
697
|
-
# It is recommended that +beat+ be a Rational or an Integer for accuracy.
|
|
698
|
-
# Float will be converted to Rational, and a warning will be issued.
|
|
699
|
-
#
|
|
700
|
-
# When called without an argument, this method does nothing and returns the current beat.
|
|
701
|
-
# For this purpose, this method is equivalent to {#beat}.
|
|
702
|
-
#
|
|
703
|
-
# This method must be called after {#offset}.
|
|
704
|
-
# @param beat [Rational, Integer] the new current beat.
|
|
705
|
-
# @raise [OffsetError] if {#offset} has not been called.
|
|
706
|
-
# @return [Rational] the new current beat.
|
|
707
|
-
# @see #beat
|
|
708
|
-
# @example Set the current beat and inspect it
|
|
709
|
-
# offset 0.1; bpm 120
|
|
710
|
-
# p b! # Outputs 0, this is the initial value
|
|
711
|
-
# p b! 1 # Outputs 1, because it is set to 1
|
|
712
|
-
# p b! 1/2r # Outputs 1/2, because it is set to 1/2
|
|
713
|
-
# p time_at # Outputs 0.35, which is offset + 60s / BPM * beat
|
|
714
|
-
def beat! beat = @current_beat
|
|
715
|
-
raise OffsetError.new __method__ unless @current_beat
|
|
716
|
-
case beat
|
|
717
|
-
when Integer, Rational
|
|
718
|
-
@current_beat = beat.to_r
|
|
719
|
-
when Float
|
|
720
|
-
warn 'float beat is not recommended'
|
|
721
|
-
@current_beat = beat.to_r
|
|
722
|
-
else
|
|
723
|
-
raise ArgumentError, 'invalid beat'
|
|
724
|
-
end
|
|
725
|
-
end
|
|
726
|
-
alias b! beat!
|
|
727
|
-
|
|
728
|
-
# Creates a tap note at the given coordinates with the given text.
|
|
729
|
-
# The coordinates +x+ and +y+ must be numbers.
|
|
730
|
-
# The argument +text+ is the text to be displayed on the note
|
|
731
|
-
# (it is converted to a string via +to_s+ if it is not a string).
|
|
732
|
-
#
|
|
733
|
-
# Technically, this adds an event of type +:tap+ to the chart at the current time
|
|
734
|
-
# with properties containing the information provided by +x+, +y+, and +text+.
|
|
735
|
-
# @param x [Numeric] the x-coordinate of the note.
|
|
736
|
-
# @param y [Numeric] the y-coordinate of the note.
|
|
737
|
-
# @param text [String] the text to be displayed on the note.
|
|
738
|
-
# @return [Event] the event representing the tap note.
|
|
739
|
-
# @raise [ArgumentError] if +x+ or +y+ is not a number.
|
|
740
|
-
# @example
|
|
741
|
-
# offset 0.1; bpm 120
|
|
742
|
-
# t 0, 0, 'Hello'
|
|
743
|
-
# t 50, 0, 'world'
|
|
744
|
-
# # Now there are two simultaneous tap notes at (0, 0) and (50, 0)
|
|
745
|
-
# # with texts 'Hello' and 'world' respectively
|
|
746
|
-
def tap x, y, text = ''
|
|
747
|
-
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
|
748
|
-
raise ArgumentError, 'x and y must be numbers'
|
|
749
|
-
end
|
|
750
|
-
event :tap, x: x.to_f, y: y.to_f, text: text.to_s
|
|
751
|
-
end
|
|
752
|
-
alias t tap
|
|
753
|
-
|
|
754
|
-
# Creates a hold note at the given coordinates with the given duration and text.
|
|
755
|
-
# The coordinates +x+ and +y+ must be numbers.
|
|
756
|
-
# The argument +duration_beats+ is the duration of the hold note in beats.
|
|
757
|
-
# It needs to be a positive Rational or Integer.
|
|
758
|
-
# If it is a Float, it will be converted to a Rational, and a warning will be issued.
|
|
759
|
-
# The argument +text+ is the text to be displayed on the note
|
|
760
|
-
# (it is converted to a string via +to_s+ if it is not a string).
|
|
761
|
-
#
|
|
762
|
-
# Technically, this adds an event of type +:hold+ to the chart at the current time
|
|
763
|
-
# with properties containing the information provided by +x+, +y+, +duration_beats+, and +text+.
|
|
764
|
-
# @param x [Numeric] the x-coordinate of the note.
|
|
765
|
-
# @param y [Numeric] the y-coordinate of the note.
|
|
766
|
-
# @param duration_beats [Rational, Integer] the duration of the hold note in beats.
|
|
767
|
-
# @param text [String] the text to be displayed on the note.
|
|
768
|
-
# @return [Event] the event representing the hold note.
|
|
769
|
-
# @raise [ArgumentError] if +x+, +y+, or +duration_beats+ is not a number,
|
|
770
|
-
# or if +duration_beats+ is not positive.
|
|
771
|
-
# @example
|
|
772
|
-
# offset 0.1; bpm 120
|
|
773
|
-
# h 0, 0, 1, 'Hello'
|
|
774
|
-
# h 50, 0, 2, 'world'
|
|
775
|
-
# # Now there are two hold notes at (0, 0) and (50, 0)
|
|
776
|
-
# # with durations 1 and 2 beats and texts 'Hello' and 'world' respectively
|
|
777
|
-
def hold x, y, duration_beats, text = ''
|
|
778
|
-
if !x.is_a?(Numeric) || !y.is_a?(Numeric) || !duration_beats.is_a?(Numeric)
|
|
779
|
-
raise ArgumentError, 'x, y, and duration must be numbers'
|
|
780
|
-
end
|
|
781
|
-
if duration_beats <= 0
|
|
782
|
-
raise ArgumentError, 'duration must be positive'
|
|
783
|
-
end
|
|
784
|
-
if duration_beats.is_a? Float
|
|
785
|
-
warn 'Rational is recommended over Float for duration_beats'
|
|
786
|
-
end
|
|
787
|
-
event :hold, duration_beats.to_r, x: x.to_f, y: y.to_f, text: text.to_s
|
|
788
|
-
end
|
|
789
|
-
alias h hold
|
|
790
|
-
|
|
791
|
-
# Creates a drag note at the given coordinates.
|
|
792
|
-
# The coordinates +x+ and +y+ must be numbers.
|
|
793
|
-
#
|
|
794
|
-
# Technically, this adds an event of type +:drag+ to the chart at the current time
|
|
795
|
-
# with properties containing the information provided by +x+ and +y+.
|
|
796
|
-
# @param x [Numeric] the x-coordinate of the note.
|
|
797
|
-
# @param y [Numeric] the y-coordinate of the note.
|
|
798
|
-
# @return [Event] the event representing the drag note.
|
|
799
|
-
# @raise [ArgumentError] if +x+ or +y+ is not a number.
|
|
800
|
-
# @example
|
|
801
|
-
# offset 0.1; bpm 120
|
|
802
|
-
# d 0, 0
|
|
803
|
-
# d 50, 0
|
|
804
|
-
# # Now there are two drag notes at (0, 0) and (50, 0)
|
|
805
|
-
def drag x, y
|
|
806
|
-
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
|
807
|
-
raise ArgumentError, 'x and y must be numbers'
|
|
808
|
-
end
|
|
809
|
-
event :drag, x: x.to_f, y: y.to_f
|
|
810
|
-
end
|
|
811
|
-
alias d drag
|
|
812
|
-
|
|
813
|
-
# Creates a flick note at the given coordinates with the given direction and text.
|
|
814
|
-
# The coordinates +x+ and +y+ must be numbers.
|
|
815
|
-
# The argument +direction+ is the direction of the flick note in radians or a symbol.
|
|
816
|
-
# If it is a symbol, it should be one of the keys of {DIRECTIONS}
|
|
817
|
-
# (which are +:right+, +:up_right+, etc., abbreviated as +:r+, +:ur+ etc.).
|
|
818
|
-
# If it is a number, it should be a number representing the angle in radians,
|
|
819
|
-
# specifying the angle rorated anticlockwise starting from the positive x-direction.
|
|
820
|
-
# The argument +text+ is the text to be displayed on the note
|
|
821
|
-
# (it is converted to a string via +to_s+ if it is not a string).
|
|
822
|
-
#
|
|
823
|
-
# Technically, this adds an event of type +:flick+ to the chart at the current time
|
|
824
|
-
# with properties containing the information provided by +x+, +y+, +direction+, and +text+.
|
|
825
|
-
# @param x [Numeric] the x-coordinate of the note.
|
|
826
|
-
# @param y [Numeric] the y-coordinate of the note.
|
|
827
|
-
# @param direction [Numeric, Symbol] the direction of the flick note in radians or a symbol.
|
|
828
|
-
# @param text [String] the text to be displayed on the note.
|
|
829
|
-
# @return [Event] the event representing the flick note.
|
|
830
|
-
# @raise [ArgumentError] if +x+ or +y+ is not a number,
|
|
831
|
-
# if +direction+ is not a symbol or a number,
|
|
832
|
-
# or if the direction is a symbol that does not represent a known direction.
|
|
833
|
-
# @example
|
|
834
|
-
# offset 0.1; bpm 120
|
|
835
|
-
# f 0, 0, :r, 'Hello'
|
|
836
|
-
# f 50, 0, Math::PI / 4, 'world'
|
|
837
|
-
# # Now there are two flick notes at (0, 0) and (50, 0)
|
|
838
|
-
# # with directions right and up right and texts 'Hello' and 'world' respectively
|
|
839
|
-
def flick x, y, direction, text = ''
|
|
840
|
-
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
|
841
|
-
raise ArgumentError, 'x and y must be numbers'
|
|
842
|
-
end
|
|
843
|
-
if direction.is_a? Symbol
|
|
844
|
-
direction = DIRECTIONS[direction]
|
|
845
|
-
raise ArgumentError, "unknown direction #{direction}" unless direction
|
|
846
|
-
elsif direction.is_a? Numeric
|
|
847
|
-
warn 'Are you using degrees as angle unit instead of radians?' if direction != 0 && direction % 45 == 0
|
|
848
|
-
direction = direction.to_f
|
|
849
|
-
else
|
|
850
|
-
raise ArgumentError, 'direction must be a symbol or a number'
|
|
851
|
-
end
|
|
852
|
-
event :flick, x: x.to_f, y: y.to_f, angle: direction, text: text.to_s
|
|
853
|
-
end
|
|
854
|
-
alias f flick
|
|
855
|
-
|
|
856
|
-
# Creates a background note at the given coordinates with the given duration and text.
|
|
857
|
-
# The coordinates +x+ and +y+ must be numbers.
|
|
858
|
-
# The argument +duration_beats+ is the duration of the background note in beats.
|
|
859
|
-
# It needs to be a non-negative Rational or Integer.
|
|
860
|
-
# If it is a Float, it will be converted to a Rational, and a warning will be issued.
|
|
861
|
-
# The argument +text+ is the text to be displayed on the note
|
|
862
|
-
# (it is converted to a string via +to_s+ if it is not a string).
|
|
863
|
-
#
|
|
864
|
-
# Both the +duration_beats+ and the +text+ arguments are optional.
|
|
865
|
-
# When there are three arguments given in total,
|
|
866
|
-
# the method determines whether the third is +duration_beats+ or +text+ based on its type.
|
|
867
|
-
#
|
|
868
|
-
# Technically, this adds an event of type +:bg_note+ to the chart at the current time
|
|
869
|
-
# with properties containing the information provided by +x+, +y+, +duration_beats+, and +text+.
|
|
870
|
-
# @param x [Numeric] the x-coordinate of the note.
|
|
871
|
-
# @param y [Numeric] the y-coordinate of the note.
|
|
872
|
-
# @param duration_beats [Rational, Integer] the duration of the background note in beats.
|
|
873
|
-
# @param text [String] the text to be displayed on the note.
|
|
874
|
-
# @return [Event] the event representing the background note.
|
|
875
|
-
# @raise [ArgumentError] if +x+, +y+, or +duration_beats+ is not a number,
|
|
876
|
-
# or if +duration_beats+ is negative.
|
|
877
|
-
# @example
|
|
878
|
-
# offset 0.1; bpm 120
|
|
879
|
-
# bg_note 0, 0, 1, 'Hello' # duration is 1, text is 'Hello'
|
|
880
|
-
# bg_note 50, 0, 'world' # duration is 0, text is 'world'
|
|
881
|
-
# bg_note -50, 0, 2 # duration is 2, text is ''
|
|
882
|
-
def bg_note x, y, duration_beats = 0, text = nil
|
|
883
|
-
if text.nil?
|
|
884
|
-
if duration_beats.is_a? String
|
|
885
|
-
text = duration_beats
|
|
886
|
-
duration_beats = 0
|
|
887
|
-
else
|
|
888
|
-
text = ''
|
|
889
|
-
end
|
|
890
|
-
end
|
|
891
|
-
if !x.is_a?(Numeric) || !y.is_a?(Numeric) || !duration_beats.is_a?(Numeric)
|
|
892
|
-
raise ArgumentError, 'x, y, and duration_beats must be numbers'
|
|
893
|
-
end
|
|
894
|
-
if duration_beats < 0
|
|
895
|
-
raise ArgumentError, 'duration must be non-negative'
|
|
896
|
-
end
|
|
897
|
-
if duration_beats.is_a? Float
|
|
898
|
-
warn 'Rational is recommended over Float for duration_beats'
|
|
899
|
-
end
|
|
900
|
-
event :bg_note, duration_beats.to_r, x: x.to_f, y: y.to_f, text: text.to_s
|
|
901
|
-
end
|
|
902
|
-
|
|
903
|
-
# Creates a big text.
|
|
904
|
-
# The argument +duration_beats+ is the duration of the big text in beats.
|
|
905
|
-
# It needs to be a non-negative Rational or Integer.
|
|
906
|
-
# If it is a Float, it will be converted to a Rational, and a warning will be issued.
|
|
907
|
-
# The argument +text+ is the text to be displayed.
|
|
908
|
-
#
|
|
909
|
-
# Technically, this adds an event of type +:big_text+ to the chart at the current time
|
|
910
|
-
# with properties containing the information provided by +duration_beats+ and +text+.
|
|
911
|
-
# @param duration_beats [Rational, Integer] the duration of the big text in beats.
|
|
912
|
-
# @param text [String] the text to be displayed.
|
|
913
|
-
# @return [Event] the event representing the big text.
|
|
914
|
-
# @raise [ArgumentError] if +duration_beats+ is not a number or is negative.
|
|
915
|
-
# @example
|
|
916
|
-
# offset 0.1; bpm 120
|
|
917
|
-
# big_text 1, 'Hello, world!' # duration is 1, text is 'Hello, world!'
|
|
918
|
-
# b 1
|
|
919
|
-
# big_text 'Goodbye!' # duration is 0, text is 'Goodbye!'
|
|
920
|
-
def big_text duration_beats = 0, text
|
|
921
|
-
unless duration_beats.is_a? Numeric
|
|
922
|
-
raise ArgumentError, 'duration_beats must be a number'
|
|
923
|
-
end
|
|
924
|
-
if duration_beats < 0
|
|
925
|
-
raise ArgumentError, 'duration must be non-negative'
|
|
926
|
-
end
|
|
927
|
-
if duration_beats.is_a? Float
|
|
928
|
-
warn 'Rational is recommended over Float for duration_beats'
|
|
929
|
-
end
|
|
930
|
-
event :big_text, duration_beats.to_r, text: text.to_s
|
|
931
|
-
end
|
|
932
|
-
|
|
933
|
-
# @!macro [attach] bg_pattern
|
|
934
|
-
# @!method $1(duration_beats = 0)
|
|
935
|
-
# Creates a $2 background pattern.
|
|
936
|
-
# The argument +duration_beats+ is the duration of the background pattern in beats.
|
|
937
|
-
# It needs to be a non-negative Rational or Integer.
|
|
938
|
-
# If it is a Float, it will be converted to a Rational, and a warning will be issued.
|
|
939
|
-
#
|
|
940
|
-
# Technically, this adds an event of type +:bg_pattern+ to the chart at the current time
|
|
941
|
-
# with properties containing the information provided by +duration_beats+.
|
|
942
|
-
# @param duration_beats [Rational, Integer] the duration of the background pattern in beats.
|
|
943
|
-
# @return [Event] the event representing the background pattern.
|
|
944
|
-
# @raise [ArgumentError] if +duration_beats+ is not a number or is negative.
|
|
945
|
-
# @example
|
|
946
|
-
# offset 0.1; bpm 120
|
|
947
|
-
# $1 1 # duration is 1
|
|
948
|
-
# b 1
|
|
949
|
-
# $1 0 # duration is 0
|
|
950
|
-
# @!parse bg_pattern :grid, 'grid'
|
|
951
|
-
# @!parse bg_pattern :hexagon, 'hexagon'
|
|
952
|
-
# @!parse bg_pattern :checkerboard, 'checkerboard'
|
|
953
|
-
# @!parse bg_pattern :diamond_grid, 'diamond grid'
|
|
954
|
-
# @!parse bg_pattern :pentagon, 'pentagon'
|
|
955
|
-
# @!parse bg_pattern :turntable, 'turntable'
|
|
956
|
-
# @!parse bg_pattern :hexagram, 'hexagram'
|
|
957
|
-
%i[grid hexagon checkerboard diamond_grid pentagon turntable hexagram].each do |method_name|
|
|
958
|
-
define_method method_name do |duration_beats = 0|
|
|
959
|
-
unless duration_beats.is_a? Numeric
|
|
960
|
-
raise ArgumentError, 'duration_beats must be a number'
|
|
961
|
-
end
|
|
962
|
-
if duration_beats < 0
|
|
963
|
-
raise ArgumentError, 'duration must be non-negative'
|
|
964
|
-
end
|
|
965
|
-
if duration_beats.is_a? Float
|
|
966
|
-
warn 'Rational is recommended over Float for duration_beats'
|
|
967
|
-
end
|
|
968
|
-
event method_name, duration_beats.to_r
|
|
969
|
-
end
|
|
970
|
-
end
|
|
971
|
-
|
|
972
|
-
# Duplicate all events in a given array.
|
|
973
|
-
# This method is useful when you want to duplicate a set of events.
|
|
974
|
-
# The argument +events+ is an array of events to be duplicated.
|
|
975
|
-
# The argument +new_tip_points+ is a boolean indicating whether to create new tip points.
|
|
976
|
-
# If it is +true+, new tip points will be created for the duplicated events.
|
|
977
|
-
# If it is +false+, each duplicated event shares the same tip point as the original event.
|
|
978
|
-
# @param events [Array<Event>] the events to be duplicated.
|
|
979
|
-
# @param new_tip_points [Boolean] whether to create new tip points for the duplicated events.
|
|
980
|
-
# @return [Array<Event>] the duplicated events.
|
|
981
|
-
# @example Duplicate a note
|
|
982
|
-
# offset 0.1; bpm 120
|
|
983
|
-
# duplicate [t 0, 0]
|
|
984
|
-
# @example Duplicate notes that share tip points with the original notes
|
|
985
|
-
# offset 0.1; bpm 120
|
|
986
|
-
# duplicate tp_chain(0, 100, 1) { t 0, 0 }
|
|
987
|
-
def duplicate events, new_tip_points: true
|
|
988
|
-
result = []
|
|
989
|
-
events.each do |event|
|
|
990
|
-
next if event.type == :placeholder && !new_tip_points
|
|
991
|
-
result.push event = event.dup
|
|
992
|
-
if event[:tip_point] && new_tip_points
|
|
993
|
-
event[:tip_point] = "#@current_duplicate #{event[:tip_point]}"
|
|
994
|
-
end
|
|
995
|
-
@groups.each { _1.push event }
|
|
996
|
-
end
|
|
997
|
-
@current_duplicate += 1 if new_tip_points
|
|
998
|
-
result
|
|
999
|
-
end
|
|
1000
|
-
|
|
1001
|
-
# Transform all events in a given array in time and/or space.
|
|
1002
|
-
# Space transformation does not affect background patterns.
|
|
1003
|
-
def transform events, &block
|
|
1004
|
-
raise ArgumentError, 'no block given' unless block
|
|
1005
|
-
events = [events] if events.is_a? Event
|
|
1006
|
-
transform = Transform.new
|
|
1007
|
-
transform.instance_eval &block
|
|
1008
|
-
events.each { transform.apply _1 }
|
|
1009
|
-
end
|
|
1010
|
-
|
|
1011
|
-
def group preserve_beat: true, &block
|
|
1012
|
-
raise ArgumentError, 'no block given' unless block
|
|
1013
|
-
@groups.push result = []
|
|
1014
|
-
beat_backup = backup_beat unless preserve_beat
|
|
1015
|
-
instance_eval &block
|
|
1016
|
-
restore_beat beat_backup unless preserve_beat
|
|
1017
|
-
@groups.delete_if { result.equal? _1 }
|
|
1018
|
-
result
|
|
1019
|
-
end
|
|
1020
|
-
|
|
1021
|
-
def remove *events
|
|
1022
|
-
events.each { |event| @groups.each { _1.delete event } }
|
|
1023
|
-
end
|
|
1024
|
-
|
|
1025
|
-
%i[chain drop none].each do |mode|
|
|
1026
|
-
define_method "tip_point_#{mode}" do |*args, **opts, &block|
|
|
1027
|
-
tip_point mode, *args, **opts, &block
|
|
1028
|
-
end
|
|
1029
|
-
alias_method "tp_#{mode}", "tip_point_#{mode}"
|
|
1030
|
-
end
|
|
1031
|
-
|
|
1032
|
-
def mark name
|
|
1033
|
-
@bookmarks[name] = backup_state
|
|
1034
|
-
name
|
|
1035
|
-
end
|
|
1036
|
-
|
|
1037
|
-
def at name, preserve_beat: false, update_mark: false, &block
|
|
1038
|
-
raise ArgumentError, 'no block given' unless block
|
|
1039
|
-
raise ArgumentError, "unknown bookmark #{name}" unless bookmark = @bookmarks[name]
|
|
1040
|
-
backup = backup_state
|
|
1041
|
-
restore_state bookmark
|
|
1042
|
-
result = group &block
|
|
1043
|
-
mark name if update_mark
|
|
1044
|
-
beat_backup = backup_beat if preserve_beat
|
|
1045
|
-
restore_state backup
|
|
1046
|
-
restore_beat beat_backup if preserve_beat
|
|
1047
|
-
result
|
|
1048
|
-
end
|
|
1049
|
-
|
|
1050
|
-
def check(
|
|
1051
|
-
notes_in_bound: true,
|
|
1052
|
-
bg_notes_in_bound: true
|
|
1053
|
-
)
|
|
1054
|
-
out_of_bound_events = [] if notes_in_bound || bg_notes_in_bound
|
|
1055
|
-
@events.each do |event|
|
|
1056
|
-
if %i[tap hold drag flick].include?(event.type) && notes_in_bound || event.type == :bg_note && bg_notes_in_bound
|
|
1057
|
-
if event[:x] < -100-1e-10 || event[:x] > 100+1e-10 || event[:y] < -50-1e-10 || event[:y] > 50+1e-10
|
|
1058
|
-
out_of_bound_events.push event
|
|
1059
|
-
end
|
|
1060
|
-
end
|
|
1061
|
-
end
|
|
1062
|
-
if notes_in_bound || bg_notes_in_bound
|
|
1063
|
-
if out_of_bound_events.empty?
|
|
1064
|
-
puts "===== All notes are in bound ====="
|
|
1065
|
-
else
|
|
1066
|
-
puts "===== Out-of-bound notes ====="
|
|
1067
|
-
out_of_bound_events.each do |event|
|
|
1068
|
-
p event
|
|
1069
|
-
puts "at time #{event.time}"
|
|
1070
|
-
puts 'defined at:'
|
|
1071
|
-
puts event.backtrace
|
|
1072
|
-
end
|
|
1073
|
-
end
|
|
1074
|
-
end
|
|
1075
|
-
end
|
|
1076
|
-
|
|
1077
|
-
# @!endgroup
|
|
1078
|
-
|
|
1079
|
-
end
|
|
6
|
+
require_relative 'sscharter/tools'
|
|
7
|
+
require_relative 'sscharter/charter'
|