├── .yardopts ├── .rspec ├── lib ├── chroma │ ├── version.rb │ ├── errors.rb │ ├── rgb_generator │ │ ├── base.rb │ │ ├── from_hsl.rb │ │ ├── from_hsv.rb │ │ ├── from_rgb.rb │ │ ├── from_hsv_values.rb │ │ ├── from_rgb_values.rb │ │ ├── from_hsl_values.rb │ │ ├── from_hex_string_values.rb │ │ └── from_string.rb │ ├── extensions │ │ └── string.rb │ ├── converters │ │ ├── base.rb │ │ ├── hsv_converter.rb │ │ ├── hsl_converter.rb │ │ └── rgb_converter.rb │ ├── rgb_generator.rb │ ├── helpers │ │ └── bounders.rb │ ├── color │ │ ├── attributes.rb │ │ ├── modifiers.rb │ │ └── serializers.rb │ ├── color_modes.rb │ ├── palette_builder.rb │ ├── color.rb │ └── harmonies.rb ├── support │ └── named_colors.yml └── chroma.rb ├── Gemfile ├── .gitignore ├── Rakefile ├── .travis.yml ├── spec ├── custom_matchers.rb ├── color │ ├── custom_palette_spec.rb │ ├── attributes_spec.rb │ ├── modifiers_spec.rb │ ├── serializers_spec.rb │ └── palette_spec.rb ├── color_spec.rb ├── chroma_spec.rb ├── chroma │ ├── define_palette_spec.rb │ └── paint_spec.rb └── spec_helper.rb ├── LICENSE ├── chroma.gemspec ├── CHANGELOG.md └── README.md /.yardopts: -------------------------------------------------------------------------------- 1 | --markup=markdown 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /lib/chroma/version.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | VERSION = '0.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in colortastic.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/chroma/errors.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module Errors 3 | class PaletteDefinedError < StandardError; end 4 | class UnrecognizedColor < StandardError; end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) 6 | task default: :spec 7 | rescue LoadError 8 | # Rspec not available 9 | end 10 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/base.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | # Base rgb generator class. 4 | # @abstract 5 | class Base 6 | include Helpers::Bounders 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | langage: ruby 2 | 3 | rvm: 4 | - 2.4 5 | - 2.3 6 | - 2.2 7 | 8 | before_install: 9 | - gem update --system 10 | - gem --version 11 | - gem update bundler 12 | 13 | script: 14 | - rake spec 15 | -------------------------------------------------------------------------------- /lib/chroma/extensions/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | # Creates {Chroma::Color} directly from a string representing a color. 3 | # 4 | # @example 5 | # 'red'.paint 6 | # '#f00'.paint 7 | # '#ff0000'.paint 8 | # 'rgb(255, 0, 0)'.paint 9 | # 'hsl(0, 100%, 50%)'.paint 10 | # 'hsv(0, 100%, 100%)'.paint 11 | # 12 | # @return [Chroma::Color] 13 | def paint 14 | Chroma.paint(self) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/custom_matchers.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :generate_palette do |expected| 2 | expected.map!(&:paint) 3 | 4 | match do |actual| 5 | actual == expected 6 | end 7 | end 8 | 9 | RSpec::Matchers.define :have_format do |expected| 10 | match do |actual| 11 | actual.format == expected 12 | end 13 | end 14 | 15 | RSpec::Matchers.define :be_dark do 16 | match { |value| value.dark? } 17 | end 18 | 19 | RSpec::Matchers.define :be_light do 20 | match { |value| value.light? } 21 | end 22 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/from_hsl.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | class FromHsl < Base 4 | # @param format [Symbol] color format 5 | # @param hsl [ColorModes::Hsl] 6 | def initialize(format, hsl) 7 | @format = format 8 | @hsl = hsl 9 | end 10 | 11 | # Generates a {ColorModes::Rgb}. 12 | # @return [ColorModes::Rgb] 13 | def generate 14 | FromHslValues.new(@format, *@hsl.to_a).generate 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/from_hsv.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | class FromHsv < Base 4 | # @param format [Symbol] color format 5 | # @param hsv [ColorModes::Hsv] 6 | def initialize(format, hsv) 7 | @format = format 8 | @hsv = hsv 9 | end 10 | 11 | # Generates a {ColorModes::Rgb}. 12 | # @return [ColorModes::Rgb] 13 | def generate 14 | FromHsvValues.new(@format, *@hsv.to_a).generate 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/color/custom_palette_spec.rb: -------------------------------------------------------------------------------- 1 | describe Chroma::Color, '#custom_palette' do 2 | let(:subject) do 3 | 'red'.paint.custom_palette do 4 | spin 60 5 | spin 180 6 | spin(60).brighten(20) 7 | greyscale 8 | end 9 | end 10 | 11 | it 'generates the correct colors' do 12 | expect(subject). 13 | to generate_palette %w(#ff0000 #ffff00 #00ffff #ffff33 #808080) 14 | end 15 | 16 | it 'keeps the same format' do 17 | expect(subject).to all have_format :name 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/from_rgb.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | class FromRgb < Base 4 | # @param format [Symbol] color format 5 | # @param rgb [ColorModes::Rgb] 6 | def initialize(format, rgb) 7 | @format = format 8 | @rgb = rgb 9 | end 10 | 11 | # Generates a {ColorModes::Rgb}. 12 | # @return [ColorModes::Rgb] 13 | def generate 14 | FromRgbValues.new(@format, @rgb.r, @rgb.g, @rgb.b, @rgb.a).generate 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Jeremy Fairbank 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /spec/color_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chroma::Color do 4 | let(:red) { 'red'.paint } 5 | let(:other_red) { '#f00'.paint } 6 | let(:blue) { 'blue'.paint } 7 | 8 | context 'with equality' do 9 | it 'equals itself' do 10 | expect(red).to eql(red) 11 | expect(red).to eq(red) 12 | end 13 | 14 | it 'equals another instance of the same color' do 15 | expect(red).to eql(other_red) 16 | expect(red).to eq(other_red) 17 | end 18 | 19 | it 'does not equal another instance of a different color' do 20 | expect(red).to_not eql(blue) 21 | expect(red).to_not eq(blue) 22 | end 23 | end 24 | 25 | describe '#paint' do 26 | it 'returns itself' do 27 | expect(red.paint).to equal(red) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/chroma_spec.rb: -------------------------------------------------------------------------------- 1 | describe Chroma do 2 | describe '.hex_from_name' do 3 | it 'returns the hex representation for a color name' do 4 | Chroma.send(:named_colors_map).each do |name, hex| 5 | expect(Chroma.hex_from_name(name)).to eq hex 6 | end 7 | end 8 | 9 | it 'returns nil for unknown colors names' do 10 | expect(Chroma.hex_from_name('foo')).to be_nil 11 | end 12 | end 13 | 14 | describe '.name_from_hex' do 15 | it 'returns a color name for a hex representation' do 16 | Chroma.send(:named_colors_map).each do |name, hex| 17 | expect(Chroma.name_from_hex(hex)).to be 18 | end 19 | end 20 | 21 | it 'returns nil for hex values without a corresponding color name' do 22 | expect(Chroma.name_from_hex('#123123')).to be_nil 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/from_hsv_values.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | class FromHsvValues < Base 4 | # @param format [Symbol] color format 5 | # @param h [String, Numeric] hue value 6 | # @param s [String, Numeric] saturation value 7 | # @param v [String, Numeric] value value 8 | # @param a [String, Numeric] alpha value 9 | def initialize(format, h, s, v, a = 1) 10 | s = to_percentage(s) 11 | v = to_percentage(v) 12 | 13 | @format = format || :hsv 14 | @hsv = ColorModes::Hsv.new(h, s, v, a) 15 | end 16 | 17 | # Generates a {ColorModes::Rgb}. 18 | # @return [ColorModes::Rgb] 19 | def generate 20 | [Converters::RgbConverter.convert_hsv(@hsv), @format] 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/from_rgb_values.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | class FromRgbValues < Base 4 | # @param format [Symbol] color format 5 | # @param r [String, Numeric] red value 6 | # @param g [String, Numeric] green value 7 | # @param b [String, Numeric] blue value 8 | # @param a [String, Numeric] alpha value 9 | def initialize(format, r, g, b, a = 1) 10 | @format = format || :rgb 11 | @r, @g, @b, @a = r, g, b, a 12 | end 13 | 14 | # Generates a {ColorModes::Rgb}. 15 | # @return [ColorModes::Rgb] 16 | def generate 17 | r, g, b = [@r, @g, @b].map { |n| bound01(n, 255) * 255 } 18 | a = bound_alpha(@a) 19 | [ColorModes::Rgb.new(r, g, b, a), @format] 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/from_hsl_values.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | class FromHslValues < Base 4 | # @param format [Symbol] color format 5 | # @param h [String, Numeric] hue value 6 | # @param s [String, Numeric] saturation value 7 | # @param l [String, Numeric] lightness value 8 | # @param a [String, Numeric] alpha value 9 | def initialize(format, h, s, l, a = 1) 10 | s = to_percentage(s) 11 | l = to_percentage(l) 12 | 13 | @format = format || :hsl 14 | @hsl = ColorModes::Hsl.new(h, s, l, a) 15 | end 16 | 17 | # Generates a {ColorModes::Rgb}. 18 | # @return [ColorModes::Rgb] 19 | def generate 20 | [Converters::RgbConverter.convert_hsl(@hsl), @format] 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/chroma/define_palette_spec.rb: -------------------------------------------------------------------------------- 1 | describe Chroma, '.define_palette' do 2 | def add_palette 3 | Chroma.define_palette :foo do 4 | spin 60 5 | spin 180 6 | spin(60).brighten(20) 7 | greyscale 8 | end 9 | end 10 | 11 | def remove_palette 12 | if Chroma::Harmonies.method_defined? :foo 13 | Chroma::Harmonies.send(:remove_method, :foo) 14 | end 15 | end 16 | 17 | after(:example) { remove_palette } 18 | 19 | let(:red) { '#ff0000'.paint } 20 | 21 | it 'adds the new palette method' do 22 | expect(red.palette).to_not respond_to(:foo) 23 | add_palette 24 | expect(red.palette).to respond_to(:foo) 25 | end 26 | 27 | it 'generates the correct colors' do 28 | add_palette 29 | 30 | expect(red.palette.foo). 31 | to generate_palette %w(#ff0000 #ffff00 #00ffff #ffff33 #808080) 32 | end 33 | 34 | it 'keeps the same format' do 35 | add_palette 36 | 37 | expect('red'.paint.palette.foo).to all have_format :name 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/chroma/converters/base.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module Converters 3 | # Base class for converting one color mode to another. 4 | # @abstract 5 | class Base 6 | include Helpers::Bounders 7 | 8 | # @param input [ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv] 9 | # @return [Base] 10 | def initialize(input) 11 | @input = input 12 | end 13 | 14 | # @param rgb [ColorModes::Rgb] 15 | # @return [ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv] 16 | def self.convert_rgb(rgb) 17 | new(rgb).convert_rgb 18 | end 19 | 20 | # @param hsl [ColorModes::Hsl] 21 | # @return [ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv] 22 | def self.convert_hsl(hsl) 23 | new(hsl).convert_hsl 24 | end 25 | 26 | # @param hsv [ColorModes::Hsv] 27 | # @return [ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv] 28 | def self.convert_hsv(hsv) 29 | new(hsv).convert_hsv 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /chroma.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'chroma/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'chroma' 8 | spec.version = Chroma::VERSION 9 | spec.authors = ['Jeremy Fairbank'] 10 | spec.email = ['elpapapollo@gmail.com'] 11 | spec.summary = %q{Color manipulation and palette generation.} 12 | spec.description = %q{Chroma is a color manipulation and palette generation gem.} 13 | spec.homepage = 'https://github.com/jfairbank/chroma' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.15' 22 | spec.add_development_dependency 'rake', '~> 12.0' 23 | spec.add_development_dependency 'rspec', '~> 3.6.0' 24 | end 25 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | # Main module to generate an instance of {ColorModes::Rgb} from several 3 | # possible inputs. 4 | module RgbGenerator 5 | class << self 6 | # Generates an instance of {ColorModes::Rgb} as well as color format 7 | # symbol. 8 | # 9 | # @param input [String, ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv] 10 | # @return [[ColorModes::Rgb, Symbol]] 11 | def generate_rgb_and_format(input) 12 | get_generator(input).generate.tap do |(rgb)| 13 | rgb.r = round(rgb.r) 14 | rgb.g = round(rgb.g) 15 | rgb.b = round(rgb.b) 16 | end 17 | end 18 | 19 | private 20 | 21 | def get_generator(input) 22 | klass = case input 23 | when String then FromString 24 | when ColorModes::Hsl then FromHsl 25 | when ColorModes::Hsv then FromHsv 26 | when ColorModes::Rgb then FromRgb 27 | end 28 | 29 | klass.new(nil, input) 30 | end 31 | 32 | def round(n) 33 | n < 1 ? n.round : n 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/chroma/converters/hsv_converter.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module Converters 3 | # Class to convert a color mode to {ColorModes::Hsl}. 4 | class HsvConverter < Base 5 | # Convert rgb to hsv. 6 | # @return [ColorModes::Hsv] 7 | def convert_rgb 8 | r = bound01(@input.r, 255) 9 | g = bound01(@input.g, 255) 10 | b = bound01(@input.b, 255) 11 | 12 | rgb_array = [r, g, b] 13 | 14 | max = rgb_array.max 15 | min = rgb_array.min 16 | v = max 17 | d = (max - min).to_f 18 | s = max.zero? ? 0 : d / max 19 | 20 | if max == min 21 | h = 0 22 | else 23 | h = case max 24 | when r then (g - b) / d + (g < b ? 6 : 0) 25 | when g then (b - r) / d + 2 26 | when b then (r - g) / d + 4 27 | end 28 | 29 | h /= 6.0 30 | end 31 | 32 | ColorModes::Hsv.new(h * 360, s, v, @input.a) 33 | end 34 | 35 | # Convert hsl to hsv. 36 | # @return [ColorModes::Hsv] 37 | def convert_hsl 38 | HslConverter.convert_rgb(RgbConverter.convert_hsv(@input)) 39 | end 40 | 41 | # Returns @input because it's the same color mode. 42 | # @return [ColorModes::Hsv] 43 | def convert_hsv 44 | @input 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/chroma/helpers/bounders.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module Helpers 3 | module Bounders 4 | # Bounds a value `n` that is from `0` to `max` to `0` to `1`. 5 | # 6 | # @param n [Numeric, String] 7 | # @param max [Fixnum] 8 | # @return [Float] 9 | def bound01(n, max) 10 | is_percent = n.to_s.include? '%' 11 | n = [max, [0, n.to_f].max].min 12 | n = (n * max).to_i / 100.0 if is_percent 13 | 14 | return 1 if (n - max).abs < 0.000001 15 | 16 | (n % max) / max.to_f 17 | end 18 | 19 | # Ensure alpha value `a` is between `0` and `1`. 20 | # 21 | # @param a [Numeric, String] alpha value 22 | # @return [Numeric] 23 | def bound_alpha(a) 24 | a = a.to_f 25 | a = 1 if a < 0 || a > 1 26 | a 27 | end 28 | 29 | # Ensures a number between `0` and `1`. Returns `n` if it is between `0` 30 | # and `1`. 31 | # 32 | # @param n [Numeric] 33 | # @return [Numeric] 34 | def clamp01(n) 35 | [1, [0, n].max].min 36 | end 37 | 38 | # Converts `n` to a percentage type value. 39 | # 40 | # @param n [Numeric, String] 41 | # @return [String, Float] 42 | def to_percentage(n) 43 | n = n.to_f 44 | n = "#{n * 100}%" if n <= 1 45 | n 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/chroma/color/attributes.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | class Color 3 | # Attribute methods for {Color}. 4 | module Attributes 5 | attr_reader :format 6 | 7 | # Determines if the color is dark. 8 | # 9 | # @example 10 | # 'red'.paint.dark? #=> true 11 | # 'yellow'.paint.dark? #=> false 12 | # 13 | # @return [true, false] 14 | def dark? 15 | brightness < 128 16 | end 17 | 18 | # Determines if the color is light. 19 | # 20 | # @example 21 | # 'red'.paint.light? #=> false 22 | # 'yellow'.paint.light? #=> true 23 | # 24 | # @return [true, false] 25 | def light? 26 | !dark? 27 | end 28 | 29 | # Returns the alpha channel value. 30 | # 31 | # @example 32 | # 'red'.paint.alpha #=> 1.0 33 | # 'rgba(0, 0, 0, 0.5)'.paint.alpha #=> 0.5 34 | # 35 | # @return [Float] 36 | def alpha 37 | @rgb.a 38 | end 39 | 40 | # Calculates the brightness. 41 | # 42 | # @example 43 | # 'red'.paint.brightness #=> 76.245 44 | # 'yellow'.paint.brightness #=> 225.93 45 | # 46 | # @return [Float] 47 | def brightness 48 | (@rgb.r * 299 + @rgb.g * 587 + @rgb.b * 114) / 1000.0 49 | end 50 | 51 | private 52 | 53 | def rounded_alpha 54 | @rounded_alpha ||= (alpha * 100).round / 100.0 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/chroma/converters/hsl_converter.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module Converters 3 | # Class to convert a color mode to {ColorModes::Hsl}. 4 | class HslConverter < Base 5 | # Convert rgb to hsl. 6 | # @return [ColorModes::Hsl] 7 | def convert_rgb 8 | r = bound01(@input.r, 255) 9 | g = bound01(@input.g, 255) 10 | b = bound01(@input.b, 255) 11 | 12 | rgb_array = [r, g, b] 13 | 14 | max = rgb_array.max 15 | min = rgb_array.min 16 | l = (max + min) * 0.5 17 | 18 | if max == min 19 | h = s = 0 20 | else 21 | d = (max - min).to_f 22 | 23 | s = if l > 0.5 24 | d / (2 - max - min) 25 | else 26 | d / (max + min) 27 | end 28 | 29 | h = case max 30 | when r then (g - b) / d + (g < b ? 6 : 0) 31 | when g then (b - r) / d + 2 32 | when b then (r - g) / d + 4 33 | end 34 | 35 | h /= 6.0 36 | end 37 | 38 | ColorModes::Hsl.new(h * 360, s, l, @input.a) 39 | end 40 | 41 | # Returns @input because it's the same color mode. 42 | # @return [ColorModes::Hsl] 43 | def convert_hsl 44 | @input 45 | end 46 | 47 | # Convert hsv to hsl. 48 | # @return [ColorModes::Hsl] 49 | def convert_hsv 50 | HsvConverter.convert_rgb(RgbConverter.convert_hsl(@input)) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/color/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | describe Chroma::Color do 2 | let(:red) { 'red'.paint } 3 | let(:black) { 'black'.paint } 4 | let(:white) { 'white'.paint } 5 | let(:yellow) { 'yellow'.paint } 6 | 7 | describe '#dark?' do 8 | it 'returns true for dark colors' do 9 | expect(red).to be_dark 10 | expect(black).to be_dark 11 | end 12 | 13 | it 'returns false for light colors' do 14 | expect(white).to_not be_dark 15 | expect(yellow).to_not be_dark 16 | end 17 | end 18 | 19 | describe '#light?' do 20 | it 'returns false for dark colors' do 21 | expect(red).to_not be_light 22 | expect(black).to_not be_light 23 | end 24 | 25 | it 'returns true for light colors' do 26 | expect(white).to be_light 27 | expect(yellow).to be_light 28 | end 29 | end 30 | 31 | describe '#alpha' do 32 | it 'returns the correct alpha value' do 33 | expect('rgba(255, 0, 0, 0.75)'.paint.alpha).to eq 0.75 34 | expect('#80ff0000'.paint.alpha).to be_within(0.01).of(0.5) 35 | expect('transparent'.paint.alpha).to eq 0 36 | expect('hsla(0, 100%, 50%, 0'.paint.alpha).to eq 0 37 | expect(red.alpha).to eq 1 38 | end 39 | end 40 | 41 | describe '#brightness' do 42 | it 'returns the correct brightness' do 43 | expect(red.brightness).to eq 76.245 44 | expect(black.brightness).to eq 0 45 | expect(white.brightness).to eq 255 46 | expect(yellow.brightness).to eq 225.93 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/chroma/color_modes.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module ColorModes 3 | class << self 4 | private 5 | 6 | # Builds a new color mode class. 7 | # 8 | # @param name [String] the class name 9 | # @param attrs [Array] the instance attribute names 10 | # @!macro [attach] build 11 | # @!parse class $1 12 | # attr_accessor :$2, :$3, :$4, :a 13 | # 14 | # # @param $2 [Numeric] 15 | # # @param $3 [Numeric] 16 | # # @param $4 [Numeric] 17 | # # @param a [Numeric] 18 | # def initialize(${2-4}, a = 1) 19 | # @$2, @$3, @$4, @a = $2, $3, $4, a 20 | # end 21 | # 22 | # # Returns the values `$2`, `$3`, `$4`, and `a` as an array. 23 | # # 24 | # # @return [Array] 25 | # def to_a 26 | # [@$2, @$3, @$4, @a] 27 | # end 28 | # 29 | # alias_method :to_ary, :to_a 30 | # end 31 | def build(name, *attrs) 32 | class_eval <<-EOS 33 | class #{name} 34 | attr_accessor #{(attrs + [:a]).map{|attr| ":#{attr}"} * ', '} 35 | 36 | def initialize(#{attrs * ', '}, a = 1) 37 | #{attrs.map{|attr| "@#{attr}"} * ', '}, @a = #{attrs * ', '}, a 38 | end 39 | 40 | def to_a 41 | [#{attrs.map{|attr| "@#{attr}"} * ', '}, @a] 42 | end 43 | 44 | alias_method :to_ary, :to_a 45 | end 46 | EOS 47 | end 48 | end 49 | 50 | private 51 | 52 | build 'Rgb', :r, :g, :b 53 | build 'Hsl', :h, :s, :l 54 | build 'Hsv', :h, :s, :v 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/chroma/paint_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chroma do 4 | describe '.paint' do 5 | context 'with named color' do 6 | it 'creates a color' do 7 | expect(Chroma.paint('red')).to be_a(Chroma::Color) 8 | end 9 | end 10 | 11 | context 'with 6 character hexadecimal' do 12 | it 'creates a color' do 13 | expect(Chroma.paint('#ff0000')).to be_a(Chroma::Color) 14 | expect(Chroma.paint('ff0000')).to be_a(Chroma::Color) 15 | end 16 | end 17 | 18 | context 'with 3 character hexadecimal' do 19 | it 'creates a color' do 20 | expect(Chroma.paint('#f00')).to be_a(Chroma::Color) 21 | expect(Chroma.paint('f00')).to be_a(Chroma::Color) 22 | end 23 | end 24 | 25 | context 'with 8 character hexadecimal' do 26 | let(:hex) { '#80ff0000' } 27 | 28 | it 'creates a color' do 29 | expect(Chroma.paint(hex)).to be_a(Chroma::Color) 30 | expect(Chroma.paint(hex[1..-1])).to be_a(Chroma::Color) 31 | end 32 | 33 | it 'sets alpha' do 34 | expect(Chroma.paint(hex).alpha).to be_within(0.1).of(0.5) 35 | end 36 | end 37 | 38 | context 'with rgb' do 39 | it 'creates a color' do 40 | expect(Chroma.paint('rgb(255, 0, 0)')).to be_a(Chroma::Color) 41 | expect(Chroma.paint('rgba(255, 0, 0, 0.5)')).to be_a(Chroma::Color) 42 | end 43 | 44 | it 'sets alpha' do 45 | expect(Chroma.paint('rgba(255, 0, 0, 0.5)').alpha).to eq(0.5) 46 | end 47 | end 48 | 49 | context 'with hsl' do 50 | it 'creates a color' do 51 | expect(Chroma.paint('hsl(120, 100%, 50%)')).to be_a(Chroma::Color) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/chroma/converters/rgb_converter.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module Converters 3 | # Class to convert a color mode to {ColorModes::Rgb}. 4 | class RgbConverter < Base 5 | # Returns @input because it's the same color mode. 6 | # @return [ColorModes::Rgb] 7 | def convert_rgb 8 | @input 9 | end 10 | 11 | # Convert hsl to rgb. 12 | # @return [ColorModes::Rgb] 13 | def convert_hsl 14 | h, s, l = @input 15 | 16 | h = bound01(h, 360) 17 | s = bound01(s, 100) 18 | l = bound01(l, 100) 19 | 20 | if s.zero? 21 | r = g = b = l * 255 22 | else 23 | q = l < 0.5 ? l * (1 + s) : l + s - l * s 24 | p = 2 * l - q 25 | r = hue_to_rgb(p, q, h + 1/3.0) * 255 26 | g = hue_to_rgb(p, q, h) * 255 27 | b = hue_to_rgb(p, q, h - 1/3.0) * 255 28 | end 29 | 30 | ColorModes::Rgb.new(r, g, b, bound_alpha(@input.a)) 31 | end 32 | 33 | # Convert hsv to rgb. 34 | # @return [ColorModes::Rgb] 35 | def convert_hsv 36 | h, s, v = @input 37 | 38 | h = bound01(h, 360) * 6 39 | s = bound01(s, 100) 40 | v = bound01(v, 100) 41 | 42 | i = h.floor 43 | f = h - i 44 | p = v * (1 - s) 45 | q = v * (1 - f * s) 46 | t = v * (1 - (1 - f) * s) 47 | mod = i % 6 48 | 49 | r = [v, q, p, p, t, v][mod] * 255 50 | g = [t, v, v, q, p, p][mod] * 255 51 | b = [p, p, t, v, v, q][mod] * 255 52 | 53 | ColorModes::Rgb.new(r, g, b, bound_alpha(@input.a)) 54 | end 55 | 56 | private 57 | 58 | def hue_to_rgb(p, q, t) 59 | if t < 0 then t += 1 60 | elsif t > 1 then t -= 1 61 | end 62 | 63 | if t < 1/6.0 then p + (q - p) * 6 * t 64 | elsif t < 0.5 then q 65 | elsif t < 2/3.0 then p + (q - p) * (2/3.0 - t) * 6 66 | else p 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/from_hex_string_values.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | class FromHexStringValues < Base 4 | # @param format [Symbol] color format 5 | # @param r [String] red value 6 | # @param g [String] green value 7 | # @param b [String] blue value 8 | # @param a [String] alpha value 9 | def initialize(format, r, g, b, a = 'ff') 10 | @format = format || :hex 11 | @r, @g, @b, @a = r, g, b, a 12 | end 13 | 14 | # Generates a {ColorModes::Rgb}. 15 | # @return [ColorModes::Rgb] 16 | def generate 17 | r, g, b = [@r, @g, @b].map { |n| n.to_i(16) } 18 | a = @a.to_i(16) / 255.0 19 | [ColorModes::Rgb.new(r, g, b, a), @format] 20 | end 21 | 22 | class << self 23 | # Generates a {ColorModes::Rgb} from 3-character hexadecimal. 24 | # @return [ColorModes::Rgb] 25 | # 26 | # @param format [Symbol] color format 27 | # @param r [String] red value 28 | # @param g [String] green value 29 | # @param b [String] blue value 30 | def from_hex3(format, r, g, b) 31 | new(format || :hex3, r * 2, g * 2, b * 2) 32 | end 33 | 34 | # Generates a {ColorModes::Rgb} from 6-character hexadecimal. 35 | # @return [ColorModes::Rgb] 36 | # 37 | # @param format [Symbol] color format 38 | # @param r [String] red value 39 | # @param g [String] green value 40 | # @param b [String] blue value 41 | def from_hex6(format, r, g, b) 42 | new(format, r, g, b) 43 | end 44 | 45 | # Generates a {ColorModes::Rgb} from 8-character hexadecimal. 46 | # @return [ColorModes::Rgb] 47 | # 48 | # @param format [Symbol] color format 49 | # @param r [String] red value 50 | # @param g [String] green value 51 | # @param b [String] blue value 52 | # @param a [String] alpha value 53 | def from_hex8(format, a, r, g, b) 54 | new(format || :hex8, r, g, b, a) 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/chroma/palette_builder.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | # Class internally used to build custom palettes from {Chroma.define_palette}. 3 | class PaletteBuilder 4 | # Wrapper to instantiate a new instance of {PaletteBuilder} and call its 5 | # {PaletteBuilder#build} method. 6 | # 7 | # @param block [Proc] the palette definition block 8 | # @return [PaletteBuilder::PaletteEvaluator] lazy palette generator 9 | def self.build(&block) 10 | new(&block).build 11 | end 12 | 13 | # @param block [Proc] the palette definition block 14 | def initialize(&block) 15 | @block = block 16 | end 17 | 18 | # Build the custom palette 19 | # @return [PaletteBuilder::PaletteEvaluator] lazy palette generator 20 | def build 21 | dsl = PaletteBuilderDsl.new 22 | dsl.instance_eval(&@block) 23 | dsl.evaluator 24 | end 25 | 26 | private 27 | 28 | # Internal class for delaying evaluating a color to generate a 29 | # final palette 30 | class PaletteEvaluator 31 | def initialize 32 | @conversions = [] 33 | end 34 | 35 | def <<(conversion) 36 | @conversions << conversion 37 | end 38 | 39 | def evaluate(color) 40 | @conversions.map do |color_calls| 41 | color_calls.evaluate(color) 42 | end.unshift(color) 43 | end 44 | end 45 | 46 | # Internal class for palette building DSL syntax. 47 | class PaletteBuilderDsl 48 | attr_reader :evaluator 49 | 50 | def initialize 51 | @evaluator = PaletteEvaluator.new 52 | end 53 | 54 | def method_missing(name, *args) 55 | ColorCalls.new(name, args).tap do |color_calls| 56 | @evaluator << color_calls 57 | end 58 | end 59 | 60 | # Internal class to represent color modification calls in the palette 61 | # builder DSL definition syntax. 62 | class ColorCalls 63 | attr_reader :name, :args 64 | 65 | def initialize(name, args) 66 | @calls = [[name, args]] 67 | end 68 | 69 | def evaluate(color) 70 | @calls.reduce(color) do |c, (name, args)| 71 | c.send(name, *args) 72 | end 73 | end 74 | 75 | def method_missing(name, *args) 76 | @calls << [name, args] 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/color/modifiers_spec.rb: -------------------------------------------------------------------------------- 1 | describe Chroma::Color do 2 | let(:red) { 'red'.paint } 3 | let(:yellow) { 'yellow'.paint } 4 | 5 | describe '#lighten' do 6 | context 'with default amount' do 7 | it 'generates the correct color' do 8 | expect(red.lighten).to eq '#ff3333'.paint 9 | end 10 | end 11 | 12 | context 'with supplied amount' do 13 | it 'generates the correct color' do 14 | expect(red.lighten(20)).to eq '#ff6666'.paint 15 | end 16 | end 17 | end 18 | 19 | describe '#brighten' do 20 | context 'with default amount' do 21 | it 'generates the correct color' do 22 | expect(red.brighten).to eq '#ff1a1a'.paint 23 | end 24 | end 25 | 26 | context 'with supplied amount' do 27 | it 'generates the correct color' do 28 | expect(red.brighten(20)).to eq '#ff3333'.paint 29 | end 30 | end 31 | end 32 | 33 | describe '#darken' do 34 | context 'with default amount' do 35 | it 'generates the correct color' do 36 | expect(red.darken).to eq '#cc0000'.paint 37 | end 38 | end 39 | 40 | context 'with supplied amount' do 41 | it 'generates the correct color' do 42 | expect(red.darken(20)).to eq '#990000'.paint 43 | end 44 | end 45 | end 46 | 47 | describe '#desaturate' do 48 | context 'with default amount' do 49 | it 'generates the correct color' do 50 | expect(red.desaturate).to eq '#f20d0d'.paint 51 | end 52 | end 53 | 54 | context 'with supplied amount' do 55 | it 'generates the correct color' do 56 | expect(red.desaturate(20)).to eq '#e61919'.paint 57 | end 58 | end 59 | end 60 | 61 | describe '#saturate' do 62 | context 'with default amount' do 63 | it 'generates the correct color' do 64 | expect('#123'.paint.saturate).to eq '#0e2236'.paint 65 | end 66 | end 67 | 68 | context 'with supplied amount' do 69 | it 'generates the correct color' do 70 | expect('#123'.paint.saturate(20)).to eq '#0a223a'.paint 71 | end 72 | end 73 | end 74 | 75 | describe '#grayscale' do 76 | it 'generates the correct color' do 77 | expect(red.grayscale).to eq 'gray'.paint 78 | expect('green'.paint.grayscale).to eq '#404040'.paint 79 | end 80 | end 81 | 82 | describe '#opacity' do 83 | it 'sets color opacity to supplied amount' do 84 | green_a = 'rgba(0, 128, 0, 0.5)' 85 | expect(green_a.paint.opacity(1)).to eq 'rgba(0, 128, 0, 1)'.paint 86 | expect('green'.paint.opacity(0)).to eq 'rgba(0, 128, 0, 0)'.paint 87 | expect(red.opacity(0.3)).to eq 'rgba(100%, 0%, 0%, 0.3)'.paint 88 | end 89 | end 90 | 91 | describe '#spin' do 92 | it 'generates the correct color' do 93 | expect(red.spin(60)).to eq yellow 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/chroma/color.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | # The main class to represent colors. 3 | class Color 4 | include Attributes 5 | include Serializers 6 | include Modifiers 7 | include Helpers::Bounders 8 | 9 | # @param input [String, ColorModes::Rgb, ColorModes::Hsl, ColorModes::Hsv] 10 | # @param format [Symbol] the color mode format 11 | def initialize(input, format = nil) 12 | @input = input 13 | @rgb, gen_format = generate_rgb_and_format(input) 14 | @format = format || gen_format 15 | end 16 | 17 | # Returns self. Useful for ducktyping situations with {String#paint}. 18 | # 19 | # @example 20 | # red = 'red'.paint 21 | # 22 | # red.paint #=> red 23 | # red.paint.equal? red #=> true 24 | # 25 | # @return [self] 26 | def paint 27 | self 28 | end 29 | 30 | # Returns true if `self` is equal to `other` and they're both instances of 31 | # {Color}. 32 | # 33 | # @example 34 | # red = 'red'.paint 35 | # blue = 'blue'.paint 36 | # 37 | # red.eql? red #=> true 38 | # red.eql? blue #=> false 39 | # red.eql? '#f00'.paint #=> true 40 | # 41 | # @param other [Color] 42 | # @return [true, false] 43 | def eql?(other) 44 | self.class == other.class && self == other 45 | end 46 | 47 | # Returns true if both are equal in value. 48 | # 49 | # @example 50 | # red = 'red'.paint 51 | # blue = 'blue'.paint 52 | # 53 | # red == red #=> true 54 | # red == blue #=> false 55 | # red == '#f00'.paint #=> true 56 | # 57 | # @param other [Color] 58 | # @return [true, false] 59 | def ==(other) 60 | to_hex == other.to_hex 61 | end 62 | 63 | # Returns the complementary color. 64 | # 65 | # @example 66 | # 'red'.paint.complement #=> cyan 67 | # 68 | # @return [Color] the complementary color 69 | def complement 70 | hsl = self.hsl 71 | hsl.h = (hsl.h + 180) % 360 72 | self.class.new(hsl, @format) 73 | end 74 | 75 | # Returns an instance of {Harmonies} from which to call a palette method. 76 | # 77 | # @example 78 | # 'red'.paint.palette #=> # 79 | # 80 | # @return [Harmonies] 81 | def palette 82 | Harmonies.new(self) 83 | end 84 | 85 | # Defines a custom palette and immediately returns it. Uses a DSL inside 86 | # `block` that mirrors the methods in {Color::Modifiers}. 87 | # 88 | # @example 89 | # 'red'.paint.custom_palette do 90 | # spin 60 91 | # spin 180 92 | # end 93 | # #=> [red, yellow, cyan] 94 | # 95 | # @param block [Proc] the palette definition block 96 | # @return [Array] palette array of colors 97 | def custom_palette(&block) 98 | PaletteBuilder.build(&block).evaluate(self) 99 | end 100 | 101 | private 102 | 103 | def to_2char_hex(n) 104 | n.round.to_s(16).rjust(2, '0') 105 | end 106 | 107 | def generate_rgb_and_format(input) 108 | RgbGenerator.generate_rgb_and_format(input) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/chroma/rgb_generator/from_string.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | module RgbGenerator 3 | class FromString < Base 4 | # Returns the regex matchers and rgb generation classes for various 5 | # string color formats. 6 | # 7 | # @api private 8 | # @return [Hash] 9 | def self.matchers 10 | @matchers ||= begin 11 | # TinyColor.js matchers 12 | css_int = '[-\\+]?\\d+%?' 13 | css_num = '[-\\+]?\\d*\\.\\d+%?' 14 | css_unit = "(?:#{css_num})|(?:#{css_int})" 15 | permissive_prefix = '[\\s|\\(]+(' 16 | permissive_delim = ')[,|\\s]+(' 17 | permissive_suffix = ')\\s*\\)?' 18 | permissive_match3 = "#{permissive_prefix}#{[css_unit] * 3 * permissive_delim}#{permissive_suffix}" 19 | permissive_match4 = "#{permissive_prefix}#{[css_unit] * 4 * permissive_delim}#{permissive_suffix}" 20 | hex_match = '[0-9a-fA-F]' 21 | 22 | { 23 | rgb: { regex: /rgb#{permissive_match3}/, class_name: :FromRgbValues }, 24 | rgba: { regex: /rgba#{permissive_match4}/, class_name: :FromRgbValues }, 25 | hsl: { regex: /hsl#{permissive_match3}/, class_name: :FromHslValues }, 26 | hsla: { regex: /hsla#{permissive_match4}/, class_name: :FromHslValues }, 27 | hsv: { regex: /hsv#{permissive_match3}/, class_name: :FromHsvValues }, 28 | hsva: { regex: /hsva#{permissive_match4}/, class_name: :FromHsvValues }, 29 | hex3: { regex: /^#?#{"(#{hex_match}{1})" * 3}$/, class_name: :FromHexStringValues, builder: :from_hex3 }, 30 | hex6: { regex: /^#?#{"(#{hex_match}{2})" * 3}$/, class_name: :FromHexStringValues, builder: :from_hex6 }, 31 | hex8: { regex: /^#?#{"(#{hex_match}{2})" * 4}$/, class_name: :FromHexStringValues, builder: :from_hex8 } 32 | }.freeze 33 | end 34 | end 35 | 36 | # @param format [Symbol] unused 37 | # @param input [String] input to parse 38 | def initialize(format, input) 39 | @input = normalize_input(input) 40 | end 41 | 42 | # Generates a {ColorModes::Rgb}. 43 | # @return [ColorModes::Rgb] 44 | def generate 45 | get_generator.generate 46 | end 47 | 48 | private 49 | 50 | def get_generator 51 | if color = Chroma.hex_from_name(@input) 52 | format = :name 53 | elsif @input == 'transparent' 54 | return FromRgbValues.new(:name, 0, 0, 0, 0) 55 | else 56 | format = nil 57 | color = @input 58 | end 59 | 60 | match = nil 61 | 62 | _, hash = matchers.find do |_, h| 63 | !(match = h[:regex].match(color)).nil? 64 | end 65 | 66 | if match.nil? 67 | raise Errors::UnrecognizedColor, "Unrecognized color `#{color}'" 68 | end 69 | 70 | build_generator(match[1..-1], hash[:class_name], hash[:builder], format) 71 | end 72 | 73 | def build_generator(args, class_name, builder, format) 74 | builder ||= :new 75 | klass = RgbGenerator.const_get(class_name) 76 | klass.__send__(builder, *([format] + args)) 77 | end 78 | 79 | def normalize_input(input) 80 | input.clone.tap do |str| 81 | str.strip! 82 | str.downcase! 83 | end 84 | end 85 | 86 | def matchers 87 | self.class.matchers 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/support/named_colors.yml: -------------------------------------------------------------------------------- 1 | aliceblue: 'f0f8ff' 2 | antiquewhite: 'faebd7' 3 | aqua: '0ff' 4 | aquamarine: '7fffd4' 5 | azure: 'f0ffff' 6 | beige: 'f5f5dc' 7 | bisque: 'ffe4c4' 8 | black: '000' 9 | blanchedalmond: 'ffebcd' 10 | blue: '00f' 11 | blueviolet: '8a2be2' 12 | brown: 'a52a2a' 13 | burlywood: 'deb887' 14 | burntsienna: 'ea7e5d' 15 | cadetblue: '5f9ea0' 16 | chartreuse: '7fff00' 17 | chocolate: 'd2691e' 18 | coral: 'ff7f50' 19 | cornflowerblue: '6495ed' 20 | cornsilk: 'fff8dc' 21 | crimson: 'dc143c' 22 | cyan: '0ff' 23 | darkblue: '00008b' 24 | darkcyan: '008b8b' 25 | darkgoldenrod: 'b8860b' 26 | darkgray: 'a9a9a9' 27 | darkgreen: '006400' 28 | darkgrey: 'a9a9a9' 29 | darkkhaki: 'bdb76b' 30 | darkmagenta: '8b008b' 31 | darkolivegreen: '556b2f' 32 | darkorange: 'ff8c00' 33 | darkorchid: '9932cc' 34 | darkred: '8b0000' 35 | darksalmon: 'e9967a' 36 | darkseagreen: '8fbc8f' 37 | darkslateblue: '483d8b' 38 | darkslategray: '2f4f4f' 39 | darkslategrey: '2f4f4f' 40 | darkturquoise: '00ced1' 41 | darkviolet: '9400d3' 42 | deeppink: 'ff1493' 43 | deepskyblue: '00bfff' 44 | dimgray: '696969' 45 | dimgrey: '696969' 46 | dodgerblue: '1e90ff' 47 | firebrick: 'b22222' 48 | floralwhite: 'fffaf0' 49 | forestgreen: '228b22' 50 | fuchsia: 'f0f' 51 | gainsboro: 'dcdcdc' 52 | ghostwhite: 'f8f8ff' 53 | gold: 'ffd700' 54 | goldenrod: 'daa520' 55 | gray: '808080' 56 | green: '008000' 57 | greenyellow: 'adff2f' 58 | grey: '808080' 59 | honeydew: 'f0fff0' 60 | hotpink: 'ff69b4' 61 | indianred: 'cd5c5c' 62 | indigo: '4b0082' 63 | ivory: 'fffff0' 64 | khaki: 'f0e68c' 65 | lavender: 'e6e6fa' 66 | lavenderblush: 'fff0f5' 67 | lawngreen: '7cfc00' 68 | lemonchiffon: 'fffacd' 69 | lightblue: 'add8e6' 70 | lightcoral: 'f08080' 71 | lightcyan: 'e0ffff' 72 | lightgoldenrodyellow: 'fafad2' 73 | lightgray: 'd3d3d3' 74 | lightgreen: '90ee90' 75 | lightgrey: 'd3d3d3' 76 | lightpink: 'ffb6c1' 77 | lightsalmon: 'ffa07a' 78 | lightseagreen: '20b2aa' 79 | lightskyblue: '87cefa' 80 | lightslategray: '789' 81 | lightslategrey: '789' 82 | lightsteelblue: 'b0c4de' 83 | lightyellow: 'ffffe0' 84 | lime: '0f0' 85 | limegreen: '32cd32' 86 | linen: 'faf0e6' 87 | magenta: 'f0f' 88 | maroon: '800000' 89 | mediumaquamarine: '66cdaa' 90 | mediumblue: '0000cd' 91 | mediumorchid: 'ba55d3' 92 | mediumpurple: '9370db' 93 | mediumseagreen: '3cb371' 94 | mediumslateblue: '7b68ee' 95 | mediumspringgreen: '00fa9a' 96 | mediumturquoise: '48d1cc' 97 | mediumvioletred: 'c71585' 98 | midnightblue: '191970' 99 | mintcream: 'f5fffa' 100 | mistyrose: 'ffe4e1' 101 | moccasin: 'ffe4b5' 102 | navajowhite: 'ffdead' 103 | navy: '000080' 104 | oldlace: 'fdf5e6' 105 | olive: '808000' 106 | olivedrab: '6b8e23' 107 | orange: 'ffa500' 108 | orangered: 'ff4500' 109 | orchid: 'da70d6' 110 | palegoldenrod: 'eee8aa' 111 | palegreen: '98fb98' 112 | paleturquoise: 'afeeee' 113 | palevioletred: 'db7093' 114 | papayawhip: 'ffefd5' 115 | peachpuff: 'ffdab9' 116 | peru: 'cd853f' 117 | pink: 'ffc0cb' 118 | plum: 'dda0dd' 119 | powderblue: 'b0e0e6' 120 | purple: '800080' 121 | rebeccapurple: '663399' 122 | red: 'f00' 123 | rosybrown: 'bc8f8f' 124 | royalblue: '4169e1' 125 | saddlebrown: '8b4513' 126 | salmon: 'fa8072' 127 | sandybrown: 'f4a460' 128 | seagreen: '2e8b57' 129 | seashell: 'fff5ee' 130 | sienna: 'a0522d' 131 | silver: 'c0c0c0' 132 | skyblue: '87ceeb' 133 | slateblue: '6a5acd' 134 | slategray: '708090' 135 | slategrey: '708090' 136 | snow: 'fffafa' 137 | springgreen: '00ff7f' 138 | steelblue: '4682b4' 139 | tan: 'd2b48c' 140 | teal: '008080' 141 | thistle: 'd8bfd8' 142 | tomato: 'ff6347' 143 | turquoise: '40e0d0' 144 | violet: 'ee82ee' 145 | wheat: 'f5deb3' 146 | white: 'fff' 147 | whitesmoke: 'f5f5f5' 148 | yellow: 'ff0' 149 | yellowgreen: '9acd32' 150 | -------------------------------------------------------------------------------- /lib/chroma/color/modifiers.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | class Color 3 | # Methods that return a new modified {Color}. 4 | module Modifiers 5 | # Lightens the color by the given `amount`. 6 | # 7 | # @example 8 | # 'red'.paint.lighten #=> #ff3333 9 | # 'red'.paint.lighten(20) #=> #ff6666 10 | # 11 | # @param amount [Fixnum] 12 | # @return [Color] 13 | def lighten(amount = 10) 14 | hsl = self.hsl 15 | hsl.l = clamp01(hsl.l + amount / 100.0) 16 | self.class.new(hsl, @format) 17 | end 18 | 19 | # Brightens the color by the given `amount`. 20 | # 21 | # @example 22 | # 'red'.paint.brighten #=> #ff1a1a 23 | # 'red'.paint.brighten(20) #=> #ff3333 24 | # 25 | # @param amount [Fixnum] 26 | # @return [Color] 27 | def brighten(amount = 10) 28 | # Don't include alpha 29 | rgb = @rgb.to_a[0..2] 30 | amount = (255 * (-amount / 100.0)).round 31 | 32 | rgb.map! do |n| 33 | [0, [255, n - amount].min].max 34 | end 35 | 36 | self.class.new(ColorModes::Rgb.new(*rgb), @format) 37 | end 38 | 39 | # Darkens the color by the given `amount`. 40 | # 41 | # @example 42 | # 'red'.paint.darken #=> #cc0000 43 | # 'red'.paint.darken(20) #=> #990000 44 | # 45 | # @param amount [Fixnum] 46 | # @return [Color] 47 | def darken(amount = 10) 48 | hsl = self.hsl 49 | hsl.l = clamp01(hsl.l - amount / 100.0) 50 | self.class.new(hsl, @format) 51 | end 52 | 53 | # Desaturates the color by the given `amount`. 54 | # 55 | # @example 56 | # 'red'.paint.desaturate #=> #f20d0d 57 | # 'red'.paint.desaturate(20) #=> #e61919 58 | # 59 | # @param amount [Fixnum] 60 | # @return [Color] 61 | def desaturate(amount = 10) 62 | hsl = self.hsl 63 | hsl.s = clamp01(hsl.s - amount / 100.0) 64 | self.class.new(hsl, @format) 65 | end 66 | 67 | # Saturates the color by the given `amount`. 68 | # 69 | # @example 70 | # '#123'.paint.saturate #=> #0e2236 71 | # '#123'.paint.saturate(20) #=> #0a223a 72 | # 73 | # @param amount [Fixnum] 74 | # @return [Color] 75 | def saturate(amount = 10) 76 | hsl = self.hsl 77 | hsl.s = clamp01(hsl.s + amount / 100.0) 78 | self.class.new(hsl, @format) 79 | end 80 | 81 | # Converts the color to grayscale. 82 | # 83 | # @example 84 | # 'green'.paint.grayscale #=> #404040 85 | # 86 | # @return [Color] 87 | def grayscale 88 | desaturate(100) 89 | end 90 | 91 | alias_method :greyscale, :grayscale 92 | 93 | # Sets color opacity to the given 'amount'. 94 | # 95 | # @example 96 | # 'red'.paint.opacity(0.5) #=> #ff0000 97 | # 'red'.paint.opacity(0.5).to_rgb #=> 'rgba(255, 0, 0, 0.5)' 98 | # 99 | # @param amount [Fixnum] 100 | # @return [Color] 101 | def opacity(amount) 102 | rgb = @rgb.to_a[0..2] + [amount] 103 | self.class.new(ColorModes::Rgb.new(*rgb), @format) 104 | end 105 | 106 | # Spins around the hue color wheel by `amount` in degrees. 107 | # 108 | # @example 109 | # 'red'.paint.spin(30) #=> #ff8000 110 | # 'red'.paint.spin(60) #=> yellow 111 | # 'red'.paint.spin(90) #=> #80ff00 112 | # 113 | # @param amount [Fixnum] 114 | # @return [Color] 115 | def spin(amount) 116 | hsl = self.hsl 117 | hue = (hsl.h.round + amount) % 360 118 | hsl.h = hue < 0 ? 360 + hue : hue 119 | self.class.new(hsl, @format) 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.2.0 2 | 3 | ### NEW `.opacity` Method 4 | 5 | Added an `.opacity` method to set the opacity of a color. (credit [@matildasmeds](https://github.com/matildasmeds)) 6 | 7 | ```ruby 8 | 'red'.paint.opacity(0.3).to_rgb #=> 'rgba(255, 0, 0, 0.3)' 9 | ``` 10 | 11 | --- 12 | 13 | ## v0.1.0 - 2016-05-26 14 | 15 | ### Dynamic Custom Palettes 16 | 17 | You can generate custom palettes on the fly without predefining them now. The 18 | old way of defining them with a name via `Chroma.define_palette` still works 19 | too. 20 | 21 | ```ruby 22 | # New dynamic way 23 | 'red'.paint.custom_palette do 24 | spin 60 25 | spin 180 26 | end 27 | 28 | #=> [red, yellow, cyan] 29 | ``` 30 | 31 | ## v0.0.1 - 2015-01-14 32 | 33 | **Method Changes:** 34 | 35 | * Renamed options for analogous palette method. 36 | * `:results` -> `:size` 37 | * `:slices` -> `:slice_by` 38 | * Renamed option for monochromatic palette method. 39 | * `:results` -> `:size` 40 | 41 | **Miscellaneous Changes:** 42 | 43 | * Add remaining specs for public API. 44 | * Add "transparent" as color name for `Chroma.paint`. 45 | * Minor API doc example fixes. 46 | * Add public API usage examples to README. 47 | 48 | ## [v0.0.1.alpha.3] - 2015-01-13 49 | 50 | **Bug Fixes:** 51 | 52 | * Fix bug where `Color#complement` didn't return a color of the same format. 53 | * Fix bug where palettes did not have the same format as the seed color. (#16) 54 | * Fix bug where the helper method `bound01` was not always producing accurate 55 | results for percentages due to integer division. 56 | * Fix bug where a `Color` created from an rgba string 57 | (e.g. `'rgba(255, 0, 0, 0.5).paint'`) was not serializing to an rgba string 58 | from `to_s`. (#17) 59 | 60 | **Method Changes:** 61 | 62 | * Add `Color#format` method to return `@format` instance variable. 63 | * Change arguments for `analogous` and `monochromatic` to option arguments. 64 | * Add ability to output palette as an array of color format strings via the 65 | `:as` option. (#10) 66 | * On `Color` rename `greyscale` to `grayscale` and alias `greyscale` back 67 | to `grayscale`. 68 | 69 | **Miscellaneous Changes:** 70 | 71 | * Introduced custom errors and replaced `raise` calls with them. 72 | * Added API doc headers. (#4) 73 | 74 | ## [v0.0.1.alpha.2] - 2015-01-13 75 | 76 | **Bug Fixes:** 77 | 78 | * Fixed bug with number of arguments passed to generator classes in RgbGenerator. (#1) 79 | * Make `FromHexStringValues.from_hex8` take alpha as second parameter instead of last. 80 | Fixes incorrect color generation from hex8. (#6) 81 | * Ensure that string serialization rounds the alpha value where applicable. (#7) 82 | * Fix bug where `to_s` and `inspect` would return `` instead of using 83 | hex if the format was `:name` and the named color could not be found. (#2) 84 | * Fix bug where `Color` equality wasn't implemented. (#12) 85 | * Fix bug where passing in an instance of `Hsl` or `Hsv` to `Color.new` caused 86 | their values to get changed. (#11) 87 | * Fix bug with `Color` equality being off due to floating point math. (#13) 88 | * Fix bug where `Color` instances generated from hsla and hsva strings had the 89 | wrong alpha value. (#15) 90 | 91 | **Method Changes:** 92 | 93 | * Add optional `hex_for_unknown` parameter to `Color::Serializers#to_name`. 94 | If true, it allows `to_name` to default to hex string if name is not found 95 | instead of returning `''`. (#2) 96 | * Add missing conversion methods to converters (a12244f0d81c9480490cfb8a472993f54dd9fbd2) 97 | * Add equality (`eql?` and `==`) methods to `Color` class and `ColorModes` 98 | classes. (#12, #13) 99 | * Add `Chroma.define_palette` for defining custom palettes. (#9) 100 | * Add `Color#paint` method for returning itself. (#14) 101 | * Tweak `Color` serialization method names. Switched to this naming primarily 102 | to drop the `*_s` on the string serialization methods. 103 | * `to_hsv` -> `hsv` 104 | * `to_hsv_s` -> `to_hsv` 105 | * `to_hsl` -> `hsl` 106 | * `to_hsl_s` -> `to_hsl` 107 | * `to_hex` -> `to_basic_hex` (made private) 108 | * `to_hex_s` -> `to_hex` 109 | * `to_hex8` -> `to_basic_hex8` (made private) 110 | * `to_hex8_s` -> `to_hex8` 111 | * `to_rgb` -> `rgb` (moved attr_reader to serializers and made public) 112 | * `to_rgb_s` -> `to_rgb` 113 | * Removed `to_name_s` alias 114 | 115 | ## [v0.0.1.alpha.1] - 2015-01-11 116 | 117 | * Initial release 118 | -------------------------------------------------------------------------------- /lib/chroma.rb: -------------------------------------------------------------------------------- 1 | # General 2 | require 'chroma/version' 3 | require 'chroma/errors' 4 | require 'yaml' 5 | 6 | # Modules 7 | require 'chroma/helpers/bounders' 8 | 9 | # Color 10 | require 'chroma/color/attributes' 11 | require 'chroma/color/serializers' 12 | require 'chroma/color/modifiers' 13 | require 'chroma/color' 14 | require 'chroma/color_modes' 15 | 16 | # Palettes 17 | require 'chroma/harmonies' 18 | require 'chroma/palette_builder' 19 | 20 | # RGB Generators 21 | require 'chroma/rgb_generator' 22 | require 'chroma/rgb_generator/base' 23 | require 'chroma/rgb_generator/from_string' 24 | require 'chroma/rgb_generator/from_rgb_values' 25 | require 'chroma/rgb_generator/from_rgb' 26 | require 'chroma/rgb_generator/from_hsl_values' 27 | require 'chroma/rgb_generator/from_hsl' 28 | require 'chroma/rgb_generator/from_hsv_values' 29 | require 'chroma/rgb_generator/from_hsv' 30 | require 'chroma/rgb_generator/from_hex_string_values' 31 | 32 | # Converters 33 | require 'chroma/converters/base' 34 | require 'chroma/converters/rgb_converter' 35 | require 'chroma/converters/hsl_converter' 36 | require 'chroma/converters/hsv_converter' 37 | 38 | # Extensions 39 | require 'chroma/extensions/string' 40 | 41 | # The main module. 42 | module Chroma 43 | class << self 44 | # Returns a new instance of color. Supports hexadecimal, rgb, rgba, hsl, 45 | # hsla, hsv, hsva, and named color formats. 46 | # 47 | # @api public 48 | # 49 | # @example 50 | # Chroma.paint('red') 51 | # Chroma.paint('#f00') 52 | # Chroma.paint('#ff0000') 53 | # Chroma.paint('rgb(255, 0, 0)') 54 | # Chroma.paint('hsl(0, 100%, 50%)') 55 | # Chroma.paint('hsv(0, 100%, 100%)') 56 | # 57 | # @param input [String] the color 58 | # @return [Color] an instance of {Color} 59 | def paint(input) 60 | Color.new(input) 61 | end 62 | 63 | # Returns the hexadecimal string representation of a named color and nil 64 | # if no match is found. Favors 3-character hexadecimal if possible. 65 | # 66 | # @example 67 | # Chroma.hex_from_name('red') #=> 'f00' 68 | # Chroma.hex_from_name('aliceblue') #=> 'f0f8ff' 69 | # Chroma.hex_from_name('foo') #=> nil 70 | # 71 | # @param name [String] the color name 72 | # @return [String, nil] the color as a string hexadecimal or nil 73 | def hex_from_name(name) 74 | named_colors_map[name] 75 | end 76 | 77 | # Returns the color name of a hexadecimal color if available and nil if no 78 | # match is found. Requires 3-character hexadecimal input for applicable 79 | # colors. 80 | # 81 | # @example 82 | # Chroma.name_from_hex('f00') #=> 'red' 83 | # Chroma.name_from_hex('f0f8ff') #=> 'aliceblue' 84 | # Chroma.name_from_hex('123123') #=> nil 85 | # 86 | # @param hex [String] the hexadecimal color 87 | # @return [String, nil] the color name or nil 88 | def name_from_hex(hex) 89 | hex_named_colors_map[hex] 90 | end 91 | 92 | # Defines a custom palette for use by {Color#palette}. Uses a DSL inside 93 | # `block` that mirrors the methods in {Color::Modifiers}. 94 | # 95 | # @example 96 | # 'red'.paint.palette.respond_to? :my_palette #=> false 97 | # 98 | # Chroma.define_palette :my_palette do 99 | # spin 60 100 | # spin 120 101 | # spin 240 102 | # end 103 | # 104 | # 'red'.paint.palette.respond_to? :my_palette #=> true 105 | # 106 | # @param name [Symbol, String] the name of the custom palette 107 | # @param block [Proc] the palette definition block 108 | # @raise [Errors::PaletteDefinedError] if the palette is already defined 109 | # @return [Symbol, String] the name of the custom palette 110 | def define_palette(name, &block) 111 | if Harmonies.method_defined? name 112 | raise Errors::PaletteDefinedError, "Palette `#{name}' already exists" 113 | end 114 | 115 | palette_evaluator = PaletteBuilder.build(&block) 116 | 117 | Harmonies.send(:define_method, name) do 118 | palette_evaluator.evaluate(@color) 119 | end 120 | end 121 | 122 | private 123 | 124 | def hex_named_colors_map 125 | @hex_named_colors_map ||= named_colors_map.invert 126 | end 127 | 128 | def named_colors_map 129 | @named_colors ||= YAML.load_file(File.expand_path('../support/named_colors.yml', __FILE__)) 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'chroma' 2 | 3 | # This file was generated by the `rspec --init` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 6 | # file to always be loaded, without a need to explicitly require it in any files. 7 | # 8 | # Given that it is always loaded, you are encouraged to keep this file as 9 | # light-weight as possible. Requiring heavyweight dependencies from this file 10 | # will add to the boot time of your test suite on EVERY test run, even for an 11 | # individual file that may not need all of that loaded. Instead, consider making 12 | # a separate helper file that requires the additional dependencies and performs 13 | # the additional setup, and require it from the spec files that actually need it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | RSpec.configure do |config| 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | # The settings below are suggested to provide a good initial experience 44 | # with RSpec, but feel free to customize to your heart's content. 45 | =begin 46 | # These two settings work together to allow you to limit a spec run 47 | # to individual examples or groups you care about by tagging them with 48 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 49 | # get run. 50 | config.filter_run :focus 51 | config.run_all_when_everything_filtered = true 52 | 53 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 54 | # For more details, see: 55 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 56 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 57 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 58 | config.disable_monkey_patching! 59 | 60 | # This setting enables warnings. It's recommended, but in some cases may 61 | # be too noisy due to issues in dependencies. 62 | config.warnings = true 63 | 64 | # Many RSpec users commonly either run the entire suite or an individual 65 | # file, and it's useful to allow more verbose output when running an 66 | # individual spec file. 67 | if config.files_to_run.one? 68 | # Use the documentation formatter for detailed output, 69 | # unless a formatter has already been configured 70 | # (e.g. via a command-line flag). 71 | config.default_formatter = 'doc' 72 | end 73 | 74 | # Print the 10 slowest examples and example groups at the 75 | # end of the spec run, to help surface which specs are running 76 | # particularly slow. 77 | config.profile_examples = 10 78 | 79 | # Run specs in random order to surface order dependencies. If you find an 80 | # order dependency and want to debug it, you can fix the order by providing 81 | # the seed, which is printed after each run. 82 | # --seed 1234 83 | config.order = :random 84 | 85 | # Seed global randomization in this process using the `--seed` CLI option. 86 | # Setting this allows you to use `--seed` to deterministically reproduce 87 | # test failures related to randomization by passing the same `--seed` value 88 | # as the one that triggered the failure. 89 | Kernel.srand config.seed 90 | =end 91 | end 92 | 93 | require_relative 'custom_matchers' 94 | -------------------------------------------------------------------------------- /spec/color/serializers_spec.rb: -------------------------------------------------------------------------------- 1 | describe Chroma::Color do 2 | context 'with serializers' do 3 | let(:green) { 'green'.paint } 4 | let(:blue) { 'rgba(0, 0, 255, 0.5)'.paint } 5 | let(:transparent) { 'rgba(0, 0, 0, 0)'.paint } 6 | 7 | describe '#to_hsv' do 8 | it 'returns the hsv string' do 9 | expect(green.to_hsv).to eq('hsv(120, 100%, 50%)') 10 | expect(blue.to_hsv).to eq('hsva(240, 100%, 100%, 0.5)') 11 | end 12 | end 13 | 14 | describe '#to_hsl' do 15 | it 'returns the hsl string' do 16 | expect(green.to_hsl).to eq('hsl(120, 100%, 25%)') 17 | expect(blue.to_hsl).to eq('hsla(240, 100%, 50%, 0.5)') 18 | end 19 | end 20 | 21 | describe '#to_hex' do 22 | context 'with allow_3 set to false' do 23 | it 'returns the hex string' do 24 | expect(green.to_hex).to eq('#008000') 25 | expect(blue.to_hex).to eq('#0000ff') 26 | end 27 | end 28 | 29 | context 'with allow_3 set to true' do 30 | it 'returns the hex string' do 31 | expect(green.to_hex(true)).to eq('#008000') 32 | expect(blue.to_hex(true)).to eq('#00f') 33 | end 34 | end 35 | end 36 | 37 | describe '#to_hex8' do 38 | it 'returns the hex8 string' do 39 | expect(green.to_hex8).to eq('#ff008000') 40 | expect(blue.to_hex8).to eq('#800000ff') 41 | end 42 | end 43 | 44 | describe '#to_rgb' do 45 | it 'returns the rgb string' do 46 | expect(green.to_rgb).to eq('rgb(0, 128, 0)') 47 | expect(blue.to_rgb).to eq('rgba(0, 0, 255, 0.5)') 48 | end 49 | end 50 | 51 | describe '#to_name' do 52 | context 'with hex_for_unknown set to false' do 53 | context 'with known named color' do 54 | context 'when alpha = 1' do 55 | it 'returns the named color' do 56 | expect(green.to_name).to eq('green') 57 | end 58 | end 59 | 60 | context 'when alpha < 1' do 61 | it 'returns ""' do 62 | expect(blue.to_name).to eq('') 63 | end 64 | end 65 | end 66 | 67 | context 'when alpha = 0' do 68 | it 'returns "transparent"' do 69 | expect(transparent.to_name).to eq('transparent') 70 | end 71 | end 72 | 73 | context 'with unknown named color' do 74 | it 'returns ""' do 75 | expect('#123'.paint.to_name).to eq('') 76 | end 77 | end 78 | end 79 | 80 | context 'with hex_for_unknown set to true' do 81 | context 'with known named color' do 82 | context 'when alpha = 1' do 83 | it 'returns the named color' do 84 | expect(green.to_name(true)).to eq('green') 85 | end 86 | end 87 | 88 | context 'when alpha < 1' do 89 | it 'returns the hex string' do 90 | expect(blue.to_name(true)).to eq('#0000ff') 91 | end 92 | end 93 | end 94 | 95 | context 'when alpha = 0' do 96 | it 'returns "transparent"' do 97 | expect(transparent.to_name(true)).to eq('transparent') 98 | end 99 | end 100 | 101 | context 'with unknown named color' do 102 | it 'returns returns the hex string' do 103 | expect('#123'.paint.to_name(true)).to eq('#112233') 104 | end 105 | end 106 | end 107 | end 108 | 109 | describe '#to_s' do 110 | it 'returns the appropriate string according to format' do 111 | expect('#ff0000'.paint.to_s).to eq('#ff0000') 112 | expect('#f00'.paint.to_s).to eq('#f00') 113 | expect('#80ff0000'.paint.to_s).to eq('#80ff0000') 114 | expect('hsl(120, 100%, 50%)'.paint.to_s).to eq('hsl(120, 100%, 50%)') 115 | expect('hsla(120, 100%, 50%, 0.5)'.paint.to_s).to eq('hsla(120, 100%, 50%, 0.5)') 116 | expect('hsv(120, 100%, 50%)'.paint.to_s).to eq('hsv(120, 100%, 50%)') 117 | expect('hsva(120, 100%, 50%, 0.5)'.paint.to_s).to eq('hsva(120, 100%, 50%, 0.5)') 118 | expect('red'.paint.to_s).to eq('red') 119 | end 120 | end 121 | 122 | describe '#hsv' do 123 | it 'returns an hsv instance' do 124 | hsv = green.hsv 125 | 126 | expect(hsv).to be_a(Chroma::ColorModes::Hsv) 127 | expect(hsv.h).to be_within(0.01).of(120) 128 | expect(hsv.s).to be_within(0.01).of(1) 129 | expect(hsv.v).to be_within(0.01).of(0.5) 130 | expect(hsv.a).to eq(1) 131 | end 132 | end 133 | 134 | describe '#hsl' do 135 | it 'returns an hsl instance' do 136 | hsl = green.hsl 137 | 138 | expect(hsl).to be_a(Chroma::ColorModes::Hsl) 139 | expect(hsl.h).to be_within(0.01).of(120) 140 | expect(hsl.s).to be_within(0.01).of(1) 141 | expect(hsl.l).to be_within(0.01).of(0.25) 142 | expect(hsl.a).to eq(1) 143 | end 144 | end 145 | 146 | describe '#rgb' do 147 | it 'returns the underlying @rgb iv' do 148 | expect(green.rgb).to equal(green.instance_variable_get(:@rgb)) 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/chroma/harmonies.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | # Class to hold all palette methods. 3 | class Harmonies 4 | # @param color [Color] 5 | def initialize(color) 6 | @color = color 7 | end 8 | 9 | # Generate a complement palette. 10 | # 11 | # @example 12 | # 'red'.paint.palette.complement #=> [red, cyan] 13 | # 'red'.paint.palette.complement(as: :name) #=> ['red', 'cyan'] 14 | # 'red'.paint.palette.complement(as: :hex) #=> ['#ff0000', '#00ffff'] 15 | # 16 | # @param options [Hash] 17 | # @option options :as [Symbol] (nil) optional format to output colors as strings 18 | # @return [Array, Array] depending on presence of `options[:as]` 19 | def complement(options = {}) 20 | with_reformat([@color, @color.complement], options[:as]) 21 | end 22 | 23 | # Generate a triad palette. 24 | # 25 | # @example 26 | # 'red'.paint.palette.triad #=> [red, lime, blue] 27 | # 'red'.paint.palette.triad(as: :name) #=> ['red', 'lime', 'blue'] 28 | # 'red'.paint.palette.triad(as: :hex) #=> ['#ff0000', '#00ff00', '#0000ff'] 29 | # 30 | # @param options [Hash] 31 | # @option options :as [Symbol] (nil) optional format to output colors as strings 32 | # @return [Array, Array] depending on presence of `options[:as]` 33 | def triad(options = {}) 34 | hsl_map([0, 120, 240], options) 35 | end 36 | 37 | # Generate a tetrad palette. 38 | # 39 | # @example 40 | # 'red'.paint.palette.tetrad #=> [red, #80ff00, cyan, #7f00ff] 41 | # 'red'.paint.palette.tetrad(as: :name) #=> ['red', '#80ff00', 'cyan', '#7f00ff'] 42 | # 'red'.paint.palette.tetrad(as: :hex) #=> ['#ff0000', '#80ff00', '#00ffff', '#7f00ff'] 43 | # 44 | # @param options [Hash] 45 | # @option options :as [Symbol] (nil) optional format to output colors as strings 46 | # @return [Array, Array] depending on presence of `options[:as]` 47 | def tetrad(options = {}) 48 | hsl_map([0, 90, 180, 270], options) 49 | end 50 | 51 | # Generate a split complement palette. 52 | # 53 | # @example 54 | # 'red'.paint.palette.split_complement #=> [red, #ccff00, #0066ff] 55 | # 'red'.paint.palette.split_complement(as: :name) #=> ['red', '#ccff00', '#0066ff'] 56 | # 'red'.paint.palette.split_complement(as: :hex) #=> ['#ff0000', '#ccff00', '#0066ff'] 57 | # 58 | # @param options [Hash] 59 | # @option options :as [Symbol] (nil) optional format to output colors as strings 60 | # @return [Array, Array] depending on presence of `options[:as]` 61 | def split_complement(options = {}) 62 | hsl_map([0, 72, 216], options) 63 | end 64 | 65 | # Generate an analogous palette. 66 | # 67 | # @example 68 | # 'red'.paint.palette.analogous #=> [red, #ff0066, #ff0033, red, #ff3300, #ff6600] 69 | # 'red'.paint.palette.analogous(as: :hex) #=> ['#f00', '#f06', '#f03', '#f00', '#f30', '#f60'] 70 | # 'red'.paint.palette.analogous(size: 3) #=> [red, #ff001a, #ff1a00] 71 | # 'red'.paint.palette.analogous(size: 3, slice_by: 60) #=> [red, #ff000d, #ff0d00] 72 | # 73 | # @param options [Hash] 74 | # @option options :size [Symbol] (6) number of results to return 75 | # @option options :slice_by [Symbol] (30) 76 | # the angle in degrees to slice the hue circle per color 77 | # @option options :as [Symbol] (nil) optional format to output colors as strings 78 | # @return [Array, Array] depending on presence of `options[:as]` 79 | def analogous(options = {}) 80 | size = options[:size] || 6 81 | slices = options[:slice_by] || 30 82 | 83 | hsl = @color.hsl 84 | part = 360 / slices 85 | hsl.h = ((hsl.h - (part * size >> 1)) + 720) % 360 86 | 87 | palette = (size - 1).times.reduce([@color]) do |arr, n| 88 | hsl.h = (hsl.h + part) % 360 89 | arr << Color.new(hsl, @color.format) 90 | end 91 | 92 | with_reformat(palette, options[:as]) 93 | end 94 | 95 | # Generate a monochromatic palette. 96 | # 97 | # @example 98 | # 'red'.paint.palette.monochromatic #=> [red, #2a0000, #550000, maroon, #aa0000, #d40000] 99 | # 'red'.paint.palette.monochromatic(as: :hex) #=> ['#ff0000', '#2a0000', '#550000', '#800000', '#aa0000', '#d40000'] 100 | # 'red'.paint.palette.monochromatic(size: 3) #=> [red, #550000, #aa0000] 101 | # 102 | # @param options [Hash] 103 | # @option options :size [Symbol] (6) number of results to return 104 | # @option options :as [Symbol] (nil) optional format to output colors as strings 105 | # @return [Array, Array] depending on presence of `options[:as]` 106 | def monochromatic(options = {}) 107 | size = options[:size] || 6 108 | 109 | h, s, v = @color.hsv 110 | modification = 1.0 / size 111 | 112 | palette = size.times.map do 113 | Color.new(ColorModes::Hsv.new(h, s, v), @color.format).tap do 114 | v = (v + modification) % 1 115 | end 116 | end 117 | 118 | with_reformat(palette, options[:as]) 119 | end 120 | 121 | private 122 | 123 | def with_reformat(palette, as) 124 | palette.map! { |color| color.to_s(as) } unless as.nil? 125 | palette 126 | end 127 | 128 | def hsl_map(degrees, options) 129 | h, s, l = @color.hsl 130 | 131 | degrees.map! do |deg| 132 | Color.new(ColorModes::Hsl.new((h + deg) % 360, s, l), @color.format) 133 | end 134 | 135 | with_reformat(degrees, options[:as]) 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /spec/color/palette_spec.rb: -------------------------------------------------------------------------------- 1 | describe Chroma::Color, '#palette' do 2 | let(:red) { 'red'.paint } 3 | 4 | def palette(name, *args) 5 | red.palette.send(name, *args) 6 | end 7 | 8 | it 'returns an instance of Harmonies' do 9 | expect(red.palette).to be_a Chroma::Harmonies 10 | end 11 | 12 | describe '#complement' do 13 | it 'returns the color and its complement' do 14 | expect(red.palette.complement).to generate_palette %w(red cyan) 15 | end 16 | 17 | it 'keeps the same format' do 18 | expect(red.palette.complement).to all have_format :name 19 | end 20 | 21 | context 'with option :as' do 22 | it 'outputs the palette as an array of the string format' do 23 | expect(red.palette.complement(as: :hex)). 24 | to eq %w(#ff0000 #00ffff) 25 | end 26 | end 27 | end 28 | 29 | describe '#triad' do 30 | it 'returns the triad palette' do 31 | expect(red.palette.triad).to generate_palette %w(red lime blue) 32 | end 33 | 34 | it 'keeps the same format' do 35 | expect(red.palette.triad).to all have_format :name 36 | end 37 | 38 | context 'with option :as' do 39 | it 'outputs the palette as an array of the string format' do 40 | expect(red.palette.triad(as: :hex)). 41 | to eq %w(#ff0000 #00ff00 #0000ff) 42 | end 43 | end 44 | end 45 | 46 | describe '#tetrad' do 47 | it 'returns the tetrad palette' do 48 | expect(red.palette.tetrad).to generate_palette %w(red #80ff00 cyan #7f00ff) 49 | end 50 | 51 | it 'keeps the same format' do 52 | expect(red.palette.tetrad).to all have_format :name 53 | end 54 | 55 | context 'with option :as' do 56 | it 'outputs the palette as an array of the string format' do 57 | expect(red.palette.tetrad(as: :hex)). 58 | to eq %w(#ff0000 #80ff00 #00ffff #7f00ff) 59 | end 60 | end 61 | end 62 | 63 | describe '#split_complement' do 64 | it 'returns the split complement palette' do 65 | expect(red.palette.split_complement).to generate_palette %w(red #cf0 #06f) 66 | end 67 | 68 | it 'keeps the same format' do 69 | expect(red.palette.split_complement).to all have_format :name 70 | end 71 | 72 | context 'with option :as' do 73 | it 'outputs the palette as an array of the string format' do 74 | expect(red.palette.split_complement(as: :hex)). 75 | to eq %w(#ff0000 #ccff00 #0066ff) 76 | end 77 | end 78 | end 79 | 80 | describe '#analogous' do 81 | context 'with default parameters' do 82 | it 'returns the analogous palette' do 83 | expect(red.palette.analogous). 84 | to generate_palette %w(#f00 #f06 #f03 #f00 #f30 #f60) 85 | end 86 | 87 | it 'keeps the same format' do 88 | expect(red.palette.analogous).to all have_format :name 89 | end 90 | 91 | context 'with option :as' do 92 | it 'outputs the palette as an array of the string format' do 93 | expect(red.palette.analogous(as: :hex)). 94 | to eq %w(#ff0000 #ff0066 #ff0033 #ff0000 #ff3300 #ff6600) 95 | end 96 | end 97 | end 98 | 99 | context 'with `size` argument' do 100 | it 'returns the analogous palette' do 101 | expect(red.palette.analogous(size: 3)). 102 | to generate_palette %w(#f00 #ff001a #ff1a00) 103 | end 104 | 105 | it 'keeps the same format' do 106 | expect(red.palette.analogous(size: 3)).to all have_format :name 107 | end 108 | 109 | context 'with option :as' do 110 | it 'outputs the palette as an array of the string format' do 111 | expect(red.palette.analogous(size: 3, as: :hex)). 112 | to eq %w(#ff0000 #ff001a #ff1a00) 113 | end 114 | end 115 | end 116 | 117 | context 'with `size` and `slice_by` arguments' do 118 | it 'returns the analogous palette' do 119 | expect(red.palette.analogous(size: 3, slice_by: 10)). 120 | to generate_palette %w(#f00 #ff004c #ff4d00) 121 | end 122 | 123 | it 'keeps the same format' do 124 | expect(red.palette.analogous(size: 3, slice_by: 10)).to all have_format :name 125 | end 126 | 127 | context 'with option :as' do 128 | it 'outputs the palette as an array of the string format' do 129 | expect(red.palette.analogous(size: 3, slice_by: 10, as: :hex)). 130 | to eq %w(#ff0000 #ff004c #ff4d00) 131 | end 132 | end 133 | end 134 | end 135 | 136 | describe '#monochromatic' do 137 | context 'with default parameters' do 138 | it 'returns the monochromatic palette' do 139 | expect(red.palette.monochromatic). 140 | to generate_palette %w(#f00 #2a0000 #500 #800000 #a00 #d40000) 141 | end 142 | 143 | it 'keeps the same format' do 144 | expect(red.palette.monochromatic).to all have_format :name 145 | end 146 | 147 | context 'with option :as' do 148 | it 'outputs the palette as an array of the string format' do 149 | expect(red.palette.monochromatic(as: :hex)). 150 | to eq %w(#ff0000 #2a0000 #550000 #800000 #aa0000 #d40000) 151 | end 152 | end 153 | end 154 | 155 | context 'with `size` argument' do 156 | it 'returns the monochromatic palette' do 157 | expect(red.palette.monochromatic(size: 3)). 158 | to generate_palette %w(#f00 #500 #a00) 159 | end 160 | 161 | it 'keeps the same format' do 162 | expect(red.palette.monochromatic(size: 3)).to all have_format :name 163 | end 164 | 165 | context 'with option :as' do 166 | it 'outputs the palette as an array of the string format' do 167 | expect(red.palette.monochromatic(size: 3, as: :hex)). 168 | to eq %w(#ff0000 #550000 #aa0000) 169 | end 170 | end 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/chroma/color/serializers.rb: -------------------------------------------------------------------------------- 1 | module Chroma 2 | class Color 3 | # Methods for serializing {Color} to different color mode string formats. 4 | module Serializers 5 | # Convert to hsv string. 6 | # 7 | # @example 8 | # 'red'.paint.to_hsv #=> 'hsv(0, 100%, 100%)' 9 | # 'rgba(255, 0, 0, 0.5)'.paint.to_hsv #=> 'hsva(0, 100%, 100%, 0.5)' 10 | # 11 | # @return [String] 12 | def to_hsv 13 | to_hs(:v) 14 | end 15 | 16 | # Convert to hsl string. 17 | # 18 | # @example 19 | # 'red'.paint.to_hsl #=> 'hsl(0, 100%, 50%)' 20 | # 'rgba(255, 0, 0, 0.5)'.paint.to_hsl #=> 'hsla(0, 100%, 50%, 0.5)' 21 | # 22 | # @return [String] 23 | def to_hsl 24 | to_hs(:l) 25 | end 26 | 27 | # Convert to hexadecimal string. 28 | # 29 | # @example 30 | # 'red'.paint.to_hex #=> '#ff0000' 31 | # 'red'.paint.to_hex(true) #=> '#f00' 32 | # 'rgba(255, 0, 0, 0.5)'.paint.to_hex #=> '#ff0000' 33 | # 34 | # @param allow_3 [true, false] output 3-character hexadecimal 35 | # if possible 36 | # @return [String] 37 | def to_hex(allow_3 = false) 38 | "##{to_basic_hex(allow_3)}" 39 | end 40 | 41 | # Convert to 8-character hexadecimal string. The highest order byte 42 | # (left most hexadecimal pair represents the alpha value). 43 | # 44 | # @example 45 | # 'red'.paint.to_hex #=> '#ffff0000' 46 | # 'rgba(255, 0, 0, 0.5)'.paint.to_hex #=> '#80ff0000' 47 | # 48 | # @return [String] 49 | def to_hex8 50 | "##{to_basic_hex8}" 51 | end 52 | 53 | # Convert to rgb string. 54 | # 55 | # @example 56 | # 'red'.paint.to_rgb #=> 'rgb(255, 0, 0)' 57 | # 'rgba(255, 0, 0, 0.5)'.paint.to_rgb #=> 'rgb(255, 0, 0, 0.5)' 58 | # 59 | # @return [String] 60 | def to_rgb 61 | middle = @rgb.to_a[0..2].map(&:round).join(', ') 62 | 63 | with_alpha(:rgb, middle) 64 | end 65 | 66 | # Convert to named color if possible. If a color name can't be found, it 67 | # returns `''` or the hexadecimal string based on the value of 68 | # `hex_for_unknown`. 69 | # 70 | # @example 71 | # 'red'.paint.to_name #=> 'red' 72 | # 'rgba(255, 0, 0, 0.5)'.paint.to_name #=> '' 73 | # '#00f'.paint.to_name #=> 'blue' 74 | # '#123'.paint.to_name(true) #=> '#112233' 75 | # 76 | # @param hex_for_unknown [true, false] determine how unknown color names 77 | # should be returned 78 | # @return [String] 79 | def to_name(hex_for_unknown = false) 80 | return 'transparent' if alpha.zero? 81 | 82 | if alpha < 1 || (name = Chroma.name_from_hex(to_basic_hex(true))).nil? 83 | if hex_for_unknown 84 | to_hex 85 | else 86 | '' 87 | end 88 | else 89 | name 90 | end 91 | end 92 | 93 | # Convert to a string based on the color format. 94 | # 95 | # @example 96 | # 'red'.paint.to_s #=> 'red' 97 | # 'rgb(255, 0, 0)'.paint.to_s #=> 'rgb(255, 0, 0)' 98 | # '#f00'.paint.to_s #=> '#f00' 99 | # '#80ff0000'.paint.to_s(:rgb) #=> 'rgba(255, 0, 0, 0.5)' 100 | # 101 | # @param format [Symbol] the color format 102 | # @return [String] 103 | def to_s(format = @format) 104 | use_alpha = alpha < 1 && alpha >= 0 && /^hex(3|6)?$/ =~ format 105 | 106 | return to_rgb if use_alpha 107 | 108 | case format.to_s 109 | when 'rgb' then to_rgb 110 | when 'hex', 'hex6' then to_hex 111 | when 'hex3' then to_hex(true) 112 | when 'hex8' then to_hex8 113 | when 'hsl' then to_hsl 114 | when 'hsv' then to_hsv 115 | when 'name' then to_name(true) 116 | else to_hex 117 | end 118 | end 119 | 120 | alias_method :inspect, :to_s 121 | 122 | # Converts to an instance of {ColorModes::Hsv} 123 | # @return [ColorModes::Hsv] 124 | def hsv 125 | Converters::HsvConverter.convert_rgb(@rgb) 126 | end 127 | 128 | # Converts to an instance of {ColorModes::Hsl} 129 | # @return [ColorModes::Hsl] 130 | def hsl 131 | Converters::HslConverter.convert_rgb(@rgb) 132 | end 133 | 134 | # Converts to an instance of {ColorModes::Rgb} 135 | # @return [ColorModes::Rgb] 136 | attr_reader :rgb 137 | 138 | private 139 | 140 | def to_basic_hex(allow_3 = false) 141 | r, g, b = [@rgb.r, @rgb.g, @rgb.b].map do |n| 142 | to_2char_hex(n) 143 | end 144 | 145 | if allow_3 && r[0] == r[1] && g[0] == g[1] && b[0] == b[1] 146 | return "#{r[0]}#{g[0]}#{b[0]}" 147 | end 148 | 149 | "#{[r, g, b].flatten * ''}" 150 | end 151 | 152 | def to_basic_hex8 153 | [ 154 | to_2char_hex(alpha * 255), 155 | to_2char_hex(@rgb.r), 156 | to_2char_hex(@rgb.g), 157 | to_2char_hex(@rgb.b) 158 | ].join('') 159 | end 160 | 161 | def to_hs(third) 162 | name = "hs#{third}" 163 | color = send(name) 164 | 165 | h = color.h.round 166 | s = (color.s * 100).round 167 | lv = (color.send(third) * 100).round 168 | 169 | middle = "#{h}, #{s}%, #{lv}%" 170 | 171 | with_alpha(name, middle) 172 | end 173 | 174 | def with_alpha(mode, middle) 175 | if alpha < 1 176 | "#{mode}a(#{middle}, #{rounded_alpha})" 177 | else 178 | "#{mode}(#{middle})" 179 | end 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chroma 2 | 3 | [![Gem Version](https://badge.fury.io/rb/chroma.svg)](http://badge.fury.io/rb/chroma) 4 | [![Build Status](https://travis-ci.org/jfairbank/chroma.svg?branch=master)](https://travis-ci.org/jfairbank/chroma) 5 | 6 | Chroma is a color manipulation and palette generation library. It is heavily 7 | inspired by and a very close Ruby port of the 8 | [tinycolor.js](https://bgrins.github.io/TinyColor/) 9 | library. Many thanks to [Brian Grinstead](http://www.briangrinstead.com/blog/) 10 | for his hard work on that library. 11 | 12 | Please don't hesitate to examine the code and make issues, feature requests, 13 | or pull requests. Please refer to the [Contributing](#contributing) section 14 | below. 15 | 16 | ## Installation 17 | 18 | Add this line to your application's Gemfile: 19 | 20 | ```ruby 21 | gem 'chroma' 22 | ``` 23 | 24 | And then execute: 25 | 26 | $ bundle 27 | 28 | Or install it yourself as: 29 | 30 | $ gem install chroma 31 | 32 | ## Creating Colors 33 | 34 | Colors are created via the `Chroma.paint` method. It expects any one of 35 | many possible color formats as a string, including names, hexadecimal, rgb, 36 | hsl, and hsv. As a convenience, a `String#paint` is also available for more 37 | succinct color creation. 38 | 39 | ```ruby 40 | # With Chroma.paint 41 | Chroma.paint 'red' # named colors 42 | Chroma.paint '#00ff00' # 6 character hexadecimal 43 | Chroma.paint '#00f' # 3 character hexadecimal 44 | Chroma.paint 'rgb(255, 255, 0)' # rgb 45 | Chroma.paint 'rgba(255, 255, 0, 0.5)' # rgba 46 | Chroma.paint 'hsl(60, 100%, 50%)' # hsl with percentages 47 | Chroma.paint 'hsl(60, 1, 0.5)' # hsl with decimals 48 | Chroma.paint 'hsla(60, 100%, 50%, 0.5)' # hsla 49 | Chroma.paint 'hsv(60, 100%, 50%)' # hsv with percentages 50 | Chroma.paint 'hsv(60, 1, 0.5)' # hsv with decimals 51 | Chroma.paint 'hsva(60, 100%, 50%, 0.75)' # hsva 52 | 53 | # With String#paint 54 | 'red'.paint 55 | '#00ff00'.paint 56 | '#00f'.paint 57 | 'rgb(255, 255, 0)'.paint 58 | 'rgba(255, 255, 0, 0.5)'.paint 59 | 'hsl(60, 100%, 50%)'.paint 60 | 'hsla(60, 100%, 50%, 0.5)'.paint 61 | 'hsv(60, 100%, 50%)'.paint 62 | 'hsva(60, 100%, 50%. 0.5)'.paint 63 | ``` 64 | 65 | ## Motivation 66 | 67 | Chroma's major strength is manipulating colors and generating color palettes, 68 | which allows you to easily generate dynamic colors, dynamic themes for a web 69 | application, and more. 70 | 71 | ## Color Manipulation 72 | 73 | #### Lighten 74 | 75 | Lighten the color by a given amount. Defaults to 10. 76 | 77 | ```ruby 78 | 'red'.paint.lighten #=> #ff3333 79 | 'red'.paint.lighten(20) #=> #ff6666 80 | ``` 81 | 82 | #### Brighten 83 | 84 | Brighten the color by a given amount. Defaults to 10. 85 | 86 | ```ruby 87 | 'red'.paint.brighten #=> #ff1a1a 88 | 'red'.paint.brighten(20) #=> #ff3333 89 | ``` 90 | 91 | #### Darken 92 | 93 | Darken the color by a given amount. Defaults to 10. 94 | 95 | ```ruby 96 | 'red'.paint.darken #=> #cc0000 97 | 'red'.paint.darken(20) #=> #990000 98 | ``` 99 | 100 | #### Desaturate 101 | 102 | Desaturate the color by a given amount. Defaults to 10. 103 | 104 | ```ruby 105 | 'red'.paint.desaturate #=> #f20d0d 106 | 'red'.paint.desaturate(20) #=> #e61919 107 | ``` 108 | 109 | #### Saturate 110 | 111 | Saturate the color by a given amount. Defaults to 10. 112 | 113 | ```ruby 114 | '#123'.paint.saturate #=> #0e2236 115 | '#123'.paint.saturate(20) #=> #0a223a 116 | ``` 117 | 118 | #### Grayscale 119 | 120 | Convert the color to grayscale. 121 | 122 | ```ruby 123 | 'green'.paint.grayscale #=> #404040 124 | 125 | # greyscale is an alias 126 | 'red'.paint.greyscale #=> #808080 127 | ``` 128 | 129 | #### Opacity 130 | 131 | Set the opacity of the color to a given amount. 132 | 133 | ```ruby 134 | 'red'.paint.opacity(0.3) #=> #ff0000 135 | 'red'.paint.opacity(0.3).to_rgb #=> 'rgba(255, 0, 0, 0.3)' 136 | ``` 137 | 138 | #### Spin 139 | 140 | Spin a given amount in degrees around the hue wheel. 141 | 142 | ```ruby 143 | 'red'.paint.spin(30) #=> #ff8000 144 | 'red'.paint.spin(60) #=> yellow 145 | 'red'.paint.spin(90) #=> #80ff00 146 | ``` 147 | 148 | ## Generating Palettes 149 | 150 | Chroma's most powerful feature is palette generation. You can use the default 151 | palettes available or even create your own custom palettes. 152 | 153 | Palette methods are available via `Color#palette` and by default output an 154 | array of colors. If you want the underlying color strings, you can pass in 155 | the desired format via the `:as` option. 156 | 157 | #### Available Formats 158 | 159 | * name 160 | * rgb 161 | * hex 162 | * hex6 (alias for hex) 163 | * hex3 164 | * hex8 (includes the alpha value in the highest order byte) 165 | * hsl 166 | * hsv 167 | 168 | #### Complement 169 | 170 | Generate a complement palette. 171 | 172 | ```ruby 173 | 'red'.paint.palette.complement #=> [red, cyan] 174 | 'red'.paint.palette.complement(as: :name) #=> ['red', 'cyan'] 175 | 'red'.paint.palette.complement(as: :hex) #=> ['#ff0000', '#00ffff'] 176 | ``` 177 | 178 | #### Triad 179 | 180 | Generate a triad palette. 181 | 182 | ```ruby 183 | 'red'.paint.palette.triad #=> [red, lime, blue] 184 | 'red'.paint.palette.triad(as: :name) #=> ['red', 'lime', 'blue'] 185 | 'red'.paint.palette.triad(as: :hex) #=> ['#ff0000', '#00ff00', '#0000ff'] 186 | ``` 187 | 188 | #### Tetrad 189 | 190 | Generate a tetrad palette. 191 | 192 | ```ruby 193 | 'red'.paint.palette.tetrad 194 | #=> [red, #80ff00, cyan, #7f00ff] 195 | 196 | 'red'.paint.palette.tetrad(as: :name) 197 | #=> ['red', '#80ff00', 'cyan', '#7f00ff'] 198 | 199 | 'red'.paint.palette.tetrad(as: :hex) 200 | #=> ['#ff0000', '#80ff00', '#00ffff', '#7f00ff'] 201 | ``` 202 | 203 | #### Split Complement 204 | 205 | Generate a split complement palette. 206 | 207 | ```ruby 208 | 'red'.paint.palette.split_complement 209 | #=> [red, #ccff00, #0066ff] 210 | 211 | 'red'.paint.palette.split_complement(as: :name) 212 | #=> ['red', '#ccff00', '#0066ff'] 213 | 214 | 'red'.paint.palette.split_complement(as: :hex) 215 | #=> ['#ff0000', '#ccff00', '#0066ff'] 216 | ``` 217 | 218 | #### Analogous 219 | 220 | Generate an analogous palette. Pass in a `:size` option to specify the size 221 | of the palette (defaults to 6). Pass in a `:slice_by` option to specify the 222 | angle size to slice into the hue wheel (defaults to 30 degrees). 223 | 224 | ```ruby 225 | 'red'.paint.palette.analogous 226 | #=> [red, #ff0066, #ff0033, red, #ff3300, #ff6600] 227 | 228 | 'red'.paint.palette.analogous(as: :hex) 229 | #=> ['#f00', '#f06', '#f03', '#f00', '#f30', '#f60'] 230 | 231 | 'red'.paint.palette.analogous(size: 3) 232 | #=> [red, #ff001a, #ff1a00] 233 | 234 | 'red'.paint.palette.analogous(size: 3, slice_by: 60) 235 | #=> [red, #ff000d, #ff0d00] 236 | ``` 237 | 238 | #### Monochromatic 239 | 240 | Generate a monochromatic palette. Pass in a `:size` option to specify the size 241 | of the palette (defaults to 6). 242 | 243 | ```ruby 244 | 'red'.paint.palette.monochromatic 245 | #=> [red, #2a0000, #550000, maroon, #aa0000, #d40000] 246 | 247 | 'red'.paint.palette.monochromatic(as: :hex) 248 | #=> ['#ff0000', '#2a0000', '#550000', '#800000', '#aa0000', '#d40000'] 249 | 250 | 'red'.paint.palette.monochromatic(size: 3) 251 | #=> [red, #550000, #aa0000] 252 | ``` 253 | 254 | ## Defining Custom Palettes 255 | 256 | Chroma allows you to define your own custom palettes if the default ones aren't 257 | all you're looking for. You can define a custom palette by calling 258 | `Chroma.define_palette`, passing in a palette name and definition block. The 259 | definition block uses the color manipulation methods (i.e. `lighten`, `spin`, 260 | etc.) as its DSL. Every DSL call defines a new color that will be included 261 | in the palette. Your seed color (i.e. the color from which you call the 262 | palette method) will be included as the first color in your palette too. 263 | 264 | ```ruby 265 | red = 'red'.paint 266 | 267 | red.palette.respond_to? :my_palette #=> false 268 | 269 | # Define a palette with 5 colors including the seed color 270 | Chroma.define_palette :my_palette do 271 | spin 60 272 | spin 180 273 | spin(60).brighten(20) # chain calls as well 274 | greyscale 275 | end 276 | 277 | red.palette.respond_to? :my_palette #=> true 278 | 279 | red.palette.my_palette #=> [#ff0000 #ffff00 #00ffff #ffff33 #808080] 280 | ``` 281 | 282 | ## Dynamic Custom Palettes 283 | 284 | You can generate custom palettes on the fly too with 285 | `Chroma::Color#custom_palette`. 286 | 287 | ```ruby 288 | 'red'.paint.custom_palette do 289 | spin 60 290 | spin 180 291 | end 292 | 293 | #=> [red, yellow, cyan] 294 | ``` 295 | 296 | ## Serializing Colors 297 | 298 | Colors offer several methods to output to different string color [formats](#available-formats). 299 | 300 | | Method | Description | 301 | | --------- | ---------------------------------------------------------------------------------------------------------------- | 302 | | `to_hsv` | output to hsv string, outputs hsva if alpha < 1 | 303 | | `to_hsl` | output to hsl string, outputs hsla if alpha < 1 | 304 | | `to_hex` | output to hex string, optional argument allows 3-character hex output if possible | 305 | | `to_hex8` | output to 8-character hex string with alpha value in the highest order byte | 306 | | `to_rgb` | output to rgb string, outputs rgba if alpha < 1 | 307 | | `to_name` | output to color name string if available, otherwise `''` or `to_hex` output based on optional arg value | 308 | | `to_s` | output to the appropriate string format based on how the color was created, optional arg forces the format | 309 | 310 | ```ruby 311 | # to_hsv 312 | 'red'.paint.to_hsv #=> 'hsv(0, 100%, 100%)' 313 | 'rgba(255, 0, 0, 0.5)'.paint.to_hsv #=> 'hsva(0, 100%, 100%, 0.5)' 314 | 315 | # to_hsl 316 | 'red'.paint.to_hsl #=> 'hsl(0, 100%, 50%)' 317 | 'rgba(255, 0, 0, 0.5)'.paint.to_hsl #=> 'hsla(0, 100%, 50%, 0.5)' 318 | 319 | # to_hex 320 | 'red'.paint.to_hex #=> '#ff0000' 321 | 'red'.paint.to_hex(true) #=> '#f00' 322 | 'rgba(255, 0, 0, 0.5)'.paint.to_hex #=> '#ff0000' 323 | 'red'.paint.to_hex #=> '#ffff0000' 324 | 'rgba(255, 0, 0, 0.5)'.paint.to_hex #=> '#80ff0000' 325 | 326 | # to_rgb 327 | 'red'.paint.to_rgb #=> 'rgb(255, 0, 0)' 328 | 'rgba(255, 0, 0, 0.5)'.paint.to_rgb #=> 'rgb(255, 0, 0, 0.5)' 329 | 330 | # to_name 331 | 'red'.paint.to_name #=> 'red' 332 | '#00f'.paint.to_name #=> 'blue' 333 | 'rgba(255, 0, 0, 0.5)'.paint.to_name #=> '' 334 | '#123'.paint.to_name(true) #=> '#112233' 335 | 336 | # to_s 337 | 'red'.paint.to_s #=> 'red' 338 | 'rgb(255, 0, 0)'.paint.to_s #=> 'rgb(255, 0, 0)' 339 | '#f00'.paint.to_s #=> '#f00' 340 | '#80ff0000'.paint.to_s(:rgb) #=> 'rgba(255, 0, 0, 0.5)' 341 | ``` 342 | 343 | ## Other Methods 344 | 345 | Colors also have a few other helper methods: 346 | 347 | | Method | Description | 348 | | ------------ | ------------------------------------------------------ | 349 | | `dark?` | is the color dark? | 350 | | `light?` | is the color light? | 351 | | `alpha` | retrieve the alpha value | 352 | | `brightness` | calculate the brightness as a number between 0 and 255 | 353 | | `complement` | return the complementary color | 354 | 355 | ```ruby 356 | # dark? 357 | 'red'.paint.dark? #=> true 358 | 'yellow'.paint.dark? #=> false 359 | 360 | # light? 361 | 'red'.paint.light? #=> false 362 | 'yellow'.paint.light? #=> true 363 | 364 | # alpha 365 | 'red'.paint.alpha #=> 1.0 366 | 'rgba(0, 0, 0, 0.5)'.paint.alpha #=> 0.5 367 | 368 | # brightness 369 | 'red'.paint.brightness #=> 76.245 370 | 'yellow'.paint.brightness #=> 225.93 371 | 'white'.paint.brightness #=> 255.0 372 | 'black'.paint.brightness #=> 0.0 373 | 374 | # complement 375 | 'red'.paint.complement #=> cyan 376 | ``` 377 | 378 | ## Contributing 379 | 380 | Please branch from **dev** for all pull requests. 381 | 382 | 1. Fork it (https://github.com/jfairbank/chroma/fork) 383 | 2. Checkout dev (`git checkout dev`) 384 | 3. Create your feature branch (`git checkout -b my-new-feature`) 385 | 4. Commit your changes (`git commit -am 'Add some feature'`) 386 | 5. Push to the branch (`git push origin my-new-feature`) 387 | 6. Create a new pull request against dev 388 | --------------------------------------------------------------------------------