kodachroma 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module Kodachroma
3
+ module Converters
4
+ # Base class for converting one color mode to another.
5
+ # @abstract
6
+ class Base
7
+ include Helpers::Bounders
8
+
9
+ # @param input [ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv]
10
+ # @return [Base]
11
+ def initialize(input)
12
+ @input = input
13
+ end
14
+
15
+ # @param rgb [ColorModes::Rgb]
16
+ # @return [ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv]
17
+ def self.convert_rgb(rgb)
18
+ new(rgb).convert_rgb
19
+ end
20
+
21
+ # @param hsl [ColorModes::Hsl]
22
+ # @return [ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv]
23
+ def self.convert_hsl(hsl)
24
+ new(hsl).convert_hsl
25
+ end
26
+
27
+ # @param hsv [ColorModes::Hsv]
28
+ # @return [ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv]
29
+ def self.convert_hsv(hsv)
30
+ new(hsv).convert_hsv
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ module Kodachroma
3
+ module Converters
4
+ # Class to convert a color mode to {ColorModes::Hsl}.
5
+ class HslConverter < Base
6
+ # Convert rgb to hsl.
7
+ # @return [ColorModes::Hsl]
8
+ def convert_rgb
9
+ r = bound01(@input.r, 255)
10
+ g = bound01(@input.g, 255)
11
+ b = bound01(@input.b, 255)
12
+
13
+ rgb_array = [r, g, b]
14
+
15
+ max = rgb_array.max
16
+ min = rgb_array.min
17
+ l = (max + min) * 0.5
18
+
19
+ if max == min
20
+ h = s = 0
21
+ else
22
+ d = (max - min).to_f
23
+
24
+ s = if l > 0.5
25
+ d / (2 - max - min)
26
+ else
27
+ d / (max + min)
28
+ end
29
+
30
+ h = case max
31
+ when r then ((g - b) / d) + (g < b ? 6 : 0)
32
+ when g then ((b - r) / d) + 2
33
+ when b then ((r - g) / d) + 4
34
+ end
35
+
36
+ h /= 6.0
37
+ end
38
+
39
+ ColorModes::Hsl.new(h * 360, s, l, @input.a)
40
+ end
41
+
42
+ # Returns @input because it's the same color mode.
43
+ # @return [ColorModes::Hsl]
44
+ def convert_hsl
45
+ @input
46
+ end
47
+
48
+ # Convert hsv to hsl.
49
+ # @return [ColorModes::Hsl]
50
+ def convert_hsv
51
+ HsvConverter.convert_rgb(RgbConverter.convert_hsl(@input))
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ module Kodachroma
3
+ module Converters
4
+ # Class to convert a color mode to {ColorModes::Hsl}.
5
+ class HsvConverter < Base
6
+ # Convert rgb to hsv.
7
+ # @return [ColorModes::Hsv]
8
+ def convert_rgb
9
+ r = bound01(@input.r, 255)
10
+ g = bound01(@input.g, 255)
11
+ b = bound01(@input.b, 255)
12
+
13
+ rgb_array = [r, g, b]
14
+
15
+ max = rgb_array.max
16
+ min = rgb_array.min
17
+ v = max
18
+ d = (max - min).to_f
19
+ s = max.zero? ? 0 : d / max
20
+
21
+ if max == min
22
+ h = 0
23
+ else
24
+ h = case max
25
+ when r then ((g - b) / d) + (g < b ? 6 : 0)
26
+ when g then ((b - r) / d) + 2
27
+ when b then ((r - g) / d) + 4
28
+ end
29
+
30
+ h /= 6.0
31
+ end
32
+
33
+ ColorModes::Hsv.new(h * 360, s, v, @input.a)
34
+ end
35
+
36
+ # Convert hsl to hsv.
37
+ # @return [ColorModes::Hsv]
38
+ def convert_hsl
39
+ HslConverter.convert_rgb(RgbConverter.convert_hsv(@input))
40
+ end
41
+
42
+ # Returns @input because it's the same color mode.
43
+ # @return [ColorModes::Hsv]
44
+ def convert_hsv
45
+ @input
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+ module Kodachroma
3
+ module Converters
4
+ # Class to convert a color mode to {ColorModes::Rgb}.
5
+ class RgbConverter < Base
6
+ # Returns @input because it's the same color mode.
7
+ # @return [ColorModes::Rgb]
8
+ def convert_rgb
9
+ @input
10
+ end
11
+
12
+ # Convert hsl to rgb.
13
+ # @return [ColorModes::Rgb]
14
+ def convert_hsl
15
+ h, s, l = @input
16
+
17
+ h = bound01(h, 360)
18
+ s = bound01(s, 100)
19
+ l = bound01(l, 100)
20
+
21
+ if s.zero?
22
+ r = g = b = l * 255
23
+ else
24
+ q = l < 0.5 ? l * (1 + s) : l + s - (l * s)
25
+ p = (2 * l) - q
26
+ r = hue_to_rgb(p, q, h + (1 / 3.0)) * 255
27
+ g = hue_to_rgb(p, q, h) * 255
28
+ b = hue_to_rgb(p, q, h - (1 / 3.0)) * 255
29
+ end
30
+
31
+ ColorModes::Rgb.new(r, g, b, bound_alpha(@input.a))
32
+ end
33
+
34
+ # Convert hsv to rgb.
35
+ # @return [ColorModes::Rgb]
36
+ def convert_hsv
37
+ h, s, v = @input
38
+
39
+ h = bound01(h, 360) * 6
40
+ s = bound01(s, 100)
41
+ v = bound01(v, 100)
42
+
43
+ i = h.floor
44
+ f = h - i
45
+ p = v * (1 - s)
46
+ q = v * (1 - (f * s))
47
+ t = v * (1 - ((1 - f) * s))
48
+ mod = i % 6
49
+
50
+ r = [v, q, p, p, t, v][mod] * 255
51
+ g = [t, v, v, q, p, p][mod] * 255
52
+ b = [p, p, t, v, v, q][mod] * 255
53
+
54
+ ColorModes::Rgb.new(r, g, b, bound_alpha(@input.a))
55
+ end
56
+
57
+ private
58
+
59
+ def hue_to_rgb(p, q, t)
60
+ if t.negative? then t += 1
61
+ elsif t > 1 then t -= 1
62
+ end
63
+
64
+ if t < 1 / 6.0 then p + ((q - p) * 6 * t)
65
+ elsif t < 0.5 then q
66
+ elsif t < 2 / 3.0 then p + ((q - p) * ((2 / 3.0) - t) * 6)
67
+ else p
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ module Kodachroma
3
+ module Errors
4
+ class PaletteDefinedError < StandardError; end
5
+
6
+ class UnrecognizedColor < StandardError; end
7
+ end
8
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+ module Kodachroma
3
+ # Class to hold all palette methods.
4
+ class Harmonies
5
+ # @param color [Color]
6
+ def initialize(color)
7
+ @color = color
8
+ end
9
+
10
+ # Generate a complement palette.
11
+ #
12
+ # @example
13
+ # 'red'.paint.palette.complement #=> [red, cyan]
14
+ # 'red'.paint.palette.complement(as: :name) #=> ['red', 'cyan']
15
+ # 'red'.paint.palette.complement(as: :hex) #=> ['#ff0000', '#00ffff']
16
+ #
17
+ # @param options [Hash]
18
+ # @option options :as [Symbol] (nil) optional format to output colors as strings
19
+ # @return [Array<Color>, Array<String>] depending on presence of `options[:as]`
20
+ def complement(options = {})
21
+ with_reformat([@color, @color.complement], options[:as])
22
+ end
23
+
24
+ # Generate a triad palette.
25
+ #
26
+ # @example
27
+ # 'red'.paint.palette.triad #=> [red, lime, blue]
28
+ # 'red'.paint.palette.triad(as: :name) #=> ['red', 'lime', 'blue']
29
+ # 'red'.paint.palette.triad(as: :hex) #=> ['#ff0000', '#00ff00', '#0000ff']
30
+ #
31
+ # @param options [Hash]
32
+ # @option options :as [Symbol] (nil) optional format to output colors as strings
33
+ # @return [Array<Color>, Array<String>] depending on presence of `options[:as]`
34
+ def triad(options = {})
35
+ hsl_map([0, 120, 240], options)
36
+ end
37
+
38
+ # Generate a tetrad palette.
39
+ #
40
+ # @example
41
+ # 'red'.paint.palette.tetrad #=> [red, #80ff00, cyan, #7f00ff]
42
+ # 'red'.paint.palette.tetrad(as: :name) #=> ['red', '#80ff00', 'cyan', '#7f00ff']
43
+ # 'red'.paint.palette.tetrad(as: :hex) #=> ['#ff0000', '#80ff00', '#00ffff', '#7f00ff']
44
+ #
45
+ # @param options [Hash]
46
+ # @option options :as [Symbol] (nil) optional format to output colors as strings
47
+ # @return [Array<Color>, Array<String>] depending on presence of `options[:as]`
48
+ def tetrad(options = {})
49
+ hsl_map([0, 90, 180, 270], options)
50
+ end
51
+
52
+ # Generate a split complement palette.
53
+ #
54
+ # @example
55
+ # 'red'.paint.palette.split_complement #=> [red, #ccff00, #0066ff]
56
+ # 'red'.paint.palette.split_complement(as: :name) #=> ['red', '#ccff00', '#0066ff']
57
+ # 'red'.paint.palette.split_complement(as: :hex) #=> ['#ff0000', '#ccff00', '#0066ff']
58
+ #
59
+ # @param options [Hash]
60
+ # @option options :as [Symbol] (nil) optional format to output colors as strings
61
+ # @return [Array<Color>, Array<String>] depending on presence of `options[:as]`
62
+ def split_complement(options = {})
63
+ hsl_map([0, 72, 216], options)
64
+ end
65
+
66
+ # Generate an analogous palette.
67
+ #
68
+ # @example
69
+ # 'red'.paint.palette.analogous #=> [red, #ff0066, #ff0033, red, #ff3300, #ff6600]
70
+ # 'red'.paint.palette.analogous(as: :hex) #=> ['#f00', '#f06', '#f03', '#f00', '#f30', '#f60']
71
+ # 'red'.paint.palette.analogous(size: 3) #=> [red, #ff001a, #ff1a00]
72
+ # 'red'.paint.palette.analogous(size: 3, slice_by: 60) #=> [red, #ff000d, #ff0d00]
73
+ #
74
+ # @param options [Hash]
75
+ # @option options :size [Symbol] (6) number of results to return
76
+ # @option options :slice_by [Symbol] (30)
77
+ # the angle in degrees to slice the hue circle per color
78
+ # @option options :as [Symbol] (nil) optional format to output colors as strings
79
+ # @return [Array<Color>, Array<String>] depending on presence of `options[:as]`
80
+ def analogous(options = {})
81
+ size = options[:size] || 6
82
+ slices = options[:slice_by] || 30
83
+
84
+ hsl = @color.hsl
85
+ part = 360 / slices
86
+ hsl.h = ((hsl.h - ((part * size) >> 1)) + 720) % 360
87
+
88
+ palette = (size - 1).times.reduce([@color]) do |arr, _n|
89
+ hsl.h = (hsl.h + part) % 360
90
+ arr << Color.new(hsl, @color.format)
91
+ end
92
+
93
+ with_reformat(palette, options[:as])
94
+ end
95
+
96
+ # Generate a monochromatic palette.
97
+ #
98
+ # @example
99
+ # 'red'.paint.palette.monochromatic #=> [red, #2a0000, #550000, maroon, #aa0000, #d40000]
100
+ # 'red'.paint.palette.monochromatic(as: :hex) #=> ['#ff0000', '#2a0000', '#550000', '#800000', '#aa0000', '#d40000']
101
+ # 'red'.paint.palette.monochromatic(size: 3) #=> [red, #550000, #aa0000]
102
+ #
103
+ # @param options [Hash]
104
+ # @option options :size [Symbol] (6) number of results to return
105
+ # @option options :as [Symbol] (nil) optional format to output colors as strings
106
+ # @return [Array<Color>, Array<String>] depending on presence of `options[:as]`
107
+ def monochromatic(options = {})
108
+ size = options[:size] || 6
109
+
110
+ h, s, v = @color.hsv
111
+ modification = 1.0 / size
112
+
113
+ palette = size.times.map do
114
+ Color.new(ColorModes::Hsv.new(h, s, v), @color.format).tap do
115
+ v = (v + modification) % 1
116
+ end
117
+ end
118
+
119
+ with_reformat(palette, options[:as])
120
+ end
121
+
122
+ private
123
+
124
+ def with_reformat(palette, as)
125
+ palette.map! { |color| color.to_s(as) } unless as.nil?
126
+ palette
127
+ end
128
+
129
+ def hsl_map(degrees, options)
130
+ h, s, l = @color.hsl
131
+
132
+ degrees.map! do |deg|
133
+ Color.new(ColorModes::Hsl.new((h + deg) % 360, s, l), @color.format)
134
+ end
135
+
136
+ with_reformat(degrees, options[:as])
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ module Kodachroma
3
+ module Helpers
4
+ module Bounders
5
+ # Bounds a value `n` that is from `0` to `max` to `0` to `1`.
6
+ #
7
+ # @param n [Numeric, String]
8
+ # @param max [Fixnum]
9
+ # @return [Float]
10
+ def bound01(n, max)
11
+ is_percent = n.to_s.include? '%'
12
+ n = [max, [0, n.to_f].max].min
13
+ n = (n * max).to_i / 100.0 if is_percent
14
+
15
+ return 1 if (n - max).abs < 0.000001
16
+
17
+ (n % max) / max.to_f
18
+ end
19
+
20
+ # Ensure alpha value `a` is between `0` and `1`.
21
+ #
22
+ # @param a [Numeric, String] alpha value
23
+ # @return [Numeric]
24
+ def bound_alpha(a)
25
+ a = a.to_f
26
+ a = 1 if a.negative? || a > 1
27
+ a
28
+ end
29
+
30
+ # Ensures a number between `0` and `1`. Returns `n` if it is between `0`
31
+ # and `1`.
32
+ #
33
+ # @param n [Numeric]
34
+ # @return [Numeric]
35
+ def clamp01(n)
36
+ [1, [0, n].max].min
37
+ end
38
+
39
+ # Converts `n` to a percentage type value.
40
+ #
41
+ # @param n [Numeric, String]
42
+ # @return [String, Float]
43
+ def to_percentage(n)
44
+ n = n.to_f
45
+ n = "#{ n * 100 }%" if n <= 1
46
+ n
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+ module Kodachroma
3
+ # Class internally used to build custom palettes from {Kodachroma.define_palette}.
4
+ class PaletteBuilder
5
+ # Wrapper to instantiate a new instance of {PaletteBuilder} and call its
6
+ # {PaletteBuilder#build} method.
7
+ #
8
+ # @param block [Proc] the palette definition block
9
+ # @return [PaletteBuilder::PaletteEvaluator] lazy palette generator
10
+ def self.build(&block)
11
+ new(&block).build
12
+ end
13
+
14
+ # @param block [Proc] the palette definition block
15
+ def initialize(&block)
16
+ @block = block
17
+ end
18
+
19
+ # Build the custom palette
20
+ # @return [PaletteBuilder::PaletteEvaluator] lazy palette generator
21
+ def build
22
+ dsl = PaletteBuilderDsl.new
23
+ dsl.instance_eval(&@block)
24
+ dsl.evaluator
25
+ end
26
+
27
+ # Internal class for delaying evaluating a color to generate a
28
+ # final palette
29
+ class PaletteEvaluator
30
+ def initialize
31
+ @conversions = []
32
+ end
33
+
34
+ def <<(conversion)
35
+ @conversions << conversion
36
+ end
37
+
38
+ def evaluate(color)
39
+ @conversions.map do |color_calls|
40
+ color_calls.evaluate(color)
41
+ end.unshift(color)
42
+ end
43
+ end
44
+
45
+ # Internal class for palette building DSL syntax.
46
+ class PaletteBuilderDsl
47
+ attr_reader :evaluator
48
+
49
+ def initialize
50
+ @evaluator = PaletteEvaluator.new
51
+ end
52
+
53
+ def method_missing(name, *args)
54
+ ColorCalls.new(name, args).tap do |color_calls|
55
+ @evaluator << color_calls
56
+ end
57
+ end
58
+
59
+ # Internal class to represent color modification calls in the palette
60
+ # builder DSL definition syntax.
61
+ class ColorCalls
62
+ attr_reader :name, :args
63
+
64
+ def initialize(name, args)
65
+ @calls = [[name, args]]
66
+ end
67
+
68
+ def evaluate(color)
69
+ @calls.reduce(color) do |c, (name, args)|
70
+ c.send(name, *args)
71
+ end
72
+ end
73
+
74
+ def method_missing(name, *args)
75
+ @calls << [name, args]
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end