aboutsummaryrefslogtreecommitdiff
path: root/src/view/timer.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/timer.ts')
-rw-r--r--src/view/timer.ts158
1 files changed, 158 insertions, 0 deletions
diff --git a/src/view/timer.ts b/src/view/timer.ts
new file mode 100644
index 0000000..ddcea71
--- /dev/null
+++ b/src/view/timer.ts
@@ -0,0 +1,158 @@
+import * as Config from 'config'
+import * as State from 'state'
+import * as Arc from 'arc'
+import * as Router from 'router'
+import * as Audio from 'audio'
+import h from 'h'
+
+let interval: number | undefined = undefined
+
+export function clearInterval() {
+ if (interval !== undefined) {
+ window.clearInterval(interval)
+ interval = undefined
+ }
+}
+
+export function view(config: Config.Config, showPage: (route: Router.Route) => void) {
+
+ const formUrl = `${Router.toString({ kind: Router.Kind.Form, config })}`
+ const duration = Config.getDuration(config)
+
+ // State
+ let isPlaying = true
+ let elapsed = 0
+ let state = State.getAt(config, elapsed)
+
+ // Elements
+ const section = h('section', { className: timerClass(state.step) })
+ const stepElt = document.createTextNode(State.prettyPrintStep(state.step))
+ const stepCountElt = document.createTextNode(stepCount(state))
+ const arcPathElt = h('path', { class: 'g-Timer__ArcProgress' })
+
+ const updateDom = () => {
+ const angle = elapsed / duration * 360
+ arcPathElt.setAttribute("d", Arc.describe(0, 0, 90, 0, angle))
+ section.className = timerClass(state.step)
+ stepElt.textContent = State.prettyPrintStep(state.step)
+ stepCountElt.textContent = stepCount(state)
+ Audio.playFromStep(config, state)
+ }
+
+ const quit = () => {
+ const formRoute = { kind: Router.Kind.Form, config }
+ history.pushState({}, '', Router.toString(formRoute))
+ showPage(formRoute)
+ }
+
+ const update = () => {
+ if (isPlaying) {
+ elapsed = elapsed + 1
+ state = State.getAt(config, elapsed)
+ elapsed > duration
+ ? quit()
+ : updateDom()
+ }
+ }
+
+ // Start timer
+ if (interval !== undefined) {
+ window.clearInterval(interval)
+ interval = undefined
+ }
+ interval = window.setInterval(update, 1000)
+
+ section.append(
+ h('div',
+ { className: 'g-Timer__Dial' },
+ h('svg',
+ { class: 'g-Timer__Arc',
+ viewBox: '-100 -100 200 200'
+ },
+ h('path',
+ { class: 'g-Timer__ArcTotal',
+ d: Arc.describe(0, 0, 90.0, 0, 359.999)
+ }
+ ),
+ ...arcPaths(config),
+ arcPathElt
+ ),
+ h('div', { className: 'g-Timer__Step' }, stepElt),
+ h('div', {}, stepCountElt)
+ ),
+ h('div',
+ { className: 'g-Timer__Buttons' },
+ h('button',
+ { className: 'g-Timer__Button',
+ onclick: (e: MouseEvent) => {
+ isPlaying = !isPlaying
+ const elt = e.target as HTMLElement
+ elt.textContent = isPlaying
+ ? 'pause'
+ : 'resume'
+ elt.className = isPlaying
+ ? 'g-Timer__Button'
+ : 'g-Timer__Button g-Timer__Button--Active'
+ }
+ },
+ 'pause'
+ ),
+ h('a',
+ { className: 'g-Timer__Button',
+ href: formUrl
+ },
+ 'quit'
+ )
+ )
+ )
+
+ return section
+}
+
+function arcPaths(config: Config.Config): Element[] {
+ const paths = []
+
+ let t = 0
+ const totalDuration = Config.getDuration(config)
+
+ let arc = (kind: string, duration: number): Element => {
+ const startAngle = 360 * t / totalDuration
+ const endAngle = 360 * (t + duration) / totalDuration
+
+ t += duration
+
+ return h('path',
+ { class: `g-Timer__Arc${kind}`,
+ d: Arc.describe(0, 0, 90.0, startAngle, endAngle)
+ }
+ )
+ }
+
+ for (let tabata = 0; tabata < config.tabatas; tabata++) {
+ paths.push(arc('Prepare', config.prepare))
+ for (let cycle = 0; cycle < config.cycles; cycle++) {
+ paths.push(arc('Work', config.work))
+ paths.push(arc('Rest', config.rest))
+ }
+ }
+
+ return paths
+}
+
+function timerClass(step: State.Step): string {
+ if (step === State.Step.Work) {
+ return 'g-Layout__Page g-Timer g-Timer--Work'
+ } else if (step === State.Step.Rest) {
+ return 'g-Layout__Page g-Timer g-Timer--Rest'
+ } else {
+ return 'g-Layout__Page g-Timer g-Timer--Prepare'
+ }
+}
+
+function stepCount(state: State.State): string {
+ if (state.step === State.Step.Work || state.step === State.Step.Rest) {
+ return `#${state.tabata.toString()}.${state.cycle.toString()}`
+ } else {
+ return `#${state.tabata.toString()}`
+ }
+}