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.
@@ -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