Skip to content

Instantly share code, notes, and snippets.

@scmmishra
Last active November 8, 2023 14:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scmmishra/8e2fa38c75bf6fb598ec022018cd271f to your computer and use it in GitHub Desktop.
Save scmmishra/8e2fa38c75bf6fb598ec022018cd271f to your computer and use it in GitHub Desktop.
Contrast colors with Ruby
module ColorHelper
def hex_to_rgb(hex_color)
# Remove the '#' character if it's there
hex_color = hex_color.tr('#', '')
# Split the hex color string into the RGB components
r = hex_color[0..1].to_i(16)
g = hex_color[2..3].to_i(16)
b = hex_color[4..5].to_i(16)
[r, g, b]
end
def rgb_to_hex(red, green, blue)
hex = [red, green, blue].map do |color|
color = 255 if color > 255
color = 0 if color.negative?
color.round.to_s(16).rjust(2, '0')
end.join.upcase
"##{hex}".downcase
end
def get_luminance(color)
return 0 if color == 'transparent'
r, g, b = hex_to_rgb(color)
(0.2126 * channel_to_luminance(r)) + (0.7152 * channel_to_luminance(g)) + (0.0722 * channel_to_luminance(b))
end
def get_contrast(color1, color2)
luminance1 = get_luminance(color1)
luminance2 = get_luminance(color2)
if luminance1 > luminance2
(luminance1 + 0.05) / (luminance2 + 0.05)
else
(luminance2 + 0.05) / (luminance1 + 0.05)
end
end
def mix(color1, color2, weight)
# Parse colors and normalize
r1, g1, b1 = hex_to_rgb(color1).map { |c| c / 255.0 }
r2, g2, b2 = hex_to_rgb(color2).map { |c| c / 255.0 }
# Mixing algorithm from Sass implementation
weight1, weight2 = get_weight_for_mixing(weight)
# Calculate final color values
r = ((r1 * weight1) + (r2 * weight2)) * 255
g = ((g1 * weight1) + (g2 * weight2)) * 255
b = ((b1 * weight1) + (b2 * weight2)) * 255
# Convert final values to a CSS RGBA color string
rgb_to_hex(r, g, b)
end
def adjust_color_for_contrast(color, background_color, target_ratio)
max_iterations = 20
adjusted_color = color
max_iterations.times do
current_ratio = get_contrast(adjusted_color, background_color)
break if current_ratio >= target_ratio
adjustment_direction = get_luminance(adjusted_color) < 0.5 ? '#ffffff' : '#151718'
adjusted_color = mix(adjusted_color, adjustment_direction, 0.05)
end
adjusted_color
end
private
def get_weight_for_mixing(weight)
normalized_weight = (weight * 2) - 1
weight2 = (normalized_weight + 1) / 2
weight1 = 1 - weight2
[weight1, weight2]
end
def channel_to_luminance(channel)
channel /= 255.0
if channel <= 0.03928
channel / 12.92
else
((channel + 0.055) / 1.055)**2.4
end
end
end
require 'rails_helper'
# the values for these test cases are based off https://github.com/ricokahler/color2k/
RSpec.describe ColorHelper do
describe '#hex_to_rgb' do
it 'converts a hex color to RGB' do
pairs = [
['#fefce8', [254, 252, 232]],
['#fef9c3', [254, 249, 195]],
['#fde047', [253, 224, 71]],
['#ffe047', [255, 224, 71]],
['#ffff47', [255, 255, 71]],
['#000f47', [0, 15, 71]]
]
pairs.each do |pair|
expect(helper.hex_to_rgb(pair[0])).to eq(pair[1])
end
end
end
describe '#rgb_to_hex' do
it 'converts RGB to a hex color' do
pairs = [
['#fefce8', [254, 252, 232]],
['#fef9c3', [254, 249, 195]],
['#fde047', [253, 224, 71]],
['#ffe047', [255, 224, 71]],
['#ffff47', [255, 255, 71]],
['#000f47', [0, 15, 71]]
]
pairs.each do |pair|
expect(helper.rgb_to_hex(*pair[1])).to eq(pair[0])
end
end
end
describe '#get_luminance' do
it 'gets luminance' do
pairs = [
['#fefce8', 0.9651783305430648],
['#fef9c3', 0.9276232470289122],
['#fde047', 0.7464888809386645],
['#ffe047', 0.7502624139378438],
['#ffff47', 0.9323493232745587],
['#000f47', 0.007965800403950861]
]
pairs.each do |pair|
expect(helper.get_luminance(pair[0])).to be_within(0.0001).of(pair[1])
end
end
end
describe '#get_contrast' do
it 'gets contrast' do
pairs = [
['#fff7ed', '#082f49', 13.071630965576853],
['#ffedd5', '#0c4a6e', 8.25405437296673],
['#fed7aa', '#075985', 5.587542088216559],
['#fdba74', '#0369a1', 3.518444977234888],
['#fb923c', '#0284c7', 1.8094825558825534],
['#f97316', '#0ea5e9', 1.01141749353127],
['#ea580c', '#38bdf8', 1.66155548862194],
['#c2410c', '#7dd3fc', 3.1059042598363495],
['#9a3412', '#bae6fd', 5.506356434201567],
['#7c2d12', '#e0f2fe', 8.166263099299048],
['#431407', '#f0f9ff', 14.681391442562134]
]
pairs.each do |pair|
expect(helper.get_contrast(pair[0], pair[1])).to be_within(0.0001).of(pair[2])
end
end
end
describe '#mix' do
it 'mixes at 0.5' do
pairs = [
['#fff7ed', '#082f49', '#84939b'],
['#ffedd5', '#0c4a6e', '#869ca2'],
['#fed7aa', '#075985', '#839898'],
['#fdba74', '#0369a1', '#80928b'],
['#fb923c', '#0284c7', '#7e8b82'],
['#f97316', '#0ea5e9', '#848c80'],
['#ea580c', '#38bdf8', '#918b82'],
['#c2410c', '#7dd3fc', '#a08a84'],
['#9a3412', '#bae6fd', '#aa8d88'],
['#7c2d12', '#e0f2fe', '#ae9088'],
['#431407', '#f0f9ff', '#9a8783']
]
pairs.each do |pair|
expect(helper.mix(pair[0], pair[1], 0.5)).to eq(pair[2])
end
end
it 'mixes at 0.2' do
pairs = [
['#fff7ed', '#082f49', '#cecfcc'],
['#ffedd5', '#0c4a6e', '#ceccc0'],
['#fed7aa', '#075985', '#cdbea3'],
['#fdba74', '#0369a1', '#cbaa7d'],
['#fb923c', '#0284c7', '#c98f58'],
['#f97316', '#0ea5e9', '#ca7d40'],
['#ea580c', '#38bdf8', '#c66c3b'],
['#c2410c', '#7dd3fc', '#b45e3c'],
['#9a3412', '#bae6fd', '#a05841'],
['#7c2d12', '#e0f2fe', '#905441'],
['#431407', '#f0f9ff', '#664239']
]
pairs.each do |pair|
expect(helper.mix(pair[0], pair[1], 0.2)).to eq(pair[2])
end
end
end
describe '#adjust_color_for_contrast' do
target_ratio = 3.1
it 'adjusts a color to meet the contrast ratio against a light background' do
color = '#ff0000'
background_color = '#ffffff'
adjusted_color = helper.adjust_color_for_contrast(color, background_color, target_ratio)
ratio = helper.get_contrast(adjusted_color, background_color)
expect(ratio).to be >= target_ratio
end
it 'adjusts a color to meet the contrast ratio against a dark background' do
color = '#00ff00'
background_color = '#000000'
adjusted_color = helper.adjust_color_for_contrast(color, background_color, target_ratio)
ratio = helper.get_contrast(adjusted_color, background_color)
expect(ratio).to be >= target_ratio
end
it 'returns a string representation of the color' do
color = '#00ff00'
background_color = '#000000'
adjusted_color = helper.adjust_color_for_contrast(color, background_color, target_ratio)
expect(adjusted_color).to be_a(String)
end
it 'handles cases where the color already meets the contrast ratio' do
color = '#000000'
background_color = '#ffffff'
adjusted_color = helper.adjust_color_for_contrast(color, background_color, target_ratio)
ratio = helper.get_contrast(adjusted_color, background_color)
expect(ratio).to be >= target_ratio
expect(adjusted_color).to eq(color)
end
it 'does not modify a color that already exceeds the contrast ratio' do
color = '#000000'
background_color = '#ffffff'
adjusted_color = helper.adjust_color_for_contrast(color, background_color, target_ratio)
expect(adjusted_color).to eq(color)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment