fretboards 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/bin/fretboards_render +13 -0
- data/fretboards.gemspec +24 -0
- data/lib/..rb +5 -0
- data/lib/fretboards/ext/hash.rb +23 -0
- data/lib/fretboards/fretboard.rb +205 -0
- data/lib/fretboards/fretboard_collection.rb +74 -0
- data/lib/fretboards/pitch.rb +52 -0
- data/lib/fretboards/renderer/base.rb +13 -0
- data/lib/fretboards/renderer/svg.rb +304 -0
- data/lib/fretboards/tuning.rb +6 -0
- data/lib/fretboards/version.rb +3 -0
- data/lib/fretboards.rb +4 -0
- data/readme.mdown +5 -0
- data/test/test_fretboard.rb +81 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NDBhZmZlM2VkZWIzMWNmY2VhZGY1YWFkMDY2MGE3ZmM4MjZkYzkzYg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZGRmYzc2NDc3ZTgxN2QxZGFhNjA1YmM1YWZjYjZmMzdiYTAwOGQyMg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NWRkNGEzZTI4Y2NmODQyZDE5MjczMTczOTE3NjRmM2FkMmFjYjQ4OWM3MjM5
|
10
|
+
MTE2MjY5MTM4M2FiZjIyNjdhNzY4NWI1NGJmMzRmODI0M2I0ZjgwYzkyOWQw
|
11
|
+
NTg0OTVkYmY1YWY2N2E1YmJhNzM4ZmFmZDhmOTY4YmI5ZjEwZTQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
OWViNjc0NmVmZjYzODFiMmUyOTdhMmU5ODc2ZDE4MDQxZmFhY2FiM2E4M2Zi
|
14
|
+
ZTYyZjFhOTI1YWRmNTU2Y2U1YWZlOTI1MjY3MjQ0MzJmODI2YmM2MjkyMzUw
|
15
|
+
YjdmZWQ5Y2ZjYmNjNjcyMDU1MDgxNmMyM2NhOTU3MTM5MTA5MTQ=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "fretboards"
|
4
|
+
require "fretboards/renderer/svg"
|
5
|
+
|
6
|
+
# TODO make something useful
|
7
|
+
terse = ARGV
|
8
|
+
# puts terse.inspect
|
9
|
+
|
10
|
+
fb = Fretboards::Fretboard.new(:tuning => %w{g' c' e' a'})
|
11
|
+
fb.terse(ARGV)
|
12
|
+
renderer = Fretboards::Renderer::Svg.new()
|
13
|
+
puts renderer.render(fb)
|
data/fretboards.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "fretboards/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "fretboards"
|
7
|
+
s.version = Fretboards::VERSION
|
8
|
+
s.authors = ["Choan Galvez"]
|
9
|
+
s.email = ["choan.galvez@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = 'Define and draw fretboards'
|
12
|
+
s.description = 'Allows defining instrument fretboard structures and representing them as highly customizable SVG graphics.'
|
13
|
+
|
14
|
+
s.rubyforge_project = "fretboards"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
s.add_runtime_dependency "builder"
|
24
|
+
end
|
data/lib/..rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Fretboards
|
2
|
+
module Ext
|
3
|
+
module Hash
|
4
|
+
|
5
|
+
def deep_merge(other_hash)
|
6
|
+
dup.deep_merge!(other_hash)
|
7
|
+
end
|
8
|
+
|
9
|
+
def deep_merge!(other_hash)
|
10
|
+
other_hash.each_pair do |k,v|
|
11
|
+
tv = self[k]
|
12
|
+
self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
|
13
|
+
end
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Hash
|
22
|
+
include Fretboards::Ext::Hash
|
23
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require "fretboards/pitch"
|
2
|
+
|
3
|
+
module Fretboards
|
4
|
+
class Fretboard
|
5
|
+
|
6
|
+
attr_reader :marks, :labels, :barres, :conf, :opens, :mutes
|
7
|
+
|
8
|
+
attr_accessor :title
|
9
|
+
|
10
|
+
def initialize(conf = {}, &block)
|
11
|
+
@labels = []
|
12
|
+
@marks = []
|
13
|
+
@barres = []
|
14
|
+
@conf = {
|
15
|
+
# default tuning is ukulele tuning
|
16
|
+
:tuning => Fretboards::Tuning::UKULELE
|
17
|
+
}
|
18
|
+
@mutes = []
|
19
|
+
@opens = []
|
20
|
+
configure(conf)
|
21
|
+
self.instance_eval block if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def configure(conf)
|
26
|
+
@conf.update(conf)
|
27
|
+
end
|
28
|
+
|
29
|
+
def terse(a, opts = {})
|
30
|
+
a = a.split(/\s+/) if a.is_a?(String)
|
31
|
+
self.title = opts[:title] if opts[:title]
|
32
|
+
barres = {}
|
33
|
+
a.each_with_index do |m, i|
|
34
|
+
attrs = {}
|
35
|
+
attrs[:string] = m.match(%r{/(\d+)})[1].to_i rescue index_to_string_number(i)
|
36
|
+
attrs[:fret] = m.match(%r{^(\d+)})[1].to_i rescue nil
|
37
|
+
has_mute = m.start_with?('x')
|
38
|
+
if !attrs[:fret] && !has_mute
|
39
|
+
if pitch = m.match(/^([a-g](es|is){0,2}[',]*)/)[1]
|
40
|
+
attrs[:fret] = pitch_to_fret(pitch, attrs[:string])
|
41
|
+
attrs[:pitch] = pitch
|
42
|
+
end
|
43
|
+
end
|
44
|
+
attrs[:finger] = m.match(%r{-(\d+)})[1].to_i rescue nil
|
45
|
+
attrs[:function] = m.match(%r{\(([^\)]*)\)})[1] rescue nil
|
46
|
+
|
47
|
+
if m.include?("!") && m.include?("?")
|
48
|
+
attrs[:symbol] = :phantom_root
|
49
|
+
else
|
50
|
+
attrs[:symbol] = :root if m.include?("!")
|
51
|
+
attrs[:symbol] = :phantom if m.include?("?")
|
52
|
+
end
|
53
|
+
|
54
|
+
attrs.reject! { |k, v| v.nil? }
|
55
|
+
mark(attrs) if attrs[:fret]
|
56
|
+
|
57
|
+
mute(attrs[:string]) if has_mute
|
58
|
+
|
59
|
+
bs = m[-1..-1] == "["
|
60
|
+
barres[attrs[:fret]] = [attrs[:fret], attrs[:string]] if bs
|
61
|
+
be = m[-1..-1] == "]"
|
62
|
+
barres[attrs[:fret]] << attrs[:string] if be
|
63
|
+
end
|
64
|
+
|
65
|
+
barres.each do |k, b|
|
66
|
+
fret, f, t = *b
|
67
|
+
barre(fret, f, t || 1)
|
68
|
+
end
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
def mark(s, f = nil, settings = {})
|
75
|
+
if !s.is_a? Hash
|
76
|
+
s = { :string => s, :fret => f }.update(settings)
|
77
|
+
else
|
78
|
+
s = {}.merge(s)
|
79
|
+
end
|
80
|
+
@marks.push(s)
|
81
|
+
end
|
82
|
+
|
83
|
+
def label(number, offset = 0)
|
84
|
+
# TODO we're still not painting this
|
85
|
+
@labels[offset] = number
|
86
|
+
end
|
87
|
+
|
88
|
+
def mute(s)
|
89
|
+
@mutes << s
|
90
|
+
end
|
91
|
+
|
92
|
+
def open(s)
|
93
|
+
mark({:fret => 0, :string => s})
|
94
|
+
end
|
95
|
+
|
96
|
+
def barre(fret, from = :max, to = 1)
|
97
|
+
if fret.is_a? Hash
|
98
|
+
b = {}.update(fret)
|
99
|
+
else
|
100
|
+
from = index_to_string_number(0) if from == :max
|
101
|
+
b = {:fret => fret, :from => from, :to => to}
|
102
|
+
end
|
103
|
+
@barres.push(b)
|
104
|
+
end
|
105
|
+
|
106
|
+
def string_count
|
107
|
+
# @conf[:string_count] ||
|
108
|
+
@conf[:tuning].length
|
109
|
+
end
|
110
|
+
|
111
|
+
def pitch_to_fret(pitch, string)
|
112
|
+
diff = Pitch.to_diff(pitch)
|
113
|
+
tunings = tuning_to_diffs
|
114
|
+
t = tunings[string_number_to_index(string)]
|
115
|
+
# TODO warn if < 0
|
116
|
+
diff - t
|
117
|
+
end
|
118
|
+
|
119
|
+
def mark_pitch(pitch, opts = { })
|
120
|
+
diff = Pitch.to_diff(pitch)
|
121
|
+
tunings = tuning_to_diffs
|
122
|
+
if !opts[:string]
|
123
|
+
tunings.each_with_index do |t, i|
|
124
|
+
# puts diff - t
|
125
|
+
if t <= diff && (!opts[:range] || opts[:range].include?(diff - t))
|
126
|
+
# puts pitch, diff-t, opts[:range]
|
127
|
+
mark(index_to_string_number(i), diff - t, :symbol => opts[:symbol], :finger => opts[:finger])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
else
|
131
|
+
# TODO Warning if the fret number is negative
|
132
|
+
mark(opts[:string], diff - tunings[string_number_to_index(string)])
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def index_to_string_number(i)
|
137
|
+
string_count - i
|
138
|
+
end
|
139
|
+
|
140
|
+
def string_number_to_index(i)
|
141
|
+
# 4 -> 0
|
142
|
+
# 1 -> 3
|
143
|
+
-(i - string_count)
|
144
|
+
end
|
145
|
+
|
146
|
+
def tuning_to_diffs
|
147
|
+
@tuning_diffs ||= @conf[:tuning].map { |p| Pitch.to_diff(p) }
|
148
|
+
end
|
149
|
+
|
150
|
+
def clone
|
151
|
+
copy = Fretboard.new
|
152
|
+
copy.configure(@conf)
|
153
|
+
@marks.each { |m| copy.mark(m) }
|
154
|
+
@barres.each { |m| copy.barre(m) }
|
155
|
+
@mutes.each { |m| copy.mute(m) }
|
156
|
+
copy
|
157
|
+
end
|
158
|
+
|
159
|
+
def transpose(steps)
|
160
|
+
copy = self.clone
|
161
|
+
copy.transpose_marks(steps)
|
162
|
+
copy.transpose_barres(steps)
|
163
|
+
copy
|
164
|
+
end
|
165
|
+
|
166
|
+
alias :+ transpose
|
167
|
+
|
168
|
+
def -(delta)
|
169
|
+
transpose(-delta)
|
170
|
+
end
|
171
|
+
|
172
|
+
def transpose_marks(steps)
|
173
|
+
@marks.each do |m|
|
174
|
+
m[:fret] += steps
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def transpose_barres(steps)
|
179
|
+
@barres.each do |b|
|
180
|
+
b[:fret] += steps
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def fret_range(size = 4)
|
185
|
+
if marks.empty?
|
186
|
+
[1, size]
|
187
|
+
else
|
188
|
+
min = marks.inject { |sum, i| i[:fret] < sum[:fret] ? i : sum }[:fret]
|
189
|
+
max = marks.inject { |sum, i| i[:fret] > sum[:fret] ? i : sum }[:fret]
|
190
|
+
if size >= max
|
191
|
+
[1, size]
|
192
|
+
else
|
193
|
+
# puts "#{self.title} pasa por el segundo hilo"
|
194
|
+
min = 1 if min == 0
|
195
|
+
max = (min + size) if (size > (max - min) )
|
196
|
+
[min, max]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
|
203
|
+
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'fretboards/renderer/svg'
|
2
|
+
|
3
|
+
module Fretboards
|
4
|
+
|
5
|
+
class FretboardCollection
|
6
|
+
|
7
|
+
def initialize(settings = {})
|
8
|
+
@col = []
|
9
|
+
@forms = {}
|
10
|
+
@tuning = %w{ g' c' e' a' }
|
11
|
+
@table = "default"
|
12
|
+
end
|
13
|
+
|
14
|
+
def set_tuning(a)
|
15
|
+
@tuning = a
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(dots, attrs = {})
|
19
|
+
fb = fretboard(dots, attrs)
|
20
|
+
@col << fb
|
21
|
+
fb
|
22
|
+
end
|
23
|
+
|
24
|
+
def define(title, a, attrs = {})
|
25
|
+
form_add(title, a, { :title => title }.merge(attrs))
|
26
|
+
end
|
27
|
+
|
28
|
+
def use(title)
|
29
|
+
raise "#{title} form not available" unless @forms[title]
|
30
|
+
@forms[title]
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def fretboard(dots, opts = {})
|
35
|
+
if opts.is_a? String
|
36
|
+
attrs = {}
|
37
|
+
attrs[:title] = opts
|
38
|
+
else
|
39
|
+
attrs = opts.dup
|
40
|
+
end
|
41
|
+
if dots.is_a? Fretboard
|
42
|
+
fb = dots
|
43
|
+
fb.title = attrs[:title] if attrs[:title]
|
44
|
+
else
|
45
|
+
fb = Fretboard.new(:tuning => @tuning)
|
46
|
+
fb.terse(dots, attrs)
|
47
|
+
end
|
48
|
+
fb
|
49
|
+
end
|
50
|
+
|
51
|
+
def form_add(name, a, attrs = {})
|
52
|
+
add(form_create(name, a, attrs))
|
53
|
+
end
|
54
|
+
|
55
|
+
def form_create(name, a, attrs = {})
|
56
|
+
@forms[name] = fretboard(a, attrs)
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def renderer(settings = {})
|
61
|
+
Renderer::Svg.new(settings)
|
62
|
+
end
|
63
|
+
|
64
|
+
def render_to_files(output_dir = '.', settings = {})
|
65
|
+
# TODO may we use a filenaming lambda?
|
66
|
+
r = renderer(settings)
|
67
|
+
@col.each do |fb|
|
68
|
+
File.open("#{output_dir}/#{fb.title.gsub(/[^A-z0-9]/, "_")}.svg", "w") { |f| f.puts(r.render(fb)) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Fretboards
|
2
|
+
module Pitch
|
3
|
+
|
4
|
+
TABLE = {
|
5
|
+
"c" => 0,
|
6
|
+
"cis" => 1,
|
7
|
+
"des" => 1,
|
8
|
+
"d" => 2,
|
9
|
+
"dis" => 3,
|
10
|
+
"ees" => 3,
|
11
|
+
"e" => 4,
|
12
|
+
"f" => 5,
|
13
|
+
"fis" => 6,
|
14
|
+
"ges" => 6,
|
15
|
+
"g" => 7,
|
16
|
+
"gis" => 8,
|
17
|
+
"aes" => 8,
|
18
|
+
"a" => 9,
|
19
|
+
"beses" => 9,
|
20
|
+
"ais" => 10,
|
21
|
+
"bes" => 10,
|
22
|
+
"b" => 11
|
23
|
+
}
|
24
|
+
|
25
|
+
def self.to_diff(name)
|
26
|
+
pitch, alt, octave = name.scan(/([a-g](es|is){0,2})([',]*)/)[0]
|
27
|
+
base = TABLE[pitch]
|
28
|
+
octave_shift = if octave.nil?
|
29
|
+
-12
|
30
|
+
elsif octave.start_with?(",")
|
31
|
+
-12 * (octave.length + 1)
|
32
|
+
else
|
33
|
+
12 * (octave.length - 1)
|
34
|
+
end
|
35
|
+
# pp octave_shift
|
36
|
+
diff = base + octave_shift
|
37
|
+
diff
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.from_diff(diff)
|
41
|
+
diff += 12
|
42
|
+
octave_direction = diff > 0 ? 1 : -1
|
43
|
+
abs_diff = diff.abs
|
44
|
+
octave_shift = diff / 12
|
45
|
+
shift = diff % 12
|
46
|
+
s_pitch = TABLE.find { |k, v| v == shift }.first
|
47
|
+
suffix = (octave_direction > 0 ? "'" : ',') * octave_shift
|
48
|
+
s_pitch + suffix
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,304 @@
|
|
1
|
+
require "fretboards/renderer/base"
|
2
|
+
require "fretboards/ext/hash"
|
3
|
+
|
4
|
+
module Fretboards
|
5
|
+
module Renderer
|
6
|
+
class Svg < Base
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
# TODO configuration should merge recursively
|
10
|
+
@opts = {
|
11
|
+
:string_attrs => { :"stroke-width" => 1, :stroke => "#222" },
|
12
|
+
:fret_attrs => { :"stroke-width" => 1, :stroke => "#222", :"stroke-linecap" => "round" },
|
13
|
+
:rectangular_fret_attrs => { :height => 1.5, :fill => "#666", :stroke => "#666", :"stroke-width" => 1, :rx => 1, :ry => 1 },
|
14
|
+
:nut_attrs => { :"stroke-width" => 5, :stroke => "#222", :"stroke-linecap" => "round"},
|
15
|
+
:dot_attrs => { :fill => "#000", :r => 8 },
|
16
|
+
:open_attrs => { :"stroke-width" => 1, :stroke => "#000", :fill => "#fff", :r => 3 },
|
17
|
+
:open_root_symbol_attrs => { :"stroke-width" => 2, :r => 3 },
|
18
|
+
:open_phantom_root_symbol_attrs => { :"stroke-width" => 1, :r => 3, :"stroke-dasharray" => "1 1" },
|
19
|
+
:svg_attrs => { :xmlns => "http://www.w3.org/2000/svg", :version => "1.1" },
|
20
|
+
:in_dot_attrs => { :"text-anchor" => "middle", :fill => "#fff", :"font-weight" => "normal", :"font-size" => 10, :"font-family" => "sans-serif", :"font-weight" => "bold"},
|
21
|
+
:in_dot_root_symbol_attrs => { :fill => "#000", :"font-weight" => "bold" },
|
22
|
+
:in_bottom_attrs => { :"text-anchor" => "middle", :fill => "#000", :"font-weight" => "normal", :"font-size" => 8, :"font-family" => "sans-serif" },
|
23
|
+
:label_attrs => { :"text-anchor" => "end", :fill => "#000", :"font-size" => 10, :"font-family" => 'sans-serif' },
|
24
|
+
:root_symbol_attrs => { :fill => "#fff", :stroke => "#000", :r => 7, :"stroke-width" => 2 },
|
25
|
+
:phantom_root_symbol_attrs => { :fill => "#fff", :stroke => "#333", :r => 7, :"stroke-width" => 1, :"stroke-dasharray" => "1 1" },
|
26
|
+
:title_attrs => { :"text-anchor" => "middle", :fill => "#000", :"font-weight" => "bold", :"font-size" => 18, :"font-family" => "sans-serif" } ,
|
27
|
+
:mute_attrs => { :stroke => "#000"},
|
28
|
+
:barre_attrs => { :height => 20, :stroke => "#000", :fill => "#fff", :rx => 9, :ry => 9 },
|
29
|
+
:group_attrs => { },
|
30
|
+
:in_dot => :finger,
|
31
|
+
:in_bottom => :function,
|
32
|
+
:padding_left => 20,
|
33
|
+
:padding_right => 15,
|
34
|
+
:padding_top => 30,
|
35
|
+
:padding_bottom => 20,
|
36
|
+
:height => 180,
|
37
|
+
:width => 108,
|
38
|
+
:string_ext_bottom => 5,
|
39
|
+
:string_ext_top => 5,
|
40
|
+
:fret_ext_left => 2,
|
41
|
+
:fret_ext_right => 2,
|
42
|
+
:string_widths => [ 2, 2, 2, 2 ], # TODO calculate on demand if not passed
|
43
|
+
:fret_reduction_factor => 0.95,
|
44
|
+
:rectangular_frets => true,
|
45
|
+
:fret_count => 4,
|
46
|
+
:show_labels => true,
|
47
|
+
:show_title => true,
|
48
|
+
:open_margin_bottom => 4,
|
49
|
+
}.deep_merge(opts)
|
50
|
+
end
|
51
|
+
|
52
|
+
def render(fb)
|
53
|
+
@fb = fb
|
54
|
+
require "builder"
|
55
|
+
xml = ::Builder::XmlMarkup.new(:indent => 2)
|
56
|
+
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
57
|
+
view_box = [ @opts[:width], @opts[:height] ]
|
58
|
+
view_box = view_box.reverse if @opts[:landscape]
|
59
|
+
xml.svg(@opts[:svg_attrs].merge(:viewBox => "0 0 #{view_box.join(' ')}")) do |svg|
|
60
|
+
svg.g(@opts[:group_attrs]) do
|
61
|
+
svg.g(landscape_attributes) do
|
62
|
+
draw_title(svg) unless @opts[:show_title] == false
|
63
|
+
draw_frets(svg)
|
64
|
+
draw_strings(svg)
|
65
|
+
draw_barres(svg)
|
66
|
+
draw_marks(svg)
|
67
|
+
draw_in_dots(svg, @opts[:in_dot]) if @opts[:in_dot]
|
68
|
+
draw_in_bottom(svg, @opts[:in_bottom]) if @opts[:in_bottom]
|
69
|
+
draw_labels(svg) unless @opts[:show_labels] == false
|
70
|
+
# draw_open(svg)
|
71
|
+
draw_mutes(svg)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
# @svg
|
76
|
+
# xml
|
77
|
+
end
|
78
|
+
|
79
|
+
def landscape_attributes
|
80
|
+
if @opts[:landscape]
|
81
|
+
{
|
82
|
+
:transform => "rotate(-90 #{@opts[:width]} 0) translate(0 -#{@opts[:width]})"
|
83
|
+
}
|
84
|
+
else
|
85
|
+
{}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def string_attrs
|
90
|
+
@opts[:string_attrs]
|
91
|
+
end
|
92
|
+
|
93
|
+
def fret_attrs
|
94
|
+
@opts[:fret_attrs]
|
95
|
+
end
|
96
|
+
|
97
|
+
def nut_attrs
|
98
|
+
@opts[:nut_attrs]
|
99
|
+
end
|
100
|
+
|
101
|
+
def string_spacing
|
102
|
+
(@opts[:width] - @opts[:padding_left] - @opts[:padding_right]) / (@fb.string_count - 1)
|
103
|
+
end
|
104
|
+
|
105
|
+
def draw_strings(svg)
|
106
|
+
(0..@fb.string_count-1).each do |sn|
|
107
|
+
# x = @opts[:padding_left] + sn * string_spacing(fb)
|
108
|
+
x = get_string_x(@fb.index_to_string_number(sn))
|
109
|
+
y1 = @opts[:padding_top]
|
110
|
+
y2 = @opts[:height] - @opts[:padding_bottom]
|
111
|
+
attrs = string_attrs.merge(:x1 => x, :x2 => x, :y1 => y1, :y2 => y2, :class => 'string')
|
112
|
+
if (!@opts[:string_widths].empty?)
|
113
|
+
attrs = attrs.merge({ :"stroke-width" => @opts[:string_widths][sn] })
|
114
|
+
end
|
115
|
+
svg.line(attrs)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def draw_title(svg)
|
120
|
+
# TODO calculate ideal gap
|
121
|
+
gap = @opts[:title_attrs][:"font-size"]
|
122
|
+
svg.text(@fb.title, { :x => @opts[:width] * 0.5 + ((@opts[:padding_left] - @opts[:padding_right])*0.5), :y => @opts[:padding_top] - gap, :class => 'title' }.merge(@opts[:title_attrs]))
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_string_x(sn)
|
126
|
+
sn = @fb.string_number_to_index(sn)
|
127
|
+
@opts[:padding_left] + sn * string_spacing
|
128
|
+
end
|
129
|
+
|
130
|
+
def draw_frets(svg)
|
131
|
+
fret_range = @fb.fret_range(@opts[:fret_count])
|
132
|
+
total_frets = fret_range.last - fret_range.first
|
133
|
+
total_frets += 1 if (fret_range.first == 1)
|
134
|
+
# nut / first line
|
135
|
+
if fret_range.first == 1
|
136
|
+
draw_nut(svg)
|
137
|
+
else
|
138
|
+
draw_fret(svg, 0)
|
139
|
+
end
|
140
|
+
total_frets.times do |n|
|
141
|
+
draw_fret(svg, n+1)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def draw_labels(svg)
|
146
|
+
fret_range = @fb.fret_range(@opts[:fret_count])
|
147
|
+
if fret_range.first > 1
|
148
|
+
y = get_dot_position(0, fret_range.first)[1] + @opts[:label_attrs][:"font-size"] * 0.4
|
149
|
+
x = @opts[:padding_left] - @opts[:label_attrs][:"font-size"] * 1.25
|
150
|
+
# TODO allow rotating
|
151
|
+
svg.text(fret_range.first, { :y => y, :x => x, :class => 'label' }.merge(@opts[:label_attrs]))
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def draw_fret(svg, n)
|
156
|
+
y = get_fret_y(n)
|
157
|
+
if @opts[:rectangular_frets]
|
158
|
+
svg.rect({ :y => y - 1.25, :x => @opts[:padding_left] - @opts[:fret_ext_left], :width => @opts[:width] - @opts[:padding_left] - @opts[:padding_right] + @opts[:fret_ext_left] + @opts[:fret_ext_right], :class => 'fret' }.merge(@opts[:rectangular_fret_attrs]))
|
159
|
+
else
|
160
|
+
svg.line(fret_attrs.merge(:x1 => @opts[:padding_left], :x2 => @opts[:width] - @opts[:padding_right], :y1 => y, :y2 => y, :class => 'fret'))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def draw_nut(svg)
|
165
|
+
y = @opts[:padding_top] - @opts[:nut_attrs][:"stroke-width"] * 0.5
|
166
|
+
extra_first = 0 # @opts[:string_widths][0] * 0.5
|
167
|
+
extra_last = 0 # @opts[:string_widths].last * 0.5
|
168
|
+
svg.line(nut_attrs.merge(:x1 => @opts[:padding_left] - extra_first, :x2 => @opts[:width] - @opts[:padding_right] + extra_last, :y1 => y, :y2 => y, :class => 'nut'))
|
169
|
+
end
|
170
|
+
|
171
|
+
def get_fret_y(fret_number)
|
172
|
+
fret_range = @fb.fret_range(@opts[:fret_count])
|
173
|
+
avail = @opts[:height] - @opts[:padding_top] - @opts[:padding_bottom] - @opts[:string_ext_bottom]
|
174
|
+
avail -= @opts[:string_ext_top] if (fret_range.first != 1)
|
175
|
+
total_frets = fret_range.last - fret_range.first
|
176
|
+
total_frets += 1 if (fret_range.first == 1)
|
177
|
+
start = @opts[:padding_top]
|
178
|
+
start += @opts[:string_ext_top] if (fret_range.first != 1)
|
179
|
+
ff_size = get_first_fret_size(total_frets, @opts[:fret_reduction_factor], avail)
|
180
|
+
# size_each = avail.to_f / total_frets
|
181
|
+
# y = start
|
182
|
+
y = (0..fret_number-1).inject(start) do |sum, f|
|
183
|
+
sum + ff_size*@opts[:fret_reduction_factor]**f
|
184
|
+
end
|
185
|
+
y
|
186
|
+
end
|
187
|
+
|
188
|
+
def get_first_fret_size(gaps, factor, avail)
|
189
|
+
sum = 0
|
190
|
+
(0..gaps-1).each do |t|
|
191
|
+
sum += factor**t
|
192
|
+
end
|
193
|
+
avail.to_f/sum.to_f
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
def draw_marks(svg)
|
198
|
+
@fb.marks.each do |m|
|
199
|
+
if m[:fret] == 0
|
200
|
+
draw_open(svg, m)
|
201
|
+
else
|
202
|
+
x, y = *get_dot_position(m[:string], m[:fret])
|
203
|
+
method_name = ("draw_" + m[:symbol].to_s + "_symbol").to_sym
|
204
|
+
if m[:symbol] && self.respond_to?(method_name)
|
205
|
+
self.send(method_name, svg, x, y, m)
|
206
|
+
else
|
207
|
+
draw_dot(svg, x, y, m)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def draw_blue_note_symbol(svg, x, y, m)
|
214
|
+
svg.rect(:x => x - 4, :y => y - 4, :width => 8, :height => 8, :fill => "#000", :stroke => "", :transform => "rotate(-45 #{x} #{y})")
|
215
|
+
# svg.circle(:cx => x, :cy => y, :r => 3, :fill => "blue")
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
def draw_dot(svg, x, y, m)
|
220
|
+
cnames = %w[dot]
|
221
|
+
cnames << "dot-#{m[:symbol]}" if m[:symbol]
|
222
|
+
attrs = @opts[:dot_attrs].merge(:cx => x, :cy => y, :class => cnames.join(' '))
|
223
|
+
attrs = attrs.merge(@opts[(m[:symbol].to_s + "_symbol_attrs").to_sym]) if (m[:symbol] && @opts[(m[:symbol].to_s + "_symbol_attrs").to_sym])
|
224
|
+
svg.circle(attrs)
|
225
|
+
end
|
226
|
+
|
227
|
+
def get_dot_position(string, fret)
|
228
|
+
fret_range = @fb.fret_range(@opts[:fret_count])
|
229
|
+
diff = fret_range.first == 1 ? 0 : fret_range.first - 1
|
230
|
+
fret -= diff
|
231
|
+
x = get_string_x(string)
|
232
|
+
y = 0.5 * (get_fret_y(fret - 1) + get_fret_y(fret))
|
233
|
+
[x, y]
|
234
|
+
end
|
235
|
+
|
236
|
+
def draw_in_dots(svg, name)
|
237
|
+
# TODO allow rotating dot text on rotated fretboards
|
238
|
+
sym = name.to_sym
|
239
|
+
sym_attrs = "in_dot_attrs".to_sym
|
240
|
+
@fb.marks.each do |m|
|
241
|
+
if m[sym] && m[:fret] > 0
|
242
|
+
custom_attrs = ("in_dot_" + m[:symbol].to_s + "_symbol_attrs").to_sym
|
243
|
+
x, y = *get_dot_position(m[:string], m[:fret])
|
244
|
+
y += @opts[sym_attrs][:"font-size"] / 3.0
|
245
|
+
attrs = @opts[sym_attrs]
|
246
|
+
attrs = attrs.merge(@opts[custom_attrs]) if (m[:symbol] && @opts[custom_attrs])
|
247
|
+
attrs = attrs.merge(:x => x, :y => y)
|
248
|
+
attrs = attrs.merge(:class => 'text-in-dot')
|
249
|
+
svg.text(m[sym].to_s, attrs)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def draw_in_bottom(svg, name)
|
255
|
+
# TODO allow rotating bottom text on rotated fretboards
|
256
|
+
sym = name.to_sym
|
257
|
+
sym_attrs = "in_bottom_attrs".to_sym
|
258
|
+
@fb.marks.each do |m|
|
259
|
+
if m[sym]
|
260
|
+
x = get_string_x(m[:string])
|
261
|
+
y = @opts[:height] - @opts[:padding_bottom] + @opts[:in_bottom_attrs][:"font-size"] * 1.5
|
262
|
+
attrs = @opts[sym_attrs].merge(:x => x, :y => y, :class => 'text-at-bottom')
|
263
|
+
svg.text(m[sym].to_s, attrs)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def draw_open(svg, m)
|
269
|
+
margin_bottom = @opts[:open_margin_bottom]
|
270
|
+
y = @opts[:padding_top] - @opts[:open_attrs][:r] - @opts[:nut_attrs][:"stroke-width"] - margin_bottom
|
271
|
+
x = get_string_x(m[:string])
|
272
|
+
attrs = {:cx => x, :cy => y, :class => 'open'}.merge(@opts[:open_attrs])
|
273
|
+
symbol_attrs = "open_#{m[:symbol]}_symbol_attrs".to_sym
|
274
|
+
attrs = attrs.merge(@opts[symbol_attrs]) if (m[:symbol] && @opts[symbol_attrs])
|
275
|
+
svg.circle(attrs)
|
276
|
+
end
|
277
|
+
|
278
|
+
def draw_mutes(svg)
|
279
|
+
margin_bottom = @opts[:open_margin_bottom]
|
280
|
+
cy = @opts[:padding_top] - @opts[:open_attrs][:r] - @opts[:nut_attrs][:"stroke-width"] - margin_bottom
|
281
|
+
@fb.mutes.each do |s|
|
282
|
+
delta = 3
|
283
|
+
cx = get_string_x(s)
|
284
|
+
svg.line({:x1 => cx - delta, :x2 => cx + delta, :y1 => cy - delta, :y2 => cy + delta}.merge(@opts[:mute_attrs]))
|
285
|
+
svg.line({:x1 => cx - delta, :x2 => cx + delta, :y1 => cy + delta, :y2 => cy - delta}.merge(@opts[:mute_attrs]))
|
286
|
+
# svg.text("x", { :x => cx, :y => cy })
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def draw_barres(svg)
|
291
|
+
barre_attrs = @opts[:barre_attrs]
|
292
|
+
@fb.barres.each do |b|
|
293
|
+
dot_pos = get_dot_position(b[:from], b[:fret])
|
294
|
+
w = get_string_x(b[:to]) - dot_pos[0] + barre_attrs[:height]
|
295
|
+
x = dot_pos[0] - barre_attrs[:height] * 0.5
|
296
|
+
y = dot_pos[1] - barre_attrs[:height] * 0.5
|
297
|
+
svg.rect({:y => y, :x => x, :width => w, :class => :barre}.merge(barre_attrs))
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
data/lib/fretboards.rb
ADDED
data/readme.mdown
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'fretboards'
|
3
|
+
|
4
|
+
include Fretboards
|
5
|
+
|
6
|
+
# TODO write useful tests
|
7
|
+
class FretboardTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def test_string_count
|
10
|
+
uke = Fretboard.new()
|
11
|
+
assert_equal(4, uke.string_count)
|
12
|
+
|
13
|
+
guit = Fretboard.new(:tuning => Fretboards::Tuning::GUITAR)
|
14
|
+
assert_equal(6, guit.string_count)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_mark
|
18
|
+
fb = Fretboard.new
|
19
|
+
fb.mark :string => 1, :fret => 5
|
20
|
+
assert_equal([{:string => 1, :fret => 5}], fb.marks)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_barre
|
24
|
+
fb = Fretboard.new(:tuning => %w{ g' c' e' a' })
|
25
|
+
fb.barre 1
|
26
|
+
assert_equal([{:fret => 1, :from => 4, :to => 1 }], fb.barres)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_mute
|
30
|
+
fb = Fretboard.new
|
31
|
+
fb.mute(1)
|
32
|
+
assert_equal([1], fb.mutes)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_open
|
36
|
+
fb = Fretboard.new
|
37
|
+
fb.open(1)
|
38
|
+
assert_equal([{:string => 1, :fret => 0}], fb.marks)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_terse_full
|
42
|
+
fb = Fretboard.new
|
43
|
+
fb.terse %w{ 1/4-3(5) }
|
44
|
+
assert_equal([{:string => 4, :finger => 3, :function => "5", :fret => 1}], fb.marks)
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_terse_minimal
|
48
|
+
fb = Fretboard.new
|
49
|
+
fb.terse %w{ 1 }
|
50
|
+
assert_equal([{ :string => 4, :fret => 1 } ], fb.marks)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_terse_barre
|
54
|
+
fb = Fretboard.new
|
55
|
+
fb.terse %w{ 3 1[ 1] 0 }
|
56
|
+
assert_equal([{:fret => 1, :from => 3, :to => 2 }], fb.barres)
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_terse_unfinished_barre
|
60
|
+
fb = Fretboard.new
|
61
|
+
fb.terse %w{ 3 1[ 1 1 }
|
62
|
+
assert_equal([{:fret => 1, :from => 3, :to => 1 }], fb.barres)
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_terse_symbols
|
66
|
+
# puts "symbols"
|
67
|
+
fb = Fretboard.new
|
68
|
+
fb.terse %w{ 3! }
|
69
|
+
assert_equal(:root, fb.marks.first[:symbol])
|
70
|
+
|
71
|
+
fb = Fretboard.new
|
72
|
+
fb.terse %w{ 3? }
|
73
|
+
assert_equal(:phantom, fb.marks.first[:symbol])
|
74
|
+
|
75
|
+
fb = Fretboard.new
|
76
|
+
fb.terse %w{ 3!? }
|
77
|
+
assert_equal(:phantom_root, fb.marks.first[:symbol])
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fretboards
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Choan Galvez
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: builder
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Allows defining instrument fretboard structures and representing them
|
28
|
+
as highly customizable SVG graphics.
|
29
|
+
email:
|
30
|
+
- choan.galvez@gmail.com
|
31
|
+
executables:
|
32
|
+
- fretboards_render
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- .gitignore
|
37
|
+
- Gemfile
|
38
|
+
- Rakefile
|
39
|
+
- bin/fretboards_render
|
40
|
+
- fretboards.gemspec
|
41
|
+
- lib/..rb
|
42
|
+
- lib/fretboards.rb
|
43
|
+
- lib/fretboards/ext/hash.rb
|
44
|
+
- lib/fretboards/fretboard.rb
|
45
|
+
- lib/fretboards/fretboard_collection.rb
|
46
|
+
- lib/fretboards/pitch.rb
|
47
|
+
- lib/fretboards/renderer/base.rb
|
48
|
+
- lib/fretboards/renderer/svg.rb
|
49
|
+
- lib/fretboards/tuning.rb
|
50
|
+
- lib/fretboards/version.rb
|
51
|
+
- readme.mdown
|
52
|
+
- test/test_fretboard.rb
|
53
|
+
homepage: ''
|
54
|
+
licenses: []
|
55
|
+
metadata: {}
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubyforge_project: fretboards
|
72
|
+
rubygems_version: 2.2.2
|
73
|
+
signing_key:
|
74
|
+
specification_version: 4
|
75
|
+
summary: Define and draw fretboards
|
76
|
+
test_files:
|
77
|
+
- test/test_fretboard.rb
|