servicepoint-tanks/tank-frontend/src/theme.tsx
2024-05-07 13:21:10 +02:00

166 lines
4.6 KiB
TypeScript

import {useStoredObjectState} from './useStoredState.ts';
import {createContext, ReactNode, useContext, useMemo, useRef, useState} from 'react';
export type HSL = {
h: number;
s: number;
l: number;
}
export type HslTheme = {
primary: HSL;
secondary: HSL;
background: HSL;
tertiary: HSL;
}
type Rgba = Uint8ClampedArray;
export type RgbaTheme = {
primary: Rgba;
secondary: Rgba;
background: Rgba;
tertiary: Rgba;
}
// @ts-ignore
const rootStyle = document.querySelector(':root')?.style;
function getRandom(min: number, max: number) {
return min + Math.random() * (max - min);
}
function getRandomHsl(params: {
minHue?: number,
maxHue?: number,
minSaturation?: number,
maxSaturation?: number,
minLightness?: number,
maxLightness?: number,
}): HSL {
const values = {
minHue: 0,
maxHue: 360,
minSaturation: 0,
maxSaturation: 100,
minLightness: 0,
maxLightness: 100,
...params
};
const h = getRandom(values.minHue, values.maxHue);
const s = getRandom(values.minSaturation, values.maxSaturation);
const l = getRandom(values.minLightness, values.maxLightness);
return {h, s, l};
}
export function hslToString({h, s, l}: HSL) {
return `hsl(${h},${s}%,${l}%)`;
}
function angle(a: number) {
return ((a % 360.0) + 360) % 360;
}
export function getRandomTheme(): HslTheme {
const goldenAngle = 180 * (3 - Math.sqrt(5));
const background = getRandomHsl({maxSaturation: 50, minLightness: 10, maxLightness: 30});
const otherColorParams = {
minSaturation: background.s,
maxSaturation: 90,
minLightness: background.l + 20,
maxLightness: 90
};
const primary = getRandomHsl(otherColorParams);
primary.h = angle(-1 * goldenAngle + primary.h);
const secondary = getRandomHsl(otherColorParams);
primary.h = angle(+1 * goldenAngle + primary.h);
const tertiary = getRandomHsl(otherColorParams);
primary.h = angle(+3 * goldenAngle + primary.h);
return {background, primary, secondary, tertiary};
}
const dummyRgbaTheme: RgbaTheme = {
primary: new Uint8ClampedArray([0, 0, 0, 0]),
secondary: new Uint8ClampedArray([0, 0, 0, 0]),
background: new Uint8ClampedArray([0, 0, 0, 0]),
tertiary: new Uint8ClampedArray([0, 0, 0, 0])
};
function hslToRgba(context: CanvasRenderingContext2D, color: HSL) {
context.fillStyle = hslToString(color);
context.fillRect(0, 0, 1, 1);
return context.getImageData(0, 0, 1, 1).data;
}
const HslThemeContext = createContext<null | {
hslTheme: HslTheme,
setHslTheme: (mutator: (oldState: HslTheme) => HslTheme) => void
}>(null);
const RgbaThemeContext = createContext<RgbaTheme>(dummyRgbaTheme);
export function useRgbaTheme() {
return useContext(RgbaThemeContext);
}
export function useHslTheme() {
const context = useContext(HslThemeContext);
if (context === null) {
throw new Error();
}
return context;
}
export function ThemeProvider({children}: {
children?: ReactNode;
}) {
const [rgbaTheme, setRgbaTheme] = useState<RgbaTheme | null>(null);
function applyTheme(theme: HslTheme) {
console.log('apply theme', theme);
rootStyle.setProperty('--color-primary', hslToString(theme.primary));
rootStyle.setProperty('--color-secondary', hslToString(theme.secondary));
rootStyle.setProperty('--color-background', hslToString(theme.background));
}
const [hslTheme, setHslTheme] = useStoredObjectState<HslTheme>('theme', getRandomTheme, {
load: applyTheme,
save: applyTheme
});
const canvasRef = useRef<HTMLCanvasElement>(null);
const canvas = canvasRef.current;
useMemo(() => {
if (!canvas)
return;
const drawContext = canvas.getContext('2d', {
alpha: false,
colorSpace: 'srgb',
willReadFrequently: true
});
if (!drawContext)
throw new Error('could not get draw context');
setRgbaTheme({
background: hslToRgba(drawContext, hslTheme.background),
primary: hslToRgba(drawContext, hslTheme.primary),
secondary: hslToRgba(drawContext, hslTheme.secondary),
tertiary: hslToRgba(drawContext, hslTheme.tertiary),
});
}, [hslTheme, canvas]);
return <HslThemeContext.Provider value={{hslTheme, setHslTheme}}>
<RgbaThemeContext.Provider value={rgbaTheme || dummyRgbaTheme}>
<canvas hidden={true} ref={canvasRef}/>
{children}
</RgbaThemeContext.Provider>;
</HslThemeContext.Provider>;
}