Transforms are added...endlessly
Asked Answered
L

3

3

I'm creating a simple asteroids-like game in CSS and JS using the DOM over canvas for...experimentation purposes.

My code is pretty small in this example to make it easy to see what's going on below. The ultimate goal: Let arrow keys smoothly rotate and translate the spaceship around the window without creating an infinite amount of transforms. I think I'm 90% there:

Use the arrow keys to control the snippet below.

'use strict';

function defineDistances() {
  var distance = {};

  distance.up = -1;
  distance.right = 1;
  distance.down = 1;
  distance.left = -1;
      
  return distance;
}

function defineKeys() {
  var keys = {};

  keys.up = 38;
  keys.right = 39;
  keys.down = 40;
  keys.left = 37;
      
  return keys;
}

function checkKeys( e ) {
  var triBx = document.getElementById( 'v-wrp' ),
      keys = defineKeys(),
      distance = defineDistances();

  switch( e.keyCode ) {
    case keys.up:
      triBx.style.transform += 'translateY(' + distance.up + 'px)';
      break;
    case keys.right:
      triBx.style.transform += 'rotate(' + distance.right + 'deg)';
      break;
    case keys.down:
      triBx.style.transform += 'translateY(' + distance.down + 'px)';
      break;
    case keys.left:
      triBx.style.transform += 'rotate(' + distance.left + 'deg)';
      break;
  }
}

function detectMovement( e ) {
  setInterval ( 
    function() {
      checkKeys( e );
    },
    1000/24
  );  
}

function start() {
  window.addEventListener( 'keydown', detectMovement );
  preventBrowserWindowScroll()
}

start();
@import url( "https://fonts.googleapis.com/css?family=Nunito" );

html {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  font-family: "Nunito", sans-serif;
  font-size: 2rem;
}

.v {
  display: block;
  transform: rotate( 180deg );
}
<div id="v-wrp" class="v-wrp">
  <b class="v">V</b>
</div>

<script>
function preventBrowserWindowScroll() {
  window.addEventListener( 'keydown', function( e ) {
    // space and arrow keys
    if([32, 37, 38, 39, 40].indexOf( e.keyCode ) > -1 ) {
      e.preventDefault();
    }
  }, false )
}
</script>

If you inspect the v-wrp element in the browser you can see the transforms get added endlessly.

enter image description here

The reason I use += to add transforms is to avoid this problem: Reset CSS transform origin after translation / rotation

( The transform-origin doesn't move with the element as it moves, causing undesired effects unless all transforms are added in addition to the previous ones... )

So how do I overcome these challenges? I suspect the snippet is so choppy because of the endless transforms being added. How do I get this working similarly to the way it is now without all the memory loss/ choppiness/ bugginess / endless transforms?

Edit: Another major problem is how the ship will travel in the same directions continuously once the keys are pressed, even going in a circular like pattern if you hit the correct keys. I want it to drift like in space but not turn once the keys are let go. The trajectory should stay straight as it "floats" What am I doing wrong?.

Leonoraleonore answered 1/5, 2017 at 17:40 Comment(1)
I have posted a quick answer. Sorry, I don't have much time now, and I can't fully finish it. Hopefully you get the idea. – Pigmy
O
1

For more info and a demo of the answer below see https://mcmap.net/q/831343/-how-do-i-give-this-spaceship-acceleration


Using CSS transform : matrix function

If given a object position, scale and rotation the quickest way to set the transform is to do it as a single matrix element.style.transform = "matrix(a,b,c,d,e,f)";

The 6 values represent the direction and scale of the X axis (a,b), Y axis (c,d) , and the local origin (e,f)

As most of the time you don't want to skew and the scale is uniform (x and y scale the same) the function to create and set the transform is quick. All you do is pass the position, scale and rotation.

const setElementTransform = (function(){
    const matrix = [1,0,0,1,0,0]; // predefine the array (helps ease the GC load
    const m = matrix; // alias for code readability.
    return function(element, x, y, scale, rotation);
        m[3] = m[0] = Math.cos(rotation) * scale;     // set rotation and scale
        m[2] = -(m[1] = Math.sin(rotation) * scale);  // set rotation and scale
        m[4] = x;
        m[5] = y;
        element.style.transform = `matrix(${m.join(",")})`;
    }
}());

Don't use keyboardEvent.keyCode it has depreciated.

