For Pickr, which is a tournament playing app, I recently implemented a feature where after playing the game, the user would be shown an image of their play history.

Here’s my playthrough of the Most Entertaining Spier-Man movie of all time:

Spider-Man: Far From Home was the Spider-Man 2 of the Tom Holland era; characters were well established, the origin story told, and we were ready to have an awesome villain in Mysterio and a budding romance plot between Peter and MJ. All these elements combined for what was the best Spidey movie we’ve seen so far.

But I digress. To try the new feature out yourself, pick any tournament topic that interests you, play a game, and you’ll see the results.

The primary building block at least on the canvas implementation side was react-konva, which is a library which provides declarative and reactive bindings to the Konva Framework. I wanted to use declarative syntax to build the canvas, and wanted the canvas rendering code to look similar to the rest of my application.

After some searching for pre-existing solutions, React Konva immediately jumped out, and all it took was about 30 minutes of playing around with a codesandbox to verify that it could do everything that was required of it.

What good is a canvas if there’s no subject matter to paint? To keep a record of the game the user plays through, a useGameHistory hook was implemented, which passed a GameHistory object to an already existing useStoredGameResult hook.


Let’s talk a bit about why this feature was prioritized over some of the others. This was an effort to drive more organic, high quality traffic to the website. The only way to share Pickr right now is with a URL link to topics, but a link is not as appealing as seeing an image which summarizes the value proposition, and can be a great way to start conversations. In other words, being able to export your results as an image, was a crucial part of the feature.

There were of course, other ways this could be implemented, for example by generating an image server-side. The obvious benefit of this approach is that it’s even easier to share than a png file the user has to download and upload somewhere else; they can simply share the URL itself.

However, the canvas solution was chosen to start with because one, I was familiar with HTML5 canvas since using it during my time at a previous gig to build a performant, powerful data grid renderer and two, I didn’t want to take on the burden of hosting and serving the generated images myself, as the number of these would grow every time a user finished a game, which happens quite often.


On Desktop providing a user affordance to download the generated canvas is quite straight forward, it’s described well in the React Konva docs.

const handleDownloadClick = () => {
  // Get the dataURL of the canvas...
  const uri = stageRef.current.toDataURL();
  downloadURI(uri, 'stage.png');
}

  
// Then download it for the user
// from https://stackoverflow.com/a/15832662/512042
const downloadURI = (uri, name) => {
  var link = document.createElement('a');
  link.download = name;
  link.href = uri;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

The tricky part was making this work for iOS. With the above code block, the download doesn’t happen properly, due to limitations of the OS.

This is part one of the solution I came up with:

const handleDownloadClick = () => {
  const uri = stageRef.current.toDataURL()

  const img = new Image()
  img.src = uri

  const w = window.open('')
  w.document.write(img.outerHTML)
}

The second part of the solution was to update the copy of the download button on mobile to indicate to open the image up in a new tab. On iOS, long pressing the canvas while it was part of my application didn’t prompt a download dialog, but when it was in the new tab as the source of the image node, long pressing prompted me to download the file to my Photos. Perfect!


Some other hurdles I had to overcome was the problem with tainted canvases which were evident from my local environment. Because I was using the use-image hook the konvajs team had published, getting around this initially was trivial.

But when I was live-testing my app, I noticed the images weren’t loading from my S3 bucket because of a CORS issue; the header Access-Control-Allow-Origin wasn’t set in the resource that was fetched. So I updated my Netlify, S3, and Cloudflare settings to ensure this this header was being passed through.

Finally, I ran into a Chrome issue where if it reused the cached version of the image, the resource wouldn’t have the Access-Control-Allow-Origin header set on it, and so the images when accessed from cache wouldn’t load. By passing a query param to the Cloudflare image path, I was able to request for a new image each time bypassing the cache and the solution worked.


For next steps regarding this feature, the UI could be improved, for example by adding a gradient to indicate the progress of the tournament. Another thought is that it is overly tall and not a great dimension to share around, especially for the earlier rounds.

Hope you’ll have much some fun with it! Looking forward to seeing more of people’s opinions and images of playthroughs. If you have any questions or comments you’d like me to know about, please reach out to my twitter.