1

Good day everybody, i want to integrate this simple typing game in my app, but the keyboard don't show up automatically in android, and if i add an input field, then the script doesn't work, any ideas? (note: i am not using jquery)

loadGame();
polyfillKey(); 


function loadGame() {
  var button = document.createElement('button');
  button.textContent = 'Start Game';
  var main = document.getElementById('main');
  main.appendChild(button);
  var rules = document.createElement('p');
  rules.textContent = 'Letters will fall... if you have a keyboard, press the correct key to knock it away before it hits the ground';
  main.appendChild(rules);
  button.addEventListener('click', function startIt(e) {
    main.textContent = '';
    playGame();
  });
}

function playGame(replay) {
  var LETTERS = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
  var animations = {'a':[],'b':[],'c':[],'d':[],'e':[],'f':[],'g':[],'h':[],'i':[],'j':[],'k':[],'l':[],'m':[],'n':[],'o':[],'p':[],'q':[],'r':[],'s':[],'t':[],'u':[],'v':[],'w':[],'x':[],'y':[],'z':[]};
  var gameOn = true;
  var timeOffset = 2000; //interval between letters starting, will be faster over time
  var DURATION = 10000;
  var main = document.getElementById('main');
  var header = document.querySelector('header');
  var scoreElement = document.getElementById('score');
  var score = parseFloat(scoreElement.textContent);
  var rate = 1;
  var RATE_INTERVAL = .05; //playbackRate will increase by .05 for each letter... so after 20 letters, the rate of falling will be 2x what it was at the start
  var misses = 0;

  //Create a letter element and setup its falling animation, add the animation to the active animation array, and setup an onfinish handler that will represent a miss. 
  function create() {
    var idx = Math.floor(Math.random() * LETTERS.length);
    var x = (Math.random() * 85) + 'vw';
    var container = document.createElement('div');
    var letter = document.createElement('span');
    var letterText = document.createElement('b');
    letterText.textContent = LETTERS[idx];
    letter.appendChild(letterText);
    container.appendChild(letter);
    main.appendChild(container);
    var animation = container.animate([
      {transform: 'translate3d('+x+',-2.5vh,0)'},
      {transform: 'translate3d('+x+',82.5vh,0)'}
    ], {
      duration: DURATION,
      easing: 'linear',
      fill: 'both'
    });
    
    animations[LETTERS[idx]].splice(0, 0, {animation: animation, element: container});
    rate = rate + RATE_INTERVAL;
    animation.playbackRate = rate;
    
    //If an animation finishes, we will consider that as a miss, so we will remove it from the active animations array and increment our miss count
    animation.onfinish = function(e) {
      var target = container;
      var char = target.textContent;
                                      
      animations[char].pop();
      target.classList.add('missed');
      handleMisses();
    }
  }
  
  //When a miss is registered, check if we have reached the max number of misses
  function handleMisses() {
    misses++;
    var missedMarker = document.querySelector('.misses:not(.missed)');
    if (missedMarker) {
      missedMarker.classList.add('missed');
    } else {
      gameOver();
    }
  }
  
  //End game and show screen
  function gameOver() {
    gameOn = false;
    clearInterval(cleanupInterval);
    getAllAnimations().forEach(function(anim) {
      anim.pause();
    });

    //Could use Web Animations API here, but showing how you can use a mix of Web Animations API and CSS transistions
    document.getElementById('game-over').classList.add('indeed');
  }

  //Periodically remove missed elements, and lower the interval between falling elements
  var cleanupInterval = setInterval(function() {
    timeOffset = timeOffset * 4 / 5;
    cleanup();
  }, 20000);
  function cleanup() {
    [].slice.call(main.querySelectorAll('.missed')).forEach(function(missed) {
      main.removeChild(missed);
    });
  }
  
  //Firefox 48 supports document.getAnimations as per latest spec, Chrome 52 and polyfill use older spec
  function getAllAnimations() {
    if (document.getAnimations) {
      return document.getAnimations();
    } else if (document.timeline && document.timeline.getAnimations) {
      return document.timeline.getAnimations();
    }
    return [];
  }
  
  //On key press, see if it matches an active animating (falling) letter. If so, pop it from active array, pause it (to keep it from triggering "finish" logic), and add an animation on inner element with random 3d rotations that look like the letter is being kicked away to the distance. Also update score.
  function onPress(e) {
    var char = e.key;
    if (char.length === 1) {
      char = char.toLowerCase();
      if (animations[char] && animations[char].length) {
        var popped = animations[char].pop();
        popped.animation.pause();
        var target = popped.element.querySelector('b');
        var degs = [(Math.random() * 1000)-500,(Math.random() * 1000)-500,(Math.random() * 2000)-1000];
        target.animate([
          {transform: 'scale(1) rotateX(0deg) rotateY(0deg) rotateZ(0deg)',opacity:1},
          {transform: 'scale(0) rotateX('+degs[0]+'deg) rotateY('+degs[1]+'deg) rotateZ('+degs[2]+'deg)', opacity: 0}
        ], {
          duration: Math.random() * 500 + 850,
          easing: 'ease-out',
          fill: 'both'
        });
        addScore();
        header.textContent += char;
      }
    }
  }
  function addScore() {
    score++;
    scoreElement.textContent = score;
  }
  
  document.body.addEventListener('keypress', onPress);

  //start the letters falling... create the element+animation, and setup timeout for next letter to start
  function setupNextLetter() {
    if (gameOn) {
      create();
      setTimeout(function() {
        setupNextLetter();
      }, timeOffset);
    }
  }
  setupNextLetter();
}

