This is a React exercise to create a component which draws a background of little triangles like this:
With random shift, tilt right only:
No random shift, tilt right only:
With random shift, tilt right/left:
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;
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;
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>
...
Any question or suggestions are welcome.
Top comments (0)