Shapes Background #164
feat/shapes-background
into main
2 years ago
@ -0,0 +1,20 @@ |
||||
.shapesContainer { |
||||
position: absolute; |
||||
overflow: hidden; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
z-index: -10; |
||||
} |
||||
|
||||
.shape { |
||||
--blue: invert(53%) sepia(80%) saturate(4689%) hue-rotate(189deg) |
||||
brightness(92%) contrast(93%); |
||||
--teal: invert(76%) sepia(39%) saturate(578%) hue-rotate(110deg) |
||||
brightness(91%) contrast(88%); |
||||
|
||||
position: absolute; |
||||
filter: var(--blue); |
||||
opacity: 20%; |
||||
} |
@ -0,0 +1,308 @@ |
||||
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, |
||||
}, |
||||
a3thakra marked this conversation as resolved
|
||||
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)", |
||||
}, |
||||
], |
||||
}; |
After Width: | Height: | Size: 836 B |
After Width: | Height: | Size: 425 B |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 365 B |
After Width: | Height: | Size: 227 B |
After Width: | Height: | Size: 190 B |
After Width: | Height: | Size: 220 B |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.7 KiB |
Why is it not just
triangle
? Same for waves.triangleBig
is a different SVG file from the smallertriangle
. Scalingtriangle.svg
up to be really big caused the lines to be too thick, sotriangleBig.svg
has thinner lines which allow it to be scaled up without looking out of place. Same withwaves
/wavesBig
.