colormath 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ module ColorMath
2
+ ParsingError = Class.new(RuntimeError)
3
+
4
+ # Instantiate an RGB colour from a 3- or 6-digit hexadecimal representation.
5
+ # "#abc", "#abcdef", "abc", and "abcdef" are all valid.
6
+ #
7
+ # Invalid representations will raise a ParsingError.
8
+ #
9
+ def hex_color(s)
10
+ if m = s.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
11
+ RGB.new( m[1].to_i(16) / 255.0,
12
+ m[2].to_i(16) / 255.0,
13
+ m[3].to_i(16) / 255.0 )
14
+ elsif m = s.match(/^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i)
15
+ RGB.new( (m[1] + m[1]).to_i(16) / 255.0,
16
+ (m[2] + m[2]).to_i(16) / 255.0,
17
+ (m[3] + m[3]).to_i(16) / 255.0 )
18
+ else
19
+ raise ParsingError, "invalid hex sequence '#{s}'"
20
+ end
21
+ end
22
+
23
+ extend self
24
+ end
25
+
26
+ require "colormath/color"
27
+ require "colormath/blend"
28
+ require "colormath/version"
@@ -0,0 +1,23 @@
1
+ module ColorMath
2
+
3
+ # Blend two or more colours and return a new colour.
4
+ #
5
+ module Blend
6
+
7
+ # Blend ca with cb. alpha represents the proportion of cb,
8
+ # i.e. alpha = 0 => ca; alpha = 1 => cb.
9
+ #
10
+ def alpha(ca, cb, alpha)
11
+ for_rgb(ca, cb){ |a, b| (alpha * b + (1 - alpha) * a) }
12
+ end
13
+
14
+ extend self
15
+
16
+ private
17
+ def for_rgb(a, b, &blk)
18
+ RGB.new(*([:red, :green, :blue].map{ |channel|
19
+ blk.call(a.__send__(channel), b.__send__(channel))
20
+ }))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ module ColorMath
2
+
3
+ # Color can be mixed into any class that responds to red, green, and blue, where 0 <= c <= 1
4
+ #
5
+ module Color
6
+ EPSILON = 1/256.0
7
+
8
+ # Returns true if the RGB components of the two colours differ by less than EPSILON,
9
+ # i.e. they are the same when represented in 8 bits.
10
+ #
11
+ def ==(other)
12
+ deltas = [ other.red - self.red,
13
+ other.green - self.green,
14
+ other.blue - self.blue ].map{ |e| e.abs }
15
+ deltas.max < EPSILON
16
+ end
17
+
18
+ # The six-digit hexadecimal representation of the colour, e.g. "#cafe66"
19
+ #
20
+ def hex
21
+ "#%02x%02x%02x" % [red * 0xff, green * 0xff, blue * 0xff]
22
+ end
23
+
24
+ def inspect(*args)
25
+ "<%s r=%0.3f g=%0.3f b=%0.3f>" % [self.class.to_s, red, green, blue]
26
+ end
27
+
28
+ private
29
+ def force_range(v, min, max)
30
+ [[min, v].max, max].min
31
+ end
32
+ end
33
+ end
34
+
35
+ require "colormath/color/rgb"
36
+ require "colormath/color/hsl"
@@ -0,0 +1,78 @@
1
+ module ColorMath
2
+
3
+ # A colour represented and stored as hue, saturation and luminance components
4
+ #
5
+ class HSL
6
+ include Color
7
+
8
+ attr_reader :hue, :saturation, :luminance, :alpha
9
+
10
+ # Initialize an HSL colour where:
11
+ # 0 <= h <= 360
12
+ # 0 <= s <= 1
13
+ # 0 <= l <= 1
14
+ #
15
+ # Values outside these ranges will be clippped.
16
+ #
17
+ def initialize(h, s, l)
18
+ @hue = force_range(h, 0, 360).to_f
19
+ @saturation = force_range(s, 0, 1).to_f
20
+ @luminance = force_range(l, 0, 1).to_f
21
+ end
22
+
23
+ # The red component of the colour in RGB representation where 0 <= r <= 1
24
+ #
25
+ def red
26
+ t = component(hk + (1/3.0))
27
+ end
28
+
29
+ # The green component of the colour in RGB representation where 0 <= g <= 1
30
+ #
31
+ def green
32
+ t = component(hk)
33
+ end
34
+
35
+ # The blue component of the colour in RGB representation where 0 <= b <= 1
36
+ #
37
+ def blue
38
+ t = component(hk - (1/3.0))
39
+ end
40
+
41
+ private
42
+ def hk
43
+ hue / 360.0
44
+ end
45
+
46
+ def q
47
+ @q ||= if luminance < 0.5
48
+ luminance * (1.0 + saturation)
49
+ else
50
+ luminance + saturation - (luminance * saturation)
51
+ end
52
+ end
53
+
54
+ def p
55
+ @p ||= 2 * luminance - q
56
+ end
57
+
58
+ def component(t)
59
+ t = if t < 0
60
+ t + 1.0
61
+ elsif t > 1
62
+ t - 1.0
63
+ else
64
+ t
65
+ end
66
+
67
+ if t < (1/6.0)
68
+ p + ((q - p) * 6.0 * t)
69
+ elsif (1/6.0) <= t && t < 0.5
70
+ q
71
+ elsif 0.5 <= t && t < (2/3.0)
72
+ p + ((q - p) * 6.0 * (2/3.0 - t))
73
+ else
74
+ p
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,63 @@
1
+ module ColorMath
2
+
3
+ # A colour represented and stored as red, green and blue components
4
+ #
5
+ class RGB
6
+ include Color
7
+
8
+ attr_reader :red, :green, :blue
9
+
10
+ # Initialize an RGB colour where:
11
+ # 0 <= r <= 1
12
+ # 0 <= g <= 1
13
+ # 0 <= b <= 1
14
+ #
15
+ # Values outside these ranges will be clippped.
16
+ #
17
+ def initialize(r, g, b)
18
+ @red = force_range(r, 0, 1).to_f
19
+ @green = force_range(g, 0, 1).to_f
20
+ @blue = force_range(b, 0, 1).to_f
21
+ end
22
+
23
+ # The hue component of the colour in HSL representation where 0 <= h < 360
24
+ #
25
+ def hue
26
+ case max
27
+ when red
28
+ (60.0 * ((green - blue) / (max - min))) % 360.0
29
+ when green
30
+ 60.0 * ((blue - red) / (max - min)) + 120.0
31
+ when blue
32
+ 60.0 * ((red - green) / (max - min)) + 240.0
33
+ end
34
+ end
35
+
36
+ # The saturation component of the colour in HSL representation where 0 <= s <= 1
37
+ #
38
+ def saturation
39
+ if max == min
40
+ 0
41
+ elsif luminance <= 0.5
42
+ (max - min) / (2.0 * luminance)
43
+ else
44
+ (max - min) / (2.0 - 2.0 * luminance)
45
+ end
46
+ end
47
+
48
+ # The luminance component of the colour in HSL representation where 0 <= l <= 1
49
+ #
50
+ def luminance
51
+ 0.5 * (max + min)
52
+ end
53
+
54
+ private
55
+ def min
56
+ [red, green, blue].min
57
+ end
58
+
59
+ def max
60
+ [red, green, blue].max
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ module ColorMath
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require "test/unit"
3
+ require "shoulda"
4
+ require "colormath"
5
+
6
+ class AlphaBlendTest < Test::Unit::TestCase
7
+ def blend(a, b, alpha)
8
+ ColorMath::Blend.alpha(ColorMath::hex_color(a), ColorMath::hex_color(b), alpha).hex
9
+ end
10
+
11
+ context "with sample colors" do
12
+ setup do
13
+ @background = "#ffffff"
14
+ @foreground = "#862e8b"
15
+ end
16
+
17
+ should "blend 0% sample" do
18
+ assert_equal @background, blend(@background, @foreground, 0.0)
19
+ end
20
+
21
+ should "blend 10% sample" do
22
+ assert_equal "#f2eaf3", blend(@background, @foreground, 0.1)
23
+ end
24
+
25
+ should "blend 30% sample" do
26
+ assert_equal "#dac0dc", blend(@background, @foreground, 0.3)
27
+ end
28
+
29
+ should "blend 100% sample" do
30
+ assert_equal @foreground, blend(@background, @foreground, 1.0)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,104 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require "test/unit"
3
+ require "shoulda"
4
+ require "colormath"
5
+
6
+ class ConversionTest < Test::Unit::TestCase
7
+ EPSILON = 1e-2
8
+
9
+ EDGE_CASES = [
10
+ [[1, 0, 0], [ 0, 1, 0.5 ]],
11
+ [[0.5, 1, 0.5], [120, 1, 0.75]],
12
+ [[0, 0, 0.5], [240, 1, 0.25]],
13
+ ]
14
+
15
+ context "with edge cases" do
16
+ EDGE_CASES.each do |(r,g,b), (h,s,l)|
17
+ should "convert RGB(#{r},#{g},#{b}) to HSL(#{h},#{s},#{l})" do
18
+ c = ColorMath::RGB.new(r, g, b)
19
+ assert_in_delta h, c.hue, EPSILON
20
+ assert_in_delta s, c.saturation, EPSILON
21
+ assert_in_delta l, c.luminance, EPSILON
22
+ end
23
+
24
+ should "convert HSL(#{h},#{s},#{l}) to RGB(#{r},#{g},#{b})" do
25
+ c = ColorMath::HSL.new(h, s, l)
26
+ assert_in_delta r, c.red, EPSILON
27
+ assert_in_delta g, c.green, EPSILON
28
+ assert_in_delta b, c.blue, EPSILON
29
+ end
30
+ end
31
+ end
32
+
33
+ HSL_SAMPLES = [
34
+ [267.0, 0.14, 0.17],
35
+ [322.0, 0.22, 0.54],
36
+ [211.0, 0.54, 0.19],
37
+ [347.0, 0.90, 0.38],
38
+ [184.0, 0.75, 0.13],
39
+ [177.0, 0.07, 0.14],
40
+ [ 97.0, 0.93, 0.70],
41
+ [139.0, 0.04, 0.37],
42
+ [ 17.0, 0.88, 0.67],
43
+ [162.0, 0.21, 0.61],
44
+ [358.0, 0.78, 0.50],
45
+ [104.0, 0.78, 0.63],
46
+ [280.0, 0.38, 0.66],
47
+ [298.0, 0.06, 0.72],
48
+ [162.0, 0.39, 0.86],
49
+ [305.0, 0.55, 0.16],
50
+ [248.0, 0.80, 0.84],
51
+ [109.0, 0.23, 0.23],
52
+ [328.0, 0.88, 0.88],
53
+ [ 26.0, 0.99, 0.52],
54
+ ]
55
+
56
+ context "roundtripping HSL -> RGB -> HSL" do
57
+ HSL_SAMPLES.each do |h,s,l|
58
+ should "roundtrip HSL(#{h},#{s},#{l})" do
59
+ hsl = ColorMath::HSL.new(h, s, l)
60
+ rgb = ColorMath::RGB.new(hsl.red, hsl.green, hsl.blue)
61
+
62
+ assert_in_delta h, rgb.hue, EPSILON
63
+ assert_in_delta s, rgb.saturation, EPSILON
64
+ assert_in_delta l, rgb.luminance, EPSILON
65
+ end
66
+ end
67
+ end
68
+
69
+ RGB_SAMPLES = [
70
+ [0.19, 0.41, 0.70],
71
+ [0.13, 0.22, 0.28],
72
+ [0.45, 0.44, 0.24],
73
+ [0.96, 0.94, 0.24],
74
+ [0.76, 0.01, 0.16],
75
+ [0.55, 0.96, 0.01],
76
+ [0.07, 0.61, 0.73],
77
+ [0.05, 0.58, 0.51],
78
+ [0.43, 0.05, 0.24],
79
+ [0.66, 0.80, 0.80],
80
+ [0.54, 0.35, 0.10],
81
+ [0.12, 0.41, 0.27],
82
+ [0.78, 0.32, 0.93],
83
+ [0.52, 0.15, 0.43],
84
+ [0.17, 0.26, 0.53],
85
+ [0.19, 0.96, 0.66],
86
+ [0.54, 0.36, 0.84],
87
+ [0.12, 0.89, 0.60],
88
+ [0.75, 0.03, 0.83],
89
+ [0.09, 0.35, 0.83],
90
+ ]
91
+
92
+ context "roundtripping RGB -> HSL -> RGB" do
93
+ RGB_SAMPLES.each do |r,g,b|
94
+ should "roundtrip RGB(#{r},#{g},#{b})" do
95
+ rgb = ColorMath::RGB.new(r, g, b)
96
+ hsl = ColorMath::HSL.new(rgb.hue, rgb.saturation, rgb.luminance)
97
+
98
+ assert_in_delta r, hsl.red, EPSILON
99
+ assert_in_delta g, hsl.green, EPSILON
100
+ assert_in_delta b, hsl.blue, EPSILON
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,56 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require "test/unit"
3
+ require "shoulda"
4
+ require "colormath"
5
+
6
+ class HexDecodingTest < Test::Unit::TestCase
7
+ EPSILON = 1e-3
8
+
9
+ should "decode 6-digit hex string with leading #" do
10
+ c = ColorMath::hex_color("#abcd54")
11
+ assert_in_delta (0xab / 255.0), c.red, EPSILON
12
+ assert_in_delta (0xcd / 255.0), c.green, EPSILON
13
+ assert_in_delta (0x54 / 255.0), c.blue, EPSILON
14
+ end
15
+
16
+ should "decode 6-digit hex string without leading #" do
17
+ c = ColorMath::hex_color("abcd54")
18
+ assert_in_delta (0xab / 255.0), c.red, EPSILON
19
+ assert_in_delta (0xcd / 255.0), c.green, EPSILON
20
+ assert_in_delta (0x54 / 255.0), c.blue, EPSILON
21
+ end
22
+
23
+ should "decode 3-digit hex string with leading #" do
24
+ c = ColorMath::hex_color("#a1c")
25
+ assert_in_delta (0xaa / 255.0), c.red, EPSILON
26
+ assert_in_delta (0x11 / 255.0), c.green, EPSILON
27
+ assert_in_delta (0xcc / 255.0), c.blue, EPSILON
28
+ end
29
+
30
+ should "decode 3-digit hex string without leading #" do
31
+ c = ColorMath::hex_color("a1c")
32
+ assert_in_delta (0xaa / 255.0), c.red, EPSILON
33
+ assert_in_delta (0x11 / 255.0), c.green, EPSILON
34
+ assert_in_delta (0xcc / 255.0), c.blue, EPSILON
35
+ end
36
+
37
+ should "decode 6-digit hex string in upper case" do
38
+ c = ColorMath::hex_color("#ABCD54")
39
+ assert_in_delta (0xab / 255.0), c.red, EPSILON
40
+ assert_in_delta (0xcd / 255.0), c.green, EPSILON
41
+ assert_in_delta (0x54 / 255.0), c.blue, EPSILON
42
+ end
43
+
44
+ should "decode 3-digit hex string in upper case" do
45
+ c = ColorMath::hex_color("#A1C")
46
+ assert_in_delta (0xaa / 255.0), c.red, EPSILON
47
+ assert_in_delta (0x11 / 255.0), c.green, EPSILON
48
+ assert_in_delta (0xcc / 255.0), c.blue, EPSILON
49
+ end
50
+
51
+ should "raise a ParsingError when the hex string is invalid" do
52
+ assert_raises ColorMath::ParsingError do
53
+ ColorMath::hex_color("kjhhdfs")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,44 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require "test/unit"
3
+ require "shoulda"
4
+ require "colormath"
5
+
6
+ class HSLTest < Test::Unit::TestCase
7
+ should "be equal if initialized with same values" do
8
+ assert_equal ColorMath::HSL.new(123, 0.5, 0.7), ColorMath::HSL.new(123, 0.5, 0.7)
9
+ end
10
+
11
+ should "not be equal if initialized with different values" do
12
+ assert_not_equal ColorMath::HSL.new(124, 0.4, 0.8), ColorMath::HSL.new(123, 0.5, 0.7)
13
+ end
14
+
15
+ should "force hue >= 0" do
16
+ c = ColorMath::HSL.new(-2, 0, 0)
17
+ assert_equal 0, c.hue
18
+ end
19
+
20
+ should "force hue <= 360" do
21
+ c = ColorMath::HSL.new(361, 0, 0)
22
+ assert_equal 360, c.hue
23
+ end
24
+
25
+ should "force saturation >= 0" do
26
+ c = ColorMath::HSL.new(0, -1, 0)
27
+ assert_equal 0, c.saturation
28
+ end
29
+
30
+ should "force saturation <= 1" do
31
+ c = ColorMath::HSL.new(0, 1.1, 0)
32
+ assert_equal 1, c.saturation
33
+ end
34
+
35
+ should "force luminance >= 0" do
36
+ c = ColorMath::HSL.new(0, 0, -1)
37
+ assert_equal 0, c.luminance
38
+ end
39
+
40
+ should "force luminance <= 1" do
41
+ c = ColorMath::HSL.new(0, 0, 1.1)
42
+ assert_equal 1, c.luminance
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require "test/unit"
3
+ require "shoulda"
4
+ require "colormath"
5
+
6
+ class RGBTest < Test::Unit::TestCase
7
+ should "be equal if initialized with same values" do
8
+ assert_equal ColorMath::RGB.new(0.3, 0.5, 0.7), ColorMath::RGB.new(0.3, 0.5, 0.7)
9
+ end
10
+
11
+ should "not be equal if initialized with different values" do
12
+ assert_not_equal ColorMath::RGB.new(0.5, 0.4, 0.8), ColorMath::RGB.new(0.3, 0.5, 0.7)
13
+ end
14
+
15
+ should "force red >= 0" do
16
+ c = ColorMath::HSL.new(-2, 0, 0)
17
+ assert_equal 0, c.red
18
+ end
19
+
20
+ should "force red <= 1" do
21
+ c = ColorMath::RGB.new(1.1, 0, 0)
22
+ assert_equal 1, c.red
23
+ end
24
+
25
+ should "force green >= 0" do
26
+ c = ColorMath::RGB.new(0, -1, 0)
27
+ assert_equal 0, c.green
28
+ end
29
+
30
+ should "force green <= 1" do
31
+ c = ColorMath::RGB.new(0, 1.1, 0)
32
+ assert_equal 1, c.green
33
+ end
34
+
35
+ should "force blue >= 0" do
36
+ c = ColorMath::RGB.new(0, 0, -1)
37
+ assert_equal 0, c.blue
38
+ end
39
+
40
+ should "force blue <= 1" do
41
+ c = ColorMath::RGB.new(0, 0, 1.1)
42
+ assert_equal 1, c.blue
43
+ end
44
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: colormath
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Battley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-09 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description:
26
+ email: pbattley@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - test/blend_test.rb
35
+ - test/conversion_test.rb
36
+ - test/hex_decoding_test.rb
37
+ - test/hsl_test.rb
38
+ - test/rgb_test.rb
39
+ - lib/colormath/blend.rb
40
+ - lib/colormath/color/hsl.rb
41
+ - lib/colormath/color/rgb.rb
42
+ - lib/colormath/color.rb
43
+ - lib/colormath/version.rb
44
+ - lib/colormath.rb
45
+ has_rdoc: true
46
+ homepage:
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.3.4
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Colour manipulation library for Ruby
73
+ test_files: []
74
+