I’m no stranger to camera work, having built a camera powered app as recently as the David Bowie Space Oddity campaign. Check out that case study or Quote Camera for an understanding of how I integrate WebRTC to create custom camera experiences in the browser.

Typically these camera campaigns involve a silo’d individual making content but our project required the photos be brought into a shared visual. This requires temporary storage and real time event broadcasting. I decided to create an AWS S3 bucket which I would upload photos directly to with a pre-signed URL. This forced my otherwise static website into the world of server processes. 😔 Luckily, my host Netlify had me covered. Following a few tutorials on the subject, I created a Netlify Function which would create a pre-signed url for putting objects on S3. I then used Uppy and it’s AWS S3 plugin as the interface for managing file uploads. Uppy signs the upload the Netlify function and returns the URL of the uploaded file.

let uppy = Uppy({

autoProceed: false,

}) uppy.use(AwsS3, {

getUploadParameters (file) {

return fetch(`/.netlify/functions/presign?key=${file.name}`)

.then(response => response.json())

.then(data => data)

}

}) uppy.on('upload-success', (file, data) => {

let url = data.body.location

})

This URL is then sent to the visualization interface using Pusher.

IP Address to Heatmap

We knew we wanted to include a map of activity as part of our visualization which shows where fans were coming from. Initially, I thought I would build this on top of Mapbox but the terms weren’t clear on broadcasting their map live without a special license. Instead, I focused on coming up with a simple custom solution. I thought if I could turn an IP address into a pair of coordinates and then turn those coordinates into screen pixels, I could create a simple visual.

Maxmind provides an excellent GeoIP service with companion Javascript library which will turn a user’s IP address into a location at several levels of fidelity. It’s not perfect but it’s close enough for our abstract visual. Each pair of coordinates is then sent to the visual using Pusher.

geoip2.city(data => {

// data.location.longitude

// data.location.latitude

}, error => {

console.log(error)

}, {})

We can then take the coordinates and convert them into pixels based on mercator projection. Thank you Stack Overflow.

let coordinateToPixel = (height, width, coordinate) => {

let x = (coordinate[0] + 180) * (width / 360)

let latRad = coordinate[1] * Math.PI / 180

let mercN = Math.log(Math.tan((Math.PI / 4) + (latRad / 2)))

let y = (height / 2) - (width * mercN / (2 * Math.PI)) return [x, y]

}

With screen pixels readily available, you can come up with any kind of visual. The client envisioned a heat map which showed “hot” points around the world. You could just about imagine my surprise when I stumbled onto mourner’s simpleheat library which did exactly that. Not that it is surprising mourner had developed a solution, more that Vladamir’s libraries always seem to make it into my projects. (See suncalc on David Bowie.) Anyway, simpleheat is nice and simple, just the way I like it. You pass in some data and visualization parameters and it projects as a heatmap visual onto a canvas of your choice.

let heat = simpleheat(canvas) heat.max(15) heat.gradient({

0.5: 'white',

0.75: 'black',

1.0: 'red'

}) heat.add([x, y, z]) heat.draw()

Of course, I wasn’t the only person excited about integrating simpleheat into this Slipknot project.

Single File (Maggots)

One thing I was keen on preventing was the visual crashing (to the best of my ability.) I suspected both photos and locations would come in at a high rate, especially on launch. Since the photos arrive as URLs from Pusher, they must first be loaded before being added to interface. To do this, I rely on a handy promise function.

let loadImage = (url) => {

return new Promise((resolve, revoke) => {

let img = new Image() img.onload = () => {

resolve(img)

} img.crossOrigin = 'Anonymous'

img.src = url

})

}

Rather than try and load every photo as it comes in, I decided a queue would make more sense. Luckily, Async provides an aptly named queue method which queues functions at a concurrency you decide. To keep things chill, each photo is loaded one at a time.

import queue from 'async/queue' let photoQueue = queue((task, callback) => {

loadImage(task.url)

.then(img => {

// add image to visualization callback()

})

}, 1) photoQueue.push({

url: url

})

I used a similar setup to receive and draw adjustments to the heatmap. It’s a less costly function but just in case…

Visualizer