Profile picture Sebastian Remm

Hey there!
Welcome to my blog and digital playground.

Building the Game of Life part 1

29.04.2023 - 21:48

During my early days as a TA at Le Wagon, a colleague and I discovered Conway's Game of Life and were intrigued by the concept. We spun around the idea of using it in art installations or at parties as a lighting effect and thought about how to translate it into code. At that time, my skills were much less developed and nothing more than theoretical ideas came out of it. Since then, this idea has been sitting in my head and a few weeks ago I finally sat down and implemented the whole thing with Typescript and React.

 

The current version can be tested here.

 

Divided into several blogposts, I would like to explain how I coded the individual components.

 

The actual page:

1import cn from 'classnames'
2import { NextSeo } from 'next-seo'
3
4import { useGameOfLife } from '@/components/gameoflife'
5
6import gameOfLifeSEO from '@/config/gameoflife-seo.config'
7
8import gameStyles from '@/styles/game.module.scss'
9
10export default function Gameoflife() {
11  const {
12    useInitializedGame,
13    gameState,
14    gameFieldRef,
15    handleTouchStart,
16    InitializedGameControls
17  } = useGameOfLife()
18
19  useInitializedGame()
20
21  return (
22    <main
23      className={cn(gameStyles['layout-shift'], gameStyles['confirm-dialog'])}
24    >
25      <NextSeo {...gameOfLifeSEO} />
26      <section
27        className={cn(
28          gameStyles.container,
29          { [gameStyles.loading]: gameState.isLoading },
30          { [gameStyles.fullscreen]: gameState.isFullscreen }
31        )}
32        draggable="false"
33      >
34        {InitializedGameControls}
35        <div
36          className={gameStyles['game-field']}
37          ref={gameFieldRef}
38          onClick={(event) => handleTouchStart(event)}
39          onTouchMove={(event) => handleTouchStart(event)}
40          onMouseMove={(event) =>
41            handleTouchStart(event, gameState.isMouseDown)
42          }
43        >
44          {gameState.grid.map((rows, rowindex) => (
45            <div key={rowindex} className={gameStyles.row} id={`${rowindex}`}>
46              {rows.map((cell, cellIndex) => (
47                <div
48                  key={`${rowindex}-${cellIndex}`}
49                  id={`${rowindex}-${cellIndex}`}
50                  data-row={rowindex}
51                  data-cell={cellIndex}
52                  className={cn(gameStyles.cell, {
53                    [gameStyles.active]: cell
54                  })}
55                />
56              ))}
57            </div>
58          ))}
59        </div>
60      </section>
61    </main>
62  )
63}

 

Let's have a look at the individual parts:

1<main
2  className={cn(gameStyles['layout-shift'], gameStyles['confirm-dialog'])}
3>

The initial load causes a layout shift as the cells pop in. To prevent this I use the follwoing CSS:

1.layout-shift {
2  height: calc(100vh - 7rem);
3}

This way the height of the page is already set and the layout shift is prevented. gameStyles['confirm-dialog'] overwrites some of the default styles coming from prime-reacts confirm dialog. It was a bit challenging to select the css classes and I came up with this:

1.confirm-dialog {
2  [class~='p-dialog-mask'] {
3    background-color: var(--black-transparent);
4    padding: 2rem;
5    text-align: center;
6
7    [class~='p-dialog'] {
8      border: 2px solid var(--grey);
9      border-radius: 0.2em;
10      background: var(--light-black);
11      padding: 2em;
12      text-align: center;
13
14      [class~='p-dialog-header'] {
15        display: none;
16      }
17
18      [class~='p-dialog-content'] {
19        margin-bottom: 2em;
20        color: var(--white);
21      }
22
23      [class~='p-dialog-footer'] {
24        align-self: center;
25
26        // duplicate code warning is unavoidable here
27        // -> no direct access to the button for now
28        //noinspection DuplicatedCode
29        [class~='p-button'] {
30          cursor: pointer;
31          box-shadow: 0 0 2px var(--white);
32          border: none;
33          border-radius: 0.2em;
34          background: var(--black);
35          padding: 1em;
36          min-width: 4rem;
37          color: var(--white);
38          font-weight: bold;
39
40          &:first-child {
41            margin-right: 3rem;
42          }
43
44          &:hover {
45            box-shadow: 0 0 2px var(--grey);
46            color: var(--grey);
47          }
48
49          // deactivate blue focus color
50          &:focus-visible {
51            outline: inherit;
52          }
53        }
54      }
55    }
56  }
57}

