From d1ce8774ec3291374c222c8f64c085e3a99f6147 Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 15 May 2021 12:47:04 +0200 Subject: Add warm up --- public/main.css | 17 +++++++++++- src/audio.ts | 8 +++--- src/config.ts | 6 +++-- src/router.ts | 10 ++++--- src/state.ts | 80 ++++++++++++++++++++++++++++++++++++------------------- src/view/form.ts | 13 ++++----- src/view/timer.ts | 34 +++++++++++++---------- 7 files changed, 110 insertions(+), 58 deletions(-) diff --git a/public/main.css b/public/main.css index 5df74af..deec437 100644 --- a/public/main.css +++ b/public/main.css @@ -16,7 +16,8 @@ html { --color-action-darker: #222222; --color-prepare: #3B6EDC; --color-pause: #888888; - --color-work: #71b571; + --color-warm-up: #C679D9; + --color-work: #71B571; --color-rest: #B15B5B; --color-timer-arc-total: #222222; --color-timer-hover: #DDEEDD; @@ -123,19 +124,27 @@ body { display: grid; grid-template-columns: 10% auto 10%; grid-template-rows: 10% auto 10% 10% 10%; + color: black; +} + +.g-Timer--WarmUp { + background-color: var(--color-warm-up); color: white; } .g-Timer--Work { background-color: var(--color-work); + color: white; } .g-Timer--Rest { background-color: var(--color-rest); + color: white; } .g-Timer--Prepare { background-color: var(--color-prepare); + color: white; } .g-Timer__Pause { @@ -180,6 +189,12 @@ body { stroke-width: 18; } +.g-Timer__ArcWarmUp { + stroke: var(--color-warm-up); + fill: none; + stroke-width: 18; +} + .g-Timer__ArcWork { stroke: var(--color-work); fill: none; diff --git a/src/audio.ts b/src/audio.ts index bdf64eb..4147164 100644 --- a/src/audio.ts +++ b/src/audio.ts @@ -7,13 +7,15 @@ const endTabata = new Audio('sound/end-tabata.mp3') const endTraining = new Audio('sound/end-training.mp3') export function playFromStep(config: Config.Config, state: State.State) { - if (state.step === State.Step.Work && state.remaining === config.work) { + if (state.step === State.Step.WarmUp && state.remaining === config.warmup) { + start.play() + } else if (state.step === State.Step.Work && state.remaining === config.work) { start.play() } else if (state.step === State.Step.Rest && state.remaining === config.rest) { stop.play() - } else if (state.step === State.Step.Prepare && state.remaining === config.prepare) { + } else if (state.step === State.Step.Prepare && state.remaining === config.prepare && state.elapsed > 0) { endTabata.play() - } else if (state.step === State.Step.End) { + } else if (state.step === State.Step.End && state.elapsed === Config.getDuration(config)) { endTraining.play() } } diff --git a/src/config.ts b/src/config.ts index c20bff2..d1d369e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,5 @@ export interface Config { + warmup: number; prepare : number; tabatas : number; cycles : number; @@ -8,8 +9,9 @@ export interface Config { export function init(): Config { return { - prepare: 10, + warmup: 120, tabatas: 4, + prepare: 10, cycles: 8, work: 20, rest: 10 @@ -17,5 +19,5 @@ export function init(): Config { } export function getDuration(c: Config): number { - return c.tabatas * (c.prepare + (c.cycles * (c.work + c.rest))) + return c.warmup + c.tabatas * (c.prepare + (c.cycles * (c.work + c.rest))) } diff --git a/src/router.ts b/src/router.ts index abbdc65..ff6c724 100644 --- a/src/router.ts +++ b/src/router.ts @@ -24,8 +24,9 @@ export function from(location: Location): Route { if (xs.length === 2) { const key = xs[0] const value = parseInt(xs[1]) - if (key == 'prepare') config.prepare = value - else if (key == 'tabatas') config.tabatas = value + if (key == 'warmup') config.warmup = value + else if (key == 'tabatas') config.tabatas = value + else if (key == 'prepare') config.prepare = value else if (key == 'cycles') config.cycles = value else if (key == 'work') config.work = value else if (key == 'rest') config.rest = value @@ -41,10 +42,11 @@ export function toString(route: Route): string { const path = route.kind === Kind.Form ? '/' : '/timer' let query = '' if (route.config !== undefined) { - const { prepare, tabatas, cycles, work, rest } = route.config + const { warmup, tabatas, prepare, cycles, work, rest } = route.config const params = [ - `prepare=${prepare}`, + `warmup=${warmup}`, `tabatas=${tabatas}`, + `prepare=${prepare}`, `cycles=${cycles}`, `work=${work}`, `rest=${rest}`, diff --git a/src/state.ts b/src/state.ts index 3b390c5..a0348f0 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,6 +1,7 @@ import * as Config from 'config' export enum Step { + WarmUp, Prepare, Work, Rest, @@ -8,56 +9,79 @@ export enum Step { } export function prettyPrintStep(step: Step): string { + if (step === Step.WarmUp) + return 'Warm Up' if (step === Step.Prepare) - return "Prepare" + return 'Prepare' else if (step === Step.Work) - return "Work" + return 'Work' else if (step === Step.Rest) - return "Rest" + return 'Rest' else - return "End" + return 'End' } export interface State { step: Step, remaining: number, - tabata: number, - cycle: number, + info: string, + elapsed: number, } export function getAt(config: Config.Config, elapsed: number): State { + if (elapsed < config.warmup) { + return { + step: Step.WarmUp, + remaining: config.warmup - elapsed, + info: '', + elapsed + } + } + + const tabataElapsed = elapsed - config.warmup + const cycleDuration = config.work + config.rest const tabataDuration = config.prepare + (config.cycles * cycleDuration) - if (elapsed >= tabataDuration * config.tabatas) { + + if (tabataElapsed >= tabataDuration * config.tabatas) { return { step: Step.End, remaining: 0, - tabata: config.tabatas, - cycle: config.cycles, + info: '', + elapsed } + } + + const currentTabataElapsed = tabataElapsed % tabataDuration + let step, remaining + if (currentTabataElapsed < config.prepare) { + step = Step.Prepare + remaining = config.prepare - currentTabataElapsed } else { - const currentTabataElapsed = elapsed % tabataDuration - let step, remaining - if (currentTabataElapsed < config.prepare) { - step = Step.Prepare - remaining = config.prepare - currentTabataElapsed + const currentCycleElapsed = (currentTabataElapsed - config.prepare) % cycleDuration + if (currentCycleElapsed < config.work) { + step = Step.Work + remaining = config.work - currentCycleElapsed } else { - const currentCycleElapsed = (currentTabataElapsed - config.prepare) % cycleDuration - if (currentCycleElapsed < config.work) { - step = Step.Work - remaining = config.work - currentCycleElapsed - } else { - step = Step.Rest - remaining = config.work + config.rest - currentCycleElapsed - } + step = Step.Rest + remaining = config.work + config.rest - currentCycleElapsed } + } - const tabata = Math.floor(elapsed / tabataDuration) + 1 - const cycle = - currentTabataElapsed < config.prepare - ? 1 - : Math.floor((currentTabataElapsed - config.prepare) / cycleDuration) + 1 + const tabata = Math.floor(tabataElapsed / tabataDuration) + 1 + const cycle = + currentTabataElapsed < config.prepare + ? 1 + : Math.floor((currentTabataElapsed - config.prepare) / cycleDuration) + 1 + const info = stepCount(step, tabata, cycle) - return { step, remaining, tabata, cycle } + return { step, remaining, info, elapsed } +} + +function stepCount(step: Step, tabata: number, cycle: number): string { + if (step === Step.Work || step === Step.Rest) { + return `#${tabata.toString()}.${cycle.toString()}` + } else { + return `#${tabata.toString()}` } } diff --git a/src/view/form.ts b/src/view/form.ts index 60e5f08..0b04cfb 100644 --- a/src/view/form.ts +++ b/src/view/form.ts @@ -36,12 +36,13 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v 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)), + labelledInput('Warm Up', 0, config.warmup, n => { config.warmup = n; wd()}), + labelledInput('Tabatas', 1, config.tabatas, n => { config.tabatas = n; wd()}), + labelledInput('Prepare', 0, config.prepare, n => { config.prepare = 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 index ddcea71..061f5d4 100644 --- a/src/view/timer.ts +++ b/src/view/timer.ts @@ -7,6 +7,8 @@ import h from 'h' let interval: number | undefined = undefined +const endDuration: number = 4 + export function clearInterval() { if (interval !== undefined) { window.clearInterval(interval) @@ -27,21 +29,22 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v // Elements const section = h('section', { className: timerClass(state.step) }) const stepElt = document.createTextNode(State.prettyPrintStep(state.step)) - const stepCountElt = document.createTextNode(stepCount(state)) + const stepInfoElt = document.createTextNode(state.info) const arcPathElt = h('path', { class: 'g-Timer__ArcProgress' }) const updateDom = () => { - const angle = elapsed / duration * 360 + const angle = Math.min(359.999, 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) + stepInfoElt.textContent = state.info Audio.playFromStep(config, state) } const quit = () => { const formRoute = { kind: Router.Kind.Form, config } history.pushState({}, '', Router.toString(formRoute)) + clearInterval() showPage(formRoute) } @@ -49,7 +52,7 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v if (isPlaying) { elapsed = elapsed + 1 state = State.getAt(config, elapsed) - elapsed > duration + elapsed >= duration + endDuration ? quit() : updateDom() } @@ -62,6 +65,9 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v } interval = window.setInterval(update, 1000) + // Play initial audio + Audio.playFromStep(config, state) + section.append( h('div', { className: 'g-Timer__Dial' }, @@ -78,7 +84,7 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v arcPathElt ), h('div', { className: 'g-Timer__Step' }, stepElt), - h('div', {}, stepCountElt) + h('div', {}, stepInfoElt) ), h('div', { className: 'g-Timer__Buttons' }, @@ -128,6 +134,10 @@ function arcPaths(config: Config.Config): Element[] { ) } + if (config.warmup > 0) { + paths.push(arc('WarmUp', config.warmup)) + } + for (let tabata = 0; tabata < config.tabatas; tabata++) { paths.push(arc('Prepare', config.prepare)) for (let cycle = 0; cycle < config.cycles; cycle++) { @@ -140,19 +150,15 @@ function arcPaths(config: Config.Config): Element[] { } function timerClass(step: State.Step): string { - if (step === State.Step.Work) { + if (step === State.Step.WarmUp) { + return 'g-Layout__Page g-Timer g-Timer--WarmUp' + } else 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 { + } else if (step === State.Step.Prepare) { 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()}` + return 'g-Layout__Page g-Timer' } } -- cgit v1.2.3