abachrome 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.
- checksums.yaml +7 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +5 -0
- data/README.md +99 -0
- data/demos/ncurses/plasma.rb +124 -0
- data/devenv.lock +100 -0
- data/devenv.nix +51 -0
- data/devenv.yaml +15 -0
- data/lib/abachrome/abc_decimal.rb +161 -0
- data/lib/abachrome/color.rb +74 -0
- data/lib/abachrome/color_mixins/blend.rb +45 -0
- data/lib/abachrome/color_mixins/lighten.rb +39 -0
- data/lib/abachrome/color_mixins/to_colorspace.rb +38 -0
- data/lib/abachrome/color_mixins/to_lrgb.rb +49 -0
- data/lib/abachrome/color_mixins/to_oklab.rb +48 -0
- data/lib/abachrome/color_mixins/to_oklch.rb +48 -0
- data/lib/abachrome/color_mixins/to_srgb.rb +63 -0
- data/lib/abachrome/color_models/hsv.rb +22 -0
- data/lib/abachrome/color_models/oklab.rb +16 -0
- data/lib/abachrome/color_models/oklch.rb +47 -0
- data/lib/abachrome/color_models/rgb.rb +28 -0
- data/lib/abachrome/color_space.rb +97 -0
- data/lib/abachrome/converter.rb +59 -0
- data/lib/abachrome/converters/base.rb +57 -0
- data/lib/abachrome/converters/lrgb_to_oklab.rb +27 -0
- data/lib/abachrome/converters/lrgb_to_srgb.rb +30 -0
- data/lib/abachrome/converters/oklab_to_lrgb.rb +42 -0
- data/lib/abachrome/converters/oklab_to_oklch.rb +23 -0
- data/lib/abachrome/converters/oklab_to_srgb.rb +17 -0
- data/lib/abachrome/converters/oklch_to_lrgb.rb +15 -0
- data/lib/abachrome/converters/oklch_to_oklab.rb +23 -0
- data/lib/abachrome/converters/oklch_to_srgb.rb +18 -0
- data/lib/abachrome/converters/srgb_to_lrgb.rb +27 -0
- data/lib/abachrome/converters/srgb_to_oklab.rb +15 -0
- data/lib/abachrome/converters/srgb_to_oklch.rb +18 -0
- data/lib/abachrome/gamut/base.rb +72 -0
- data/lib/abachrome/gamut/p3.rb +25 -0
- data/lib/abachrome/gamut/rec2020.rb +23 -0
- data/lib/abachrome/gamut/srgb.rb +27 -0
- data/lib/abachrome/illuminants/base.rb +33 -0
- data/lib/abachrome/illuminants/d50.rb +31 -0
- data/lib/abachrome/illuminants/d55.rb +27 -0
- data/lib/abachrome/illuminants/d65.rb +35 -0
- data/lib/abachrome/illuminants/d75.rb +27 -0
- data/lib/abachrome/named/css.rb +164 -0
- data/lib/abachrome/outputs/css.rb +117 -0
- data/lib/abachrome/palette.rb +131 -0
- data/lib/abachrome/palette_mixins/interpolate.rb +31 -0
- data/lib/abachrome/palette_mixins/resample.rb +59 -0
- data/lib/abachrome/palette_mixins/stretch_luminance.rb +70 -0
- data/lib/abachrome/parsers/hex.rb +50 -0
- data/lib/abachrome/to_abcd.rb +13 -0
- data/lib/abachrome/version.rb +5 -0
- data/lib/abachrome.rb +99 -0
- metadata +172 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module ColorMixins
|
5
|
+
module Lighten
|
6
|
+
def lighten(amount = 0.1)
|
7
|
+
amount = AbcDecimal(amount)
|
8
|
+
oklab = to_oklab
|
9
|
+
l, a, b = oklab.coordinates
|
10
|
+
|
11
|
+
new_l = l + amount
|
12
|
+
new_l = AbcDecimal("1.0") if new_l > 1
|
13
|
+
new_l = AbcDecimal("0.0") if new_l.negative?
|
14
|
+
|
15
|
+
Color.new(
|
16
|
+
ColorSpace.find(:oklab),
|
17
|
+
[new_l, a, b],
|
18
|
+
alpha
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def lighten!(amount = 0.1)
|
23
|
+
lightened = lighten(amount)
|
24
|
+
@color_space = lightened.color_space
|
25
|
+
@coordinates = lightened.coordinates
|
26
|
+
@alpha = lightened.alpha
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def darken(amount = 0.1)
|
31
|
+
lighten(-amount)
|
32
|
+
end
|
33
|
+
|
34
|
+
def darken!(amount = 0.1)
|
35
|
+
lighten!(-amount)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module ColorMixins
|
5
|
+
module ToColorspace
|
6
|
+
def to_color_space(target_space)
|
7
|
+
return self if color_space == target_space
|
8
|
+
|
9
|
+
Converter.convert(self, target_space.name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_color_space!(target_space)
|
13
|
+
unless color_space == target_space
|
14
|
+
converted = to_color_space(target_space)
|
15
|
+
@color_space = converted.color_space
|
16
|
+
@coordinates = converted.coordinates
|
17
|
+
end
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def convert_to(space_name)
|
22
|
+
to_color_space(ColorSpace.find(space_name))
|
23
|
+
end
|
24
|
+
|
25
|
+
def convert_to!(space_name)
|
26
|
+
to_color_space!(ColorSpace.find(space_name))
|
27
|
+
end
|
28
|
+
|
29
|
+
def in_color_space(space_name)
|
30
|
+
convert_to(space_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def in_color_space!(space_name)
|
34
|
+
convert_to!(space_name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../converter"
|
4
|
+
|
5
|
+
module Abachrome
|
6
|
+
module ColorMixins
|
7
|
+
module ToLrgb
|
8
|
+
def to_lrgb
|
9
|
+
return self if color_space.name == :lrgb
|
10
|
+
|
11
|
+
Converter.convert(self, :lrgb)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_lrgb!
|
15
|
+
unless color_space.name == :lrgb
|
16
|
+
lrgb_color = to_lrgb
|
17
|
+
@color_space = lrgb_color.color_space
|
18
|
+
@coordinates = lrgb_color.coordinates
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def lred
|
24
|
+
to_lrgb.coordinates[0]
|
25
|
+
end
|
26
|
+
|
27
|
+
def lgreen
|
28
|
+
to_lrgb.coordinates[1]
|
29
|
+
end
|
30
|
+
|
31
|
+
def lblue
|
32
|
+
to_lrgb.coordinates[2]
|
33
|
+
end
|
34
|
+
|
35
|
+
def lrgb_values
|
36
|
+
to_lrgb.coordinates
|
37
|
+
end
|
38
|
+
|
39
|
+
def rgb_array
|
40
|
+
to_rgb.coordinates.map { |c| (c * 255).round.clamp(0, 255) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def rgb_hex
|
44
|
+
r, g, b = rgb_array
|
45
|
+
format("#%02x%02x%02x", r, g, b)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../converter"
|
4
|
+
|
5
|
+
module Abachrome
|
6
|
+
module ColorMixins
|
7
|
+
module ToOklab
|
8
|
+
def to_oklab
|
9
|
+
return self if color_space.name == :oklab
|
10
|
+
|
11
|
+
Converter.convert(self, :oklab)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_oklab!
|
15
|
+
unless color_space.name == :oklab
|
16
|
+
oklab_color = to_oklab
|
17
|
+
@color_space = oklab_color.color_space
|
18
|
+
@coordinates = oklab_color.coordinates
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def lightness
|
24
|
+
to_oklab.coordinates[0]
|
25
|
+
end
|
26
|
+
|
27
|
+
def l
|
28
|
+
to_oklab.coordinates[0]
|
29
|
+
end
|
30
|
+
|
31
|
+
def a
|
32
|
+
to_oklab.coordinates[1]
|
33
|
+
end
|
34
|
+
|
35
|
+
def b
|
36
|
+
to_oklab.coordinates[2]
|
37
|
+
end
|
38
|
+
|
39
|
+
def oklab_values
|
40
|
+
to_oklab.coordinates
|
41
|
+
end
|
42
|
+
|
43
|
+
def oklab_array
|
44
|
+
to_oklab.coordinates
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module ColorMixins
|
5
|
+
module ToOklch
|
6
|
+
def to_oklch
|
7
|
+
return self if color_space.name == :oklch
|
8
|
+
|
9
|
+
if color_space.name == :oklab
|
10
|
+
Converters::OklabToOklch.convert(self)
|
11
|
+
else
|
12
|
+
# For other color spaces, convert to OKLab first
|
13
|
+
oklab_color = to_oklab
|
14
|
+
Converters::OklabToOklch.convert(oklab_color)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_oklch!
|
19
|
+
unless color_space.name == :oklch
|
20
|
+
oklch_color = to_oklch
|
21
|
+
@color_space = oklch_color.color_space
|
22
|
+
@coordinates = oklch_color.coordinates
|
23
|
+
end
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def lightness
|
28
|
+
to_oklch.coordinates[0]
|
29
|
+
end
|
30
|
+
|
31
|
+
def chroma
|
32
|
+
to_oklch.coordinates[1]
|
33
|
+
end
|
34
|
+
|
35
|
+
def hue
|
36
|
+
to_oklch.coordinates[2]
|
37
|
+
end
|
38
|
+
|
39
|
+
def oklch_values
|
40
|
+
to_oklch.coordinates
|
41
|
+
end
|
42
|
+
|
43
|
+
def oklch_array
|
44
|
+
to_oklch.coordinates
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../converter"
|
4
|
+
|
5
|
+
module Abachrome
|
6
|
+
module ColorMixins
|
7
|
+
module ToSrgb
|
8
|
+
def to_srgb
|
9
|
+
return self if color_space.name == :srgb
|
10
|
+
|
11
|
+
Converter.convert(self, :srgb)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_rgb
|
15
|
+
# assume they mean srgb
|
16
|
+
to_srgb
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_srgb!
|
20
|
+
unless color_space.name == :srgb
|
21
|
+
srgb_color = to_srgb
|
22
|
+
@color_space = srgb_color.color_space
|
23
|
+
@coordinates = srgb_color.coordinates
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_rgb!
|
29
|
+
# assume they mean srgb
|
30
|
+
to_srgb!
|
31
|
+
end
|
32
|
+
|
33
|
+
def red
|
34
|
+
to_srgb.coordinates[0]
|
35
|
+
end
|
36
|
+
|
37
|
+
def green
|
38
|
+
to_srgb.coordinates[1]
|
39
|
+
end
|
40
|
+
|
41
|
+
def blue
|
42
|
+
to_srgb.coordinates[2]
|
43
|
+
end
|
44
|
+
|
45
|
+
def srgb_values
|
46
|
+
to_srgb.coordinates
|
47
|
+
end
|
48
|
+
|
49
|
+
def rgb_values
|
50
|
+
to_srgb.coordinates
|
51
|
+
end
|
52
|
+
|
53
|
+
def rgb_array
|
54
|
+
to_srgb.coordinates.map { |c| (c * 255).round.clamp(0, 255) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def rgb_hex
|
58
|
+
r, g, b = rgb_array
|
59
|
+
format("#%02x%02x%02x", r, g, b)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module ColorModels
|
5
|
+
class HSV < Base
|
6
|
+
#
|
7
|
+
# Internally, we use 0..1.0 values for hsv, unlike the standard 0..360, 0..255, 0..255.
|
8
|
+
#
|
9
|
+
# Values can be converted for output.
|
10
|
+
#
|
11
|
+
|
12
|
+
register :hsv, "HSV", %w[hue saturation value]
|
13
|
+
|
14
|
+
def valid_coordinates?(coordinates)
|
15
|
+
h, s, v = coordinates
|
16
|
+
h >= 0 && h <= 1.0 &&
|
17
|
+
s >= 0 && s <= 1.0 &&
|
18
|
+
v >= 0 && v <= 1.0
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module ColorModels
|
5
|
+
class Oklch
|
6
|
+
def self.normalize(l, c, h)
|
7
|
+
l = AbcDecimal(l)
|
8
|
+
c = AbcDecimal(c)
|
9
|
+
h = AbcDecimal(h)
|
10
|
+
|
11
|
+
# Normalize hue to 0-360 range
|
12
|
+
h -= 360 while h >= 360
|
13
|
+
h += 360 while h.negative?
|
14
|
+
|
15
|
+
# Normalize lightness and chroma to 0-1 range
|
16
|
+
l = l.clamp(0, 1)
|
17
|
+
c = c.clamp(0, 1)
|
18
|
+
|
19
|
+
[l, c, h]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.to_oklab(l, c, h)
|
23
|
+
# Convert OKLCH to OKLab
|
24
|
+
h_rad = h * Math::PI / 180
|
25
|
+
a = c * Math.cos(h_rad)
|
26
|
+
b = c * Math.sin(h_rad)
|
27
|
+
[l, a, b]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.from_oklab(l, a, b)
|
31
|
+
# Convert OKLab to OKLCH
|
32
|
+
c = Math.sqrt((a * a) + (b * b))
|
33
|
+
h = Math.atan2(b, a) * 180 / Math::PI
|
34
|
+
h += 360 if h.negative?
|
35
|
+
[l, c, h]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
ColorSpace.register(
|
42
|
+
:oklch,
|
43
|
+
"OKLCh",
|
44
|
+
%w[lightness chroma hue],
|
45
|
+
nil,
|
46
|
+
["ok-lch"]
|
47
|
+
)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module ColorModels
|
5
|
+
class RGB
|
6
|
+
class << self
|
7
|
+
def normalize(r, g, b)
|
8
|
+
[r, g, b].map do |value|
|
9
|
+
case value
|
10
|
+
when String
|
11
|
+
if value.end_with?("%")
|
12
|
+
AbcDecimal(value.chomp("%")) / AbcDecimal(100)
|
13
|
+
else
|
14
|
+
AbcDecimal(value) / AbcDecimal(255)
|
15
|
+
end
|
16
|
+
when Numeric
|
17
|
+
if value > 1
|
18
|
+
AbcDecimal(value) / AbcDecimal(255)
|
19
|
+
else
|
20
|
+
AbcDecimal(value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
class ColorSpace
|
5
|
+
class << self
|
6
|
+
def registry
|
7
|
+
@registry ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(name, &block)
|
11
|
+
registry[name.to_sym] = new(name, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def alias(name, aliased_name)
|
15
|
+
registry[aliased_name.to_sym] = registry[name.to_sym]
|
16
|
+
end
|
17
|
+
|
18
|
+
def find(name)
|
19
|
+
registry[name.to_sym] or raise ArgumentError, "Unknown color space: #{name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :name, :coordinates, :white_point, :color_model
|
24
|
+
|
25
|
+
def initialize(name)
|
26
|
+
@name = name.to_sym
|
27
|
+
yield self if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
def coordinates=(*coords)
|
31
|
+
@coordinates = coords.flatten
|
32
|
+
end
|
33
|
+
|
34
|
+
def white_point=(point)
|
35
|
+
@white_point = point.to_sym
|
36
|
+
end
|
37
|
+
|
38
|
+
def color_model=(model)
|
39
|
+
@color_model = model.to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
def ==(other)
|
43
|
+
return false unless other.is_a?(ColorSpace)
|
44
|
+
|
45
|
+
name == other.name
|
46
|
+
end
|
47
|
+
|
48
|
+
def eql?(other)
|
49
|
+
self == other
|
50
|
+
end
|
51
|
+
|
52
|
+
def hash
|
53
|
+
name.hash
|
54
|
+
end
|
55
|
+
|
56
|
+
def id
|
57
|
+
name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ColorSpace.register(:srgb) do |s|
|
62
|
+
s.coordinates = %i[red green blue]
|
63
|
+
s.white_point = :D65
|
64
|
+
s.color_model = :srgb
|
65
|
+
end
|
66
|
+
ColorSpace.alias(:srgb, :rgb)
|
67
|
+
|
68
|
+
ColorSpace.register(:lrgb) do |s|
|
69
|
+
s.coordinates = %i[red green blue]
|
70
|
+
s.white_point = :D65
|
71
|
+
s.color_model = :lrgb
|
72
|
+
end
|
73
|
+
|
74
|
+
ColorSpace.register(:hsl) do |s|
|
75
|
+
s.coordinates = %i[hue saturation lightness]
|
76
|
+
s.white_point = :D65
|
77
|
+
s.color_model = :hsl
|
78
|
+
end
|
79
|
+
|
80
|
+
ColorSpace.register(:lab) do |s|
|
81
|
+
s.coordinates = %i[lightness a b]
|
82
|
+
s.white_point = :D65
|
83
|
+
s.color_model = :lab
|
84
|
+
end
|
85
|
+
|
86
|
+
ColorSpace.register(:oklab) do |s|
|
87
|
+
s.coordinates = %i[lightness a b]
|
88
|
+
s.white_point = :D65
|
89
|
+
s.color_model = :oklab
|
90
|
+
end
|
91
|
+
|
92
|
+
ColorSpace.register(:oklch) do |s|
|
93
|
+
s.coordinates = %i[lightness chroma hue]
|
94
|
+
s.white_point = :D65
|
95
|
+
s.color_model = :oklch
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
class Converter
|
5
|
+
class << self
|
6
|
+
def registry
|
7
|
+
@registry ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(from_space, to_space, converter_class)
|
11
|
+
registry[[from_space.to_s, to_space.to_s]] = converter_class
|
12
|
+
end
|
13
|
+
|
14
|
+
def convert(color, to_space_name)
|
15
|
+
to_space = ColorSpace.find(to_space_name)
|
16
|
+
return color if color.color_space == to_space
|
17
|
+
|
18
|
+
# convert model first
|
19
|
+
to_model = to_space.color_model
|
20
|
+
converter = find_converter(color.color_space.color_model, to_model.to_s)
|
21
|
+
raise "No converter found from #{color.color_space.color_model} to #{to_model}" unless converter
|
22
|
+
|
23
|
+
converter.convert(color)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Automatically register all converters in the Converters namespace
|
27
|
+
def register_all_converters
|
28
|
+
Converters.constants.each do |const_name|
|
29
|
+
const = Converters.const_get(const_name)
|
30
|
+
next unless const.is_a?(Class)
|
31
|
+
|
32
|
+
# Parse from_space and to_space from class name (e.g., LrgbToOklab)
|
33
|
+
next unless const_name.to_s =~ /^(.+)To(.+)$/
|
34
|
+
|
35
|
+
from_space = ::Regexp.last_match(1).downcase.to_sym
|
36
|
+
to_space = ::Regexp.last_match(2).downcase.to_sym
|
37
|
+
|
38
|
+
# Register the converter
|
39
|
+
register(from_space, to_space, const)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def find_converter(from_space_name, to_space_name)
|
46
|
+
registry[[from_space_name.to_s, to_space_name.to_s]]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Load all converter files
|
52
|
+
converters_path = File.join(__dir__, "converters", "*.rb")
|
53
|
+
Dir[converters_path].each do |file|
|
54
|
+
require file
|
55
|
+
end
|
56
|
+
|
57
|
+
# Auto-register all converters
|
58
|
+
Converter.register_all_converters
|
59
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module Converters
|
5
|
+
class Base
|
6
|
+
attr_reader :from_space, :to_space
|
7
|
+
|
8
|
+
def initialize(from_space, to_space)
|
9
|
+
@from_space = from_space
|
10
|
+
@to_space = to_space
|
11
|
+
end
|
12
|
+
|
13
|
+
def convert(color)
|
14
|
+
raise NotImplementedError, "Subclasses must implement #convert"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.raise_unless(color, model)
|
18
|
+
return if color.color_space.color_model == model
|
19
|
+
|
20
|
+
raise "#{color} is #{color.color_space.color_model}), expecting #{model}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def can_convert?(color)
|
24
|
+
color.color_space == from_space
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.register(from_space_id, to_space_id, converter_class)
|
28
|
+
@converters ||= {}
|
29
|
+
@converters[[from_space_id, to_space_id]] = converter_class
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.find_converter(from_space_id, to_space_id)
|
33
|
+
@converters ||= {}
|
34
|
+
@converters[[from_space_id, to_space_id]]
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.convert(color, to_space)
|
38
|
+
converter_class = find_converter(color.color_space.id, to_space.id)
|
39
|
+
unless converter_class
|
40
|
+
raise ConversionError,
|
41
|
+
"No converter found from #{color.color_space.name} to #{to_space.name}"
|
42
|
+
end
|
43
|
+
|
44
|
+
converter = converter_class.new(color.color_space, to_space)
|
45
|
+
converter.convert(color)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def validate_color!(color)
|
51
|
+
return if can_convert?(color)
|
52
|
+
|
53
|
+
raise ArgumentError, "Cannot convert color from #{color.color_space.name} (expected #{from_space.name})"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module Converters
|
5
|
+
class LrgbToOklab < Abachrome::Converters::Base
|
6
|
+
def self.convert(rgb_color)
|
7
|
+
raise_unless rgb_color, :lrgb
|
8
|
+
|
9
|
+
r, g, b = rgb_color.coordinates.map { |_| AbcDecimal(_) }
|
10
|
+
|
11
|
+
l = (AD("0.41222147079999993") * r) + (AD("0.5363325363") * g) + (AD("0.0514459929") * b)
|
12
|
+
m = (AD("0.2119034981999999") * r) + (AD("0.680699545099999") * g) + (AD("0.1073969566") * b)
|
13
|
+
s = (AD("0.08830246189999998") * r) + (AD("0.2817188376") * g) + (AD("0.6299787005000002") * b)
|
14
|
+
|
15
|
+
l_ = AbcDecimal(l)**Rational(1, 3)
|
16
|
+
m_ = AbcDecimal(m)**Rational(1, 3)
|
17
|
+
s_ = AbcDecimal(s)**Rational(1, 3)
|
18
|
+
|
19
|
+
lightness = (AD("0.2104542553") * l_) + (AD("0.793617785") * m_) - (AD("0.0040720468") * s_)
|
20
|
+
a = (AD("1.9779984951") * l_) - (AD("2.4285922050") * m_) + (AD("0.4505937099") * s_)
|
21
|
+
b = (AD("0.0259040371") * l_) + (AD("0.7827717662") * m_) - (AD("0.8086757660") * s_)
|
22
|
+
|
23
|
+
Color.new(ColorSpace.find(:oklab), [lightness, a, b], rgb_color.alpha)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module Converters
|
5
|
+
class LrgbToSrgb < Abachrome::Converters::Base
|
6
|
+
def self.convert(lrgb_color)
|
7
|
+
raise_unless lrgb_color, :lrgb
|
8
|
+
r, g, b = lrgb_color.coordinates.map { |c| to_srgb(AbcDecimal(c)) }
|
9
|
+
|
10
|
+
output_coords = [r, g, b]
|
11
|
+
|
12
|
+
Color.new(
|
13
|
+
ColorSpace.find(:srgb),
|
14
|
+
output_coords,
|
15
|
+
lrgb_color.alpha
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.to_srgb(v)
|
20
|
+
v_abs = v.abs
|
21
|
+
v_sign = v.negative? ? -1 : 1
|
22
|
+
if v_abs <= AD("0.0031308")
|
23
|
+
v * AD("12.92")
|
24
|
+
else
|
25
|
+
v_sign * ((AD("1.055") * (v_abs**Rational(1.0, 2.4))) - AD("0.055"))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|