├── .gitignore ├── LICENSE ├── README.md ├── docs ├── index.html ├── script.js └── style.css ├── index.html ├── package.json ├── script.es5.js ├── script.js └── style.scss /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /style.css -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reminder App 2 | Just prototyping with CSS animations & transitions 3 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reminder App 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
18 | 21 |
26 | 27 | 28 | 29 | 30 |
31 | 32 | {{ tooltipText }} 33 | 34 |
35 |
36 |
37 | sphere 38 |
39 | 40 | 41 | 42 | Select voice 43 |
44 |
45 | 46 | 47 | 48 |
49 | 74 |
75 | 125 |
126 | Your browser doesn't support speech synthesis. 127 |
128 |
129 | 130 |
131 | {{ time.value }} 132 |
133 |
134 |
135 |
136 |
137 |
138 | wave2 139 |
140 |
141 | wave2 142 |
143 |
144 |
145 |
146 | wave2 147 |
148 |
149 | wave2 150 |
151 |
152 |
153 |
154 |
155 | 156 |
{{ percentsLeft }}
157 |
158 | % 159 |
160 |
161 | 166 |
167 | 168 | 169 | -------------------------------------------------------------------------------- /docs/script.js: -------------------------------------------------------------------------------- 1 | var SpeechRecognition = SpeechRecognition || window.webkitSpeechRecognition || undefined; 2 | var numbers = Array.apply(null, Array(101)).map(function (_, i) {return i;}); 3 | 4 | if(SpeechRecognition) { 5 | var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList || undefined; 6 | var SpeechRecognitionEvent = SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent || undefined; 7 | 8 | var commands = ['reset', 'timer' ].concat( numbers); 9 | var grammar = '#JSGF V1.0; grammar colors; public = ' + commands.join(' | ') + ' ;' 10 | 11 | var speechRecognitionList = new SpeechGrammarList(); 12 | speechRecognitionList.addFromString(grammar, 1); 13 | 14 | var recognition = new SpeechRecognition(); 15 | recognition.grammars = speechRecognitionList; 16 | //recognition.continuous = false; 17 | recognition.lang = 'en-US'; 18 | recognition.interimResults = true; 19 | recognition.maxAlternatives = 1; 20 | } 21 | 22 | var speechSynth = new SpeechSynthesisUtterance(); 23 | 24 | var padDigits = function (number, digits) { 25 | return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; 26 | } 27 | 28 | var calculatePercentsLeft = function (value, from) { 29 | return Math.floor(Math.ceil(value/1000) / (from * 60) * 100) 30 | } 31 | 32 | var calculateScaleFactor = function (percent) { 33 | return 1-(100-percent)/100; 34 | } 35 | 36 | function guid() { 37 | function s4() { 38 | return Math.floor((1 + Math.random()) * 0x10000) 39 | .toString(16) 40 | .substring(1); 41 | } 42 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 43 | s4() + '-' + s4() + s4() + s4(); 44 | } 45 | 46 | var settings = { 47 | water: { 48 | warningMsg: 'Remember to drink', 49 | timeIsUpMsg: 'Time\'s up. You really need to drink now', 50 | buttonTxt: 'Drink', 51 | waveFrontColor: '#32BAFA', 52 | waveBackColor: '#2C7FBE', 53 | stageBg: '#1E384C', 54 | durationInMinutes: 1 55 | }, 56 | coffee: { 57 | warningMsg: 'It\'s almost coffee time.', 58 | timeIsUpMsg: 'Time\'s up. Let\'s take a coffee break!', 59 | buttonTxt: 'Drink coffee', 60 | waveFrontColor: '#b39374', 61 | waveBackColor: '#7a6057', 62 | stageBg: '#392a2c', 63 | durationInMinutes: 2 64 | }, 65 | break: { 66 | warningMsg: 'It is time to rest your eyes soon!', 67 | timeIsUpMsg: 'Time\'s up. Now, it\'s really time to rest your eyes!', 68 | buttonTxt: 'Take a break', 69 | waveFrontColor: '#02C39A', 70 | waveBackColor: '#028090', 71 | stageBg: '#012F35', 72 | durationInMinutes: 1 73 | } 74 | }; 75 | 76 | new Vue({ 77 | el: '#stage', 78 | data: function data() { 79 | return { 80 | color: '', 81 | percents: [100], 82 | percentsLeft: 100, 83 | secondsLeft: 0, 84 | waveStyles: '', 85 | duration: 1, 86 | timer: [], 87 | voicesOpen: false, 88 | voices: [], 89 | selectedVoice: {}, 90 | countdownObj: {}, 91 | activeReminder: settings.water, 92 | menuOpen: false, 93 | isListening: false, 94 | tooltipText: 'Say eg. "reset"', 95 | stageBg: settings.water.stageBg 96 | } 97 | }, 98 | mounted: function mounted() { 99 | var this$1 = this; 100 | 101 | this.resetTimer(); 102 | this.voices = speechSynthesis.getVoices(); 103 | 104 | if(this.voices.length === 0) { 105 | speechSynthesis.onvoiceschanged = function () { 106 | this$1.voices = speechSynthesis.getVoices(); 107 | }; 108 | } 109 | }, 110 | computed: { 111 | supportSpeechSynth: function supportSpeechSynth() { 112 | return 'speechSynthesis' in window; 113 | }, 114 | supportSpeechRecognition: function supportSpeechRecognition() { 115 | return SpeechRecognition; 116 | } 117 | }, 118 | watch: { 119 | percentsLeft: function(val, oldVal) { 120 | if (val === oldVal) { 121 | return; 122 | } 123 | this.percents.splice(0, 1); 124 | this.percents.push(val); 125 | } 126 | }, 127 | methods: { 128 | setActiveReminder: function setActiveReminder(reminder) { 129 | this.activeReminder = settings[reminder]; 130 | this.stageBg = this.activeReminder.stageBg; 131 | }, 132 | toggleMenu: function toggleMenu() { 133 | this.menuOpen = !this.menuOpen; 134 | if(this.menuOpen) { 135 | this.pauseTimer(); 136 | this.waveStyles = "transform: translate3d(0,100%,0); transition-delay: .25s;"; 137 | }else { 138 | this.continueTimer(); 139 | } 140 | }, 141 | toggleVoicesMenu: function toggleVoicesMenu() { 142 | this.voicesOpen = !this.voicesOpen; 143 | }, 144 | voiceSelected: function voiceSelected(voice) { 145 | this.selectedVoice = voice; 146 | speechSynth.voice = voice; 147 | }, 148 | start: function start(reminder) { 149 | this.setActiveReminder(reminder); 150 | this.percents = [100]; 151 | this.timer = []; 152 | this.menuOpen = false; 153 | this.resetTimer(); 154 | }, 155 | resetTimer: function resetTimer() { 156 | var durationInSeconds = 60 * this.activeReminder.durationInMinutes; 157 | this.startTimer(durationInSeconds); 158 | }, 159 | startTimer: function startTimer(secondsLeft) { 160 | var this$1 = this; 161 | 162 | var now = new Date(); 163 | 164 | // later on, this timer may be stopped 165 | if(this.countdown) { 166 | window.clearInterval(this.countdown); 167 | } 168 | 169 | this.countdown = countdown(function (ts) { 170 | this$1.secondsLeft= Math.ceil(ts.value/1000); 171 | this$1.percentsLeft = calculatePercentsLeft(ts.value,this$1.activeReminder.durationInMinutes); 172 | this$1.waveStyles = "transform: scale(1," + (calculateScaleFactor(this$1.percentsLeft)) + ")"; 173 | this$1.updateCountdown(ts); 174 | if(this$1.percentsLeft == 10) { 175 | this$1.giveWarning(); 176 | } 177 | if(this$1.percentsLeft <= 0){ 178 | this$1.timeIsUpMessage(); 179 | this$1.pauseTimer(); 180 | this$1.timer = []; 181 | setTimeout(function () { 182 | this$1.startListenVoiceCommands(); 183 | }, 1500); 184 | 185 | } 186 | }, now.getTime() + (secondsLeft * 1000)); 187 | }, 188 | updateCountdown: function updateCountdown(ts) { 189 | if(this.timer.length > 2) { 190 | this.timer.splice(2); 191 | } 192 | 193 | var newTime = { 194 | id: guid(), 195 | value: ((padDigits(ts.minutes, 2)) + ":" + (padDigits(ts.seconds, 2))) 196 | }; 197 | 198 | this.timer.unshift(newTime); 199 | }, 200 | pauseTimer: function pauseTimer() { 201 | window.clearInterval(this.countdown); 202 | }, 203 | continueTimer: function continueTimer() { 204 | if(this.secondsLeft > 0) { 205 | this.startTimer(this.secondsLeft-1); 206 | } 207 | }, 208 | giveWarning: function giveWarning() { 209 | speechSynth.text = this.activeReminder.warningMsg; 210 | window.speechSynthesis.speak(speechSynth); 211 | }, 212 | timeIsUpMessage: function timeIsUpMessage() { 213 | speechSynth.text = this.activeReminder.timeIsUpMsg; 214 | window.speechSynthesis.speak(speechSynth); 215 | }, 216 | timerResetMessage: function timerResetMessage() { 217 | speechSynth.text = "Timer reset. Time left " + (this.activeReminder.durationInMinutes) + " " + (this.activeReminder.durationInMinutes > 1 ? 'minutes': 'minute'); 218 | window.speechSynthesis.speak(speechSynth); 219 | }, 220 | reset: function reset() { 221 | this.resetTimer(); 222 | this.timerResetMessage(); 223 | }, 224 | startListenVoiceCommands: function startListenVoiceCommands() { 225 | var this$1 = this; 226 | 227 | if(this.isListening || !this.supportSpeechRecognition) { return; } 228 | 229 | this.isListening = true; 230 | recognition.start(); 231 | recognition.onresult = function (event) { 232 | var last = event.results.length - 1; 233 | var transcript = event.results[last][0].transcript; 234 | var splittedTranscript = transcript.split(' '); 235 | var isFinal = event.results[last].isFinal; 236 | 237 | this$1.tooltipText = transcript; 238 | 239 | if(transcript == "reset") { 240 | this$1.resetTimer(); 241 | this$1.timerResetMessage(); 242 | } 243 | if( 244 | splittedTranscript.length >= 3 && 245 | splittedTranscript[0] == 'timer' && 246 | isFinal && 247 | numbers.includes(Number(splittedTranscript[1])) && 248 | (splittedTranscript[2] == 'minute' || splittedTranscript[2] == 'minutes') 249 | ) { 250 | this$1.activeReminder.durationInMinutes = numbers[splittedTranscript[1]]; 251 | this$1.resetTimer(); 252 | this$1.timerResetMessage(); 253 | } 254 | 255 | 256 | } 257 | recognition.onend = function () { 258 | this$1.isListening = false; 259 | this$1.tooltipText = ''; 260 | recognition.stop(); 261 | } 262 | recognition.onsoundend = function () { 263 | this$1.isListening = false; 264 | recognition.stop(); 265 | } 266 | }, 267 | mouseOver: function mouseOver(type) { 268 | this.stageBg = settings[type].stageBg; 269 | }, 270 | mouseOut: function mouseOut() { 271 | this.stageBg = this.activeReminder.stageBg; 272 | } 273 | } 274 | }); 275 | 276 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Roboto:100,400"); 2 | *, *::after, *::before { 3 | box-sizing: border-box; } 4 | 5 | html, body { 6 | height: 100%; 7 | min-height: 100%; } 8 | 9 | body { 10 | margin: 0; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | font-family: 'Roboto', sans-serif; 15 | background: linear-gradient(to bottom right, #c8c897, #6590A2); } 16 | 17 | [v-cloak] { 18 | display: none; } 19 | 20 | a { 21 | -webkit-tap-highlight-color: transparent; 22 | user-select: none; } 23 | 24 | ::-webkit-scrollbar { 25 | display: none; } 26 | 27 | .stage { 28 | position: relative; 29 | overflow: hidden; 30 | width: 100%; 31 | height: 100%; 32 | background: #fff; 33 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); 34 | background: #1E384C; 35 | transition: background-color .3s; } 36 | @media (min-width: 500px) { 37 | .stage { 38 | border-radius: 5px; 39 | max-height: 550px; 40 | max-width: 350px; } } 41 | .stage.menu-open .microphone { 42 | transform: translate3d(-1em, 0, 0); 43 | opacity: 0; } 44 | .stage.menu-open .voices-menu__button { 45 | z-index: 40; 46 | transform: translate3d(0, 0, 0); 47 | opacity: 1; } 48 | .stage.menu-open .menu { 49 | z-index: 25; } 50 | .stage.menu-open .time { 51 | transform: translate3d(0, -200%, 0); 52 | transition: .5s opacity, .5s transform; 53 | opacity: 0; } 54 | .stage.menu-open button { 55 | transform: translate3d(0, 200%, 0); 56 | transition-delay: 0s; 57 | opacity: 0; } 58 | .stage.menu-open .percent { 59 | transition: .4s opacity, .4s transform; 60 | transform: translate3d(0, 50%, 0); 61 | opacity: 0; } 62 | .stage.menu-open .menu__item { 63 | opacity: 1; 64 | transform: translate3d(0, 0, 0); } 65 | .stage.menu-open .menu__item:nth-child(1) { 66 | transition-delay: .2s; } 67 | .stage.menu-open .menu__item:nth-child(2) { 68 | transition-delay: .3s; } 69 | .stage.menu-open .menu__item:nth-child(3) { 70 | transition-delay: .4s; } 71 | .stage.voices-open .voices-menu { 72 | z-index: 35; } 73 | .stage.voices-open .voices-menu__bg { 74 | transform: scale(6); } 75 | .stage.voices-open .voices-menu__close { 76 | opacity: 1; 77 | transform: translate3d(0, 0, 0) rotate(0); } 78 | .stage.voices-open .voices-list-wrapper { 79 | opacity: 1; } 80 | .stage.voices-open .voices-list__item { 81 | opacity: 1; 82 | transform: translate(0, 0); 83 | transition: opacity .15s, transform .2s; } 84 | .stage.voices-open .voices-list__item:nth-child(1) { 85 | transition-delay: 75ms; } 86 | .stage.voices-open .voices-list__item:nth-child(2) { 87 | transition-delay: 150ms; } 88 | .stage.voices-open .voices-list__item:nth-child(3) { 89 | transition-delay: 225ms; } 90 | .stage.voices-open .voices-list__item:nth-child(4) { 91 | transition-delay: 300ms; } 92 | .stage.voices-open .voices-list__item:nth-child(5) { 93 | transition-delay: 375ms; } 94 | .stage.voices-open .voices-list__item:nth-child(6) { 95 | transition-delay: 450ms; } 96 | .stage.voices-open .voices-list__item:nth-child(7) { 97 | transition-delay: 525ms; } 98 | .stage.voices-open .voices-list__item:nth-child(8) { 99 | transition-delay: 600ms; } 100 | .stage.voices-open .voices-list__item:nth-child(9) { 101 | transition-delay: 675ms; } 102 | .stage.voices-open .voices-list__item:nth-child(10) { 103 | transition-delay: 750ms; } 104 | 105 | .microphone { 106 | z-index: 30; 107 | position: absolute; 108 | top: -.5em; 109 | left: -.8em; 110 | width: 70px; 111 | height: 70px; 112 | display: flex; 113 | justify-content: center; 114 | align-items: center; 115 | cursor: pointer; 116 | color: rgba(255, 255, 255, 0.5); 117 | transition: opacity .3s, transform .3s, color .2s; } 118 | .microphone:hover { 119 | color: rgba(255, 255, 255, 0.8); } 120 | .microphone svg { 121 | z-index: 2; 122 | position: relative; 123 | font-size: 2em; 124 | width: 1em; 125 | height: 1em; } 126 | .microphone:before, .microphone:after { 127 | content: ''; 128 | position: absolute; 129 | top: 0; 130 | left: 0; 131 | width: 100%; 132 | height: 100%; 133 | border-radius: 50%; 134 | opacity: 0; } 135 | .microphone:after { 136 | z-index: 1; 137 | background: rgba(255, 255, 255, 0.1); 138 | transition: opacity .3s; } 139 | .microphone:before { 140 | z-index: 2; 141 | border: 3px solid rgba(255, 255, 255, 0.1); 142 | opacity: 0; } 143 | .microphone.is-listening { 144 | color: #d82e2e; } 145 | .microphone.is-listening:before { 146 | animation: pulseAway 1s infinite; } 147 | .microphone.is-listening:after { 148 | opacity: 1; 149 | animation: pulse 1.5s linear infinite; } 150 | .microphone .voice-tooltip { 151 | position: absolute; 152 | top: 110%; 153 | left: 25px; 154 | padding: .4em .6em; 155 | color: rgba(255, 255, 255, 0.8); 156 | font-size: .8em; 157 | font-weight: 300; 158 | text-transform: uppercase; 159 | white-space: nowrap; 160 | background: rgba(255, 255, 255, 0.1); 161 | border-radius: 3px; } 162 | .microphone .voice-tooltip:before { 163 | content: ''; 164 | position: absolute; 165 | bottom: 100%; 166 | left: 5px; 167 | width: 0; 168 | height: 0; 169 | border-style: solid; 170 | border-width: 0 5px 5px 5px; 171 | border-color: transparent transparent rgba(255, 255, 255, 0.1) transparent; } 172 | 173 | .fade-enter-active, 174 | .fade-leave-active { 175 | transition: all .15s ease; } 176 | 177 | .fade-enter, .fade-leave-to { 178 | opacity: 0; } 179 | 180 | .voices-menu { 181 | position: absolute; 182 | top: 0; 183 | left: 0; 184 | right: 0; 185 | bottom: 0; } 186 | @media (min-width: 500px) { 187 | .voices-menu { 188 | border-radius: 5px; 189 | overflow: hidden; } } 190 | .voices-menu__bg { 191 | position: absolute; 192 | top: -15em; 193 | left: -15em; 194 | transform-origin: 50% 50%; 195 | width: 20em; 196 | height: 20em; 197 | color: #222; 198 | transform: scale(0.2); 199 | transition: transform .3s; } 200 | .voices-menu__button { 201 | position: absolute; 202 | top: 0; 203 | left: 0; 204 | padding: .8em .6em; 205 | color: rgba(255, 255, 255, 0.5); 206 | opacity: 0; 207 | cursor: pointer; 208 | transform: translate3d(1em, 0, 0); 209 | transition: opacity .3s, transform .3s, color .2s; } 210 | .voices-menu__button:hover { 211 | color: rgba(255, 255, 255, 0.8); } 212 | .voices-menu__button > * { 213 | vertical-align: middle; 214 | font-weight: 300; 215 | letter-spacing: 1px; } 216 | .voices-menu__button svg { 217 | width: 1.8em; 218 | height: 1.8em; } 219 | .voices-menu__close { 220 | position: absolute; 221 | top: 0; 222 | right: 0; 223 | padding: 10px 10px; 224 | font-size: 2em; 225 | font-weight: 300; 226 | color: rgba(255, 255, 255, 0.5); 227 | opacity: 0; 228 | cursor: pointer; 229 | transform: translate3d(1em, 0, 0) rotate(45deg); 230 | transition: opacity .3s, transform .3s, color .2s; } 231 | .voices-menu__close svg { 232 | width: 1em; 233 | height: 1em; } 234 | .voices-menu__close:hover { 235 | color: rgba(255, 255, 255, 0.8); } 236 | 237 | .voices-list-wrapper { 238 | position: absolute; 239 | top: 60px; 240 | left: 0; 241 | bottom: 0; 242 | right: 0; 243 | overflow-y: auto; 244 | opacity: 0; } 245 | 246 | .voices-list { 247 | margin: 0; 248 | padding: 0; } 249 | .voices-list__item { 250 | display: block; 251 | opacity: 0; 252 | transform: translate(0, 1em); } 253 | .voices-list__item.is-selected .voices-list__icon { 254 | opacity: 1; 255 | transform: translate3d(0, 0, 0) rotate(0); } 256 | .voices-list__icon { 257 | position: relative; 258 | margin-right: 20px; 259 | color: #02C39A; 260 | opacity: 0; 261 | transform: translate3d(-1em, 0, 0) rotate(-30deg); 262 | transition: opacity .2s, transform .2s; } 263 | .voices-list__icon svg { 264 | width: 1.2em; 265 | height: 1.2em; } 266 | .voices-list__link { 267 | display: flex; 268 | padding: .5em 1.1em; 269 | font-size: 1.3em; 270 | font-weight: 300; 271 | color: rgba(255, 255, 255, 0.8); 272 | text-decoration: none; } 273 | .voices-list__link:hover { 274 | background: rgba(255, 255, 255, 0.05); } 275 | .voices-list__link span { 276 | display: inline-block; 277 | vertical-align: middle; } 278 | .voices-list__content { 279 | line-height: 1; } 280 | .voices-list__content span { 281 | font-size: .5em; } 282 | .voices-list__default { 283 | color: #02C39A; } 284 | 285 | .menu { 286 | z-index: 10; 287 | position: absolute; 288 | width: 100%; 289 | height: 100%; 290 | display: flex; 291 | justify-content: center; 292 | align-items: center; } 293 | .menu__button { 294 | z-index: 30; 295 | position: absolute; 296 | top: 0; 297 | right: 0; 298 | display: inline-block; 299 | padding: 1.5em 1em; 300 | cursor: pointer; } 301 | .menu__button:hover .menu__dot, 302 | .menu__button:hover .menu__dot:before, 303 | .menu__button:hover .menu__dot:after { 304 | background: rgba(255, 255, 255, 0.8); } 305 | .menu__dot { 306 | position: relative; 307 | border-radius: 50%; 308 | width: 6px; 309 | height: 6px; 310 | background: rgba(255, 255, 255, 0.5); 311 | transition: background .2s; } 312 | .menu__dot:before, .menu__dot:after { 313 | position: absolute; 314 | content: ''; 315 | border-radius: 50%; 316 | width: 6px; 317 | height: 6px; 318 | background: rgba(255, 255, 255, 0.5); 319 | transition: background .2s; } 320 | .menu__dot:before { 321 | top: 10px; } 322 | .menu__dot:after { 323 | bottom: 10px; } 324 | .menu__list { 325 | list-style: none; 326 | padding: 0; 327 | margin: 0; 328 | width: 100%; } 329 | .menu__item { 330 | overflow: hidden; 331 | opacity: 0; 332 | transform: translate3d(0, 100%, 0); 333 | transition: .4s transform, .4s opacity; } 334 | .menu__item a { 335 | font-size: 1.8em; 336 | font-weight: 300; 337 | display: block; 338 | color: rgba(255, 255, 255, 0.5); 339 | text-transform: uppercase; 340 | text-decoration: none; 341 | padding: .5em 1.5em; } 342 | .menu__item a span { 343 | display: inline-block; 344 | vertical-align: middle; 345 | transition: transform .3s; } 346 | .menu__item a:hover svg, .menu__item a:focus svg { 347 | transform: scale(1.2); } 348 | .menu__item a:hover .water-glass__water, .menu__item a:focus .water-glass__water { 349 | fill: #32BAFA; 350 | transform: scale(1, 0.8); } 351 | .menu__item a:hover .coffee-cup__coffee, .menu__item a:focus .coffee-cup__coffee { 352 | fill: #BF9E87; 353 | transform: scale(1, 0.8); } 354 | .menu__item a:hover .clock__short, .menu__item a:focus .clock__short { 355 | fill: #02C39A; 356 | transform-origin: 0% 50%; 357 | transform: rotate(20deg); 358 | transition: transform 1s, color .2s; } 359 | .menu__item a:hover .clock__long, .menu__item a:focus .clock__long { 360 | fill: #02C39A; 361 | transition: transform 1s, color .2s; 362 | transform-origin: 50% 95%; 363 | transform: rotate(360deg); } 364 | .menu__item svg { 365 | display: inline-block; 366 | vertical-align: middle; 367 | width: 1em; 368 | height: 1em; 369 | margin-right: 1em; 370 | transition: transform .3s; } 371 | .menu__item svg path { 372 | fill: #fff; 373 | transition: all .3s; 374 | transform-origin: 100% 100%; } 375 | 376 | .browser-support { 377 | color: #fff; 378 | font-size: .8rem; 379 | text-align: center; 380 | padding: .5rem; } 381 | 382 | .content { 383 | z-index: 20; 384 | position: absolute; 385 | bottom: 0; 386 | left: 0; 387 | width: 100%; 388 | height: 100%; 389 | display: flex; 390 | align-items: center; 391 | justify-content: center; } 392 | 393 | .time { 394 | overflow: hidden; 395 | padding: 1em; 396 | font-size: 1.1em; 397 | text-align: center; 398 | transition: .5s .2s opacity, .5s transform .2s; } 399 | 400 | .timer__item { 401 | transition: all 1s; 402 | margin-right: 10px; 403 | color: rgba(255, 255, 255, 0.8); } 404 | .timer__item:first-child, .timer__item:nth-child(3) { 405 | color: rgba(255, 255, 255, 0.2); } 406 | 407 | .timer-enter, .timer-leave-to { 408 | opacity: 0; 409 | transform: translate3d(0, -100%, 0); } 410 | 411 | .timer-leave-to { 412 | transition-duration: .5s; } 413 | 414 | .timer-leave-active { 415 | transform: translate3d(0, 0, 0); } 416 | 417 | .percent { 418 | z-index: 2; 419 | position: relative; 420 | font-size: 7em; 421 | font-weight: 100; 422 | color: rgba(255, 255, 255, 0.7); 423 | transition: .4s .2s opacity, .4s .2s transform; } 424 | .percent > div { 425 | display: inline-block; } 426 | .percent > span { 427 | margin-left: -.4em; 428 | font-size: .5em; } 429 | 430 | .percent-left-enter-active, .percent-left-leave-active { 431 | transition: transform .1s ease; } 432 | 433 | .percent-left-enter, .percent-left-leave-to { 434 | transform: scale(1.05); } 435 | 436 | button { 437 | z-index: 20; 438 | position: absolute; 439 | display: block; 440 | width: 70%; 441 | margin: auto; 442 | left: 0; 443 | right: 0; 444 | bottom: 1.5em; 445 | padding: .6em; 446 | color: rgba(255, 255, 255, 0.8); 447 | font-size: 1.1em; 448 | font-weight: 300; 449 | letter-spacing: 1px; 450 | text-transform: uppercase; 451 | background: transparent; 452 | border: 1px solid rgba(255, 255, 255, 0.8); 453 | border-radius: 2em; 454 | outline: none; 455 | transition: .2s background, .4s .3s transform, .4s .3s opacity; 456 | cursor: pointer; } 457 | button:hover { 458 | background: #fff; 459 | color: currentColor; } 460 | 461 | .waves { 462 | position: absolute; 463 | bottom: 0; 464 | left: 0; 465 | width: 100%; 466 | height: 100%; 467 | overflow: hidden; 468 | transition: .4s transform ease; 469 | transform-origin: bottom center; } 470 | @media (min-width: 500px) { 471 | .waves { 472 | border-radius: 5px; } } 473 | 474 | .wave { 475 | position: absolute; 476 | bottom: 0; 477 | left: 0; 478 | width: 100%; 479 | height: 100%; 480 | animation: wave 1s linear infinite; } 481 | .wave--front { 482 | z-index: 2; 483 | color: #32BAFA; } 484 | .wave--back { 485 | z-index: 1; 486 | color: #2C7FBE; 487 | animation-direction: reverse; } 488 | 489 | .water { 490 | position: absolute; 491 | bottom: 0; 492 | left: 0; 493 | width: 100%; 494 | height: 80%; 495 | background: currentColor; } 496 | .water svg { 497 | position: absolute; 498 | width: 100%; 499 | left: 0; 500 | right: 0; 501 | bottom: 99.9%; } 502 | 503 | .water:first-of-type { 504 | transform: translate(-100%, 0); } 505 | 506 | svg { 507 | fill: currentColor; } 508 | 509 | @keyframes wave { 510 | 0% { 511 | transform: translate3d(0, 0, 0); } 512 | 50% { 513 | transform: translate3d(50%, 0.5em, 0); } 514 | 100% { 515 | transform: translate3d(100%, 0, 0); } } 516 | 517 | @keyframes pulse { 518 | 0% { 519 | transform: scale(1); } 520 | 50% { 521 | transform: scale(1.1); } 522 | 100% { 523 | transform: scale(1); } } 524 | 525 | @keyframes pulseAway { 526 | 0% { 527 | opacity: 0; 528 | transform: scale(0.5); } 529 | 50% { 530 | opacity: 1; } 531 | 100% { 532 | transform: scale(1.4); } } 533 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reminder App 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
18 | 21 |
26 | 27 | 28 | 29 | 30 |
31 | 32 | {{ tooltipText }} 33 | 34 |
35 |
36 |
37 | sphere 38 |
39 | 40 | 41 | 42 | Select voice 43 |
44 |
45 | 46 | 47 | 48 |
49 | 74 |
75 | 125 |
126 | Your browser doesn't support speech synthesis. 127 |
128 |
129 | 130 |
131 | {{ time.value }} 132 |
133 |
134 |
135 |
136 |
137 |
138 | wave2 139 |
140 |
141 | wave2 142 |
143 |
144 |
145 |
146 | wave2 147 |
148 |
149 | wave2 150 |
151 |
152 |
153 |
154 |
155 | 156 |
{{ percentsLeft }}
157 |
158 | % 159 |
160 |
161 | 166 |
167 | 168 | 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reminderapp", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "node-sass --output-style compressed style.scss style.css", 8 | "develop": "npm run browsersync & node-sass -w style.scss style.css", 9 | "browsersync": "browser-sync start --server -f 'style.scss,script.js'" 10 | }, 11 | "author": "Irko Palenius", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "browser-sync": "latest", 15 | "node-sass": "latest" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /script.es5.js: -------------------------------------------------------------------------------- 1 | var SpeechRecognition = SpeechRecognition || window.webkitSpeechRecognition || undefined; 2 | var numbers = Array.apply(null, Array(101)).map(function (_, i) {return i;}); 3 | 4 | if(SpeechRecognition) { 5 | var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList || undefined; 6 | var SpeechRecognitionEvent = SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent || undefined; 7 | 8 | var commands = ['reset', 'timer' ].concat( numbers); 9 | var grammar = '#JSGF V1.0; grammar colors; public = ' + commands.join(' | ') + ' ;' 10 | 11 | var speechRecognitionList = new SpeechGrammarList(); 12 | speechRecognitionList.addFromString(grammar, 1); 13 | 14 | var recognition = new SpeechRecognition(); 15 | recognition.grammars = speechRecognitionList; 16 | //recognition.continuous = false; 17 | recognition.lang = 'en-US'; 18 | recognition.interimResults = true; 19 | recognition.maxAlternatives = 1; 20 | } 21 | 22 | var speechSynth = new SpeechSynthesisUtterance(); 23 | 24 | var padDigits = function (number, digits) { 25 | return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; 26 | } 27 | 28 | var calculatePercentsLeft = function (value, from) { 29 | return Math.floor(Math.ceil(value/1000) / (from * 60) * 100) 30 | } 31 | 32 | var calculateScaleFactor = function (percent) { 33 | return 1-(100-percent)/100; 34 | } 35 | 36 | function guid() { 37 | function s4() { 38 | return Math.floor((1 + Math.random()) * 0x10000) 39 | .toString(16) 40 | .substring(1); 41 | } 42 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 43 | s4() + '-' + s4() + s4() + s4(); 44 | } 45 | 46 | var settings = { 47 | water: { 48 | warningMsg: 'Remember to drink', 49 | timeIsUpMsg: 'Time\'s up. You really need to drink now', 50 | buttonTxt: 'Drink', 51 | waveFrontColor: '#32BAFA', 52 | waveBackColor: '#2C7FBE', 53 | stageBg: '#1E384C', 54 | durationInMinutes: 1 55 | }, 56 | coffee: { 57 | warningMsg: 'It\'s almost coffee time.', 58 | timeIsUpMsg: 'Time\'s up. Let\'s take a coffee break!', 59 | buttonTxt: 'Drink coffee', 60 | waveFrontColor: '#b39374', 61 | waveBackColor: '#7a6057', 62 | stageBg: '#392a2c', 63 | durationInMinutes: 2 64 | }, 65 | break: { 66 | warningMsg: 'It is time to rest your eyes soon!', 67 | timeIsUpMsg: 'Time\'s up. Now, it\'s really time to rest your eyes!', 68 | buttonTxt: 'Take a break', 69 | waveFrontColor: '#02C39A', 70 | waveBackColor: '#028090', 71 | stageBg: '#012F35', 72 | durationInMinutes: 1 73 | } 74 | }; 75 | 76 | new Vue({ 77 | el: '#stage', 78 | data: function data() { 79 | return { 80 | color: '', 81 | percents: [100], 82 | percentsLeft: 100, 83 | secondsLeft: 0, 84 | waveStyles: '', 85 | duration: 1, 86 | timer: [], 87 | voicesOpen: false, 88 | voices: [], 89 | selectedVoice: {}, 90 | countdownObj: {}, 91 | activeReminder: settings.water, 92 | menuOpen: false, 93 | isListening: false, 94 | tooltipText: 'Say eg. "reset"', 95 | stageBg: settings.water.stageBg 96 | } 97 | }, 98 | mounted: function mounted() { 99 | var this$1 = this; 100 | 101 | this.resetTimer(); 102 | this.voices = speechSynthesis.getVoices(); 103 | 104 | if(this.voices.length === 0) { 105 | speechSynthesis.onvoiceschanged = function () { 106 | this$1.voices = speechSynthesis.getVoices(); 107 | }; 108 | } 109 | }, 110 | computed: { 111 | supportSpeechSynth: function supportSpeechSynth() { 112 | return 'speechSynthesis' in window; 113 | }, 114 | supportSpeechRecognition: function supportSpeechRecognition() { 115 | return SpeechRecognition; 116 | } 117 | }, 118 | watch: { 119 | percentsLeft: function(val, oldVal) { 120 | if (val === oldVal) { 121 | return; 122 | } 123 | this.percents.splice(0, 1); 124 | this.percents.push(val); 125 | } 126 | }, 127 | methods: { 128 | setActiveReminder: function setActiveReminder(reminder) { 129 | this.activeReminder = settings[reminder]; 130 | this.stageBg = this.activeReminder.stageBg; 131 | }, 132 | toggleMenu: function toggleMenu() { 133 | this.menuOpen = !this.menuOpen; 134 | if(this.menuOpen) { 135 | this.pauseTimer(); 136 | this.waveStyles = "transform: translate3d(0,100%,0); transition-delay: .25s;"; 137 | }else { 138 | this.continueTimer(); 139 | } 140 | }, 141 | toggleVoicesMenu: function toggleVoicesMenu() { 142 | this.voicesOpen = !this.voicesOpen; 143 | }, 144 | voiceSelected: function voiceSelected(voice) { 145 | this.selectedVoice = voice; 146 | speechSynth.voice = voice; 147 | }, 148 | start: function start(reminder) { 149 | this.setActiveReminder(reminder); 150 | this.percents = [100]; 151 | this.timer = []; 152 | this.menuOpen = false; 153 | this.resetTimer(); 154 | }, 155 | resetTimer: function resetTimer() { 156 | var durationInSeconds = 60 * this.activeReminder.durationInMinutes; 157 | this.startTimer(durationInSeconds); 158 | }, 159 | startTimer: function startTimer(secondsLeft) { 160 | var this$1 = this; 161 | 162 | var now = new Date(); 163 | 164 | // later on, this timer may be stopped 165 | if(this.countdown) { 166 | window.clearInterval(this.countdown); 167 | } 168 | 169 | this.countdown = countdown(function (ts) { 170 | this$1.secondsLeft= Math.ceil(ts.value/1000); 171 | this$1.percentsLeft = calculatePercentsLeft(ts.value,this$1.activeReminder.durationInMinutes); 172 | this$1.waveStyles = "transform: scale(1," + (calculateScaleFactor(this$1.percentsLeft)) + ")"; 173 | this$1.updateCountdown(ts); 174 | if(this$1.percentsLeft == 10) { 175 | this$1.giveWarning(); 176 | } 177 | if(this$1.percentsLeft <= 0){ 178 | this$1.timeIsUpMessage(); 179 | this$1.pauseTimer(); 180 | this$1.timer = []; 181 | setTimeout(function () { 182 | this$1.startListenVoiceCommands(); 183 | }, 1500); 184 | 185 | } 186 | }, now.getTime() + (secondsLeft * 1000)); 187 | }, 188 | updateCountdown: function updateCountdown(ts) { 189 | if(this.timer.length > 2) { 190 | this.timer.splice(2); 191 | } 192 | 193 | var newTime = { 194 | id: guid(), 195 | value: ((padDigits(ts.minutes, 2)) + ":" + (padDigits(ts.seconds, 2))) 196 | }; 197 | 198 | this.timer.unshift(newTime); 199 | }, 200 | pauseTimer: function pauseTimer() { 201 | window.clearInterval(this.countdown); 202 | }, 203 | continueTimer: function continueTimer() { 204 | if(this.secondsLeft > 0) { 205 | this.startTimer(this.secondsLeft-1); 206 | } 207 | }, 208 | giveWarning: function giveWarning() { 209 | speechSynth.text = this.activeReminder.warningMsg; 210 | window.speechSynthesis.speak(speechSynth); 211 | }, 212 | timeIsUpMessage: function timeIsUpMessage() { 213 | speechSynth.text = this.activeReminder.timeIsUpMsg; 214 | window.speechSynthesis.speak(speechSynth); 215 | }, 216 | timerResetMessage: function timerResetMessage() { 217 | speechSynth.text = "Timer reset. Time left " + (this.activeReminder.durationInMinutes) + " " + (this.activeReminder.durationInMinutes > 1 ? 'minutes': 'minute'); 218 | window.speechSynthesis.speak(speechSynth); 219 | }, 220 | reset: function reset() { 221 | this.resetTimer(); 222 | this.timerResetMessage(); 223 | }, 224 | startListenVoiceCommands: function startListenVoiceCommands() { 225 | var this$1 = this; 226 | 227 | if(this.isListening || !this.supportSpeechRecognition) { return; } 228 | 229 | this.isListening = true; 230 | recognition.start(); 231 | recognition.onresult = function (event) { 232 | var last = event.results.length - 1; 233 | var transcript = event.results[last][0].transcript; 234 | var splittedTranscript = transcript.split(' '); 235 | var isFinal = event.results[last].isFinal; 236 | 237 | this$1.tooltipText = transcript; 238 | 239 | if(transcript == "reset") { 240 | this$1.resetTimer(); 241 | this$1.timerResetMessage(); 242 | } 243 | if( 244 | splittedTranscript.length >= 3 && 245 | splittedTranscript[0] == 'timer' && 246 | isFinal && 247 | numbers.includes(Number(splittedTranscript[1])) && 248 | (splittedTranscript[2] == 'minute' || splittedTranscript[2] == 'minutes') 249 | ) { 250 | this$1.activeReminder.durationInMinutes = numbers[splittedTranscript[1]]; 251 | this$1.resetTimer(); 252 | this$1.timerResetMessage(); 253 | } 254 | 255 | 256 | } 257 | recognition.onend = function () { 258 | this$1.isListening = false; 259 | this$1.tooltipText = ''; 260 | recognition.stop(); 261 | } 262 | recognition.onsoundend = function () { 263 | this$1.isListening = false; 264 | recognition.stop(); 265 | } 266 | }, 267 | mouseOver: function mouseOver(type) { 268 | this.stageBg = settings[type].stageBg; 269 | }, 270 | mouseOut: function mouseOut() { 271 | this.stageBg = this.activeReminder.stageBg; 272 | } 273 | } 274 | }); 275 | 276 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | var SpeechRecognition = SpeechRecognition || window.webkitSpeechRecognition || undefined; 2 | var numbers = Array.apply(null, Array(101)).map(function (_, i) {return i;}); 3 | 4 | if(SpeechRecognition) { 5 | var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList || undefined; 6 | var SpeechRecognitionEvent = SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent || undefined; 7 | 8 | var commands = ['reset', 'timer', ...numbers]; 9 | var grammar = '#JSGF V1.0; grammar colors; public = ' + commands.join(' | ') + ' ;' 10 | 11 | var speechRecognitionList = new SpeechGrammarList(); 12 | speechRecognitionList.addFromString(grammar, 1); 13 | 14 | var recognition = new SpeechRecognition(); 15 | recognition.grammars = speechRecognitionList; 16 | //recognition.continuous = false; 17 | recognition.lang = 'en-US'; 18 | recognition.interimResults = true; 19 | recognition.maxAlternatives = 1; 20 | } 21 | 22 | var speechSynth = new SpeechSynthesisUtterance(); 23 | 24 | const padDigits = (number, digits) => { 25 | return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; 26 | } 27 | 28 | const calculatePercentsLeft = (value, from) => { 29 | return Math.floor(Math.ceil(value/1000) / (from * 60) * 100) 30 | } 31 | 32 | const calculateScaleFactor = (percent) => { 33 | return 1-(100-percent)/100; 34 | } 35 | 36 | function guid() { 37 | function s4() { 38 | return Math.floor((1 + Math.random()) * 0x10000) 39 | .toString(16) 40 | .substring(1); 41 | } 42 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 43 | s4() + '-' + s4() + s4() + s4(); 44 | } 45 | 46 | const settings = { 47 | water: { 48 | warningMsg: 'Remember to drink', 49 | timeIsUpMsg: 'Time\'s up. You really need to drink now', 50 | buttonTxt: 'Drink', 51 | waveFrontColor: '#32BAFA', 52 | waveBackColor: '#2C7FBE', 53 | stageBg: '#1E384C', 54 | durationInMinutes: 1 55 | }, 56 | coffee: { 57 | warningMsg: 'It\'s almost coffee time.', 58 | timeIsUpMsg: 'Time\'s up. Let\'s take a coffee break!', 59 | buttonTxt: 'Drink coffee', 60 | waveFrontColor: '#b39374', 61 | waveBackColor: '#7a6057', 62 | stageBg: '#392a2c', 63 | durationInMinutes: 2 64 | }, 65 | break: { 66 | warningMsg: 'It is time to rest your eyes soon!', 67 | timeIsUpMsg: 'Time\'s up. Now, it\'s really time to rest your eyes!', 68 | buttonTxt: 'Take a break', 69 | waveFrontColor: '#02C39A', 70 | waveBackColor: '#028090', 71 | stageBg: '#012F35', 72 | durationInMinutes: 1 73 | } 74 | }; 75 | 76 | new Vue({ 77 | el: '#stage', 78 | data() { 79 | return { 80 | color: '', 81 | percents: [100], 82 | percentsLeft: 100, 83 | secondsLeft: 0, 84 | waveStyles: '', 85 | duration: 1, 86 | timer: [], 87 | voicesOpen: false, 88 | voices: [], 89 | selectedVoice: {}, 90 | countdownObj: {}, 91 | activeReminder: settings.water, 92 | menuOpen: false, 93 | isListening: false, 94 | tooltipText: 'Say eg. "reset"', 95 | stageBg: settings.water.stageBg 96 | } 97 | }, 98 | mounted() { 99 | this.resetTimer(); 100 | this.voices = speechSynthesis.getVoices(); 101 | 102 | if(this.voices.length === 0) { 103 | speechSynthesis.onvoiceschanged = () => { 104 | this.voices = speechSynthesis.getVoices(); 105 | }; 106 | } 107 | }, 108 | computed: { 109 | supportSpeechSynth() { 110 | return 'speechSynthesis' in window; 111 | }, 112 | supportSpeechRecognition() { 113 | return SpeechRecognition; 114 | } 115 | }, 116 | watch: { 117 | percentsLeft: function(val, oldVal) { 118 | if (val === oldVal) { 119 | return; 120 | } 121 | this.percents.splice(0, 1); 122 | this.percents.push(val); 123 | } 124 | }, 125 | methods: { 126 | setActiveReminder(reminder) { 127 | this.activeReminder = settings[reminder]; 128 | this.stageBg = this.activeReminder.stageBg; 129 | }, 130 | toggleMenu() { 131 | this.menuOpen = !this.menuOpen; 132 | if(this.menuOpen) { 133 | this.pauseTimer(); 134 | this.waveStyles = `transform: translate3d(0,100%,0); transition-delay: .25s;`; 135 | }else { 136 | this.continueTimer(); 137 | } 138 | }, 139 | toggleVoicesMenu() { 140 | this.voicesOpen = !this.voicesOpen; 141 | }, 142 | voiceSelected(voice) { 143 | this.selectedVoice = voice; 144 | speechSynth.voice = voice; 145 | }, 146 | start(reminder) { 147 | this.setActiveReminder(reminder); 148 | this.percents = [100]; 149 | this.timer = []; 150 | this.menuOpen = false; 151 | this.resetTimer(); 152 | }, 153 | resetTimer() { 154 | let durationInSeconds = 60 * this.activeReminder.durationInMinutes; 155 | this.startTimer(durationInSeconds); 156 | }, 157 | startTimer(secondsLeft) { 158 | let now = new Date(); 159 | 160 | // later on, this timer may be stopped 161 | if(this.countdown) { 162 | window.clearInterval(this.countdown); 163 | } 164 | 165 | this.countdown = countdown(ts => { 166 | this.secondsLeft= Math.ceil(ts.value/1000); 167 | this.percentsLeft = calculatePercentsLeft(ts.value,this.activeReminder.durationInMinutes); 168 | this.waveStyles = `transform: scale(1,${calculateScaleFactor(this.percentsLeft)})`; 169 | this.updateCountdown(ts); 170 | if(this.percentsLeft == 10) { 171 | this.giveWarning(); 172 | } 173 | if(this.percentsLeft <= 0){ 174 | this.timeIsUpMessage(); 175 | this.pauseTimer(); 176 | this.timer = []; 177 | setTimeout(() => { 178 | this.startListenVoiceCommands(); 179 | }, 1500); 180 | 181 | } 182 | }, now.getTime() + (secondsLeft * 1000)); 183 | }, 184 | updateCountdown(ts) { 185 | if(this.timer.length > 2) { 186 | this.timer.splice(2); 187 | } 188 | 189 | const newTime = { 190 | id: guid(), 191 | value: `${padDigits(ts.minutes, 2)}:${padDigits(ts.seconds, 2)}` 192 | }; 193 | 194 | this.timer.unshift(newTime); 195 | }, 196 | pauseTimer() { 197 | window.clearInterval(this.countdown); 198 | }, 199 | continueTimer() { 200 | if(this.secondsLeft > 0) { 201 | this.startTimer(this.secondsLeft-1); 202 | } 203 | }, 204 | giveWarning() { 205 | speechSynth.text = this.activeReminder.warningMsg; 206 | window.speechSynthesis.speak(speechSynth); 207 | }, 208 | timeIsUpMessage() { 209 | speechSynth.text = this.activeReminder.timeIsUpMsg; 210 | window.speechSynthesis.speak(speechSynth); 211 | }, 212 | timerResetMessage() { 213 | speechSynth.text = `Timer reset. Time left ${this.activeReminder.durationInMinutes} ${this.activeReminder.durationInMinutes > 1 ? 'minutes': 'minute'}`; 214 | window.speechSynthesis.speak(speechSynth); 215 | }, 216 | reset() { 217 | this.resetTimer(); 218 | this.timerResetMessage(); 219 | }, 220 | startListenVoiceCommands() { 221 | if(this.isListening || !this.supportSpeechRecognition) return; 222 | 223 | this.isListening = true; 224 | recognition.start(); 225 | recognition.onresult = (event) => { 226 | let last = event.results.length - 1; 227 | let transcript = event.results[last][0].transcript; 228 | let splittedTranscript = transcript.split(' '); 229 | let isFinal = event.results[last].isFinal; 230 | 231 | this.tooltipText = transcript; 232 | 233 | if(transcript == "reset") { 234 | this.resetTimer(); 235 | this.timerResetMessage(); 236 | } 237 | if( 238 | splittedTranscript.length >= 3 && 239 | splittedTranscript[0] == 'timer' && 240 | isFinal && 241 | numbers.includes(Number(splittedTranscript[1])) && 242 | (splittedTranscript[2] == 'minute' || splittedTranscript[2] == 'minutes') 243 | ) { 244 | this.activeReminder.durationInMinutes = numbers[splittedTranscript[1]]; 245 | this.resetTimer(); 246 | this.timerResetMessage(); 247 | } 248 | 249 | 250 | } 251 | recognition.onend = () => { 252 | this.isListening = false; 253 | this.tooltipText = ''; 254 | recognition.stop(); 255 | } 256 | recognition.onsoundend = () => { 257 | this.isListening = false; 258 | recognition.stop(); 259 | } 260 | }, 261 | mouseOver(type) { 262 | this.stageBg = settings[type].stageBg; 263 | }, 264 | mouseOut() { 265 | this.stageBg = this.activeReminder.stageBg; 266 | } 267 | } 268 | }); 269 | -------------------------------------------------------------------------------- /style.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:100,400'); 2 | 3 | $blue-dark: #1E384C; 4 | $blue: #2C7FBE; 5 | $blue-light: #32BAFA; 6 | $green: #02C39A; 7 | 8 | $stage-bg: $blue-dark; 9 | 10 | *, *::after, *::before { 11 | box-sizing: border-box; 12 | } 13 | html, body { 14 | height: 100%; 15 | min-height: 100%; 16 | } 17 | body { 18 | margin: 0; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | font-family: 'Roboto', sans-serif; 23 | background: linear-gradient(to bottom right, #c8c897, #6590A2); 24 | } 25 | [v-cloak] { display:none; } 26 | a { 27 | -webkit-tap-highlight-color: rgba(0,0,0,0); 28 | user-select: none; 29 | } 30 | 31 | ::-webkit-scrollbar { 32 | display: none; 33 | } 34 | 35 | .stage { 36 | position: relative; 37 | overflow: hidden; 38 | width: 100%; 39 | height: 100%; 40 | background: #fff; 41 | box-shadow: 0 10px 20px rgba(0,0,0,0.2); 42 | background: $stage-bg; 43 | transition: background-color .3s; 44 | 45 | @media (min-width: 500px) { 46 | border-radius: 5px; 47 | max-height: 550px; 48 | max-width: 350px; 49 | } 50 | 51 | &.menu-open { 52 | .microphone { 53 | transform: translate3d(-1em,0,0); 54 | opacity: 0; 55 | } 56 | .voices-menu__button { 57 | z-index: 40; 58 | transform: translate3d(0,0,0); 59 | opacity: 1; 60 | } 61 | .menu { 62 | z-index: 25; 63 | } 64 | .time { 65 | transform: translate3d(0,-200%,0); 66 | transition: .5s opacity, .5s transform; 67 | opacity: 0; 68 | } 69 | button { 70 | transform: translate3d(0,200%,0); 71 | transition-delay: 0s; 72 | opacity: 0; 73 | } 74 | .percent { 75 | transition: .4s opacity, .4s transform; 76 | transform: translate3d(0,50%,0); 77 | opacity: 0; 78 | } 79 | 80 | .menu__item { 81 | opacity: 1; 82 | transform: translate3d(0,0,0); 83 | 84 | &:nth-child(1) { 85 | transition-delay: .2s; 86 | } 87 | &:nth-child(2) { 88 | transition-delay: .3s; 89 | } 90 | &:nth-child(3) { 91 | transition-delay: .4s; 92 | } 93 | } 94 | } 95 | 96 | &.voices-open { 97 | .voices-menu { 98 | z-index: 35; 99 | } 100 | .voices-menu__bg { 101 | transform: scale(6); 102 | } 103 | 104 | .voices-menu__close { 105 | opacity: 1; 106 | transform: translate3d(0,0,0) rotate(0); 107 | } 108 | 109 | .voices-list-wrapper { 110 | opacity: 1; 111 | } 112 | .voices-list__item { 113 | opacity: 1; 114 | transform: translate(0,0); 115 | transition: opacity .15s, transform .2s; 116 | } 117 | 118 | @for $i from 1 through 10 { 119 | .voices-list__item:nth-child(#{$i}) { 120 | transition-delay: 75ms * $i; 121 | } 122 | } 123 | } 124 | } 125 | 126 | .microphone { 127 | z-index: 30; 128 | position: absolute; 129 | top: -.5em; 130 | left: -.8em; 131 | width: 70px; 132 | height: 70px; 133 | display: flex; 134 | justify-content: center; 135 | align-items: center; 136 | cursor: pointer; 137 | 138 | color: rgba(#fff, .5); 139 | transition: opacity .3s, transform .3s, color .2s; 140 | 141 | &:hover { 142 | color: rgba(#fff, .8); 143 | } 144 | 145 | svg { 146 | z-index: 2; 147 | position: relative; 148 | font-size: 2em; 149 | width: 1em; 150 | height: 1em; 151 | } 152 | &:before, 153 | &:after { 154 | content: ''; 155 | position: absolute; 156 | top: 0; 157 | left: 0; 158 | width: 100%; 159 | height: 100%; 160 | border-radius: 50%; 161 | opacity: 0; 162 | } 163 | &:after { 164 | z-index: 1; 165 | background: rgba(#fff, .1); 166 | transition: opacity .3s; 167 | } 168 | 169 | &:before { 170 | z-index: 2; 171 | border: 3px solid rgba(#fff, .1); 172 | opacity: 0; 173 | } 174 | 175 | &.is-listening { 176 | color: rgba(#D82E2E, 1); 177 | &:before { 178 | animation: pulseAway 1s infinite; 179 | } 180 | &:after { 181 | opacity: 1; 182 | animation: pulse 1.5s linear infinite; 183 | } 184 | } 185 | 186 | .voice-tooltip { 187 | position: absolute; 188 | top: 110%; 189 | left: 25px; 190 | padding: .4em .6em; 191 | color: rgba(#fff, .8); 192 | 193 | font-size: .8em; 194 | font-weight: 300; 195 | text-transform: uppercase; 196 | white-space: nowrap; 197 | 198 | background: rgba(#fff, .1); 199 | border-radius: 3px; 200 | 201 | &:before { 202 | content: ''; 203 | position: absolute; 204 | bottom: 100%; 205 | left: 5px; 206 | width: 0; 207 | height: 0; 208 | border-style: solid; 209 | border-width: 0 5px 5px 5px; 210 | border-color: transparent transparent rgba(#fff, .1) transparent; 211 | } 212 | } 213 | } 214 | 215 | .fade-enter-active, 216 | .fade-leave-active { 217 | transition: all .15s ease; 218 | } 219 | .fade-enter, .fade-leave-to { 220 | opacity: 0; 221 | } 222 | 223 | .voices-menu { 224 | position: absolute; 225 | top: 0; 226 | left: 0; 227 | right: 0; 228 | bottom: 0; 229 | 230 | @media (min-width: 500px) { 231 | border-radius: 5px; 232 | overflow: hidden; 233 | } 234 | 235 | &__bg { 236 | position: absolute; 237 | top: -15em; 238 | left: -15em; 239 | transform-origin: 50% 50%; 240 | width: 20em; 241 | height: 20em; 242 | color: #222; 243 | transform: scale(0.2); 244 | transition: transform .3s; 245 | } 246 | 247 | &__button { 248 | position: absolute; 249 | top: 0; 250 | left: 0; 251 | padding: .8em .6em; 252 | color: rgba(#fff, .5); 253 | opacity: 0; 254 | cursor: pointer; 255 | transform: translate3d(1em,0,0); 256 | transition: opacity .3s, transform .3s, color .2s; 257 | 258 | &:hover { 259 | color: rgba(#fff, .8); 260 | } 261 | 262 | > * { 263 | vertical-align: middle; 264 | font-weight: 300; 265 | letter-spacing: 1px; 266 | } 267 | svg { 268 | width: 1.8em; 269 | height: 1.8em; 270 | } 271 | } 272 | 273 | &__close { 274 | position: absolute; 275 | top: 0; 276 | right: 0; 277 | padding: 10px 10px; 278 | font-size: 2em; 279 | font-weight: 300; 280 | color: rgba(#fff, .5); 281 | opacity: 0; 282 | cursor: pointer; 283 | transform: translate3d(1em,0,0) rotate(45deg); 284 | transition: opacity .3s, transform .3s, color .2s; 285 | 286 | svg { 287 | width: 1em; 288 | height: 1em; 289 | } 290 | 291 | &:hover { 292 | color: rgba(#fff, .8); 293 | } 294 | } 295 | } 296 | .voices-list-wrapper { 297 | position: absolute; 298 | top: 60px; 299 | left: 0; 300 | bottom: 0; 301 | right: 0; 302 | overflow-y: auto; 303 | opacity: 0; 304 | } 305 | .voices-list { 306 | margin: 0; 307 | padding: 0; 308 | 309 | &__item { 310 | display: block; 311 | opacity: 0; 312 | transform: translate(0,1em); 313 | 314 | &.is-selected { 315 | .voices-list__icon { 316 | opacity: 1; 317 | transform: translate3d(0,0,0) rotate(0); 318 | } 319 | } 320 | } 321 | 322 | &__icon { 323 | position: relative; 324 | margin-right: 20px; 325 | color: $green; 326 | opacity: 0; 327 | transform: translate3d(-1em,0,0) rotate(-30deg); 328 | transition: opacity .2s, transform .2s; 329 | 330 | svg { 331 | width: 1.2em; 332 | height: 1.2em; 333 | } 334 | } 335 | 336 | &__link { 337 | display: flex; 338 | padding: .5em 1.1em; 339 | font-size: 1.3em; 340 | font-weight: 300; 341 | color: rgba(#fff, .8); 342 | text-decoration: none; 343 | 344 | &:hover { 345 | background: rgba(#fff, .05); 346 | } 347 | 348 | span { 349 | display: inline-block; 350 | vertical-align: middle; 351 | } 352 | } 353 | 354 | &__content { 355 | line-height: 1; 356 | span { 357 | font-size: .5em; 358 | } 359 | } 360 | 361 | &__default { 362 | color: $green; 363 | } 364 | } 365 | 366 | .menu { 367 | z-index: 10; 368 | position: absolute; 369 | width: 100%; 370 | height: 100%; 371 | display: flex; 372 | justify-content: center; 373 | align-items: center; 374 | 375 | &__button { 376 | z-index: 30; 377 | position: absolute; 378 | top: 0; 379 | right: 0; 380 | display: inline-block; 381 | padding: 1.5em 1em; 382 | cursor: pointer; 383 | 384 | &:hover { 385 | .menu__dot, 386 | .menu__dot:before, 387 | .menu__dot:after { 388 | background: rgba(#fff, .8); 389 | } 390 | } 391 | } 392 | &__dot { 393 | position: relative; 394 | border-radius: 50%; 395 | width: 6px; 396 | height: 6px; 397 | background: rgba(#fff, .5); 398 | transition: background .2s; 399 | 400 | &:before, 401 | &:after { 402 | position: absolute; 403 | content: ''; 404 | border-radius: 50%; 405 | width: 6px; 406 | height: 6px; 407 | background: rgba(#fff, .5); 408 | transition: background .2s; 409 | } 410 | &:before { 411 | top: 10px; 412 | } 413 | &:after { 414 | bottom: 10px; 415 | } 416 | } 417 | 418 | &__list { 419 | list-style: none; 420 | padding: 0; 421 | margin: 0; 422 | width: 100%; 423 | } 424 | 425 | &__item { 426 | overflow: hidden; 427 | opacity: 0; 428 | transform: translate3d(0,100%,0); 429 | transition: .4s transform, .4s opacity; 430 | 431 | a { 432 | font-size: 1.8em; 433 | font-weight: 300; 434 | display: block; 435 | color: rgba(#fff, .5); 436 | text-transform: uppercase; 437 | text-decoration: none; 438 | padding: .5em 1.5em; 439 | 440 | span { 441 | display: inline-block; 442 | vertical-align: middle; 443 | transition: transform .3s; 444 | } 445 | 446 | &:hover, 447 | &:focus { 448 | svg { 449 | transform: scale(1.2); 450 | } 451 | 452 | .water-glass__water { 453 | fill: $blue-light; 454 | transform: scale(1,.8); 455 | } 456 | .coffee-cup__coffee { 457 | fill: #BF9E87; 458 | transform: scale(1,.8); 459 | } 460 | .clock__short { 461 | fill: #02C39A; 462 | transform-origin: 0% 50%; 463 | transform: rotate(20deg); 464 | transition: transform 1s, color .2s; 465 | } 466 | .clock__long { 467 | fill: #02C39A; 468 | transition: transform 1s, color .2s; 469 | transform-origin: 50% 95%; 470 | transform: rotate(360deg); 471 | } 472 | } 473 | } 474 | 475 | svg { 476 | display: inline-block; 477 | vertical-align: middle; 478 | width: 1em; 479 | height: 1em; 480 | margin-right: 1em; 481 | transition: transform .3s; 482 | 483 | path { 484 | fill: #fff; 485 | transition: all .3s; 486 | transform-origin: 100% 100%; 487 | } 488 | } 489 | 490 | } 491 | } 492 | 493 | .browser-support { 494 | color: #fff; 495 | font-size: .8rem; 496 | text-align: center; 497 | padding: .5rem; 498 | } 499 | 500 | .content { 501 | z-index: 20; 502 | position: absolute; 503 | bottom: 0; 504 | left: 0; 505 | width: 100%; 506 | height: 100%; 507 | display: flex; 508 | align-items: center; 509 | justify-content: center; 510 | } 511 | 512 | .time { 513 | overflow: hidden; 514 | padding: 1em; 515 | font-size: 1.1em; 516 | text-align: center; 517 | transition: .5s .2s opacity, .5s transform .2s; 518 | } 519 | 520 | .timer__item { 521 | transition: all 1s; 522 | margin-right: 10px; 523 | color: rgba(#fff, .8); 524 | 525 | &:first-child, 526 | &:nth-child(3) { 527 | color: rgba(#fff,.2); 528 | } 529 | } 530 | .timer-enter, .timer-leave-to { 531 | opacity: 0; 532 | transform: translate3d(0,-100%,0); 533 | } 534 | .timer-leave-to { 535 | transition-duration: .5s; 536 | } 537 | .timer-leave-active { 538 | transform: translate3d(0,0,0); 539 | } 540 | 541 | .percent { 542 | z-index: 2; 543 | position: relative; 544 | font-size: 7em; 545 | font-weight: 100; 546 | color: rgba(#fff, 0.7); 547 | transition: .4s .2s opacity, .4s .2s transform; 548 | 549 | > div { 550 | display: inline-block; 551 | } 552 | > span { 553 | margin-left: -.4em; 554 | font-size: .5em; 555 | } 556 | } 557 | .percent-left-enter-active, .percent-left-leave-active { 558 | transition: transform .1s ease; 559 | } 560 | .percent-left-enter, .percent-left-leave-to { 561 | transform: scale(1.05); 562 | } 563 | 564 | button { 565 | z-index: 20; 566 | position: absolute; 567 | display: block; 568 | width: 70%; 569 | margin: auto; 570 | left: 0; 571 | right: 0; 572 | bottom: 1.5em; 573 | padding: .6em; 574 | 575 | color: rgba(#fff, 0.8); 576 | font-size: 1.1em; 577 | font-weight: 300; 578 | letter-spacing: 1px; 579 | text-transform: uppercase; 580 | 581 | background: transparent; 582 | border: 1px solid rgba(#fff, 0.8); 583 | border-radius: 2em; 584 | outline: none; 585 | transition: .2s background, .4s .3s transform, .4s .3s opacity; 586 | cursor: pointer; 587 | 588 | &:hover { 589 | background: #fff; 590 | color: currentColor; 591 | } 592 | } 593 | .waves { 594 | position: absolute; 595 | bottom: 0; 596 | left: 0; 597 | width: 100%; 598 | height: 100%; 599 | overflow: hidden; 600 | 601 | transition: .4s transform ease; 602 | transform-origin: bottom center; 603 | 604 | @media (min-width: 500px) { 605 | border-radius: 5px; 606 | } 607 | } 608 | .wave { 609 | position: absolute; 610 | bottom: 0; 611 | left: 0; 612 | width: 100%; 613 | height: 100%; 614 | animation: wave 1s linear infinite; 615 | 616 | &--front { 617 | z-index: 2; 618 | color: $blue-light; 619 | } 620 | 621 | &--back { 622 | z-index: 1; 623 | color: $blue; 624 | animation-direction: reverse; 625 | } 626 | } 627 | 628 | .water { 629 | position: absolute; 630 | bottom: 0; 631 | left: 0; 632 | width: 100%; 633 | height: 80%; 634 | background: currentColor; 635 | 636 | svg { 637 | position: absolute; 638 | width: 100%; 639 | left: 0; 640 | right: 0; 641 | bottom: 99.9%; 642 | } 643 | } 644 | .water:first-of-type { 645 | transform: translate(-100%,0); 646 | } 647 | 648 | svg { 649 | fill: currentColor; 650 | } 651 | 652 | @keyframes wave{ 653 | 0% { 654 | transform: translate3d(0,0,0); 655 | } 656 | 50% { 657 | transform: translate3d(50%,.5em,0); 658 | } 659 | 100% { 660 | transform: translate3d(100%,0,0); 661 | } 662 | } 663 | 664 | @keyframes pulse{ 665 | 0% { 666 | transform: scale(1); 667 | } 668 | 50% { 669 | transform: scale(1.1); 670 | } 671 | 100% { 672 | transform: scale(1); 673 | } 674 | } 675 | 676 | @keyframes pulseAway { 677 | 0% { 678 | opacity: 0; 679 | transform: scale(.5); 680 | } 681 | 50% { 682 | opacity: 1; 683 | } 684 | 100% { 685 | transform: scale(1.4); 686 | } 687 | } --------------------------------------------------------------------------------