sscharter 0.7.0 → 0.9.0
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 +0 -13
- data/Gemfile.lock +13 -15
- data/LICENSE +1 -1
- data/README.md +3 -3
- data/Rakefile +12 -6
- data/lib/sscharter/chart.rb +35 -3
- data/lib/sscharter/cli.rb +48 -27
- data/lib/sscharter/version.rb +1 -1
- data/lib/sscharter.rb +520 -178
- data/tutorial/tutorial.md +71 -6
- metadata +34 -9
data/lib/sscharter.rb
CHANGED
@@ -30,6 +30,8 @@ class Sunniesnow::Charter
|
|
30
30
|
class BpmChangeList
|
31
31
|
|
32
32
|
class BpmChange
|
33
|
+
include Comparable
|
34
|
+
|
33
35
|
attr_accessor :beat, :bps
|
34
36
|
|
35
37
|
def initialize beat, bpm
|
@@ -51,6 +53,8 @@ class Sunniesnow::Charter
|
|
51
53
|
|
52
54
|
def add beat, bpm
|
53
55
|
@list.push BpmChange.new beat, bpm
|
56
|
+
@list.sort!
|
57
|
+
self
|
54
58
|
end
|
55
59
|
|
56
60
|
def time_at beat
|
@@ -286,16 +290,6 @@ class Sunniesnow::Charter
|
|
286
290
|
}.freeze
|
287
291
|
|
288
292
|
DIRECTIONS = {
|
289
|
-
right: 0.0,
|
290
|
-
up_right: Math::PI / 4,
|
291
|
-
up: Math::PI / 2,
|
292
|
-
up_left: Math::PI * 3 / 4,
|
293
|
-
left: Math::PI,
|
294
|
-
down_left: -Math::PI * 3 / 4,
|
295
|
-
down: -Math::PI / 2,
|
296
|
-
down_right: -Math::PI / 4
|
297
|
-
}
|
298
|
-
{
|
299
293
|
right: %i[r],
|
300
294
|
up_right: %i[ur ru],
|
301
295
|
up: %i[u],
|
@@ -304,22 +298,60 @@ class Sunniesnow::Charter
|
|
304
298
|
down_left: %i[dl ld],
|
305
299
|
down: %i[d],
|
306
300
|
down_right: %i[dr rd]
|
307
|
-
}.
|
308
|
-
|
309
|
-
|
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
|
+
|
310
314
|
DIRECTIONS.freeze
|
311
315
|
|
312
|
-
|
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
|
313
322
|
@charts = {}
|
314
323
|
|
324
|
+
# An array of events.
|
325
|
+
# @return [Array<Sunniesnow::Charter::Event>]
|
315
326
|
attr_reader :events
|
316
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
|
317
346
|
def self.open name, &block
|
318
347
|
result = @charts[name] ||= new name
|
319
348
|
result.instance_eval &block if block
|
320
349
|
result
|
321
350
|
end
|
322
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.
|
323
355
|
def initialize name
|
324
356
|
@name = name
|
325
357
|
init_chart_info
|
@@ -350,30 +382,192 @@ class Sunniesnow::Charter
|
|
350
382
|
@current_tip_point_group_stack = []
|
351
383
|
@tip_point_peak = 0
|
352
384
|
@current_duplicate = 0
|
385
|
+
@tip_point_start_stack = [nil]
|
353
386
|
@tip_point_start_to_add_stack = [nil]
|
354
387
|
@groups = [@events]
|
355
388
|
end
|
356
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.
|
357
522
|
def title title
|
358
523
|
raise ArgumentError, 'title must be a string' unless title.is_a? String
|
359
524
|
@title = title
|
360
525
|
end
|
361
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.
|
362
533
|
def artist artist
|
363
534
|
raise ArgumentError, 'artist must be a string' unless artist.is_a? String
|
364
535
|
@artist = artist
|
365
536
|
end
|
366
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.
|
367
544
|
def charter charter
|
368
545
|
raise ArgumentError, 'charter must be a string' unless charter.is_a? String
|
369
546
|
@charter = charter
|
370
547
|
end
|
371
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.
|
372
555
|
def difficulty_name difficulty_name
|
373
556
|
raise ArgumentError, 'difficulty_name must be a string' unless difficulty_name.is_a? String
|
374
557
|
@difficulty_name = difficulty_name
|
375
558
|
end
|
376
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.
|
377
571
|
def difficulty_color difficulty_color
|
378
572
|
@difficulty_color = case difficulty_color
|
379
573
|
when Symbol
|
@@ -393,25 +587,98 @@ class Sunniesnow::Charter
|
|
393
587
|
end
|
394
588
|
end
|
395
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).
|
396
598
|
def difficulty difficulty
|
397
599
|
@difficulty = difficulty.to_s
|
398
600
|
end
|
399
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).
|
400
610
|
def difficulty_sup difficulty_sup
|
401
611
|
@difficulty_sup = difficulty_sup.to_s
|
402
612
|
end
|
403
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
|
404
637
|
def offset offset
|
405
638
|
raise ArgumentError, 'offset must be a number' unless offset.is_a? Numeric
|
406
639
|
@current_beat = 0r
|
407
640
|
@bpm_changes = BpmChangeList.new offset.to_f
|
408
641
|
end
|
409
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.
|
410
652
|
def bpm bpm
|
411
653
|
raise OffsetError.new __method__ unless @bpm_changes
|
412
654
|
@bpm_changes.add @current_beat, bpm
|
413
655
|
end
|
414
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
|
415
682
|
def beat delta_beat = 0
|
416
683
|
raise OffsetError.new __method__ unless @current_beat
|
417
684
|
case delta_beat
|
@@ -426,6 +693,24 @@ class Sunniesnow::Charter
|
|
426
693
|
end
|
427
694
|
alias b beat
|
428
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
|
429
714
|
def beat! beat = @current_beat
|
430
715
|
raise OffsetError.new __method__ unless @current_beat
|
431
716
|
case beat
|
@@ -440,156 +725,24 @@ class Sunniesnow::Charter
|
|
440
725
|
end
|
441
726
|
alias b! beat!
|
442
727
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
@bpm_changes = backup[:bpm_changes]
|
462
|
-
end
|
463
|
-
|
464
|
-
def group preserve_beat: true, &block
|
465
|
-
raise ArgumentError, 'no block given' unless block
|
466
|
-
@groups.push result = []
|
467
|
-
beat_backup = backup_beat unless preserve_beat
|
468
|
-
instance_eval &block
|
469
|
-
restore_beat beat_backup unless preserve_beat
|
470
|
-
@groups.delete_if { result.equal? _1 }
|
471
|
-
result
|
472
|
-
end
|
473
|
-
|
474
|
-
def backup_state
|
475
|
-
{
|
476
|
-
current_beat: @current_beat,
|
477
|
-
bpm_changes: @bpm_changes,
|
478
|
-
tip_point_mode_stack: @tip_point_mode_stack.dup,
|
479
|
-
current_tip_point_stack: @current_tip_point_stack.dup,
|
480
|
-
current_tip_point_group_stack: @current_tip_point_group_stack.dup,
|
481
|
-
current_duplicate: @current_duplicate,
|
482
|
-
tip_point_start_to_add_stack: @tip_point_start_to_add_stack.dup,
|
483
|
-
groups: @groups.dup
|
484
|
-
}
|
485
|
-
end
|
486
|
-
|
487
|
-
def restore_state backup
|
488
|
-
@current_beat = backup[:current_beat]
|
489
|
-
@bpm_changes = backup[:bpm_changes]
|
490
|
-
@tip_point_mode_stack = backup[:tip_point_mode_stack]
|
491
|
-
@current_tip_point_stack = backup[:current_tip_point_stack]
|
492
|
-
@current_tip_point_group_stack = backup[:current_tip_point_group_stack]
|
493
|
-
@current_duplicate = backup[:current_duplicate]
|
494
|
-
@tip_point_start_to_add_stack = backup[:tip_point_start_to_add_stack]
|
495
|
-
@groups = backup[:groups]
|
496
|
-
nil
|
497
|
-
end
|
498
|
-
|
499
|
-
def mark name
|
500
|
-
@bookmarks[name] = backup_state
|
501
|
-
name
|
502
|
-
end
|
503
|
-
|
504
|
-
def at name, preserve_beat: false, update_mark: false, &block
|
505
|
-
raise ArgumentError, 'no block given' unless block
|
506
|
-
raise ArgumentError, "unknown bookmark #{name}" unless bookmark = @bookmarks[name]
|
507
|
-
backup = backup_state
|
508
|
-
restore_state bookmark
|
509
|
-
result = group &block
|
510
|
-
mark name if update_mark
|
511
|
-
beat_backup = backup_beat if preserve_beat
|
512
|
-
restore_state backup
|
513
|
-
restore_beat beat_backup if preserve_beat
|
514
|
-
result
|
515
|
-
end
|
516
|
-
|
517
|
-
def tip_point mode, *args, preserve_beat: true, **opts, &block
|
518
|
-
@tip_point_mode_stack.push mode
|
519
|
-
if mode == :none
|
520
|
-
@tip_point_start_to_add_stack.push nil
|
521
|
-
@current_tip_point_stack.push nil
|
522
|
-
else
|
523
|
-
@tip_point_start_to_add_stack.push TipPointStart.new *args, **opts
|
524
|
-
@current_tip_point_stack.push @tip_point_peak
|
525
|
-
@tip_point_peak += 1
|
526
|
-
end
|
527
|
-
result = group preserve_beat: do
|
528
|
-
@current_tip_point_group_stack.push @groups.last
|
529
|
-
instance_eval &block
|
530
|
-
end
|
531
|
-
@tip_point_start_to_add_stack.pop
|
532
|
-
@tip_point_mode_stack.pop
|
533
|
-
@current_tip_point_stack.pop
|
534
|
-
@current_tip_point_group_stack.pop
|
535
|
-
result
|
536
|
-
end
|
537
|
-
|
538
|
-
def remove *events
|
539
|
-
events.each { |event| @groups.each { _1.delete event } }
|
540
|
-
end
|
541
|
-
|
542
|
-
def event type, duration_beats = nil, **properties
|
543
|
-
raise OffsetError.new __method__ unless @bpm_changes
|
544
|
-
event = Event.new type, @current_beat, duration_beats, @bpm_changes, **properties
|
545
|
-
@groups.each { _1.push event }
|
546
|
-
return event unless event.tip_pointable?
|
547
|
-
case @tip_point_mode_stack.last
|
548
|
-
when :chain
|
549
|
-
push_tip_point_start event
|
550
|
-
@tip_point_start_to_add_stack[-1] = nil
|
551
|
-
when :drop
|
552
|
-
push_tip_point_start event
|
553
|
-
@current_tip_point_stack[-1] = @tip_point_peak
|
554
|
-
@tip_point_peak += 1
|
555
|
-
when :none
|
556
|
-
# pass
|
557
|
-
end
|
558
|
-
event
|
559
|
-
end
|
560
|
-
|
561
|
-
def push_tip_point_start start_event
|
562
|
-
start_event[:tip_point] = @current_tip_point_stack.last.to_s
|
563
|
-
tip_point_start = @tip_point_start_to_add_stack.last&.get_start_placeholder start_event
|
564
|
-
return unless tip_point_start
|
565
|
-
@groups.each do |group|
|
566
|
-
group.push tip_point_start
|
567
|
-
break if group.equal?(@current_tip_point_group_stack.last) && @tip_point_mode_stack.last != :drop
|
568
|
-
end
|
569
|
-
end
|
570
|
-
|
571
|
-
def transform events, &block
|
572
|
-
raise ArgumentError, 'no block given' unless block
|
573
|
-
events = [events] if events.is_a? Event
|
574
|
-
transform = Transform.new
|
575
|
-
transform.instance_eval &block
|
576
|
-
events.each { transform.apply _1 }
|
577
|
-
end
|
578
|
-
|
579
|
-
def duplicate events, new_tip_points: true
|
580
|
-
result = []
|
581
|
-
events.each do |event|
|
582
|
-
next if event.type == :placeholder && !new_tip_points
|
583
|
-
result.push event = event.dup
|
584
|
-
if event[:tip_point] && new_tip_points
|
585
|
-
event[:tip_point] = "#@current_duplicate #{event[:tip_point]}"
|
586
|
-
end
|
587
|
-
@groups.each { _1.push event }
|
588
|
-
end
|
589
|
-
@current_duplicate += 1 if new_tip_points
|
590
|
-
result
|
591
|
-
end
|
592
|
-
|
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
|
593
746
|
def tap x, y, text = ''
|
594
747
|
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
595
748
|
raise ArgumentError, 'x and y must be numbers'
|
@@ -598,6 +751,29 @@ class Sunniesnow::Charter
|
|
598
751
|
end
|
599
752
|
alias t tap
|
600
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
|
601
777
|
def hold x, y, duration_beats, text = ''
|
602
778
|
if !x.is_a?(Numeric) || !y.is_a?(Numeric) || !duration_beats.is_a?(Numeric)
|
603
779
|
raise ArgumentError, 'x, y, and duration must be numbers'
|
@@ -612,6 +788,20 @@ class Sunniesnow::Charter
|
|
612
788
|
end
|
613
789
|
alias h hold
|
614
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)
|
615
805
|
def drag x, y
|
616
806
|
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
617
807
|
raise ArgumentError, 'x and y must be numbers'
|
@@ -620,6 +810,32 @@ class Sunniesnow::Charter
|
|
620
810
|
end
|
621
811
|
alias d drag
|
622
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
|
623
839
|
def flick x, y, direction, text = ''
|
624
840
|
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
625
841
|
raise ArgumentError, 'x and y must be numbers'
|
@@ -637,6 +853,32 @@ class Sunniesnow::Charter
|
|
637
853
|
end
|
638
854
|
alias f flick
|
639
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 ''
|
640
882
|
def bg_note x, y, duration_beats = 0, text = nil
|
641
883
|
if text.nil?
|
642
884
|
if duration_beats.is_a? String
|
@@ -658,6 +900,23 @@ class Sunniesnow::Charter
|
|
658
900
|
event :bg_note, duration_beats.to_r, x: x.to_f, y: y.to_f, text: text.to_s
|
659
901
|
end
|
660
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!'
|
661
920
|
def big_text duration_beats = 0, text
|
662
921
|
unless duration_beats.is_a? Numeric
|
663
922
|
raise ArgumentError, 'duration_beats must be a number'
|
@@ -671,6 +930,30 @@ class Sunniesnow::Charter
|
|
671
930
|
event :big_text, duration_beats.to_r, text: text.to_s
|
672
931
|
end
|
673
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'
|
674
957
|
%i[grid hexagon checkerboard diamond_grid pentagon turntable hexagram].each do |method_name|
|
675
958
|
define_method method_name do |duration_beats = 0|
|
676
959
|
unless duration_beats.is_a? Numeric
|
@@ -686,25 +969,82 @@ class Sunniesnow::Charter
|
|
686
969
|
end
|
687
970
|
end
|
688
971
|
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
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
|
699
998
|
result
|
700
999
|
end
|
701
1000
|
|
702
|
-
|
703
|
-
|
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 }
|
704
1009
|
end
|
705
1010
|
|
706
|
-
def
|
707
|
-
|
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
|
708
1048
|
end
|
709
1049
|
|
710
1050
|
def check(
|
@@ -734,4 +1074,6 @@ class Sunniesnow::Charter
|
|
734
1074
|
end
|
735
1075
|
end
|
736
1076
|
|
1077
|
+
# @!endgroup
|
1078
|
+
|
737
1079
|
end
|