DEV Community

Cover image for React exercise with canvas & 2-D array: background with triangles
Ellis
Ellis

Posted on • Updated on

React exercise with canvas & 2-D array: background with triangles

This is a React exercise to create a component which draws a background of little triangles like this:

With shift, tilt right only:
Image description

No shift, tilt right only:
Image description

With shift, tilt right/left:
Image description

The exercise discovers how to:

  • create a 2 dimensional matrix in js.
  • draw triangles with html canvas element,
  • display the canvas in React.

We will use: useRef, useEffect.

We will also use: ts & styled-components, which are both optional here.

Comments inside the code should hopefully provide sufficient explanation.

From MDN Web Docs: In js, functions are first-class objects, they can have properties and methods just like any other object. What distinguishes them from other objects is that functions can be called.

Step 1: Inside your React project, create a file "matrix.ts"

// ------------------------------------
// This provides a 2 dimensional array.
// Dimensions are x & y.
// Each cell is a string.
// It is implemented as a function object.
// Prototype functions: setValue, getValue.
function Matrix() {
  this.getValue = (x: number, y: number): string => this[`${x}:${y}`];

  this.setValue = (x: number, y: number, value: string) => {
    this[`${x}:${y}`] = value;
  };
}

export default Matrix;
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a new component file "BackgroundWithTriangles.tsx"

import React, { useRef, useEffect } from "react";
import styled from "styled-components";

// @ts-ignore
import Matrix from "misc/matrix";

// SETTINGS:
// Canvas contains many squares, each square contains 2 triangles.
// l=logical, w=width, h=height.
const canvas_lw = 1000; // higher for less pixelation
const canvas_lh = 1000; // higher for less pixelation
const square_lw = 30;
const square_lh = 30;
const squareShift_lx = 4; // horizontal
const squareShift_ly = 4; // vertical
const tilt = 0.5; // 0=left, 0.5=equal, 1=right
const drawSquaresOnly = false;

// THESE THREE MUST ADD UP TO 256, FOR RGB:
const grayMinimum = 180; // higher for lighter.
const colourShift = 3; // 0 for full grayscale.
const grayShift = 256 - grayMinimum - colourShift; // 0+

// ------------------------------------
const Canvas = styled.canvas`
  position: fixed;
  z-index: -1;
  top: 0;
  left: 0;
  width: calc(100vw + 50px); // 50px: compensate for the shifting at the end
  height: calc(100vh + 50px); // 50px: compensate for the shifting at the end
`;

// ------------------------------------
// Output range: 0 .. maxIncl.
const getRandomInt = (maxIncl: number) =>
  Math.floor(Math.random() * (maxIncl + 1));

// Output range: -x/2 .. x/2
const getShiftPositiveOrNegative = (x: number) => getRandomInt(x) - x / 2;

// ------------------------------------
const getRandomGrayishRgb = () => {
  const randomGrayBase = grayMinimum + getRandomInt(grayShift);
  const r = randomGrayBase + getRandomInt(colourShift);
  const g = randomGrayBase + getRandomInt(colourShift);
  const b = randomGrayBase + getRandomInt(colourShift);
  return `rgb(${r},${g},${b})`;
};

// ------------------------------------
// "12:34" --> [12, 34]
const stringToArray = (value: string): number[] =>
  value.split(":").map((s: string) => Number(s));

// [12, 34] --> "12:34"
const arrayToString = (valueX: number, valueY: number): string =>
  `${valueX}:${valueY}`;

// ------------------------------------
const drawTriangle = (
  ctx: any,
  x1: number, y1: number,
  x2: number, y2: number,
  x3: number, y3: number,
  fillStyle: string
) => {
  ctx.beginPath();
  ctx.lineWidth = 0;
  ctx.fillStyle = fillStyle;
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.lineTo(x3, y3);
  ctx.fill();
};

