-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfretboard.elm
372 lines (325 loc) · 12.9 KB
/
fretboard.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
module Main exposing (..)
import Html exposing (Html)
import Style exposing (..)
import Style.Color as Color
import Style.Font as Font
import Style.Border as Border
import Style.Shadow as Shadow
import Element exposing (..)
import Element.Attributes exposing (..)
import Element.Events exposing (..)
import Color exposing (..)
import Style.Scale as Scale
import List
import Dict exposing (Dict)
import String
import Maybe exposing (withDefault)
import Set exposing (Set)
import Char
scale = Scale.modular 16 1.618
setToggle : comparable -> Set comparable -> Set comparable
setToggle el set =
if Set.member el set then Set.remove el set
else Set.insert el set
listShift : Int -> List a -> List a
listShift n lst =
if n < 1 then
lst
else
case (n, lst) of
(n, h::t) -> listShift (n-1) (t ++ [h])
(_, []) -> []
notesInOrder = String.split "," "C,C#,D,D#,E,F,F#,G,G#,A,A#,B"
intervalNames = Dict.fromList [ ( 0, "R")
, ( 1, "b2")
, ( 2, "2")
, ( 3, "b3")
, ( 4, "3")
, ( 5, "4")
, ( 6, "b5")
, ( 7, "5")
, ( 8, "#5")
, ( 9, "6")
, (10, "b7")
, (11, "7")
, (13, "b9")
, (14, "9")
, (15, "#9")
, (16, "b11")
, (17, "11")
, (18, "#11")
, (19, "b13")
, (20, "13")
, (21, "#13")
]
intervalColors = Dict.fromList [ ( 0, Color.black) -- "R"
, ( 1, Color.lightOrange) -- "m2"
, ( 2, Color.orange) -- "2"
, ( 3, Color.lightBlue) -- "m3"
, ( 4, Color.blue) -- "3"
, ( 5, Color.purple) -- "4"
, ( 6, Color.lightGreen) -- "b5"
, ( 7, Color.green) -- "5"
, ( 8, Color.darkGreen) -- "#5"
, ( 9, Color.yellow) -- "6"
, (10, Color.lightRed) -- "b7"
, (11, Color.red) -- "7"
, (13, Color.lightOrange) -- "b9"
, (14, Color.orange) -- "9"
, (15, Color.darkOrange) -- "#9"
, (16, Color.lightPurple) -- "b11"
, (17, Color.purple) -- "11"
, (18, Color.darkPurple) -- "#11"
, (19, Color.lightBrown) -- "b13"
, (20, Color.brown) -- "13"
, (21, Color.darkBrown) -- "#13"
]
-- Interval in semitones between two notes, if valid names are provided.
intervalBetween : String -> String -> Maybe Int
intervalBetween root other =
let
notes = notesInOrder |> List.indexedMap (\i e -> (e,i)) |> Dict.fromList
in
case (Dict.get root notes, Dict.get other notes) of
(Just i, Just j) ->
if j >= i then Just(j-i)
else Just(j-i+12)
_ -> Nothing
-- Note (name) obtained by going interval semitones up from root.
intervalToNote : String -> Int -> String
intervalToNote root interval =
let
noteToIndex = notesInOrder |> List.indexedMap (\i e -> (e,i)) |> Dict.fromList
indexToNote = notesInOrder |> List.indexedMap (,) |> Dict.fromList
in
case Dict.get root noteToIndex of
Just i -> withDefault "?" (Dict.get ((i + interval) % 12) indexToNote)
_ -> "?"
type MyStyles
= Default
| Headline
| Button
| Title
| Colored Int Bool
| ColoredLabel
| Test Int
type Variations = Selected | Highlight
defaultType = [Font.font "Helvetica"]
darkStroke = Color.darkGray
noteCircleStyles selected =
let
makeStyle interval =
let
intervalColor = Dict.get interval intervalColors |> withDefault Color.black
backgroundColor = if selected then intervalColor else Color.white
textColor = if selected then Color.white else darkStroke
in
style
(Colored interval selected)
[ Color.background backgroundColor
, Color.border darkStroke
, Color.text textColor
, Border.all 1
, Font.size (scale 2)
, Border.rounded 20
, Style.cursor "pointer"
, variation Highlight [Shadow.glow darkStroke 3]
]
in
List.map makeStyle (Dict.keys intervalColors)
stylesheet =
Style.styleSheet (
[ style Default
[ Color.text darkStroke
, Color.background white
, Font.size (scale 2)
, Font.typeface defaultType
]
, style Headline
[ Font.size (scale 4)
, Color.text black
]
, style Button
[ Color.text darkStroke
, Color.background white
, Color.border darkStroke
, Font.size (scale 2)
, Font.typeface defaultType
, Style.cursor "pointer"
, Border.rounded 20
, variation Selected [ Color.text white, Color.background black ]
]
, style ColoredLabel
[ Color.text darkStroke
, Font.size (scale 1)
, Style.cursor "pointer"
, variation Selected [Color.text white]
]
]
++ (noteCircleStyles True)
++ (noteCircleStyles False) )
-- Display interval or note name in note circles.
type NoteLabels
= Interval
| Note
type alias Model =
{ highlight : Maybe Int
, selection : Set Int
, rootNote : String
, noteLabelType : NoteLabels
}
init : ( Model, Cmd Msg )
init = (
{ highlight = Nothing
-- initially showing root
, selection = Set.fromList [0]
, rootNote = "E"
, noteLabelType = Interval
},
Cmd.none )
type Msg
= IntervalHighlightOn Int
| IntervalHighlightOff
| ToggleIntervalSelection Int
| SetRoot String
| ToggleNoteLabelType
| ClearIntervalSelection
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
IntervalHighlightOn interval ->
{ model | highlight = Just interval} ! []
IntervalHighlightOff ->
{ model | highlight = Nothing } ! []
ToggleIntervalSelection interval ->
{ model | selection = setToggle interval model.selection } ! []
SetRoot root ->
{ model | rootNote = root } ! []
ToggleNoteLabelType ->
{ model | noteLabelType = if model.noteLabelType == Note then Interval else Note } ! []
ClearIntervalSelection ->
-- always clear to showing the root
{ model | selection = Set.fromList [0] } ! []
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
view model =
let
noteCircle size noteName =
let
minInterval = intervalBetween model.rootNote noteName |> withDefault -100
interval = if Set.member (minInterval + 12) model.selection then minInterval + 12 else minInterval
isSelected = Set.member interval model.selection || Set.member (interval + 12) model.selection
label = case model.noteLabelType of
Note -> noteName
Interval -> withDefault "?" (Dict.get interval intervalNames)
in
circle size
(Colored interval isSelected)
[ onMouseOver (IntervalHighlightOn interval)
, onMouseOut IntervalHighlightOff
, onClick (ToggleIntervalSelection interval)
, vary Highlight (model.highlight==Just interval || model.highlight==Just (interval + 12)) ]
(el ColoredLabel [center, verticalCenter, vary Selected isSelected] (text label))
guitarString rootNoteShift =
let
circles = notesInOrder |> listShift rootNoteShift |> List.map (noteCircle 25)
in
row Default [padding 0, spacingXY 50 0, center, verticalCenter] circles
rootNoteShifts = [0, 5, 10, 15, 19, 24] |> List.map (\n -> n + 4) |> List.reverse
rootNoteButton rootNote =
el Button
[spacing 20, padding (scale 1), onClick (SetRoot rootNote), vary Selected (rootNote==model.rootNote)]
(text rootNote)
rootNoteRow =
row Default
[padding 50, spacing 20, center, verticalCenter]
((el Default [] (text "Root note is ")) :: (List.map rootNoteButton notesInOrder))
intervalButton interval =
el (Colored interval (Set.member interval model.selection))
[ spacing 20, padding (scale 1)
, onClick (ToggleIntervalSelection interval)
, onMouseOver (IntervalHighlightOn interval)
, onMouseOut IntervalHighlightOff
, center
, verticalCenter]
(text <| withDefault "?" <| Dict.get interval intervalNames)
clearIntervalButton =
el (Colored 0 False)
[spacing 20, padding (scale 1), onClick ClearIntervalSelection]
(text "clear")
intervalButtonRow1 =
let ivls = intervalNames |> Dict.keys |> List.filter (\iv -> iv <= 12)
in
row Default
[padding (scale 1), spacing 20, center, verticalCenter]
(List.map intervalButton ivls)
intervalButtonRow2 =
let ivls = intervalNames |> Dict.keys |> List.filter (\iv -> iv > 12)
in
row Default
[padding (scale 1), spacing 20, center, verticalCenter]
((List.map intervalButton ivls) ++ [clearIntervalButton])
noteDisplayMode =
let
label = case model.noteLabelType of
Interval -> "show notes"
Note -> "show intervals"
in
row Default [padding (scale 1), spacing 20, center, verticalCenter] [
el Button [spacing 20, padding (scale 1), onClick ToggleNoteLabelType] (text label)
]
intervalButtonCells1 =
let
intervals = intervalNames |> Dict.keys |> List.filter (\iv -> iv <= 12)
buttons = List.map intervalButton intervals
in
buttons
|> List.indexedMap
(\col btn ->
cell
{ start = (col, 1)
, width = 1, height = 1
, content = row Default [center, padding 5] [btn] }
)
intervalButtonCells2 =
let
intervals = intervalNames |> Dict.keys |> List.filter (\iv -> iv > 12)
buttons = (empty :: (List.map intervalButton intervals)) ++ [empty, empty, clearIntervalButton]
in
buttons
|> List.indexedMap
(\col btn ->
cell
{ start = (col, 2)
, width = 1 , height = 1
, content = row Default [center, padding 5] [btn] }
)
intervalButtonGrid =
grid Default [center, verticalCenter]
{ columns = []-- List.repeat 13 (px 100) -- px 100, px 100, px 100, px 100
, rows =
[
]
, cells = intervalButtonCells1 ++ intervalButtonCells2
}
in
Element.layout stylesheet <| row Default [center, verticalCenter, width fill, height fill] [
column Default [padding (scale 1), spacing 5, center, verticalCenter , width fill, height fill ] (
[el Headline [padding (scale 2), center] (text <| String.fromChar <| Char.fromCode <| 0x266C)] ++
(List.map guitarString rootNoteShifts) ++
[ rootNoteRow
, intervalButtonGrid
--, intervalButtonRow1
--, intervalButtonRow2
, noteDisplayMode]
)
]
main : Program Never Model Msg
main =
Html.program
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}