www-new/components/ShapesBackground.tsx

309 lines
7.4 KiB
TypeScript

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}>
{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)",
},
],
};