diff --git a/src/Components/ColorPicker.tsx b/src/Components/ColorPicker.tsx new file mode 100644 index 0000000..a95b1d5 --- /dev/null +++ b/src/Components/ColorPicker.tsx @@ -0,0 +1,123 @@ +import React, { useRef, useEffect, useState, FC } from 'react'; + +type ColorPickerProps = { + onChange: (color: string) => void; +}; + +export const ColorPicker: FC = ({ onChange }) => { + const canvasRef = useRef(null); + const [context, setContext] = useState(null); + const [dragging, setDragging] = useState(false); + const [cursorPosition, setCursorPosition] = useState<{ + x: number; + y: number; + }>({ x: 0, y: 0 }); + + useEffect(() => { + const canvas = canvasRef.current; + if (canvas) { + const ctx = canvas.getContext('2d'); + if (ctx) { + setContext(ctx); + drawColorWheel(ctx); + } + } + }, []); + + useEffect(() => { + if (context) { + drawCursor(context, cursorPosition); + } + }, [context, cursorPosition]); + + const drawColorWheel = (ctx: CanvasRenderingContext2D) => { + const width = ctx.canvas.width; + const height = ctx.canvas.height; + const centerX = width / 2; + const centerY = height / 2; + const radius = Math.min(centerX, centerY); + + for (let angle = 0; angle <= 360; angle += 1) { + const gradient = ctx.createLinearGradient( + centerX, + centerY, + centerX + radius * Math.cos((angle * Math.PI) / 180), + centerY + radius * Math.sin((angle * Math.PI) / 180) + ); + gradient.addColorStop(0, `hsl(${angle}, 100%, 50%)`); + gradient.addColorStop(1, `hsl(${angle + 1}, 100%, 50%)`); + + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.arc( + centerX, + centerY, + radius, + ((angle - 0.5) * Math.PI) / 180, + ((angle + 0.5) * Math.PI) / 180 + ); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.closePath(); + } + }; + + const drawCursor = ( + ctx: CanvasRenderingContext2D, + position: { x: number; y: number } + ) => { + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + drawColorWheel(ctx); + const radius = 5; + ctx.beginPath(); + ctx.arc(position.x, position.y, radius, 0, 2 * Math.PI); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.closePath(); + }; + + const handleMouseDown = ( + event: React.MouseEvent + ) => { + setDragging(true); + handleMouseMove(event); + }; + + const handleMouseMove = ( + event: React.MouseEvent + ) => { + if (dragging && context) { + const canvas = canvasRef.current; + if (canvas) { + const rect = canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const imageData = context.getImageData(x, y, 1, 1); + const pixel = imageData.data; + const color = rgbToHex(pixel[0], pixel[1], pixel[2]); + onChange(color); + setCursorPosition({ x, y }); + } + } + }; + + const handleMouseUp = () => { + setDragging(false); + }; + + const rgbToHex = (r: number, g: number, b: number): string => { + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + }; + + return ( + + ); +}; diff --git a/src/Components/Counters/ExtraCountersBar.tsx b/src/Components/Counters/ExtraCountersBar.tsx index d63890c..ed04244 100644 --- a/src/Components/Counters/ExtraCountersBar.tsx +++ b/src/Components/Counters/ExtraCountersBar.tsx @@ -10,6 +10,8 @@ import { import { CounterType, Player, Rotation } from '../../Types/Player'; import { RotationDivProps } from '../Buttons/CommanderDamage'; import ExtraCounter from '../Buttons/ExtraCounter'; +import { useEffect, useState } from 'react'; +import { checkContrast } from '../../Utils/checkContrast'; const Container = twc.div((props) => [ 'flex', @@ -31,6 +33,17 @@ type ExtraCountersBarProps = { const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { const { updatePlayer } = usePlayers(); + const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark'); + + useEffect(() => { + const contrast = checkContrast(player.color, '#00000080'); + + if (contrast === 'Fail') { + setIconColor('light'); + } else { + setIconColor('dark'); + } + }, [player.color]); const handleCounterChange = ( updatedCounterTotal: number, @@ -93,7 +106,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { {useCommanderDamage && ( } + Icon={ + + } type={CounterType.CommanderTax} counterTotal={ player.extraCounters?.find( @@ -108,7 +127,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { {Boolean(useCommanderDamage && usePartner) && ( } + Icon={ + + } type={CounterType.PartnerTax} counterTotal={ player.extraCounters?.find( @@ -123,7 +148,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { {usePoison && ( } + Icon={ + + } type={CounterType.Poison} counterTotal={ player.extraCounters?.find((counter) => counter.type === 'poison') @@ -137,7 +168,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { {useEnergy && ( } + Icon={ + + } type={CounterType.Energy} counterTotal={ player.extraCounters?.find((counter) => counter.type === 'energy') @@ -151,7 +188,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { {useExperience && ( } + Icon={ + + } type={CounterType.Experience} counterTotal={ player.extraCounters?.find( diff --git a/src/Components/Player/PlayerMenu.tsx b/src/Components/Player/PlayerMenu.tsx index 4fad7c2..8bc942a 100644 --- a/src/Components/Player/PlayerMenu.tsx +++ b/src/Components/Player/PlayerMenu.tsx @@ -5,7 +5,6 @@ import { theme } from '../../Data/theme'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { usePlayers } from '../../Hooks/usePlayers'; import { useSafeRotate } from '../../Hooks/useSafeRotate'; -import { HexColorPicker } from 'react-colorful'; import { Energy, Exit, @@ -18,6 +17,7 @@ import { } from '../../Icons/generated'; import { Player, Rotation } from '../../Types/Player'; import { RotationDivProps } from '../Buttons/CommanderDamage'; +import { ColorPicker } from '../ColorPicker'; const CheckboxContainer = twc.div``; @@ -66,7 +66,7 @@ const ButtonsSections = twc.div` flex-wrap `; -const ColorPicker = twc.button` +const ColorPickerButton = twc.button` h-[8vmax] w-[8vmax] max-h-12 @@ -158,12 +158,11 @@ const PlayerMenu = ({ > - colorPickerDialogRef.current?.show()} aria-label="Color picker" /> - {/* */} {player.settings.useCommanderDamage && ( + colorPickerDialogRef.current?.close()} >
- + style={{ + height: '80%', + width: '60%', + }} + /> */} +
diff --git a/src/Utils/checkContrast.ts b/src/Utils/checkContrast.ts new file mode 100644 index 0000000..4a39c4f --- /dev/null +++ b/src/Utils/checkContrast.ts @@ -0,0 +1,87 @@ +type RGBA = { + red: number; + green: number; + blue: number; + alpha: number; +}; + +export const hexToRgb = (hex: string): RGBA => { + hex = hex.replace(/^#/, ''); + let alpha = 255; + + if (hex.length === 8) { + alpha = parseInt(hex.slice(6, 8), 16); + hex = hex.substring(0, 6); + } + + if (hex.length === 4) { + alpha = parseInt(hex.slice(3, 4).repeat(2), 16); + hex = hex.substring(0, 3); + } + + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + + const num = parseInt(hex, 16); + const red = num >> 16; + const green = (num >> 8) & 255; + const blue = num & 255; + + return { red, green, blue, alpha }; +}; + +export const luminance = (a: number, b: number) => { + const l1 = Math.max(a, b); + const l2 = Math.min(a, b); + return (l1 + 0.05) / (l2 + 0.05); +}; + +export const rgbContrast = (a: RGBA, b: RGBA) => { + return luminance(relativeLuminance(a), relativeLuminance(b)); +}; + +// calculate the color contrast ratio +export const checkContrast = (hexC1: string, hexC2: string) => { + const color1rgb = hexToRgb(hexC1); + const color2rgb = hexToRgb(hexC2); + + const contrast = rgbContrast(color1rgb, color2rgb); + + if (contrast >= 7) { + return 'AAA'; + } + + if (contrast >= 4.5) { + return 'AA'; + } + + if (contrast >= 3) { + return 'AA Large'; + } + + return 'Fail'; +}; + +// red, green, and blue coefficients +const rc = 0.2126; +const gc = 0.7152; +const bc = 0.0722; +// low-gamma adjust coefficient +const lowc = 1 / 12.92; + +function adjustGamma(_) { + return Math.pow((_ + 0.055) / 1.055, 2.4); +} + +export const relativeLuminance = (rgb: RGBA) => { + const rsrgb = rgb.red / 255; + const gsrgb = rgb.green / 255; + const bsrgb = rgb.blue / 255; + + const r = rsrgb <= 0.03928 ? rsrgb * lowc : adjustGamma(rsrgb); + const g = gsrgb <= 0.03928 ? gsrgb * lowc : adjustGamma(gsrgb); + const b = bsrgb <= 0.03928 ? bsrgb * lowc : adjustGamma(bsrgb); + + return r * rc + g * gc + b * bc; +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 714d343..44b0e56 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -54,6 +54,10 @@ export default { backdrop: 'rgba(0, 0, 0, 0.3)', settings: 'rgba(20, 20, 0, 0.9)', }, + icons: { + dark: '#000000', + light: '#FFFFFF', + }, text: { primary: '#F5F5F5', secondary: '#76A6A5',