unified ThemeProvider

This commit is contained in:
Vinzenz Schroeter 2024-05-07 12:06:44 +02:00
parent e8238c6ea7
commit 40bb1ec28a
4 changed files with 77 additions and 55 deletions

View file

@ -9,39 +9,37 @@ import Row from './components/Row.tsx';
import Scoreboard from './Scoreboard.tsx'; import Scoreboard from './Scoreboard.tsx';
import Button from './components/Button.tsx'; import Button from './components/Button.tsx';
import MapChooser from './MapChooser.tsx'; import MapChooser from './MapChooser.tsx';
import {getRandomTheme, RgbaThemeProvider, ThemeContext, useStoredTheme} from './theme.tsx'; import {ThemeProvider} from './theme.tsx';
import './App.css'; import './App.css';
import ThemeChooser from './ThemeChooser.tsx';
export default function App() { export default function App() {
const [theme, setTheme] = useStoredTheme();
const [name, setName] = useState<string | null>(null); const [name, setName] = useState<string | null>(null);
return <ThemeContext.Provider value={theme}> return <ThemeProvider>
<RgbaThemeProvider> <Column className="flex-grow">
<Column className="flex-grow">
<ClientScreen player={name}/> <ClientScreen player={name}/>
<Row> <Row>
<h1 className="flex-grow">CCCB-Tanks!</h1> <h1 className="flex-grow">CCCB-Tanks!</h1>
<MapChooser/> <MapChooser/>
<Button text="☼ change colors" onClick={() => setTheme(_ => getRandomTheme())}/> <ThemeChooser/>
<Button <Button
onClick={() => window.open('https://github.com/kaesaecracker/cccb-tanks-cs', '_blank')?.focus()} onClick={() => window.open('https://github.com/kaesaecracker/cccb-tanks-cs', '_blank')?.focus()}
text="⌂ source"/> text="⌂ source"/>
{name !== '' && {name !== '' &&
<Button onClick={() => setName(_ => '')} text="∩ logout"/>} <Button onClick={() => setName(_ => '')} text="∩ logout"/>}
</Row> </Row>
{name || <JoinForm onDone={name => setName(_ => name)}/>} {name || <JoinForm onDone={name => setName(_ => name)}/>}
<Row className="GadgetRows"> <Row className="GadgetRows">
{name && <Controls player={name}/>} {name && <Controls player={name}/>}
{name && <PlayerInfo player={name}/>} {name && <PlayerInfo player={name}/>}
<Scoreboard/> <Scoreboard/>
</Row> </Row>
</Column> </Column>
</RgbaThemeProvider> </ThemeProvider>;
</ThemeContext.Provider>;
} }

View file

@ -0,0 +1,11 @@
import Button from './components/Button.tsx';
import {getRandomTheme, useHslTheme} from './theme.tsx';
export default function ThemeChooser({}: {}) {
const {setHslTheme} = useHslTheme();
return <>
<Button
text="☼ change colors"
onClick={() => setHslTheme(_ => getRandomTheme())}/>
</>;
}

View file

