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.

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

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

Okay