function polyfillKey() {
  if (!('KeyboardEvent' in window) ||
        'key' in KeyboardEvent.prototype) {
    return false;
  }
  
  console.log('polyfilling KeyboardEvent.prototype.key')
  var keys = {};
  var letter = '';
  for (var i = 65; i < 91; ++i) {
    letter = String.fromCharCode(i);
    keys[i] = letter.toUpperCase();
  }
  for (var i = 97; i < 123; ++i) {
    letter = String.fromCharCode(i);
    keys[i] = letter.toLowerCase();
  }
  var proto = {
    get: function (x) {
      var key = keys[this.which || this.keyCode];
      console.log(key);
      return key;
    }
  };
  Object.defineProperty(KeyboardEvent.prototype, 'key', proto);
}
$light: #eee;
$dark: #252627;
$bar: #aa6657;

body {
  height: 100vh;
  background: $dark;
  overflow: hidden;
  display: flex;
  align-items: stretch;
  flex-direction: column;
  color: $light;
  font-family: -apple-system, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
  font-family: monospace;
  font-size: 5vh;
  position: relative;
}
main {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  
  p {
    font-size: 1rem;
    text-align: center;
    margin-top: 5vh;
    padding: 0 2rem;
    max-width: 30rem;
    line-height: 1.4;
  }
}
header,
footer {
  height: 5vh;
  line-height: 5vh;
  font-size: 3vh;
  background: $bar;
  text-align: right;
  text-transform: uppercase;
  padding: 0 2.5vh;
}
footer {
  display: flex;
  justify-content: space-between;
  ul {
    display: flex;
    flex-direction: row-reverse;
    
    .misses {
      padding: .5vh;
      transition: all .225s ease-out;
      &.missed {
        opacity: .4;
        transform: rotate(-45deg);
      }
    }
  }
}

main > div {
  position: absolute;
  top: 5vh;
  left: 0;
  text-transform: uppercase;
  perspective: 300px;
  transition: opacity .7s ease-in;
  font-size: 5vh;
  
  &.popped {
    opacity: 0;
    > span {
      b {
        opacity: 0;
      }
      animation-play-state: paused;
    }
  }
  &.missed {
    opacity: 0;
    > span {
      //  transform: scaleY(0);
      animation-play-state: paused;
    }
  }
  
  > span {
    position: absolute;
    display: block;
    animation: waver 2s infinite alternate ease-in-out;
    perspective: 300px;
    b {
      display: block;
      padding: 2.5vh;
      transition: opacity .25s linear;
    }
  }
}

