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)