Creating a Fabulous Frontend for Flatiron Field Day

Dick Ward
8 min readSep 19, 2018

--

Flatiron Field Day was a project inspired by Reddit’s r/place experiment. If you aren’t familiar, r/place was a canvas that allowed users to draw anything they want. The caveat was that they could only draw one pixel at a time, and only once every ten minutes or so. The end result of this was a series of alliances building out colors, patterns or pictures that were important to them, and trying to protect them from people that would seek to overwrite.

Timelapse of the Field Day Canvas

In our version, participants could paint pixels up to four times a second, but they had another advantage that those on r/place didn’t — they were able to use code to paint. Students at our school are at a variety of levels and depending on that level may be practicing Ruby, JavaScript or Python, so we built out clients for each of these languages. We also built out a stat tracker that updated live, a server to handle all these requests, and a program we called ‘The Nexus’ to filter out bad requests and limit the rate at which students were able to paint.

The final piece of the puzzle was the canvas client, which was my domain. There was an alpha version of the client waiting for me when I started, but there wasn’t a ton of functionality. It happily displayed tiles that were already set, and users could jump into the console and programmatically set tiles, but it wasn’t pretty and it wasn’t user friendly. More importantly, it wasn’t fun to use!

I had a few goals going into this rehaul of the canvas client, but the overall directive was ‘make it cool, make it fun, make it sexy’.

- A user should be able to drag the canvas around the screen
- A user should be able to move the canvas with arrow keys
- A user should be able to zoom the canvas
- A user should be able to select a color
- A user should be able to click a pixel to paint it

Canvas Zoom

cycleZoom(e) {
e.preventDefault()
switch (this.zoom) {
case 1:
this.zoom = 2
this.zoomStatus.innerText = `x2`
break;
case 2:
this.zoom = 4
this.zoomStatus.innerText = `x4`
break;
case 4:
this.zoom = 8
this.zoomStatus.innerText = `x8`
break;
case 8:
this.zoom = 16
this.zoomStatus.innerText = `x16`
break;
case 16:
this.zoom = 1
this.zoomStatus.innerText = `x1`
break;
default:
this.zoom = 1
this.zoomStatus.innerText = `x1`
}
this.zoomer.style.transform = `scale(${this.zoomMult(this.zoom)}, ${this.zoomMult(this.zoom)})`
}

I took my inspiration for our canvas zoom feature from the blog of the r/place developers. It was a tremendous resource throughout, and a great read in general. I took a lot of information from the blog about CSS transformations, which were my main mode of manipulation throughout.

We use a 500 x 500 canvas, and decided to implement a variety of zoom levels, so that our users could be as close as they felt was helpful to them. To do this, we wrapped the canvas in a div that we called the zoomer, all controlled by our CanvasManager class.

I attached a keyDown event listener to the body of the site. This way, every time someone hit spacebar, this listener would trigger cycleZoom in the CanvasManager. In turn, cycleZoom would check to see the current zoom level and then switch our user to the next level available. This was all pretty easy to handle, and worked beautifully until I hooked up my computer to the 4K monitor that we would be displaying the board on during Field Day. The canvas ended up looking tiny on the massive screen.

To take care of this, and to account for whatever other screen resolution people might be using, I created a formula that would adjust based on the size of the screen. In this case, the sizeAdjust variable would be .00165 times the height of the screen. Multiplying this by the current zoom level would yield the actual number I needed to properly adjust the scale.

Canvas Movement

To make movement work, I used both the CanvasManager class and a new Dragger class. This would have been done better using a single class for movement, and it will be in the future, but a time crunch can make you code silly things.

The drag itself is pretty standard, although movement math tends to get confusing. I gave the constructor for Dragger startingX and startingY properties, as well as lastTimeX and lastTimeY. By using the clientX and clientY that a mouse move event gives us, I calculated how much a user moved their mouse, their starting position, and their stopping position.

I also added a boolean to track whether a drag event was taking place. On mouse down, it switches from false to true. When true, I would transform the div based on user movement.

mouseMove(e){
if (this.dragEvent === true){
this.xDiff = this.startingX - e.clientX
this.yDiff = this.startingY - e.clientY
this.newX = this.lastTimeX - this.xDiff
this.newY = this.lastTimeY - this.yDiff
const newPos = `translate(${this.newX}px, ${this.newY}px)`
this.dragger.style.transform = newPos
this.handleTilt(e)
}
}

