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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.org +401 -0
- data/lib/kodachroma/color/attributes.rb +59 -0
- data/lib/kodachroma/color/modifiers.rb +124 -0
- data/lib/kodachroma/color/serializers.rb +184 -0
- data/lib/kodachroma/color.rb +112 -0
- data/lib/kodachroma/color_modes.rb +55 -0
- data/lib/kodachroma/converters/base.rb +34 -0
- data/lib/kodachroma/converters/hsl_converter.rb +55 -0
- data/lib/kodachroma/converters/hsv_converter.rb +49 -0
- data/lib/kodachroma/converters/rgb_converter.rb +72 -0
- data/lib/kodachroma/errors.rb +8 -0
- data/lib/kodachroma/harmonies.rb +139 -0
- data/lib/kodachroma/helpers/bounders.rb +50 -0
- data/lib/kodachroma/palette_builder.rb +80 -0
- data/lib/kodachroma/rgb_generator/base.rb +10 -0
- data/lib/kodachroma/rgb_generator/from_hex_string_values.rb +63 -0
- data/lib/kodachroma/rgb_generator/from_hsl.rb +19 -0
- data/lib/kodachroma/rgb_generator/from_hsl_values.rb +25 -0
- data/lib/kodachroma/rgb_generator/from_hsv.rb +19 -0
- data/lib/kodachroma/rgb_generator/from_hsv_values.rb +25 -0
- data/lib/kodachroma/rgb_generator/from_rgb.rb +19 -0
- data/lib/kodachroma/rgb_generator/from_rgb_values.rb +27 -0
- data/lib/kodachroma/rgb_generator/from_string.rb +89 -0
- data/lib/kodachroma/rgb_generator.rb +38 -0
- data/lib/kodachroma/version.rb +4 -0
- data/lib/kodachroma.rb +130 -0
- data/lib/support/named_colors.yml +149 -0
- metadata +186 -0
@@ -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,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
|