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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 88fab17313d84aca8190acb8c108af84d1129ce9e240353e2afec2225fc5aa37
|
4
|
+
data.tar.gz: f98569f28bf4a1fa967f43bc8538de967e7dfa34f40f22749099b51b8b7512ac
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1df4f29f6700b0488656b3a7c7dc22a1aa72cfab8d6a11cfc89363acd57488a39e9ded2d79f397c2727e7cbb5a2b203f41975cdbc891de3028435b3d40be1915
|
7
|
+
data.tar.gz: d31cb69b1412ab7e5c06349ce2e44b9b1ecf1324d2b862d26e19adbf496bfeeec0c1195382bf5f4092d0fe12407e4d9d9024c7fe1065e52b3b353f94e55a79a4
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Abachrome
|
2
|
+
|
3
|
+
Abachrome is a Ruby gem for parsing, manipulating, and managing colors. It provides a robust set of tools for working with various color formats including hex, RGB, HSL, and named colors.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'abachrome'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install abachrome
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Basic Color Creation
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# Create colors in different ways
|
31
|
+
color = Abachrome.from_rgb(1.0, 0.0, 0.0) # Red using RGB values
|
32
|
+
color = Abachrome.from_hex('#FF0000') # Red using hex
|
33
|
+
color = Abachrome.from_name('red') # Red using CSS color name
|
34
|
+
|
35
|
+
# Create color with alpha
|
36
|
+
color = Abachrome.from_rgb(1.0, 0.0, 0.0, 0.5) # Semi-transparent red
|
37
|
+
```
|
38
|
+
|
39
|
+
### Color Space Conversion
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# Convert between color spaces
|
43
|
+
rgb_color = Abachrome.from_rgb(1.0, 0.0, 0.0)
|
44
|
+
oklab_color = rgb_color.to_oklab # Convert to Oklab
|
45
|
+
rgb_again = oklab_color.to_rgb # Convert back to RGB
|
46
|
+
```
|
47
|
+
|
48
|
+
### Color Output Formats
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
color = Abachrome.from_rgb(1.0, 0.0, 0.0)
|
52
|
+
|
53
|
+
# Different output formats
|
54
|
+
color.rgb_hex # => "#ff0000"
|
55
|
+
Abachrome::Outputs::CSS.format(color) # => "#ff0000"
|
56
|
+
Abachrome::Outputs::CSS.format_rgb(color) # => "rgb(255, 0, 0)"
|
57
|
+
```
|
58
|
+
|
59
|
+
### Working with Color Gamuts
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# Check if color is within gamut
|
63
|
+
srgb_gamut = Abachrome::Gamut::SRGB.new
|
64
|
+
color = Abachrome.from_rgb(1.2, -0.1, 0.5)
|
65
|
+
mapped_color = srgb_gamut.map(color.coordinates) # Map color to gamut
|
66
|
+
```
|
67
|
+
|
68
|
+
## Features
|
69
|
+
|
70
|
+
- Support for multiple color spaces (RGB, HSL, Lab, Oklab)
|
71
|
+
- Color space conversion
|
72
|
+
- Gamut mapping
|
73
|
+
- CSS color parsing and formatting
|
74
|
+
- Support for CSS named colors
|
75
|
+
- High-precision color calculations using BigDecimal
|
76
|
+
- Alpha channel support
|
77
|
+
|
78
|
+
## Requirements
|
79
|
+
|
80
|
+
- Ruby >= 3.0.0
|
81
|
+
|
82
|
+
## License
|
83
|
+
|
84
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
85
|
+
|
86
|
+
## Development
|
87
|
+
|
88
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
89
|
+
|
90
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
91
|
+
|
92
|
+
## Contributing
|
93
|
+
|
94
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/durableprogramming/abachrome.
|
95
|
+
|
96
|
+
# Acknowledgement
|
97
|
+
|
98
|
+
We'd like to thank the excellent Color.js and culori color libraries, which helped inspire this project and
|
99
|
+
inform its design.
|
@@ -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,100 @@
|
|
1
|
+
{
|
2
|
+
"nodes": {
|
3
|
+
"devenv": {
|
4
|
+
"locked": {
|
5
|
+
"dir": "src/modules",
|
6
|
+
"lastModified": 1739172804,
|
7
|
+
"owner": "cachix",
|
8
|
+
"repo": "devenv",
|
9
|
+
"rev": "f8be0ed0141abab89cefb20d3d375740671fbee1",
|
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": 1733328505,
|
23
|
+
"owner": "edolstra",
|
24
|
+
"repo": "flake-compat",
|
25
|
+
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
26
|
+
"type": "github"
|
27
|
+
},
|
28
|
+
"original": {
|
29
|
+
"owner": "edolstra",
|
30
|
+
"repo": "flake-compat",
|
31
|
+
"type": "github"
|
32
|
+
}
|
33
|
+
},
|
34
|
+
"gitignore": {
|
35
|
+
"inputs": {
|
36
|
+
"nixpkgs": [
|
37
|
+
"pre-commit-hooks",
|
38
|
+
"nixpkgs"
|
39
|
+
]
|
40
|
+
},
|
41
|
+
"locked": {
|
42
|
+
"lastModified": 1709087332,
|
43
|
+
"owner": "hercules-ci",
|
44
|
+
"repo": "gitignore.nix",
|
45
|
+
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
46
|
+
"type": "github"
|
47
|
+
},
|
48
|
+
"original": {
|
49
|
+
"owner": "hercules-ci",
|
50
|
+
"repo": "gitignore.nix",
|
51
|
+
"type": "github"
|
52
|
+
}
|
53
|
+
},
|
54
|
+
"nixpkgs": {
|
55
|
+
"locked": {
|
56
|
+
"lastModified": 1733477122,
|
57
|
+
"owner": "cachix",
|
58
|
+
"repo": "devenv-nixpkgs",
|
59
|
+
"rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857",
|
60
|
+
"type": "github"
|
61
|
+
},
|
62
|
+
"original": {
|
63
|
+
"owner": "cachix",
|
64
|
+
"ref": "rolling",
|
65
|
+
"repo": "devenv-nixpkgs",
|
66
|
+
"type": "github"
|
67
|
+
}
|
68
|
+
},
|
69
|
+
"pre-commit-hooks": {
|
70
|
+
"inputs": {
|
71
|
+
"flake-compat": "flake-compat",
|
72
|
+
"gitignore": "gitignore",
|
73
|
+
"nixpkgs": [
|
74
|
+
"nixpkgs"
|
75
|
+
]
|
76
|
+
},
|
77
|
+
"locked": {
|
78
|
+
"lastModified": 1737465171,
|
79
|
+
"owner": "cachix",
|
80
|
+
"repo": "pre-commit-hooks.nix",
|
81
|
+
"rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17",
|
82
|
+
"type": "github"
|
83
|
+
},
|
84
|
+
"original": {
|
85
|
+
"owner": "cachix",
|
86
|
+
"repo": "pre-commit-hooks.nix",
|
87
|
+
"type": "github"
|
88
|
+
}
|
89
|
+
},
|
90
|
+
"root": {
|
91
|
+
"inputs": {
|
92
|
+
"devenv": "devenv",
|
93
|
+
"nixpkgs": "nixpkgs",
|
94
|
+
"pre-commit-hooks": "pre-commit-hooks"
|
95
|
+
}
|
96
|
+
}
|
97
|
+
},
|
98
|
+
"root": "root",
|
99
|
+
"version": 7
|
100
|
+
}
|
data/devenv.nix
ADDED
@@ -0,0 +1,51 @@
|
|
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 pkgs.asciinema pkgs.asciinema-agg];
|
9
|
+
|
10
|
+
languages.ruby.enable = true;
|
11
|
+
|
12
|
+
languages.javascript.enable = true;
|
13
|
+
languages.javascript.npm.enable = true;
|
14
|
+
languages.javascript.bun.enable = true;
|
15
|
+
|
16
|
+
# https://devenv.sh/languages/
|
17
|
+
# languages.rust.enable = true;
|
18
|
+
|
19
|
+
# https://devenv.sh/processes/
|
20
|
+
# processes.cargo-watch.exec = "cargo-watch";
|
21
|
+
|
22
|
+
# https://devenv.sh/services/
|
23
|
+
# services.postgres.enable = true;
|
24
|
+
|
25
|
+
# https://devenv.sh/scripts/
|
26
|
+
scripts.hello.exec = ''
|
27
|
+
echo hello from $GREET
|
28
|
+
'';
|
29
|
+
|
30
|
+
enterShell = ''
|
31
|
+
hello
|
32
|
+
git --version
|
33
|
+
'';
|
34
|
+
|
35
|
+
# https://devenv.sh/tasks/
|
36
|
+
# tasks = {
|
37
|
+
# "myproj:setup".exec = "mytool build";
|
38
|
+
# "devenv:enterShell".after = [ "myproj:setup" ];
|
39
|
+
# };
|
40
|
+
|
41
|
+
# https://devenv.sh/tests/
|
42
|
+
enterTest = ''
|
43
|
+
echo "Running tests"
|
44
|
+
git --version | grep --color=auto "${pkgs.git.version}"
|
45
|
+
'';
|
46
|
+
|
47
|
+
# https://devenv.sh/pre-commit-hooks/
|
48
|
+
# pre-commit.hooks.shellcheck.enable = true;
|
49
|
+
|
50
|
+
# See full reference at https://devenv.sh/reference/options/
|
51
|
+
}
|
data/devenv.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
2
|
+
inputs:
|
3
|
+
nixpkgs:
|
4
|
+
url: github:cachix/devenv-nixpkgs/rolling
|
5
|
+
|
6
|
+
# If you're using non-OSS software, you can set allowUnfree to true.
|
7
|
+
# allowUnfree: true
|
8
|
+
|
9
|
+
# If you're willing to use a package that's vulnerable
|
10
|
+
# permittedInsecurePackages:
|
11
|
+
# - "openssl-1.1.1w"
|
12
|
+
|
13
|
+
# If you have more than one devenv you can merge them
|
14
|
+
#imports:
|
15
|
+
# - ./backend
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
module Abachrome
|
7
|
+
class AbcDecimal
|
8
|
+
extend Forwardable
|
9
|
+
DEFAULT_PRECISION = (ENV["ABC_DECIMAL_PRECISION"] || "24").to_i
|
10
|
+
|
11
|
+
attr_accessor :value, :precision
|
12
|
+
|
13
|
+
def_delegators :@value, :to_i, :to_f, :zero?, :nonzero?
|
14
|
+
|
15
|
+
def initialize(value, precision = DEFAULT_PRECISION)
|
16
|
+
@precision = precision
|
17
|
+
@value = case value
|
18
|
+
when AbcDecimal
|
19
|
+
value.value
|
20
|
+
when BigDecimal
|
21
|
+
value
|
22
|
+
when Rational
|
23
|
+
value
|
24
|
+
else
|
25
|
+
BigDecimal(value.to_s, precision)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
if @value.is_a?(Rational)
|
31
|
+
BigDecimal(@value, precision).to_s("F")
|
32
|
+
else
|
33
|
+
@value.to_s("F") # different behaviour than default BigDecimal
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_f
|
38
|
+
@value.to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.from_string(str, precision = DEFAULT_PRECISION)
|
42
|
+
new(str, precision)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.from_rational(rational, precision = DEFAULT_PRECISION)
|
46
|
+
new(rational, precision)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.from_float(float, precision = DEFAULT_PRECISION)
|
50
|
+
new(float, precision)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.from_integer(integer, precision = DEFAULT_PRECISION)
|
54
|
+
new(integer, precision)
|
55
|
+
end
|
56
|
+
|
57
|
+
def +(other)
|
58
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
59
|
+
self.class.new(@value + other_value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def -(other)
|
63
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
64
|
+
self.class.new(@value - other_value)
|
65
|
+
end
|
66
|
+
|
67
|
+
def *(other)
|
68
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
69
|
+
self.class.new(@value * other_value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def /(other)
|
73
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
74
|
+
self.class.new(@value / other_value)
|
75
|
+
end
|
76
|
+
|
77
|
+
def %(other)
|
78
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
79
|
+
self.class.new(@value % other_value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def clamp(min,max)
|
83
|
+
@value.clamp(AbcDecimal(min),AbcDecimal(max))
|
84
|
+
end
|
85
|
+
|
86
|
+
def **(other)
|
87
|
+
if other.is_a?(Rational)
|
88
|
+
self.class.new(@value**other)
|
89
|
+
else
|
90
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
91
|
+
self.class.new(@value**other_value)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def coerce(other)
|
96
|
+
[self.class.new(other), self]
|
97
|
+
end
|
98
|
+
|
99
|
+
def inspect
|
100
|
+
"#{self.class}('#{self}')"
|
101
|
+
end
|
102
|
+
|
103
|
+
def ==(other)
|
104
|
+
@value == (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
105
|
+
end
|
106
|
+
|
107
|
+
def <=>(other)
|
108
|
+
@value <=> (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
109
|
+
end
|
110
|
+
|
111
|
+
def >(other)
|
112
|
+
@value > (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
113
|
+
end
|
114
|
+
|
115
|
+
def >=(other)
|
116
|
+
@value >= (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
117
|
+
end
|
118
|
+
|
119
|
+
def <(other)
|
120
|
+
@value < (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
121
|
+
end
|
122
|
+
|
123
|
+
def <=(other)
|
124
|
+
@value <= (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
125
|
+
end
|
126
|
+
|
127
|
+
def clamp(*args)
|
128
|
+
AbcDecimal(@value.clamp(*args))
|
129
|
+
end
|
130
|
+
|
131
|
+
def round(*args)
|
132
|
+
AbcDecimal(@value.round(*args))
|
133
|
+
end
|
134
|
+
|
135
|
+
def abs(*args)
|
136
|
+
AbcDecimal(@value.abs(*args))
|
137
|
+
end
|
138
|
+
|
139
|
+
def sqrt
|
140
|
+
AbcDecimal(Math.sqrt(@value))
|
141
|
+
end
|
142
|
+
|
143
|
+
def negative?
|
144
|
+
@value.negative?
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.atan2(y, x)
|
148
|
+
y_value = y.is_a?(AbcDecimal) ? y.value : AbcDecimal(y).value
|
149
|
+
x_value = x.is_a?(AbcDecimal) ? x.value : AbcDecimal(x).value
|
150
|
+
new(Math.atan2(y_value, x_value))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def AbcDecimal(*args)
|
156
|
+
Abachrome::AbcDecimal.new(*args)
|
157
|
+
end
|
158
|
+
|
159
|
+
def AD(*args)
|
160
|
+
Abachrome::AbcDecimal.new(*args)
|
161
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-inflector"
|
4
|
+
require_relative "abc_decimal"
|
5
|
+
require_relative "color_space"
|
6
|
+
|
7
|
+
module Abachrome
|
8
|
+
class Color
|
9
|
+
attr_reader :color_space, :coordinates, :alpha
|
10
|
+
|
11
|
+
def initialize(color_space, coordinates, alpha = AbcDecimal("1.0"))
|
12
|
+
@color_space = color_space
|
13
|
+
@coordinates = coordinates.map { |c| AbcDecimal(c.to_s) }
|
14
|
+
@alpha = AbcDecimal.new(alpha.to_s)
|
15
|
+
|
16
|
+
validate_coordinates!
|
17
|
+
end
|
18
|
+
|
19
|
+
mixins_path = File.join(__dir__, "color_mixins", "*.rb")
|
20
|
+
Dir[mixins_path].each do |file|
|
21
|
+
require file
|
22
|
+
mixin_name = File.basename(file, ".rb")
|
23
|
+
inflector = Dry::Inflector.new
|
24
|
+
mixin_module = Abachrome::ColorMixins.const_get(inflector.camelize(mixin_name))
|
25
|
+
include mixin_module
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.from_rgb(r, g, b, a = 1.0)
|
29
|
+
space = ColorSpace.find(:srgb)
|
30
|
+
new(space, [r, g, b], a)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.from_oklab(l, a, b, alpha = 1.0)
|
34
|
+
space = ColorSpace.find(:oklab)
|
35
|
+
new(space, [l, a, b], alpha)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from_oklch(l, c, h, alpha = 1.0)
|
39
|
+
space = ColorSpace.find(:oklch)
|
40
|
+
new(space, [l, c, h], alpha)
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(other)
|
44
|
+
return false unless other.is_a?(Color)
|
45
|
+
|
46
|
+
color_space == other.color_space &&
|
47
|
+
coordinates == other.coordinates &&
|
48
|
+
alpha == other.alpha
|
49
|
+
end
|
50
|
+
|
51
|
+
def eql?(other)
|
52
|
+
self == other
|
53
|
+
end
|
54
|
+
|
55
|
+
def hash
|
56
|
+
[color_space, coordinates, alpha].map(&:to_s).hash
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
coord_str = coordinates.map { |c| c.to_f.round(3) }.join(", ")
|
61
|
+
alpha_str = alpha == AbcDecimal.new("1.0") ? "" : ", #{alpha.to_f.round(3)}"
|
62
|
+
"#{color_space.name}(#{coord_str}#{alpha_str})"
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def validate_coordinates!
|
68
|
+
return if coordinates.size == color_space.coordinates.size
|
69
|
+
|
70
|
+
raise ArgumentError,
|
71
|
+
"Expected #{color_space.coordinates.size} coordinates for #{color_space.name}, got #{coordinates.size}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abachrome
|
4
|
+
module ColorMixins
|
5
|
+
module Blend
|
6
|
+
def blend(other, amount = 0.5, target_color_space: nil)
|
7
|
+
amount = AbcDecimal(amount)
|
8
|
+
|
9
|
+
source = target_color_space ? to_color_space(target_color_space) : self
|
10
|
+
other = other.to_color_space(source.color_space)
|
11
|
+
|
12
|
+
l1, a1, b1 = coordinates.map { |_| AbcDecimal(_) }
|
13
|
+
l2, a2, b2 = other.coordinates.map { |_| AbcDecimal(_) }
|
14
|
+
|
15
|
+
blended_l = (AbcDecimal(1 - amount) * l1) + (AbcDecimal(amount) * l2)
|
16
|
+
blended_a = (AbcDecimal(1 - amount) * a1) + (AbcDecimal(amount) * a2)
|
17
|
+
blended_b = (AbcDecimal(1 - amount) * b1) + (AbcDecimal(amount) * b2)
|
18
|
+
|
19
|
+
blended_alpha = alpha + ((other.alpha - alpha) * amount)
|
20
|
+
|
21
|
+
Color.new(
|
22
|
+
color_space,
|
23
|
+
[blended_l, blended_a, blended_b],
|
24
|
+
blended_alpha
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def blend!(other, amount = 0.5)
|
29
|
+
blended = blend(other, amount)
|
30
|
+
@color_space = blended.color_space
|
31
|
+
@coordinates = blended.coordinates
|
32
|
+
@alpha = blended.alpha
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def mix(other, amount = 0.5)
|
37
|
+
blend(other, amount)
|
38
|
+
end
|
39
|
+
|
40
|
+
def mix!(other, amount = 0.5)
|
41
|
+
blend!(other, amount)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|