Color blindness (simulating and daltonizing)

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
User avatar
spill
Prole
Posts: 27
Joined: Thu May 07, 2015 1:53 am
Contact:

Color blindness (simulating and daltonizing)

Post by spill »

I'm working on a game that involves some random color palettes and I realized that I could very easily make a set of colors that would be hard to differentiate for people with color blindness. So, I did a bit of googling and a bit of hacking and wrote this code that can do two things:
  • 1. Simulate what your game would look like to someone with different types of color blindness.
    2. Daltonize your game to make it easier for someone with a particular type of color blindness to differentiate the colors in your game.
Hopefully someone else can get some use out of this code.

The code:

Code: Select all

--[[ Copyright 2015 Bruce Hill <bruce@bruce-hill.com>
    This work is free. You can redistribute it and/or modify it under the
    terms of the Do What The Fuck You Want To Public License, Version 2,
    as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.

    Based on: https://elderscrollsonline.reddit.com/comments/2578wc/

    This module has two functions: simulate(colorblindType) and
    daltonize(colorblindType). simulate will simulate the specified type of
    color blindness and daltonize will adjust colors so that someone with the
    specified type of color blindness can more easily differentiate colors.

    simulate() and daltonize() both return a function that toggles their visual
    effect and both should only be called after love.draw() is defined.

    Supported color blindness types: 'protanope', 'deuteranope', 'tritanope' ]]
local colorblind = {}
local colorblindTypeCode = {
    protanope = [[
        vec3 lms = vec3(dot(LMS, vec3(0.,2.02344,-2.52581)),
                        dot(LMS, vec3(0.,1.,0.)),
                        dot(LMS, vec3(0.,0.,1.)));]],
    deuteranope = [[
        vec3 lms = vec3(dot(LMS, vec3(1.,0.,0.)),
                        dot(LMS, vec3(.494207,0.,1.24827)),
                        dot(LMS, vec3(0.,0.,1.)));]],
    tritanope = [[
        vec3 lms = vec3(dot(LMS, vec3(1.,0.,0.)),
                        dot(LMS, vec3(0.,1.,0.)),
                        dot(LMS, vec3(-.395913,.801109,0.)));]],
}
local sharedCode = [[
vec3 getError(vec3 color)
{
    // RGB to LMS matrix conversion
    vec3 LMS = vec3(dot(color, vec3(17.8824,43.5161,4.11935)),
                    dot(color, vec3(3.45565,27.1554,3.86714)),
                    dot(color, vec3(.0299566,.184309,1.46709)));
    
    %s
    vec3 error = vec3(
        dot(lms, vec3(.0809444479,-.130504409,.116721066)),
        dot(lms, vec3(-.0102485335,.0540193266,-.113614708)),
        dot(lms, vec3(-.000365296938,-.00412161469,.693511405)));
    return error;
}
]]

local function configure(shaderCode, colorblindType)
    assert(colorblindTypeCode[colorblindType], 'Color blindness type must be "protanope", "deuteranope", or "tritanope"')
    assert(love.draw, "love.draw hasn't been defined yet!")
    local oldDraw = love.draw
    local canvas = love.graphics.newCanvas()
    local shader = love.graphics.newShader(shaderCode:format(colorblindTypeCode[colorblindType]))
    function love.draw(...)
        local oldCanvas = love.graphics.getCanvas()
        love.graphics.setCanvas(canvas)
        canvas:clear(love.graphics.getBackgroundColor())
        oldDraw(...)
        love.graphics.setCanvas(oldCanvas)
        local oldShader = love.graphics.getShader()
        love.graphics.setShader(shader)
        love.graphics.draw(canvas)
        love.graphics.setShader(oldShader)
    end
    return function() love.draw, oldDraw = oldDraw, love.draw end
end

local simulateShaderCode = sharedCode..[[
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords)
{
    vec4 original = Texel(texture, texture_coords);
    original.rgb = getError(original.rgb);
    return clamp(original, 0., 1.);
}
]]
function colorblind.simulate(colorblindType)
    return configure(simulateShaderCode, colorblindType)
end

local daltonizeShaderCode = sharedCode..[[
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords)
{
    vec4 original = Texel(texture, texture_coords);
    vec3 error = getError(original.rgb);
    error = (original.rgb - error);
    
    // Shift colors towards visible spectrum (apply error modifications)
    vec3 correction = vec3(0., error.r * 0.7 + error.g * 1.0, error.r * 0.7 + error.b * 1.0);
    
    // Add compensation to original values
    original.rgb += correction;
    return clamp(original, 0., 1.);
}
]]
function colorblind.daltonize(colorblindType)
    return configure(daltonizeShaderCode, colorblindType)
end

return colorblind
Usage:

Code: Select all

local colorblind = require 'colorblind'

function love.load()
    love.graphics.setBackgroundColor(255,100,0)
    love.graphics.setFont(love.graphics.newFont(64))
end

function love.draw()
    love.graphics.setColor(255,255,0)
    love.graphics.printf("HELLO", 0, .3*love.graphics.getHeight(), love.graphics.getWidth(), 'center')
    love.graphics.setColor(0,255,0)
    love.graphics.printf("WORLD", 0, .6*love.graphics.getHeight(), love.graphics.getWidth(), 'center')
end

colorblind.simulate('protanope')
Original:
Screen Shot 2015-09-08 at 9.34.55 PM.png
Screen Shot 2015-09-08 at 9.34.55 PM.png (15.84 KiB) Viewed 3433 times
Simulated protanopic color blindness:
Screen Shot 2015-09-10 at 6.06.29 PM.png
Screen Shot 2015-09-10 at 6.06.29 PM.png (15.9 KiB) Viewed 3309 times
Daltonized for protanopic color blindness:
Screen Shot 2015-09-08 at 9.35.39 PM.png
Screen Shot 2015-09-08 at 9.35.39 PM.png (16.6 KiB) Viewed 3433 times
[Edit: just spotted a bug in the simulating code. Fixed it and replaced the screenshot.]
Last edited by spill on Fri Sep 11, 2015 1:12 am, edited 1 time in total.
Rickton
Party member
Posts: 128
Joined: Tue Mar 19, 2013 4:59 pm
Contact:

Re: Color blindness (simulating and daltonizing)

Post by Rickton »

This is great! I've seen tools that lets you colorblind-test a static image, but not as the game plays. Thanks for sharing!
Possession - Escape from the Nether Regions, my roguelike made in LÖVE for the 2013 7-Day Roguelike Challenge
And its sequel, simply called Possession , which is available on itch.io or Steam, and whose engine I've open-sourced!
User avatar
Reenen
Prole
Posts: 44
Joined: Tue Nov 08, 2011 9:44 am

Re: Color blindness (simulating and daltonizing)

Post by Reenen »

I am a little colour blind (dark reds and dark greens are tough to decern). And original hurts my eyes, and the image for Daltonized for protanopic color blindness looks similar to the original but doesn't hurt my eyes.
User avatar
DeltaF1
Citizen
Posts: 64
Joined: Mon Apr 27, 2015 4:12 pm
Location: The Bottom of the Stack
Contact:

Re: Color blindness (simulating and daltonizing)

Post by DeltaF1 »

This is very neat! I don't often think about colour blindness when desiging a colour scheme or UI, so I'm glad this code makes it so easy to make my games more accesible. I'l be adding the option in my future projects for sure :)
Post Reply

Who is online

Users browsing this forum: No registered users and 3 guests