diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 46aaa0d699b..52ee5161b16 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -532,6 +532,7 @@ GLOBAL_LIST_INIT(all_volume_channels, list( #define COLORMATE_TINT 1 #define COLORMATE_HSV 2 #define COLORMATE_MATRIX 3 +#define COLORMATE_MATRIX_AUTO 4 #define DEFAULT_COLORMATRIX list(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) diff --git a/code/game/machinery/painter_vr.dm b/code/game/machinery/painter_vr.dm index 70e8ce9dbdd..d8fe09ba776 100644 --- a/code/game/machinery/painter_vr.dm +++ b/code/game/machinery/painter_vr.dm @@ -213,7 +213,7 @@ switch(active_mode) if(COLORMATE_TINT) color_to_use = activecolor - if(COLORMATE_MATRIX) + if(COLORMATE_MATRIX, COLORMATE_MATRIX_AUTO) color_to_use = rgb_construct_color_matrix( text2num(color_matrix_last[1]), text2num(color_matrix_last[2]), @@ -244,7 +244,7 @@ if(inserted) //sanity var/list/cm switch(active_mode) - if(COLORMATE_MATRIX) + if(COLORMATE_MATRIX, COLORMATE_MATRIX_AUTO) cm = rgb_construct_color_matrix( text2num(color_matrix_last[1]), text2num(color_matrix_last[2]), diff --git a/code/modules/tgui_input/matrix.dm b/code/modules/tgui_input/matrix.dm index 2ac97c8ff8a..f5def9d0764 100644 --- a/code/modules/tgui_input/matrix.dm +++ b/code/modules/tgui_input/matrix.dm @@ -14,7 +14,7 @@ * * default - The default (or current) value, shown as a placeholder. Users can press refresh with this. * * timeout - The timeout of the matrix input, after which the modal will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_colormatrix(mob/user, message, title = "Matrix Recolor", atom/movable/target, list/default = DEFAULT_COLORMATRIX, matrix_only = FALSE, timeout = 10 MINUTES, ui_state = GLOB.tgui_always_state) +/proc/tgui_input_colormatrix(mob/user, message, title = "Matrix Recolor", atom/movable/target, list/default = DEFAULT_COLORMATRIX, matrix_only = FALSE, timeout = 30 MINUTES, ui_state = GLOB.tgui_always_state) if (!user) user = usr if (!istype(user)) @@ -182,6 +182,8 @@ return switch(action) if("switch_modes") + if(matrix_only && active_mode < 3) + return FALSE active_mode = text2num(params["mode"]) return TRUE if("choose_color") @@ -239,7 +241,7 @@ switch(active_mode) if(COLORMATE_TINT) color_to_use = activecolor - if(COLORMATE_MATRIX) + if(COLORMATE_MATRIX, COLORMATE_MATRIX_AUTO) color_to_use = rgb_construct_color_matrix( text2num(color_matrix_last[1]), text2num(color_matrix_last[2]), @@ -276,7 +278,7 @@ if(target) //sanity var/list/cm switch(active_mode) - if(COLORMATE_MATRIX) + if(COLORMATE_MATRIX, COLORMATE_MATRIX_AUTO) cm = rgb_construct_color_matrix( text2num(color_matrix_last[1]), text2num(color_matrix_last[2]), diff --git a/tgui/packages/tgui/interfaces/ColorMate/ColorMateColor.tsx b/tgui/packages/tgui/interfaces/ColorMate/ColorMateColor.tsx deleted file mode 100644 index 15ea4481960..00000000000 --- a/tgui/packages/tgui/interfaces/ColorMate/ColorMateColor.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useBackend } from 'tgui/backend'; -import { Button, Slider, Table } from 'tgui-core/components'; - -import type { Data } from './types'; - -export const ColorMateTint = (props) => { - const { act } = useBackend(); - - return ( - - ); -}; - -export const ColorMateHSV = (props) => { - const { act, data } = useBackend(); - - const { buildhue, buildsat, buildval } = data; - return ( - - -
Hue:
- - value.toFixed()} - onChange={(e, value: number) => - act('set_hue', { - buildhue: value, - }) - } - /> - -
- -
Saturation:
- - value.toFixed(2)} - onChange={(e, value: number) => - act('set_sat', { - buildsat: value, - }) - } - /> - -
- -
Value:
- - value.toFixed(2)} - onChange={(e, value: number) => - act('set_val', { - buildval: value, - }) - } - /> - -
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/ColorMate/ColorMateMatrix.tsx b/tgui/packages/tgui/interfaces/ColorMate/ColorMateMatrix.tsx deleted file mode 100644 index eeb730f2eb3..00000000000 --- a/tgui/packages/tgui/interfaces/ColorMate/ColorMateMatrix.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { useBackend } from 'tgui/backend'; -import { - Box, - Icon, - Input, - LabeledList, - NumberInput, - Table, -} from 'tgui-core/components'; - -import type { Data } from './types'; - -export const ColorMateMatrix = (props) => { - const { act, data } = useBackend(); - - const { matrixcolors } = data; - - return ( - <> - - - - RR: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 1, - value, - }) - } - /> - - - GR: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 4, - value, - }) - } - /> - - - BR: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 7, - value, - }) - } - /> - - - - - RG: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 2, - value, - }) - } - /> - - - GG: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 5, - value, - }) - } - /> - - - BG: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 8, - value, - }) - } - /> - - - - - RB: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 3, - value, - }) - } - /> - - - GB: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 6, - value, - }) - } - /> - - - BB: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 9, - value, - }) - } - /> - - - - - CR: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 10, - value, - }) - } - /> - - - CG: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 11, - value, - }) - } - /> - - - CB: - value.toFixed(2)} - onChange={(value: number) => - act('set_matrix_color', { - color: 12, - value, - }) - } - /> - - - - RG means red will become - this much green. -
- CR means this much red - will be added. -
-
- - - - act('set_matrix_string', { value })} - /> - - - - - ); -}; diff --git a/tgui/packages/tgui/interfaces/ColorMate/Helpers/ConfigField.tsx b/tgui/packages/tgui/interfaces/ColorMate/Helpers/ConfigField.tsx new file mode 100644 index 00000000000..da732f34d46 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ColorMate/Helpers/ConfigField.tsx @@ -0,0 +1,20 @@ +import { useBackend } from 'tgui/backend'; +import { Input, LabeledList } from 'tgui-core/components'; +import type { Data } from '../types'; + +export const ConfigField = (props) => { + const { act, data } = useBackend(); + const { matrixcolors } = data; + + return ( + + + act('set_matrix_string', { value })} + /> + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ColorMate/Helpers/ImageCanvas.tsx b/tgui/packages/tgui/interfaces/ColorMate/Helpers/ImageCanvas.tsx new file mode 100644 index 00000000000..8f744db897e --- /dev/null +++ b/tgui/packages/tgui/interfaces/ColorMate/Helpers/ImageCanvas.tsx @@ -0,0 +1,90 @@ +import type React from 'react'; +import { useEffect, useRef } from 'react'; +import type { ColorUpdate } from '../types'; + +export function ColorPickerCanvas(props: { + imageData: string | null; + onPick: ColorUpdate; + isMatrix: boolean; +}) { + const { imageData, onPick, isMatrix } = props; + const canvasRef = useRef(null); + + const CANVAS_WIDTH = 475; + const CANVAS_HEIGHT = 475; + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas || !imageData) return; + + const ctx = canvas.getContext('2d'); + const img = new Image(); + img.src = `data:image/jpeg;base64,${imageData}`; + + img.onload = () => { + canvas.width = CANVAS_WIDTH; + canvas.height = CANVAS_HEIGHT; + + ctx?.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + const imgAspect = img.width / img.height; + const canvasAspect = CANVAS_WIDTH / CANVAS_HEIGHT; + + let drawWidth = CANVAS_WIDTH; + let drawHeight = CANVAS_HEIGHT; + + if (imgAspect > canvasAspect) { + drawWidth = CANVAS_WIDTH; + drawHeight = CANVAS_WIDTH / imgAspect; + } else { + drawHeight = CANVAS_HEIGHT; + drawWidth = CANVAS_HEIGHT * imgAspect; + } + + const offsetX = (CANVAS_WIDTH - drawWidth) / 2; + const offsetY = (CANVAS_HEIGHT - drawHeight) / 2; + if (ctx) { + ctx.imageSmoothingEnabled = false; + ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); + } + }; + }, [imageData]); + + const handleClick = (e: React.MouseEvent) => { + if (!isMatrix) return; + const canvas = canvasRef.current; + if (!canvas) return; + + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const x = Math.floor((e.clientX - rect.left) * scaleX); + const y = Math.floor((e.clientY - rect.top) * scaleY); + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const pixel = ctx.getImageData(x, y, 1, 1).data; + const hex = `#${[pixel[0], pixel[1], pixel[2]] + .map((c) => c.toString(16).padStart(2, '0')) + .join('')}`; + + onPick(hex); + }; + + return ( + + ); +} diff --git a/tgui/packages/tgui/interfaces/ColorMate/Helpers/MatrixColorBox.tsx b/tgui/packages/tgui/interfaces/ColorMate/Helpers/MatrixColorBox.tsx new file mode 100644 index 00000000000..a75145df8a3 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ColorMate/Helpers/MatrixColorBox.tsx @@ -0,0 +1,71 @@ +import { type HsvaColor, hexToHsva, hsvaToHex } from 'common/colorpicker'; +import { useEffect, useState } from 'react'; +import { Box, Floating } from 'tgui-core/components'; +import { ColorSelector } from '../../ColorPickerModal'; + +export const ColorMatrixColorBox = (props: { + selectedColor: string; + onSelectedColor: (value: string) => void; +}) => { + const { selectedColor, onSelectedColor } = props; + + const [selectedPreset, setSelectedPreset] = useState( + undefined, + ); + const [currentColor, setCurrentColor] = useState( + hexToHsva(selectedColor), + ); + const [initialColor, setInitialColor] = useState(selectedColor); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + if (!isOpen) { + setInitialColor(selectedColor); + setCurrentColor(hexToHsva(selectedColor)); + } + }, [isOpen, selectedColor]); + + const handleSetColor = ( + value: HsvaColor | ((prev: HsvaColor) => HsvaColor), + ) => { + const newColor = typeof value === 'function' ? value(currentColor) : value; + setCurrentColor(newColor); + onSelectedColor(hsvaToHex(newColor)); + }; + + const pixelSize = 20; + const parentSize = `${pixelSize}px`; + const childSize = `${pixelSize - 4}px`; + + return ( + + } + > + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMateColor.tsx b/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMateColor.tsx new file mode 100644 index 00000000000..ee954764d1d --- /dev/null +++ b/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMateColor.tsx @@ -0,0 +1,89 @@ +import { useBackend } from 'tgui/backend'; +import { Button, Slider, Stack } from 'tgui-core/components'; +import type { Data } from '../types'; + +export const ColorMateTint = (props) => { + const { act } = useBackend(); + + return ( + + ); +}; + +export const ColorMateHSV = (props) => { + const { act, data } = useBackend(); + + const { buildhue, buildsat, buildval } = data; + return ( + + + + + Hue: + + + value.toFixed()} + onChange={(e, value: number) => + act('set_hue', { + buildhue: value, + }) + } + /> + + + + + + + Saturation: + + + value.toFixed(2)} + onChange={(e, value: number) => + act('set_sat', { + buildsat: value, + }) + } + /> + + + + + + + Value: + + + value.toFixed(2)} + onChange={(e, value: number) => + act('set_val', { + buildval: value, + }) + } + /> + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMateMatrix.tsx b/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMateMatrix.tsx new file mode 100644 index 00000000000..f86af78dfe9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMateMatrix.tsx @@ -0,0 +1,60 @@ +import { useBackend } from 'tgui/backend'; +import { Icon, NumberInput, Stack } from 'tgui-core/components'; +import { MATRIX_COLUMS } from '../constants'; +import { ConfigField } from '../Helpers/ConfigField'; +import type { Data } from '../types'; + +export const ColorMateMatrix = (props) => { + const { act, data } = useBackend(); + const { matrixcolors } = data; + + return ( + + + + {MATRIX_COLUMS.map((column, colIndex) => ( + + + {column.map(({ label, key, color }) => ( + + + {label}: + + value.toFixed(2)} + onChange={(value: number) => + act('set_matrix_color', { color: color, value }) + } + /> + + + + ))} + + + ))} + + + + RG means red will + become this much green. + + + CR means this much + red will be added. + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMatrixSolver.tsx b/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMatrixSolver.tsx new file mode 100644 index 00000000000..abf9f832847 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMatrixSolver.tsx @@ -0,0 +1,192 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, Input, Section, Stack } from 'tgui-core/components'; +import { computeMatrixFromPairs, isValidHex } from '../functions'; +import { ColorMatrixColorBox } from '../Helpers/MatrixColorBox'; +import type { + ColorPair, + ColorUpdate, + Data, + MatrixColors, + SelectedId, +} from '../types'; + +export const ColorMateMatrixSolver = (props: { + activeID: SelectedId; + onActiveId: React.Dispatch>; + colorPairs: ColorPair[]; + onColorPairs: React.Dispatch>; + onPick: ColorUpdate; +}) => { + const { act } = useBackend(); + const { activeID, onActiveId, colorPairs, onColorPairs, onPick } = props; + + function handleColorUpdate( + newColor: string, + type: string, + index: number, + ): void { + if (!isValidHex(newColor)) { + return; + } + onPick(newColor, type, index); + } + + function toggleDripper(index: number, type: 'input' | 'output') { + if (activeID.id === index && activeID.type === type) { + onActiveId({ id: null, type: null }); + } else { + onActiveId({ id: index, type: type }); + } + } + + function removePair(index: number) { + const newPairs = [...colorPairs]; + newPairs.splice(index, 1); + onColorPairs(newPairs); + if (activeID.id !== index) return; + onActiveId({ id: null, type: null }); + } + + function calculateColor() { + try { + const matrix = computeMatrixFromPairs(colorPairs); + + const newMatrixcolors: MatrixColors = { + rr: matrix[0][0], + rg: matrix[0][1], + rb: matrix[0][2], + + gr: matrix[1][0], + gg: matrix[1][1], + gb: matrix[1][2], + + br: matrix[2][0], + bg: matrix[2][1], + bb: matrix[2][2], + + cr: matrix[0][3], + cg: matrix[1][3], + cb: matrix[2][3], + }; + + const ourMatrix = Object.values(newMatrixcolors) + .map((v) => v.toFixed(2)) + .toString(); + + act('set_matrix_string', { value: ourMatrix }); + } catch (err) { + console.log(`Matrix computation failed: ${err.message}`); + } + } + + function updateColor(color: string, type: string, index: number) { + handleColorUpdate(color, type, index); + } + + return ( +
+ + {colorPairs.map((colorPair, index) => ( + + + + {`${index + 1}: `} + Source + + + + updateColor(value, 'input', index) + } + /> + + + + + {colorPairs.length < 20 && ( + + + + )} + + )} + {index > 0 && ( + + removePair(index)} + /> + + )} + + + ))} + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/ColorMate/constants.ts b/tgui/packages/tgui/interfaces/ColorMate/constants.ts new file mode 100644 index 00000000000..498c7874679 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ColorMate/constants.ts @@ -0,0 +1,22 @@ +export const MATRIX_COLUMS = [ + [ + { label: 'RR', key: 'rr', color: 1 }, + { label: 'GR', key: 'gr', color: 4 }, + { label: 'BR', key: 'br', color: 7 }, + ], + [ + { label: 'RG', key: 'rg', color: 2 }, + { label: 'GG', key: 'gg', color: 5 }, + { label: 'BG', key: 'bg', color: 8 }, + ], + [ + { label: 'RB', key: 'rb', color: 3 }, + { label: 'GB', key: 'gb', color: 6 }, + { label: 'BB', key: 'bb', color: 9 }, + ], + [ + { label: 'CR', key: 'cr', color: 10 }, + { label: 'CG', key: 'cg', color: 11 }, + { label: 'CB', key: 'cb', color: 12 }, + ], +]; diff --git a/tgui/packages/tgui/interfaces/ColorMate/functions.ts b/tgui/packages/tgui/interfaces/ColorMate/functions.ts new file mode 100644 index 00000000000..e6fb81f53ff --- /dev/null +++ b/tgui/packages/tgui/interfaces/ColorMate/functions.ts @@ -0,0 +1,128 @@ +export function isValidHex(hex: string): boolean { + return /^#[0-9A-Fa-f]{6}$/.test(hex); +} + +export function computeMatrixFromPairs( + pairs: { input: string; output: string }[], +): number[][] { + const identityColors = ['#ff0000', '#00ff00', '#0000ff', '#ffffff']; + + const workingPairs = [...pairs]; + + for (const idColor of identityColors) { + const alreadyUsed = workingPairs.some( + (p) => p.input.toLowerCase() === idColor, + ); + if (!alreadyUsed) { + workingPairs.push({ + input: idColor, + output: idColor, + }); + } + } + + const rgbIn = workingPairs.map((p) => hexToRgb(p.input)); + const rgbOut = workingPairs.map((p) => hexToRgb(p.output)); + + const matrix: number[][] = []; + + for (let channel = 0; channel < 3; channel++) { + let attempts = 0; + let weights: number[] = []; + + while (attempts < 5) { + try { + const a = rgbIn.map((rgb) => [rgb[0], rgb[1], rgb[2], 1]); + const b = rgbOut.map((rgb) => rgb[channel]); + + weights = leastSquares(a, b); + + if (!weights.every((w) => w >= -10 && w <= 10)) { + throw new Error('Computed weights out of range'); + } + + break; + } catch (e) { + const idx = rgbOut.length - 1; + rgbOut[idx] = rgbOut[idx].map( + (val) => val + Math.random() * 0.01 - 0.005, + ); + attempts++; + } + } + + if (weights.length !== 4) { + throw new Error( + `Matrix computation failed for channel ${channel} after ${attempts} attempts`, + ); + } + + matrix.push(weights); + } + + return matrix; +} + +function hexToRgb(hex: string): number[] { + const clean = hex.replace('#', '').padEnd(6, '0'); + const r = parseInt(clean.slice(0, 2), 16) / 255; + const g = parseInt(clean.slice(2, 4), 16) / 255; + const b = parseInt(clean.slice(4, 6), 16) / 255; + return [r, g, b]; +} + +function transpose(matrix: number[][]): number[][] { + return matrix[0].map((_, i) => matrix.map((row) => row[i])); +} + +function multiply(a: number[][], b: number[][]): number[][] { + const result: number[][] = Array(a.length) + .fill(0) + .map(() => Array(b[0].length).fill(0)); + for (let i = 0; i < a.length; i++) { + for (let j = 0; j < b[0].length; j++) { + for (let k = 0; k < b.length; k++) { + result[i][j] += a[i][k] * b[k][j]; + } + } + } + return result; +} + +function inverse(matrix: number[][]): number[][] { + const size = matrix.length; + const augmented = matrix.map((row, i) => [ + ...row, + ...Array(size) + .fill(0) + .map((_, j) => (i === j ? 1 : 0)), + ]); + + for (let i = 0; i < size; i++) { + const diag = augmented[i][i]; + if (diag === 0) { + throw new Error('Singular matrix'); + } + for (let j = 0; j < size * 2; j++) augmented[i][j] /= diag; + for (let k = 0; k < size; k++) { + if (k === i) continue; + const factor = augmented[k][i]; + for (let j = 0; j < size * 2; j++) { + augmented[k][j] -= factor * augmented[i][j]; + } + } + } + return augmented.map((row) => row.slice(size)); +} + +function leastSquares(a: number[][], b: number[]): number[] { + const AT = transpose(a); + const ATa = multiply(AT, a); + const ATb = multiply( + AT, + b.map((v) => [v]), + ); + const ATainv = inverse(ATa); + const result = multiply(ATainv, ATb); + return result.map((r) => r[0]); +} diff --git a/tgui/packages/tgui/interfaces/ColorMate/index.tsx b/tgui/packages/tgui/interfaces/ColorMate/index.tsx index 1f116dd6479..734d42d78c5 100644 --- a/tgui/packages/tgui/interfaces/ColorMate/index.tsx +++ b/tgui/packages/tgui/interfaces/ColorMate/index.tsx @@ -1,23 +1,30 @@ +import { useState } from 'react'; import { useBackend } from 'tgui/backend'; import { Window } from 'tgui/layouts'; import { Box, Button, - Image, NoticeBox, Section, Stack, Table, Tabs, } from 'tgui-core/components'; - -import { ColorMateHSV, ColorMateTint } from './ColorMateColor'; -import { ColorMateMatrix } from './ColorMateMatrix'; +import { ColorPickerCanvas } from './Helpers/ImageCanvas'; +import { ColorMateHSV, ColorMateTint } from './MatrixTabs/ColorMateColor'; +import { ColorMateMatrix } from './MatrixTabs/ColorMateMatrix'; +import { ColorMateMatrixSolver } from './MatrixTabs/ColorMatrixSolver'; import type { Data } from './types'; export const ColorMate = (props) => { const { act, data } = useBackend(); + const [colorPairs, setColorPairs] = useState([ + { input: '#ffffff', output: '#000000' }, + ]); + + const [activeID, setActiveId] = useState({ id: null, type: null }); + const { activemode, temp, @@ -34,14 +41,53 @@ export const ColorMate = (props) => { tab[1] = ; tab[2] = ; tab[3] = ; + tab[4] = ( + + ); - const height = 720 + (matrix_only ? -20 : 0) + (message ? 20 : 0); + const height = + 750 + (matrix_only ? -20 : 0) + (message ? 20 : 0) + (temp ? 20 : 0); + + function handleColorUpdate( + hexCol: string, + mode?: string, + index?: number, + ): void { + if (activemode !== 4) return; + + const usedIndex = index ?? activeID.id; + + if (usedIndex === null || usedIndex >= colorPairs.length) return; + + const usedMode = mode ?? activeID.type; + + if (!usedMode) return; + + if(!mode && !index) { + setActiveId({ id: null, type: null }); + } + + setColorPairs((prev) => { + const newPairs = [...prev]; + newPairs[usedIndex] = { + ...newPairs[usedIndex], + [usedMode]: hexCol, + }; + return newPairs; + }); + } return (
- + {!!temp && ( {temp} @@ -50,59 +96,73 @@ export const ColorMate = (props) => { {message} - + {item_name ? ( - <> - - -
-
Item:
- -
-
- -
-
Preview:
- -
-
-
- {!matrix_only && ( + + + + +
+ + Item: + + + + +
+
+ +
+ + Preview: + + + + +
+
+
+
+ - - act('switch_modes', { - mode: 1, - }) - } - > - Tint coloring (Simple) - - - act('switch_modes', { - mode: 2, - }) - } - > - HSV coloring (Normal) - + {!matrix_only && ( + <> + + act('switch_modes', { + mode: 1, + }) + } + > + Tint coloring (Simple) + + + act('switch_modes', { + mode: 2, + }) + } + > + HSV coloring (Normal) + + + )} { > Matrix coloring (Advanced) + + act('switch_modes', { + mode: 4, + }) + } + > + Matrix coloring (Automatic) + - )} -
Coloring: {item_name}
- - - act('paint')} - > - Paint - - act('clear')} - > - Clear - - act('drop')} - > - Eject - - - - {tab[activemode] || Error} - -
- +
+ Coloring: {item_name} + + + + + + act('paint')} + > + Paint + + + + act('clear')} + > + Clear + + + + act('drop')} + > + Eject + + + + + + {tab[activemode] || Error} + + + +
) : (
No item inserted.
)} diff --git a/tgui/packages/tgui/interfaces/ColorMate/types.ts b/tgui/packages/tgui/interfaces/ColorMate/types.ts index c6490907547..cda596a8324 100644 --- a/tgui/packages/tgui/interfaces/ColorMate/types.ts +++ b/tgui/packages/tgui/interfaces/ColorMate/types.ts @@ -1,19 +1,6 @@ export type Data = { activemode: number; - matrixcolors: { - rr: number; - rg: number; - rb: number; - gr: number; - gg: number; - gb: number; - br: number; - bg: number; - bb: number; - cr: number; - cg: number; - cb: number; - }; + matrixcolors: MatrixColors; buildhue: number; buildsat: number; buildval: number; @@ -25,3 +12,23 @@ export type Data = { title?: string; matrix_only?: number; }; + +export type MatrixColors = { + rr: number; + rg: number; + rb: number; + gr: number; + gg: number; + gb: number; + br: number; + bg: number; + bb: number; + cr: number; + cg: number; + cb: number; +}; + +export type ColorPair = { input: string; output: string }; +export type SelectedId = { id: number | null; type: 'input' | 'output' | null }; + +export type ColorUpdate = (hex: string, mode?: string, index?: number) => void; diff --git a/tgui/packages/tgui/interfaces/ColorPickerModal.tsx b/tgui/packages/tgui/interfaces/ColorPickerModal.tsx index 3b108d880ec..0fc73cd07df 100644 --- a/tgui/packages/tgui/interfaces/ColorPickerModal.tsx +++ b/tgui/packages/tgui/interfaces/ColorPickerModal.tsx @@ -49,7 +49,7 @@ interface ColorPickerData { timeout: number; title: string; default_color: string; - presets: string; + presets?: string; } type ColorPickerModalProps = Record; @@ -61,7 +61,7 @@ export const ColorPickerModal: React.FC = () => { message, autofocus, default_color = '#000000', - presets = '', + presets, } = data; let { title } = data; @@ -105,23 +105,26 @@ export const ColorPickerModal: React.FC = () => { undefined, ); - const ourPresets = presets - .replaceAll('#', '') - .replace(/(^;)|(;$)/g, '') - .split(';'); - while (ourPresets.length < 20) { - ourPresets.push('FFFFFF'); + let presetList; + if (presets) { + const ourPresets = presets + .replaceAll('#', '') + .replace(/(^;)|(;$)/g, '') + .split(';'); + while (ourPresets.length < 20) { + ourPresets.push('FFFFFF'); + } + presetList = ourPresets.reduce( + (input, entry, index) => { + if (index < 10) { + return [[...input[0], entry], input[1]]; + } else { + return [input[0], [...input[1], entry]]; + } + }, + [[], []], + ); } - const presetList = ourPresets.reduce( - (input, entry, index) => { - if (index < 10) { - return [[...input[0], entry], input[1]]; - } else { - return [input[0], [...input[1], entry]]; - } - }, - [[], []], - ); return ( = () => { interface ColorPresetsProps { setColor: (color: HsvaColor) => void; setShowPresets: (show: boolean) => void; - presetList: string[][]; + presetList?: string[][]; selectedPreset: number | undefined; onSelectedPreset: React.Dispatch>; - allowEditing: boolean; - onAllowEditing: React.Dispatch>; + allowEditing?: boolean; + onAllowEditing?: React.Dispatch>; } const ColorPresets: React.FC = React.memo( @@ -221,7 +224,7 @@ const ColorPresets: React.FC = React.memo( ))}
- {presetList.map((row, index) => ( + {presetList?.map((row, index) => ( {row.map((entry, i) => ( @@ -251,14 +254,16 @@ const ColorPresets: React.FC = React.memo( ))}
-