mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-09 16:12:17 +00:00
[MIRROR] colorsolver (#11773)
Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
32b62f335b
commit
9709af12a9
@@ -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)
|
||||
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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 (
|
||||
<Button fluid icon="paint-brush" onClick={() => act('choose_color')}>
|
||||
Select new color
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColorMateHSV = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const { buildhue, buildsat, buildval } = data;
|
||||
return (
|
||||
<Table>
|
||||
<Table.Row>
|
||||
<center>Hue:</center>
|
||||
<Table.Cell width="85%">
|
||||
<Slider
|
||||
tickWhileDragging
|
||||
minValue={0}
|
||||
maxValue={360}
|
||||
step={1}
|
||||
value={buildhue}
|
||||
format={(value: number) => value.toFixed()}
|
||||
onChange={(e, value: number) =>
|
||||
act('set_hue', {
|
||||
buildhue: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<center>Saturation:</center>
|
||||
<Table.Cell>
|
||||
<Slider
|
||||
tickWhileDragging
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={buildsat}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(e, value: number) =>
|
||||
act('set_sat', {
|
||||
buildsat: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<center>Value:</center>
|
||||
<Table.Cell>
|
||||
<Slider
|
||||
tickWhileDragging
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={buildval}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(e, value: number) =>
|
||||
act('set_val', {
|
||||
buildval: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@@ -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<Data>();
|
||||
|
||||
const { matrixcolors } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table>
|
||||
<Table.Cell>
|
||||
<Table.Row>
|
||||
RR:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.rr}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 1,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
GR:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.gr}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 4,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
BR:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.br}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 7,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Table.Row>
|
||||
RG:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.rg}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 2,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
GG:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.gg}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 5,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
BG:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.bg}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 8,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Table.Row>
|
||||
RB:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.rb}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 3,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
GB:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.gb}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 6,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
BB:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.bb}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 9,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Table.Row>
|
||||
CR:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.cr}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 10,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
CG:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.cg}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 11,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
CB:
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors.cb}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', {
|
||||
color: 12,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Row>
|
||||
</Table.Cell>
|
||||
<Table.Cell width="40%">
|
||||
<Icon name="question-circle" color="blue" /> RG means red will become
|
||||
this much green.
|
||||
<br />
|
||||
<Icon name="question-circle" color="blue" /> CR means this much red
|
||||
will be added.
|
||||
</Table.Cell>
|
||||
</Table>
|
||||
<Box mt={3}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Config">
|
||||
<Input
|
||||
fluid
|
||||
value={Object.values(matrixcolors).toString()}
|
||||
onBlur={(value: string) => act('set_matrix_string', { value })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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<Data>();
|
||||
const { matrixcolors } = data;
|
||||
|
||||
return (
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Config">
|
||||
<Input
|
||||
fluid
|
||||
value={Object.values(matrixcolors).toString()}
|
||||
onBlur={(value: string) => act('set_matrix_string', { value })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
);
|
||||
};
|
||||
@@ -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<HTMLCanvasElement>(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<HTMLCanvasElement>) => {
|
||||
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 (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
cursor: isMatrix ? 'crosshair' : 'default',
|
||||
imageRendering: 'pixelated',
|
||||
display: 'block',
|
||||
margin: '0 auto',
|
||||
}}
|
||||
width={CANVAS_WIDTH}
|
||||
height={CANVAS_HEIGHT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<number | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [currentColor, setCurrentColor] = useState<HsvaColor>(
|
||||
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 (
|
||||
<Floating
|
||||
onOpenChange={setIsOpen}
|
||||
placement="bottom-end"
|
||||
contentClasses="MatrixEditor__Floating"
|
||||
content={
|
||||
<ColorSelector
|
||||
color={currentColor}
|
||||
setColor={handleSetColor}
|
||||
defaultColor={initialColor}
|
||||
selectedPreset={selectedPreset}
|
||||
onSelectedPreset={setSelectedPreset}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
border: '2px solid white',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
width={parentSize}
|
||||
height={parentSize}
|
||||
>
|
||||
<Box
|
||||
backgroundColor={selectedColor}
|
||||
width={childSize}
|
||||
height={childSize}
|
||||
/>
|
||||
</Box>
|
||||
</Floating>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Button fluid icon="paint-brush" onClick={() => act('choose_color')}>
|
||||
Select new color
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColorMateHSV = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const { buildhue, buildsat, buildval } = data;
|
||||
return (
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<Stack align="center">
|
||||
<Stack.Item textAlign="center" basis="15%">
|
||||
Hue:
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Slider
|
||||
tickWhileDragging
|
||||
minValue={0}
|
||||
maxValue={360}
|
||||
step={1}
|
||||
value={buildhue}
|
||||
format={(value: number) => value.toFixed()}
|
||||
onChange={(e, value: number) =>
|
||||
act('set_hue', {
|
||||
buildhue: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack>
|
||||
<Stack.Item textAlign="center" basis="15%">
|
||||
Saturation:
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Slider
|
||||
tickWhileDragging
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={buildsat}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(e, value: number) =>
|
||||
act('set_sat', {
|
||||
buildsat: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack>
|
||||
<Stack.Item textAlign="center" basis="15%">
|
||||
Value:
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Slider
|
||||
tickWhileDragging
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={buildval}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(e, value: number) =>
|
||||
act('set_val', {
|
||||
buildval: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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<Data>();
|
||||
const { matrixcolors } = data;
|
||||
|
||||
return (
|
||||
<Stack fill vertical>
|
||||
<Stack.Item grow>
|
||||
<Stack>
|
||||
{MATRIX_COLUMS.map((column, colIndex) => (
|
||||
<Stack.Item key={`col-${colIndex}`}>
|
||||
<Stack vertical>
|
||||
{column.map(({ label, key, color }) => (
|
||||
<Stack.Item ml="20px" key={label}>
|
||||
<Stack align="center">
|
||||
<Stack.Item>{label}:</Stack.Item>
|
||||
<Stack.Item>
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={-10}
|
||||
maxValue={10}
|
||||
step={0.01}
|
||||
value={matrixcolors[key]}
|
||||
format={(value: number) => value.toFixed(2)}
|
||||
onChange={(value: number) =>
|
||||
act('set_matrix_color', { color: color, value })
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
))}
|
||||
<Stack.Item ml="20px" width="40%">
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<Icon name="question-circle" color="blue" /> RG means red will
|
||||
become this much green.
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Icon name="question-circle" color="blue" /> CR means this much
|
||||
red will be added.
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ConfigField />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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<React.SetStateAction<SelectedId>>;
|
||||
colorPairs: ColorPair[];
|
||||
onColorPairs: React.Dispatch<React.SetStateAction<ColorPair[]>>;
|
||||
onPick: ColorUpdate;
|
||||
}) => {
|
||||
const { act } = useBackend<Data>();
|
||||
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 (
|
||||
<Section fill scrollable>
|
||||
<Stack vertical>
|
||||
{colorPairs.map((colorPair, index) => (
|
||||
<Stack.Item key={index}>
|
||||
<Stack align="center">
|
||||
<Stack.Item>
|
||||
<Box
|
||||
color="label"
|
||||
inline
|
||||
preserveWhitespace
|
||||
>{`${index + 1}: `}</Box>
|
||||
<Box inline>Source</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ColorMatrixColorBox
|
||||
selectedColor={colorPair.input}
|
||||
onSelectedColor={(value) =>
|
||||
updateColor(value, 'input', index)
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
selected={activeID.id === index && activeID.type === 'input'}
|
||||
onClick={() => toggleDripper(index, 'input')}
|
||||
tooltip="Pick color from image"
|
||||
icon="eye-dropper"
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Input
|
||||
fluid
|
||||
value={colorPair.input}
|
||||
onChange={(value) => handleColorUpdate(value, 'input', index)}
|
||||
width="80px"
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color="label">{`==>`}</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box>Target</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ColorMatrixColorBox
|
||||
selectedColor={colorPair.output}
|
||||
onSelectedColor={(value) =>
|
||||
updateColor(value, 'output', index)
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
selected={activeID.id === index && activeID.type === 'output'}
|
||||
onClick={() => toggleDripper(index, 'output')}
|
||||
tooltip="Pick color from image"
|
||||
icon="eye-dropper"
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Input
|
||||
fluid
|
||||
value={colorPair.output}
|
||||
onChange={(value) =>
|
||||
handleColorUpdate(value, 'output', index)
|
||||
}
|
||||
width="80px"
|
||||
/>
|
||||
</Stack.Item>
|
||||
{index === 0 && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button onClick={() => calculateColor()}>Calculate</Button>
|
||||
</Stack.Item>
|
||||
{colorPairs.length < 20 && (
|
||||
<Stack.Item>
|
||||
<Button
|
||||
onClick={() =>
|
||||
onColorPairs([
|
||||
...colorPairs,
|
||||
{ input: '#ffffff', output: '#000000' },
|
||||
])
|
||||
}
|
||||
>
|
||||
Add Pair
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{index > 0 && (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
icon="trash"
|
||||
color="red"
|
||||
onClick={() => removePair(index)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
22
tgui/packages/tgui/interfaces/ColorMate/constants.ts
Normal file
22
tgui/packages/tgui/interfaces/ColorMate/constants.ts
Normal file
@@ -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 },
|
||||
],
|
||||
];
|
||||
128
tgui/packages/tgui/interfaces/ColorMate/functions.ts
Normal file
128
tgui/packages/tgui/interfaces/ColorMate/functions.ts
Normal file
@@ -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]);
|
||||
}
|
||||
@@ -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<Data>();
|
||||
|
||||
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] = <ColorMateTint />;
|
||||
tab[2] = <ColorMateHSV />;
|
||||
tab[3] = <ColorMateMatrix />;
|
||||
tab[4] = (
|
||||
<ColorMateMatrixSolver
|
||||
activeID={activeID}
|
||||
onActiveId={setActiveId}
|
||||
colorPairs={colorPairs}
|
||||
onColorPairs={setColorPairs}
|
||||
onPick={handleColorUpdate}
|
||||
/>
|
||||
);
|
||||
|
||||
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 (
|
||||
<Window title={title} width={980} height={height}>
|
||||
<Window.Content overflow="auto">
|
||||
<Section fill>
|
||||
<Stack vertical>
|
||||
<Stack fill vertical>
|
||||
{!!temp && (
|
||||
<Stack.Item>
|
||||
<NoticeBox>{temp}</NoticeBox>
|
||||
@@ -50,59 +96,73 @@ export const ColorMate = (props) => {
|
||||
<Stack.Item>
|
||||
<Box>{message}</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack.Item grow>
|
||||
{item_name ? (
|
||||
<>
|
||||
<Table>
|
||||
<Table.Cell width="50%">
|
||||
<Section>
|
||||
<center>Item:</center>
|
||||
<Image
|
||||
src={`data:image/jpeg;base64,${item_sprite}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Section>
|
||||
<center>Preview:</center>
|
||||
<Image
|
||||
src={`data:image/jpeg;base64,${item_preview}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
</Table.Cell>
|
||||
</Table>
|
||||
{!matrix_only && (
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<Table>
|
||||
<Table.Cell width="50%">
|
||||
<Section fill>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item align="center">Item:</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ColorPickerCanvas
|
||||
imageData={item_sprite}
|
||||
onPick={handleColorUpdate}
|
||||
isMatrix={
|
||||
activemode === 4 && activeID.id !== null
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Section fill>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item align="center">Preview:</Stack.Item>
|
||||
<Stack.Item>
|
||||
<ColorPickerCanvas
|
||||
imageData={item_preview}
|
||||
onPick={handleColorUpdate}
|
||||
isMatrix={
|
||||
activemode === 4 && activeID.id !== null
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
</Table.Cell>
|
||||
</Table>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Tabs fluid>
|
||||
<Tabs.Tab
|
||||
key="1"
|
||||
selected={activemode === 1}
|
||||
onClick={() =>
|
||||
act('switch_modes', {
|
||||
mode: 1,
|
||||
})
|
||||
}
|
||||
>
|
||||
Tint coloring (Simple)
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
key="2"
|
||||
selected={activemode === 2}
|
||||
onClick={() =>
|
||||
act('switch_modes', {
|
||||
mode: 2,
|
||||
})
|
||||
}
|
||||
>
|
||||
HSV coloring (Normal)
|
||||
</Tabs.Tab>
|
||||
{!matrix_only && (
|
||||
<>
|
||||
<Tabs.Tab
|
||||
key="1"
|
||||
selected={activemode === 1}
|
||||
onClick={() =>
|
||||
act('switch_modes', {
|
||||
mode: 1,
|
||||
})
|
||||
}
|
||||
>
|
||||
Tint coloring (Simple)
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
key="2"
|
||||
selected={activemode === 2}
|
||||
onClick={() =>
|
||||
act('switch_modes', {
|
||||
mode: 2,
|
||||
})
|
||||
}
|
||||
>
|
||||
HSV coloring (Normal)
|
||||
</Tabs.Tab>
|
||||
</>
|
||||
)}
|
||||
<Tabs.Tab
|
||||
key="3"
|
||||
selected={activemode === 3}
|
||||
@@ -114,44 +174,65 @@ export const ColorMate = (props) => {
|
||||
>
|
||||
Matrix coloring (Advanced)
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
key="4"
|
||||
selected={activemode === 4}
|
||||
onClick={() =>
|
||||
act('switch_modes', {
|
||||
mode: 4,
|
||||
})
|
||||
}
|
||||
>
|
||||
Matrix coloring (Automatic)
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
)}
|
||||
<center>Coloring: {item_name}</center>
|
||||
<Table mt={1}>
|
||||
<Table.Cell width="33%">
|
||||
<Button.Confirm
|
||||
fluid
|
||||
icon="fill"
|
||||
confirmIcon="fill"
|
||||
confirmContent="Confirm Paint?"
|
||||
onClick={() => act('paint')}
|
||||
>
|
||||
Paint
|
||||
</Button.Confirm>
|
||||
<Button.Confirm
|
||||
fluid
|
||||
icon="eraser"
|
||||
confirmIcon="eraser"
|
||||
confirmContent="Confirm Clear?"
|
||||
onClick={() => act('clear')}
|
||||
>
|
||||
Clear
|
||||
</Button.Confirm>
|
||||
<Button.Confirm
|
||||
fluid
|
||||
icon="eject"
|
||||
confirmIcon="eject"
|
||||
confirmContent="Confirm Eject?"
|
||||
onClick={() => act('drop')}
|
||||
>
|
||||
Eject
|
||||
</Button.Confirm>
|
||||
</Table.Cell>
|
||||
<Table.Cell width="66%">
|
||||
{tab[activemode] || <Box textColor="red">Error</Box>}
|
||||
</Table.Cell>
|
||||
</Table>
|
||||
</>
|
||||
</Stack.Item>
|
||||
<Stack.Item align="center">Coloring: {item_name}</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Stack fill mt={1}>
|
||||
<Stack.Item width="33%">
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
fluid
|
||||
icon="fill"
|
||||
confirmIcon="fill"
|
||||
confirmContent="Confirm Paint?"
|
||||
onClick={() => act('paint')}
|
||||
>
|
||||
Paint
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
fluid
|
||||
icon="eraser"
|
||||
confirmIcon="eraser"
|
||||
confirmContent="Confirm Clear?"
|
||||
onClick={() => act('clear')}
|
||||
>
|
||||
Clear
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
fluid
|
||||
icon="eject"
|
||||
confirmIcon="eject"
|
||||
confirmContent="Confirm Eject?"
|
||||
onClick={() => act('drop')}
|
||||
>
|
||||
Eject
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item width="66%">
|
||||
{tab[activemode] || <Box textColor="red">Error</Box>}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
) : (
|
||||
<center>No item inserted.</center>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -49,7 +49,7 @@ interface ColorPickerData {
|
||||
timeout: number;
|
||||
title: string;
|
||||
default_color: string;
|
||||
presets: string;
|
||||
presets?: string;
|
||||
}
|
||||
|
||||
type ColorPickerModalProps = Record<never, never>;
|
||||
@@ -61,7 +61,7 @@ export const ColorPickerModal: React.FC<ColorPickerModalProps> = () => {
|
||||
message,
|
||||
autofocus,
|
||||
default_color = '#000000',
|
||||
presets = '',
|
||||
presets,
|
||||
} = data;
|
||||
let { title } = data;
|
||||
|
||||
@@ -105,23 +105,26 @@ export const ColorPickerModal: React.FC<ColorPickerModalProps> = () => {
|
||||
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 (
|
||||
<Window
|
||||
height={message ? 460 : 420}
|
||||
@@ -168,11 +171,11 @@ export const ColorPickerModal: React.FC<ColorPickerModalProps> = () => {
|
||||
interface ColorPresetsProps {
|
||||
setColor: (color: HsvaColor) => void;
|
||||
setShowPresets: (show: boolean) => void;
|
||||
presetList: string[][];
|
||||
presetList?: string[][];
|
||||
selectedPreset: number | undefined;
|
||||
onSelectedPreset: React.Dispatch<React.SetStateAction<number | undefined>>;
|
||||
allowEditing: boolean;
|
||||
onAllowEditing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
allowEditing?: boolean;
|
||||
onAllowEditing?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const ColorPresets: React.FC<ColorPresetsProps> = React.memo(
|
||||
@@ -221,7 +224,7 @@ const ColorPresets: React.FC<ColorPresetsProps> = React.memo(
|
||||
))}
|
||||
</Stack.Item>
|
||||
<Stack.Item mt={0.5}>
|
||||
{presetList.map((row, index) => (
|
||||
{presetList?.map((row, index) => (
|
||||
<Stack.Item key={index} grow>
|
||||
<Stack justify="center" g={0}>
|
||||
{row.map((entry, i) => (
|
||||
@@ -251,14 +254,16 @@ const ColorPresets: React.FC<ColorPresetsProps> = React.memo(
|
||||
))}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Button
|
||||
color={allowEditing ? 'green' : 'red'}
|
||||
position="absolute"
|
||||
right="4px"
|
||||
bottom="4px"
|
||||
icon="lock"
|
||||
onClick={() => onAllowEditing(!allowEditing)}
|
||||
/>
|
||||
{!!onAllowEditing && (
|
||||
<Button
|
||||
color={allowEditing ? 'green' : 'red'}
|
||||
position="absolute"
|
||||
right="4px"
|
||||
bottom="4px"
|
||||
icon="lock"
|
||||
onClick={() => onAllowEditing(!allowEditing)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -268,14 +273,14 @@ interface ColorSelectorProps {
|
||||
color: HsvaColor;
|
||||
setColor: React.Dispatch<React.SetStateAction<HsvaColor>>;
|
||||
defaultColor: string;
|
||||
presetList: string[][];
|
||||
presetList?: string[][];
|
||||
selectedPreset: number | undefined;
|
||||
onSelectedPreset: React.Dispatch<React.SetStateAction<number | undefined>>;
|
||||
allowEditing: boolean;
|
||||
onAllowEditing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
allowEditing?: boolean;
|
||||
onAllowEditing?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const ColorSelector: React.FC<ColorSelectorProps> = React.memo(
|
||||
export const ColorSelector: React.FC<ColorSelectorProps> = React.memo(
|
||||
({
|
||||
color,
|
||||
setColor,
|
||||
|
||||
14
tgui/packages/tgui/styles/interfaces/ColorMatrixEditor.scss
Normal file
14
tgui/packages/tgui/styles/interfaces/ColorMatrixEditor.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.MatrixEditor__Floating {
|
||||
height: 290px;
|
||||
width: 580px;
|
||||
background-color: var(--color-section);
|
||||
backdrop-filter: var(--blur-medium);
|
||||
border: var(--border-thickness-tiny) solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-shadow: var(--shadow-glow-medium) hsla(0, 0%, 0%, 0.5);
|
||||
margin: var(--space-m);
|
||||
color: var(--color-text);
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding: var(--space-m);
|
||||
}
|
||||
@@ -57,6 +57,7 @@
|
||||
@include meta.load-css('./interfaces/VorePanel.scss');
|
||||
@include meta.load-css('./interfaces/Wires.scss');
|
||||
@include meta.load-css('./interfaces/PlushEditor.scss');
|
||||
@include meta.load-css('./interfaces/ColorMatrixEditor.scss');
|
||||
|
||||
// Layouts
|
||||
@include meta.load-css('./layouts/Layout.scss');
|
||||
|
||||
Reference in New Issue
Block a user