Add color blind accessibility filter
This commit is contained in:
parent
b42569eb9c
commit
fa0bada313
2 changed files with 132 additions and 2 deletions
61
main.lua
61
main.lua
|
|
@ -31,6 +31,7 @@ function love.load()
|
||||||
_initGridVars()
|
_initGridVars()
|
||||||
_initSounds()
|
_initSounds()
|
||||||
_initGlobalColors()
|
_initGlobalColors()
|
||||||
|
_initAccessibility()
|
||||||
gameWorld = World:new()
|
gameWorld = World:new()
|
||||||
_initLevelEditor()
|
_initLevelEditor()
|
||||||
|
|
||||||
|
|
@ -116,16 +117,42 @@ function love.draw()
|
||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
love.graphics.setCanvas()
|
love.graphics.setCanvas()
|
||||||
|
|
||||||
|
-- Collect every visible layer before the accessibility pass. Applying the
|
||||||
|
-- shader here also covers the editor sidebar, FPS display, and dialogue,
|
||||||
|
-- rather than only recolouring the room sprites.
|
||||||
|
love.graphics.setCanvas(presentationCanvas)
|
||||||
|
love.graphics.clear()
|
||||||
love.graphics.setBlendMode("alpha")
|
love.graphics.setBlendMode("alpha")
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(finalCanvas, 0, 0)
|
love.graphics.draw(finalCanvas, 0, 0)
|
||||||
love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y)
|
love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y)
|
||||||
love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10)
|
love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10)
|
||||||
|
|
||||||
Dialogue.draw()
|
Dialogue.draw()
|
||||||
|
love.graphics.setCanvas()
|
||||||
|
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
local filter = accessibilityModes[accessibilityModeIndex]
|
||||||
|
if filter.shader then
|
||||||
|
filter.shader:send("accessibilityTime", love.timer.getTime())
|
||||||
|
love.graphics.setShader(filter.shader)
|
||||||
|
end
|
||||||
|
love.graphics.draw(presentationCanvas, 0, 0)
|
||||||
|
love.graphics.setShader()
|
||||||
|
|
||||||
|
if accessibilityMessage and love.timer.getTime() < accessibilityMessageExpires then
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.printf(accessibilityMessage, 0, 30, love.graphics.getWidth(), "center")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.keypressed(key)
|
function love.keypressed(key)
|
||||||
|
-- Available even while a dialogue is open, so the player never has to
|
||||||
|
-- dismiss something important just to change the colour treatment.
|
||||||
|
if key == "f4" then
|
||||||
|
cycleAccessibilityMode()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Talking to a sign captures input: navigate/choose, don't move the player.
|
-- Talking to a sign captures input: navigate/choose, don't move the player.
|
||||||
if Dialogue.isOpen() then
|
if Dialogue.isOpen() then
|
||||||
Dialogue.keypressed(key)
|
Dialogue.keypressed(key)
|
||||||
|
|
@ -237,6 +264,7 @@ end
|
||||||
function _initCanvases()
|
function _initCanvases()
|
||||||
gameCanvas = love.graphics.newCanvas()
|
gameCanvas = love.graphics.newCanvas()
|
||||||
finalCanvas = love.graphics.newCanvas()
|
finalCanvas = love.graphics.newCanvas()
|
||||||
|
presentationCanvas = love.graphics.newCanvas()
|
||||||
sideBarCanvas = love.graphics.newCanvas(math.max(1, sideBarBox.w), math.max(1, sideBarBox.h))
|
sideBarCanvas = love.graphics.newCanvas(math.max(1, sideBarBox.w), math.max(1, sideBarBox.h))
|
||||||
sideBarCanvas:setFilter("nearest", "nearest") -- never average when sampled
|
sideBarCanvas:setFilter("nearest", "nearest") -- never average when sampled
|
||||||
end
|
end
|
||||||
|
|
@ -253,6 +281,37 @@ function _initGlobalColors()
|
||||||
gColor.red, gColor.green, gColor.blue, gColor.white = Color:new("red"), Color:new("green"), Color:new("blue"), Color:new("white")
|
gColor.red, gColor.green, gColor.blue, gColor.white = Color:new("red"), Color:new("green"), Color:new("blue"), Color:new("white")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _initAccessibility()
|
||||||
|
accessibilityModes = {
|
||||||
|
{ id = "normal", label = "Normal", shader = nil },
|
||||||
|
{ id = "deuteranopia", label = "Deuteranopia", shader = accessibilityColorShader },
|
||||||
|
}
|
||||||
|
accessibilityModeIndex = 1
|
||||||
|
|
||||||
|
local savedMode = love.filesystem.read("accessibility_mode.txt")
|
||||||
|
if savedMode then
|
||||||
|
savedMode = savedMode:match("[%w_]+")
|
||||||
|
if savedMode == "deuteranopia_high_contrast" then savedMode = "deuteranopia" end
|
||||||
|
for index, mode in ipairs(accessibilityModes) do
|
||||||
|
if mode.id == savedMode then
|
||||||
|
accessibilityModeIndex = index
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local mode = accessibilityModes[accessibilityModeIndex]
|
||||||
|
accessibilityMessage = "Color filter: " .. mode.label .. " (F4 to change)"
|
||||||
|
accessibilityMessageExpires = love.timer.getTime() + 4
|
||||||
|
end
|
||||||
|
|
||||||
|
function cycleAccessibilityMode()
|
||||||
|
accessibilityModeIndex = accessibilityModeIndex % #accessibilityModes + 1
|
||||||
|
local mode = accessibilityModes[accessibilityModeIndex]
|
||||||
|
love.filesystem.write("accessibility_mode.txt", mode.id)
|
||||||
|
accessibilityMessage = "Color filter: " .. mode.label .. " (F4 to change)"
|
||||||
|
accessibilityMessageExpires = love.timer.getTime() + 2.5
|
||||||
|
end
|
||||||
|
|
||||||
function _initSounds()
|
function _initSounds()
|
||||||
staticWave = sfxr.newSound()
|
staticWave = sfxr.newSound()
|
||||||
staticWave.waveform = sfxr.WAVEFORM.NOISE -- was `wavetype = 3`; field renamed in this sfxr
|
staticWave.waveform = sfxr.WAVEFORM.NOISE -- was `wavetype = 3`; field renamed in this sfxr
|
||||||
|
|
|
||||||
73
shaders.lua
73
shaders.lua
|
|
@ -57,4 +57,75 @@ glow = love.graphics.newShader[[
|
||||||
}
|
}
|
||||||
return pixel_me;
|
return pixel_me;
|
||||||
}
|
}
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
-- Accessibility post-process. The puzzle's RGB channels are normally also
|
||||||
|
-- its visual language, so this runs only after the entire scene has been
|
||||||
|
-- composed. It leaves neutral UI/text alone and remaps saturated colours to
|
||||||
|
-- a palette whose red, green, and yellow accents remain distinct for people
|
||||||
|
-- with deuteranopia.
|
||||||
|
accessibilityColorShader = love.graphics.newShader[[
|
||||||
|
extern number accessibilityTime;
|
||||||
|
|
||||||
|
vec3 rgbToHsv(vec3 c) {
|
||||||
|
float maximum = max(c.r, max(c.g, c.b));
|
||||||
|
float minimum = min(c.r, min(c.g, c.b));
|
||||||
|
float delta = maximum - minimum;
|
||||||
|
float hue = 0.0;
|
||||||
|
|
||||||
|
if (delta > 0.0001) {
|
||||||
|
if (maximum == c.r) {
|
||||||
|
hue = mod((c.g - c.b) / delta, 6.0);
|
||||||
|
} else if (maximum == c.g) {
|
||||||
|
hue = ((c.b - c.r) / delta) + 2.0;
|
||||||
|
} else {
|
||||||
|
hue = ((c.r - c.g) / delta) + 4.0;
|
||||||
|
}
|
||||||
|
hue = hue / 6.0;
|
||||||
|
if (hue < 0.0) hue += 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float saturation = maximum <= 0.0 ? 0.0 : delta / maximum;
|
||||||
|
return vec3(hue, saturation, maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
||||||
|
vec4 pixel = Texel(texture, texture_coords) * color;
|
||||||
|
vec3 hsv = rgbToHsv(pixel.rgb);
|
||||||
|
|
||||||
|
// Preserve greys, white (including additive RGB overlap), and near-black
|
||||||
|
// background detail. Only colourful puzzle elements are recoloured.
|
||||||
|
if (hsv.y < 0.22 || hsv.z < 0.06) return pixel;
|
||||||
|
|
||||||
|
// Keep every additive pair in its own bucket. In particular, magenta
|
||||||
|
// (red + blue) must not become the same colour as red, and cyan
|
||||||
|
// (green + blue) must not become the same colour as green.
|
||||||
|
vec3 replacement;
|
||||||
|
float pattern = 0.0;
|
||||||
|
// Advance in whole pixels so the texture stays crisp instead of swimming.
|
||||||
|
float ditherStep = floor(accessibilityTime * 1.5);
|
||||||
|
if (hsv.x < 0.055 || hsv.x >= 0.94) {
|
||||||
|
replacement = vec3(0.93, 0.18, 0.50); // red: pink
|
||||||
|
} else if (hsv.x < 0.19) {
|
||||||
|
replacement = vec3(1.00, 0.78, 0.08); // red + green: gold
|
||||||
|
vec2 cell = mod(floor(screen_coords + vec2(ditherStep, 0.0)), vec2(9.0));
|
||||||
|
pattern = length(cell - vec2(4.0)) < 1.6 ? 1.0 : 0.0; // dithered dots
|
||||||
|
} else if (hsv.x < 0.43) {
|
||||||
|
replacement = vec3(0.00, 0.72, 0.62); // green: turquoise
|
||||||
|
} else if (hsv.x < 0.58) {
|
||||||
|
replacement = vec3(0.12, 0.55, 0.96); // green + blue: sky blue
|
||||||
|
pattern = mod(floor(screen_coords.x + screen_coords.y + ditherStep), 10.0) < 2.0 ? 1.0 : 0.0; // dithered slashes
|
||||||
|
} else if (hsv.x < 0.77) {
|
||||||
|
replacement = vec3(0.38, 0.32, 0.96); // blue: indigo
|
||||||
|
} else {
|
||||||
|
replacement = vec3(0.72, 0.25, 0.92); // red + blue: purple
|
||||||
|
pattern = mod(floor((screen_coords.x + ditherStep) / 4.0) + floor(screen_coords.y / 4.0), 2.0); // dithered checker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary colours get a gently animated dither cue, so they are
|
||||||
|
// identifiable even when their hue is hard to perceive. Lighting and
|
||||||
|
// fades remain.
|
||||||
|
replacement = mix(replacement, vec3(1.0), pattern * 0.55);
|
||||||
|
return vec4(replacement * hsv.z, pixel.a);
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue