Super Silly Hackathon 2019
Tags: hackathon
Published: 2020-01-08 00:02:19

---
title: Super Silly Hackathon 2019
published: true
date: 2019-12-14 08:00:00 UTC
tags: hackathon
canonical_url: https://lihautan.com/super-silly-hackathon-2019/
---

I just came back from the [Super Silly Hackathon 2019](https://supersillyhackathon.sg/). It was my second time to participate in this Super Silly Hackathon.

> Hello everybody we are ready for this year, are you?? [pic.twitter.com/lem52qHO6Q](https://t.co/lem52qHO6Q)
> 
> — Super Silly Hackathon — 14 Dec 2019 (@supersillyhack) [December 14, 2019](https://twitter.com/supersillyhack/status/1205671402432450563?ref_src=twsrc%5Etfw)

This year, my silly hackathon idea is to build [a little pet in the browser](https://github.com/tanhauhau/browser-pet)!

 ![](https://lihautan.com/6f7c624b7b3ae27344790675b9e7e574/rest.gif)

## <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Inspiration?

I saw the [tamagotchi](https://github.com/graceavery/tamagotchiTemp) on touchbar a while ago, I don’t have a touchbar, but I have a browser. So why not create a pet in the browser through Chrome Extension?

## <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>What can it do?

#### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>1. It runs away from mouse cursor

 ![](https://lihautan.com/1d1ced362bf6c6431b5dee23f8251cea/evade.gif)

#### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>2. It eats whatever you try to type into an input

 ![](https://lihautan.com/80135ffeff1de9d5c0073f64d9716807/eat.gif)

#### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>3. It annihilates the buttons you hover onto

 ![](https://lihautan.com/e4881b273a45480b8201197c50c074e0/annihilate.gif)

## <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Implementation

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Chrome Extension

Firstly, it is a Chrome Extension. I used [Content Scripts](https://developer.chrome.com/extensions/content_scripts), so that I have access to the DOM of the pages I am visiting.

Here is my `manifest.json` for my extension:

```
{
  "manifest_version": 2,
  "name": "Super Silly Hackathon",
  "description": "Super Silly Hackathon",
  "version": "1.0.0",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["content-ext.css"],
      "run_at": "document_start",
      "all_frames": true
    }
  ]
}
```

To start, I added an `onload` event, to add my pet into the browser:

content.js

```
function onload() {
  const pet = document.createElement('div');
  updatePosition(100, 100);
  pet.classList.add('ssh-pet');
  document.body.appendChild(pet);
}
window.addEventListener('load', onload);
```

content.css

```
.ssh-pet {
  height: 30px;
  width: 30px;
  position: fixed;
  z-index: 1000;
  background-image: url('/images/rest.png');
  background-size: 30px 30px;
  background-position: 0 0;
}
```

The first problem I encountered, is that I couldn’t access the `/images/rest.png`. After some googling, I need to:

#### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>1. Add `web_accessible_resources` into the `manifest.json`

```
{
  // ...
  "web_accessible_resources": ["images/*"]
}
```

#### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>2. Prepend `chrome-extension:// __MSG_@@extension_id__ /` for the URL

```
.ssh-pet {
  /* ... */
  background-image: url('chrome-extension:// __MSG_@@extension_id__ /images/rest.png');
}
```

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Make it look alive

A static image will look fake.

As the quote says, **“eyes is the window to the soul”** , to make the pet look real, we need to make the eye blink.

_(That’s how you judge a person is dead or alive in movies right? Dead body will just stare blankly to nowhere)_

To make the pet blink, I found some [useful tutorials](https://blog.teamtreehouse.com/css-sprite-sheet-animations-steps) on how to create CSS sprite sheet animations.

![spritesheet for resting](https://lihautan.com/static/3730bc062daa9a64c8f69a0538d59703/799d3/rest-sprite.png "Sprite Sheet for Resting")<figcaption>Sprite Sheet for Resting</figcaption>

```
@keyframes ssh-pet-rest {
  0% {
    background-position-x: 0;
  }
  100% {
    background-position-x: -90px;
  }
}

.ssh-pet.rest {
  background-image: url('chrome-extension:// __MSG_@@extension_id__ /images/rest-sprite.png');
  background-repeat: repeat-x;
  background-size: 90px 30px;
  background-position: 0 0;
  animation: ssh-pet-rest 0.2s steps(3);
}
```

To animate it, I added the `rest` class to the pet, and removed it when it’s done animating.

```
pet.classList.add('rest');
// when the animation finished, remove the class
const onanimationend = () => {
  pet.removeEventListener('animationend', onanimationend);
  pet.classList.remove('rest');
};
pet.addEventListener('animationend', onanimationend);
```

At the beginning, I thought I would add more other micro expressions to the pet, so I wrote a scheduler, to schedule expressions randomly:

```
async function doSomething(random) {
  switch (random) {
    case 0:
    // ...
    default:
      return animateRest();
  }
}

function scheduleSomething() {
  setTimeout(async () => {
    const choices = 10;
    // cheat to do Math.floor
    const random = ~~(Math.random() * choices);
    await doSomething(random);
    scheduleSomething();
  }, 800 + Math.random() * 1000);
}

// make animation promise based
function animateRest() {
  return animateClass('rest');
}
function animateClass(cls) {
  return new Promise(resolve => {
    pet.classList.add(cls);
    const onanimationend = () => {
      pet.removeEventListener('animationend', onanimationend);
      pet.classList.remove(cls);
    };
    pet.addEventListener('animationend', onanimationend);
  });
}
```

I used `await` to wait for animation to finish, before schedule the next action again, so that it will not have 2 actions running in parallel.

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Avoid the cursor

I drew a sprite sheet that make the pet looks like it is squiggling away,

 ![](https://lihautan.com/564592799f66aba34cdbc50bb49d08fa/shift-left.gif)

while at the same time set the `transform: translateX()` to actually move it:

```
@keyframes ssh-pet-shift-left {
  0% {
    background-position-x: 0;
    transform: translateX(0); }
  100% {
    background-position-x: -240px;
    transform: translateX(-30px); }
}

.ssh-pet.shift-left {
  background-image: url('/images/shift-left-sprite.png');
  background-repeat: repeat-x;
  background-size: 240px 30px;
  background-position: 0 0;
  animation: ssh-pet-shift-left 0.8s steps(8);
}
```

In JavaScript, I tried to keep the position state inside an object:

```
const position = { x: 0, y; 0 };

function offsetPosition(x, y) {
  updatePosition(position.x + x, position.y + y);
}
function updatePosition(x, y) {
  pet.style.left = (position.x = x) + 'px';
  pet.style.top = (position.y = y) + 'px';
}
```

So, to shift away from the cursor would be:

```
async function animateShiftLeft() {
  await animateClass(`shift-left`);
  offsetPosition(-30, 0);
}
```

So, when the animation ends, the `transform: translateX()` will go from `-30px` back to `0`, and at the same time, I moved the position of the pet `-30px` in x-axis.

To know when the cursor is on top of the pet, I used `mousemove` event:

```
pet.addEventListener('mousemove', async event => {
  if (!animating) {
    animating = true;
    await shiftAway();
    animating = false;
  }
});

// edge avoidance shifting
async function shiftAway() {
  if (position.x < 40) {
    await animateShiftRight(true);
  } else if (position.x > window.innerWidth - 40) {
    await animateShiftLeft(true);
  } else if (Math.random() > 0.5) {
    await animateShiftLeft(true);
  } else {
    await animateShiftRight(true);
  }
}
```

To avoid reanimate while still animate, I added a `animating` flag, as an mutex.

Besides, it would be weird to see the pet shifting pass through the browser window, because that way, you would never be able to interact with it anymore 😢

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Eating the characters

To know when someone is typing into any of the input box, I attach an `input` event listener on to the `document.body`:

```
document.body.addEventListener('input', async event => {
  debounceByElement(event.target, eatInputValue, 1000);
});
```

There maybe multiple `<input />` on the page, so I created a debounce function for each `<input />`.

To make the eating simple, I will always eat from the left, consuming characters from left to right:

```
function eatInputValue(elem) {
  elem.value = elem.value.slice(1);

  debounceByElement(event.target, eatInputValue, 1000);
}
```

However naively eating the characters this way, will screw up your cursor position / selection.So I googled and pasted the code snippet to improve it:

```
function eatInputValue(elem) {
  // remember selection position const start = elem.selectionStart, end = elem.selectionEnd; elem.value = elem.value.slice(1);
  // restore selection position elem.setSelectionRange(Math.max(start - 1, 0), Math.max(end - 1, 0));
  debounceByElement(event.target, eatInputValue, 1000);
}
```

After able to “eat” the characters, I need to animate the pet eating the characters

![spritesheet for eating](https://lihautan.com/static/9e73d8c405d424fe50adc75e042a6363/799d3/eat-sprite.png "Sprite Sheet for Eating")<figcaption>Sprite Sheet for Eating</figcaption>

To align the eating with the removing of a character, I split the animation into 2 parts, **the eating** and **the chewing** _(I named it digesting during the hackathon)_.

```
function eatInputValue(elem) {
  await animateClass('eat');
  const start = elem.selectionStart,
    end = elem.selectionEnd;
  elem.value = elem.value.slice(1);
  elem.setSelectionRange(Math.max(start - 1, 0), Math.max(end - 1, 0));
  await animateClass('digest');
  debounceByElement(event.target, eatInputValue, 1000);
}
```

To make it more realistic, I tried to make the pet tilt forward, while consuming the character:

```
@keyframes ssh-pet-eat-move {
  0%,
  30% {
    transform: translateX(0);
  }
  50% {
    transform: translateX(4px);
  }
  100% {
    transform: translateX(0);
  }
}
```

The pixel values had been tuned to make the movement subtle, yet natural.

Now, how do I place my pet to the left of the input?

Instead of squiggle to the right position, I made the pet **teleport**!

```
async function animateTeleport(x, y) {
  await animateClass('disappear');
  pet.style.opacity = 0;
  updatePosition(x, y);
  pet.style.opacity = 100;
  await animateClass('reappear');
}
```

To calculate the teleport destination, I used [getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect):

```
function leftSideOf(elem) {
  const { x, y, height } = elem.getBoundingClientRect();
  const to_x = x - pet.size.width + paddingLeft(elem);
  const to_y = y + height / 2 - pet.size.height / 2;
  return [to_x, to_y];
}

function paddingLeft(elem) {
  return (
    Number(
      window
        .getComputedStyle(elem)
        .getPropertyValue('padding-left')
        .replace(/px|r?em/, '')
    ) || 0
  );
}
```

The padding of the input is important. It brings the pet closer to the character it is going to eat. 🤤

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Annihilate the buttons

Pet don’t like buttons because buttons take away your concentration from your pet.

![Eys full of hatred](https://lihautan.com/static/76ea36f778866087ce195cbb69909ec7/40714/hate.png "Eys full of hatred")

To get a list of buttons, I use [document.querySelectorAll()](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll):

```
const buttons = document.querySelectorAll('button, input[type="submit"], [role="button"]');
buttons.forEach(button =>
  button.addEventListener('mousemove', mouseMove)
);
```

To annihilate the buttons, I animated the pet with a pair of dead red eyes, and shake it for 400 milli-seconds, before annihilating the elements into vapor:

```
async function removeTheElement(elem) {
  if (animating) {
    return;
  }

  animating = true;
  pet.classList.add('hate');
  await timeout(400);

  elem.removeEventListener('mousemove', mouseMove);

  await animateClass('item-disappearing', elem);
  elem.style.visibility = 'hidden';
  pet.classList.remove('hate');
  animating = false;
}
```

I used `visibility: hidden`, so that I dont disrupt the DOM structure, yet making it disappear and unclickable, unlike `opacity: none`.

The pet with hatred is slightly bigger and agitated than the normal pet:

```
@keyframes hating {
  0% {
    transform: scale(1.1) translateX(-2px)
  }
  50% {
    transform: scale(1.1) translateX(2px)
  }
  100% {
    transform: scale(1.1) translateX(-2px)
  }
}
```

To turn the button annihilated into vapor, I used `filter: blur()`:

```
@keyframes item-disappearing {
  0% {
    filter: blur(0px);
  }
  80% {
    opacity: 1;
  }
  100% {
    filter: blur(8px);
    transform: scale(1.5);
    opacity: 0;
  }
}
```

## <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Difficulties

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Synchronising interactions

A lot edge cases has been found and fixed by using the `animating`, or `eating` flag:

- where to avoid cursor, while eating the characters
- to annihilate or to eat?There’s an heuristic to choose which one to do, when both get scheduled at the same time.

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Developing locally

Everytime when the content script is updated, I need to click refresh in the `chrome://extensions` page:

![click refresh](https://lihautan.com/static/a7d17a8422a411053f4efa44439dc8e3/799d3/refresh.png "click refresh")

In order to developed faster, I created a simple HTML, and test it locally.

```
<html>
  <link rel="stylesheet" type="text/css" href="content.css" />
  <body>
    <script src="content.js"></script>
  </body>
</html>
```

## <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Demo

I did a super quick demo, which went quite well, _I think._ 🤔

For those who are curious, and want to try out, you can visit [here for the demo](https://lihautan.com/browser-pet/).

For those who are curious about the source code, can visit [the Github repo](https://github.com/tanhauhau/browser-pet).

## <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Summary

It has been a fruitful event.

> And [@lihautan](https://twitter.com/lihautan?ref_src=twsrc%5Etfw) wins a bag of tropical fruits [pic.twitter.com/CtQUBGOI7h](https://t.co/CtQUBGOI7h)
> 
> — Yishu See (@yishusee) [December 14, 2019](https://twitter.com/yishusee/status/1205828141194727430?ref_src=twsrc%5Etfw)

Appreciate the organisers time and effort to make the Hackathon goes as smoothly as possible.

I managed to do something that I never really done before:

- CSS sprite sheet animation
- Chrome Extension that interacts with the user and manipulates the DOM

Hopefully I will partcipate the [Super Silly Hackathon](https://twitter.com/supersillyhack) again next year.