Rather than use the old (and obscure key values) keyCode property to read the keys you should use the code property that has a string representing which key is down or up.

const keys = {
    ArrowLeft : false,  // add only the named keys you want to listen to.
    ArrowRight: false,  
    ArrowUp   : false,  
    ArrowDown : false,  
    stopKeyListener : (function(){  // adds a listener and returns function to stop key listener if needed.
        function keyEvent(e){
            if(keys[e.code] !== undefined){ // is the key on the named list
                keys[e.code] = e.type === "keydown"; // set true if down else false
                e.preventDefault(); // prevent the default Browser action for this key.
        }
        addEventListener("keydown",keyEvent);
        addEventListener("keyup",keyEvent);
        return function(){
            removeEventListener("keydown",keyEvent);
            removeEventListener("keyup",keyEvent);
        }
    }()) //
}

Now at any time you can just check if a key is down with if(keys.ArrowLeft){


Updating the DOM regularly? use requestAnimationFrame

If you are making many changes to the DOM at regular intervals you should use requestAnimationFrame and it tells the browser your intention and will cause all DOM changes made from within the callback to sync with the display hardware and the DOM's own compositing and rendering.

requestAnimationFrame(mainLoop);  // will start the animation once code below has been parse and executed.
var player = {  // the player
    x : 0,
    y : 0,
    scale : 1,
    rotate : 0,
    speed : 0,
    element : document.getElementById("thePlayer")
}
function mainLoop(time){ // requestAnimationFrame adds the time as the first argument for the callback
     if(keys.ArrowLeft){ player.rotate -= 1 }
     if(keys.ArrowRight){ player.rotate += 1 }
     if(keys.ArrowUp){ player.speed  += 1 }
     if(keys.ArrowRight){ player.speed -= 1 }
     player.x += Math.cos(player.rotate) * player.speed;
     player.y += Math.sin(player.rotate) * player.speed;
     setElementTransform(
         player.element,
         player.x, player.y,
         player.scale,
         player.rotate
     );
     requestAnimationFrame(mainLoop);
}
      

For a demo https://mcmap.net/q/831343/-how-do-i-give-this-spaceship-acceleration (same link as at top of answer)

Olympe answered 5/5, 2017 at 2:19 Comment(3)
Hey, I think this might be inaccurate: direction and scale of the X axis (a,b), Y axis (c,d). MDN says: The values represent the following functions: matrix(scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY()) (notice how skew is in between and not respective) so it should be more like the X axis (a,c), Y axis (d,b), or? πŸ™ƒ – Isochronal
@Isochronal The answer is correct. (a, b) is the vector representation {x: a, y: b} of the X Axis (top edge of pixel), and (c, d) is the vector representation {x: c, y: d} of the Y Axis (left edge of pixel). One can deduce the X, and Y axis scales with, Math.hypot(a, b) is X axis scale, and Math.hypot(c, d) is the Y axis scale. – Olympe
Aha, I see.. I suppose I misrepresented the meaning from MDN. Thanks for clarifying! – Isochronal
P
2

I have done a rapid answer - probably there are some aspect to smooth, but you'll get the idea: (ES6 code)

'use strict'

class Ship {
    constructor (elem) {
        this.posX = 0;
        this.posY = 0;
        this.deg = 0;
        this.rad = 0;
        this.speed = 0;
    }

    update (event) {
      switch( event.key ) {
        case "ArrowUp":
          this.speed += 5;
          break;
        case "ArrowDown":
          this.speed -= 5;
          if (this.speed < 0) this.speed = 0;
          break;
        case "ArrowRight":
          this.deg += 3;
          break;
        case "ArrowLeft":
          this.deg -= 3;
          break;
      }
          this.rad = (this.deg + 90) * Math.PI / 180;
    }
    move () {
      this.posX += this.speed * Math.cos(this.rad);
      this.posY += this.speed * Math.sin(this.rad);
      if (this.speed > 0) {
          this.speed -= 0.1;
      }
      if (this.elem == undefined) {
        this.elem = document.getElementById('ship');
      }
      var translation = 'translate(' + this.posX +'px, ' + this.posY + 'px) ';
      var rotation = 'rotate(' + this.deg + 'deg)';
      this.elem.style.transform = translation + rotation;
                     
    }
}


var ship = new Ship

function update( e ) {
  ship.update(e);
  return false;
}

function start() {
  window.addEventListener( 'keydown', update );
  setInterval ( 
    function() {
      ship.move();
    },
    1000 / 24
  );  
}

start();
#ship {
  position: absolute;
  left: 50%;
  top: 50%;
}
<div id="ship">V</div>
Pigmy answered 1/5, 2017 at 20:36 Comment(3)
You have a completely different approach. Interesting. Thank you for this and i'll try to use the parts of this to hack out what I want. I like how you have a speed variable, However, I really need to work on allowing multiple keys to be pressed at once, as well as smoothing like you said. Thank you for your input! – Leonoraleonore
Check this out! jsfiddle.net/whptpmab. Under 50 lines of JS code and working smoothly on multiple keypresses! I just need to solve the acceleration problem and give the ship some momentum like how it is in actual space...man so close. Oh, and transforms are still added endlessly lol. Doesn't seem to impact performance yet tho on that ex. – Leonoraleonore
@solacyon Good that you handle updates in a requestAnimation... and user input in a separate event. But I don't think you can continue adding transforms for a really long time ... And to get momentum you will need a speed variable sooner or later, I think. – Pigmy
V
1

I think the problem is the detectMovement is calling the checkKeys again and again in infinite loop with same event e.

I tried adding listeners for keyup, keydown, keyleft and keyright so that checkkeys is called only when these keys are pressed.

Please comment if I have understood wrongly

'use strict';

function defineDistances() {
  var distance = {};

  distance.up = -1;
  distance.right = 1;
  distance.down = 1;
  distance.left = -1;
      
  return distance;
}

function defineKeys() {
  var keys = {};

  keys.up = 38;
  keys.right = 39;
  keys.down = 40;
  keys.left = 37;
      
  return keys;
}

function checkKeys( e ) {
e.preventDefault();
  var triBx = document.getElementById( 'v-wrp' ),
      keys = defineKeys(),
      distance = defineDistances();

  switch( e.keyCode ) {
    case keys.up:
      triBx.style.transform += 'translateY(' + distance.up + 'px)';
      break;
    case keys.right:
      triBx.style.transform += 'rotate(' + distance.right + 'deg)';
      break;
    case keys.down:
      triBx.style.transform += 'translateY(' + distance.down + 'px)';
      break;
    case keys.left:
      triBx.style.transform += 'rotate(' + distance.left + 'deg)';
      break;
  }
}

function detectMovement( e ) {
  setInterval ( 
    function() {
      checkKeys( e );
    },
    1000/24
  );  
}

function start() {
  window.addEventListener( 'keydown', checkKeys );
  window.addEventListener( 'keyup', checkKeys );
  window.addEventListener( 'keyright', checkKeys );
  window.addEventListener( 'keyleft', checkKeys );
}

start();
@import url( "https://fonts.googleapis.com/css?family=Nunito" );

html {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  font-family: "Nunito", sans-serif;
  font-size: 2rem;
}

.v {
  display: block;
  transform: rotate( 180deg );
}
<div id="v-wrp" class="v-wrp">
  <b class="v">V</b>
</div>

<script>
function preventBrowserWindowScroll() {
  window.addEventListener( 'keydown', function( e ) {
    // space and arrow keys
    if([32, 37, 38, 39, 40].indexOf( e.keyCode ) > -1 ) {
      e.preventDefault();
    }
  }, false )
}
</script>
Velamen answered 1/5, 2017 at 18:50 Comment(8)
Hmm. Sort of works but I wonder why the up and down arrows don't do anything now. – Leonoraleonore
I have added e.preventDefault. Please check now – Velamen
You almost got it! But the reason I had that function in a loop in the first place was to allow multiple button presses at once. Like up and left held down simultaneously would both affect the ship. Now with your code they do not. You did fix the first problem tho but caused another that led me to using the loop in the first place...lol. – Leonoraleonore
Actually on second inspection it doesn't seem like my original code is registering 2 key presses at once either....That might be my real problem. How to simultaneously have all the arrow keys affect this element. – Leonoraleonore
setInterval will call the checkKeys continously every (1000/24 ) milliseconds. So if I press up, it will call up continously. If I press down, it will call both up and down continously. Is this required functionality – Velamen
No. I'm just trying to register multiple key presses at once. – Leonoraleonore
Can we use onkeydown and onkeyup event and detect the pressed or released state of arrow keys? #3397254 – Velamen
Let us continue this discussion in chat. – Velamen
O
1

For more info and a demo of the answer below see https://mcmap.net/q/831343/-how-do-i-give-this-spaceship-acceleration


Using CSS transform : matrix function

If given a object position, scale and rotation the quickest way to set the transform is to do it as a single matrix element.style.transform = "matrix(a,b,c,d,e,f)";

The 6 values represent the direction and scale of the X axis (a,b), Y axis (c,d) , and the local origin (e,f)

As most of the time you don't want to skew and the scale is uniform (x and y scale the same) the function to create and set the transform is quick. All you do is pass the position, scale and rotation.

const setElementTransform = (function(){
    const matrix = [1,0,0,1,0,0]; // predefine the array (helps ease the GC load
    const m = matrix; // alias for code readability.
    return function(element, x, y, scale, rotation);
        m[3] = m[0] = Math.cos(rotation) * scale;     // set rotation and scale
        m[2] = -(m[1] = Math.sin(rotation) * scale);  // set rotation and scale
        m[4] = x;
        m[5] = y;
        element.style.transform = `matrix(${m.join(",")})`;
    }
}());