@keyframes waver {
  100% {
    transform: translate3d(6vw, 0, 0);
  }
}


#game-over {
  opacity: 0;
  pointer-events: none;
  transition: opacity .75s ease-out;
  background: rgba(0,0,0,.75);
  
  position: absolute;
  top: 5vh;
  right: 0;
  bottom: 5vh;
  left: 0;
  width: 100%;
  
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  
  text-transform: uppercase;
  
  &.indeed {
    opacity: 1;
    pointer-events: auto;
  }
}
button {
  appearance: none;
  border-radius: 0;
  border: .3rem solid $light;
  color: $light;
  font-size: 3vh;
  padding: 1.5vh 2vh;
  background: transparent;
  margin-top: 5vh;
  font-family: monospace;
  
  &:hover {
    border-color: $bar;
  }
}
<header></header>
<main id="main"></main>
<footer>
  <ul>
    <li class="misses">+</li>
    <li class="misses">+</li>
    <li class="misses">+</li>
    <li class="misses">+</li>
    <li class="misses">+</li>
    <li class="misses">+</li>
  </ul>
  <div id="score">0</div>
</footer>
<div id="game-over">
  <p>Game Over</p>
</div>

Please ignore this (lorem ipsum text,Your question couldn't be submitted. Please see the error above.Your question couldn't be submitted. Please see the error above.Your question couldn't be submitted. Please see the error above.Your question couldn't be submitted. Please see the error above.Your question couldn't be submitted. Please see the error above.Your question couldn't be submitted. Please see the error above.Your question couldn't be submitted. Please see the error above.Your question couldn't be submitted. Please see the error above.)

