aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/chord.ts40
-rw-r--r--src/main.ts8
-rw-r--r--src/view/form.ts191
-rw-r--r--src/view/options.ts70
-rw-r--r--src/view/play.ts10
5 files changed, 227 insertions, 92 deletions
diff --git a/src/chord.ts b/src/chord.ts
index 118f146..9393e96 100644
--- a/src/chord.ts
+++ b/src/chord.ts
@@ -1,22 +1,28 @@
-export type Options = {
- major: boolean,
- minor: boolean,
- seventh: boolean,
- minorSeventh: boolean,
- majorSeventh: boolean,
-}
+export type Chord
+ = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G'
+ | 'A♭' | 'B♭' | 'C♭' | 'D♭' | 'E♭' | 'F♭' | 'G♭'
+ | 'A♯' | 'B♯' | 'C♯' | 'D♯' | 'E♯' | 'F♯' | 'G♯'
-export function generate(o: Options): [string, string] {
- let base = all[Math.floor(Math.random() * all.length)]
- let alterations: string[] = []
+export let plains: Array<Chord> = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G' ]
+export let bemols: Array<Chord> = [ 'A♭', 'B♭', 'C♭', 'D♭', 'E♭', 'F♭', 'G♭' ]
+export let sharps: Array<Chord> = [ 'A♯', 'B♯', 'C♯', 'D♯', 'E♯', 'F♯', 'G♯' ]
- if (o.major) alterations = alterations.concat('')
- if (o.minor) alterations = alterations.concat('-')
- if (o.seventh) alterations = alterations.concat('7')
- if (o.minorSeventh) alterations = alterations.concat('-7')
- if (o.majorSeventh) alterations = alterations.concat('7')
+export type Chords = Set<Chord>
- return [base, alterations[Math.floor(Math.random() * alterations.length)]]
+export type GenerateParams = {
+ major: Chords,
+ minor: Chords,
+ seventh: Chords,
+ minorSeventh: Chords,
+ majorSeventh: Chords,
}
-const all: string[] = [ 'C', 'D♭', 'D', 'E♭', 'E', 'F', 'G♭', 'G', 'A♭', 'A', 'B♭', 'B' ]
+export function generate(o: GenerateParams): [string, string] {
+ const major: Array<[string, string]> = [...o.major].map(c => [c, ''])
+ const minor: Array<[string, string]> = [...o.minor].map(c => [c, '-'])
+ const seventh: Array<[string, string]> = [...o.seventh].map(c => [c, '7'])
+ const minorSeventh: Array<[string, string]> = [...o.minorSeventh].map(c => [c, '-7'])
+ const majorSeventh: Array<[string, string]> = [...o.majorSeventh].map(c => [c, '7'])
+ const all = [...major, ...minor, ...seventh, ...minorSeventh, ...majorSeventh]
+ return all[Math.floor(Math.random() * all.length)]
+}
diff --git a/src/main.ts b/src/main.ts
index bba799e..e588aab 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,4 @@
-import { h, withVar, mount } from 'lib/rx'
+import { h, withState, mount } from 'lib/rx'
import * as Form from 'view/form'
import * as Play from 'view/play'
import * as Options from 'view/options'
@@ -9,9 +9,9 @@ enum Page {
}
mount(
- withVar(Page.Form, (page, updatePage) => [
+ withState(Page.Form, page => [
h('header',
- { onclick: () => updatePage(_ => Page.Form) },
+ { onclick: () => page.update(_ => Page.Form) },
'Chords'
),
page.map(p =>
@@ -20,7 +20,7 @@ mount(
options: Options.load(),
onSubmit: (options: Options.Model) => {
Options.save(options)
- updatePage(_ => Page.Play)
+ page.update(_ => Page.Play)
}
})
: Play.view({
diff --git a/src/view/form.ts b/src/view/form.ts
index 5547e0c..e499b05 100644
--- a/src/view/form.ts
+++ b/src/view/form.ts
@@ -1,5 +1,6 @@
-import { h, withVar, Html, Rx } from 'lib/rx'
+import { h, withState, Html, Rx, pure, RxAble } from 'lib/rx'
import * as Options from 'view/options'
+import * as Chord from 'chord'
interface Params {
options: Options.Model
@@ -7,7 +8,7 @@ interface Params {
}
export function view({ options, onSubmit }: Params): Html {
- return withVar(options, (opts, updateOptions) =>
+ return withState(options, opts =>
h('form',
{ className: 'g-Form',
onsubmit: opts.map(o =>
@@ -17,58 +18,71 @@ export function view({ options, onSubmit }: Params): Html {
}
)
},
- chordCheckbox({
- label: '',
- checked: opts.map(o => o.major),
- onCheck: (checked => updateOptions(o => {
- o.major = checked
- return o
- }))
- }),
- chordCheckbox({
- label: '-',
- checked: opts.map(o => o.minor),
- onCheck: (checked => updateOptions(o => {
- o.minor = checked
- return o
- }))
- }),
- chordCheckbox({
- label: '7',
- checked: opts.map(o => o.seventh),
- onCheck: (checked => updateOptions(o => {
- o.seventh = checked
- return o
- }))
- }),
- chordCheckbox({
- label: '-7',
- checked: opts.map(o => o.minorSeventh),
- onCheck: (checked => updateOptions(o => {
- o.minorSeventh = checked
- return o
- }))
- }),
- chordCheckbox({
- label: '7',
- checked: opts.map(o => o.majorSeventh),
- onCheck: (checked => updateOptions(o => {
- o.majorSeventh = checked
- return o
- }))
- }),
- numberInput({
- label: 'BPM',
+ h('table',
+ { className: 'g-ChordsTable' },
+ chordsGroup({
+ title: 'Major',
+ chords: opts.map(o => o.major),
+ updateChords: (chords: Chord.Chords) => {
+ opts.update(o => {
+ o.major = chords
+ return o
+ })
+ }
+ }),
+ chordsGroup({
+ title: 'Minor',
+ chords: opts.map(o => o.minor),
+ updateChords: (chords: Chord.Chords) => {
+ opts.update(o => {
+ o.minor = chords
+ return o
+ })
+ }
+ }),
+ chordsGroup({
+ title: '7th',
+ chords: opts.map(o => o.seventh),
+ updateChords: (chords: Chord.Chords) => {
+ opts.update(o => {
+ o.seventh = chords
+ return o
+ })
+ }
+ }),
+ chordsGroup({
+ title: 'Minor 7th',
+ chords: opts.map(o => o.minorSeventh),
+ updateChords: (chords: Chord.Chords) => {
+ opts.update(o => {
+ o.minorSeventh = chords
+ return o
+ })
+ }
+ }),
+ chordsGroup({
+ title: 'Major 7th',
+ chords: opts.map(o => o.majorSeventh),
+ updateChords: (chords: Chord.Chords) => {
+ opts.update(o => {
+ o.majorSeventh = chords
+ return o
+ })
+ }
+ })
+ ),
+ numberInput({
+ label: 'BPM',
value: opts.map(o => o.bpm.toString()),
- onChange: (n => updateOptions(o => {
+ onChange: (n => opts.update(o => {
o.bpm = n
return o
}))
}),
- numberInput({
- label: 'Beats per Chord',
+ numberInput({
+ label: 'Beats per Chord',
value: opts.map(o => o.beatsPerChord.toString()),
- onChange: (n => updateOptions(o => {
+ onChange: (n => opts.update(o => {
o.beatsPerChord = n
return o
}))
@@ -78,10 +92,85 @@ export function view({ options, onSubmit }: Params): Html {
)
}
+interface ChordsGroupParams {
+ title: string,
+ chords: Rx<Chord.Chords>,
+ updateChords: (chords: Chord.Chords) => void
+}
+
+function chordsGroup({ title, chords, updateChords }: ChordsGroupParams): Html {
+ const line =
+ (lineChords: Array<Chord.Chord>, label: (chord: Chord.Chord) => string) =>
+ h('div',
+ { className: 'g-ChordsLine' },
+ chordCheckbox({
+ checked: chords.map(cs => lineChords.every(c => cs.has(c))),
+ onCheck: chords.map(cs =>
+ (checked: boolean) => {
+ if (checked) {
+ lineChords.forEach(c => cs.add(c))
+ } else {
+ lineChords.forEach(c => cs.delete(c))
+ }
+ updateChords(cs)
+ }
+ )
+ }),
+ lineChords.map(chord =>
+ chordCheckbox({
+ label: label(chord),
+ checked: chords.map(cs => cs.has(chord)),
+ onCheck: chords.map(cs =>
+ (checked: boolean) => {
+ if (checked) {
+ cs.add(chord)
+ } else {
+ cs.delete(chord)
+ }
+ updateChords(cs)
+ }
+ )
+ })
+ )
+ )
+
+ return h('tr',
+ h('td',
+ chordCheckbox({
+ label: title,
+ checked: chords.map(cs =>
+ Chord.sharps.every(c => cs.has(c))
+ && Chord.plains.every(c => cs.has(c))
+ && Chord.bemols.every(c => cs.has(c))
+ ),
+ onCheck: chords.map(cs =>
+ (checked: boolean) => {
+ if (checked) {
+ Chord.sharps.forEach(c => cs.add(c))
+ Chord.plains.forEach(c => cs.add(c))
+ Chord.bemols.forEach(c => cs.add(c))
+ } else {
+ Chord.sharps.forEach(c => cs.delete(c))
+ Chord.plains.forEach(c => cs.delete(c))
+ Chord.bemols.forEach(c => cs.delete(c))
+ }
+ updateChords(cs)
+ }
+ )
+ })
+ ),
+ h('td',
+ line(Chord.sharps, () => '♯'),
+ line(Chord.plains, chord => chord),
+ line(Chord.bemols, () => '♭')
+ )
+ )
+}
+
interface ChordCheckboxParams {
- label: string
+ label?: string
checked: Rx<boolean>
- onCheck: (checked: boolean) => void
+ onCheck: Rx<(checked: boolean) => void>
}
function chordCheckbox({ label, checked, onCheck }: ChordCheckboxParams): Html {
@@ -90,7 +179,7 @@ function chordCheckbox({ label, checked, onCheck }: ChordCheckboxParams): Html {
h('input',
{ type: 'checkbox',
checked,
- onchange: (event: Event) => onCheck((event.target as HTMLInputElement).checked)
+ onchange: onCheck.map(f => (event: Event) => f((event.target as HTMLInputElement).checked))
}
),
label
diff --git a/src/view/options.ts b/src/view/options.ts
index 31fd631..0782fe4 100644
--- a/src/view/options.ts
+++ b/src/view/options.ts
@@ -1,30 +1,70 @@
+import * as Chord from 'chord'
+
+// Model version: upgrade whenever the model is updated. Options are loaded
+// only if versions match.
+const version = '2'
+
export interface Model {
- major: boolean
- minor: boolean
- seventh: boolean
- minorSeventh: boolean
- majorSeventh: boolean
+ major: Chord.Chords
+ minor: Chord.Chords
+ seventh: Chord.Chords
+ minorSeventh: Chord.Chords
+ majorSeventh: Chord.Chords
bpm: number
beatsPerChord: number
}
let init: Model = {
- major: true,
- minor: false,
- seventh: false,
- minorSeventh: false,
- majorSeventh: false,
+ major: new Set(Chord.plains),
+ minor: new Set(),
+ seventh: new Set(),
+ minorSeventh: new Set(),
+ majorSeventh: new Set(),
bpm: 90,
beatsPerChord: 4
}
-let key: string = 'options'
-
export function load(): Model {
- let str = localStorage[key]
- return str && JSON.parse(str) || init
+ let storedVersion = localStorage['version']
+
+ if (storedVersion === undefined || storedVersion !== version) {
+ return init
+ } else {
+ let str = localStorage['options']
+ return str && fromString(str) || init
+ }
}
export function save(options: Model): void {
- localStorage[key] = JSON.stringify(options)
+ localStorage['version'] = version
+ localStorage['options'] = toString(options)
+}
+
+function toString(o: Model): string {
+ return JSON.stringify({
+ major: [...o.major],
+ minor: [...o.minor],
+ seventh: [...o.seventh],
+ minorSeventh: [...o.minorSeventh],
+ majorSeventh: [...o.majorSeventh],
+ bpm: o.bpm,
+ beatsPerChord: o.beatsPerChord
+ })
+}
+
+function fromString(str: string): Model | undefined {
+ const o = JSON.parse(str)
+ if (o === undefined) {
+ return undefined
+ } else {
+ return {
+ major: new Set(o.major),
+ minor: new Set(o.minor),
+ seventh: new Set(o.seventh),
+ minorSeventh: new Set(o.minorSeventh),
+ majorSeventh: new Set(o.majorSeventh),
+ bpm: o.bpm,
+ beatsPerChord: o.beatsPerChord
+ }
+ }
}
diff --git a/src/view/play.ts b/src/view/play.ts
index b85e505..4abc4ec 100644
--- a/src/view/play.ts
+++ b/src/view/play.ts
@@ -1,4 +1,4 @@
-import { h, withVar, Html } from 'lib/rx'
+import { h, withState, Html } from 'lib/rx'
import * as Options from 'view/options'
import * as Chord from 'chord'
@@ -9,19 +9,19 @@ interface ViewParams {
export function view({ options }: ViewParams): Html {
const initChords: Array<[string, string]> = [['', ''], Chord.generate(options), Chord.generate(options)]
- return withVar(initChords, (chords, updateChords) =>
- withVar(undefined, (beat, updateBeat) => {
+ return withState(initChords, chords =>
+ withState(undefined, beat => {
let chordBeat = 1
const interval = setInterval(() => {
if (chordBeat === options.beatsPerChord) {
- updateChords(xs => {
+ chords.update(xs => {
xs.shift()
xs.push(Chord.generate(options))
return xs
})
chordBeat = 1
} else {
- updateBeat(_ => undefined)
+ beat.update(_ => undefined)
chordBeat += 1
}
}, 60 / options.bpm * 1000)