Skip to content

Instantly share code, notes, and snippets.

@tobiashm
Last active March 24, 2024 01:03
Show Gist options
  • Save tobiashm/243103 to your computer and use it in GitHub Desktop.
Save tobiashm/243103 to your computer and use it in GitHub Desktop.
contrast-color for SASS
Gem::Specification.new do |spec|
spec.name = 'contrast-color'
spec.version = '0.1.1'
spec.platform = Gem::Platform::RUBY
spec.author = 'Tobias H. Michaelsen'
spec.email = '[email protected]'
spec.summary = 'Generate contrast colors in SASS'
spec.description = 'A simple extension to SASS that alows you to calculate colors that conforms to WAI and WCAG20 contrast requirements.'
spec.homepage = 'https://gist.github.com/243103'
spec.license = 'MIT'
spec.files = ['contrast-color.rb']
spec.test_file = 'test.rb'
spec.require_path = '.'
spec.add_dependency('sass')
end
require 'sass'
module ContrastColor
BRIGHTNESS_COEFS = [0.299, 0.587, 0.114]
LUMINANCE_COEFS = [0.2126, 0.7152, 0.0722]
module Color
def diff(other)
# W3C
Utils.sum(Utils.abs(rgb, other.rgb))
end
def diff_alt(other)
# 3D - Sqrt(dr^2+dg^2+db^2)
Math.sqrt(Utils.sum(Utils.sq(Utils.abs(rgb, other.rgb))))
end
def brightness
# W3C; Rec. 601 luma
Utils.sum(Utils.mul(rgb, BRIGHTNESS_COEFS))
end
def brightness_alt
# http://alienryderflex.com/hsp.html
Math.sqrt(Utils.sum(Utils.mul(Utils.sq(rgb), [0.241, 0.691, 0.068])))
end
def luminance
# http://www.w3.org/TR/WCAG20/#relativeluminancedef
norm_rgb = rgb.map { |value| value.to_f / 255 }
Utils.sum(Utils.mul(norm_rgb.map { |v| v <= 0.03928 ? v/12.92 : ((v+0.055)/1.055) ** 2.4 }, LUMINANCE_COEFS))
end
end
module Functions
def contrast_color(color, seed_color = nil)
seed_color ||= color
assert_type color, :Color
assert_type seed_color, :Color
direction = color.brightness > 127 ? darken_method : lighten_method
new_color = seed_color
percentage = 0.0
until conform(new_color, color) or percentage > 100.0 do
amount = Sass::Script::Number.new percentage, ['%']
new_color = self.send direction, seed_color, amount
percentage += 0.1
end
new_color
end
# http://www.w3.org/WAI/ER/WD-AERT/#color-contrast
MIN_BRIGHT_DIFF = 125
MIN_COLOR_DIFF = 500
def conform(color1, color2, wcag20_level = :aa)
wcag20_level = :aa unless /^aaa?(?:_large)?$/ === wcag20_level.to_s
bright_diff = (color1.brightness - color2.brightness).abs
color_diff = color1.diff(color2)
bright_diff >= MIN_BRIGHT_DIFF && color_diff >= MIN_COLOR_DIFF && send("wcag20_conform_#{wcag20_level}".to_sym, color1, color2)
end
# http://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast
MIN_CONTRAST_RATE_AA = 4.5
MIN_CONTRAST_RATE_AA_LARGE_TEXT = 3
MIN_CONTRAST_RATE_AAA = 7
MIN_CONTRAST_RATE_AAA_LARGE_TEXT = 4.5
def wcag20_conform_aa(color1, color2)
contrast_ratio(color1, color2) >= MIN_CONTRAST_RATE_AA
end
alias :wcag20_conform :wcag20_conform_aa
def wcag20_conform_aa_large(color1, color2)
contrast_ratio(color1, color2) >= MIN_CONTRAST_RATE_AA_LARGE_TEXT
end
def wcag20_conform_aaa(color1, color2)
contrast_ratio(color1, color2) >= MIN_CONTRAST_RATE_AAA
end
def wcag20_conform_aaa_large(color1, color2)
contrast_ratio(color1, color2) >= MIN_CONTRAST_RATE_AAA_LARGE_TEXT
end
def contrast_ratio(color1, color2)
assert_type color1, :Color
assert_type color2, :Color
l1, l2 = color1.luminance, color2.luminance
l2, l1 = l1, l2 if l2 > l1
(l1 + 0.05) / (l2 + 0.05)
end
private
def lighten_method
respond_to?(:add_brightness) ? :add_brightness : :lighten
end
def darken_method
respond_to?(:detract_brightness) ? :detract_brightness : :darken
end
end
module Utils
def self.abs(array, other)
array.zip(other).map { |x, y| (x.to_f - y.to_f).abs }
end
def self.mul(array, other)
array.zip(other).map { |x, y| x.to_f * y.to_f }
end
def self.sum(array)
array.inject(0) { |sum, value| sum + value.to_f }
end
def self.sq(array)
array.map { |e| e ** 2 }
end
end
end
module Sass::Script
class Color
include ContrastColor::Color
end
module Functions
include ContrastColor::Functions
end
end
require 'minitest/autorun'
require './contrast-color'
describe ContrastColor do
it "calculates brightness" do
white = Sass::Script::Color.new [255, 255, 255]
white.brightness.must_be_within_delta 255.0
grey = Sass::Script::Color.new [0xe5, 0xe5, 0xe5]
grey.brightness.must_be_within_delta 0.9*255, 1.0
end
it 'should generate colors with enough contrast' do
data = File.read(__FILE__).split('__END__').last
css = Sass::Engine.new(data, :syntax => :scss).to_css
rules = css.gsub(/\s+/, ' ').gsub(/\} a/, '}\na').split('\n')
puts rules
rules.each { |rule| rule.must_match(/a \{ color: (#?\w+); background: \1; \}/) }
end
end
__END__
a {
color: contrast_color(white, #ff0000);
background: #ee0000;
}
a {
color: contrast_color(rgb(127,127,127));
background: white;
}
a {
color: contrast_color(rgb(128,128,128));
background: black;
}
a {
color: contrast_color(white);
background: #585858;
}
a {
color: contrast_color(black);
background: #a7a7a7;
}
a {
color: contrast_color(red);
background: white;
}
a {
color: contrast_color(green);
background: #c3ffc3;
}
a {
color: contrast_color(blue);
background: #fafaff;
}
a {
color: contrast_color(#123456);
background: #c3dbf2;
}
@tobiashm
Copy link
Author

This is now part of the sass-extras gem https://github.com/tobiashm/sass-extras

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment