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 |