Recreating Game Elements for the Web: The Among Us Card Swipe

Avatar of Thomas Park
Thomas Park on

Find and fix web accessibility issues with ease using axe DevTools Pro. Try for free!

As a web developer, I pay close attention to the design of video games. From the HUD in Overwatch to the catch screen in Pokemon Go to hunting in Oregon Trail, games often have interesting mechanics and satisfying interactions, many of which inspire my own coding games at Codepip.

Beyond that, implementing small slices of these game designs on a web stack is a fun, effective way to broaden your skills. By focusing on a specific element, your time is spent working on an interesting part without having to build out a whole game with everything that entails. And even in this limited scope, you often get exposed to new technologies and techniques that push on the boundaries of your dev knowledge.

As a case study for this idea, I’ll walk you through my recreation of the card swipe from Among Us. For anyone in the dark, Among Us is a popular multiplayer game. Aboard a spaceship, crewmates have to deduce who among them is an imposter. All the while, they complete mundane maintenance tasks and avoid being offed by the imposter.

The card swipe is the most infamous of the maintenance tasks. Despite being simple, so many players have struggled with it that it’s become the stuff of streams and memes.

Here’s my demo

This is my rendition of the card swipe task:

Next, I’ll walk you through some of the techniques I used to create this demo.

Swiping with mouse and touch events

After quickly wireframing the major components in code, I had to make the card draggable. In the game, when you start dragging the card, it follows your pointer’s position horizontally, but stays aligned with the card reader vertically. The card has a limit in how far past the reader it can be dragged to its left or right. Lastly, when you lift your mouse or finger, the card returns to its original position.

All of this is accomplished by assigning functions to mouse and touch events. Three functions are all that‘s needed to handle mouse down, mouse move, and mouse up (or touch start, touch move, and touch end if you‘re on a touchscreen device). Here’s the skeleton of that JavaScript code:

const card = document.getElementById('card');
const reader = document.getElementById('reader');
let active = false;
let initialX;

// set event handlers
document.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchstart', dragStart);
document.addEventListener('touchmove', drag);
document.addEventListener('touchend', dragEnd);

function dragStart(e) {
  // continue only if drag started on card
  if (e.target !== card) return;

  // get initial pointer position
  if (e.type === 'touchstart') {
    initialX = e.touches[0].clientX;
  } else {
    initialX = e.clientX;
  }

  active = true;
}

function drag(e) {
  // continue only if drag started on card
  if (!active) return;

  e.preventDefault();
  
  let x;

  // get current pointer position
  if (e.type === 'touchmove') {
    x = e.touches[0].clientX - initialX;
  } else {
    x = e.clientX - initialX;
  }

  // update card position
  setTranslate(x);
}

function dragEnd(e) {
  // continue only if drag started on card
  if (!active) return;

  e.preventDefault();
  
  let x;

  // get final pointer position
  if (e.type === 'touchend') {
    x = e.touches[0].clientX - initialX;
  } else {
    x = e.clientX - initialX;
  }

  active = false;
  
  // reset card position
  setTranslate(0);
}

function setTranslate(x) {
  // don't let card move too far left or right
  if (x < 0) {
    x = 0;
  } else if (x > reader.offsetWidth) {
    x = reader.offsetWidth;
  }

  // set card position on center instead of left edge
  x -= (card.offsetWidth / 2);
  
  card.style.transform = 'translateX(' + x + 'px)';
}

Setting status with performance.now()

Next, I had to determine whether the card swipe was valid or invalid. For it to be valid, you must drag the card across the reader at just the right speed. Didn’t drag it far enough? Invalid. Too fast? Invalid. Too slow? Invalid.

To find if the card has been swiped far enough, I checked the card’s position relative to the right edge of the card reader in the function dragEnd:

let status;

// check if card wasn't swiped all the way
if (x < reader.offsetWidth) {
  status = 'invalid';
}

setStatus(status);

To measure the duration of the card swipe, I set start and end timestamps in dragStart and dragEnd respectively, using performance.now().

function setStatus(status) {

  // status is only set for incomplete swipes so far
  if (typeof status === 'undefined') {

    // timestamps taken at drag start and end using performance.now()
    let duration = timeEnd - timeStart;

    if (duration > 700) {
      status = 'slow';
    } else if (duration < 400) {
      status = 'fast';
    } else {
      status = 'valid';
    }
  }

  // set [data-status] attribute on reader
  reader.dataset.status = status;
}

Based on each condition, a different value is set on the reader’s data-status attribute. CSS is used to display the relevant message and illuminate either a red or green light.

#message:after {
  content: "Please swipe card";
}

[data-status="invalid"] #message:after {
  content: "Bad read. Try again.";
}

[data-status="slow"] #message:after {
  content: "Too slow. Try again.";
}

[data-status="fast"] #message:after {
  content: "Too fast. Try again.";
}

[data-status="valid"] #message:after {
  content: "Accepted. Thank you.";
}

.red {
  background-color: #f52818;
  filter: saturate(0.6) brightness(0.7);
}

.green {
  background-color: #3dd022;
  filter: saturate(0.6) brightness(0.7);
}

[data-status="invalid"] .red,
[data-status="slow"] .red,
[data-status="fast"] .red,
[data-status="valid"] .green {
  filter: none;
}

Final touches with fonts, animations, and audio

With the core functionality complete, I added a few more touches to get the project looking even more like Among Us.

First, I used a free custom font called DSEG to imitate the segmented type from old LCDs. All it took was hosting the files and declaring the font face in CSS.

@font-face {
  font-family: 'DSEG14Classic';
  src: url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.woff2') format('woff2'),
       url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.woff') format('woff'),
       url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.ttf') format('truetype');
}

Next, I copied the jitter animation of the text in the original. Game developers often add subtle animations to breath life into an element, like making a background drift or a character, well, breathe. To achieve the jitter, I defined a CSS animation:

@keyframes jitter {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(5px);
  }
}

At this point, the text glides smoothly back and forth. Instead, what I want is for it to jump back and forth five pixels at a time. Enter the steps() function:

#message {
  animation: jitter 3s infinite steps(2);
}

Finally, I added the same audio feedback as used in Among Us.

let soundAccepted = new Audio('./audio/CardAccepted.mp3');
let soundDenied = new Audio('./audio/CardDenied.mp3');

if (status === 'valid') {
  soundAccepted.play();
} else {
  soundDenied.play();
}

Sound effects are often frowned upon in the web development world. A project like this an opportunity to run wild with audio.

And with that, the we’re done! Here’s that demo again:

Try your own

Given how standardized the web has become in look and feel, this approach of pulling an element from a game and implementing it for the web is a good way to break out of your comfort zone and try something new.

Take this Among Us card swipe. In a small, simple demo, I tinkered with web fonts and animations in CSS. I monkeyed with input events and audio in JavaScript. I dabbled with an unconventional visual style.

Now it’s time for you to survey interesting mechanics from your favorite games and try your hand at replicating them. You might be surprised what you learn.