Shapes Background (#164)
We have decided to use randomly-generated shapes for most of the pages, instead of hard-coding the shapes backgrounds. Closes #25 Co-authored-by: Amy <a258wang@uwaterloo.ca> Reviewed-on: www/www-new#164 Reviewed-by: Aditya Thakral <a3thakra@csclub.uwaterloo.ca> Co-authored-by: Amy <a258wang@csclub.uwaterloo.ca> Co-committed-by: Amy <a258wang@csclub.uwaterloo.ca>fix-md-font
@ -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, |
||||
}, |
||||
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 |