Skip to content

Instantly share code, notes, and snippets.

@mattdanielbrown
Created June 8, 2024 01:00
Show Gist options
  • Select an option

  • Save mattdanielbrown/5e834651c72e71638021bf6a669c6af0 to your computer and use it in GitHub Desktop.

Select an option

Save mattdanielbrown/5e834651c72e71638021bf6a669c6af0 to your computer and use it in GitHub Desktop.

Revisions

  1. mattdanielbrown created this gist Jun 8, 2024.
    76 changes: 76 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,76 @@
    <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>
    229 changes: 229 additions & 0 deletions script.coffeescript
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,229 @@
    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

    8 changes: 8 additions & 0 deletions singing-text-to-speech.markdown
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    Singing Text To Speech
    ----------------------
    Still a WIP but it's fun to play with.
    Only tested in chrome.

    A [Pen](https://codepen.io/mattdanielbrown/pen/LYojZqB) by [Matt Daniel Brown](https://codepen.io/mattdanielbrown) on [CodePen](https://codepen.io).

    [License](https://codepen.io/license/pen/LYojZqB).
    371 changes: 371 additions & 0 deletions style.sass
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,371 @@
    @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