Still a WIP but it's fun to play with. Only tested in chrome.
A Pen by Matt Daniel Brown on CodePen.
| <menu> | |
| <h1></h1> | |
| </menu> | |
| <main> | |
| <ul class="words"> | |
| <div class="slider"> | |
| <li class="word"> | |
| <div class="position"> | |
| <input class="string" type="text" value="daisy"> | |
| </div> | |
| <div class="settings"> | |
| <button class="play"></button> | |
| <button class="delete"></button> | |
| <label>Rate</label> | |
| <input class="rate" type="range" min="0" value="5" max="50"> | |
| <label>Pitch</label> | |
| <input class="pitch" type="range" min="0" value="150" max="200"> </div> | |
| </li><li class="word"> | |
| <div class="position"> | |
| <input class="string" type="text" value="daisy"> </div> | |
| <div class="settings"> | |
| <button class="play"></button> | |
| <button class="delete"></button> | |
| <label>Rate</label> | |
| <input class="rate" type="range" min="0" value="5" max="50"> | |
| <label>Pitch</label> | |
| <input class="pitch" type="range" min="0" value="125" max="200"> </div> | |
| </li><li class="word"> | |
| <div class="position"> | |
| <input class="string" type="text" value="give me your"> </div> | |
| <div class="settings"> | |
| <button class="play"></button> | |
| <button class="delete"></button> | |
| <label>Rate</label> | |
| <input class="rate" type="range" min="0" value="5" max="50"> | |
| <label>Pitch</label> | |
| <input class="pitch" type="range" min="0" value="100" max="200"> </div> | |
| </li><li class="word"> | |
| <div class="position"> | |
| <input class="string" type="text" value="answer"> </div> | |
| <div class="settings"> | |
| <button class="play"></button> | |
| <button class="delete"></button> | |
| <label>Rate</label> | |
| <input class="rate" type="range" min="0" value="5" max="50"> | |
| <label>Pitch</label> | |
| <input class="pitch" type="range" min="0" value="75" max="200"> </div> | |
| </li><li class="word"> | |
| <div class="position"> | |
| <input class="string" type="text" value="do"> </div> | |
| <div class="settings"> | |
| <button class="play"></button> | |
| <button class="delete"></button> | |
| <label>Rate</label> | |
| <input class="rate" type="range" min="0" value="5" max="50"> | |
| <label>Pitch</label> | |
| <input class="pitch" type="range" min="0" value="100" max="200"> </div> | |
| </li> | |
| </div> | |
| </ul> | |
| <button class="plus-sign add-words"></button> | |
| </main> | |
| <nav> | |
| <button class="play-state"></button> | |
| <select class="voice-name"></select><i></i> | |
| <button class="export">Export</button> | |
| <button class="import">Import</button> | |
| </nav> | |
| <aside> | |
| <div class="center"> | |
| <p class="exporting">Copy and save this code.</p> | |
| <p class="importing">Paste your code into the text area.</p> | |
| <button class="close"></button> | |
| <textarea></textarea> | |
| </div> | |
| </aside> |
| class SubClass | |
| constructor: ( parent , data ) -> | |
| @.root = parent.root or parent | |
| @.parent = parent | |
| @.init? data | |
| class Words extends SubClass | |
| template: " | |
| <div class=\"position\"> | |
| <input class=\"string\" type=\"text\" /> | |
| </div> | |
| <div class=\"settings\"> | |
| <button class=\"play\"></button><button class=\"delete\"></button> | |
| <label>Rate</label> | |
| <input class=\"rate\" type=\"range\" min=\"0\" value=\"10\" max=\"50\" /> | |
| <label>Pitch</label> | |
| <input class=\"pitch\" type=\"range\" min=\"0\" value=\"100\" max=\"200\" /> | |
| </div> | |
| " | |
| list: [] | |
| elements: [] | |
| init: -> | |
| @.getElements() | |
| @.addListeners() | |
| getElements: -> | |
| @.sliderContainer = document.querySelector ".words" | |
| @.elementContainer = document.querySelector ".words .slider" | |
| @.elements = document.querySelectorAll ".word" | |
| @.newWordButton = document.querySelector ".add-words" | |
| for element in @.elements | |
| @.addItemListeners element | |
| addListeners: -> | |
| @.newWordButton.addEventListener "click" , @.makeNewWordElement | |
| @.elementContainer.addEventListener "mousewheel" , @.onScroll | |
| onScroll: ( e ) => | |
| e.preventDefault() | |
| @.sliderContainer.scrollLeft += e.deltaY | |
| @.sliderContainer.scrollLeft += e.deltaX | |
| makeNewWordElement: => | |
| item = document.createElement "li" | |
| item.setAttribute "class" , "word" | |
| item.innerHTML = @.template | |
| last = @.elementContainer.lastChild | |
| @.elementContainer.insertBefore item , last | |
| @.addItemListeners item | |
| item.querySelector(".string").focus() | |
| addItemListeners: ( item ) -> | |
| item.querySelector( ".delete" ).onclick = @.deleteItem | |
| item.querySelector( ".play" ).onclick = => @.root.parse.item item | |
| item.querySelector( ".string" ).onchange = => @.root.parse.item item | |
| item.querySelector( ".pitch" ).onchange = => | |
| pitch = item.querySelector( ".pitch" ).value / 100 | |
| item.querySelector( ".string" ).style.top = "#{100 - (10+ ( pitch / 2 * 80 ))}%" | |
| @.root.parse.item item | |
| item.querySelector( ".rate" ).onchange = => @.root.parse.item item | |
| pitch = item.querySelector( ".pitch" ).value / 100 | |
| item.querySelector( ".string" ).style.top = "#{100 - (10+ ( pitch / 2 * 80 ))}%" | |
| deleteItem: ( event ) => | |
| item = event.srcElement.parentNode.parentNode | |
| item.parentNode.removeChild item | |
| class Parse extends SubClass | |
| voice: null | |
| utterances: [] | |
| init: -> | |
| voices = window.speechSynthesis.getVoices() | |
| select = document.querySelector ".voice-name" | |
| if voices.length is 0 | |
| setTimeout => | |
| @.init() | |
| , 100 | |
| else | |
| for voice in voices | |
| if voice.name.substring( 0, 6 ) isnt "Google" | |
| option = document.createElement "option" | |
| option.text = voice.name | |
| option.voice = voice | |
| select.appendChild option | |
| item: ( item ) => | |
| @.utterances = [] | |
| voices = speechSynthesis.getVoices() | |
| name = document.querySelector ".voice-name" | |
| voice = name[ name.selectedIndex ].voice | |
| string = item.querySelector( ".string" ).value | |
| rate = item.querySelector( ".rate" ).value / 10 | |
| pitch = item.querySelector( ".pitch" ).value / 100 | |
| if string.length > 0 | |
| utterance = new SpeechSynthesisUtterance string | |
| utterance.voice = voice | |
| utterance.pitch = pitch | |
| utterance.rate = rate | |
| utterance.element = item | |
| @.utterances.push utterance | |
| @.root.player.run() | |
| words: => | |
| @.utterances = [] | |
| voices = speechSynthesis.getVoices() | |
| items = document.querySelectorAll ".words .slider .word" | |
| name = document.querySelector ".voice-name" | |
| voice = name[ name.selectedIndex ].voice | |
| for item in items | |
| string = item.querySelector( ".string" ).value | |
| rate = item.querySelector( ".rate" ).value / 10 | |
| pitch = item.querySelector( ".pitch" ).value / 100 | |
| item.querySelector( ".string" ).style.top = "#{100 - (10 + ( pitch / 2 * 80 ))}%" | |
| if string.length > 0 | |
| utterance = new SpeechSynthesisUtterance string | |
| utterance.voice = voice | |
| utterance.pitch = pitch | |
| utterance.rate = rate | |
| utterance.element = item | |
| @.utterances.push utterance | |
| @.root.player.run() | |
| class Player extends SubClass | |
| run: -> | |
| utterances = @.root.parse.utterances | |
| if utterances.length > 0 | |
| for utterance, index in utterances | |
| if index + 1 isnt utterances.length | |
| utterance.next = utterances[ index + 1 ] | |
| self = @ | |
| utterance.onend = -> | |
| @.element.classList.remove "playing" | |
| next = @.next | |
| self.speak next | |
| @.onend = undefined | |
| @.speak utterances[0] | |
| speak: ( utterance ) -> | |
| @.lastUtterance?.element.classList.remove "playing" | |
| @.lastUtterance = utterance | |
| @.lastUtterance.element.classList.add "playing" | |
| if @.lastUtterance.onend is null | |
| @.lastUtterance.onend = -> | |
| @.element.classList.remove "playing" | |
| window.speechSynthesis.speak utterance | |
| class Interface extends SubClass | |
| init: -> | |
| @.getElements() | |
| @.addListeners() | |
| getElements: -> | |
| @.playingButton = document.querySelector ".play-state" | |
| addListeners: -> | |
| @.playingButton.addEventListener "click" , => | |
| if @.playingButton.classList.contains "playing" | |
| # todo: pause | |
| else | |
| @.root.parse.words() | |
| setInterval => | |
| if window.speechSynthesis.speaking | |
| @.playingButton.classList.add "playing" | |
| else | |
| @.playingButton.classList.remove "playing" | |
| , 100 | |
| class Porting extends SubClass | |
| init: -> | |
| @.getElements() | |
| @.addListeners() | |
| getElements: -> | |
| @.importButton = document.querySelector "button.import" | |
| @.exportButton = document.querySelector "button.export" | |
| @.modal = document.querySelector "aside" | |
| @.closeButton = @.modal.querySelector ".close" | |
| addListeners: -> | |
| @.importButton.addEventListener "click" , => | |
| @.modal.classList.add "active" | |
| @.modal.classList.remove "exporting" | |
| @.modal.classList.add "importing" | |
| @.exportButton.addEventListener "click" , => | |
| @.modal.classList.add "active" | |
| @.modal.classList.remove "importing" | |
| @.modal.classList.add "exporting" | |
| @.closeButton.addEventListener "click" , => | |
| @.modal.classList.remove "active" | |
| class App | |
| constructor: -> | |
| @.words = new Words @ | |
| @.parse = new Parse @ | |
| @.player = new Player @ | |
| @.interface = new Interface @ | |
| @.porting = new Porting @ | |
| new App | |
Still a WIP but it's fun to play with. Only tested in chrome.
A Pen by Matt Daniel Brown on CodePen.
| @import "compass" | |
| $interface-height: 90px | |
| $interface-color: rgba( 0, 155, 215, 1 ) | |
| =element-reset | |
| font: inherit | |
| -webkit-appearance: none | |
| -moz-appearance: none | |
| -ms-appearance: none | |
| outline: none | |
| border: none | |
| appearance: none | |
| background: none | |
| box-shadow: none | |
| border-radius: 0 | |
| padding: 0 | |
| margin: 0 | |
| select , button , input | |
| @include element-reset | |
| letter-spacing: 0.05em | |
| html , body , menu , main , nav , header , ul | |
| color: rgba( $interface-color , 0.75 ) | |
| letter-spacing: 0.05em | |
| position: absolute | |
| overflow: hidden | |
| font-family: sans-serif | |
| font-weight: 100 | |
| right: 0 | |
| left: 0 | |
| html , body | |
| bottom: 0 | |
| top: 0 | |
| menu , nav | |
| background-color: rgba( $interface-color , 0.85 ) | |
| height: $interface-height | |
| color: white | |
| menu | |
| top: 0 | |
| main | |
| top: $interface-height | |
| bottom: $interface-height | |
| nav | |
| white-space: nowrap | |
| vertical-align: middle | |
| bottom: 0 | |
| h1 | |
| text-transform: uppercase | |
| position: relative | |
| display: inline-block | |
| line-height: $interface-height | |
| font-size: 32px | |
| height: $interface-height | |
| padding: 0 15px | |
| label | |
| font-size: 13px | |
| padding: 5px 15px | |
| display: block | |
| select | |
| background-color: white | |
| border: 2px solid white | |
| color: $interface-color | |
| vertical-align: middle | |
| display: inline-block | |
| margin: -30px 15px 0 0 | |
| padding: 10px 35px 10px 10px | |
| cursor: pointer | |
| position: relative | |
| z-index: 2 | |
| i | |
| position: absolute | |
| display: inline-block | |
| pointer-events: none | |
| border-top: 7px solid $interface-color | |
| border-left: 5px solid transparent | |
| border-right: 5px solid transparent | |
| margin-top: 43px | |
| margin-left: -35px | |
| z-index: 2 | |
| ul | |
| top: 0 | |
| bottom: 0 | |
| right: 64px | |
| .slider | |
| display: inline-block | |
| position: absolute | |
| height: 100% | |
| width: auto | |
| white-space: nowrap | |
| .word | |
| position: relative | |
| display: inline-block | |
| height: 100% | |
| background-color: rgba( $interface-color , 0.1 ) | |
| width: 300px | |
| vertical-align: middle | |
| transition: background 0.1s ease-in-out | |
| &:nth-of-type( even ) | |
| background-color: rgba( $interface-color , 0.025 ) | |
| &.playing , &:nth-of-type( even ).playing | |
| background-color: rgba( $interface-color , 0.5 ) | |
| color: white | |
| &:hover .settings * | |
| transition-delay: 0s | |
| opacity: 1 | |
| .string | |
| font-weight: 100 | |
| width: 250px | |
| color: white | |
| text-align: center | |
| line-height: 36px | |
| height: 36px | |
| left: 50% | |
| top: 50% | |
| background-color: rgba( $interface-color , 0.75 ) | |
| position: absolute | |
| transform: translate( -50% , -50% ) | |
| .position | |
| position: absolute | |
| bottom: 140px | |
| right: 0 | |
| left: 0 | |
| top: 0 | |
| .settings | |
| position: absolute | |
| background-color: rgba( $interface-color , 0.1 ) | |
| text-align: center | |
| width: 100% | |
| height: 140px | |
| bottom: 0 | |
| * | |
| transition: opacity 0.15s ease-in-out | |
| transition-delay: 0.25s | |
| opacity: 0 | |
| .play , .delete | |
| margin: 10px 10px 0 10px | |
| border-radius: 50% | |
| background-color: rgba( $interface-color , 0.4 ) | |
| cursor: pointer | |
| position: relative | |
| display: inline-block | |
| height: 25px | |
| width: 25px | |
| .play::before | |
| content: "" | |
| position: absolute | |
| top: 50% | |
| left: 50% | |
| border-left: 8px solid white | |
| border-top: 5px solid transparent | |
| border-bottom: 5px solid transparent | |
| margin: -5px 0 0 -3px | |
| .delete | |
| &:before , &:after | |
| content: "" | |
| backface-visibility: hidden | |
| position: absolute | |
| background-color: white | |
| margin: -5px 0 0 -1px | |
| height: 10px | |
| width: 2px | |
| left: 50% | |
| top: 50% | |
| &:before | |
| transform: rotate( -45deg ) | |
| &:after | |
| transform: rotate( 45deg ) | |
| label | |
| display: block | |
| text-align: left | |
| input[type="range"] | |
| border-radius: 30px | |
| background-color: rgba( $interface-color , 0.4 ) | |
| padding: 3px | |
| width: 264px | |
| margin-bottom: 15px | |
| input[type=range]::-webkit-slider-thumb | |
| @include element-reset | |
| background-color: white | |
| border-radius: 50% | |
| cursor: grab | |
| width: 7px | |
| height: 7px | |
| &:active | |
| cursor: grabbing | |
| .add-words | |
| position: absolute | |
| border-radius: 50% | |
| background-color: rgba( $interface-color , 0.75 ) | |
| cursor: pointer | |
| margin: -15px 15px 0 0 | |
| width: 30px | |
| height: 30px | |
| right: 0 | |
| top: 50% | |
| &:before , &:after | |
| content: "" | |
| height: 2px | |
| background-color: white | |
| position: absolute | |
| margin: -1px 0 0 -7px | |
| width: 14px | |
| left: 50% | |
| top: 50% | |
| &:before | |
| transform: rotate( 90deg ) | |
| .play-state | |
| display: inline-block | |
| width: $interface-height/2 | |
| height: $interface-height/2 | |
| margin: $interface-height/4 15px | |
| overflow: hidden | |
| box-sizing: border-box | |
| border: 2px solid white | |
| border-radius: 50% | |
| position: relative | |
| cursor: pointer | |
| &:before , &:after | |
| content: "" | |
| position: absolute | |
| transition: transform 0.1s ease-in-out | |
| &:before | |
| border-left: 10px solid white | |
| border-top: 7px solid transparent | |
| border-bottom: 7px solid transparent | |
| margin: -6px 0 0 -4px | |
| left: 50% | |
| top: 50% | |
| transform: translate( 0, 0 ) | |
| &:after | |
| content: "" | |
| position: absolute | |
| background-color: white | |
| box-shadow: 9px 0 0 0 white | |
| margin: -8px 0 0 -8px | |
| width: 5px | |
| height: 16px | |
| transform: translate( $interface-height/3 , 0 ) | |
| &.playing | |
| &:before | |
| transform: translate( -$interface-height/3 , 0 ) | |
| &:after | |
| transform: translate( 0, 0 ) | |
| .export , .import | |
| cursor: pointer | |
| border: 2px solid white | |
| vertical-align: middle | |
| padding: 10px | |
| margin: -30px 15px 0 0 | |
| display: inline-block | |
| color: white | |
| aside | |
| position: absolute | |
| background-color: rgba( $interface-color , 0.9 ) | |
| left: 0 | |
| top: 100% | |
| width: 100% | |
| height: 100% | |
| overflow: hidden | |
| vertical-align: middle | |
| color: white | |
| opacity: 0 | |
| transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out, top 0s linear 0.25s | |
| &.active | |
| transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out, top 0s linear 0s | |
| opacity: 1 | |
| top: 0 | |
| &.exporting .importing | |
| display: none | |
| &.importing .exporting | |
| display: none | |
| .center | |
| transform: translate( -50% , -50% ) | |
| max-width: 90% | |
| width: 800px | |
| display: inline-block | |
| text-align: left | |
| position: absolute | |
| left: 50% | |
| top: 50% | |
| textarea | |
| @include element-reset | |
| font-family: monospace | |
| border: 2px solid white | |
| color: white | |
| box-sizing: border-box | |
| min-height: 350px | |
| padding: 15px | |
| margin: 15px 0 | |
| position: relative | |
| display: block | |
| width: 100% | |
| resize: none | |
| .close | |
| position: absolute | |
| border-radius: 50% | |
| background-color: white | |
| cursor: pointer | |
| margin: 0 | |
| width: 30px | |
| height: 30px | |
| right: 0 | |
| top: -10px | |
| &:before , &:after | |
| content: "" | |
| height: 2px | |
| backface-visibility: hidden | |
| background-color: $interface-color | |
| position: absolute | |
| margin: -1px 0 0 -7px | |
| width: 14px | |
| left: 50% | |
| top: 50% | |
| &:before | |
| transform: rotate( -45deg ) | |
| &:after | |
| transform: rotate( 45deg ) | |
| .import, .export | |
| display: none |