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
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
class Sunniesnow::Charter
|
|
6
|
+
|
|
7
|
+
using Sunniesnow::Utils
|
|
8
|
+
|
|
9
|
+
class Event
|
|
10
|
+
include Metronomic
|
|
11
|
+
|
|
12
|
+
# The types ({#type}) of events that
|
|
13
|
+
# should be considered possibly visited by a tip point from the charting perspective.
|
|
14
|
+
# They affect {#tip_pointable?} and suggests whether this event should be visited by a tip point
|
|
15
|
+
# if it is created inside blocks in {Charter#tip_point_chain} and {Charter#tip_point_drop}.
|
|
16
|
+
# For this reason, although placeholder events can be visited by a tip point from a technical perspective,
|
|
17
|
+
# it is not included in this list.
|
|
18
|
+
TIP_POINTABLE_TYPES = %i[tap hold flick drag drag_flick].freeze
|
|
19
|
+
|
|
20
|
+
TIP_POINTABLE_TYPES_SET = TIP_POINTABLE_TYPES.to_set.freeze
|
|
21
|
+
|
|
22
|
+
# @return [Symbol]
|
|
23
|
+
attr_accessor :type
|
|
24
|
+
|
|
25
|
+
# @return [Hash{Symbol => Numeric, String}]
|
|
26
|
+
attr_accessor :properties
|
|
27
|
+
|
|
28
|
+
# @return [TimeDependent]
|
|
29
|
+
attr_accessor :time_dependent
|
|
30
|
+
|
|
31
|
+
# @return [Array<String>]
|
|
32
|
+
attr_reader :backtrace
|
|
33
|
+
|
|
34
|
+
# @param type [Symbol]
|
|
35
|
+
# @param beat [Rational]
|
|
36
|
+
# @param duration_beats [Rational?]
|
|
37
|
+
# @param bpm_changes [BpmChangeList]
|
|
38
|
+
# @param properties [Hash{Symbol => Numeric, String}]
|
|
39
|
+
def initialize type, beat, duration_beats = nil, bpm_changes, **properties
|
|
40
|
+
@beat = beat
|
|
41
|
+
@duration_beats = duration_beats
|
|
42
|
+
@type = type
|
|
43
|
+
@bpm_changes = bpm_changes
|
|
44
|
+
@properties = properties
|
|
45
|
+
@time_dependent = TimeDependent.new beat, bpm_changes
|
|
46
|
+
@offset = 0.0
|
|
47
|
+
@backtrace = caller.filter { _1.sub! /^#{PROJECT_DIR}\//, '' }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Numeric, String]
|
|
51
|
+
# @param key [Symbol]
|
|
52
|
+
def [] key
|
|
53
|
+
@properties[key]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Numeric, String]
|
|
57
|
+
# @param key [Symbol]
|
|
58
|
+
# @param value [Numeric, String]
|
|
59
|
+
def []= key, value
|
|
60
|
+
@properties[key] = value
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Sunniesnow::Event]
|
|
64
|
+
def to_sunniesnow
|
|
65
|
+
t = time
|
|
66
|
+
properties = @properties.transform_keys &:snake_to_camel
|
|
67
|
+
properties[:duration] = end_time - t if @duration_beats
|
|
68
|
+
result = Sunniesnow::Event.new t, @type.snake_to_camel, **properties
|
|
69
|
+
result.time_dependent = @time_dependent.to_sunniesnow unless @time_dependent.empty?
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Event]
|
|
74
|
+
# TODO: use +initialize_copy+.
|
|
75
|
+
def dup
|
|
76
|
+
result = super
|
|
77
|
+
result.properties = @properties.dup
|
|
78
|
+
result.time_dependent = @time_dependent.dup
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Suggests whether this event should be visited by a tip point
|
|
83
|
+
# if it is created inside blocks in {Charter#tip_point_chain} and {Charter#tip_point_drop}.
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def tip_pointable?
|
|
86
|
+
TIP_POINTABLE_TYPES_SET.include? @type
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [String]
|
|
90
|
+
def inspect
|
|
91
|
+
"#<#@type at #@beat#{@duration_beats && " for #@duration_beats"} offset #@offset: " +
|
|
92
|
+
@properties.map { |k, v| "#{k}=#{v.inspect}" }.join(', ') + '>'
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class TimeDependent
|
|
97
|
+
class DataPoint
|
|
98
|
+
include Metronomic
|
|
99
|
+
|
|
100
|
+
# @return [Float, String]
|
|
101
|
+
attr_accessor :value
|
|
102
|
+
|
|
103
|
+
# @param beat [Rational]
|
|
104
|
+
# @param bpm_changes [BpmChangeList]
|
|
105
|
+
# @param value [Float, String]
|
|
106
|
+
def initialize beat, bpm_changes, value
|
|
107
|
+
@offset = 0.0
|
|
108
|
+
@beat = beat
|
|
109
|
+
@bpm_changes = bpm_changes
|
|
110
|
+
@value = value
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @return [Hash]
|
|
114
|
+
def to_sunniesnow
|
|
115
|
+
{time: time, value: @value}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class PiecewiseData
|
|
120
|
+
include Metronomic
|
|
121
|
+
|
|
122
|
+
# @return [Array<DataPoint>]
|
|
123
|
+
attr_accessor :data_points
|
|
124
|
+
|
|
125
|
+
# @param beat [Rational]
|
|
126
|
+
# @param bpm_changes [BpmChangeList]
|
|
127
|
+
def initialize beat, bpm_changes
|
|
128
|
+
@offset = 0.0
|
|
129
|
+
@beat = beat
|
|
130
|
+
@bpm_changes = bpm_changes
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @param beat [Rational]
|
|
134
|
+
# @param value [Float, String]
|
|
135
|
+
# @return [DataPoint]
|
|
136
|
+
def data_point beat, value
|
|
137
|
+
DataPoint.new(beat, @bpm_changes, value).tap { (@data_points ||= []).push _1 }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
def interpolable?
|
|
142
|
+
raise NotImplementedError
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @return [PiecewiseData]
|
|
146
|
+
# TODO: use +initialize_copy+.
|
|
147
|
+
def dup
|
|
148
|
+
super.tap { _1.data_points = @data_points&.map &:dup }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @return [Hash]
|
|
152
|
+
def to_sunniesnow
|
|
153
|
+
return {} unless @data_points
|
|
154
|
+
data_points = @data_points.map &:to_sunniesnow
|
|
155
|
+
data_points.sort_by! { _1[:time] }
|
|
156
|
+
data_points.each { _1[:time] += @offset }
|
|
157
|
+
{dataPoints: data_points}
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Represents a piecewise linear function defined by some data points.
|
|
162
|
+
class InterpolablePiecewiseData < PiecewiseData
|
|
163
|
+
|
|
164
|
+
# @return [Float]
|
|
165
|
+
attr_accessor :speed
|
|
166
|
+
|
|
167
|
+
# @return [Float]
|
|
168
|
+
attr_accessor :beat_speed
|
|
169
|
+
|
|
170
|
+
# @param value [Float]
|
|
171
|
+
def speed= value
|
|
172
|
+
@speed = value
|
|
173
|
+
@beat_speed = nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# @param value [Float]
|
|
177
|
+
def beat_speed= value
|
|
178
|
+
@beat_speed = value
|
|
179
|
+
@speed = nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @return [true]
|
|
183
|
+
def interpolable?
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [Hash]
|
|
188
|
+
def to_sunniesnow
|
|
189
|
+
speed = @speed || @beat_speed && @bpm_changes.bps_before(@data_points&.min_by(&:beat)&.beat || @beat) * @beat_speed
|
|
190
|
+
super.tap { _1[:speed] = speed if speed }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Represents a piecewise constant function defined by some data points.
|
|
195
|
+
class UninterpolablePiecewiseData < PiecewiseData
|
|
196
|
+
# @return [Integer, Float, String]
|
|
197
|
+
attr_accessor :value
|
|
198
|
+
|
|
199
|
+
# @return [false]
|
|
200
|
+
def interpolable?
|
|
201
|
+
false
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @return [Hash]
|
|
205
|
+
def to_sunniesnow
|
|
206
|
+
super.tap { _1[:value] = @value if @value }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
include BeatSeries
|
|
211
|
+
|
|
212
|
+
# @return [Hash{INTERPOLABLE_SET, UNINTERPOLABLE_SET => PiecewiseData}]
|
|
213
|
+
attr_accessor :data
|
|
214
|
+
|
|
215
|
+
# @param beat [Rational]
|
|
216
|
+
# @param bpm_changes [BpmChangeList]
|
|
217
|
+
def initialize beat, bpm_changes
|
|
218
|
+
@current_beat = beat
|
|
219
|
+
@bpm_changes = bpm_changes
|
|
220
|
+
@data = Hash.new { |h, k| h[k] = (INTERPOLABLE_SET.include?(k) ? InterpolablePiecewiseData : UninterpolablePiecewiseData).new beat, bpm_changes }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @param key [Symbol]
|
|
224
|
+
# @return [PiecewiseData?]
|
|
225
|
+
def [] key
|
|
226
|
+
@data.has_key?(key) ? @data[key] : nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
INTERPOLABLE = %i[
|
|
230
|
+
x y opacity size scale_x scale_y skew_x skew_y rotation text tint_red tint_green tint_blue
|
|
231
|
+
circle_opacity circle_scale_x circle_scale_y circle_skew_x circle_skew_y circle_rotation
|
|
232
|
+
circle_tint_red circle_tint_green circle_tint_blue
|
|
233
|
+
width height anchor_x anchor_y
|
|
234
|
+
].freeze
|
|
235
|
+
|
|
236
|
+
INTERPOLABLE_SET = INTERPOLABLE.to_set.freeze
|
|
237
|
+
|
|
238
|
+
UNINTERPOLABLE = %i[z blend_mode circle_blend_mode text].freeze
|
|
239
|
+
|
|
240
|
+
UNINTERPOLABLE_SET = UNINTERPOLABLE.to_set.freeze
|
|
241
|
+
|
|
242
|
+
BLEND_MODES = %i[
|
|
243
|
+
normal add multiply screen darken lighten erase color_dodge color_burn linear_burn linear_dodge
|
|
244
|
+
linear_light hard_light soft_light pin_light difference exclusion overlay saturation color luminosity
|
|
245
|
+
normal_npm add_npm screen_npm none subtract divide vivid_light hard_mix negation min max
|
|
246
|
+
].freeze
|
|
247
|
+
|
|
248
|
+
BLEND_MODES_SET = BLEND_MODES.to_set
|
|
249
|
+
|
|
250
|
+
UNINTERPOLABLE_TYPES = {
|
|
251
|
+
blend_mode: BLEND_MODES_SET,
|
|
252
|
+
circle_blend_mode: BLEND_MODES_SET,
|
|
253
|
+
text: String
|
|
254
|
+
}.tap { _1.default = Numeric }.freeze
|
|
255
|
+
|
|
256
|
+
# @!group DSL Methods
|
|
257
|
+
|
|
258
|
+
# @!parse
|
|
259
|
+
# # @!macro [attach] interpolable_property
|
|
260
|
+
# # @!method $1 data_value = nil, speed: nil, s: nil
|
|
261
|
+
# # @return [DataPoint, Float]
|
|
262
|
+
# # @overload $1 data_value
|
|
263
|
+
# # @param data_value [Numeric]
|
|
264
|
+
# # @return [DataPoint]
|
|
265
|
+
# # @overload $1 speed:
|
|
266
|
+
# # @param speed [Numeric]
|
|
267
|
+
# # @return [Float]
|
|
268
|
+
# # @overload $1 s:
|
|
269
|
+
# # This overload is the same as the +speed:+ overload, but with a shorter name for convenience.
|
|
270
|
+
# # @param s [Numeric]
|
|
271
|
+
# # @return [Float]
|
|
272
|
+
# interpolable_property x
|
|
273
|
+
# interpolable_property y
|
|
274
|
+
# interpolable_property opacity
|
|
275
|
+
# interpolable_property size
|
|
276
|
+
# interpolable_property scale_x
|
|
277
|
+
# interpolable_property scale_y
|
|
278
|
+
# interpolable_property skew_x
|
|
279
|
+
# interpolable_property skew_y
|
|
280
|
+
# interpolable_property rotation
|
|
281
|
+
# interpolable_property text
|
|
282
|
+
# interpolable_property tint_red
|
|
283
|
+
# interpolable_property tint_green
|
|
284
|
+
# interpolable_property tint_blue
|
|
285
|
+
# interpolable_property circle_opacity
|
|
286
|
+
# interpolable_property circle_scale_x
|
|
287
|
+
# interpolable_property circle_scale_y
|
|
288
|
+
# interpolable_property circle_skew_x
|
|
289
|
+
# interpolable_property circle_skew_y
|
|
290
|
+
# interpolable_property circle_rotation
|
|
291
|
+
# interpolable_property circle_tint_red
|
|
292
|
+
# interpolable_property circle_tint_green
|
|
293
|
+
# interpolable_property circle_tint_blue
|
|
294
|
+
# interpolable_property width
|
|
295
|
+
# interpolable_property height
|
|
296
|
+
# interpolable_property anchor_x
|
|
297
|
+
# interpolable_property anchor_y
|
|
298
|
+
INTERPOLABLE.each do |property|
|
|
299
|
+
define_method property do |data_value = nil, speed: nil, s: nil|
|
|
300
|
+
raise ArgumentError, 'cannot specify both speed and s' if !speed.nil? && !s.nil?
|
|
301
|
+
speed = s if speed.nil?
|
|
302
|
+
raise ArgumentError, 'must specify one and only one of data_value and speed' if [data_value, speed].compact.size != 1
|
|
303
|
+
if !data_value.nil?
|
|
304
|
+
raise ArgumentError, "#{property} must be a number" unless data_value.is_a? Numeric
|
|
305
|
+
@data[property].data_point @current_beat, data_value.to_f
|
|
306
|
+
else
|
|
307
|
+
raise ArgumentError, "speed must be a number" unless speed.is_a? Numeric
|
|
308
|
+
@data[property].speed = speed.to_f
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# @!parse
|
|
314
|
+
# # @!macro [attach] uninterpolable_property
|
|
315
|
+
# # @!method $1 data_value = nil, value: nil, v: nil
|
|
316
|
+
# # @return [DataPoint, $2]
|
|
317
|
+
# # @overload $1 data_value
|
|
318
|
+
# # @param data_value [$2]
|
|
319
|
+
# # @return [DataPoint]
|
|
320
|
+
# # @overload $1 value:
|
|
321
|
+
# # @param value [$2]
|
|
322
|
+
# # @return [$2]
|
|
323
|
+
# # @overload $1 v:
|
|
324
|
+
# # This overload is the same as the +value:+ overload, but with a shorter name for convenience.
|
|
325
|
+
# # @param v [$2]
|
|
326
|
+
# # @return [$2]
|
|
327
|
+
# uninterpolable_property z, Numeric
|
|
328
|
+
# uninterpolable_property blend_mode, BLEND_MODES_SET
|
|
329
|
+
# uninterpolable_property circle_blend_mode, BLEND_MODES_SET
|
|
330
|
+
# uninterpolable_property text, String
|
|
331
|
+
UNINTERPOLABLE.each do |property|
|
|
332
|
+
define_method property do |data_value = nil, value: nil, v: nil|
|
|
333
|
+
raise ArgumentError, "cannot specify both value and v" if !value.nil? && !v.nil?
|
|
334
|
+
value = v if value.nil?
|
|
335
|
+
raise ArgumentError, "must specify one and only one of data_value and value" if [data_value, value].compact.size != 1
|
|
336
|
+
if !data_value.nil?
|
|
337
|
+
raise ArgumentError, "wrong data type for data_value of #{property}" unless UNINTERPOLABLE_TYPES[property] === data_value
|
|
338
|
+
@data[property].data_point @current_beat, data_value
|
|
339
|
+
else
|
|
340
|
+
raise ArgumentError, "wrong data type for value of #{property}" unless UNINTERPOLABLE_TYPES[property] === value
|
|
341
|
+
@data[property].value = value
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# @!endgroup
|
|
347
|
+
|
|
348
|
+
# @return [Boolean]
|
|
349
|
+
def empty?
|
|
350
|
+
@data.empty?
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# @return [TimeDependent]
|
|
354
|
+
# TODO: use +initialize_copy+.
|
|
355
|
+
def dup
|
|
356
|
+
result = super
|
|
357
|
+
result.data = @data.transform_values { _1.dup }
|
|
358
|
+
result.data.default_proc = @data.default_proc
|
|
359
|
+
result
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# @return [Hash]
|
|
363
|
+
def to_sunniesnow
|
|
364
|
+
result = @data.transform_keys &:snake_to_camel
|
|
365
|
+
result.transform_values! &:to_sunniesnow
|
|
366
|
+
%i[blendMode circleBlendMode].each do |key|
|
|
367
|
+
next unless result.has_key? key
|
|
368
|
+
result[key][:dataPoints]&.each { _1[:value] = _1[:value].to_s.tr ?_, ?- }
|
|
369
|
+
result[key][:value] = result[key][:value].to_s.tr ?_, ?- if result[key][:value]
|
|
370
|
+
end
|
|
371
|
+
result
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# An array of events.
|
|
376
|
+
# @return [Array<Event>]
|
|
377
|
+
attr_reader :events
|
|
378
|
+
|
|
379
|
+
# @note Internal API.
|
|
380
|
+
# @param type [Symbol]
|
|
381
|
+
# @param duration_beats [Integer, Rational, nil]
|
|
382
|
+
# @param properties [Hash{Symbol => Numeric, String}]
|
|
383
|
+
# @return [Event]
|
|
384
|
+
def event type, duration_beats = nil, **properties
|
|
385
|
+
raise OffsetError.new __method__ unless @bpm_changes
|
|
386
|
+
event = Event.new type, @current_beat, duration_beats, @bpm_changes, **properties
|
|
387
|
+
@groups.each { _1.push event }
|
|
388
|
+
return event unless event.tip_pointable?
|
|
389
|
+
case @tip_point_mode_stack.last
|
|
390
|
+
when :chain
|
|
391
|
+
if @tip_point_start_to_add_stack.last
|
|
392
|
+
@current_tip_point_stack[-1] = @tip_point_peak
|
|
393
|
+
@tip_point_peak += 1
|
|
394
|
+
end
|
|
395
|
+
push_tip_point_start event
|
|
396
|
+
@tip_point_start_to_add_stack[-1] = nil
|
|
397
|
+
when :drop
|
|
398
|
+
@current_tip_point_stack[-1] = @tip_point_peak
|
|
399
|
+
@tip_point_peak += 1
|
|
400
|
+
push_tip_point_start event
|
|
401
|
+
when :none
|
|
402
|
+
# pass
|
|
403
|
+
end
|
|
404
|
+
event
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# @!group DSL Methods
|
|
408
|
+
|
|
409
|
+
# @yieldself [TimeDependent]
|
|
410
|
+
# @param events [Array<Event>, Event]
|
|
411
|
+
# @param goto_beat [Boolean]
|
|
412
|
+
# @param preserve_beat [Boolean]
|
|
413
|
+
# @return [Array<Event>]
|
|
414
|
+
def time_dependent events, goto_beat: true, preserve_beat: false, &block
|
|
415
|
+
raise ArgumentError, 'no block given' unless block
|
|
416
|
+
events = [events] if events.is_a? Event
|
|
417
|
+
beat_backup = current_beat_state if !goto_beat || !preserve_beat
|
|
418
|
+
events.each do |event|
|
|
419
|
+
event.time_dependent.restore_beat_state goto_beat ? event.beat_state : beat_backup
|
|
420
|
+
event.time_dependent.instance_eval &block
|
|
421
|
+
end
|
|
422
|
+
restore_beat_state preserve_beat ? events.last.time_dependent.current_beat_state : beat_backup
|
|
423
|
+
events
|
|
424
|
+
end
|
|
425
|
+
alias td time_dependent
|
|
426
|
+
|
|
427
|
+
# @!endgroup
|
|
428
|
+
|
|
429
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Sunniesnow::Charter
|
|
4
|
+
|
|
5
|
+
# Implements homography.
|
|
6
|
+
class Transform
|
|
7
|
+
include Math
|
|
8
|
+
|
|
9
|
+
# @return [Float]
|
|
10
|
+
attr_reader :xx, :xy, :xz, :yx, :yy, :yz, :zx, :zy, :zz
|
|
11
|
+
|
|
12
|
+
# @return [Rational]
|
|
13
|
+
attr_reader :tt, :t1
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@xx = @yy = @zz = 1.0
|
|
17
|
+
@xy = @xz = @yx = @yz = @zx = @zy = 0.0
|
|
18
|
+
@t1 = 0r
|
|
19
|
+
@tt = 1r
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param event [Event]
|
|
23
|
+
# @return [Event] same as +event+.
|
|
24
|
+
def apply event
|
|
25
|
+
event.beat = @t1 + @tt * event.beat
|
|
26
|
+
return unless x = event[:x]
|
|
27
|
+
return unless y = event[:y]
|
|
28
|
+
rx = xx*x + xy*y + xz
|
|
29
|
+
ry = yx*x + yy*y + yz
|
|
30
|
+
d = zx*x + zy*y + zz
|
|
31
|
+
event[:x] = xp = rx / d
|
|
32
|
+
event[:y] = yp = ry / d
|
|
33
|
+
|
|
34
|
+
return event unless angle = event[:angle]
|
|
35
|
+
dx = cos angle
|
|
36
|
+
dy = sin angle
|
|
37
|
+
cross = y*dx - x*dy
|
|
38
|
+
|
|
39
|
+
cx0 = zy*xx - xy*zx
|
|
40
|
+
cxx = zz*xx - xz*zx
|
|
41
|
+
cxy = zz*xy - xz*zy
|
|
42
|
+
dxp = cx0*cross + cxx*dx + cxy*dy
|
|
43
|
+
|
|
44
|
+
cy0 = zx*yy - yx*zy
|
|
45
|
+
cyy = zz*yy - yz*zy
|
|
46
|
+
cyx = zz*yx - yz*zx
|
|
47
|
+
dyp = cy0*-cross + cyy*dy + cyx*dx
|
|
48
|
+
|
|
49
|
+
event[:angle] = atan2 dyp, dxp
|
|
50
|
+
event
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @!group DSL Methods
|
|
54
|
+
|
|
55
|
+
# @param xx [Numeric]
|
|
56
|
+
# @param xy [Numeric]
|
|
57
|
+
# @param yx [Numeric]
|
|
58
|
+
# @param yy [Numeric]
|
|
59
|
+
# @return [void]
|
|
60
|
+
def compound_linear xx, xy, yx, yy
|
|
61
|
+
raise ArgumentError, 'arguments must be numbers' unless [xx, xy, yx, yy].all? { _1.is_a? Numeric }
|
|
62
|
+
@xx, @xy, @xz, @yx, @yy, @yz = [
|
|
63
|
+
xx * @xx + xy * @yx,
|
|
64
|
+
xx * @xy + xy * @yy,
|
|
65
|
+
xx * @xz + xy * @yz,
|
|
66
|
+
yx * @xx + yy * @yx,
|
|
67
|
+
yx * @xy + yy * @yy,
|
|
68
|
+
yx * @xz + yy * @yz,
|
|
69
|
+
]
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @param dx [Numeric]
|
|
74
|
+
# @param dy [Numeric]
|
|
75
|
+
# @return [void]
|
|
76
|
+
def translate dx, dy
|
|
77
|
+
raise ArgumentError, 'dx and dy must be numbers' unless dx.is_a?(Numeric) && dy.is_a?(Numeric)
|
|
78
|
+
@xz += dx
|
|
79
|
+
@yz += dy
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [void]
|
|
83
|
+
def horizontal_flip
|
|
84
|
+
compound_linear -1, 0, 0, 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [void]
|
|
88
|
+
def vertical_flip
|
|
89
|
+
compound_linear 1, 0, 0, -1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param angle [Numeric] in radians.
|
|
93
|
+
# @return [void]
|
|
94
|
+
def rotate angle
|
|
95
|
+
raise ArgumentError, 'angle must be a number' unless angle.is_a? Numeric
|
|
96
|
+
warn 'Are you using degrees as angle unit instead of radians?' if angle != 0 && angle % 45 == 0
|
|
97
|
+
c = cos angle
|
|
98
|
+
s = sin angle
|
|
99
|
+
compound_linear c, -s, s, c
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param sx [Numeric]
|
|
103
|
+
# @param sy [Numeric]
|
|
104
|
+
# @return [void]
|
|
105
|
+
def scale sx, sy = sx
|
|
106
|
+
raise ArgumentError, 'sx and sy must be numbers' unless sx.is_a?(Numeric) && sy.is_a?(Numeric)
|
|
107
|
+
compound_linear sx, 0, 0, sy
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @param delta_beat [Integer, Rational]
|
|
111
|
+
# @return [void]
|
|
112
|
+
def beat_translate delta_beat
|
|
113
|
+
raise ArgumentError, 'delta_beat must be a number' unless delta_beat.is_a? Numeric
|
|
114
|
+
warn 'Rational is recommended over Float for delta_beat' if delta_beat.is_a? Float
|
|
115
|
+
@t1 += delta_beat.to_r
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @!endgroup
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @!group DSL Methods
|
|
123
|
+
|
|
124
|
+
# Duplicate all events in a given array.
|
|
125
|
+
# This method is useful when you want to duplicate a set of events.
|
|
126
|
+
# The argument +events+ is an array of events to be duplicated.
|
|
127
|
+
# The argument +new_tip_points+ is a boolean indicating whether to create new tip points.
|
|
128
|
+
# If it is +true+, new tip points will be created for the duplicated events.
|
|
129
|
+
# If it is +false+, each duplicated event shares the same tip point as the original event.
|
|
130
|
+
# @param events [Array<Event>] the events to be duplicated.
|
|
131
|
+
# @param new_tip_points [Boolean] whether to create new tip points for the duplicated events.
|
|
132
|
+
# @return [Array<Event>] the duplicated events.
|
|
133
|
+
# @example Duplicate a note
|
|
134
|
+
# offset 0.1; bpm 120
|
|
135
|
+
# duplicate [t 0, 0]
|
|
136
|
+
# @example Duplicate notes that share tip points with the original notes
|
|
137
|
+
# offset 0.1; bpm 120
|
|
138
|
+
# duplicate tp_chain(0, 100, 1) { t 0, 0 }
|
|
139
|
+
def duplicate events, new_tip_points: true
|
|
140
|
+
result = []
|
|
141
|
+
events.each do |event|
|
|
142
|
+
next if event.type == :placeholder && !new_tip_points
|
|
143
|
+
result.push event = event.dup
|
|
144
|
+
if event[:tip_point] && new_tip_points
|
|
145
|
+
event[:tip_point] = "#@current_duplicate #{event[:tip_point]}"
|
|
146
|
+
end
|
|
147
|
+
@groups.each { _1.push event }
|
|
148
|
+
end
|
|
149
|
+
@current_duplicate += 1 if new_tip_points
|
|
150
|
+
result
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Transform all events in a given array in time and/or space.
|
|
154
|
+
# Space transformation does not affect background patterns.
|
|
155
|
+
# @yieldself [Transform]
|
|
156
|
+
# @param events [Array<Event>, Event]
|
|
157
|
+
# @return [Array<Event>]
|
|
158
|
+
def transform events, &block
|
|
159
|
+
raise ArgumentError, 'no block given' unless block
|
|
160
|
+
events = [events] if events.is_a? Event
|
|
161
|
+
transform = Transform.new
|
|
162
|
+
transform.instance_eval &block
|
|
163
|
+
events.each { transform.apply _1 }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Remove events from the chart.
|
|
167
|
+
# @param events [Array<Event>]
|
|
168
|
+
# @return [Array<Event>]
|
|
169
|
+
def remove *events
|
|
170
|
+
events.each { |event| @groups.each { _1.delete event } }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @!endgroup
|
|
174
|
+
|
|
175
|
+
end
|