use gauss elimination for color matrix (#19190)

* use gauss elimination for color matrix

* .

* Update functions.ts

* Update functions.ts

* some more improvements

* .

* .

* .

* ugh

---------

Co-authored-by: Cameron Lennox <killer65311@gmail.com>
This commit is contained in:
Kashargul
2026-02-17 09:36:18 -05:00
committed by GitHub
co-authored by Cameron Lennox
parent 3303d1617b
commit 6c5f04de76
6 changed files with 142 additions and 127 deletions
+7 -7
View File
@@ -626,19 +626,19 @@ ADMIN_VERB(cmd_admin_add_freeform_ai_law, R_FUN, "Add Custom AI law", "Adds a cu
command_announcement.Announce("Ion storm detected near the [station_name()]. Please check all AI-controlled equipment for errors.", "Anomaly Alert", new_sound = 'sound/AI/ionstorm.ogg')
feedback_add_details("admin_verb","IONC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
ADMIN_VERB_AND_CONTEXT_MENU(cmd_admin_rejuvenate, R_ADMIN|R_FUN|R_MOD, "Rejuvenate", "Fully restores the target mob.", ADMIN_CATEGORY_GAME, mob/living/traget_mob as mob in GLOB.mob_list)
if(!traget_mob)
ADMIN_VERB_AND_CONTEXT_MENU(cmd_admin_rejuvenate, R_ADMIN|R_FUN|R_MOD, "Rejuvenate", "Fully restores the target mob.", ADMIN_CATEGORY_GAME, mob/living/target_mob as mob in GLOB.mob_list)
if(!target_mob)
return
if(!istype(traget_mob))
if(!istype(target_mob))
tgui_alert_async(user, "Cannot revive a ghost")
return
if(CONFIG_GET(flag/allow_admin_rev))
traget_mob.revive()
target_mob.revive()
log_admin("[key_name(user)] healed / revived [key_name(traget_mob)]")
var/msg = span_danger("Admin [key_name_admin(user)] healed / revived [ADMIN_LOOKUPFLW(traget_mob)]!")
log_admin("[key_name(user)] healed / revived [key_name(target_mob)]")
var/msg = span_danger("Admin [key_name_admin(user)] healed / revived [ADMIN_LOOKUPFLW(target_mob)]!")
message_admins(msg)
admin_ticket_log(traget_mob, msg)
admin_ticket_log(target_mob, msg)
else
tgui_alert_async(user, "Admin revive disabled")
feedback_add_details("admin_verb","REJU") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
@@ -111,6 +111,8 @@
sleep(rand(3,5))
if(!length(neighbors))
break
if(QDELETED(src)) // we sleep, might get deleted!
return
spread_to(pick(neighbors))
// We shouldn't have spawned if the controller doesn't exist.
@@ -36,7 +36,7 @@ Controlled by the player_tips subsystem under code/controllers/subsystems/player
if(!(target_mob.client?.prefs?.read_preference(/datum/preference/toggle/player_tips)))
return
if(target_mob.key && !(target_mob.key in HasReceived))
to_chat(target_mob, span_warning("You have periodic player tips enabled. You may turn them off at any time with the Toggle Receiving Player Tips verb in Preferences, or in character set up under the OOC tab!\n Player tips appear every 45-75 minutes."))
to_chat(target_mob, span_warning("You have periodic player tips enabled. You may turn them off at any time with the Toggle Receiving Player Tips verb in Preferences, or in character set up under the OOC tab!\nPlayer tips appear every 45-75 minutes."))
HasReceived.Add(target_mob.key)
to_chat(target_mob, span_notice("[GLOB.is_valid_url.Replace(last_tip, span_linkify("$1"))]"))
@@ -2,13 +2,7 @@ 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';
import type { ColorPair, ColorUpdate, Data, SelectedId } from '../types';
export const ColorMateMatrixSolver = (props: {
activeID: SelectedId;
@@ -51,28 +45,19 @@ export const ColorMateMatrixSolver = (props: {
try {
const matrix = computeMatrixFromPairs(colorPairs);
const newMatrixcolors: MatrixColors = {
rr: matrix[0][0],
rg: matrix[0][1],
rb: matrix[0][2],
const parts: number[] = [];
gr: matrix[1][0],
gg: matrix[1][1],
gb: matrix[1][2],
for (let col = 0; col < 3; col++) {
for (let row = 0; row < 3; row++) {
parts.push(Math.round(matrix[row][col] * 100) / 100);
}
}
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();
for (let row = 0; row < 3; row++) {
parts.push(Math.round(matrix[row][3] * 100) / 100);
}
const ourMatrix = parts.toString();
act('set_matrix_string', { value: ourMatrix });
} catch (err) {
console.log(`Matrix computation failed: ${err.message}`);
@@ -122,7 +107,35 @@ export const ColorMateMatrixSolver = (props: {
/>
</Stack.Item>
<Stack.Item>
<Box color="label">{`==>`}</Box>
<Button
icon="arrow-right"
onClick={() =>
handleColorUpdate(colorPair.input, 'output', index)
}
/>
</Stack.Item>
<Stack.Item>
<Box color="label">{`==`}</Box>
</Stack.Item>
<Stack.Item>
<Button
icon="arrow-right-arrow-left"
onClick={() => {
handleColorUpdate(colorPair.input, 'output', index);
handleColorUpdate(colorPair.output, 'input', index);
}}
/>
</Stack.Item>
<Stack.Item>
<Box color="label">{`=>`}</Box>
</Stack.Item>
<Stack.Item>
<Button
icon="arrow-left"
onClick={() =>
handleColorUpdate(colorPair.output, 'input', index)
}
/>
</Stack.Item>
<Stack.Item>
<Box>Target</Box>
@@ -164,7 +177,7 @@ export const ColorMateMatrixSolver = (props: {
onClick={() =>
onColorPairs([
...colorPairs,
{ input: '#ffffff', output: '#000000' },
{ input: '#ffffff', output: '#ffffff' },
])
}
>
@@ -5,56 +5,23 @@ export function isValidHex(hex: string): boolean {
export function computeMatrixFromPairs(
pairs: { input: string; output: string }[],
): number[][] {
const identityColors = ['#ff0000', '#00ff00', '#0000ff', '#ffffff'];
const rgbIn = pairs.map((p) => hexToRgb255(p.input));
const rgbOut = pairs.map((p) => hexToRgb255(p.output));
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,
});
}
if (rgbIn.length === 0) {
throw new Error('At least one color pair is required');
}
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[] = [];
const a = rgbIn.map((rgb) => [rgb[0], rgb[1], rgb[2], 1]);
const b = rgbOut.map((rgb) => rgb[channel]);
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++;
}
}
const weights = leastSquares(a, b);
if (weights.length !== 4) {
throw new Error(
`Matrix computation failed for channel ${channel} after ${attempts} attempts`,
);
throw new Error(`Matrix computation failed for channel ${channel}`);
}
matrix.push(weights);
@@ -63,66 +30,99 @@ export function computeMatrixFromPairs(
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;
function hexToRgb255(hex: string): number[] {
const clean = hex.replace('#', '').padStart(6, '0');
const r = parseInt(clean.slice(0, 2), 16);
const g = parseInt(clean.slice(2, 4), 16);
const b = parseInt(clean.slice(4, 6), 16);
return [r, g, b];
}
function transpose(matrix: number[][]): number[][] {
return matrix[0].map((_, i) => matrix.map((row) => row[i]));
}
function gaussElim(a: number[][], b: number[]): number[] {
const n = a.length;
const m = a[0].length;
const aug = a.map((row, i) => [...row, b[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];
let rank = 0;
for (let col = 0; col < m && rank < n; col++) {
let best = rank;
for (let row = rank + 1; row < n; row++) {
if (Math.abs(aug[row][col]) > Math.abs(aug[best][col])) {
best = row;
}
}
}
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');
if (Math.abs(aug[best][col]) < 1e-12) {
continue;
}
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];
[aug[rank], aug[best]] = [aug[best], aug[rank]];
for (let row = rank + 1; row < n; row++) {
const factor = aug[row][col] / aug[rank][col];
for (let j = col; j <= m; j++) {
aug[row][j] -= factor * aug[rank][j];
}
}
rank++;
}
return augmented.map((row) => row.slice(size));
const x = new Array(m).fill(0);
for (let i = Math.min(rank, m) - 1; i >= 0; i--) {
if (Math.abs(aug[i][i]) < 1e-12) continue;
let sum = aug[i][m];
for (let j = i + 1; j < m; j++) {
sum -= aug[i][j] * x[j];
}
x[i] = sum / aug[i][i];
}
return x;
}
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 rows = a.length;
const cols = a[0].length;
const ATa: number[][] = Array.from({ length: cols }, () =>
new Array(cols).fill(0),
);
const ATainv = inverse(ATa);
const result = multiply(ATainv, ATb);
return result.map((r) => r[0]);
const ATb = new Array(cols).fill(0);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < cols; j++) {
for (let k = 0; k < rows; k++) {
ATa[i][j] += a[k][i] * a[k][j];
}
}
for (let k = 0; k < rows; k++) {
ATb[i] += a[k][i] * b[k];
}
}
return gaussElim(ATa, ATb);
}
export function transformColor(
r: number,
g: number,
b: number,
matrix: number[][],
): number[] {
const outR = clamp(
matrix[0][0] * r + matrix[0][1] * g + matrix[0][2] * b + matrix[0][3],
);
const outG = clamp(
matrix[1][0] * r + matrix[1][1] * g + matrix[1][2] * b + matrix[1][3],
);
const outB = clamp(
matrix[2][0] * r + matrix[2][1] * g + matrix[2][2] * b + matrix[2][3],
);
return [outR, outG, outB];
}
function clamp(v: number): number {
return Math.max(0, Math.min(255, Math.round(v)));
}
@@ -20,7 +20,7 @@ export const ColorMate = (props) => {
const { act, data } = useBackend<Data>();
const [colorPairs, setColorPairs] = useState([
{ input: '#ffffff', output: '#000000' },
{ input: '#ffffff', output: '#ffffff' },
]);
const [activeID, setActiveId] = useState({ id: null, type: null });
@@ -190,7 +190,7 @@ export const ColorMate = (props) => {
<Stack.Item align="center">Coloring: {item_name}</Stack.Item>
<Stack.Item grow>
<Stack fill mt={1}>
<Stack.Item width="33%">
<Stack.Item width="25%">
<Stack vertical>
<Stack.Item>
<Button.Confirm
@@ -227,7 +227,7 @@ export const ColorMate = (props) => {
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item width="66%">
<Stack.Item grow>
{tab[activemode] || <Box textColor="red">Error</Box>}
</Stack.Item>
</Stack>