From 221b6451fb4f8559a10e7fefebd13ce125ef29d0 Mon Sep 17 00:00:00 2001 From: Joris Date: Thu, 13 May 2021 14:50:51 +0200 Subject: Rewrite in TypeScript BuckleScript is no longer maintained. Choose a widely used techno that will still be maintained in the following years. --- src/view/form.ts | 47 ++++++++++++++++ src/view/timer.ts | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 src/view/form.ts create mode 100644 src/view/timer.ts (limited to 'src/view') diff --git a/src/view/form.ts b/src/view/form.ts new file mode 100644 index 0000000..60e5f08 --- /dev/null +++ b/src/view/form.ts @@ -0,0 +1,47 @@ +import * as Config from 'config' +import h from 'h' +import * as Router from 'router' +import * as Duration from 'duration' + +function labelledInput( + labelValue: string, + min: number, + value: number, + update: (n: number) => void +): Element { + return h('label', + { className: 'g-Form__Label', + oninput: (e: Event) => { + if (e.target !== null) { + const target = e.target as HTMLInputElement + update(parseInt(target.value) || 0) + } + } + }, + labelValue, + h('input', { className: 'g-Form__Input', type: 'number', min, value })) +} + +export function view(config: Config.Config, showPage: (route: Router.Route) => void) { + const duration = document.createTextNode(Duration.prettyPrint(Config.getDuration(config))) + const wd = () => duration.textContent = Duration.prettyPrint(Config.getDuration(config)) + return h('div', + { className: 'g-Layout__Page' }, + h('header', { className: 'g-Layout__Header' }, 'Tabata timer'), + h('form', + { className: 'g-Form' + , onsubmit: (e: Event) => { + e.preventDefault() + const timerRoute = { kind: Router.Kind.Timer, config } + history.pushState({}, '', Router.toString(timerRoute)) + showPage(timerRoute) + }}, + labelledInput('prepare', 0, config.prepare, n => { config.prepare = n; wd()}), + labelledInput('tabatas', 1, config.tabatas, n => { config.tabatas = n; wd()}), + labelledInput('cycles', 1, config.cycles, n => { config.cycles = n; wd()}), + labelledInput('work', 5, config.work, n => { config.work = n; wd()}), + labelledInput('rest', 5, config.rest, n => { config.rest = n; wd()}), + h('div', { className: 'g-Form__Duration' }, 'duration', h('div', {}, duration)), + h('button', { className: 'g-Form__Start' }, 'start')) + ) +} 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()}` + } +} -- cgit v1.2.3