skip to content

JavaScript: Twister Controller with Speech

We're all familiar with the Twister game, and how frustrating it is to try and spin the spinner while also participating in the game.

To solve this problem, we've created a simple JavaScript class which can emulate the spinner and present instructions in both audio and visual format.

Working demonstration

Here you can see and use a working example of the Twister class. Just click inside the circle to start (or stop) the game:

The speed and voice parameters can all be changed using public methods which have been documented below.

Source Code

There is nothing too complicated about the code other than the use of the SpeechSynthesisUtterance interface, which is supported by most major browsers (but not Internet Explorer or some Android browsers).

For this class we've chosen to use hybrid notation which allows us to have both private and public variables and methods in our class:

var Twister = function(el) {   // Original JavaScript code by Chirp Internet: www.chirpinternet.eu   // Please acknowledge use of this code by including this header.   /* private variables */   var displayZone  = document.getElementById(el);   var bodyParts    = ["left hand", "left foot", "right hand", "right foot"];   var colours      = ["red", "yellow", "green", "blue"];   var defaultVoice = navigator.language;   var delay = 5000;   var rate  = 0.8;   var pitch = 1.2;   var availableVoices;   var currentPart, currentColour;   var running = false;   var timer;   var utter = new SpeechSynthesisUtterance();   /* private methods */   var setVoice = function(idx) {     if("undefined" !== typeof availableVoices[idx]) {       utter.voice = availableVoices[idx];       return true;     }   };   var loadVoice = function(lang) {     var idx;     var re = new RegExp("^" + lang);     availableVoices = window.speechSynthesis.getVoices();     for(idx = 0; idx < availableVoices.length; idx++) {       if(re.test(availableVoices[idx].lang)) {         setVoice(idx);         break;       }     }     if(!utter.voice) {       setVoice(0);     }   };   /* sourced from: https://stackoverflow.com/a/33906108 */   Array.prototype.sample = function() {     return this[Math.floor(Math.random() * this.length)];   };   var updateDisplay = function() {     displayZone.className = (currentPart + " " + currentColour).trim();   };   var speak = function() {     currentPart = bodyParts.sample();     if(Math.random() > 0.90) {       currentColour = "";       utter.text = "Wave your " + currentPart + " in the air like you just don't care.";     } else {       currentColour = colours.sample();       utter.text = "Put your " + currentPart + " on " + currentColour + ".";     }     utter.rate = rate;     utter.pitch = pitch;     window.speechSynthesis.speak(utter);     updateDisplay();   };   displayZone.addEventListener("click", function(e) {     if(running) {       currentPart = currentColour = "";       window.clearTimeout(timer);       updateDisplay();       running = false;     } else {       speak();       running = true;     }   }, false);   utter.addEventListener("end", function(e) {     if(running) {       timer = window.setTimeout(speak, delay);     }   }, false);   // sourced from: https://usefulangle.com/post/98/javascript-text-to-speech   if(0 == window.speechSynthesis.getVoices().length) {     window.speechSynthesis.addEventListener("voiceschanged", function(e) {       loadVoice(defaultVoice);     });   } else {     loadVoice(defaultVoice);   }   return {     /* public methods and variables */     setDelay: function(newDelay) {       if((newDelay >= 1) && (newDelay <= 10)) {         delay = 1000 * newDelay;         return true;       }     },     setVoice: function(idx) {       return setVoice(idx);     },     setRate: function(newRate) {       if((newRate >= 0.1) && (newRate <= 10)) {         rate = newRate;         return true;       }     },     setPitch: function(newPitch) {       if((newPitch >= 0) && (newPitch <= 2)) {         pitch = newPitch;         return true;       }     },     voices: availableVoices,   }; }; /* Twister */

expand code box

To handle the display side of things we are just setting the class attribute of our DIV and relying on CSS to change the image and background colour.

The start and stop the script will rely on a click handler.

Constructor and public methods

To instantiate the class we just use:

<head> ... <link rel="stylesheet" href="/twister.css"> </head> <body> <div id="twister"><!-- --></div> <script src="/twister.js"></script> <script> var twister = new Twister("twister"); </script> </body>

where the single argument to the constructor is the id of an empty DIV element on the page where the content is to appear.

If you examine twister object using the Console you will see that the class also provides the following public methods:

twister.setDelay:
function(pause between speech, in seconds)
twister.setPitch:
function(pitch between 0 and 2)
twister.setRate:
function(rate between 0.1 and 10)
twister.setVoice:
function(voiceId)

Where voiceId is an index value from one of the available browser voices, for example:

twister.voices:
[0] => Alex (en-US)
[1] => Alice (it-IT)
[2] => Alva (sv-SE)
...
[49] => Zuzana (cs-CZ)

By default the script will select the first voice matching navigator.language, or otherwise, the first voice in the list. Note that this only changes the voice/accent and not the language. That would be a separate project.

References

< JavaScript

Post your comment or question
top