clef 0.1.0 → 1.0.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/README.md +77 -90
- data/Rakefile +21 -1
- data/exe/clef +21 -0
- data/lib/clef/compiler.rb +107 -4
- data/lib/clef/core/chord.rb +9 -3
- data/lib/clef/core/duration.rb +7 -3
- data/lib/clef/core/key_signature.rb +43 -36
- data/lib/clef/core/measure.rb +14 -10
- data/lib/clef/core/metadata.rb +52 -0
- data/lib/clef/core/note.rb +50 -4
- data/lib/clef/core/pitch.rb +73 -4
- data/lib/clef/core/rest.rb +11 -3
- data/lib/clef/core/score.rb +148 -9
- data/lib/clef/core/staff.rb +13 -3
- data/lib/clef/core/staff_group.rb +8 -2
- data/lib/clef/core/tempo.rb +5 -0
- data/lib/clef/core/tuplet.rb +48 -0
- data/lib/clef/core/validation.rb +39 -0
- data/lib/clef/core/voice.rb +21 -5
- data/lib/clef/engraving/font_manager.rb +1 -1
- data/lib/clef/engraving/glyph_table.rb +18 -3
- data/lib/clef/engraving/style.rb +41 -2
- data/lib/clef/ir/moment.rb +2 -2
- data/lib/clef/ir/music_tree.rb +2 -2
- data/lib/clef/ir/timeline.rb +25 -5
- data/lib/clef/layout/beam_layout.rb +2 -2
- data/lib/clef/layout/item.rb +26 -0
- data/lib/clef/layout/spacing.rb +6 -4
- data/lib/clef/layout/stem.rb +10 -6
- data/lib/clef/layout/system_layout.rb +71 -0
- data/lib/clef/midi/channel_map.rb +5 -2
- data/lib/clef/midi/exporter.rb +316 -38
- data/lib/clef/notation/dynamic.rb +5 -0
- data/lib/clef/notation/lyric.rb +33 -1
- data/lib/clef/parser/dsl.rb +249 -58
- data/lib/clef/parser/lilypond_lexer.rb +43 -3
- data/lib/clef/parser/lilypond_parser.rb +231 -17
- data/lib/clef/plugins/base.rb +24 -4
- data/lib/clef/plugins/registry.rb +80 -10
- data/lib/clef/renderer/base.rb +2 -2
- data/lib/clef/renderer/drawing_context.rb +26 -0
- data/lib/clef/renderer/notation_helpers.rb +92 -1
- data/lib/clef/renderer/pdf_renderer.rb +487 -82
- data/lib/clef/renderer/svg_renderer.rb +510 -97
- data/lib/clef/version.rb +1 -1
- data/lib/clef.rb +60 -7
- data/sig/clef.rbs +292 -0
- metadata +14 -5
|
@@ -8,27 +8,45 @@ module Clef
|
|
|
8
8
|
include NotationHelpers
|
|
9
9
|
|
|
10
10
|
WIDTH = 1024
|
|
11
|
-
|
|
11
|
+
LEFT_PADDING = 60
|
|
12
|
+
RIGHT_PADDING = 60
|
|
13
|
+
STAFF_START_X = 140
|
|
14
|
+
STAFF_TOP = 90
|
|
12
15
|
|
|
13
16
|
# @param score [Clef::Core::Score]
|
|
14
17
|
# @param path [String]
|
|
15
|
-
# @param
|
|
16
|
-
|
|
18
|
+
# @param positions [Hash]
|
|
19
|
+
# @param _layout [Hash, nil]
|
|
20
|
+
def render(score, path, positions: nil, layout: nil, **_options)
|
|
21
|
+
height = svg_height(score, layout)
|
|
17
22
|
document = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
18
|
-
xml.svg(xmlns: "http://www.w3.org/2000/svg",
|
|
19
|
-
|
|
23
|
+
xml.svg(xmlns: "http://www.w3.org/2000/svg",
|
|
24
|
+
width: WIDTH,
|
|
25
|
+
height: height,
|
|
26
|
+
viewBox: "0 0 #{WIDTH} #{height}") do
|
|
27
|
+
draw_score(xml, score, positions: positions, layout: layout)
|
|
20
28
|
end
|
|
21
29
|
end
|
|
22
|
-
|
|
30
|
+
write_output(path, document.to_xml)
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
# @param xml [Nokogiri::XML::Builder]
|
|
26
34
|
# @param score [Clef::Core::Score]
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
# @param positions [Hash, nil]
|
|
36
|
+
# @param layout [Hash, nil]
|
|
37
|
+
def draw_score(xml, score, positions: nil, layout: nil)
|
|
38
|
+
return draw_systems(xml, score, layout) if layout&.dig(:systems)&.any?
|
|
39
|
+
|
|
40
|
+
draw_header(xml, score)
|
|
41
|
+
staff_index = 0
|
|
42
|
+
score.staff_groups.each do |group|
|
|
43
|
+
group_start = STAFF_TOP + (staff_index * style.staff_gap)
|
|
44
|
+
group.staves.each do |staff|
|
|
45
|
+
baseline = STAFF_TOP + (staff_index * style.staff_gap)
|
|
46
|
+
draw_staff(xml, staff, baseline, positions: positions, layout: layout)
|
|
47
|
+
staff_index += 1
|
|
48
|
+
end
|
|
49
|
+
draw_staff_group(xml, group, group_start, STAFF_TOP + ((staff_index - 1) * style.staff_gap)) if group.staves.length > 1
|
|
32
50
|
end
|
|
33
51
|
end
|
|
34
52
|
|
|
@@ -37,24 +55,8 @@ module Clef
|
|
|
37
55
|
def draw_staff_lines(xml, baseline)
|
|
38
56
|
5.times do |line|
|
|
39
57
|
y = baseline + (line * style.staff_space)
|
|
40
|
-
xml.line(x1:
|
|
41
|
-
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# @param xml [Nokogiri::XML::Builder]
|
|
45
|
-
# @param staff [Clef::Core::Staff]
|
|
46
|
-
# @param baseline [Float]
|
|
47
|
-
def draw_notes(xml, staff, baseline)
|
|
48
|
-
x = 140
|
|
49
|
-
staff.measures.each do |measure|
|
|
50
|
-
voice = measure.voices.values.first
|
|
51
|
-
next unless voice
|
|
52
|
-
|
|
53
|
-
voice.elements.each do |element|
|
|
54
|
-
draw_element(xml, element, x, baseline, staff.clef)
|
|
55
|
-
x += duration_spacing(element)
|
|
56
|
-
end
|
|
57
|
-
x += 16
|
|
58
|
+
xml.line(x1: LEFT_PADDING, y1: y, x2: WIDTH - RIGHT_PADDING, y2: y,
|
|
59
|
+
stroke: "black", "stroke-width": 1, class: "staff-line")
|
|
58
60
|
end
|
|
59
61
|
end
|
|
60
62
|
|
|
@@ -65,134 +67,429 @@ module Clef
|
|
|
65
67
|
# @param clef [Clef::Core::Clef]
|
|
66
68
|
def draw_element(xml, element, x, baseline, clef)
|
|
67
69
|
case element
|
|
68
|
-
when Clef::Core::Note then draw_note(xml, element, x, baseline, clef)
|
|
70
|
+
when Clef::Core::Note then draw_note(xml, element, x, baseline, clef, accidental_state: {}, key_signature: nil)
|
|
69
71
|
when Clef::Core::Rest then draw_rest(xml, element, x, baseline)
|
|
70
|
-
when Clef::Core::Chord then draw_chord(xml, element, x, baseline, clef)
|
|
72
|
+
when Clef::Core::Chord then draw_chord(xml, element, x, baseline, clef, accidental_state: {}, key_signature: nil)
|
|
73
|
+
when Clef::Core::Tuplet then draw_tuplet(xml, element, x, baseline, clef, accidental_state: {}, key_signature: nil)
|
|
74
|
+
when Clef::Notation::Dynamic then draw_dynamic(xml, element, x, baseline)
|
|
75
|
+
when Clef::Core::Tempo then draw_tempo_change(xml, element, x, baseline)
|
|
71
76
|
end
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
private
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
def draw_header(xml, score)
|
|
82
|
+
draw_text(xml, score.title, x: LEFT_PADDING, y: 30, class: "title", "font-size": 18, fill: "black") if score.title
|
|
83
|
+
if score.composer
|
|
84
|
+
draw_text(xml, score.composer, x: WIDTH - RIGHT_PADDING, y: 30, class: "composer",
|
|
85
|
+
"font-size": 12, fill: "black", "text-anchor": "end")
|
|
86
|
+
end
|
|
87
|
+
return unless score.tempo
|
|
88
|
+
|
|
89
|
+
draw_text(xml, tempo_text(score.tempo), x: LEFT_PADDING, y: 52, class: "tempo", "font-size": 11, fill: "black")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def draw_staff(xml, staff, baseline, positions:, layout:)
|
|
93
|
+
draw_staff_lines(xml, baseline)
|
|
94
|
+
cursor = draw_clef(xml, staff.clef, LEFT_PADDING + 8, baseline)
|
|
95
|
+
cursor = draw_key_signature(xml, staff.key_signature, cursor + style.staff_space, baseline)
|
|
96
|
+
cursor = draw_time_signature(xml, staff.time_signature, cursor + style.staff_space, baseline)
|
|
97
|
+
note_points = draw_measures(xml, staff, [cursor + style.measure_padding, STAFF_START_X].max, baseline, positions, layout)
|
|
98
|
+
draw_lyrics(xml, staff, note_points, baseline)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def draw_systems(xml, score, layout)
|
|
102
|
+
draw_header(xml, score)
|
|
103
|
+
@system_page_height = svg_page_height_for(score)
|
|
104
|
+
layout[:systems].each do |system|
|
|
105
|
+
score.staff_groups.each do |group|
|
|
106
|
+
baselines = group.staves.map { |staff| svg_system_baseline(system, staff) }
|
|
107
|
+
group.staves.each do |staff|
|
|
108
|
+
draw_staff_system(xml, staff, baselines[group.staves.index(staff)], system, layout)
|
|
109
|
+
end
|
|
110
|
+
draw_staff_group(xml, group, baselines.first, baselines.last) if group.staves.length > 1
|
|
111
|
+
end
|
|
112
|
+
draw_layout_items(xml, layout, system)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def draw_staff_system(xml, staff, baseline, system, layout)
|
|
117
|
+
draw_staff_lines(xml, baseline)
|
|
118
|
+
cursor = draw_clef(xml, staff.clef, LEFT_PADDING + 8, baseline)
|
|
119
|
+
cursor = draw_key_signature(xml, staff.key_signature, cursor + style.staff_space, baseline)
|
|
120
|
+
cursor = draw_time_signature(xml, staff.time_signature, cursor + style.staff_space, baseline)
|
|
121
|
+
note_points = draw_measures(xml, staff, [cursor + style.measure_padding, STAFF_START_X].max,
|
|
122
|
+
baseline, layout[:positions], layout, system: system)
|
|
123
|
+
draw_lyrics(xml, staff, note_points, baseline)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def draw_staff_group(xml, group, first_baseline, last_baseline)
|
|
127
|
+
x = LEFT_PADDING - 24
|
|
128
|
+
case group.bracket_type
|
|
129
|
+
when :brace
|
|
130
|
+
draw_text(xml, "{", x: x, y: first_baseline + style.staff_space * 3.5,
|
|
131
|
+
class: "staff-group brace", "font-size": (last_baseline - first_baseline + 50), fill: "black")
|
|
132
|
+
when :bracket
|
|
133
|
+
xml.path(d: "M #{x + 8} #{first_baseline} L #{x} #{first_baseline} L #{x} #{last_baseline + (style.staff_space * 4)} L #{x + 8} #{last_baseline + (style.staff_space * 4)}",
|
|
134
|
+
fill: "none", stroke: "black", "stroke-width": 2, class: "staff-group bracket")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def draw_measures(xml, staff, start_x, baseline, positions, layout, system: nil)
|
|
139
|
+
note_points = {}
|
|
140
|
+
voice_elements = Hash.new { |hash, key| hash[key] = [] }
|
|
141
|
+
measure_start = Clef::Ir::Moment.new(0)
|
|
142
|
+
staff.measures.each do |measure|
|
|
143
|
+
measure_points, measure_voice_elements = draw_measure(xml, measure, staff, start_x, baseline, measure_start, positions, layout, system: system)
|
|
144
|
+
note_points.merge!(measure_points)
|
|
145
|
+
measure_voice_elements.each { |voice_id, elements| voice_elements[voice_id].concat(elements) }
|
|
146
|
+
bar_moment = measure_start + measure_length_for(measure)
|
|
147
|
+
if system.nil? || system.include_moment?(bar_moment)
|
|
148
|
+
bar_x = x_for_moment(positions, bar_moment, start_x, position_offset: system&.position_offset)
|
|
149
|
+
draw_barline(xml, bar_x, baseline)
|
|
150
|
+
end
|
|
151
|
+
measure_start += measure_length_for(measure)
|
|
152
|
+
end
|
|
153
|
+
voice_elements.each_value { |elements| draw_note_connections(xml, elements, note_points) }
|
|
154
|
+
note_points
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def draw_measure(xml, measure, staff, start_x, baseline, measure_start, positions, layout, system: nil)
|
|
158
|
+
note_points = {}
|
|
159
|
+
voice_elements = {}
|
|
160
|
+
accidental_state = {}
|
|
161
|
+
measure.voices.each_with_index do |(voice_id, voice), index|
|
|
162
|
+
voice_elements[voice_id] = voice.elements
|
|
163
|
+
voice_offset = voice_vertical_offset(index)
|
|
164
|
+
cursor = Clef::Ir::Moment.new(measure_start.value)
|
|
165
|
+
voice.elements.each do |element|
|
|
166
|
+
if system.nil? || system.include_moment?(cursor)
|
|
167
|
+
x = x_for_moment(positions, cursor, start_x, position_offset: system&.position_offset)
|
|
168
|
+
draw_element_with_context(xml, element, x, baseline + voice_offset, staff, measure, accidental_state, note_points)
|
|
169
|
+
end
|
|
170
|
+
cursor += element.length
|
|
171
|
+
end
|
|
172
|
+
draw_beams(xml, layout, staff, measure, voice_id, note_points)
|
|
173
|
+
end
|
|
174
|
+
[note_points, voice_elements]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def draw_element_with_context(xml, element, x, baseline, staff, measure, accidental_state, note_points)
|
|
178
|
+
case element
|
|
179
|
+
when Clef::Core::Note
|
|
180
|
+
draw_note(xml, element, x, baseline, staff.clef,
|
|
181
|
+
accidental_state: accidental_state,
|
|
182
|
+
key_signature: measure.key_signature || staff.key_signature)
|
|
183
|
+
note_points[element.object_id] = [x, pitch_y(element.pitch, baseline, staff.clef)]
|
|
184
|
+
when Clef::Core::Rest
|
|
185
|
+
draw_rest(xml, element, x, baseline)
|
|
186
|
+
when Clef::Core::Chord
|
|
187
|
+
draw_chord(xml, element, x, baseline, staff.clef,
|
|
188
|
+
accidental_state: accidental_state,
|
|
189
|
+
key_signature: measure.key_signature || staff.key_signature)
|
|
190
|
+
note_points[element.object_id] = [x, baseline]
|
|
191
|
+
when Clef::Core::Tuplet
|
|
192
|
+
draw_tuplet(xml, element, x, baseline, staff.clef,
|
|
193
|
+
accidental_state: accidental_state,
|
|
194
|
+
key_signature: measure.key_signature || staff.key_signature,
|
|
195
|
+
note_points: note_points)
|
|
196
|
+
when Clef::Notation::Dynamic
|
|
197
|
+
draw_dynamic(xml, element, x, baseline)
|
|
198
|
+
when Clef::Core::Tempo
|
|
199
|
+
draw_tempo_change(xml, element, x, baseline)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def draw_note(xml, note, x, baseline, clef, accidental_state:, key_signature:)
|
|
82
204
|
y = pitch_y(note.pitch, baseline, clef)
|
|
205
|
+
draw_ledger_lines(xml, x, y, baseline)
|
|
83
206
|
draw_notehead(xml, x, y, duration: note.duration)
|
|
84
|
-
draw_accidental(xml, note.pitch, x, y)
|
|
207
|
+
draw_accidental(xml, note.pitch, x, y, key_signature, accidental_state)
|
|
85
208
|
draw_stem(xml, note, x, y, clef) if stem_required?(note.duration)
|
|
209
|
+
draw_flag(xml, note, x, y, clef) if flag_required?(note.duration)
|
|
86
210
|
draw_dot(xml, note.duration, x, y)
|
|
87
211
|
draw_articulations(xml, note.articulations, x, y)
|
|
88
212
|
end
|
|
89
213
|
|
|
90
|
-
# @param xml [Nokogiri::XML::Builder]
|
|
91
|
-
# @param rest [Clef::Core::Rest]
|
|
92
|
-
# @param x [Float]
|
|
93
|
-
# @param baseline [Float]
|
|
94
214
|
def draw_rest(xml, rest, x, baseline)
|
|
95
|
-
if rest.
|
|
96
|
-
|
|
215
|
+
return if rest.kind == :invisible || rest.kind == :spacer
|
|
216
|
+
|
|
217
|
+
y = rest_y(rest, baseline)
|
|
218
|
+
if rest.kind == :multi_measure
|
|
219
|
+
draw_multi_measure_rest(xml, rest, x, baseline)
|
|
220
|
+
elsif rest.duration.base == :whole
|
|
221
|
+
xml.rect(x: x - 4, y: y, width: 8, height: 3, fill: "black", class: "rest rest-whole")
|
|
222
|
+
elsif rest.duration.base == :half
|
|
223
|
+
xml.rect(x: x - 4, y: y - style.staff_space, width: 8, height: 3, fill: "black", class: "rest rest-half")
|
|
97
224
|
else
|
|
98
|
-
|
|
225
|
+
draw_shaped_rest(xml, rest, x, y)
|
|
99
226
|
end
|
|
100
227
|
end
|
|
101
228
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def
|
|
229
|
+
def draw_dynamic(xml, dynamic, x, baseline)
|
|
230
|
+
draw_text(xml, dynamic_text(dynamic), x: x, y: baseline + (style.staff_space * 6),
|
|
231
|
+
class: "dynamic", "font-size": 11, fill: "black", "font-style": "italic")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def draw_tempo_change(xml, tempo, x, baseline)
|
|
235
|
+
draw_text(xml, tempo_text(tempo), x: x, y: baseline - (style.staff_space * 1.5),
|
|
236
|
+
class: "tempo-change", "font-size": 10, fill: "black")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def draw_chord(xml, chord, x, baseline, clef, accidental_state:, key_signature:)
|
|
108
240
|
notes = chord_notes(chord)
|
|
241
|
+
offsets = chord_note_offsets(notes)
|
|
109
242
|
ys = []
|
|
110
|
-
notes.
|
|
243
|
+
notes.each_with_index do |note, index|
|
|
111
244
|
y = pitch_y(note.pitch, baseline, clef)
|
|
112
245
|
ys << y
|
|
113
|
-
|
|
114
|
-
|
|
246
|
+
note_x = x + offsets[index]
|
|
247
|
+
draw_ledger_lines(xml, note_x, y, baseline)
|
|
248
|
+
draw_notehead(xml, note_x, y, duration: chord.duration)
|
|
249
|
+
draw_accidental(xml, note.pitch, note_x - accidental_offset(index), y, key_signature, accidental_state)
|
|
115
250
|
end
|
|
116
251
|
draw_chord_stem(xml, notes, x, ys, clef) if stem_required?(chord.duration)
|
|
117
|
-
draw_dot(xml, chord.duration, x,
|
|
252
|
+
ys.each { |y| draw_dot(xml, chord.duration, x, y) }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def draw_tuplet(xml, tuplet, x, baseline, clef, accidental_state:, key_signature:, note_points: {})
|
|
256
|
+
cursor = x
|
|
257
|
+
tuplet.elements.each do |element|
|
|
258
|
+
draw_element_with_context(xml, element, cursor, baseline,
|
|
259
|
+
Struct.new(:clef, :key_signature).new(clef, key_signature),
|
|
260
|
+
Struct.new(:key_signature).new(key_signature),
|
|
261
|
+
accidental_state,
|
|
262
|
+
note_points)
|
|
263
|
+
cursor += duration_spacing(element) * tuplet.ratio
|
|
264
|
+
end
|
|
265
|
+
draw_text(xml, tuplet.actual.to_s, x: x + ((cursor - x) / 2.0), y: baseline - style.staff_space,
|
|
266
|
+
class: "tuplet", "font-size": 10, fill: "black", "text-anchor": "middle")
|
|
118
267
|
end
|
|
119
268
|
|
|
120
|
-
# @param xml [Nokogiri::XML::Builder]
|
|
121
|
-
# @param x [Float]
|
|
122
|
-
# @param y [Float]
|
|
123
|
-
# @param duration [Clef::Core::Duration]
|
|
124
269
|
def draw_notehead(xml, x, y, duration:)
|
|
125
270
|
if filled_notehead?(duration)
|
|
126
|
-
xml.circle(cx: x, cy: y, r:
|
|
271
|
+
xml.circle(cx: x, cy: y, r: style.notehead_width / 2.0, fill: "black", class: "notehead filled")
|
|
127
272
|
else
|
|
128
|
-
xml.ellipse(cx: x, cy: y, rx:
|
|
273
|
+
xml.ellipse(cx: x, cy: y, rx: style.notehead_width / 2.0, ry: 2.5,
|
|
274
|
+
fill: "white", stroke: "black", "stroke-width": 1, class: "notehead hollow")
|
|
129
275
|
end
|
|
130
276
|
end
|
|
131
277
|
|
|
132
|
-
# @param xml [Nokogiri::XML::Builder]
|
|
133
|
-
# @param note [Clef::Core::Note]
|
|
134
|
-
# @param x [Float]
|
|
135
|
-
# @param y [Float]
|
|
136
|
-
# @param clef [Clef::Core::Clef]
|
|
137
278
|
def draw_stem(xml, note, x, y, clef)
|
|
138
279
|
direction = Clef::Layout::Stem.direction(note, clef)
|
|
139
280
|
stem_len = style.staff_space * Clef::Layout::Stem.length(note, clef, direction)
|
|
140
|
-
y2 = direction == :up ? y - stem_len : y + stem_len
|
|
141
|
-
stem_x = direction == :up ? x + 3 : x - 3
|
|
142
|
-
xml.line(x1: stem_x, y1: y, x2: stem_x, y2: y2, stroke: "black", "stroke-width": 1)
|
|
281
|
+
y2 = (direction == :up) ? y - stem_len : y + stem_len
|
|
282
|
+
stem_x = (direction == :up) ? x + 3 : x - 3
|
|
283
|
+
xml.line(x1: stem_x, y1: y, x2: stem_x, y2: y2, stroke: "black", "stroke-width": 1, class: "stem")
|
|
143
284
|
end
|
|
144
285
|
|
|
145
|
-
# @param xml [Nokogiri::XML::Builder]
|
|
146
|
-
# @param notes [Array<Clef::Core::Note>]
|
|
147
|
-
# @param x [Float]
|
|
148
|
-
# @param ys [Array<Float>]
|
|
149
|
-
# @param clef [Clef::Core::Clef]
|
|
150
286
|
def draw_chord_stem(xml, notes, x, ys, clef)
|
|
151
287
|
direction = Clef::Layout::Stem.direction(notes, clef)
|
|
152
288
|
anchor_note = chord_stem_anchor_note(notes, direction)
|
|
153
289
|
anchor_index = notes.index(anchor_note)
|
|
154
290
|
anchor_y = ys[anchor_index]
|
|
155
291
|
stem_len = style.staff_space * chord_stem_length(notes, clef, direction)
|
|
156
|
-
y2 = direction == :up ? anchor_y - stem_len : anchor_y + stem_len
|
|
157
|
-
stem_x = direction == :up ? x + 3 : x - 3
|
|
158
|
-
xml.line(x1: stem_x, y1: anchor_y, x2: stem_x, y2: y2, stroke: "black", "stroke-width": 1)
|
|
292
|
+
y2 = (direction == :up) ? anchor_y - stem_len : anchor_y + stem_len
|
|
293
|
+
stem_x = (direction == :up) ? x + 3 : x - 3
|
|
294
|
+
xml.line(x1: stem_x, y1: anchor_y, x2: stem_x, y2: y2, stroke: "black", "stroke-width": 1, class: "stem")
|
|
159
295
|
end
|
|
160
296
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
297
|
+
def draw_flag(xml, note, x, y, clef)
|
|
298
|
+
direction = Clef::Layout::Stem.direction(note, clef)
|
|
299
|
+
stem_len = style.staff_space * Clef::Layout::Stem.length(note, clef, direction)
|
|
300
|
+
stem_x = (direction == :up) ? x + 3 : x - 3
|
|
301
|
+
stem_y = (direction == :up) ? y - stem_len : y + stem_len
|
|
302
|
+
sweep = (direction == :up) ? 10 : -10
|
|
303
|
+
xml.path(d: "M #{stem_x} #{stem_y} q 12 #{sweep} 4 #{sweep * 2}",
|
|
304
|
+
fill: "none", stroke: "black", "stroke-width": 1, class: "flag")
|
|
305
|
+
end
|
|
168
306
|
|
|
169
|
-
|
|
307
|
+
def draw_accidental(xml, pitch, x, y, key_signature, state)
|
|
308
|
+
alteration = accidental_for_pitch(pitch, key_signature, state)
|
|
309
|
+
return if alteration.nil?
|
|
310
|
+
|
|
311
|
+
draw_text(xml, accidental_text(alteration), x: x - 12, y: y + 3, fill: "black", "font-size": 9, class: "accidental")
|
|
170
312
|
end
|
|
171
313
|
|
|
172
|
-
# @param xml [Nokogiri::XML::Builder]
|
|
173
|
-
# @param duration [Clef::Core::Duration]
|
|
174
|
-
# @param x [Float]
|
|
175
|
-
# @param y [Float]
|
|
176
314
|
def draw_dot(xml, duration, x, y)
|
|
177
315
|
return if duration.dots.zero?
|
|
178
316
|
|
|
179
317
|
duration.dots.times do |index|
|
|
180
|
-
xml.circle(cx: x + 8 + (index * 3), cy: y, r: 1, fill: "black")
|
|
318
|
+
xml.circle(cx: x + 8 + (index * 3), cy: y, r: 1, fill: "black", class: "dot")
|
|
181
319
|
end
|
|
182
320
|
end
|
|
183
321
|
|
|
184
|
-
# @param xml [Nokogiri::XML::Builder]
|
|
185
|
-
# @param articulations [Array<Symbol>]
|
|
186
|
-
# @param x [Float]
|
|
187
|
-
# @param y [Float]
|
|
188
322
|
def draw_articulations(xml, articulations, x, y)
|
|
189
|
-
|
|
323
|
+
articulations.each do |articulation|
|
|
324
|
+
case articulation
|
|
325
|
+
when :staccato
|
|
326
|
+
xml.circle(cx: x, cy: y - 12, r: 1.5, fill: "black", class: "articulation staccato")
|
|
327
|
+
when :tenuto
|
|
328
|
+
xml.line(x1: x - 4, y1: y - 12, x2: x + 4, y2: y - 12, stroke: "black",
|
|
329
|
+
"stroke-width": 1, class: "articulation tenuto")
|
|
330
|
+
when :accent
|
|
331
|
+
draw_text(xml, ">", x: x - 4, y: y - 10, fill: "black", "font-size": 9, class: "articulation accent")
|
|
332
|
+
when :marcato
|
|
333
|
+
xml.path(d: "M #{x - 4} #{y - 10} L #{x} #{y - 15} L #{x + 4} #{y - 10}",
|
|
334
|
+
fill: "none", stroke: "black", "stroke-width": 1, class: "articulation marcato")
|
|
335
|
+
when :fermata
|
|
336
|
+
xml.path(d: "M #{x - 7} #{y - 13} Q #{x} #{y - 20} #{x + 7} #{y - 13}",
|
|
337
|
+
fill: "none", stroke: "black", "stroke-width": 1, class: "articulation fermata")
|
|
338
|
+
xml.circle(cx: x, cy: y - 13, r: 1.2, fill: "black", class: "articulation fermata-dot")
|
|
339
|
+
else
|
|
340
|
+
draw_text(xml, articulation.to_s, x: x - 4, y: y - 12, fill: "black", "font-size": 6, class: "articulation")
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def draw_multi_measure_rest(xml, rest, x, baseline)
|
|
346
|
+
y = baseline + (style.staff_space * 2)
|
|
347
|
+
xml.line(x1: x - 12, y1: y, x2: x + 12, y2: y, stroke: "black", "stroke-width": 4,
|
|
348
|
+
class: "rest rest-multi-measure")
|
|
349
|
+
draw_text(xml, rest.measures.to_s, x: x, y: baseline - style.staff_space,
|
|
350
|
+
fill: "black", "font-size": 10, "text-anchor": "middle", class: "rest-count")
|
|
351
|
+
end
|
|
190
352
|
|
|
191
|
-
|
|
353
|
+
def draw_shaped_rest(xml, rest, x, y)
|
|
354
|
+
case rest.duration.base
|
|
355
|
+
when :quarter
|
|
356
|
+
xml.path(d: "M #{x} #{y - 12} c -6 5 8 8 0 14 c 6 3 -5 7 0 13",
|
|
357
|
+
fill: "none", stroke: "black", "stroke-width": 2, class: "rest rest-quarter")
|
|
358
|
+
when :eighth, :sixteenth
|
|
359
|
+
xml.path(d: "M #{x} #{y - 12} q 10 4 1 12 l -6 14",
|
|
360
|
+
fill: "none", stroke: "black", "stroke-width": 2, class: "rest rest-#{rest.duration.base}")
|
|
361
|
+
xml.path(d: "M #{x + 1} #{y - 4} q 8 4 0 9",
|
|
362
|
+
fill: "none", stroke: "black", "stroke-width": 1, class: "rest rest-#{rest.duration.base}-flag")
|
|
363
|
+
if rest.duration.base == :sixteenth
|
|
364
|
+
xml.path(d: "M #{x - 1} #{y + 2} q 8 4 0 9",
|
|
365
|
+
fill: "none", stroke: "black", "stroke-width": 1, class: "rest rest-sixteenth-flag")
|
|
366
|
+
end
|
|
367
|
+
else
|
|
368
|
+
draw_text(xml, rest_label(rest.duration), x: x - 4, y: y, fill: "black", "font-size": 14,
|
|
369
|
+
class: "rest rest-#{rest.duration.base}")
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def draw_beams(xml, layout, staff, measure, voice_id, note_points)
|
|
374
|
+
Array(layout&.dig(:beams, staff.id, measure.number, voice_id)).each do |group|
|
|
375
|
+
points = group.filter_map { |note| note_points[note.object_id] }
|
|
376
|
+
next if points.length < 2
|
|
377
|
+
|
|
378
|
+
y = points.map(&:last).min - (style.staff_space * 3.5)
|
|
379
|
+
xml.line(x1: points.first.first + 3, y1: y, x2: points.last.first + 3, y2: y,
|
|
380
|
+
stroke: "black", "stroke-width": style.beam_thickness, class: "beam")
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def draw_note_connections(xml, elements, note_points)
|
|
385
|
+
notes = flatten_elements(elements).select { |element| element.is_a?(Clef::Core::Note) }
|
|
386
|
+
notes.each_with_index do |note, index|
|
|
387
|
+
draw_connection_to_next(xml, note, notes[(index + 1)..], note_points, "tie") if note.tie_state == :start
|
|
388
|
+
draw_connection_to_next(xml, note, notes[(index + 1)..], note_points, "slur") if note.slur_start
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def draw_connection_to_next(xml, note, candidates, note_points, class_name)
|
|
393
|
+
target = (class_name == "tie") ? candidates&.find { |candidate| candidate.pitch.enharmonic?(note.pitch) } : candidates&.find(&:slur_end)
|
|
394
|
+
return unless target
|
|
395
|
+
|
|
396
|
+
start_point = note_points[note.object_id]
|
|
397
|
+
end_point = note_points[target.object_id]
|
|
398
|
+
return unless start_point && end_point
|
|
399
|
+
|
|
400
|
+
lift = (class_name == "tie") ? 8 : 14
|
|
401
|
+
path = "M #{start_point[0] + 5} #{start_point[1] - 5} C #{start_point[0] + 18} #{start_point[1] - lift}, " \
|
|
402
|
+
"#{end_point[0] - 18} #{end_point[1] - lift}, #{end_point[0] - 5} #{end_point[1] - 5}"
|
|
403
|
+
xml.path(d: path, fill: "none", stroke: "black", "stroke-width": 1, class: class_name)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def draw_lyrics(xml, staff, note_points, baseline)
|
|
407
|
+
Array(staff.metadata[:lyrics]).each do |lyric|
|
|
408
|
+
elements = staff.measures.flat_map { |measure| Array(measure.voices[lyric.voice_id]&.elements) }
|
|
409
|
+
.then { |elements| lyric_elements(elements) }
|
|
410
|
+
lyric_events(lyric, elements).each do |event|
|
|
411
|
+
draw_lyric_event(xml, event, note_points, baseline)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def draw_lyric_event(xml, event, note_points, baseline)
|
|
417
|
+
y = baseline + (style.staff_space * 6.5)
|
|
418
|
+
case event[:type]
|
|
419
|
+
when :text
|
|
420
|
+
point = note_points[event[:element]&.object_id]
|
|
421
|
+
return unless point
|
|
422
|
+
|
|
423
|
+
draw_text(xml, event[:syllable], x: point.first, y: y,
|
|
424
|
+
fill: "black", "font-size": 10, "text-anchor": "middle", class: "lyric")
|
|
425
|
+
when :hyphen
|
|
426
|
+
from = note_points[event[:from]&.object_id]
|
|
427
|
+
to = note_points[event[:to]&.object_id]
|
|
428
|
+
return unless from && to
|
|
429
|
+
|
|
430
|
+
draw_text(xml, "-", x: (from.first + to.first) / 2.0, y: y,
|
|
431
|
+
fill: "black", "font-size": 10, "text-anchor": "middle", class: "lyric-hyphen")
|
|
432
|
+
when :extender
|
|
433
|
+
from = note_points[event[:from]&.object_id]
|
|
434
|
+
to = note_points[event[:to]&.object_id]
|
|
435
|
+
return unless from && to
|
|
436
|
+
|
|
437
|
+
xml.line(x1: from.first + 8, y1: y - 4, x2: to.first - 8, y2: y - 4,
|
|
438
|
+
stroke: "black", "stroke-width": 1, class: "lyric-extender")
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def draw_ledger_lines(xml, x, y, baseline)
|
|
443
|
+
top = baseline
|
|
444
|
+
bottom = baseline + (style.staff_space * 4)
|
|
445
|
+
return if y.between?(top, bottom)
|
|
446
|
+
|
|
447
|
+
start_line = (y < top) ? y : bottom + style.staff_space
|
|
448
|
+
end_line = (y < top) ? top - style.staff_space : y
|
|
449
|
+
current = nearest_staff_line(start_line)
|
|
450
|
+
while current <= end_line
|
|
451
|
+
xml.line(x1: x - 6, y1: current, x2: x + 6, y2: current,
|
|
452
|
+
stroke: "black", "stroke-width": 1, class: "ledger-line")
|
|
453
|
+
current += style.staff_space
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def draw_clef(xml, clef, x, baseline)
|
|
458
|
+
draw_text(xml, fallback_clef_text(clef), x: x, y: baseline + (style.staff_space * 3.2),
|
|
459
|
+
class: "clef", "font-size": 22, fill: "black")
|
|
460
|
+
x + 24
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def draw_key_signature(xml, key_signature, x, baseline)
|
|
464
|
+
return x unless key_signature
|
|
465
|
+
|
|
466
|
+
accidentals = key_signature.accidentals
|
|
467
|
+
order = (accidentals[:type] == :sharp) ? NotationHelpers::SHARP_ORDER : NotationHelpers::FLAT_ORDER
|
|
468
|
+
text = (accidentals[:type] == :sharp) ? "#" : "b"
|
|
469
|
+
order.first(accidentals[:count].to_i).each_with_index do |note_name, index|
|
|
470
|
+
draw_text(xml, text, x: x + (index * 8), y: baseline + key_signature_y(note_name),
|
|
471
|
+
class: "key-signature", "font-size": 12, fill: "black")
|
|
472
|
+
end
|
|
473
|
+
x + (accidentals[:count].to_i * 8)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def draw_time_signature(xml, time_signature, x, baseline)
|
|
477
|
+
return x unless time_signature
|
|
478
|
+
|
|
479
|
+
draw_text(xml, time_signature.numerator.to_s, x: x, y: baseline + (style.staff_space * 1.6),
|
|
480
|
+
class: "time-signature numerator", "font-size": 11, fill: "black", "text-anchor": "middle")
|
|
481
|
+
draw_text(xml, time_signature.denominator.to_s, x: x, y: baseline + (style.staff_space * 3.3),
|
|
482
|
+
class: "time-signature denominator", "font-size": 11, fill: "black", "text-anchor": "middle")
|
|
483
|
+
x + 14
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def draw_barline(xml, x, baseline)
|
|
487
|
+
xml.line(x1: x, y1: baseline, x2: x, y2: baseline + (style.staff_space * 4),
|
|
488
|
+
stroke: "black", "stroke-width": 1, class: "barline")
|
|
192
489
|
end
|
|
193
490
|
|
|
194
491
|
def pitch_y(pitch, baseline, clef)
|
|
195
|
-
calculate_pitch_y(pitch, baseline, clef, vertical_axis:
|
|
492
|
+
calculate_pitch_y(pitch, baseline, clef, vertical_axis: drawing_context.vertical_axis)
|
|
196
493
|
end
|
|
197
494
|
|
|
198
495
|
def draw_text(xml, content, **attributes)
|
|
@@ -201,6 +498,122 @@ module Clef
|
|
|
201
498
|
node.content = content.to_s
|
|
202
499
|
xml.parent << node
|
|
203
500
|
end
|
|
501
|
+
|
|
502
|
+
def x_for_moment(positions, moment, start_x, position_offset: 0.0)
|
|
503
|
+
return start_x unless positions
|
|
504
|
+
|
|
505
|
+
start_x + positions.fetch(moment, position_offset.to_f) - position_offset.to_f
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def measure_length_for(measure)
|
|
509
|
+
return measure.time_signature.measure_length if measure.time_signature
|
|
510
|
+
|
|
511
|
+
measure.voices.values.map(&:total_length).max || Rational(0, 1)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def voice_vertical_offset(index)
|
|
515
|
+
return 0 if index.zero?
|
|
516
|
+
|
|
517
|
+
index.odd? ? style.staff_space * 0.8 : -style.staff_space * 0.8
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def rest_y(rest, baseline)
|
|
521
|
+
offset = {
|
|
522
|
+
whole: 2.8,
|
|
523
|
+
half: 2.8,
|
|
524
|
+
quarter: 2.3,
|
|
525
|
+
eighth: 2.0,
|
|
526
|
+
sixteenth: 2.0
|
|
527
|
+
}.fetch(rest.duration.base, 2.0)
|
|
528
|
+
baseline + (style.staff_space * offset)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def fallback_clef_text(clef)
|
|
532
|
+
{
|
|
533
|
+
treble: "G",
|
|
534
|
+
bass: "F",
|
|
535
|
+
alto: "C",
|
|
536
|
+
tenor: "C",
|
|
537
|
+
percussion: "||",
|
|
538
|
+
tab: "TAB"
|
|
539
|
+
}.fetch(clef.type, clef.type.to_s[0].upcase)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def key_signature_y(note_name)
|
|
543
|
+
{
|
|
544
|
+
f: style.staff_space * 0.9,
|
|
545
|
+
c: style.staff_space * 2.4,
|
|
546
|
+
g: style.staff_space * 0.5,
|
|
547
|
+
d: style.staff_space * 2.0,
|
|
548
|
+
a: style.staff_space * 3.4,
|
|
549
|
+
e: style.staff_space * 1.5,
|
|
550
|
+
b: style.staff_space * 3.0
|
|
551
|
+
}.fetch(note_name)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def chord_note_offsets(notes)
|
|
555
|
+
notes.each_with_index.map do |note, index|
|
|
556
|
+
previous = notes[index - 1]
|
|
557
|
+
(previous && (diatonic_step(note.pitch) - diatonic_step(previous.pitch)).abs == 1) ? 6 : 0
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def accidental_offset(index)
|
|
562
|
+
index * 4
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def nearest_staff_line(value)
|
|
566
|
+
(value / style.staff_space).floor * style.staff_space
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def tempo_text(tempo)
|
|
570
|
+
"#{tempo.beat_unit.to_lilypond} = #{tempo.bpm}"
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def svg_height(score, layout)
|
|
574
|
+
systems = Array(layout&.dig(:systems))
|
|
575
|
+
return [STAFF_TOP + (score.staves.length * style.staff_gap) + style.staff_gap, 180].max if systems.empty?
|
|
576
|
+
|
|
577
|
+
page_count = systems.map(&:page_index).max.to_i + 1
|
|
578
|
+
[STAFF_TOP + (page_count * svg_page_height_for(score)) + style.system_gap, 180].max
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def svg_page_height_for(score)
|
|
582
|
+
STAFF_TOP + (score.staves.length * style.staff_gap) + (style.system_gap * 2)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def svg_system_baseline(system, staff)
|
|
586
|
+
STAFF_TOP + (system.page_index * @system_page_height.to_f) + system.line_top + system.staff_offset(staff.id)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def draw_layout_items(xml, layout, system)
|
|
590
|
+
Array(layout[:items]).each do |item|
|
|
591
|
+
next unless item.type == :text
|
|
592
|
+
next unless system.include_moment?(item.moment)
|
|
593
|
+
|
|
594
|
+
x = x_for_moment(layout[:positions], item.moment, STAFF_START_X, position_offset: system.position_offset)
|
|
595
|
+
y = STAFF_TOP + (system.page_index * @system_page_height.to_f) + system.line_top - style.staff_space
|
|
596
|
+
draw_text(xml, item.payload.fetch(:text), x: x, y: y, class: "layout-item", "font-size": 10, fill: "black")
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def drawing_context
|
|
601
|
+
@drawing_context ||= Clef::Renderer::DrawingContext.svg
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def write_output(target, content)
|
|
605
|
+
return target.write(content) if target.respond_to?(:write)
|
|
606
|
+
|
|
607
|
+
ensure_parent_directory!(target)
|
|
608
|
+
File.write(target, content)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def ensure_parent_directory!(path)
|
|
612
|
+
parent = File.dirname(path.to_s)
|
|
613
|
+
return if parent.nil? || parent == "." || Dir.exist?(parent)
|
|
614
|
+
|
|
615
|
+
raise ArgumentError, "output directory does not exist: #{parent}"
|
|
616
|
+
end
|
|
204
617
|
end
|
|
205
618
|
end
|
|
206
619
|
end
|