Aral Balkan

Mastodon icon RSS feed icon

Draw Together

Let me take you step-by-step as I remake Draw Together from scratch.

Draw Together is a little collaborative drawing toy I made with Kitten on Saturday night.

You can play with it in the live embed above, watch the video in this post to learn how to build it, view its source code, and read through a complete break-down of the source code, below.

Update to video: Add the following code to the button elements to make them accessible by allowing the buttons to be differentiated for people using assistive technologies:

aria-label='pixel-${rowIndex}-${columnIndex}'

Source code

The code sets up a real-time collaborative drawing tool on a 20×20 pixel grid where people can click to toggle the colour of each pixel.

Here’s a breakdown of the entire source code, detailing what each section does:

Create 20×20 pixel grid

const rows = new Array(20).fill().map(row => new Array(20).fill(0))
const colours = ['white', 'black']

Creates a 20x20 grid initialised with zeros. Each cell will store an index corresponding to the colours array.

Validation function

const isValid = data => typeof data === 'object' && typeof data.rowIndex === 'number' && typeof data.columnIndex === 'number' && data.rowIndex >= 0 && data.rowIndex <= rows.length-1 && data.columnIndex >= 0 && data.columnIndex <= rows[0].length-1

Checks if the data object has valid rowIndex and columnIndex within grid bounds.

Handle connections and pixel updates

export function onConnect ({ page }) {
  page.on('pixel', data => {
    if (!isValid(data)) return
    rows[data.rowIndex][data.columnIndex]++
    if (rows[data.rowIndex][data.columnIndex] === colours.length) {
      rows[data.rowIndex][data.columnIndex] = 0
    }
    page.everyone.send(kitten.html`
      <${Pixel} rowIndex=${data.rowIndex} columnIndex=${data.columnIndex} pixelColourIndex=${rows[data.rowIndex][data.columnIndex]} />
    `)
  })
}

Listens for pixel updates, validates data, updates the grid, and sends the updated pixel state to everyone connected.

Pixel component

const Pixel = ({ rowIndex, columnIndex, pixelColourIndex }) => kitten.html`
  <button
    id='pixel-${rowIndex}-${columnIndex}'
    aria-label='pixel-${rowIndex}-${columnIndex}'
    name='pixel'
    style='
      background-color: ${colours[pixelColourIndex]};
      top: calc(5% * ${rowIndex});
      left: calc(5% * ${columnIndex});
    '
    connect
    data='{rowIndex: ${rowIndex}, columnIndex: ${columnIndex}}'
    morph
  ></button>
`

Defines a button element representing a pixel, styled according to its colour and position.

Canvas component

const Canvas = () => kitten.html`${
  rows.map((row, rowIndex) => row.map((pixelColourIndex, columnIndex) => kitten.html`
    <${Pixel} rowIndex=${rowIndex} columnIndex=${columnIndex} pixelColourIndex=${pixelColourIndex} />
  `))
}`

Renders the entire grid by mapping over each row and column, creating Pixel components.

Styles component

const Styles = () => kitten.css`
  [name='pixel'] {
    position: fixed;
    width: 5%;
    height: 5%;
    border: 0;
  }
`

Applies CSS styles to the pixel buttons to size and position them correctly.

Main export

export default () => kitten.html`
  <page title='Draw Together'>
  <${Canvas} />
  <${Styles} />
`

Constructs the main page with the canvas and styles.

Like this? Fund us!

Small Technology Foundation is a tiny, independent not-for-profit.

We exist in part thanks to patronage by people like you. If you share our vision and want to support our work, please become a patron or donate to us today and help us continue to exist.