diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae3fdc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.bundle +*.so +*.o +*.a +mkmf.log diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..b721770 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in colortastic.gemspec +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3932cdf --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2015, Jeremy Fairbank + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a89768c --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Chroma + +Chroma is a color manipulation and palette generation library. It is heavily +inspired by and a very close Ruby port of the +[tinycolor.js](https://bgrins.github.io/TinyColor/) +library. Many thanks to [Brian Grinstead](http://www.briangrinstead.com/blog/) +for his hard work on that library. + +Chroma is in alpha stage at the moment. Most of the API methods from tinycolor +have been ported over with a few exceptions. However, I will be working on +cleaning up the API where needed, adding docs and examples, and adding tests +before a first release. Because this is currently in alpha, please be +prepared for possible API changes or bugs. + +Please don't hesitate to examine the code and make issues or pull requests +where you feel it is necessary. Please refer to the +[Contributing](#contributing) section below. + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'chroma', '0.0.1.alpha.1' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install chroma + +## Usage + +Chroma adds several of the methods available in +[tinycolor.js](https://github.com/bgrins/TinyColor) but renamed appropriately +for Ruby conventions (i.e. `isDark` becomes `dark?`). + +To create a color, just call the `Chroma.paint` method, passing in a string +that represents a color. + +```ruby +Chroma.paint 'red' # named colors +Chroma.paint '#00ff00' # 6 character hexadecimal +Chroma.paint '#00f' # 3 character hexadecimal +Chroma.paint 'rgb(255, 255, 0)' # rgb +Chroma.paint 'rgba(255, 255, 0, 0.5)' # rgba +Chroma.paint 'hsl(60, 100%, 50%)' # hsl with percentages +Chroma.paint 'hsl(60, 1, 0.5)' # hsl with decimals +Chroma.paint 'hsv(60, 100%, 50%)' # hsv with percentages +Chroma.paint 'hsv(60, 1, 0.5)' # hsv with decimals +``` + +To work directly from a string you can also use the `String#paint` method: + +```ruby +'red'.paint +'#00f'.paint +'rgb(255, 255, 0)'.paint + +# etc... +``` + +## Contributing + +Please branch from **dev** for all pull requests. + +1. Fork it (https://github.com/jfairbank/chroma/fork) +2. Checkout dev (`git checkout dev`) +3. Create your feature branch (`git checkout -b my-new-feature`) +4. Commit your changes (`git commit -am 'Add some feature'`) +5. Push to the branch (`git push origin my-new-feature`) +6. Create a new pull request against dev diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..809eb56 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +require "bundler/gem_tasks" + diff --git a/chroma.gemspec b/chroma.gemspec new file mode 100644 index 0000000..2ac63ca --- /dev/null +++ b/chroma.gemspec @@ -0,0 +1,23 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'chroma/version' + +Gem::Specification.new do |spec| + spec.name = 'chroma' + spec.version = Chroma::VERSION + spec.authors = ['Jeremy Fairbank'] + spec.email = ['elpapapollo@gmail.com'] + spec.summary = %q{Color manipulation and palette generation.} + spec.description = %q{Chroma is a color manipulation and palette generation gem.} + spec.homepage = 'https://github.com/jfairbank/chroma' + spec.license = 'MIT' + + spec.files = `git ls-files -z`.split("\x0") + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ['lib'] + + spec.add_development_dependency 'bundler', '~> 1.7' + spec.add_development_dependency 'rake', '~> 10.0' +end diff --git a/lib/chroma.rb b/lib/chroma.rb new file mode 100644 index 0000000..b2a1cad --- /dev/null +++ b/lib/chroma.rb @@ -0,0 +1,63 @@ +# General +require 'chroma/version' +require 'yaml' + +# Modules +require 'chroma/helpers/bounders' + +# Color +require 'chroma/color/attributes' +require 'chroma/color/serializers' +require 'chroma/color/modifiers' +require 'chroma/color' +require 'chroma/color_modes' + +# Palettes +require 'chroma/harmonies' + +# RGB Generators +require 'chroma/rgb_generator' +require 'chroma/rgb_generator/base' +require 'chroma/rgb_generator/from_string' +require 'chroma/rgb_generator/from_rgb_values' +require 'chroma/rgb_generator/from_rgb' +require 'chroma/rgb_generator/from_hsl_values' +require 'chroma/rgb_generator/from_hsl' +require 'chroma/rgb_generator/from_hsv_values' +require 'chroma/rgb_generator/from_hsv' +require 'chroma/rgb_generator/from_hex_string_values' + +# Converters +require 'chroma/converters/base' +require 'chroma/converters/rgb_converter' +require 'chroma/converters/hsl_converter' +require 'chroma/converters/hsv_converter' + +# Extensions +require 'chroma/extensions/string' + +module Chroma + class << self + def paint(input) + Color.new(input) + end + + def hex_from_name(name) + named_colors_map[name] + end + + def name_from_hex(hex) + hex_named_colors_map[hex] + end + + private + + def hex_named_colors_map + @hex_named_colors_map ||= named_colors_map.invert + end + + def named_colors_map + @named_colors ||= YAML.load_file(File.expand_path('../support/named_colors.yml', __FILE__)) + end + end +end diff --git a/lib/chroma/color.rb b/lib/chroma/color.rb new file mode 100644 index 0000000..23cd5ea --- /dev/null +++ b/lib/chroma/color.rb @@ -0,0 +1,34 @@ +module Chroma + class Color + include Attributes + include Serializers + include Modifiers + include Helpers::Bounders + + def initialize(input, format = nil) + @input = input + @rgb, gen_format = generate_rgb_and_format(input) + @format = format || gen_format + end + + def complement + hsl = to_hsl + hsl.h = (hsl.h + 180) % 360 + Color.new(hsl) + end + + def palette + Harmonies.new(self) + end + + private + + def to_2char_hex(n) + n.round.to_s(16).rjust(2, '0') + end + + def generate_rgb_and_format(input) + RgbGenerator.generate_rgb_and_format(input) + end + end +end diff --git a/lib/chroma/color/attributes.rb b/lib/chroma/color/attributes.rb new file mode 100644 index 0000000..dd8bb21 --- /dev/null +++ b/lib/chroma/color/attributes.rb @@ -0,0 +1,21 @@ +module Chroma + class Color + module Attributes + def dark? + brightness < 128 + end + + def light? + !dark? + end + + def alpha + @rgb.a + end + + def brightness + (@rgb.r * 299 + @rgb.g * 587 + @rgb.b * 114) / 1000.0 + end + end + end +end diff --git a/lib/chroma/color/modifiers.rb b/lib/chroma/color/modifiers.rb new file mode 100644 index 0000000..56e7ede --- /dev/null +++ b/lib/chroma/color/modifiers.rb @@ -0,0 +1,52 @@ +module Chroma + class Color + module Modifiers + def lighten(amount = 10) + hsl = to_hsl + hsl.l = clamp01(hsl.l + amount / 100.0) + self.class.new(hsl, @format) + end + + def brighten(amount = 10) + # Don't include alpha + rgb = @rgb.to_a[0..2].map(&:round) + amount = (255 * (-amount / 100.0)).round + + rgb.map! do |n| + [0, [255, n - amount].min].max + end + + self.class.new(ColorModes::Rgb.new(*rgb), @format) + end + + def darken(amount = 10) + hsl = to_hsl + hsl.l = clamp01(hsl.l - amount / 100.0) + self.class.new(hsl, @format) + end + + def desaturate(amount = 10) + hsl = to_hsl + hsl.s = clamp01(hsl.s - amount / 100.0) + self.class.new(hsl, @format) + end + + def saturate(amount = 10) + hsl = to_hsl + hsl.s = clamp01(hsl.s + amount / 100.0) + self.class.new(hsl, @format) + end + + def greyscale + desaturate(100) + end + + def spin(amount) + hsl = to_hsl + hue = (hsl.h.round + amount) % 360 + hsl.h = hue < 0 ? 360 + hue : hue + self.class.new(hsl, @format) + end + end + end +end diff --git a/lib/chroma/color/serializers.rb b/lib/chroma/color/serializers.rb new file mode 100644 index 0000000..4cebb3c --- /dev/null +++ b/lib/chroma/color/serializers.rb @@ -0,0 +1,113 @@ +module Chroma + class Color + module Serializers + def to_hsv + Converters::HsvConverter.convert_rgb(@rgb) + end + + def to_hsv_s + to_hs_s(:v) + end + + def to_hsl + Converters::HslConverter.convert_rgb(@rgb) + end + + def to_hsl_s + to_hs_s(:l) + end + + def to_hex(allow_3 = false) + r, g, b = [@rgb.r, @rgb.g, @rgb.b].map do |n| + to_2char_hex(n) + end + + if allow_3 && r[0] == r[1] && g[0] == g[1] && b[0] == b[1] + return "#{r[0]}#{g[0]}#{b[0]}" + end + + [r, g, b].flatten * '' + end + + def to_hex_s(allow_3 = false) + "##{to_hex(allow_3)}" + end + + def to_hex8 + [ + to_2char_hex(alpha * 255), + to_2char_hex(@rgb.r), + to_2char_hex(@rgb.g), + to_2char_hex(@rgb.b) + ].join('') + end + + def to_hex8_s + "##{to_hex8}" + end + + def to_rgb + @rgb + end + + def to_rgb_s + middle = @rgb.to_a[0..2].map(&:round).join(', ') + + to_alpha_s(:rgb, middle) + end + + def to_name + return 'transparent' if alpha.zero? + + if alpha < 1 || (name = Chroma.name_from_hex(to_hex(true))).nil? + '' + else + name + end + end + + alias_method :to_name_s, :to_name + + def to_s(format = @format) + use_alpha = alpha < 1 && alpha >= 0 && /^hex(3|6)?/ =~ format + + return to_rgb_s if use_alpha + + case format.to_s + when 'rgb' then to_rgb_s + when 'hex', 'hex6' then to_hex_s + when 'hex3' then to_hex_s(true) + when 'hex8' then to_hex8_s + when 'hsl' then to_hsl_s + when 'hsv' then to_hsv_s + when 'name' then to_name + else to_hex_s + end + end + + alias_method :inspect, :to_s + + private + + def to_hs_s(third) + color = send("to_hs#{third}") + + h = color.h.round + s = (color.s * 100).round + lv = (color.send(third) * 100).round + + middle = "#{h}, #{s}%, #{lv}%" + + to_alpha_s("hs#{third}", middle) + end + + def to_alpha_s(mode, middle) + if alpha < 1 + "#{mode}a(#{middle}, #{alpha})" + else + "#{mode}(#{middle})" + end + end + end + end +end diff --git a/lib/chroma/color_modes.rb b/lib/chroma/color_modes.rb new file mode 100644 index 0000000..7d49469 --- /dev/null +++ b/lib/chroma/color_modes.rb @@ -0,0 +1,29 @@ +module Chroma + module ColorModes + class << self + private + + def build(*attrs) + Class.new do + attr_accessor *(attrs + [:a]) + + class_eval <<-EOS + def initialize(#{attrs * ', '}, a = 1) + #{attrs.map{|attr| "@#{attr}"} * ', '}, @a = #{attrs * ', '}, a + end + + def to_a + [#{attrs.map{|attr| "@#{attr}"} * ', '}, @a] + end + + alias_method :to_ary, :to_a + EOS + end + end + end + + Rgb = build :r, :g, :b + Hsl = build :h, :s, :l + Hsv = build :h, :s, :v + end +end diff --git a/lib/chroma/converters/base.rb b/lib/chroma/converters/base.rb new file mode 100644 index 0000000..65f6dbd --- /dev/null +++ b/lib/chroma/converters/base.rb @@ -0,0 +1,23 @@ +module Chroma + module Converters + class Base + include Helpers::Bounders + + def initialize(input) + @input = input + end + + def self.convert_rgb(rgb) + new(rgb).convert_rgb + end + + def self.convert_hsl(hsl) + new(hsl).convert_hsl + end + + def self.convert_hsv(hsv) + new(hsv).convert_hsv + end + end + end +end diff --git a/lib/chroma/converters/hsl_converter.rb b/lib/chroma/converters/hsl_converter.rb new file mode 100644 index 0000000..e012b27 --- /dev/null +++ b/lib/chroma/converters/hsl_converter.rb @@ -0,0 +1,39 @@ +module Chroma + module Converters + class HslConverter < Base + def convert_rgb + r = bound01(@input.r, 255) + g = bound01(@input.g, 255) + b = bound01(@input.b, 255) + + rgb_array = [r, g, b] + + max = rgb_array.max + min = rgb_array.min + l = (max + min) * 0.5 + + if max == min + h = s = 0 + else + d = (max - min).to_f + + s = if l > 0.5 + d / (2 - max - min) + else + d / (max + min) + end + + h = case max + when r then (g - b) / d + (g < b ? 6 : 0) + when g then (b - r) / d + 2 + when b then (r - g) / d + 4 + end + + h /= 6.0 + end + + ColorModes::Hsl.new(h * 360, s, l, @input.a) + end + end + end +end diff --git a/lib/chroma/converters/hsv_converter.rb b/lib/chroma/converters/hsv_converter.rb new file mode 100644 index 0000000..3b04a87 --- /dev/null +++ b/lib/chroma/converters/hsv_converter.rb @@ -0,0 +1,33 @@ +module Chroma + module Converters + class HsvConverter < Base + def convert_rgb + r = bound01(@input.r, 255) + g = bound01(@input.g, 255) + b = bound01(@input.b, 255) + + rgb_array = [r, g, b] + + max = rgb_array.max + min = rgb_array.min + v = max + d = (max - min).to_f + s = max.zero? ? 0 : d / max + + if max == min + h = 0 + else + h = case max + when r then (g - b) / d + (g < b ? 6 : 0) + when g then (b - r) / d + 2 + when b then (r - g) / d + 4 + end + + h /= 6.0 + end + + ColorModes::Hsv.new(h * 360, s, v, @input.a) + end + end + end +end diff --git a/lib/chroma/converters/rgb_converter.rb b/lib/chroma/converters/rgb_converter.rb new file mode 100644 index 0000000..70c52c6 --- /dev/null +++ b/lib/chroma/converters/rgb_converter.rb @@ -0,0 +1,60 @@ +module Chroma + module Converters + class RgbConverter < Base + def convert_hsl + h, s, l = @input + + h = bound01(h, 360) + s = bound01(s, 100) + l = bound01(l, 100) + + if s.zero? + r = g = b = l * 255 + else + q = l < 0.5 ? l * (1 + s) : l + s - l * s + p = 2 * l - q + r = hue_to_rgb(p, q, h + 1/3.0) * 255 + g = hue_to_rgb(p, q, h) * 255 + b = hue_to_rgb(p, q, h - 1/3.0) * 255 + end + + ColorModes::Rgb.new(r, g, b) + end + + def convert_hsv + h, s, v = @input + + h = bound01(h, 360) * 6 + s = bound01(s, 100) + v = bound01(v, 100) + + i = h.floor + f = h - i + p = v * (1 - s) + q = v * (1 - f * s) + t = v * (1 - (1 - f) * s) + mod = i % 6 + + r = [v, q, p, p, t, v][mod] * 255 + g = [t, v, v, q, p, p][mod] * 255 + b = [p, p, t, v, v, q][mod] * 255 + + ColorModes::Rgb.new(r, g, b) + end + + private + + def hue_to_rgb(p, q, t) + if t < 0 then t += 1 + elsif t > 1 then t -= 1 + end + + if t < 1/6.0 then p + (q - p) * 6 * t + elsif t < 0.5 then q + elsif t < 2/3.0 then p + (q - p) * (2/3.0 - t) * 6 + else p + end + end + end + end +end diff --git a/lib/chroma/extensions/string.rb b/lib/chroma/extensions/string.rb new file mode 100644 index 0000000..67b8ac9 --- /dev/null +++ b/lib/chroma/extensions/string.rb @@ -0,0 +1,5 @@ +class String + def paint + Chroma.paint(self) + end +end diff --git a/lib/chroma/harmonies.rb b/lib/chroma/harmonies.rb new file mode 100644 index 0000000..4d125c6 --- /dev/null +++ b/lib/chroma/harmonies.rb @@ -0,0 +1,55 @@ +module Chroma + class Harmonies + def initialize(color) + @color = color + end + + def complement + [@color, @color.complement] + end + + def triad + hsl_map([0, 120, 240]) + end + + def tetrad + hsl_map([0, 90, 180, 270]) + end + + def split_complement + hsl_map([0, 72, 216]) + end + + def analogous(results = 6, slices = 30) + hsl = @color.to_hsl + part = 360 / slices + hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360 + + (results - 1).times.reduce([@color]) do |arr, n| + hsl.h = (hsl.h + part) % 360 + arr << Color.new(hsl) + end + end + + def monochromatic(results = 6) + h, s, v = @color.to_hsv + modification = 1.0 / results + + results.times.map do + Color.new(ColorModes::Hsv.new(h, s, v)).tap do + v = (v + modification) % 1 + end + end + end + + private + + def hsl_map(degrees) + h, s, l = @color.to_hsl + + degrees.map do |deg| + Color.new(ColorModes::Hsl.new((h + deg) % 360, s, l)) + end + end + end +end diff --git a/lib/chroma/helpers/bounders.rb b/lib/chroma/helpers/bounders.rb new file mode 100644 index 0000000..a4b18ab --- /dev/null +++ b/lib/chroma/helpers/bounders.rb @@ -0,0 +1,31 @@ +module Chroma + module Helpers + module Bounders + def bound01(n, max) + is_percent = n.to_s.include? '%' + n = [max, [0, n.to_f].max].min + n = (n * max).to_i / 100 if is_percent + + return 1 if (n - max).abs < 0.000001 + + (n % max) / max.to_f + end + + def bound_alpha(a) + a = a.to_f + a = 1 if a < 0 || a > 1 + a + end + + def clamp01(n) + [1, [0, n].max].min + end + + def to_percentage(n) + n = n.to_f + n = "#{n * 100}%" if n <= 1 + n + end + end + end +end diff --git a/lib/chroma/rgb_generator.rb b/lib/chroma/rgb_generator.rb new file mode 100644 index 0000000..2763ca5 --- /dev/null +++ b/lib/chroma/rgb_generator.rb @@ -0,0 +1,35 @@ +module Chroma + module RgbGenerator + class << self + def generate_rgb_and_format(input) + get_generator(input).generate.tap do |(rgb)| + rgb.r = round(rgb.r) + rgb.g = round(rgb.g) + rgb.b = round(rgb.b) + end + end + + private + + def get_generator(input) + klass = case input + when String then FromString + when ColorModes::Hsl then FromHsl + when ColorModes::Hsv then FromHsv + when ColorModes::Rgb then FromRgb + end + + klass.new(input) + end + + def round(n) + if n < 1 + n.round + else + #(n * 100).round / 100 + n + end + end + end + end +end diff --git a/lib/chroma/rgb_generator/base.rb b/lib/chroma/rgb_generator/base.rb new file mode 100644 index 0000000..60cac83 --- /dev/null +++ b/lib/chroma/rgb_generator/base.rb @@ -0,0 +1,7 @@ +module Chroma + module RgbGenerator + class Base + include Helpers::Bounders + end + end +end diff --git a/lib/chroma/rgb_generator/from_hex_string_values.rb b/lib/chroma/rgb_generator/from_hex_string_values.rb new file mode 100644 index 0000000..807ba99 --- /dev/null +++ b/lib/chroma/rgb_generator/from_hex_string_values.rb @@ -0,0 +1,30 @@ +module Chroma + module RgbGenerator + class FromHexStringValues < Base + def initialize(format, r, g, b, a = 'ff') + @format = format || :hex + @r, @g, @b, @a = r, g, b, a + end + + def generate + r, g, b = [@r, @g, @b].map { |n| n.to_i(16) } + a = @a.to_i(16) / 255.0 + [ColorModes::Rgb.new(r, g, b, a), @format] + end + + class << self + def from_hex3(format, r, g, b) + new(format || :hex3, r * 2, g * 2, b * 2) + end + + def from_hex6(format, r, g, b) + new(format, r, g, b) + end + + def from_hex8(format, r, g, b, a) + new(format || :hex8, r, g, b, a) + end + end + end + end +end diff --git a/lib/chroma/rgb_generator/from_hsl.rb b/lib/chroma/rgb_generator/from_hsl.rb new file mode 100644 index 0000000..0f8ec3e --- /dev/null +++ b/lib/chroma/rgb_generator/from_hsl.rb @@ -0,0 +1,14 @@ +module Chroma + module RgbGenerator + class FromHsl < Base + def initialize(format, hsl) + @format = format || :hsl + @hsl = hsl + end + + def generate + [Converters::RgbConverter.convert_hsl(@hsl), @format] + end + end + end +end diff --git a/lib/chroma/rgb_generator/from_hsl_values.rb b/lib/chroma/rgb_generator/from_hsl_values.rb new file mode 100644 index 0000000..81a6dfa --- /dev/null +++ b/lib/chroma/rgb_generator/from_hsl_values.rb @@ -0,0 +1,17 @@ +module Chroma + module RgbGenerator + class FromHslValues < Base + def initialize(format, h, s, l, a = 1) + s = to_percentage(s) + l = to_percentage(l) + + @format = format + @hsl = ColorModes::Hsl.new(h, s, l, a) + end + + def generate + FromHsl.new(@format, @hsl).generate + end + end + end +end diff --git a/lib/chroma/rgb_generator/from_hsv.rb b/lib/chroma/rgb_generator/from_hsv.rb new file mode 100644 index 0000000..2796952 --- /dev/null +++ b/lib/chroma/rgb_generator/from_hsv.rb @@ -0,0 +1,14 @@ +module Chroma + module RgbGenerator + class FromHsv < Base + def initialize(format, hsv) + @format = format || :hsv + @hsv = hsv + end + + def generate + [Converters::RgbConverter.convert_hsv(@hsv), @format] + end + end + end +end diff --git a/lib/chroma/rgb_generator/from_hsv_values.rb b/lib/chroma/rgb_generator/from_hsv_values.rb new file mode 100644 index 0000000..7e55ef8 --- /dev/null +++ b/lib/chroma/rgb_generator/from_hsv_values.rb @@ -0,0 +1,17 @@ +module Chroma + module RgbGenerator + class FromHsvValues < Base + def initialize(format, h, s, v, a = 1) + s = to_percentage(s) + v = to_percentage(v) + + @format = format + @hsv = ColorModes::Hsv.new(h, s, v, a) + end + + def generate + FromHsv.new(@format, @hsv).generate + end + end + end +end diff --git a/lib/chroma/rgb_generator/from_rgb.rb b/lib/chroma/rgb_generator/from_rgb.rb new file mode 100644 index 0000000..ac44bb7 --- /dev/null +++ b/lib/chroma/rgb_generator/from_rgb.rb @@ -0,0 +1,14 @@ +module Chroma + module RgbGenerator + class FromRgb < Base + def initialize(format, rgb) + @format = format || :rgb + @rgb = rgb + end + + def generate + FromRgbValues.new(@format, @rgb.r, @rgb.g, @rgb.b, @rgb.a).generate + end + end + end +end diff --git a/lib/chroma/rgb_generator/from_rgb_values.rb b/lib/chroma/rgb_generator/from_rgb_values.rb new file mode 100644 index 0000000..f9913fa --- /dev/null +++ b/lib/chroma/rgb_generator/from_rgb_values.rb @@ -0,0 +1,16 @@ +module Chroma + module RgbGenerator + class FromRgbValues < Base + def initialize(format, r, g, b, a = 1) + @format = format + @r, @g, @b, @a = r, g, b, a + end + + def generate + r, g, b = [@r, @g, @b].map { |n| bound01(n, 255) * 255 } + a = bound_alpha(@a) + [ColorModes::Rgb.new(r, g, b, a), @format] + end + end + end +end diff --git a/lib/chroma/rgb_generator/from_string.rb b/lib/chroma/rgb_generator/from_string.rb new file mode 100644 index 0000000..1563995 --- /dev/null +++ b/lib/chroma/rgb_generator/from_string.rb @@ -0,0 +1,78 @@ +module Chroma + module RgbGenerator + class FromString < Base + def self.matchers + @matchers ||= begin + # TinyColor.js matchers + css_int = '[-\\+]?\\d+%?' + css_num = '[-\\+]?\\d*\\.\\d+%?' + css_unit = "(?:#{css_num})|(?:#{css_int})" + permissive_prefix = '[\\s|\\(]+(' + permissive_delim = ')[,|\\s]+(' + permissive_suffix = ')\\s*\\)?' + permissive_match3 = "#{permissive_prefix}#{[css_unit] * 3 * permissive_delim}#{permissive_suffix}" + permissive_match4 = "#{permissive_prefix}#{[css_unit] * 4 * permissive_delim}#{permissive_suffix}" + hex_match = '[0-9a-fA-F]' + + { + rgb: { regex: /rgb#{permissive_match3}/, class_name: :FromRgbValues }, + rgba: { regex: /rgba#{permissive_match4}/, class_name: :FromRgbValues }, + hsl: { regex: /hsl#{permissive_match3}/, class_name: :FromHslValues }, + hsla: { regex: /hsla#{permissive_match4}/, class_name: :FromHslValues }, + hsv: { regex: /hsv#{permissive_match3}/, class_name: :FromHsvValues }, + hsva: { regex: /hsva#{permissive_match4}/, class_name: :FromHsvValues }, + hex3: { regex: /^#?#{"(#{hex_match}{1})" * 3}$/, class_name: :FromHexStringValues, builder: :from_hex3 }, + hex6: { regex: /^#?#{"(#{hex_match}{2})" * 3}$/, class_name: :FromHexStringValues, builder: :from_hex6 }, + hex8: { regex: /^#?#{"(#{hex_match}{2})" * 4}$/, class_name: :FromHexStringValues, builder: :from_hex8 } + }.freeze + end + end + + def initialize(input) + @input = normalize_input(input) + end + + def generate + get_generator.generate + end + + private + + def get_generator + if color = Chroma.hex_from_name(@input) + format = :name + else + format = nil + color = @input + end + + match = nil + + _, hash = matchers.find do |_, h| + !(match = h[:regex].match(color)).nil? + end + + raise 'Unrecognized color' if match.nil? + + build_generator(match[1..-1], hash[:class_name], hash[:builder], format) + end + + def build_generator(args, class_name, builder, format) + builder ||= :new + klass = RgbGenerator.const_get(class_name) + klass.__send__(builder, *([format] + args)) + end + + def normalize_input(input) + input.clone.tap do |str| + str.strip! + str.downcase! + end + end + + def matchers + self.class.matchers + end + end + end +end diff --git a/lib/chroma/version.rb b/lib/chroma/version.rb new file mode 100644 index 0000000..db6e7d5 --- /dev/null +++ b/lib/chroma/version.rb @@ -0,0 +1,3 @@ +module Chroma + VERSION = '0.0.1.alpha.1' +end diff --git a/lib/support/named_colors.yml b/lib/support/named_colors.yml new file mode 100644 index 0000000..a43a269 --- /dev/null +++ b/lib/support/named_colors.yml @@ -0,0 +1,149 @@ +aliceblue: 'f0f8ff' +antiquewhite: 'faebd7' +aqua: '0ff' +aquamarine: '7fffd4' +azure: 'f0ffff' +beige: 'f5f5dc' +bisque: 'ffe4c4' +black: '000' +blanchedalmond: 'ffebcd' +blue: '00f' +blueviolet: '8a2be2' +brown: 'a52a2a' +burlywood: 'deb887' +burntsienna: 'ea7e5d' +cadetblue: '5f9ea0' +chartreuse: '7fff00' +chocolate: 'd2691e' +coral: 'ff7f50' +cornflowerblue: '6495ed' +cornsilk: 'fff8dc' +crimson: 'dc143c' +cyan: '0ff' +darkblue: '00008b' +darkcyan: '008b8b' +darkgoldenrod: 'b8860b' +darkgray: 'a9a9a9' +darkgreen: '006400' +darkgrey: 'a9a9a9' +darkkhaki: 'bdb76b' +darkmagenta: '8b008b' +darkolivegreen: '556b2f' +darkorange: 'ff8c00' +darkorchid: '9932cc' +darkred: '8b0000' +darksalmon: 'e9967a' +darkseagreen: '8fbc8f' +darkslateblue: '483d8b' +darkslategray: '2f4f4f' +darkslategrey: '2f4f4f' +darkturquoise: '00ced1' +darkviolet: '9400d3' +deeppink: 'ff1493' +deepskyblue: '00bfff' +dimgray: '696969' +dimgrey: '696969' +dodgerblue: '1e90ff' +firebrick: 'b22222' +floralwhite: 'fffaf0' +forestgreen: '228b22' +fuchsia: 'f0f' +gainsboro: 'dcdcdc' +ghostwhite: 'f8f8ff' +gold: 'ffd700' +goldenrod: 'daa520' +gray: '808080' +green: '008000' +greenyellow: 'adff2f' +grey: '808080' +honeydew: 'f0fff0' +hotpink: 'ff69b4' +indianred: 'cd5c5c' +indigo: '4b0082' +ivory: 'fffff0' +khaki: 'f0e68c' +lavender: 'e6e6fa' +lavenderblush: 'fff0f5' +lawngreen: '7cfc00' +lemonchiffon: 'fffacd' +lightblue: 'add8e6' +lightcoral: 'f08080' +lightcyan: 'e0ffff' +lightgoldenrodyellow: 'fafad2' +lightgray: 'd3d3d3' +lightgreen: '90ee90' +lightgrey: 'd3d3d3' +lightpink: 'ffb6c1' +lightsalmon: 'ffa07a' +lightseagreen: '20b2aa' +lightskyblue: '87cefa' +lightslategray: '789' +lightslategrey: '789' +lightsteelblue: 'b0c4de' +lightyellow: 'ffffe0' +lime: '0f0' +limegreen: '32cd32' +linen: 'faf0e6' +magenta: 'f0f' +maroon: '800000' +mediumaquamarine: '66cdaa' +mediumblue: '0000cd' +mediumorchid: 'ba55d3' +mediumpurple: '9370db' +mediumseagreen: '3cb371' +mediumslateblue: '7b68ee' +mediumspringgreen: '00fa9a' +mediumturquoise: '48d1cc' +mediumvioletred: 'c71585' +midnightblue: '191970' +mintcream: 'f5fffa' +mistyrose: 'ffe4e1' +moccasin: 'ffe4b5' +navajowhite: 'ffdead' +navy: '000080' +oldlace: 'fdf5e6' +olive: '808000' +olivedrab: '6b8e23' +orange: 'ffa500' +orangered: 'ff4500' +orchid: 'da70d6' +palegoldenrod: 'eee8aa' +palegreen: '98fb98' +paleturquoise: 'afeeee' +palevioletred: 'db7093' +papayawhip: 'ffefd5' +peachpuff: 'ffdab9' +peru: 'cd853f' +pink: 'ffc0cb' +plum: 'dda0dd' +powderblue: 'b0e0e6' +purple: '800080' +rebeccapurple: '663399' +red: 'f00' +rosybrown: 'bc8f8f' +royalblue: '4169e1' +saddlebrown: '8b4513' +salmon: 'fa8072' +sandybrown: 'f4a460' +seagreen: '2e8b57' +seashell: 'fff5ee' +sienna: 'a0522d' +silver: 'c0c0c0' +skyblue: '87ceeb' +slateblue: '6a5acd' +slategray: '708090' +slategrey: '708090' +snow: 'fffafa' +springgreen: '00ff7f' +steelblue: '4682b4' +tan: 'd2b48c' +teal: '008080' +thistle: 'd8bfd8' +tomato: 'ff6347' +turquoise: '40e0d0' +violet: 'ee82ee' +wheat: 'f5deb3' +white: 'fff' +whitesmoke: 'f5f5f5' +yellow: 'ff0' +yellowgreen: '9acd32'