// ------------------------------------
const drawSquare = (
  ctx: any,
  x1: number, y1: number,
  x2: number, y2: number,
  x3: number, y3: number,
  x4: number, y4: number,
  fillStyle: string
) => {
  ctx.beginPath();
  ctx.lineWidth = 0;
  ctx.fillStyle = fillStyle;
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.lineTo(x4, y4);
  ctx.lineTo(x3, y3);
  ctx.closePath();
  ctx.fill();
};

// ------------------------------------
// Two triangles forming a square.
const drawSquareOrTriangles = (
  ctx: any,
  x1: number, y1: number,
  x2: number, y2: number,
  x3: number, y3: number,
  x4: number, y4: number
) => {
  if (drawSquaresOnly) {
    drawSquare(ctx, x1, y1, x2, y2, x3, y3, x4, y4, getRandomGrayishRgb());
    return;
  }

  // Draw two triangles
  if (Math.random() <= tilt) {
    // Tilt right, like: /
    drawTriangle(ctx, x1, y1, x2, y2, x3, y3, getRandomGrayishRgb());
    drawTriangle(ctx, x2, y2, x3, y3, x4, y4, getRandomGrayishRgb());
  } else {
    // Tilt left, like: \
    drawTriangle(ctx, x1, y1, x2, y2, x4, y4, getRandomGrayishRgb());
    drawTriangle(ctx, x1, y1, x3, y3, x4, y4, getRandomGrayishRgb());
  }
};

// ------------------------------------
// x, y: top left corner of the cell, which contain 1 square or 2 triangles.
const drawCell = (matrix: any, ctx: any, x: number, y: number) => {
  // 4 corners of the square
  const x1 = x;
  const y1 = y;
  const x2 = x;
  const y2 = y + square_lh;
  const x3 = x + square_lw;
  const y3 = y;
  const x4 = x + square_lw;
  const y4 = y + square_lh;

  drawSquareOrTriangles(
    ctx,
    // @ts-ignore
    ...stringToArray(matrix.getValue(x1, y1)),
    ...stringToArray(matrix.getValue(x2, y2)),
    ...stringToArray(matrix.getValue(x3, y3)),
    ...stringToArray(matrix.getValue(x4, y4))
  );
};

// ------------------------------------
const createMatrix = (ctx: any) => {
  const matrix = new Matrix();

  // Create a matrix of dots for the squares, with shifts
  for (let x = 0; x <= canvas_lw; x += square_lw)
    for (let y = 0; y <= canvas_lh; y += square_lh) {
      const xWithShift = x + getShiftPositiveOrNegative(squareShift_lx);
      const yWithShift = y + getShiftPositiveOrNegative(squareShift_ly);
      matrix.setValue(x, y, arrayToString(xWithShift, yWithShift));
    }

  // Draw the squares (we need 4 dots for each square)
  for (let x = 0; x <= canvas_lw - square_lw; x += square_lw)
    for (let y = 0; y <= canvas_lh - square_lh; y += square_lh) {
      drawCell(matrix, ctx, x, y);
    }
};

// ------------------------------------
// COMPONENT:
// Draws a window background of squares.
// Each square draws 2 triangles.
// Each triangle has random shifts in: corner positions, and colour.
const BackgroundWithTriangles = () => {
  const ref = useRef(null);

  // ------------------------------------
  useEffect(() => {
    if (ref && ref.current) {
      const canvas: any = ref.current;
      const ctx = canvas.getContext("2d");
      createMatrix(ctx);
    }
  }, [ref?.current]);

  // ------------------------------------
  // Width and height: logical (int), not physical (px).
  return <Canvas ref={ref} width={canvas_lw} height={canvas_lh} />;
};

export default BackgroundWithTriangles;
Enter fullscreen mode Exit fullscreen mode

Step 3: Inside any component, probably a page, as in the following example, use the new component

import { BackgroundWithTriangles } from "components";

...
return (
  <Page title="Outgoing trend">
    <BackgroundWithTriangles />

    <TrendChart>
...
Enter fullscreen mode Exit fullscreen mode

Any question or suggestions are welcome.

Discussion (0)