abachrome-float 0.1.6
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/.envrc +3 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +21 -0
- data/CLA.md +45 -0
- data/CODE-OF-CONDUCT.md +9 -0
- data/LICENSE +19 -0
- data/README.md +315 -0
- data/Rakefile +15 -0
- data/SECURITY.md +94 -0
- data/abachrome-float.gemspec +36 -0
- data/demos/ncurses/plasma.rb +124 -0
- data/devenv.lock +171 -0
- data/devenv.nix +52 -0
- data/devenv.yaml +8 -0
- data/lib/abachrome/color.rb +197 -0
- data/lib/abachrome/color_mixins/blend.rb +100 -0
- data/lib/abachrome/color_mixins/lighten.rb +90 -0
- data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
- data/lib/abachrome/color_mixins/to_colorspace.rb +107 -0
- data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
- data/lib/abachrome/color_mixins/to_lrgb.rb +121 -0
- data/lib/abachrome/color_mixins/to_oklab.rb +117 -0
- data/lib/abachrome/color_mixins/to_oklch.rb +110 -0
- data/lib/abachrome/color_mixins/to_srgb.rb +142 -0
- data/lib/abachrome/color_models/cmyk.rb +159 -0
- data/lib/abachrome/color_models/hsv.rb +49 -0
- data/lib/abachrome/color_models/lms.rb +38 -0
- data/lib/abachrome/color_models/oklab.rb +37 -0
- data/lib/abachrome/color_models/oklch.rb +91 -0
- data/lib/abachrome/color_models/rgb.rb +58 -0
- data/lib/abachrome/color_models/xyz.rb +31 -0
- data/lib/abachrome/color_models/yiq.rb +37 -0
- data/lib/abachrome/color_space.rb +199 -0
- data/lib/abachrome/converter.rb +117 -0
- data/lib/abachrome/converters/base.rb +128 -0
- data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
- data/lib/abachrome/converters/lms_to_lrgb.rb +40 -0
- data/lib/abachrome/converters/lms_to_srgb.rb +27 -0
- data/lib/abachrome/converters/lms_to_xyz.rb +34 -0
- data/lib/abachrome/converters/lrgb_to_lms.rb +3 -0
- data/lib/abachrome/converters/lrgb_to_oklab.rb +57 -0
- data/lib/abachrome/converters/lrgb_to_srgb.rb +59 -0
- data/lib/abachrome/converters/lrgb_to_xyz.rb +33 -0
- data/lib/abachrome/converters/oklab_to_lms.rb +44 -0
- data/lib/abachrome/converters/oklab_to_lrgb.rb +71 -0
- data/lib/abachrome/converters/oklab_to_oklch.rb +56 -0
- data/lib/abachrome/converters/oklab_to_srgb.rb +46 -0
- data/lib/abachrome/converters/oklch_to_lrgb.rb +79 -0
- data/lib/abachrome/converters/oklch_to_oklab.rb +52 -0
- data/lib/abachrome/converters/oklch_to_srgb.rb +46 -0
- data/lib/abachrome/converters/oklch_to_xyz.rb +70 -0
- data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
- data/lib/abachrome/converters/srgb_to_lrgb.rb +55 -0
- data/lib/abachrome/converters/srgb_to_oklab.rb +45 -0
- data/lib/abachrome/converters/srgb_to_oklch.rb +47 -0
- data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
- data/lib/abachrome/converters/xyz_to_lms.rb +34 -0
- data/lib/abachrome/converters/xyz_to_oklab.rb +42 -0
- data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
- data/lib/abachrome/gamut/base.rb +74 -0
- data/lib/abachrome/gamut/p3.rb +27 -0
- data/lib/abachrome/gamut/rec2020.rb +25 -0
- data/lib/abachrome/gamut/srgb.rb +49 -0
- data/lib/abachrome/illuminants/base.rb +35 -0
- data/lib/abachrome/illuminants/d50.rb +33 -0
- data/lib/abachrome/illuminants/d55.rb +29 -0
- data/lib/abachrome/illuminants/d65.rb +37 -0
- data/lib/abachrome/illuminants/d75.rb +29 -0
- data/lib/abachrome/named/css.rb +157 -0
- data/lib/abachrome/named/tailwind.rb +301 -0
- data/lib/abachrome/outputs/css.rb +119 -0
- data/lib/abachrome/palette.rb +244 -0
- data/lib/abachrome/palette_mixins/interpolate.rb +53 -0
- data/lib/abachrome/palette_mixins/resample.rb +61 -0
- data/lib/abachrome/palette_mixins/stretch_luminance.rb +72 -0
- data/lib/abachrome/parsers/css.rb +452 -0
- data/lib/abachrome/parsers/hex.rb +52 -0
- data/lib/abachrome/parsers/tailwind.rb +45 -0
- data/lib/abachrome/spectral.rb +276 -0
- data/lib/abachrome/to_abcd.rb +23 -0
- data/lib/abachrome/version.rb +7 -0
- data/lib/abachrome.rb +242 -0
- data/logo.png +0 -0
- data/logo.webp +0 -0
- data/security/assesments/2025-10-12-SECURITY_ASSESSMENT.md +53 -0
- data/security/vex.json +21 -0
- metadata +146 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
require 'bundler/setup'
|
|
2
|
+
|
|
3
|
+
require "curses"
|
|
4
|
+
require "abachrome"
|
|
5
|
+
|
|
6
|
+
DELAY = 0.0016
|
|
7
|
+
PLASMA_SCALE = 0.02
|
|
8
|
+
TIME_SCALE = 2.0
|
|
9
|
+
|
|
10
|
+
# Generate base hue for the palette
|
|
11
|
+
base_hue = rand(0..360)
|
|
12
|
+
base_color = Abachrome.from_oklch(0.5, 0.2, base_hue)
|
|
13
|
+
|
|
14
|
+
# Create 255 shades palette interpolating from black to base_color to white
|
|
15
|
+
black = Abachrome.from_rgb(0, 0, 0)
|
|
16
|
+
white = Abachrome.from_rgb(1, 1, 1)
|
|
17
|
+
palette = Abachrome::Palette.new([black, base_color, white])
|
|
18
|
+
palette = palette.interpolate(32)
|
|
19
|
+
|
|
20
|
+
def plasma(x, y, t)
|
|
21
|
+
x_scaled = x * PLASMA_SCALE
|
|
22
|
+
y_scaled = y * PLASMA_SCALE
|
|
23
|
+
t_scaled = t * TIME_SCALE
|
|
24
|
+
|
|
25
|
+
value = Math.sin(x_scaled) + Math.sin(y_scaled) +
|
|
26
|
+
Math.sin(x_scaled + y_scaled + t_scaled) +
|
|
27
|
+
Math.sin(Math.sqrt((x_scaled * x_scaled + y_scaled * y_scaled)) + t_scaled)
|
|
28
|
+
|
|
29
|
+
# Normalize to 0..1 range
|
|
30
|
+
(value + 4) / 8
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pick_color(value, palette)
|
|
34
|
+
normalized = (value * (palette.size - 1))
|
|
35
|
+
|
|
36
|
+
# Error diffusion dithering
|
|
37
|
+
error = normalized - normalized.floor
|
|
38
|
+
if rand < error
|
|
39
|
+
normalized.ceil
|
|
40
|
+
else
|
|
41
|
+
normalized.floor
|
|
42
|
+
end + 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
require 'curses'
|
|
47
|
+
|
|
48
|
+
Curses.init_screen
|
|
49
|
+
Curses.start_color
|
|
50
|
+
Curses.curs_set(0)
|
|
51
|
+
Curses.noecho
|
|
52
|
+
Curses.cbreak
|
|
53
|
+
Curses.stdscr.nodelay = 1
|
|
54
|
+
|
|
55
|
+
total_colors = palette.size
|
|
56
|
+
bar_width = 50
|
|
57
|
+
|
|
58
|
+
Curses.setpos(Curses.lines / 2 - 1, (Curses.cols - 20) / 2)
|
|
59
|
+
Curses.addstr("Initializing colors")
|
|
60
|
+
|
|
61
|
+
Curses.use_default_colors
|
|
62
|
+
palette.each_with_index do |color, i|
|
|
63
|
+
progress = ((i + 1).to_f / total_colors * bar_width).to_i
|
|
64
|
+
|
|
65
|
+
Curses.attron(Curses.color_pair(0)) {
|
|
66
|
+
Curses.setpos(Curses.lines / 2, (Curses.cols - bar_width) / 2)
|
|
67
|
+
Curses.addstr("[" + "#" * progress + " " * (bar_width - progress) + "]")
|
|
68
|
+
|
|
69
|
+
Curses.setpos(Curses.lines / 2 + 1, (Curses.cols - 20) / 2)
|
|
70
|
+
percentage = ((i + 1).to_f / total_colors * 100).to_i
|
|
71
|
+
Curses.addstr("#{percentage}% complete")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Curses.refresh
|
|
75
|
+
|
|
76
|
+
rgb = color.rgb_array.map { |_| ((_ * 1000)/255).to_i }
|
|
77
|
+
Curses.init_color(i + 1, rgb[0], rgb[1], rgb[2])
|
|
78
|
+
Curses.init_pair(i + 1, i + 1, i + 1)
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Curses.setpos(Curses.lines / 2 + 2, (Curses.cols - 20) / 2)
|
|
83
|
+
Curses.addstr("Done!")
|
|
84
|
+
Curses.refresh
|
|
85
|
+
|
|
86
|
+
height = Curses.lines
|
|
87
|
+
width = Curses.cols
|
|
88
|
+
start_time = Time.now
|
|
89
|
+
frame_count = 0
|
|
90
|
+
last_fps_update = start_time
|
|
91
|
+
|
|
92
|
+
loop do
|
|
93
|
+
|
|
94
|
+
t = Time.now - start_time
|
|
95
|
+
frame_count += 1
|
|
96
|
+
|
|
97
|
+
# Update FPS counter every second
|
|
98
|
+
if Time.now - last_fps_update >= 1
|
|
99
|
+
fps = frame_count / (Time.now - last_fps_update)
|
|
100
|
+
frame_count = 0
|
|
101
|
+
last_fps_update = Time.now
|
|
102
|
+
fps_str = format("FPS: %.1f", fps.to_f)
|
|
103
|
+
Curses.setpos(height - 1, width - fps_str.length)
|
|
104
|
+
Curses.addstr(fps_str)
|
|
105
|
+
Curses.setpos(height - 1, 0)
|
|
106
|
+
Curses.addstr('Frame' + frame_count.to_s)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
height.times do |y|
|
|
110
|
+
width.times do |x|
|
|
111
|
+
value = plasma(x, y, t)
|
|
112
|
+
color_index = pick_color(value, palette)
|
|
113
|
+
Curses.setpos(y, x)
|
|
114
|
+
Curses.attron(Curses.color_pair(color_index.to_i)) { Curses.addstr("X") }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Curses.refresh
|
|
119
|
+
sleep(DELAY)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
ensure
|
|
123
|
+
Curses.close_screen
|
|
124
|
+
end
|
data/devenv.lock
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nodes": {
|
|
3
|
+
"devenv": {
|
|
4
|
+
"locked": {
|
|
5
|
+
"dir": "src/modules",
|
|
6
|
+
"lastModified": 1760162706,
|
|
7
|
+
"owner": "cachix",
|
|
8
|
+
"repo": "devenv",
|
|
9
|
+
"rev": "0d5ad578728fe4bce66eb4398b8b1e66deceb4e4",
|
|
10
|
+
"type": "github"
|
|
11
|
+
},
|
|
12
|
+
"original": {
|
|
13
|
+
"dir": "src/modules",
|
|
14
|
+
"owner": "cachix",
|
|
15
|
+
"repo": "devenv",
|
|
16
|
+
"type": "github"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"flake-compat": {
|
|
20
|
+
"flake": false,
|
|
21
|
+
"locked": {
|
|
22
|
+
"lastModified": 1747046372,
|
|
23
|
+
"owner": "edolstra",
|
|
24
|
+
"repo": "flake-compat",
|
|
25
|
+
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
|
26
|
+
"type": "github"
|
|
27
|
+
},
|
|
28
|
+
"original": {
|
|
29
|
+
"owner": "edolstra",
|
|
30
|
+
"repo": "flake-compat",
|
|
31
|
+
"type": "github"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"flake-compat_2": {
|
|
35
|
+
"flake": false,
|
|
36
|
+
"locked": {
|
|
37
|
+
"lastModified": 1747046372,
|
|
38
|
+
"owner": "edolstra",
|
|
39
|
+
"repo": "flake-compat",
|
|
40
|
+
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
|
41
|
+
"type": "github"
|
|
42
|
+
},
|
|
43
|
+
"original": {
|
|
44
|
+
"owner": "edolstra",
|
|
45
|
+
"repo": "flake-compat",
|
|
46
|
+
"type": "github"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"flake-utils": {
|
|
50
|
+
"inputs": {
|
|
51
|
+
"systems": "systems"
|
|
52
|
+
},
|
|
53
|
+
"locked": {
|
|
54
|
+
"lastModified": 1731533236,
|
|
55
|
+
"owner": "numtide",
|
|
56
|
+
"repo": "flake-utils",
|
|
57
|
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
58
|
+
"type": "github"
|
|
59
|
+
},
|
|
60
|
+
"original": {
|
|
61
|
+
"owner": "numtide",
|
|
62
|
+
"repo": "flake-utils",
|
|
63
|
+
"type": "github"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"git-hooks": {
|
|
67
|
+
"inputs": {
|
|
68
|
+
"flake-compat": "flake-compat",
|
|
69
|
+
"gitignore": "gitignore",
|
|
70
|
+
"nixpkgs": [
|
|
71
|
+
"nixpkgs"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"locked": {
|
|
75
|
+
"lastModified": 1759523803,
|
|
76
|
+
"owner": "cachix",
|
|
77
|
+
"repo": "git-hooks.nix",
|
|
78
|
+
"rev": "cfc9f7bb163ad8542029d303e599c0f7eee09835",
|
|
79
|
+
"type": "github"
|
|
80
|
+
},
|
|
81
|
+
"original": {
|
|
82
|
+
"owner": "cachix",
|
|
83
|
+
"repo": "git-hooks.nix",
|
|
84
|
+
"type": "github"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"gitignore": {
|
|
88
|
+
"inputs": {
|
|
89
|
+
"nixpkgs": [
|
|
90
|
+
"git-hooks",
|
|
91
|
+
"nixpkgs"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
"locked": {
|
|
95
|
+
"lastModified": 1709087332,
|
|
96
|
+
"owner": "hercules-ci",
|
|
97
|
+
"repo": "gitignore.nix",
|
|
98
|
+
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
|
99
|
+
"type": "github"
|
|
100
|
+
},
|
|
101
|
+
"original": {
|
|
102
|
+
"owner": "hercules-ci",
|
|
103
|
+
"repo": "gitignore.nix",
|
|
104
|
+
"type": "github"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"nixpkgs": {
|
|
108
|
+
"locked": {
|
|
109
|
+
"lastModified": 1758532697,
|
|
110
|
+
"owner": "cachix",
|
|
111
|
+
"repo": "devenv-nixpkgs",
|
|
112
|
+
"rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f",
|
|
113
|
+
"type": "github"
|
|
114
|
+
},
|
|
115
|
+
"original": {
|
|
116
|
+
"owner": "cachix",
|
|
117
|
+
"ref": "rolling",
|
|
118
|
+
"repo": "devenv-nixpkgs",
|
|
119
|
+
"type": "github"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"nixpkgs-ruby": {
|
|
123
|
+
"inputs": {
|
|
124
|
+
"flake-compat": "flake-compat_2",
|
|
125
|
+
"flake-utils": "flake-utils",
|
|
126
|
+
"nixpkgs": [
|
|
127
|
+
"nixpkgs"
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"locked": {
|
|
131
|
+
"lastModified": 1759902829,
|
|
132
|
+
"owner": "bobvanderlinden",
|
|
133
|
+
"repo": "nixpkgs-ruby",
|
|
134
|
+
"rev": "5fba6c022a63f1e76dee4da71edddad8959f088a",
|
|
135
|
+
"type": "github"
|
|
136
|
+
},
|
|
137
|
+
"original": {
|
|
138
|
+
"owner": "bobvanderlinden",
|
|
139
|
+
"repo": "nixpkgs-ruby",
|
|
140
|
+
"type": "github"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"root": {
|
|
144
|
+
"inputs": {
|
|
145
|
+
"devenv": "devenv",
|
|
146
|
+
"git-hooks": "git-hooks",
|
|
147
|
+
"nixpkgs": "nixpkgs",
|
|
148
|
+
"nixpkgs-ruby": "nixpkgs-ruby",
|
|
149
|
+
"pre-commit-hooks": [
|
|
150
|
+
"git-hooks"
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"systems": {
|
|
155
|
+
"locked": {
|
|
156
|
+
"lastModified": 1681028828,
|
|
157
|
+
"owner": "nix-systems",
|
|
158
|
+
"repo": "default",
|
|
159
|
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
160
|
+
"type": "github"
|
|
161
|
+
},
|
|
162
|
+
"original": {
|
|
163
|
+
"owner": "nix-systems",
|
|
164
|
+
"repo": "default",
|
|
165
|
+
"type": "github"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"root": "root",
|
|
170
|
+
"version": 7
|
|
171
|
+
}
|
data/devenv.nix
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{ pkgs, lib, config, inputs, ... }:
|
|
2
|
+
|
|
3
|
+
{
|
|
4
|
+
# https://devenv.sh/basics/
|
|
5
|
+
env.GREET = "devenv";
|
|
6
|
+
|
|
7
|
+
# https://devenv.sh/packages/
|
|
8
|
+
packages = [ pkgs.git pkgs.ncurses];
|
|
9
|
+
|
|
10
|
+
languages.ruby.enable = true;
|
|
11
|
+
languages.ruby.version = "3.3.2";
|
|
12
|
+
|
|
13
|
+
languages.javascript.enable = true;
|
|
14
|
+
languages.javascript.npm.enable = true;
|
|
15
|
+
languages.javascript.bun.enable = true;
|
|
16
|
+
|
|
17
|
+
# https://devenv.sh/languages/
|
|
18
|
+
# languages.rust.enable = true;
|
|
19
|
+
|
|
20
|
+
# https://devenv.sh/processes/
|
|
21
|
+
# processes.cargo-watch.exec = "cargo-watch";
|
|
22
|
+
|
|
23
|
+
# https://devenv.sh/services/
|
|
24
|
+
# services.postgres.enable = true;
|
|
25
|
+
|
|
26
|
+
# https://devenv.sh/scripts/
|
|
27
|
+
scripts.hello.exec = ''
|
|
28
|
+
echo hello from $GREET
|
|
29
|
+
'';
|
|
30
|
+
|
|
31
|
+
enterShell = ''
|
|
32
|
+
hello
|
|
33
|
+
git --version
|
|
34
|
+
'';
|
|
35
|
+
|
|
36
|
+
# https://devenv.sh/tasks/
|
|
37
|
+
# tasks = {
|
|
38
|
+
# "myproj:setup".exec = "mytool build";
|
|
39
|
+
# "devenv:enterShell".after = [ "myproj:setup" ];
|
|
40
|
+
# };
|
|
41
|
+
|
|
42
|
+
# https://devenv.sh/tests/
|
|
43
|
+
enterTest = ''
|
|
44
|
+
echo "Running tests"
|
|
45
|
+
git --version | grep --color=auto "${pkgs.git.version}"
|
|
46
|
+
'';
|
|
47
|
+
|
|
48
|
+
# https://devenv.sh/pre-commit-hooks/
|
|
49
|
+
# pre-commit.hooks.shellcheck.enable = true;
|
|
50
|
+
|
|
51
|
+
# See full reference at https://devenv.sh/reference/options/
|
|
52
|
+
}
|
data/devenv.yaml
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Color - Core color representation class
|
|
4
|
+
#
|
|
5
|
+
# This is the central color class that represents colors across multiple color spaces
|
|
6
|
+
# including sRGB, OKLAB, OKLCH, and linear RGB. The Color class encapsulates color
|
|
7
|
+
# coordinates, alpha values, and color space information while providing methods
|
|
8
|
+
# for color creation, conversion, and manipulation.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Create colors from RGB, OKLAB, OKLCH values with factory methods
|
|
12
|
+
# - Automatic coordinate validation against color space definitions
|
|
13
|
+
# - Immutable color objects with equality and hash support
|
|
14
|
+
# - Extensible through mixins for color space conversions and operations
|
|
15
|
+
# - High-precision decimal arithmetic using AbcDecimal for accurate calculations
|
|
16
|
+
# - Support for alpha (opacity) values with proper handling in conversions
|
|
17
|
+
#
|
|
18
|
+
# The class uses a mixin system to dynamically include functionality for converting
|
|
19
|
+
# between color spaces, blending operations, and lightness adjustments. All coordinate
|
|
20
|
+
# values are stored as AbcDecimal objects to maintain precision during color science
|
|
21
|
+
# calculations and transformations.
|
|
22
|
+
|
|
23
|
+
require "dry-inflector"
|
|
24
|
+
require_relative "color_space"
|
|
25
|
+
|
|
26
|
+
module Abachrome
|
|
27
|
+
class Color
|
|
28
|
+
attr_reader :color_space, :coordinates, :alpha
|
|
29
|
+
|
|
30
|
+
# Initializes a new Color object with the specified color space, coordinates, and alpha value.
|
|
31
|
+
#
|
|
32
|
+
# @param color_space [ColorSpace] The color space for this color instance
|
|
33
|
+
# @param coordinates [Array<Numeric, String>] The color coordinates in the specified color space
|
|
34
|
+
# @param alpha [Numeric, String] The alpha (opacity) value, between 0.0 and 1.0 (default: 1.0)
|
|
35
|
+
# @raise [ArgumentError] If the coordinates are invalid for the specified color space
|
|
36
|
+
# @return [Color] A new Color instance
|
|
37
|
+
def initialize(color_space, coordinates, alpha = "1.0".to_f)
|
|
38
|
+
@color_space = color_space
|
|
39
|
+
@coordinates = coordinates.map { |c| c.to_s.to_f }
|
|
40
|
+
@alpha = AbcDecimal.new(alpha.to_s)
|
|
41
|
+
|
|
42
|
+
validate_coordinates!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
mixins_path = File.join(__dir__, "color_mixins", "*.rb")
|
|
46
|
+
Dir[mixins_path].each do |file|
|
|
47
|
+
require file
|
|
48
|
+
mixin_name = File.basename(file, ".rb")
|
|
49
|
+
inflector = Dry::Inflector.new
|
|
50
|
+
mixin_module = Abachrome::ColorMixins.const_get(inflector.camelize(mixin_name))
|
|
51
|
+
include mixin_module
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Creates a new Color instance from RGB values
|
|
55
|
+
#
|
|
56
|
+
# @param r [Numeric] The red component value (typically 0-1)
|
|
57
|
+
# @param g [Numeric] The green component value (typically 0-1)
|
|
58
|
+
# @param b [Numeric] The blue component value (typically 0-1)
|
|
59
|
+
# @param a [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
|
|
60
|
+
# @return [Abachrome::Color] A new Color instance in the sRGB color space
|
|
61
|
+
def self.from_rgb(r, g, b, a = 1.0)
|
|
62
|
+
space = ColorSpace.find(:srgb)
|
|
63
|
+
new(space, [r, g, b], a)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Creates a new Color instance from LRGB values
|
|
67
|
+
#
|
|
68
|
+
# @param r [Numeric] The red component value (typically 0-1)
|
|
69
|
+
# @param g [Numeric] The green component value (typically 0-1)
|
|
70
|
+
# @param b [Numeric] The blue component value (typically 0-1)
|
|
71
|
+
# @param a [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
|
|
72
|
+
# @return [Abachrome::Color] A new Color instance in the sRGB color space
|
|
73
|
+
def self.from_lrgb(r, g, b, a = 1.0)
|
|
74
|
+
space = ColorSpace.find(:lrgb)
|
|
75
|
+
new(space, [r, g, b], a)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Creates a new Color object with OKLAB values.
|
|
79
|
+
#
|
|
80
|
+
# @param l [Float] The lightness component (L) of the OKLAB color space
|
|
81
|
+
# @param a [Float] The green-red component (a) of the OKLAB color space
|
|
82
|
+
# @param b [Float] The blue-yellow component (b) of the OKLAB color space
|
|
83
|
+
# @param alpha [Float] The alpha (opacity) value, from 0.0 to 1.0
|
|
84
|
+
# @return [Abachrome::Color] A new Color object in the OKLAB color space
|
|
85
|
+
def self.from_oklab(l, a, b, alpha = 1.0)
|
|
86
|
+
space = ColorSpace.find(:oklab)
|
|
87
|
+
new(space, [l, a, b], alpha)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Creates a new color instance in the OKLCH color space.
|
|
91
|
+
#
|
|
92
|
+
# @param l [Numeric] The lightness component (L), typically in range 0..1
|
|
93
|
+
# @param c [Numeric] The chroma component (C), typically starting from 0 with no upper bound
|
|
94
|
+
# @param h [Numeric] The hue component (H) in degrees, typically in range 0..360
|
|
95
|
+
# @param alpha [Float] The alpha (opacity) component, in range 0..1, defaults to 1.0 (fully opaque)
|
|
96
|
+
# @return [Abachrome::Color] A new Color instance in the OKLCH color space
|
|
97
|
+
def self.from_oklch(l, c, h, alpha = 1.0)
|
|
98
|
+
space = ColorSpace.find(:oklch)
|
|
99
|
+
new(space, [l, c, h], alpha)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Creates a new Color instance from YIQ values
|
|
103
|
+
#
|
|
104
|
+
# @param y [Numeric] The luma (brightness) component, typically in range 0 to 1
|
|
105
|
+
# @param i [Numeric] The in-phase component (orange-blue), typically in range -0.5957 to 0.5957
|
|
106
|
+
# @param q [Numeric] The quadrature component (purple-green), typically in range -0.5226 to 0.5226
|
|
107
|
+
# @param alpha [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
|
|
108
|
+
# @return [Abachrome::Color] A new Color instance in the YIQ color space
|
|
109
|
+
def self.from_yiq(y, i, q, alpha = 1.0)
|
|
110
|
+
space = ColorSpace.find(:yiq)
|
|
111
|
+
new(space, [y, i, q], alpha)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Creates a new Color instance from CMYK values
|
|
115
|
+
#
|
|
116
|
+
# @param c [Numeric] The cyan component, typically in range 0 to 1
|
|
117
|
+
# @param m [Numeric] The magenta component, typically in range 0 to 1
|
|
118
|
+
# @param y [Numeric] The yellow component, typically in range 0 to 1
|
|
119
|
+
# @param k [Numeric] The key/black component, typically in range 0 to 1
|
|
120
|
+
# @param alpha [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
|
|
121
|
+
# @return [Abachrome::Color] A new Color instance in the CMYK color space
|
|
122
|
+
def self.from_cmyk(c, m, y, k, alpha = 1.0)
|
|
123
|
+
space = ColorSpace.find(:cmyk)
|
|
124
|
+
new(space, [c, m, y, k], alpha)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Compares this color instance with another for equality.
|
|
128
|
+
#
|
|
129
|
+
# Two colors are considered equal if they have the same color space,
|
|
130
|
+
# coordinates, and alpha value.
|
|
131
|
+
#
|
|
132
|
+
# @param other [Object] The object to compare with
|
|
133
|
+
# @return [Boolean] true if the colors are equal, false otherwise
|
|
134
|
+
def ==(other)
|
|
135
|
+
return false unless other.is_a?(Color)
|
|
136
|
+
|
|
137
|
+
color_space == other.color_space &&
|
|
138
|
+
coordinates == other.coordinates &&
|
|
139
|
+
alpha == other.alpha
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Checks if this color is equal to another color object.
|
|
143
|
+
#
|
|
144
|
+
# @param other [Object] The object to compare with
|
|
145
|
+
# @return [Boolean] true if the two colors are equal, false otherwise
|
|
146
|
+
# @see ==
|
|
147
|
+
def eql?(other)
|
|
148
|
+
self == other
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Generates a hash code for this color instance
|
|
152
|
+
# based on its color space, coordinates, and alpha value.
|
|
153
|
+
# The method first converts these components to strings,
|
|
154
|
+
# then computes a hash of the resulting array.
|
|
155
|
+
#
|
|
156
|
+
# @return [Integer] a hash code that can be used for equality comparison
|
|
157
|
+
# and as a hash key in Hash objects
|
|
158
|
+
def hash
|
|
159
|
+
[color_space, coordinates, alpha].map(&:to_s).hash
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Returns a string representation of the color in the format "ColorSpaceName(coord1, coord2, coord3, alpha)"
|
|
163
|
+
#
|
|
164
|
+
# @return [String] A human-readable string representation of the color showing its
|
|
165
|
+
# color space name, coordinate values rounded to 3 decimal places, and alpha value
|
|
166
|
+
# (if not 1.0)
|
|
167
|
+
def to_s
|
|
168
|
+
coord_str = coordinates.map { |c| c.to_f.round(3) }.join(", ")
|
|
169
|
+
alpha_str = alpha == AbcDecimal.new("1.0") ? "" : ", #{alpha.to_f.round(3)}"
|
|
170
|
+
"#{color_space.name}(#{coord_str}#{alpha_str})"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Returns the color as a hexadecimal string representation.
|
|
174
|
+
#
|
|
175
|
+
# @return [String] A hex color string in the format "#RRGGBB" or "#RRGGBBAA" if alpha < 1.0
|
|
176
|
+
def to_hex
|
|
177
|
+
require_relative "outputs/css"
|
|
178
|
+
Outputs::CSS.format_hex(self)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
# Validates that the number of coordinates matches the expected number for the color space.
|
|
184
|
+
# Compares the size of the coordinates array with the number of coordinates
|
|
185
|
+
# defined in the associated color space.
|
|
186
|
+
# @raise [ArgumentError] when the number of coordinates doesn't match the color space definition
|
|
187
|
+
# @return [nil] if validation passes
|
|
188
|
+
def validate_coordinates!
|
|
189
|
+
return if coordinates.size == color_space.coordinates.size
|
|
190
|
+
|
|
191
|
+
raise ArgumentError,
|
|
192
|
+
"Expected #{color_space.coordinates.size} coordinates for #{color_space.name}, got #{coordinates.size}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::ColorMixins::Blend - Color blending and mixing functionality
|
|
4
|
+
#
|
|
5
|
+
# This mixin provides methods for blending and mixing colors together in various color spaces.
|
|
6
|
+
# The blend operation interpolates between two colors by a specified amount, creating smooth
|
|
7
|
+
# color transitions. All blending operations preserve alpha values and can be performed in
|
|
8
|
+
# the current color space or a specified target color space for optimal results.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Linear interpolation between colors with configurable blend amounts
|
|
12
|
+
# - Support for blending in different color spaces (sRGB, OKLAB, OKLCH)
|
|
13
|
+
# - Both non-destructive (blend/mix) and destructive (blend!/mix!) variants
|
|
14
|
+
# - Automatic color space conversion when blending colors from different spaces
|
|
15
|
+
# - High-precision decimal arithmetic for accurate color calculations
|
|
16
|
+
#
|
|
17
|
+
# The mixin includes both immutable methods that return new color instances and mutable
|
|
18
|
+
# methods that modify the current color object in place, providing flexibility for
|
|
19
|
+
# different use cases and performance requirements.
|
|
20
|
+
|
|
21
|
+
module Abachrome
|
|
22
|
+
module ColorMixins
|
|
23
|
+
module Blend
|
|
24
|
+
# @method blend
|
|
25
|
+
# Interpolates between two colors, creating a new color that is a blend of the two.
|
|
26
|
+
# The blend happens in the specified color space or the current color space if none is provided.
|
|
27
|
+
# #
|
|
28
|
+
# @param [Abachrome::Color] other The color to blend with
|
|
29
|
+
# @param [Float, Integer, #to_d] amount The blend amount between 0 and 1, where 0 returns the original color and 1 returns the other color. Defaults to 0.5 (midpoint)
|
|
30
|
+
# @param [Symbol, nil] target_color_space The color space to perform the blend in (optional)
|
|
31
|
+
# @return [Abachrome::Color] A new color representing the blend of the two colors
|
|
32
|
+
# @example Blend two colors equally
|
|
33
|
+
# red.blend(blue, 0.5)
|
|
34
|
+
# @example Blend with 25% of another color
|
|
35
|
+
# red.blend(blue, 0.25)
|
|
36
|
+
# @example Blend in a specific color space
|
|
37
|
+
# red.blend(blue, 0.5, target_color_space: :oklab)
|
|
38
|
+
def blend(other, amount = 0.5, target_color_space: nil)
|
|
39
|
+
amount = amount.to_f
|
|
40
|
+
|
|
41
|
+
source = target_color_space ? to_color_space(target_color_space) : self
|
|
42
|
+
other = other.to_color_space(source.color_space)
|
|
43
|
+
|
|
44
|
+
l1, a1, b1 = coordinates.map { |_| _.to_f }
|
|
45
|
+
l2, a2, b2 = other.coordinates.map { |_| _.to_f }
|
|
46
|
+
|
|
47
|
+
blended_l = (1 - amount.to_f * l1) + (amount.to_f * l2)
|
|
48
|
+
blended_a = (1 - amount.to_f * a1) + (amount.to_f * a2)
|
|
49
|
+
blended_b = (1 - amount.to_f * b1) + (amount.to_f * b2)
|
|
50
|
+
|
|
51
|
+
blended_alpha = alpha + ((other.alpha - alpha) * amount)
|
|
52
|
+
|
|
53
|
+
Color.new(
|
|
54
|
+
color_space,
|
|
55
|
+
[blended_l, blended_a, blended_b],
|
|
56
|
+
blended_alpha
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Blends this color with another color by the specified amount.
|
|
61
|
+
# This is a destructive version of the blend method, modifying the current
|
|
62
|
+
# color in place.
|
|
63
|
+
#
|
|
64
|
+
# @param other [Abachrome::Color] The color to blend with
|
|
65
|
+
# @param amount [Float] The blend amount, between 0.0 and 1.0, where 0.0 is
|
|
66
|
+
# this color and 1.0 is the other color (default: 0.5)
|
|
67
|
+
# @return [Abachrome::Color] Returns self after modification
|
|
68
|
+
def blend!(other, amount = 0.5)
|
|
69
|
+
blended = blend(other, amount)
|
|
70
|
+
@color_space = blended.color_space
|
|
71
|
+
@coordinates = blended.coordinates
|
|
72
|
+
@alpha = blended.alpha
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Alias for the blend method that mixes two colors together.
|
|
77
|
+
#
|
|
78
|
+
# @param other [Abachrome::Color] The color to mix with
|
|
79
|
+
# @param amount [Float] The amount to mix, between 0.0 and 1.0, where 0.0 returns the original color and 1.0 returns the other color (default: 0.5)
|
|
80
|
+
# @return [Abachrome::Color] A new color resulting from the mix of the two colors
|
|
81
|
+
def mix(other, amount = 0.5)
|
|
82
|
+
blend(other, amount)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Mix the current color with another color.
|
|
86
|
+
#
|
|
87
|
+
# This method is an alias for blend!. It combines the current color with
|
|
88
|
+
# the provided color at the specified amount.
|
|
89
|
+
#
|
|
90
|
+
# @param other [Abachrome::Color] The color to mix with the current color
|
|
91
|
+
# @param amount [Numeric] The amount of the other color to mix in, from 0 to 1 (default: 0.5)
|
|
92
|
+
# @return [self] Returns the modified color object
|
|
93
|
+
def mix!(other, amount = 0.5)
|
|
94
|
+
blend!(other, amount)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|