Movement based on key input was much easier, simply translating 20px in a direction based on which key the user hit. To keep key input a viable option while zoomed in, I multiplied the movement by the current zoom amount.

Color Select / Painting

Color Selector for Flatiron Field Day

This was a blast to figure out. I started by creating a dumb version of the color selector on the page. I made some fun new icons (an eyedropper and a paintbrush) for the cursor that I thought were just going to be for style, but ended up being useful for the logic as well.

Essentially, there was a click event listener on the canvas. If the cursor was an eyedropper, I would switch the cursor back to normal and change the background color of the color selection div to the color of the tile that the cursor clicked on. It’s easy enough to get the RGB values from a specific coordinate using getImageData if you’ve got the X and Y values. And by using the clientX and clientY along with the the zoom amount and zoom multiplier, I was able to grab the precise position of the mouse, even while the user was zoomed in.

Color Selector and Paint in Action

If the cursor was a paintbrush, then I would grab that X and Y again, take the background color of the color selection box, and then make a post request to our server using all of that info.

Making it Sexy

The base functionality was done with a day to spare, so I took some time to have some fun. My dream was to have the canvas tilt in response to the direction it was being dragged, which ended up being both easier that I anticipated — once I got out of my own way.

Since I thought the tilt functionality may be a bit beyond my abilities, I started by looking for a library that would handle it for me. I ended up deciding on vanilla-tilt, which is an interesting library that creates a 3D tilt effect when an element is hovered over. It’s simple to implement, and it seemed like adapting it to suit my needs would be easy.

I spent a lot of time playing in vanilla-tilt’s code, trying to make it suit my needs. After enough time spent digging through the library’s code, I figured out just how they were accomplishing their tilt effect, and decided to just create my own.

To do this, it’s back to CSS transformations. Since transformations are a string, it’s tricky to manipulate them, but easy to replace them. As such, I created a div to tilt along the Y axis and a separate to tilt along the X axis. The snippet below is the most basic version that I built out, and only for the X axis movement.

handleTilt(e){
if (e.movementX < -5) {
this.Ytilter.style.transform = "perspective(800px) rotateY(-3deg)"
} else if (e.movementX > 5) {
this.Ytilter.style.transform = "perspective(800px) rotateY(3deg)"
} else {
this.Ytilter.style.transform = "perspective(800px) rotateY(0deg)"
}
}

e.movementX and represent the distance that the mouse pointer has moved between its last event and now. Without setting the base threshold to 5, the board would tilt when users didn’t intend to, as a slightly sloppy click could still show slight movement.

Canvas Movement in action. Even smoother when not in Gif form!

After playing around around with this basic movement, I decided it needed two things. First, it needed to have smooth animation. The tilt worked, but was incredibly jerky as it changed directions. Second, I wanted it to respond to different levels of input. A faster mouse movement should tilt the canvas more. It seems like users might expect it, plus it’s fun.

if (e.movementX < -150) {
this.Ytilter.style.transform = "perspective(800px) rotateY(-360deg)"
}else if(e.movementX < -20){
this.Ytilter.style.transform = "perspective(800px) rotateY(-7deg)"
}else if (e.movementX < -5) {
this.Ytilter.style.transform = "perspective(800px) rotateY(-3deg)"
}

A few simple additions to handleTilt took care of that. A slow mouse move results in a small tilt, a faster mouse move results in a larger tilt, and a very fast mouse move will cause the board to do a flip — just an easter egg to keep people exploring.

#Ytilter {
transition: all 0.15s;
}
#Xtilter{
transition: all 0.15s;
}

As it turns out, smoothing out the tilts was also simple. All I needed to do was add transitions to both of the divs being tilted and adjust until I found a sweet spot.

Wrapping it up

It’s obvious looking at the code, and especially when writing about it, that this all could have been done more cleanly. I could have organized my code better and I could have used fewer divs. But for the timeline I was up against — I’m happy with how it all worked. None of our participants had a problem with the canvas, and it worked flawlessly for the entire day.

I was only one member of a larger team working on different aspects of this project, and every day that we met, I was amazed to see what new things people were able to put together. I’m proud of what we were able to put together, and excited to start work on our next iteration.

References

Want to learn more about Field Day? Check out this great writeup of how we built the Nexus!

https://medium.com/@sbal13/building-the-nexus-for-flatiron-field-day-72131c0942c1

--

--

Dick Ward

Full-stack web developer with a flair for the theatrical and a mission to leave the world a better place than I got it. DickWard.com