From b40c006d8dacd858e95b96f70dd5522f977b9e31 Mon Sep 17 00:00:00 2001
From: Kashargul <144968721+Kashargul@users.noreply.github.com>
Date: Fri, 3 Oct 2025 13:26:50 +0200
Subject: [PATCH] colorsolver (#18575)
* colorsolverWIP
.
implement a color box
proper handling
adjust math
update the UI
style
that
.
.
.
make this universal for future use
.
* some adjustments
* no longer needed
* Update tgui/packages/tgui/styles/interfaces/ColorMatrixEditor.scss
Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com>
* Update tgui/packages/tgui/interfaces/ColorMate/Helpers/MatrixColorBox.tsx
Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com>
---------
Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com>
---
code/__defines/misc.dm | 1 +
code/game/machinery/painter_vr.dm | 4 +-
code/modules/tgui_input/matrix.dm | 8 +-
.../interfaces/ColorMate/ColorMateColor.tsx | 78 -----
.../interfaces/ColorMate/ColorMateMatrix.tsx | 254 -----------------
.../ColorMate/Helpers/ConfigField.tsx | 20 ++
.../ColorMate/Helpers/ImageCanvas.tsx | 90 ++++++
.../ColorMate/Helpers/MatrixColorBox.tsx | 71 +++++
.../ColorMate/MatrixTabs/ColorMateColor.tsx | 89 ++++++
.../ColorMate/MatrixTabs/ColorMateMatrix.tsx | 60 ++++
.../MatrixTabs/ColorMatrixSolver.tsx | 192 +++++++++++++
.../tgui/interfaces/ColorMate/constants.ts | 22 ++
.../tgui/interfaces/ColorMate/functions.ts | 128 +++++++++
.../tgui/interfaces/ColorMate/index.tsx | 269 ++++++++++++------
.../tgui/interfaces/ColorMate/types.ts | 35 ++-
.../tgui/interfaces/ColorPickerModal.tsx | 73 ++---
.../styles/interfaces/ColorMatrixEditor.scss | 14 +
tgui/packages/tgui/styles/main.scss | 1 +
18 files changed, 930 insertions(+), 479 deletions(-)
delete mode 100644 tgui/packages/tgui/interfaces/ColorMate/ColorMateColor.tsx
delete mode 100644 tgui/packages/tgui/interfaces/ColorMate/ColorMateMatrix.tsx
create mode 100644 tgui/packages/tgui/interfaces/ColorMate/Helpers/ConfigField.tsx
create mode 100644 tgui/packages/tgui/interfaces/ColorMate/Helpers/ImageCanvas.tsx
create mode 100644 tgui/packages/tgui/interfaces/ColorMate/Helpers/MatrixColorBox.tsx
create mode 100644 tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMateColor.tsx
create mode 100644 tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMateMatrix.tsx
create mode 100644 tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMatrixSolver.tsx
create mode 100644 tgui/packages/tgui/interfaces/ColorMate/constants.ts
create mode 100644 tgui/packages/tgui/interfaces/ColorMate/functions.ts
create mode 100644 tgui/packages/tgui/styles/interfaces/ColorMatrixEditor.scss
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)
+ }
+ />
+
+
+
+
+ handleColorUpdate(value, 'input', index)}
+ width="80px"
+ />
+
+
+ {`==>`}
+
+
+ Target
+
+
+
+ updateColor(value, 'output', index)
+ }
+ />
+
+
+
+
+
+ handleColorUpdate(value, 'output', index)
+ }
+ width="80px"
+ />
+
+ {index === 0 && (
+ <>
+
+
+
+ {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 ? (
- <>
-
- {!matrix_only && (
+
+
+
+
+
-
- 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(
))}
-