Don't use keyboardEvent.keyCode it has depreciated.

Rather than use the old (and obscure key values) keyCode property to read the keys you should use the code property that has a string representing which key is down or up.

const keys = {
    ArrowLeft : false,  // add only the named keys you want to listen to.
    ArrowRight: false,  
    ArrowUp   : false,  
    ArrowDown : false,  
    stopKeyListener : (function(){  // adds a listener and returns function to stop key listener if needed.
        function keyEvent(e){
            if(keys[e.code] !== undefined){ // is the key on the named list
                keys[e.code] = e.type === "keydown"; // set true if down else false
                e.preventDefault(); // prevent the default Browser action for this key.
        }
        addEventListener("keydown",keyEvent);
        addEventListener("keyup",keyEvent);
        return function(){
            removeEventListener("keydown",keyEvent);
            removeEventListener("keyup",keyEvent);
        }
    }()) //
}

Now at any time you can just check if a key is down with if(keys.ArrowLeft){


Updating the DOM regularly? use requestAnimationFrame

If you are making many changes to the DOM at regular intervals you should use requestAnimationFrame and it tells the browser your intention and will cause all DOM changes made from within the callback to sync with the display hardware and the DOM's own compositing and rendering.

requestAnimationFrame(mainLoop);  // will start the animation once code below has been parse and executed.
var player = {  // the player
    x : 0,
    y : 0,
    scale : 1,
    rotate : 0,
    speed : 0,
    element : document.getElementById("thePlayer")
}
function mainLoop(time){ // requestAnimationFrame adds the time as the first argument for the callback
     if(keys.ArrowLeft){ player.rotate -= 1 }
     if(keys.ArrowRight){ player.rotate += 1 }
     if(keys.ArrowUp){ player.speed  += 1 }
     if(keys.ArrowRight){ player.speed -= 1 }
     player.x += Math.cos(player.rotate) * player.speed;
     player.y += Math.sin(player.rotate) * player.speed;
     setElementTransform(
         player.element,
         player.x, player.y,
         player.scale,
         player.rotate
     );
     requestAnimationFrame(mainLoop);
}
      

For a demo https://mcmap.net/q/831343/-how-do-i-give-this-spaceship-acceleration (same link as at top of answer)

Olympe answered 5/5, 2017 at 2:19 Comment(3)
Hey, I think this might be inaccurate: direction and scale of the X axis (a,b), Y axis (c,d). MDN says: The values represent the following functions: matrix(scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY()) (notice how skew is in between and not respective) so it should be more like the X axis (a,c), Y axis (d,b), or? πŸ™ƒ – Isochronal
@Isochronal The answer is correct. (a, b) is the vector representation {x: a, y: b} of the X Axis (top edge of pixel), and (c, d) is the vector representation {x: c, y: d} of the Y Axis (left edge of pixel). One can deduce the X, and Y axis scales with, Math.hypot(a, b) is X axis scale, and Math.hypot(c, d) is the Y axis scale. – Olympe
Aha, I see.. I suppose I misrepresented the meaning from MDN. Thanks for clarifying! – Isochronal

© 2022 - 2024 β€” McMap. All rights reserved.