Let’s read a storybook

365 Stories

Let’s go back in time when our parents read us stories to help us fall asleep. You may remember books with hundreds of short stories and fairy tales. Short enough for the kids to not lose focus and get bored.

Fast forward to today and what are we doing as developers? Writing stories. Well, not exactly fairy tales but technical stories in a storybook. You may have guessed it — I’m talking about Storybook.js.

What is Storybook.js?

Storybook is a powerful frontend tool that allows us to quickly prototype UI components without being tied up by slowly arriving business requirements and back-end API. Sounds like a mouthful of buzzwords but let me explain with examples how we at Evolution are using it.

At Evolution, we create many new games every year and there are times when there are only design wireframes of how the games will look like but without math and game logic we would need to wait for back-end implementation to start building the game and be able to show the progress and test it.

Storybooks enable us to do practical things like:

  • create UI components on early ideas with mocked data and we can showcase progress
  • make screenshots on storybooks to test if we didn’t unintentionally break the existing UI before deploying the new changes
  • teams can create examples and documentation on how to use common components (see picture below)

For our main mono-repo, there are around ~35 different storybooks — one for each game — where each storybook consists of dozens of stories. Each story has multiple controls for testing inputs to change results.

We have also implemented a custom addon for translating our stories in different languages to be able to quickly check those long words, e.g. “TAKE ALL” in Hungarian is “ÖSSZES ELFOGADÁSA”. In the below example we have label resize logic if words exceed borders for buttons.

Finally, hundreds of screenshot tests run daily on pipelines for continuous delivery.

If we want to continue progressing and improving our products we cannot rely on human mistakes where a simple change breaks the game which we didn’t notice in time. Fixing regression bugs can quickly become an exhausting nightmare.

Let’s build something!

Thought

A few months ago on Christmas, I visited my parents and went through my old stuff. Some old items I saw reminded me of an old memory, NHL Hockey 1999.2000 sticker book. What a nostalgia trip that was! Some 20 years ago I used to trade and collect stickers for this sticker book and get all excited when I completed a page or got that rare team logo with a holographic background. Too bad I threw it out when I started going to university.

Idea

Now imagine you have been working at Evolution for quite some time but recently your office was moved to a new bigger space. One first day you tried to find your desk but accidentally got lost. In front of you, there is a door without a label. You open it up and by the looks of it, it is a storage room. Might as well check out the room! You notice a dusty box directly in front of you and out of curiosity you open it up. You see a magazine and some kind of sticker packs. After closer inspection, it seems that it is an Evolution sticker book. “Did we ever have a sticker book at Evolution?”

What if we created something like this as a web app? It is not the same as physically putting stickers in a sticker book but it could be the next best thing.

Tech Stack

There is no better way to showcase something than to build an example from the ground up. For this idea, we are going to use Create React App (CRA) Typescript template and Storybook React. The reason is that this template saves a lot of time setting up a React project will all the necessary dependencies.

The latest CRA 5.0.0 template comes with Webpack 5, Jest 27, PostCSS 8, Tailwind CSS 4 which is nice because we can use some of the latest features and I always wanted to try TailwindCSS. It is often mentioned on tech Twitter. In short, Tailwind CSS is a utility framework for CSS and can save a ton of time on fast UI development.

After setting everything up you should see the following in the terminal:

If you are interested in how exactly to set it all up, I added a guide in the project’s README file.

Storybook initialization in the project will also create some demo stories, which I will delete as I won’t need them.

Wireframe

For the sticker book we need a few components:

  • Preparing assets (backgrounds, logo stickers, front/back cover page)
  • Page (sticker book single page with background images)
  • Sticker Book (container for pages)
  • Sticker (logo, dynamic, page)
  • Sticker list (list of available stickers to choose from)
  • Sticker pack (randomly generate 5 stickers in a pack)

We will go briefly through each of them now.

Preparing assets

Backgrounds

Let’s start with getting some Evolution games background pictures. I was able to get some marketing photos for our past games. However, each of them had different resolutions and dimensions. With photo editing tools I rescaled them to 3208 x 1912 and then split them into two sides — left and right. For one of the games I worked on previously, Cash or Crash, I did something special — I asked my Unreal Engine developer colleague, to render out some screenshots of the background city which is visible outside of the studio “blimp”. However, the maximum resolution wasn’t great so I upscaled it to 4K with AI tools and now it matched all the other background image sizes.

Stickers with logos

Regarding stickers, I used regular game logos and added a “holographic” background. These logos are the only stickers to be prepared before, the rest of the stickers will be randomly auto-generated from background images to make them more interesting.

Front and back cover for a sticker book

For covers, I will use something from the future — Nvidia Canvas. In short, this software turns your doodle into an amazing picture.

Stories

Page

Page component will show half of the game background depending if it is odd or even page. Using Storybook I can quickly validate that each game picture corresponds to the correct page side — left or right.
[ Code ]

Sticker Book

The most tricky part is making each page turn like a book. For that, we will use the CSS propertypreserve-3d on the book component, decreasing z-index for each page and position: absolute stacking on top of each other. For animation, we’ll use transform: rotateY() and lastly perspective to give the page-turning a more natural look (where to top and bottom parts are not cut off). This took me days to figure out — the pages were overlapping or showing in the wrong place due to wrong z-index . Also, the page-turning mechanism — I had a tricky issue to solve where turning one page turned multiple because pointer-events go through all layers.

At this point, I’m not sure what will be the final layout for my app but in Storybook, I can use a temporary layout to test how my sticker book is turning pages.

[ Code ]

Stickers

There will be 3types of stickers — logo, dynamically generated stickers from page backgrounds, and page stickers which will be the placeholders on pages.

Logo sticker

Logo stickers are just logos on a “holographic” background. These stickers will be unique per game. With a select control, I can check if my images match the game type.

[ Code ]

Dynamic sticker

Dynamic stickers are based on page background images. The idea will be that the background-positionwill be randomly generated to make the sticker book more unique each time.

I can use the range sliders to test out if the background-position aspect ratio is correct and I'm fully covering the whole image.

[ Code ]

Page sticker

Page sticker will be the representation of logo and dynamic sticker on the page when a sticker is either used or a placeholder. The colorful border represents that this will be a logo sticker, white — dynamic (from background image).

With boolean Storybook controls I can check if my page sticker is working correctly. Alternative would be to launch the React app, add this component somewhere on the screen, and manually use React Dev Tools to modify the internal state. Or use the trial and error approach by just changing code, saving, and waiting for hot reload to refresh the page.

[ Code ]

Sticker list

The sticker list will be our list of stickers from all sticker book pages. At the start, the stickers will be grayed out (unavailable), but when you get more stickers the ones you get will change color. In the sticker list, I’m using dynamic and logo sticker components. On top of them, I added sticker numbers to easier determine which is which.

[ Code ]

Sticker pack

A sticker pack will be a simple component from the outside — 2 sides, turned around like a card. For the sticker pack, I used perspective and transform: rotate around its center as if flipping around a card. In Storybook, I checked if the images are properly aligned and the rotation worked as intended.

[ Code ]

Sticker pack dialog

A dialog showing a zoomed-in sticker pack where you will be able to “open” the pack and take out the stickers. Let’s also add some animations for the cards to “come out” of the pack and fly off to the sticker list.

The great part about Storybook is that I can isolate the component and test it out separately before adding it to the full application.

[ Code ]

Data

We now have the UI components but the story data is hardcoded and not usable outside them. We will need page and sticker data linked together to be able to apply it for all games.

To make the app work correctly we will need the following properties for pages and stickers:

Random data

And to make the app more interesting I will create a random data generation utility to generate this data differently every time the webpage is refreshed. The benefit of random data is that it allows finding corner cases a bit faster which is difficult to spot using the same data all the time.

The random logic in this app will be:

  • pages (each time the game background and order will be different)
  • stickers (each time the location of stickers on each page will be different)
  • sticker packs (each time stickers inside the sticker packs will be different)

Sticker location

Code will choose a random location for each of the stickers from 8 areas and the inner x and y position.

To get the random logic work we can use the Javascript default approach with Math.random and Math.floor on an array to get random array item:

const randomItem = items[Math.floor(Math.random() * items.length)];

If we run this in any browser console e.g. Chrome Dev Tools

We will run eventually in one specific problem — completely random data is not all that good. For example, when I worked on random sticker positions I eventually ran into issues where 2 stickers are overlapping however it took me way too much time to replicate this bug because every time I get random locations they are always different.

How to un-random the random data

To improve the random behavior, we can replace Javascript Math.random with our implementation which we can control. This approach involves using a seed value that we provide to our random function and it always will return the same next values. It works like a save feature you can load the same state every time and it is still random generated data.

To illustrate how this works I created a sandbox. When you change the seed number the random function will return the exact values every time you use the random function.

For this, I used the Mulberry32 algorithm which fortunately was ported from C language to Javascript by one thoughtful person: https://github.com/bryc/code/blob/master/jshash/PRNGs.md#mulberry32

I wrote a Typescript version due to how my project is set up:

So, I went through a few seeds and reproduced the issue super fast, seed 421.

Now I just need to play with numbers for x and y maximum distance it can move from starting point.

Putting it together

Now that we have data and the components there is one more thing missing — how will we apply the stickers to the sticker book. I decided to go with the drag & drop feature as it feels more natural than just pressing here and then pressing there.

Optimization

With optimization I have a rule of thumb — don’t optimize prematurely because your application will change 100 times throughout development. Regarding the source images I used — the average file size of each uncompressed (3408 x 1912) image is ranging from 4–7MB (.pngformat) which is too much for any good webpage. Let's convert and compress them to .webp format which is a pretty decent format nowadays and it works in all modern browsers. For that, we can use https://squoosh.app/ (made by Google Chrome developers). There is also an option to convert all images at the same time using Squoosh CLI.

Open folder where the saved screenshots are located, then open a terminal, and run:

npx @squoosh/cli --webp '{}' . -d "./webp"

This will automatically run the CLI with the default WEBP configuration without much hassle. Now the average file size is ~300 KB (1704 x 1912) per page. I think it looks good enough for desktops and laptops.

Result

Links

Summary

  • Storybook is indeed an amazing development tool and I strongly encourage you to start using it. It will save you time and will allow you to quickly produce tangible results.
  • You can also mock component state to test out multiple corner cases and you don’t need to get rid of this code in the final product. Without Storybook, you would have to clean up such code.
  • I didn’t delve deep into screenshot tests but still wanted to mention that Storybook is an excellent base on which to produce screenshot tests because you can control the data and the behavior of the components you are testing before making screenshots.
  • You can build even UI component libraries with well-documented controls for others to use in their projects.

Final Thoughts

  • This time I decided to build something from scratch and it was quite of adventure.
  • The article may sound like this was easy to build but I had problems even starting with the latest CRA 5.0 — wasted hours to make Tailwind CSS also work in Storybook. Eventually fixed it but that happens when other 3rd party tools are not up to date.
  • The sticker book took me 2 months to build mostly in my free time due to strict deadlines on the new game our team is building. Mostly I did it to improve my skills and finally put some of my ideas on paper. After 130 commits I feel confident about my final result.

What’s Next

Maybe a Witcher 3 Gwent-style Evolution board game )

The article was written and produced by Andris Vilde.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store