@ -1,6 +1,6 @@
import {useEffect, useRef} from 'react'; import {useEffect, useRef} from 'react';
import './PixelGridCanvas.css'; import './PixelGridCanvas.css';
import {useRgbaThemeContext} from '../theme.tsx'; import {useRgbaTheme} from '../theme.tsx';
const pixelsPerRow = 352; const pixelsPerRow = 352;
const pixelsPerCol = 160; const pixelsPerCol = 160;
@ -101,7 +101,7 @@ function drawPixelsToCanvas(
export default function PixelGridCanvas({pixels}: { export default function PixelGridCanvas({pixels}: {
readonly pixels: Uint8ClampedArray; readonly pixels: Uint8ClampedArray;
}) { }) {
const theme = useRgbaThemeContext(); const theme = useRgbaTheme();
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => { useEffect(() => {

View file

@ -1,13 +1,13 @@
import {useStoredObjectState} from './useStoredState.ts'; import {useStoredObjectState} from './useStoredState.ts';
import {createContext, ReactNode, useContext, useEffect, useRef, useState} from 'react'; import {createContext, ReactNode, useContext, useEffect, useRef, useState} from 'react';
export type HSL = { type HSL = {
h: number; h: number;
s: number; s: number;
l: number; l: number;
} }
export type Theme = { export type HslTheme = {
primary: HSL; primary: HSL;
secondary: HSL; secondary: HSL;
background: HSL; background: HSL;
@ -44,7 +44,7 @@ function getRandomHsl(params: {
return {h, s, l}; return {h, s, l};
} }
export function hslToString({h, s, l}: HSL) { function hslToString({h, s, l}: HSL) {
return `hsl(${h},${s}%,${l}%)`; return `hsl(${h},${s}%,${l}%)`;
} }
@ -52,7 +52,7 @@ function angle(a: number) {
return ((a % 360.0) + 360) % 360; return ((a % 360.0) + 360) % 360;
} }
export function getRandomTheme(): Theme { export function getRandomTheme(): HslTheme {
const goldenAngle = 180 * (3 - Math.sqrt(5)); const goldenAngle = 180 * (3 - Math.sqrt(5));
const background = getRandomHsl({maxSaturation: 50, minLightness: 10, maxLightness: 30}); const background = getRandomHsl({maxSaturation: 50, minLightness: 10, maxLightness: 30});
@ -76,22 +76,13 @@ export function getRandomTheme(): Theme {
return {background, primary, secondary, tertiary}; return {background, primary, secondary, tertiary};
} }
function applyTheme(theme: Theme) { function applyTheme(theme: HslTheme) {
console.log('apply theme', theme); console.log('apply theme', theme);
rootStyle.setProperty('--color-primary', hslToString(theme.primary)); rootStyle.setProperty('--color-primary', hslToString(theme.primary));
rootStyle.setProperty('--color-secondary', hslToString(theme.secondary)); rootStyle.setProperty('--color-secondary', hslToString(theme.secondary));
rootStyle.setProperty('--color-background', hslToString(theme.background)); rootStyle.setProperty('--color-background', hslToString(theme.background));
} }
export function useStoredTheme() {
return useStoredObjectState<Theme>('theme', getRandomTheme, {
load: applyTheme,
save: applyTheme
});
}
export const ThemeContext = createContext<Theme>(getRandomTheme());
type Rgba = Uint8ClampedArray; type Rgba = Uint8ClampedArray;
export type RgbaTheme = { export type RgbaTheme = {
@ -108,20 +99,40 @@ const dummyRgbaTheme: RgbaTheme = {
tertiary: new Uint8ClampedArray([0, 0, 0, 0]) tertiary: new Uint8ClampedArray([0, 0, 0, 0])
}; };
const RgbaThemeContext = createContext<RgbaTheme>(dummyRgbaTheme); function hslToRgba(context: CanvasRenderingContext2D, color: HSL) {
function normalizeColor(context: CanvasRenderingContext2D, color: HSL) {
context.fillStyle = hslToString(color); context.fillStyle = hslToString(color);
context.fillRect(0, 0, 1, 1); context.fillRect(0, 0, 1, 1);
return context.getImageData(0, 0, 1, 1).data; return context.getImageData(0, 0, 1, 1).data;
} }
export function useRgbaThemeContext() {
const HslThemeContext = createContext<null | {
hslTheme: HslTheme,
setHslTheme: (mutator: (oldState: HslTheme) => HslTheme) => void
}>(null);
const RgbaThemeContext = createContext<RgbaTheme>(dummyRgbaTheme);
export function useRgbaTheme() {
return useContext(RgbaThemeContext); return useContext(RgbaThemeContext);
} }
export function RgbaThemeProvider({children}: { children: ReactNode }) { export function useHslTheme() {
const hslTheme = useContext(ThemeContext); const context = useContext(HslThemeContext);
if (context === null) {
throw new Error();
}
return context;
}
export function ThemeProvider({children}: {
children?: ReactNode;
}) {
const [hslTheme, setHslTheme] = useStoredObjectState<HslTheme>('theme', getRandomTheme, {
load: applyTheme,
save: applyTheme
});
const [rgbaTheme, setRgbaTheme] = useState<RgbaTheme | null>(null); const [rgbaTheme, setRgbaTheme] = useState<RgbaTheme | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
@ -139,15 +150,17 @@ export function RgbaThemeProvider({children}: { children: ReactNode }) {
throw new Error('could not get draw context'); throw new Error('could not get draw context');
setRgbaTheme({ setRgbaTheme({
background: normalizeColor(drawContext, hslTheme.background), background: hslToRgba(drawContext, hslTheme.background),
primary: normalizeColor(drawContext, hslTheme.primary), primary: hslToRgba(drawContext, hslTheme.primary),
secondary: normalizeColor(drawContext, hslTheme.secondary), secondary: hslToRgba(drawContext, hslTheme.secondary),
tertiary: normalizeColor(drawContext, hslTheme.tertiary), tertiary: hslToRgba(drawContext, hslTheme.tertiary),
}); });
}, [hslTheme, canvasRef.current]); }, [hslTheme, canvasRef.current]);
return <RgbaThemeContext.Provider value={rgbaTheme || dummyRgbaTheme}> return <HslThemeContext.Provider value={{hslTheme, setHslTheme}}>
<canvas hidden={true} ref={canvasRef}/> <RgbaThemeContext.Provider value={rgbaTheme || dummyRgbaTheme}>
{children} <canvas hidden={true} ref={canvasRef}/>
</RgbaThemeContext.Provider>; {children}
</RgbaThemeContext.Provider>;
</HslThemeContext.Provider>;
} }