DEV Community

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

Posted on • Edited on

2 1

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)

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay