DEV Community

Cover image for React & html canvas exercise: page background with triangles
Ellis
Ellis

Posted on • Updated on

React & html canvas exercise: page background with triangles

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

With random shift, tilt right only:
Image description

No random shift, tilt right only:
Image description

With random shift, tilt right/left:
Image description

The exercise discovers how to:

  • draw triangles with html canvas element,
  • display the canvas in React,
  • and also create a 2 dimensional matrix utility object in js (js does not natively support 2 dimensional arrays as such).

React hooks used: useRef, useEffect.

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

Comments inside the code hopefully provide sufficient explanation.

From MDN Web Docs: In js, functions are objects, they can have properties and methods like any other object, except: 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.
// Example usage:
//   matrix = new Matrix();
//   matrix.setValue(3, 4, 1234);
//   const value = matrix.getValue(3, 4);
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"

/* eslint-disable @typescript-eslint/no-explicit-any */
import { useRef, useEffect } from "react";
import styled from "styled-components";

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 = 25;
const square_lh = 25;
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 = 0; // higher for lighter.
const colourShift = 30; // 0 for full grayscale.
const grayShift = 256 - grayMinimum - colourShift; // 0+

const colourPalette = [
  "#628078",
  "#819993",
  "#4e6660",
  "#ff3a22",
  "#a1b7b1",
  "#c7af6b",
  "#a4893d",
  "#aaa",
  "#ccc",
  "#eeeeee",
  "#555555",
];

const useColourPalette = true;

// ------------------------------------
const Canvas = styled.canvas`
  position: fixed;
  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
  opacity: 0.4; // if using a colour palette
`;

// ------------------------------------
// 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})`;
};

// ------------------------------------
const getColour = () => {
  if (useColourPalette) {
    const index = getRandomInt(colourPalette.length - 1);
    return colourPalette[index];
  }

  return getRandomGrayishRgb();
};

// ------------------------------------
// "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: CanvasRenderingContext2D,
  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: CanvasRenderingContext2D,
  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: CanvasRenderingContext2D,
  [x1, y1, x2, y2, x3, y3, x4, y4]: number[]
) => {
  if (drawSquaresOnly) {
    drawSquare(ctx, x1, y1, x2, y2, x3, y3, x4, y4, getColour());
    return;
  }

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

// ------------------------------------
// x, y: top left corner of the cell, which contain 1 square or 2 triangles.
const drawCell = (
  matrix: any,
  ctx: CanvasRenderingContext2D,
  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, [
    ...stringToArray(matrix.getValue(x1, y1)),
    ...stringToArray(matrix.getValue(x2, y2)),
    ...stringToArray(matrix.getValue(x3, y3)),
    ...stringToArray(matrix.getValue(x4, y4)),
  ]);
};

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

  // 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<HTMLCanvasElement>(null);

  // ------------------------------------
  useEffect(() => {
    if (ref && ref.current) {
      const canvas: HTMLCanvasElement = ref.current;
      const ctx = canvas.getContext("2d");

      ctx && createMatrix(ctx);
    }
  }, []);

  // ------------------------------------
  // 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";

const Content = styled.div`
  z-index: 1;
  background-color: #f0f0f0;
`;

return (
  <Page>
    <BackgroundWithTriangles />

    <Content>Home page</Content>
...
Enter fullscreen mode Exit fullscreen mode

Any question or suggestions are welcome.

Top comments (0)