|
|
|
import { useWindowDimension } from "hooks/useWindowDimension";
|
|
|
|
import { useRouter } from "next/router";
|
|
|
|
import React, { CSSProperties, useEffect, useRef, useState } from "react";
|
|
|
|
|
|
|
|
import { Image } from "./Image";
|
|
|
|
|
|
|
|
import styles from "./ShapesBackground.module.css";
|
|
|
|
|
|
|
|
const MOBILE_WIDTH = 768;
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
getConfig?: GetShapesConfig;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function ShapesBackground({ getConfig }: Props) {
|
|
|
|
const [config, setConfig] = useState<ShapesConfig>({});
|
|
|
|
const [prevWidth, setPrevWidth] = useState<number>(-1);
|
|
|
|
const [prevRoute, setPrevRoute] = useState<string>("");
|
|
|
|
const { width, height } = useWindowDimension();
|
|
|
|
const shapesContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const containerWidth = shapesContainerRef.current?.offsetWidth;
|
|
|
|
const containerHeight = shapesContainerRef.current?.offsetHeight;
|
|
|
|
|
|
|
|
// In general, rerun getShapesConfig() if the screen size changes from desktop to mobile (or vice versa)
|
|
|
|
if (
|
|
|
|
containerWidth == null ||
|
|
|
|
containerHeight == null ||
|
|
|
|
!(
|
|
|
|
router.asPath === "/" ||
|
|
|
|
router.asPath !== prevRoute ||
|
|
|
|
prevWidth < 0 ||
|
|
|
|
(prevWidth <= MOBILE_WIDTH && MOBILE_WIDTH < width) ||
|
|
|
|
(prevWidth > MOBILE_WIDTH && MOBILE_WIDTH >= width)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setPrevWidth(width);
|
|
|
|
setPrevRoute(router.asPath);
|
|
|
|
setConfig(getConfig?.(containerWidth, containerHeight) ?? {});
|
|
|
|
}, [getConfig, width, height, prevWidth, prevRoute, router.asPath]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={styles.shapesContainer}
|
|
|
|
ref={shapesContainerRef}
|
|
|
|
aria-hidden
|
|
|
|
>
|
|
|
|
{Object.entries(config).map(([type, instances]) =>
|
|
|
|
instances.map((attributes, idx) => (
|
|
|
|
<Shape
|
|
|
|
key={idx.toString() + type}
|
|
|
|
type={type as ShapeType}
|
|
|
|
style={attributes}
|
|
|
|
/>
|
|
|
|
))
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function Shape(props: { type: ShapeType; style: CSSProperties }) {
|
|
|
|
return (
|
|
|
|
<Image
|
|
|
|
src={`/images/shapes/${props.type}.svg`}
|
|
|
|
className={styles.shape}
|
|
|
|
style={props.style}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export type ShapeType =
|
|
|
|
| "asterisk"
|
|
|
|
| "circle"
|
|
|
|
| "cross"
|
|
|
|
| "dots"
|
|
|
|
| "hash"
|
|
|
|
| "plus"
|
|
|
|
| "ring"
|
|
|
|
| "triangle"
|
|
|
|
| "triangleBig"
|
|
|
|
| "waves"
|
|
|
|
| "wavesBig";
|
|
|
|
|
|
|
|
export type ShapesConfig = {
|
|
|
|
[key in ShapeType]?: CSSProperties[];
|
|
|
|
};
|
|
|
|
|
|
|
|
export type GetShapesConfig = (width: number, height: number) => ShapesConfig;
|
|
|
|
|
|
|
|
type ShapeSize = {
|
|
|
|
[key in ShapeType]: {
|
|
|
|
size: "big" | "small";
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
// 0 <= minAngle, maxAngle <= 180
|
|
|
|
minAngle?: number;
|
|
|
|
maxAngle?: number;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const shapeTypes: ShapeType[] = [
|
|
|
|
"asterisk",
|
|
|
|
"circle",
|
|
|
|
"cross",
|
|
|
|
"dots",
|
|
|
|
"hash",
|
|
|
|
"plus",
|
|
|
|
"ring",
|
|
|
|
"triangle",
|
|
|
|
"triangleBig",
|
|
|
|
"waves",
|
|
|
|
"wavesBig",
|
|
|
|
];
|
|
|
|
|
|
|
|
const shapesSizes: ShapeSize = {
|
|
|
|
asterisk: {
|
|
|
|
size: "big",
|
|
|
|
width: 168,
|
|
|
|
height: 168,
|
|
|
|
},
|
|
|
|
circle: {
|
|
|
|
size: "big",
|
|
|
|
width: 132,
|
|
|
|
height: 132,
|
|
|
|
},
|
|
|
|
cross: {
|
|
|
|
size: "big",
|
|
|
|
width: 150,
|
|
|
|
height: 150,
|
|
|
|
},
|
|
|
|
dots: {
|
|
|
|
size: "big",
|
|
|
|
width: 232,
|
|
|
|
height: 250,
|
|
|
|
},
|
|
|
|
hash: {
|
|
|
|
size: "small",
|
|
|
|
width: 60,
|
|
|
|
height: 60,
|
|
|
|
},
|
|
|
|
plus: {
|
|
|
|
size: "small",
|
|
|
|
width: 48,
|
|
|
|
height: 48,
|
|
|
|
},
|
|
|
|
ring: {
|
|
|
|
size: "small",
|
|
|
|
width: 70,
|
|
|
|
height: 70,
|
|
|
|
},
|
|
|
|
triangle: {
|
|
|
|
size: "small",
|
|
|
|
width: 68,
|
|
|
|
height: 68,
|
|
|
|
minAngle: 15,
|
|
|
|
maxAngle: 26,
|
|
|
|
},
|
|
|
|
triangleBig: {
|
|
|
|
size: "big",
|
|
|
|
width: 138,
|
|
|
|
height: 138,
|
|
|
|
minAngle: 15,
|
|
|
|
maxAngle: 26,
|
|
|
|
},
|
|
|
|
waves: {
|
|
|
|
size: "small",
|
|
|
|
width: 102,
|
|
|
|
height: 50,
|
|
|
|
},
|
|
|
|
wavesBig: {
|
|
|
|
size: "big",
|
|
|
|
width: 252,
|
|
|
|
height: 132,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const shapesBySize = {
|
|
|
|
big: shapeTypes.filter((shape) => shapesSizes[shape]["size"] == "big"),
|
|
|
|
small: shapeTypes.filter((shape) => shapesSizes[shape]["size"] == "small"),
|
|
|
|
};
|
|
|
|
|
|
|
|
// Used to generate random shapes in the margins
|
|
|
|
export const defaultGetShapesConfig = ((pageWidth, pageHeight) => {
|
|
|
|
if (window.innerWidth <= MOBILE_WIDTH) {
|
|
|
|
return mobileShapesConfig;
|
|
|
|
}
|
|
|
|
|
|
|
|
const defaultConfig: ShapesConfig = {};
|
|
|
|
const gap = 20;
|
|
|
|
const minBoxWidth = 150;
|
|
|
|
const boxWidth = Math.max(minBoxWidth, (pageWidth - 800 - 2 * gap) / 2);
|
|
|
|
const boxHeight = 400;
|
|
|
|
const shapeGenerationProbability = 0.85;
|
|
|
|
|
|
|
|
for (let y = 0; y + boxHeight <= pageHeight; y += gap + boxHeight) {
|
|
|
|
for (let x = 0; x <= 1; ++x) {
|
|
|
|
if (Math.random() > shapeGenerationProbability) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const size =
|
|
|
|
boxWidth > minBoxWidth && (y == 0 || y + 2 * boxHeight > pageHeight)
|
|
|
|
? "big"
|
|
|
|
: "small";
|
|
|
|
const shape: ShapeType = getRandomShape(size);
|
|
|
|
const verticalOffset = getVerticalOffset(boxHeight, shape);
|
|
|
|
const horizontalOffset = getHorizontalOffset(boxWidth - 2 * gap, shape);
|
|
|
|
const shapeWidth = shapesSizes[shape]["width"];
|
|
|
|
const shapeHeight = shapesSizes[shape]["height"];
|
|
|
|
const angle = getRandomAngle(shape);
|
|
|
|
const colour = getRandomColour();
|
|
|
|
const opacity = getRandomOpacity(colour);
|
|
|
|
|
|
|
|
defaultConfig[shape] ??= [];
|
|
|
|
defaultConfig[shape]?.push({
|
|
|
|
top: `${((y + verticalOffset) / 16).toFixed(5)}rem`,
|
|
|
|
left:
|
|
|
|
x == 0
|
|
|
|
? `${(((horizontalOffset + gap) / window.innerWidth) * 100).toFixed(
|
|
|
|
5
|
|
|
|
)}vw`
|
|
|
|
: "unset",
|
|
|
|
right:
|
|
|
|
x == 1
|
|
|
|
? `${(((horizontalOffset + gap) / window.innerWidth) * 100).toFixed(
|
|
|
|
5
|
|
|
|
)}vw`
|
|
|
|
: "unset",
|
|
|
|
width: `${(shapeWidth / 16).toFixed(5)}rem`,
|
|
|
|
height: `${(shapeHeight / 16).toFixed(5)}rem`,
|
|
|
|
transform: `rotate(${angle}deg)`,
|
|
|
|
filter: `var(--${colour})`,
|
|
|
|
opacity: `${opacity}%`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return defaultConfig;
|
|
|
|
}) as GetShapesConfig;
|
|
|
|
|
|
|
|
function getRandomShape(size: "big" | "small"): ShapeType {
|
|
|
|
const idx = Math.floor(Math.random() * shapesBySize[size].length);
|
|
|
|
return shapesBySize[size][idx];
|
|
|
|
}
|
|
|
|
|
|
|
|
function getVerticalOffset(boxHeight: number, shape: ShapeType): number {
|
|
|
|
const padding = shapesSizes[shape]["height"];
|
|
|
|
return Math.floor(Math.random() * (boxHeight - padding));
|
|
|
|
}
|
|
|
|
|
|
|
|
function getHorizontalOffset(boxWidth: number, shape: ShapeType): number {
|
|
|
|
const padding = shapesSizes[shape]["width"];
|
|
|
|
return shapesSizes[shape]["size"] == "big"
|
|
|
|
? Math.floor(Math.random() * (boxWidth - padding / 2) - padding / 2)
|
|
|
|
: Math.floor(Math.random() * (boxWidth - padding));
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRandomAngle(shape: ShapeType): number {
|
|
|
|
const minAngle = shapesSizes[shape]["minAngle"] ?? 0;
|
|
|
|
const maxAngle = shapesSizes[shape]["maxAngle"] ?? 0;
|
|
|
|
const direction = Math.random() < 0.5 ? 1 : -1;
|
|
|
|
return (
|
|
|
|
(minAngle + Math.floor(Math.random() * (maxAngle - minAngle + 1))) *
|
|
|
|
direction
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRandomColour(): "blue" | "teal" {
|
|
|
|
return Math.random() < 0.7 ? "blue" : "teal";
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRandomOpacity(colour: "blue" | "teal"): number {
|
|
|
|
if (colour === "blue") {
|
|
|
|
return Math.random() < 0.8 ? 20 : 25;
|
|
|
|
} else {
|
|
|
|
return Math.random() < 0.8 ? 25 : 30;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Used for most mobile pages
|
|
|
|
export const mobileShapesConfig = {
|
|
|
|
dots: [
|
|
|
|
{
|
|
|
|
top: "calc(-6rem / 16)",
|
|
|
|
left: "calc(-95rem / 16)",
|
|
|
|
width: "calc(166rem / 16)",
|
|
|
|
height: "calc(150rem / 16)",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
hash: [
|
|
|
|
{
|
|
|
|
top: "calc(88rem / 16)",
|
|
|
|
right: "15vw",
|
|
|
|
width: "calc(40rem / 16)",
|
|
|
|
height: "calc(40rem / 16)",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
triangle: [
|
|
|
|
{
|
|
|
|
top: "calc(20rem / 16)",
|
|
|
|
right: "1vw",
|
|
|
|
width: "calc(45rem / 16)",
|
|
|
|
height: "calc(45rem / 16)",
|
|
|
|
transform: "rotate(17deg)",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|