OverripeBanana
OverripeBanana

Reputation: 31

How can I make an image move with arrow keys with javascript?

I'm pretty new to javascript and have been trying to figure out how to make an image move. I've come up with this code and it kind of works but the image stops moving after a little bit. How do I fix this?

document.onkeydown = function(event) {
  switch (event.keyCode) {
    case 37:
      moveLeft();
      break;
    case 38:
      moveUp();
      break;
    case 39:
      moveRight();
      break;
    case 40:
      moveDown();
      break;
  }
};

function moveLeft() {
  document.getElementById("img").style.left += "5px";
}

function moveRight() {
  document.getElementById("img").style.right += "5px";
}

function moveDown() {
  document.getElementById("img").style.bottom += "5px";

  function moveUp() {
    document.getElementById("img").style.top += "5px";
  }
<body>
  <p>This is filler text</p>

  <img src="https://via.placeholder.com/100" length="100" width="100" id="img" />
</body>

Upvotes: 3

Views: 3007

Answers (3)

Mr. Polywhirl
Mr. Polywhirl

Reputation: 48610

If you approach this with a game loop, you can separate the key events with the update and re-rendering logic.

You can store the pressed keys in a Set to detect them.

The example below initializes a moveable element with a position, velocity, acceleration, and friction (drag) vectors.

const arrowKeys = frozenSet('ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown');

const pressedKeys = new Set();
let activeEntity = null;

class Vector2D {
  constructor({ x = 0, y = 0 } = {}) {
    this.x = x;
    this.y = y;
  }
  
  add({ x, y }) {
    return new Vector2D({
      x: this.x + x,
      y: this.y + y
    });
  }
  
  scale(scalar) {
    return new Vector2D({
      x: this.x * scalar,
      y: this.y * scalar
    });
  }
  
  clamp(min, max) {
    return new Vector2D({
      x: Math.max(min.x, Math.min(this.x, max.x)),
      y: Math.max(min.y, Math.min(this.y, max.y))
    });
  }
}

function computeAcceleration(negKey, posKey, rate) {
  return (posKey - negKey) * rate;
}

// Input Handling
function handleKeydown(e) {
  if (!activeEntity || !arrowKeys.has(e.key)) return;
  e.preventDefault();
  pressedKeys.add(e.key);
}

function handleKeyup(e) {
  if (!activeEntity) return;
  pressedKeys.delete(e.key);
}

function handleBlur() {
  setActiveEntity(null);
}

// Focus Management
function setFocus(element, isFocused) {
  if (!element) return;
  element.classList.toggle('focused', isFocused);
  element.setAttribute('data-focused', isFocused ? 'true' : '');
}

function setActiveEntity(entity) {
  if (activeEntity) setFocus(activeEntity.element, false);
  if (entity === activeEntity) {
    activeEntity = null;
    return;
  }
  activeEntity = entity;
  if (activeEntity) setFocus(activeEntity.element, true);
}

// Entity Updates
function updateEntity(ms, entity) {
  if (!entity) return;

  const min = new Vector2D({ x: 0, y: 0 });
  const max = new Vector2D({
    x: window.innerWidth - entity.element.offsetWidth,
    y: window.innerHeight - entity.element.offsetHeight
  });

  const x = computeAcceleration(
    pressedKeys.has('ArrowLeft'),
    pressedKeys.has('ArrowRight'),
    entity.accelerationRateX
  );
  const y = computeAcceleration(
    pressedKeys.has('ArrowUp'),
    pressedKeys.has('ArrowDown'),
    entity.accelerationRateY
  );
  
  entity.acceleration = new Vector2D({ x, y });
  entity.velocity = entity.velocity.add(entity.acceleration).scale(entity.friction);
  entity.position = entity.position.add(entity.velocity).clamp(min, max);

  entity.element.style.transform = `translate3d(${entity.position.x}px, ${entity.position.y}px, 0)`;
}

// Animation Loop
function animationLoop(ms) {
  if (!activeEntity) return requestAnimationFrame(animationLoop);
  updateEntity(ms, activeEntity);
  requestAnimationFrame(animationLoop);
}

// Initialization
function initializeMovableEntities() {
  document.querySelectorAll('.moveable').forEach((el) => new MovableEntity(el));
  requestAnimationFrame(animationLoop);
}

// Helper Functions
function getElementPosition(element) {
  return (({ left, top }) => ({
    x: left + window.scrollX,
    y: top + window.scrollY
  }))(element.getBoundingClientRect());
}

function getDatasetValues(element, ...props) {
  return props.reduce((config, prop) => {
    const value = element.dataset[prop];
    if (value && (!isNaN(value) || isFinite(value))) {
      config[prop] = Number(value);
    }
    return config;
  }, {});
}

function mergeConfig(element, defaults, overrides) {
  return {
    ...defaults,
    ...getDatasetValues(element, ...Object.keys(defaults)),
    ...overrides
  };
}

class MovableEntity {
  static defaultOptions = {
    accelerationRateX: 0.1,
    accelerationRateY: 0.1,
    friction: 0.9
  };
  
  constructor(element, options = {}) {
    const config = mergeConfig(element, MovableEntity.defaultOptions, options);
    this.element = element;
    this.position = new Vector2D(getElementPosition(element));
    this.velocity = new Vector2D();
    this.acceleration = new Vector2D();
    this.accelerationRateX = config.accelerationRateX;
    this.accelerationRateY = config.accelerationRateY;
    this.friction = config.friction;
    this.element.addEventListener('click', () => setActiveEntity(this));
  }
}

// Frozen Set Helper
function freezeSet(set) {
  return Object.freeze({
    has: set.has.bind(set),
    forEach: set.forEach.bind(set),
    size: set.size,
    values: set.values.bind(set),
    entries: set.entries.bind(set),
    keys: set.keys.bind(set),
    [Symbol.iterator]: set[Symbol.iterator].bind(set),
  });
}

function frozenSet(...items) { return freezeSet(new Set(items)); }

// Unfocus on Outside Click
window.addEventListener('click', (e) => {
  if (!e.target.closest('.moveable')) setActiveEntity(null);
});

// Attach global event listeners
window.addEventListener('keydown', handleKeydown);
window.addEventListener('keyup', handleKeyup);
window.addEventListener('blur', handleBlur);

initializeMovableEntities();
.moveable {
  transition: translate 0.3s;
  outline: thin dashed red;
  border-radius: 50%;
  padding: 0.5rem;
}

.focused {
  border: none;
  outline: 2px solid blue;
}
<p>Select an image, and arrows the arrow keys to move!</p>
<img
  class="moveable"
  data-acceleration-rate-x="0.5"
  data-acceleration-rate-y="0.5"
  src="https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico"
  title="Moveable (Speed: Balanced)" />
<img
  class="moveable"
  data-acceleration-rate-x="1.0"
  data-acceleration-rate-y="0.5"
  src="https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico"
  title="Moveable (Speed: Horizontal)" />
<img
  class="moveable"
  data-acceleration-rate-x="0.5"
  data-acceleration-rate-y="1.0"
  src="https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico"
  title="Moveable (Speed: Vertical)" />

Upvotes: 0

romellem
romellem

Reputation: 6491

Several things that you'll need to change:

  • Inline styles are going to return string values generally, so doing += "5px" will concatenate your strings after the first keypress.
  • position for an element uses top / left / right / bottom as offsets from those respected edges. So for this demo, you'll probably only want to adjust left and right, with negative left values pushing it to the left and positive pushing it to the right. Similar for top.

Because of these two, it'll probably be easier to have some separate Number variables for these offsets, operate on those variables directly, then update your inline style.

Here is an example of that:

let left_offset = 0;
let top_offset = 0;
document.onkeydown = function (event) {
  let img = document.getElementById("img");
  switch (event.keyCode) {
    case 37:
      // Move left
      left_offset -= 5;
      break;
    case 38:
      // Move up
      top_offset -= 5;
      break;
    case 39:
      // Move right
      left_offset += 5;
      break;
    case 40:
      // Move down
      top_offset += 5;
      break;
  }
  img.style.left = `${left_offset}px`;
  img.style.top = `${top_offset}px`;
};
#img {
  position: relative;
}
<p>This is filler text</p>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAD0lEQVR42gEEAPv/AP8mAANNASYcJe1iAAAAAElFTkSuQmCC" length="100" width="100" id="img" />

Upvotes: 1

Roko C. Buljan
Roko C. Buljan

Reputation: 206121

  • Use CSS transform and translate instead of left / top
  • Use KeyboardEvent.key instead of the cryptic KeyboardEvent.keyCode

const elImg = document.querySelector("#img");
const pos = { x: 0, y: 0 };

const move = (x) => elImg.style.translate = `${pos.x}px ${pos.y}px`;

const keyActions = {
  ArrowLeft:  () => move(pos.x -= 40),
  ArrowRight: () => move(pos.x += 40),
};

addEventListener("keydown", (evt) => {
  evt.preventDefault();
  if (!evt.repeat) keyActions[evt.key]?.();
});
#img {
  transition: translate 0.3s;
}
<p>Use arrows left, right</p>
<img id="img" src="https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico" />

Here's another similar answer

Upvotes: 3

Related Questions