kodachroma 1.0.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.
@@ -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