i#Aj2@TZ$d?N|H
zLzbpu{(~T&56Dipq+G#`(R7^!3#z&io0V0wW`Rule`xcmN?xAjhwqucB;hm;&eK(dS8eQ9T|H>>*U#p<=q|_5H-t&i0CSnI?HbYf|ZCX(r01XYYd!3
ztJMilp2baZdV2Ec(*kY#y)C`y({)1Y+CH`5@8x7`{E7brAEECO3bQ`p6MCRotzw~A
z1Z@6g%TziwKw-8f*rHU}|MLrz)2SL9A8QoLBW$VC-eO#vWY$ASxmHyn0@&=z?T6C6
z?C#l*dsRi7J|t)6z7X&WbWoJG7i#K!c=3#zk)-kh1Qd=t;0-n%H^}7nQRVJv>c%40
zND>>b%DOXA&;iuqz<_L#(D|qBqxY=wj$X1BpGMf_C{)QcB(tLsE>ATa*7`vG$_qoMXy)dH|4@!WJ(y5n81Un>mxL
zRfh$1f;yMOm_H#3TW!
ziw)ZQvnS*on(^Zx%iArn#>-y?m0Q~{ZHkTjCN(}Mu#?gxGujh=*%qT27#IYXzH>q=
z!@o(6+t+bjbOn{tKE`#@B)F)x-AasxyuGDIe})`k_W8)Rq>A(nV6?h=3URH`&_U#x
z2||9L7vbbi%L2fN2LK_rUwl$9d`qod@rZ9RQ8#QH%_|!y2U>r^$VgLMDf^i}EknTPn3lJ3MnsY$VZGWC+)EE9TlL_2JN2?2!6>4sPHAcLdgvL!dF`FSHE2x9
zoK{2Oa|<-p`E1NArWR$dg
zOZ4hjT>XchLbMpaeN)bU#Xq|HFaQwojx>BD6OGNxZMzp}3xx9+7darTeX
zyVSrqc9{E3po#!Koa{}qw4|8Y!3_hh5jEZ^|M=cmx;#glp>LzLKhN?MqkA)($b=%k
zD&G%K%&U6Yb&ohFF6bVF&ExV~d!RL93klNCX_}BebC_{4DM;6UipMJ?;D4V|Q6O4b
z7wsHDLbSEhUFE^8-GdGli=&=pXV>q#@ZZHeP%<0GmTdx9F}t1UJ6>h=Rhg3CyXqsC
z(9zgvsdF)h?__)tjXr2iq&xLvu$Gmp8wcg5N1pQ4yc_iXH~pQqUHadif|X)uAFQZ}@J$TpaK+wcj_bG>>F*sLf8zJ;*VM@iNH#xAk(0v?})|`xx2Dh*+
zR0$_4RNZs%82`J{d8Y)woUqlX0JT2A-%3orH>CV(uW(~`+B*vATLCzH#W^u
zeE!Aa^yz7+XT$t^=M}54n%Bz*97(I1j*)?mHm(+mtByT(lVbefkF#E~Idam+h8trj
z)&O~6!M{#3!U*+fq>Gv7{YiOvc;c@gUZt!v+-b;Wg}Fc$noRmO
z@M0F_tX%zXa$&mBD=x4^de_)>j10qlP6EhR&ekXv7S<2ZqIUR*1)s2azBZIG@9!;@
zP!cO%A6NEh_$*OP;GS%-tSm=A8r_Z-jjIhd|H3jqe>OiK)XxwM+8)(DIR8D6VLMP8
zBb+oNsqDvtWaykH+~VK(5noV4L61
zL`K6fxdq}^(l4Yi28O3x{`hzT7Xb_7LmiG7%E(ZOGS7Gmnb#jV2YY(GGoa>F_@9Q1E`Ls)`#T1*27P7q{mfX!ui#@TS@CELsgBiy#qbLcf{-?
z^MAqdjwkO5TQF}Xh-L0UT6S5HHR&d!Yd8RROFbs;j6(K-h_#EI&_{F3e%TfYcSv>Y
zaQvBI+k1BOS%BjBV0ibf_gy5`e0j2z-3B>^A^w<)qh>r7LC#&w_0oe$l$Wt#Z~$-J
zS&t3N!jv)`VJP%-sw5)ZD>2miIT#c+?8uiy-6>L`oQfh;d1Uje$YMQ~4IB&4|=4!36EechG1fCR7P5jB0YnpE)H2vBt4FA^pVm
za~8)!EuCa+Vq_64Ze}iLwC{)q9a7hr`@*XjjvFlBom2>$ul+{cpRnhRAU28S1r5_N8a58JBfjnMRW2{?W6AS6
zo?%j_SqpTHRyCavuP6Qn9RI34{y
zy&roRIyL9CZ>~GvxprA8K?VfknlaYXL51aQ}%U+&&evM9v+)2Ke`tjvesT9}q{R(xt=T(C&DuU-;qRg1QVpgbr*9n?b(7%$5L`!Wg(f{b9*K~ri
z6{JD_`=N&}(?ei^{6QNis&;JSB!kr5c!qB^TWG5}XLCFmgK=tc6d>|_T8yHA>a~s9
z)8px&)#1s%Oi6+U($4R-4U8r}ITb2QXrnM5yRAx@lVIm?R(MEDw)2m;GlZ-=1Q%xI
zECqtTW$6=@CYv~19ILKy3G6sNQ3dNOqkTE>@2DgqJZsIVPk3Qp*u60SJDQ?2z7|~e
zyNamZwoz?E`u0usP9pk#Kgd!wG>~k8LD<4X{COF7zf?L&^Nt5-f1KoKuF!-u
zv+2c$?5|fufd}2As;j5oI=y9c8oQwS3OqOfxV%C?1yy-R1qvga
zoxEMsA1ar`CPg3Wuj#eReOe(fFUz!j(+2TFrInGE?~97q*eaq+K+-H09#N&4@HM&j
z;?it$4bt*2?C0NOgqj7|DpCjcn1vzyB1PRG|3nF>wS|WOzc9kbKSl>x41V^_vRCjd
z_rRnJ12!fBNf8HD<4|JE#ZRgj7Fi^SI&V4RC1Sdn6O<4sif7^Oy-{z8;hgv%EKQv4
zRDBl_gDm(AZ5V}7G;6vMRpmJbb&}_drA&T^#*M~4Zu%u`%IC{H|7u3r-zSFH;2yAK
zaCRYE-O9&<+Y-}FDK2i}Qk}{LGuvODzw6C(@`69P^??0yn8r6}H)`$&srtYjN2b%pU^`eG*;%xO7_t?tpxgwFleY(u3
z`ip?x!i%RBRDbY#5=HjV?0iMaf0pD+z;YYQ^7p3y<7T4QThZAMmfSJmuB#z*pV4NH
zuD&6&@2NaBtE*A)j-yLEUGjx^fMb!yv=o;@6&1xXvDs?>p$#urAEl0@sKE!*I^#a;
z$3Dc|>~4Vkp()lKl|?rJLtA)Tv8`A+ob(@b_sZ&@CUxZYWNaj3_fhTewk)y{-@>gD
zfU~?{9#u!Iq>7Tu68td(%adW+?lhqzzkZ7NQaRPGdj(GbeS|kq(@1V}si%{3_T7HK
zC6BM@(tW}svjFyd%D3km_05T!$Q6fQNV0l`@X
zUsmxVIe1rMpiU&+Aiqc90kR>^Hl%-@z;oQ=fUHr?ah#WTG&+fZdCUlK`X{U
zU}T=*?zRHQ(K>XQeyitv%P*W7G@haYb>9sXU+qKWz;dW1UalW;9Y;nj6qA0s6-I7l
z*OzlHG!h46W}S1Mr!i&^Z34g|rE#eTb~6yO<3YsQVdO%{oN+t73S2NqJH4dc*^g|}
zPXDtT+VvcscQUdUKC6(ut6xE0l`0trj4ddr?_!fL?S@uT7c<&%e+hzT=+Q&4@W=m)
zSs`S*nec%iA*^g(rQpu5_vIeY#Y(y-HLG#63I2pP%nq3I8;ulyj=15@Tl+`@q+QwB
z1ixZ~ehQD8yIoFm1@2J9ksj0WMxV+JnZhvYw293U+(Na35
z469wc4z+=wJ^3~{c%b`8=|vO#3+9}pR{!&!9ul8Nq4eR_IE5S=LyxoF@B6UpnVIz|
zeLZ^&SY6UgctuxuH+0AMOH1J?crMYCbRMI2yF#1s!B;P!S7KgbZI1%m$PWY92K^@J
znT9Qf!^;u>9nk9CSE}&1+kQDN@8#aD%5|zEucQNK*+Ex7j&U87q|loEbKiA>{*`jI
zh5-8F*;o-5`m=QNc30O<>MNi$p9EY4FeXx8RK&IWsygEaa0@i|sX*Pouy!7)t;7+c
zufKaAz6Tne=sduJ=uaqy@Ez#Ca4tV{<@l5Zj6>SwVscM<&!J%&(B8pS2lP#vp5IQ*
zFJvEnHm=>ZBdgc>Kq>W{c3g4}??T+cE92cXt$DB-IS67J^L!A$T!CM>t$hig1VP@Y
zKapxlSl#jCDE-lOq-g(=TI2`WOnhm)vIEKObZA!7mK$e9-q>aB4sH8c)eM8LgV=HA{j+XkJTBPHgXHYu51PLb>AeKyg{m-B+{
zts~ji(eVv1puwUS!v3o%RFd4TXGB`L*Q_%6nZ0XUDM?n=TUPdz0buH2ff>j^noizH
zzo_3l5zJ8B@WvdyvUe)4@m%9EUgp4!D$7z*RMq{$viwY@|BuGgMg?ZIwFwKvOpfQV|QBP!>bxQa&WSgXIMatAb-Kgm-R6P8M85KiqbeZvSfi8(K>9rv|>ST=hh2$O6IZ|#~DMHT9
z$)V@$E>C45UsH20J;^yMh^P!%$d<}D1&e;;5nseF_%y)dcuLh{?1!kXy>ID-(q}gL
zr>GNGm@I+>#9~JvMqxq*d_^qLnz;LGpq6o7eqD~DH4e_pfUYU_t+f}!U$EQ~IAk?wpZjba(zx;II9RlERG06KLYDz5
zGWghGz~esBE1D6aUd;PSv0z22&YcfYQc-z4$QJKW?{oEI)CG25;bIpKGnr_)XLs#{
z3@0{!4Y5O~jQ~D9{rKjL*Aw!4Ix3;iuW7}XlT3ui)U46Cdc!obs|Mn!f}%O^cjYZl
zp3TKVYyfybo)%a-`jy`@QZW9^h?H0DzE)1KZH~9&;B06d9c>+?kBc3LtINaRV?&IdpCgFzKx|d4rfh0^MBXM*G+ole|L3!
zNZRlU|2BDJEClwBZ7Raf&TDMOE6jEBm2MG}?XP(u<3y)}jPithbi7{@yX1GLw}KJn
z1N-}fUJ7sA=ItWBW@`Ms{Bcp=1ruECcwb0k@vG8g4YLk*ZNFjb3KwVPZOZP%6ZyCZ
zpV0R^z=x5tYocuu>5qlS+6#W0WEcAig&*^<9I!93X=~Ph0ZJ
z<@Cw=6$B|-et7o@*Gy&n&`)Mh;hX~kv|bhxBQ?Eko~&c|lt>tUCtf$g(hLr;YzX!}>
z{k^??y%SwF@dDMJ5fMPdY(Ke`3;COphQS?;yceK9hBWPu=CNFyAG*??Ki4XlxEvw#N}9ptyOgvDyW#iF(`K>)!+ls7SL
zlOBnl!>dq?4(7pH@XeR#bEQ=-c3Rh?++py=-382aWKH1CQhvS$eEZ~7&23J^f{Tz`
zR7{*p4XDib3mJQbK0qx+v1r?7f6ivGBI^uKcnmv^CWo`PbboEJmE&NwytohD0
z40SrcD^(lUcybL;@*5yN72qBb)1-~Ey>EU?%ldbtbu`0Bru^;_0AFaARTttDy*9!tYBwB$dz0DgU9#=uWK8T6-H)hd>3@Q>pU%`*Qh&2Ng{
zZAl(a234q+6|QV5u+X!vx%2?QO2zBgft54A)2hE+0Q94?>}jP-BoRqEc2{vT+@LjM2&
diff --git a/tgstation.dme b/tgstation.dme
index 7ba17df4ad2..e299ac439c1 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3445,6 +3445,7 @@
#include "code\modules\mod\mod_construction.dm"
#include "code\modules\mod\mod_control.dm"
#include "code\modules\mod\mod_core.dm"
+#include "code\modules\mod\mod_paint.dm"
#include "code\modules\mod\mod_theme.dm"
#include "code\modules\mod\mod_types.dm"
#include "code\modules\mod\mod_ui.dm"
diff --git a/tgui/packages/tgui/interfaces/MODpaint.js b/tgui/packages/tgui/interfaces/MODpaint.js
new file mode 100644
index 00000000000..0efe9d45d2c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/MODpaint.js
@@ -0,0 +1,171 @@
+import { useBackend } from '../backend';
+import { Box, Stack, Section, ByondUi, Slider, Flex, Button } from '../components';
+import { Window } from '../layouts';
+import { capitalize } from 'common/string';
+
+const colorToMatrix = param => {
+ switch (param) {
+ case 'red':
+ return [
+ 1, 0, 0, 0,
+ 0.25, 0.5, 0, 0,
+ 0.25, 0, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'yellow':
+ return [
+ 0.5, 0.5, 0, 0,
+ 0.5, 0.5, 0, 0,
+ 0.25, 0.25, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'green':
+ return [
+ 0.5, 0.25, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0.25, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'teal':
+ return [
+ 0.25, 0.25, 0.25, 0,
+ 0, 0.5, 0.5, 0,
+ 0, 0.5, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'blue':
+ return [
+ 0.25, 0, 0.25, 0,
+ 0, 0.5, 0.25, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'purple':
+ return [
+ 0.5, 0, 0.5, 0,
+ 0.25, 0.5, 0.25, 0,
+ 0.5, 0, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ }
+};
+
+const displayText = param => {
+ switch (param) {
+ case 'r':
+ return "Red";
+ case 'g':
+ return "Green";
+ case 'b':
+ return "Blue";
+ }
+};
+
+export const MODpaint = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { mapRef, currentColor } = data;
+ const [
+ [rr, rg, rb, ra],
+ [gr, gg, gb, ga],
+ [br, bg, bb, ba],
+ [ar, ag, ab, aa],
+ [cr, cg, cb, ca],
+ ] = currentColor;
+ const presets = ['red', 'yellow', 'green', 'teal', 'blue', 'purple'];
+ const prefixes = ['r', 'g', 'b'];
+ return (
+
+
+
+
+ {[0, 1, 2].map((row) => (
+
+ {[0, 1, 2].map((col, key) => (
+
+
+
+ {`${displayText(prefixes[col])}:`}
+
+
+
+ `${value}%`}
+ onDrag={(e, value) => {
+ let retColor = currentColor;
+ retColor[row*4+col] = value/100;
+ act("transition_color", { color: retColor });
+ }} />
+
+
+ ))}
+
+ ))}
+
+
+
+
+ {presets.map(preset => (
+
+
+
+
+
+
+
+
+
+
+ );
+};