diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index f9309b7768e..bd5997c8f3f 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -218,3 +218,4 @@
///Plane master controller keys
#define PLANE_MASTERS_GAME "plane_masters_game"
+#define PLANE_MASTERS_COLORBLIND "plane_masters_colorblind"
diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm
index 288a8b060f1..bb5883b9b02 100644
--- a/code/_onclick/hud/rendering/plane_master.dm
+++ b/code/_onclick/hud/rendering/plane_master.dm
@@ -269,3 +269,11 @@
render_target = FIELD_OF_VISION_BLOCKER_RENDER_TARGET
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
render_relay_plane = null
+
+/atom/movable/screen/plane_master/hud
+ name = "HUD plane"
+ plane = HUD_PLANE
+
+/atom/movable/screen/plane_master/above_hud
+ name = "above HUD plane"
+ plane = ABOVE_HUD_PLANE
diff --git a/code/_onclick/hud/rendering/plane_master_controller.dm b/code/_onclick/hud/rendering/plane_master_controller.dm
index 8d0c98cbd90..faae549bc70 100644
--- a/code/_onclick/hud/rendering/plane_master_controller.dm
+++ b/code/_onclick/hud/rendering/plane_master_controller.dm
@@ -95,4 +95,30 @@ INITIALIZE_IMMEDIATE(/atom/movable/plane_master_controller)
LIGHTING_PLANE,
)
+/// Controller of all planes we're ok with changing with colorblind logic
+/atom/movable/plane_master_controller/colorblind
+ name = PLANE_MASTERS_COLORBLIND
+ controlled_planes = list(
+ PLANE_SPACE_PARALLAX,
+ GRAVITY_PULSE_PLANE,
+ FLOOR_PLANE,
+ GAME_PLANE,
+ GAME_PLANE_FOV_HIDDEN,
+ GAME_PLANE_UPPER,
+ GAME_PLANE_UPPER_FOV_HIDDEN,
+ ABOVE_GAME_PLANE,
+ MASSIVE_OBJ_PLANE,
+ GHOST_PLANE,
+ POINT_PLANE,
+ RAD_TEXT_PLANE,
+ LIGHTING_PLANE,
+ O_LIGHTING_VISUAL_PLANE,
+ ABOVE_LIGHTING_PLANE,
+ CAMERA_STATIC_PLANE,
+ ATMOS_GROUP_PLANE,
+ FULLSCREEN_PLANE,
+ RUNECHAT_PLANE,
+ HUD_PLANE,
+ ABOVE_HUD_PLANE,
+ )
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 3ebbafcc3db..d74a6f06676 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -176,6 +176,7 @@ GLOBAL_PROTECT(admin_verbs_debug)
/client/proc/set_dynex_scale,
/client/proc/cmd_display_del_log,
/client/proc/outfit_manager,
+ /client/proc/open_colorblind_test,
/client/proc/generate_wikichem_list,
/client/proc/modify_goals,
/client/proc/debug_huds,
diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm
index 1766af12483..f5b1bcfd848 100644
--- a/code/modules/admin/holder2.dm
+++ b/code/modules/admin/holder2.dm
@@ -32,6 +32,7 @@ GLOBAL_PROTECT(href_token)
var/deadmined
var/datum/filter_editor/filteriffic
+ var/datum/colorblind_tester/color_test = new
/// Whether or not the user tried to connect, but was blocked by 2FA
var/blocked_by_2fa = FALSE
diff --git a/code/modules/admin/verbs/color_blind_test.dm b/code/modules/admin/verbs/color_blind_test.dm
new file mode 100644
index 00000000000..5640149cd0a
--- /dev/null
+++ b/code/modules/admin/verbs/color_blind_test.dm
@@ -0,0 +1,73 @@
+/// Used to test the game for issues with different types of color blindness
+/// WARNING ASSHOLE: Because we can only apply matrixes, and can't preform gamma correction
+/// https://web.archive.org/web/20220227030606/https://ixora.io/projects/colorblindness/color-blindness-simulation-research/
+/// The results of this tool aren't perfect. It's way better then nothing, but IT IS NOT A PROPER SIMULATION
+/// Please do not make us look like assholes by assuming it is. Thanks.
+/datum/colorblind_tester
+ /// List of simulated blindness -> matrix to use
+ /// Most of these matrixes are based off https://web.archive.org/web/20220227030606/https://ixora.io/projects/colorblindness/color-blindness-simulation-research/
+ /// AGAIN, THESE ARE NOT PERFECT BECAUSE WE CANNOT COMPUTE GAMMA CORRECTION, AND CONVERT SRGB TO LINEAR RGB
+ /// Do not assume this is absolute
+ var/list/color_matrixes = list(
+ "Protanopia" = list(0.56,0.43,0,0, 0.55,0.44,0,0, 0,0.24,0.75,0, 0,0,0,1, 0,0,0,0),
+ "Deuteranopia" = list(0.62,0.37,0,0, 0.70,0.30,0,0, 0,0.30,0.70,0, 0,0,0,1, 0,0,0,0),
+ "Tritanopia" = list(0.95,0.5,0,0, 0,0.43,0.56,0, 0,0.47,0.52,0, 0,0,0,1, 0,0,0,0),
+ "Achromatopsia" = list(0.33,0.33,0.33,0, 0.33,0.33,0.33,0, 0.33,0.33,0.33,0, 0,0,0,1, 0,0,0,0),
+ )
+ var/list/descriptions = list(
+ "Protanopia" = "No long wavelength cones, ends up not being able to see red light. Troubles with blue/green and red/green",
+ "Deuteranopia" = "No medium wavelength cones. Because the red and green parts of light nearly overlap in this space, trouble is mostly with red/green",
+ "Tritanopia" = "No short wavelength cones, so trouble with blue/green and yellow/violet. Aggressively rare, and equally hard to simulate",
+ "Achromatopsia" = "No cones at all, which leads to something close to monochromatic vision"
+ )
+ var/selected_type = ""
+
+/datum/colorblind_tester/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/colorblind_tester/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ColorBlindTester")
+ ui.open()
+
+/datum/colorblind_tester/ui_data()
+ var/list/data = list()
+ data["details"] = descriptions
+ data["selected"] = selected_type
+ return data
+
+/datum/colorblind_tester/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ var/datum/hud/our_hud = ui.user?.hud_used
+ if(!our_hud) // Nothing to act on
+ return
+
+ switch(action)
+ if("set_matrix")
+ set_selected_type(params["name"], our_hud)
+ return TRUE
+ if("clear_matrix")
+ set_selected_type("", our_hud)
+ return TRUE
+
+/datum/colorblind_tester/proc/set_selected_type(selected, datum/hud/remove_from)
+ var/atom/movable/plane_master_controller/colorblind_plane = remove_from.plane_master_controllers[PLANE_MASTERS_COLORBLIND]
+ // This is dumb, but well
+ // The parralax plane has a blend mode of 4, or BLEND_MULTIPLY
+ // It's like that so it is properly masked by darkness and such
+ // The problem is blend modes apply to filters like this too
+ // So I need to manually set and reset its blendmode to allow for proper shading of the background
+ // Sorry...
+ var/atom/movable/screen/plane_master/parralax = colorblind_plane.controlled_planes["[PLANE_SPACE_PARALLAX]"]
+ if(selected_type)
+ colorblind_plane.remove_filter(selected_type)
+ parralax.blend_mode = initial(parralax.blend_mode)
+ selected_type = selected
+ if(selected_type)
+ var/list/matrix = color_matrixes[selected_type]
+ colorblind_plane.add_filter(selected_type, 0, color_matrix_filter(matrix))
+ parralax.blend_mode = BLEND_DEFAULT
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index 8dd56c898ea..a6f87aee54f 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -601,6 +601,15 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that
usr << browse(replacetext(SSatoms.InitLog(), "\n", "
"), "window=initlog")
+/client/proc/open_colorblind_test()
+ set category = "Debug"
+ set name = "Colorblind Testing"
+ set desc = "Change your view to a budget version of colorblindness to test for usability"
+
+ if(!holder)
+ return
+ holder.color_test.ui_interact(mob)
+
/client/proc/debug_huds(i as num)
set category = "Debug"
set name = "Debug HUDs"
diff --git a/tgstation.dme b/tgstation.dme
index 27a3e99bba2..ca221d51901 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1909,6 +1909,7 @@
#include "code\modules\admin\verbs\borgpanel.dm"
#include "code\modules\admin\verbs\BrokenInhands.dm"
#include "code\modules\admin\verbs\cinematic.dm"
+#include "code\modules\admin\verbs\color_blind_test.dm"
#include "code\modules\admin\verbs\commandreport.dm"
#include "code\modules\admin\verbs\deadsay.dm"
#include "code\modules\admin\verbs\debug.dm"
diff --git a/tgui/packages/tgui/interfaces/ColorBlindTester.js b/tgui/packages/tgui/interfaces/ColorBlindTester.js
new file mode 100644
index 00000000000..76a1faac965
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ColorBlindTester.js
@@ -0,0 +1,85 @@
+import { useBackend } from '../backend';
+import { Box, Button, NoticeBox, Section } from '../components';
+import { Window } from '../layouts';
+
+export const ColorBlindTester = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { details } = data;
+ return (
+
+
+
+
+ HEY FUCKOS, these filters are based off VERY OLD and VERY FLAWED
+ matrixes.
+
+
+ There is NO GOOD WAY to do proper color blind simulation in
+ BYOND, because we have no way to extract the gamma of a pixel
+ without iterating all pixels on the screen, which we need to do to
+ properly correct for the human eye.
+
+
+ Because of this, this simulation is very imperfect. You will notice
+ things are much more bright then they should be. This is a direct
+ result of not being able to correct for gamma.
+
+
+ This tool exists so we have at least some form of baseline for
+ accessability, it is nowhere near gospel.
+
+
+ If I find you being a dick to someone over this I will clobber you
+ with a crowbar
+
+
+
+ {Object.keys(details).map((category) => (
+
+ ))}
+
+
+
+ );
+};
+
+const ColorBlindCategory = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { category } = props;
+ const { details, selected } = data;
+ if (category !== selected) {
+ return (
+ act("set_matrix", {
+ name: category,
+ })} />
+ )}>
+ {details[category]}
+
+ );
+ }
+ return (
+ act("clear_matrix")}
+ />
+ )}>
+ {details[category]}
+
+ );
+};