Mr T
  • 75
  • 10
  • Unfortunately there’s no way to show a keyboard in Cordova without an input field in focus. `If I add an input field, the script doesn’t work...`, did you check the console for any error? Which part of the script is not working and what do you mean by “not working”? Not executing at all or not returning the expected value? – maswerdna Jul 07 '20 at 10:03
  • so how to edit the script correctly? – Mr T Jul 07 '20 at 10:06
  • No, no error, but you have to edit the programing, catch what is in the input field and push it to the "game", I am not a javascript expert – Mr T Jul 07 '20 at 10:13
  • Oh well, even with that, the problem with your code is that Android webview does not send key codes (it’s always 229 . The better way would have been to use an input field. You can make it 0 width to hide it from the UI and listen to its keyup, get the letter with `input.value` and set the value to empty immediately to prepare for the next input... – maswerdna Jul 07 '20 at 10:19
  • can you demonstrate please – Mr T Jul 07 '20 at 10:21
  • Another note is that, if you’re working with Cordova, you don’t need to care for Firefox or IE. Both iOS and Android use WebKit based browsers. – maswerdna Jul 07 '20 at 10:21
  • i am just trying to adapt an exiting codepen to my app [link] (https://codepen.io/danwilson/pen/wWZWKW), so we can talk about the browser later – Mr T Jul 07 '20 at 10:26
  • Demonstrate... `document.querySelector('input').addEventListener('keyup', onPress);` Inside the `onPress` function, `var char = document.querySelector('input').value.toLowerCase();` – maswerdna Jul 07 '20 at 10:28
  • i meant in the code snippet, so we can debug it – Mr T Jul 07 '20 at 10:28
  • can we use this? [link] (https://stackoverflow.com/a/46673783/8571964) – Mr T Jul 07 '20 at 11:38
  • Your problem is not as complex as the one in the referenced link. The case is if you’re ready to use the input field. – maswerdna Jul 07 '20 at 12:17
  • i added this still the script don't function `code` `
    `
    – Mr T Jul 07 '20 at 12:39
  • Check the edit to my answer for the solution. Don't forget to mark it as accepted if it works for you. I have also added some comments for clarification. – maswerdna Jul 08 '20 at 23:42

1 Answers1

1

loadGame();
polyfillKey();


function loadGame() {
  var button = document.createElement('button');
  button.textContent = 'Start Game';
  var main = document.getElementById('main');
  main.appendChild(button);
  var rules = document.createElement('p');
  rules.textContent = 'Letters will fall... if you have a keyboard, press the correct key to knock it away before it hits the ground';
  main.appendChild(rules);
  button.addEventListener('click', function startIt(e) {
    main.textContent = '';
    playGame();
  });
}

function getfocus() {
  document.getElementById("myAnchor").focus();
}

function losefocus() {
  document.getElementById("myAnchor").blur();
}

function playGame(replay) {
  var LETTERS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
  var animations = {
    'a': [],
    'b': [],
    'c': [],
    'd': [],
    'e': [],
    'f': [],
    'g': [],
    'h': [],
    'i': [],
    'j': [],
    'k': [],
    'l': [],
    'm': [],
    'n': [],
    'o': [],
    'p': [],
    'q': [],
    'r': [],
    's': [],
    't': [],
    'u': [],
    'v': [],
    'w': [],
    'x': [],
    'y': [],
    'z': []
  };
  var gameOn = true;
  var timeOffset = 2000; //interval between letters starting, will be faster over time
  var DURATION = 10000;
  var main = document.getElementById('main');
  var header = document.querySelector('header');
  var scoreElement = document.getElementById('score');
  var score = parseFloat(scoreElement.textContent);
  var rate = 1;
  var RATE_INTERVAL = .05; //playbackRate will increase by .05 for each letter... so after 20 letters, the rate of falling will be 2x what it was at the start
  var misses = 0;

  //Create a letter element and setup its falling animation, add the animation to the active animation array, and setup an onfinish handler that will represent a miss. 
  function create() {
    var idx = Math.floor(Math.random() * LETTERS.length);
    var x = (Math.random() * 85) + 'vw';
    var container = document.createElement('div');
    var letter = document.createElement('span');
    var letterText = document.createElement('b');
    letterText.textContent = LETTERS[idx];
    letter.appendChild(letterText);
    container.appendChild(letter);
    main.appendChild(container);
    var animation = container.animate([{
        transform: 'translate3d(' + x + ',-2.5vh,0)'
      },
      {
        transform: 'translate3d(' + x + ',82.5vh,0)'
      }
    ], {
      duration: DURATION,
      easing: 'linear',
      fill: 'both'
    });

    animations[LETTERS[idx]].splice(0, 0, {
      animation: animation,
      element: container
    });
    rate = rate + RATE_INTERVAL;
    animation.playbackRate = rate;

    //If an animation finishes, we will consider that as a miss, so we will remove it from the active animations array and increment our miss count
    animation.onfinish = function(e) {
      var target = container;
      var char = target.textContent;

      animations[char].pop();
      target.classList.add('missed');
      handleMisses();
    }
  }

  //When a miss is registered, check if we have reached the max number of misses
  function handleMisses() {
    misses++;
    var missedMarker = document.querySelector('.misses:not(.missed)');
    if (missedMarker) {
      missedMarker.classList.add('missed');
    } else {
      gameOver();
    }
  }

  //End game and show screen
  function gameOver() {
    gameOn = false;
    clearInterval(cleanupInterval);
    getAllAnimations().forEach(function(anim) {
      anim.pause();
    });

    //Could use Web Animations API here, but showing how you can use a mix of Web Animations API and CSS transistions
    document.getElementById('game-over').classList.add('indeed');
  }

  //Periodically remove missed elements, and lower the interval between falling elements
  var cleanupInterval = setInterval(function() {
    timeOffset = timeOffset * 4 / 5;
    cleanup();
  }, 20000);

  function cleanup() {
    [].slice.call(main.querySelectorAll('.missed')).forEach(function(missed) {
      main.removeChild(missed);
    });
  }

  //Firefox 48 supports document.getAnimations as per latest spec, Chrome 52 and polyfill use older spec
  function getAllAnimations() {
    if (document.getAnimations) {
      return document.getAnimations();
    } else if (document.timeline && document.timeline.getAnimations) {
      return document.timeline.getAnimations();
    }
    return [];
  }

  //On key press, see if it matches an active animating (falling) letter. If so, pop it from active array, pause it (to keep it from triggering "finish" logic), and add an animation on inner element with random 3d rotations that look like the letter is being kicked away to the distance. Also update score.
  function onPress(e) {
    // var char = e.key;
    var char = this.value[this.value.length - 1];
    this.value = '';
    document.getElementById('myBox').innerText = char;

    if (char.length === 1) {
      char = char.toLowerCase();
      if (animations[char] && animations[char].length) {
        var popped = animations[char].pop();
        popped.animation.pause();
        var target = popped.element.querySelector('b');
        var degs = [(Math.random() * 1000) - 500, (Math.random() * 1000) - 500, (Math.random() * 2000) - 1000];
        target.animate([{
            transform: 'scale(1) rotateX(0deg) rotateY(0deg) rotateZ(0deg)',
            opacity: 1
          },
          {
            transform: 'scale(0) rotateX(' + degs[0] + 'deg) rotateY(' + degs[1] + 'deg) rotateZ(' + degs[2] + 'deg)',
            opacity: 0
          }
        ], {
          duration: Math.random() * 500 + 850,
          easing: 'ease-out',
          fill: 'both'
        });
        addScore();
        header.textContent += char;
      }
    }
  }

  function addScore() {
    score++;
    scoreElement.textContent = score;
  }

  // document.body.addEventListener('keypress', onPress);
  document.getElementById('myAnchor').addEventListener('keyup', onPress);

  //start the letters falling... create the element+animation, and setup timeout for next letter to start
  function setupNextLetter() {
    if (gameOn) {
      create();
      setTimeout(function() {
        setupNextLetter();
      }, timeOffset);
    }
  }
  setupNextLetter();
}

function polyfillKey() {
  if (!('KeyboardEvent' in window) ||
    'key' in KeyboardEvent.prototype) {
    return false;
  }

  console.log('polyfilling KeyboardEvent.prototype.key')
  var keys = {};
  var letter = '';
  for (var i = 65; i < 91; ++i) {
    letter = String.fromCharCode(i);
    keys[i] = letter.toUpperCase();
  }
  for (var i = 97; i < 123; ++i) {
    letter = String.fromCharCode(i);
    keys[i] = letter.toLowerCase();
  }
  var proto = {
    get: function(x) {
      var key = keys[this.which || this.keyCode];
      console.log(key);
      return key;
    }
  };
  Object.defineProperty(KeyboardEvent.prototype, 'key', proto);
}
$light: #eee;
$dark: #252627;
$bar: #aa6657;
body {
  height: 100vh;
  background: $dark;
  overflow: hidden;
  display: flex;
  align-items: stretch;
  flex-direction: column;
  color: $light;
  font-family: -apple-system, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
  font-family: monospace;
  font-size: 5vh;
  position: relative;
}

main {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  p {
    font-size: 1rem;
    text-align: center;
    margin-top: 5vh;
    padding: 0 2rem;
    max-width: 30rem;
    line-height: 1.4;
  }
}

header,
footer {
  height: 5vh;
  line-height: 5vh;
  font-size: 3vh;
  background: $bar;
  text-align: right;
  text-transform: uppercase;
  padding: 0 2.5vh;
}

footer {
  display: flex;
  justify-content: space-between;
  ul {
    display: flex;
    flex-direction: row-reverse;
    .misses {
      padding: .5vh;
      transition: all .225s ease-out;
      &.missed {
        opacity: .4;
        transform: rotate(-45deg);
      }
    }
  }
}

main>div {
  position: absolute;
  top: 5vh;
  left: 0;
  text-transform: uppercase;
  perspective: 300px;
  transition: opacity .7s ease-in;
  font-size: 5vh;
  &.popped {
    opacity: 0;
    >span {
      b {
        opacity: 0;
      }
      animation-play-state: paused;
    }
  }
  &.missed {
    opacity: 0;
    >span {
      //  transform: scaleY(0);
      animation-play-state: paused;
    }
  }
  >span {
    position: absolute;
    display: block;
    animation: waver 2s infinite alternate ease-in-out;
    perspective: 300px;
    b {
      display: block;
      padding: 2.5vh;
      transition: opacity .25s linear;
    }
  }
}

@keyframes waver {
  100% {
    transform: translate3d(6vw, 0, 0);
  }
}

#game-over {
  opacity: 0;
  pointer-events: none;
  transition: opacity .75s ease-out;
  background: rgba(0, 0, 0, .75);
  position: absolute;
  top: 5vh;
  right: 0;
  bottom: 5vh;
  left: 0;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  text-transform: uppercase;
  &.indeed {
    opacity: 1;
    pointer-events: auto;
  }
}

button {
  appearance: none;
  border-radius: 0;
  border: .3rem solid $light;
  color: $light;
  font-size: 3vh;
  padding: 1.5vh 2vh;
  background: transparent;
  margin-top: 5vh;
  font-family: monospace;
  &:hover {
    border-color: $bar;
  }
}
<header></header>
<div>
  <input id="myAnchor" style="">
  <span id="myBox"></span>
</div>
<input type="button" onclick="getfocus()" value="Get focus">
<input type="button" onclick="losefocus()" value="Lose focus">
<main id="main"></main>
<footer>
  <ul>
    <li class="misses">+</li>
    <li class="misses">+</li>
    <li class="misses">+</li>
    <li class="misses">+</li>
    <li class="misses">+</li>
    <li class="misses">+</li>
  </ul>
  <div id="score">0</div>
</footer>
<div id="game-over">
  <p>Game Over</p>
</div>

There are several ways to solve the problem: You can add this plugin https://github.com/driftyco/ionic-plugin-keyboard and call its method to show the keyboard when you need it. Although it doesn’t work as expected in many cases, but it might be perfect for your own need.

But as the method above relies on the native keyboard which I believe would reduce the real estate available for your game to run, (keyboard takes at least 30% of the screen height), you can create your own virtual keyboard and assign key codes to it. The keys can be arranged vertically on either sides of the screen (thirteen on each side), this way, you have total control of the screen, where to place your keyboard, when to hide and when to show.

If you can open source the project, I can help with the implementation as I already have a virtual keyboard plugin for entering verification tokens and OTP on several apps.

EDIT

The following changes were made:

    //The styles were removed to be able to visualize what was going on behind
    <div>
        <input id="myAnchor" style="" >
        <span id="myBox"></span> <!-- box added to show the captured value -->
    </div>
    <input type="button" onclick="getfocus()" value="Get focus">
    <input type="button" onclick="losefocus()" value="Lose focus">

    function onPress(e) {
        // remove the next line
        // var char = e.key;
        // replace the above line with this
        var char = this.value[this.value.length - 1];
        // we have to apply this hack in order to get the last key pressed
        // Android would keep the initial values
        // until the space key or the return key is pressed
        // So, even if we clear the value programmatically, as we did below,
        // the OS would still keep it alive
        this.value = '';//this line could be omitted, but the input would be overloaded
        document.getElementById('myBox').innerText = char;

        if (char.length === 1) {
          char = char.toLowerCase();
          if (animations[char] && animations[char].length) {
            var popped = animations[char].pop();
            popped.animation.pause();
            var target = popped.element.querySelector('b');
            var degs = [(Math.random() * 1000)-500,(Math.random() * 1000)-500,(Math.random() * 2000)-1000];
            target.animate([
              {transform: 'scale(1) rotateX(0deg) rotateY(0deg) rotateZ(0deg)',opacity:1},
              {transform: 'scale(0) rotateX('+degs[0]+'deg) rotateY('+degs[1]+'deg) rotateZ('+degs[2]+'deg)', opacity: 0}
            ], {
              duration: Math.random() * 500 + 850,
              easing: 'ease-out',
              fill: 'both'
            });
            addScore();
            header.textContent += char;
          }
        }
      }

    // remove the line below    
    // document.body.addEventListener('keypress', onPress);
    // replace the above line with this one
    document.getElementById('myAnchor').addEventListener('keyup', onPress);
maswerdna
  • 315
  • 2
  • 10