I know this is a lot of css for something that should work out of the box and I'm not really happy with the solution. Also prime-react adds quite a lot of css to the bundle and I'm thinking about replacing it with my own solution.

 


 

The next section is the wrapper of the gamefield and the controls:

1<section
2  className={cn(
3    gameStyles.container,
4    { [gameStyles.loading]: gameState.isLoading },
5    { [gameStyles.fullscreen]: gameState.isFullscreen }
6  )}
7  draggable="false"
8>

I'm using classnames to conditionally add css classes. The loading class adds a short transition at the beginning while the gamefield gets initialized.

1.loading {
2  opacity: 0;
3  background-color: var(--light-black);
4}
5
6.container {
7  // ...
8  opacity: 1;
9  transition: opacity 0.75s ease;
10  // ...
11}

The fullscreen class changes the position of the wrapper to fixed:

1.fullscreen {
2  position: fixed;
3  top: 0;
4  left: 0;
5  z-index: 4;
6  width: 100vw;
7  height: 100vh;
8
9  .game-field {
10    height: 100vh;
11  }
12}

 


1{
2  InitializedGameControls
3}

This loads the controls for the gamefield. I will explain this in more detail in another blogpost.


 

The next part is the actual gamefield:

1<div
2  className={gameStyles['game-field']}
3  ref={gameFieldRef}
4  onClick={(event) => handleTouchStart(event)}
5  onTouchMove={(event) => handleTouchStart(event)}
6  onMouseMove={(event) =>
7    handleTouchStart(event, gameState.isMouseDown)
8  }
9>

Here is the css of the gamefield including the red background effect:

1.game-field {
2  --g1: rgb(170, 0, 0);
3  --g2: rgb(164, 147, 151);
4  display: flex;
5  flex-direction: column;
6  justify-content: space-evenly;
7  animation: background-pan 10s linear infinite;
8  margin: 0 auto;
9  background: linear-gradient(to right, var(--g1), var(--g2), var(--g1));
10  background-size: 200%;
11  width: 100%;
12  height: 100%;
13  // ...
14}
15
16@keyframes background-pan {
17  from {
18    background-position: 0 center;
19  }
20
21  to {
22    background-position: -200% center;
23  }
24}

The animation in the background is a linear gradient at 200% width that pans from left to right.

 

1<div
2  // ...
3  onClick={(event) => handleTouchStart(event)}
4  onTouchMove={(event) => handleTouchStart(event)}
5  onMouseMove={(event) =>
6    handleTouchStart(event, gameState.isMouseDown)
7  }
8>

This part was quite tricky because I wanted the drawing feature to behave more or less the same on desktop and mobile. Initially I was working with event listeners on every cell, but that had some performance drawbacks and also didn't work with touch events as there is no touchEnter event. So I decided to use a single event listener on the gamefield itself and then calculate the cell that was touched. I will explain the logic in more detail in another blogpost. The handleTouchStart covers all events and should probably be renamed 😂.

 


 

The last part is the actual maping of the cells:

1{
2  gameState.grid.map((rows, rowindex) => (
3    <div key={rowindex} className={gameStyles.row} id={`${rowindex}`}>
4      {rows.map((cell, cellIndex) => (
5        <div
6          key={`${rowindex}-${cellIndex}`}
7          id={`${rowindex}-${cellIndex}`}
8          data-row={rowindex}
9          data-cell={cellIndex}
10          className={cn(gameStyles.cell, {
11            [gameStyles.active]: cell
12          })}
13        />
14      ))}
15    </div>
16  ))
17}

I'm still impressed that this part runs as smooth as it does right now. It's rerendering all the cells every 100ms and I did some testing: Right now the calculations roughly need 20-25ms on my machine so there is quite some headroom. In the event listener that I will show another time I use the id of each cell to determine where the user clicked. The rest is just a simple css class that gets toggled on and off.

1.cell {
2  flex-shrink: 0;
3  cursor: pointer;
4  margin: 1px;
5  background: var(--black);
6  width: 16px;
7  height: 16px;
8
9  &:hover {
10    background: var(--black-transparent);
11  }
12}
13
14.active,
15.active:hover {
16  background: transparent;
17}

 


 

And that's it for the first part. I will show all the different components in more detail in the upcoming blogposts. Btw. this was initially all done in one file with more than 400 lines... 🤯

