threedaymonk-colormath 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ ColorMath
2
+ =========
3
+
4
+ A simple Ruby library to perform operations on RGB and HSL colours.
5
+
6
+ Usage
7
+ -----
8
+
9
+ Instantiate an RGB (red, green, blue) colour:
10
+
11
+ orange = ColorMath::RGB.new(1.0, 0.5, 0)
12
+
13
+ Or from a hex value via a helper method:
14
+
15
+ white = ColorMath::hex_color("#fff")
16
+ blue = ColorMath::hex_color("#0000ff")
17
+
18
+ Instantiate an HSL (hue, saturation, luminance) colour:
19
+
20
+ pink = ColorMath::HSL.new(350, 1, 0.88)
21
+
22
+ Retrieve the RGB components of a colour:
23
+
24
+ pink.red # => 1.0
25
+ pink.green # => 0.76
26
+ pink.blue # => 0.8
27
+
28
+ Or the HSL components:
29
+
30
+ orange.hue # => 30.0
31
+ orange.saturation # => 1.0
32
+ orange.luminance # => 0.5
33
+
34
+ Combine two colours via an alpha blend, e.g. 30% orange on white:
35
+
36
+ combined = ColorMath::Blend.alpha(white, orange, 0.3)
37
+
38
+ Convert a colour to hexadecimal representation:
39
+
40
+ combined.hex # => "#ffd8b2"
41
+
42
+ That’s it. It only does the basics that I need for the job in hand, but it’s probably a good basis for extension.
data/lib/colormath.rb ADDED
@@ -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
data/test/hsl_test.rb ADDED
@@ -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
data/test/rgb_test.rb ADDED
@@ -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,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: threedaymonk-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 -07: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
+ - README.md
35
+ - test/blend_test.rb
36
+ - test/conversion_test.rb
37
+ - test/hex_decoding_test.rb
38
+ - test/hsl_test.rb
39
+ - test/rgb_test.rb
40
+ - lib/colormath
41
+ - lib/colormath/blend.rb
42
+ - lib/colormath/color
43
+ - lib/colormath/color/hsl.rb
44
+ - lib/colormath/color/rgb.rb
45
+ - lib/colormath/color.rb
46
+ - lib/colormath/version.rb
47
+ - lib/colormath.rb
48
+ has_rdoc: false
49
+ homepage:
50
+ post_install_message:
51
+ rdoc_options: []
52
+
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project:
70
+ rubygems_version: 1.2.0
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: Colour manipulation library for Ruby
74
+ test_files: []
75
+