Hold the button down!

thebutton.world was a team effort.
If anyone, anywhere in the world is holding the button, the timer keeps ticking

Collective, pointless efforts are interesting. I was inspired by that hitchiking robot that was murdered in philadelphia


The Idea

The idea was simple. Make a big button. Let people push it. See how long it will stay pushed.

Prep

  • I wanted to be able to see everyone’s cursors, so you know its a community effort.
  • I wanted to show everyone on a big globe, so the absurdity of whats happening is apparent.
  • I also wanted to make the site without any of the frameworks I often lean on.

So it was made in vanilla Javascript + html (with a couple libraries).
In particular, I’ll be using handlebars js.
Server is express.js + socket.io.


The “.world” part

Firstly I wanted to show where people were pushing the big button from. My first thought was a map, but I stumbled upon a big, spinning globe and just had to use it. You can find the code here.

So one copy and paste later, I had a start.

How am i going to show where users are on it? Good question, moving on…


The “thebutton” part

Now, I know I needed a huge button. One that really, really makes you want to press it.

So I spent way too long tweaking css until finally…

🤤👉🔴

It can tell if is pressed by using to javascript events, mousedown and mouseup.

const bigButton = document.getElementById("big-button");
 
bigButton.addEventListener("mousedown", function (event) {
  // button pressed
});
 
document.addEventListener("mouseup", function (event) {
  // button released
});

To know how long its been pressed, you really only need to know need 3 things.

  1. If is currently being pressed
  2. Time it was pressed
  3. Time it was released

Then you can make something like this:

{{#if isButtonDown}}
  <h1>Button held for {{ diff }} seconds</h1>
  <p></p>
{{else}}
  <h1>No one is holding the button. Press it!</h1>
  <p>Last held for {{ diff }} seconds</p>
{{/if }}

And using Moment JS, you can calculate the time it’s been down like this

let start, end;
if(isButtonDown) {
  start = moment(pressTime);
  end = moment(Date.now());
} else {
  start = moment(data.pressTime);
  end = moment(data.releaseTime);
}
let diff = end.diff(start, "seconds");

This is nice because it will also show the time the button was last held down for, after it is released.

When the isButtonDown state transitions, we can save these 2 times somewhere, and we’ll have a record of every time the button is pressed and for how long


Cool Cursors

Remember when you could go to sites and they would hijack your mouse and turn it into an epilepsy causing abomination? Well I miss that, and wanted to do it, with a more modern take. You know something you can identify with. yea, emojis.

Anyways, get rid of the normal cursor and replace it with something.

First make a triangle. Then a circle. Then mash them together and put an emoji inside. Add some glow and…

If you’re curious how I dynamically color and choose emojis, the code look like this:

function randomEmoji(){
  return emojis[Math.floor(Math.random() * emojis.length)];
};
 
function randomColor() {
  return 'rgb(' + 
    (Math.floor(Math.random()*156)+100) + ', ' +
    (Math.floor(Math.random()*156)+100) + ', ' +
    (Math.floor(Math.random()*156)+100) +
    ')';
}
 
const emoji = randomEmoji();
const color = randomColor();
const rgba = "rgba" + color.slice(3, color.length - 2) + ", 0.75)";
 
const cursorPoint = document.getElementById("cursor-point")
const cursorBubble = document.getElementById("cursor-bubble")
 
cursorPoint.style.borderTopColor = color; // color triangle point
cursorBubble.innerHTML = `<p class='cursor-emoji'>${emoji}</p>` // add emoji
cursorBubble.style.backgroundColor = color; // color circle
cursorBubble.style.boxShadow = `0px 0px 25px 5px ${rgba}` // add glow

And finally to make it move around where your mouse is, I used the mousemove event, and simply offset the “cursor” by the same pixel offset as the real cursor using css styling

// move the cursor based on coordinates
document.addEventListener('mousemove', function (event) {
  moveCursor(event.pageX, event.pageY);
})
 
const moveCursor = function (x, y) {
  cursor.style.left = x + 'px';
  cursor.style.top = y + 'px';
}

Multiplayer

Everything was pretty easy until this point. Then, not so much.

Adding multiple people being able to all interact was the main task. so lets do it

The Game

I had no Idea how to do this part to be honest.

But the problem reminded me of something… playing video games! Everyone is moving around, usually shooting and brutally murdering each other. It’s a similar concept.

So I found a tutorial on how to make a multiplayer snake game in javascript. So know that when you visit this site, you’re basically playing a weird looking version of snake.


Socket.io

Doing this actually wasn’t as hard as I thought using a tool called socket io

Basically, you make a little node server, run socket io on it, connect with your website, and it handles mostly everything else.

Set up is easy on the server:

const http = require('http');
const server = http.createServer(app);
const { Server } = require("socket.io");
 
const io = new Server(server, {
  cors: {
    origin: process.env.ALLOW_ORIGINS,
    credentials: false
  }
});

ps. Make sure you have the same version of socket io installed on your server and frontend. I didn’t and, well, that’s an hour of my life I’m not getting back.

The actual server code that sets up a websocket connection looks like this:

io.on('connection', (socket) => {
  log.debug('a user connected');
 
  socket.on('disconnect', () => {
    log.debug('user disconnected');
  });
 
  socket.on('join', handleJoin);
  socket.on('moveCursor', handleMoveCursor);
 
  startInterval();
 
  function handleJoin(user) {
    socket.data = user;
    socket.emit('init', { id: socket.id });
  }
 
  function handleMoveCursor(position) {
    socket.data.position = position;
  }
 
});
 
function startInterval() {
  setInterval(() => {
    emitState()
  }, 1000 / FRAME_RATE);
}

The interval runs at FRAME_RATE many times per second, and sends the locations, colors, emojis, and whatever else to all users connected.

emitState() is a bit complicated, but it basically runs through all sockets connects and organizes them in a way that’s useful on the website.


Displaying all the cursors

First, add a listener to get updates from the server, then render all cursors

socket.on('update', handleUpdate)
 
function handleUpdate(data) {
  data = JSON.parse(data);
 
	// filter all users except currentUser
  users = data.users.filter(function(item) {
    return item.id != id;
  });
  drawCursors();
}
 
function drawCursors() {
  let out = cursorsTemplate({users});
  document.getElementById("cursors").innerHTML = out;
}

The cursorsTemplate is:

{{#each users}}
  <div class="cursor" style="top: {{position.yPercent}}%; left: {{position.xPercent}}%">
    <div class="cursor-point" style="border-top-color: {{color}}"></div>
    <div class="cursor-bubble" style="background-color: {{color}}; box-shadow: 0px 0px 25px 5px {{rgba}}">
      <div>{{emoji}}</div>
    </div>
  </div>
{{/each}}

In order to update the location of the current user in the server, I added some code to the movemouse event listener I made earlier that was being used to display their cursor.

Also, I calculated the location as a percentage, instead of pixels, since people have monitors of all different sizes.

const moveCursor = function (x, y) {
  cursor.style.left = x + 'px'
  cursor.style.top = y + 'px'
  let position = {
    x: x,
    y: y,
    xPercent: 100 * (x / width),
    yPercent: 100 * (y / height)
  }
  socket.emit('moveCursor', position);
}

This updates their location on the server using socket.emit every time the mouse is moved.


Push the button, together!

Some slight tweaks to the button pushing code introduces teamwork.

The server can now manage the state of the button based on any socket that is connected holding the button.

On server:

socket.on('pressButton', handlePressButton);
socket.on('releaseButton', handleReleaseButton);
 
function handlePressButton() {
  socket.data.holding = true;
}
function handleReleaseButton() {
  socket.data.holding = false;
}

in emitState() the relevant code is this:

  let isButtonDown = false;
  for(let s of sockets) {
    if(s.data.holding) {
      isButtonDown = true;
    }
  }
 
  // check state
  if(state.isButtonDown == false && isButtonDown == true) {
		// button transitioned to being pressed down
    state.pressTime = Date.now();
  } else if (state.isButtonDown == true && isButtonDown == false) {
		// button transitioned to being released
    state.releaseTime = Date.now()
  }
  state.isButtonDown = isButtonDown;

This loops through all connected sockets to see if they are holding the button, and then compares it to the current state of the button in order to set the start/end time

Now if any single user is holding the button, it will be reflected in the server’s state.


Now go, and push the button!

Made by hew