Implementing react-markdown

27.04.2023 - 16:52

Ever since I finished my bootcamp at Le Wagon I wanted to start my own blog. In the beginning I aspired to write everything myself: text editor, database, frontend, etc... That's how my blog-api project came about.

 

This project taught me a lot (testing, for example), but as is often the case with side projects, it got a bit out of hand and I didn't finish it.

 

Since then, the idea has popped up in my head every now and then, but I didn't want to include a heavy blog framework. It should be simple, give me the possibility to write an article at any time with little effort.

 

This has now resulted in the following structure:

 

Loading of blogpost markdown files

I have the following structure in my Next project:

1public
2└── blogposts
3    ├── 27_04_2023_16_52_Implementing_react-markdown.md
4    ├── 27_04_2023_17_40_Another_blog_post.md

The files are named according to the following scheme:
DD_MM_YYYY_HH_MM_ Title_of_the_blog_post.md

 

To automatically load all the .md files, I use the following function:

1export const importAll = (
2  requireContext: __WebpackModuleApi.RequireContext
3) => {
4  return requireContext
5    .keys()
6    .filter((key) => key.match(/\.\//))
7    .map((element) => {
8      const el = element.replace(/\.|\/|md/g, '').split('_')
9      return {
10        date: `${el[0]}.${el[1]}.${el[2]} -\u00A0${el[3]}:${el[4]}`,
11        title: el.slice(5).join(' '),
12        content: requireContext(element)
13      }
14    })
15}

This function returns an array of objects with the following structure:

1;[
2  {
3    date: '27.04.2023 - 16:52',
4    title: 'Implementing react-markdown',
5    content: '...'
6  }
7  // ...
8]

In Blog.tsx I can then call the following:

1const blogPosts = importAll(require.context('~/blogposts', false, /\.md$/))

 

Rendering the markdown files

To render the markdown files I use the react-markdown package. After reading their documentation, the documentation of react-syntax-highlighter and many stackoverflow posts, I came up with the following solution:

1import dynamic from 'next/dynamic'
2import { JetBrains_Mono } from 'next/font/google'
3
4import { importAll } from '@/utils'
5import Markdown from 'react-markdown'
6import { materialDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
7import remarkGfm from 'remark-gfm'
8
9import blogStyles from '@/styles/blog.module.scss'
10
11const SyntaxHighlighter = dynamic(() =>
12  import('react-syntax-highlighter').then((mod) => mod.Prism)
13)
14
15const jetbrainsMono = JetBrains_Mono({
16  weight: '400',
17  subsets: ['latin'],
18  variable: '--jetbrains-mono'
19})
20
21const blogPosts = importAll(require.context('~/blogposts', false, /\.md$/))
22
23export default function Blog() {
24  return (
25    <main className={blogStyles.container}>
26      {blogPosts.map((post, index) => {
27        return (
28          <article key={index}>
29            <span className={blogStyles.header}>
30              <h2>{post.title}</h2>
31              <small>{post.date}</small>
32            </span>
33            <Markdown
34              remarkPlugins={[remarkGfm]}
35              className={blogStyles.content}
36              components={{
37                code({ node, inline, className, children, style, ...props }) {
38                  const match = /language-(\w+)/.exec(className || '')
39                  return !inline && match ? (
40                    <SyntaxHighlighter
41                      style={materialDark}
42                      className={jetbrainsMono.variable}
43                      showLineNumbers={true}
44                      lineNumberStyle={{
45                        minWidth: '3.25em'
46                      }}
47                      customStyle={{
48                        background: 'var(--medium-black)',
49                        boxShadow: '0 0 1px var(--grey)',
50                        padding: '0.25rem 0'
51                      }}
52                      codeTagProps={{
53                        style: {
54                          background: 'var(--medium-black)',
55                          fontFamily: 'var(--jetbrains-mono)'
56                        }
57                      }}
58                      language={match[1]}
59                      {...props}
60                    >
61                      {String(children).replace(/\n$/, '')}
62                    </SyntaxHighlighter>
63                  ) : (
64                    <code {...props} className={blogStyles.code}>
65                      {children}
66                    </code>
67                  )
68                }
69              }}
70            >
71              {`${post.content}`}
72            </Markdown>
73          </article>
74        )
75      })}
76    </main>
77  )
78}

This still needs some refactoring, but it works for now.

 

Let the blogging begin!