diff --git a/lib/glug.rb b/lib/glug.rb index 113454d..54e7fd3 100644 --- a/lib/glug.rb +++ b/lib/glug.rb @@ -10,5 +10,7 @@ module Glug # :nodoc: require_relative 'glug/stylesheet' require_relative 'glug/layer' require_relative 'glug/extensions' +require_relative 'glug/hsluv_modifiers' +require_relative 'glug/chroma_extensions' require_relative 'glug/stylesheet_dsl' require_relative 'glug/layer_dsl' diff --git a/lib/glug/chroma_extensions.rb b/lib/glug/chroma_extensions.rb new file mode 100644 index 0000000..56905a8 --- /dev/null +++ b/lib/glug/chroma_extensions.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'chroma' +require 'hsluv' + +module Chroma + class Color # :nodoc: + def mix(other, weight = 50) + other = other.paint if other.is_a?(String) + p = weight / 100.0 + + r = (rgb.r * p + other.rgb.r * (1 - p)).round + g = (rgb.g * p + other.rgb.g * (1 - p)).round + b = (rgb.b * p + other.rgb.b * (1 - p)).round + + Chroma.paint(+"rgb(#{r}, #{g}, #{b})") + end + end +end diff --git a/lib/glug/extensions.rb b/lib/glug/extensions.rb index b7e0708..4a502e5 100644 --- a/lib/glug/extensions.rb +++ b/lib/glug/extensions.rb @@ -5,19 +5,20 @@ # Colour methods on Integer class Integer - # rubocop:disable Style/StringConcatenation, Naming/MethodParameterName - def chroma_hex(op, p) - ('#' + to_s(16).rjust(6, '0')).paint.send(op, p).to_hex + # rubocop:disable Style/StringConcatenation + def chroma_hex(op, *args) + color = ('#' + to_s(16).rjust(6, '0')).paint + color.send(op, *args).to_hex end - def chroma(op, p) - chroma_hex(op, p).gsub('#', '0x').to_i(16) + def chroma(op, *args) + chroma_hex(op, *args).gsub('#', '0x').to_i(16) end def to_hex_color '#' + to_s(16).rjust(6, '0') end - # rubocop:enable Style/StringConcatenation, Naming/MethodParameterName + # rubocop:enable Style/StringConcatenation end # Top-level colour generators diff --git a/lib/glug/hsluv_modifiers.rb b/lib/glug/hsluv_modifiers.rb new file mode 100644 index 0000000..0bc1683 --- /dev/null +++ b/lib/glug/hsluv_modifiers.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'chroma' +require 'hsluv' + +module Chroma + # Add hsluv versions of the color functions to chroma + class Color + def darkenp(amount = 10) + h, s, l = to_hsluv + l = [l - amount, 0].max + from_hsluv(h, s, l) + end + + def lightenp(amount = 10) + h, s, l = to_hsluv + l = [l + amount, 100].min + from_hsluv(h, s, l) + end + + def saturatep(amount = 10) + h, s, l = to_hsluv + s = [s + amount, 100].min + from_hsluv(h, s, l) + end + + def desaturatep(amount = 10) + h, s, l = to_hsluv + s = [s - amount, 0].max + from_hsluv(h, s, l) + end + + def greyscalep + h, _s, l = to_hsluv + from_hsluv(h, 0, l) + end + alias grayscalep greyscalep + + def spinp(amount = 0) + h, s, l = to_hsluv + h = (h + amount) % 360 + from_hsluv(h, s, l) + end + + def mixp(other, weight = 50) + other = other.paint if other.is_a?(String) + p = weight / 100.0 + + h1, s1, l1 = to_hsluv + h2, s2, l2 = other.to_hsluv + + # Interpolate hue on the shortest path around the color wheel + h_diff = h2 - h1 + h_diff -= 360 if h_diff > 180 + h_diff += 360 if h_diff < -180 + h = (h1 + h_diff * (1 - p)) % 360 + s = s1 * p + s2 * (1 - p) + l = l1 * p + l2 * (1 - p) + + from_hsluv(h, s, l) + end + + def to_hsluv + Hsluv.rgb_to_hsluv(rgb.r / 255.0, rgb.g / 255.0, rgb.b / 255.0) + end + + def from_hsluv(h, s, l) # rubocop:disable Naming/MethodParameterName + rgb_values = Hsluv.rgb_prepare(Hsluv.hsluv_to_rgb(h, s, l)) + Chroma.paint(+"rgb(#{rgb_values[0]}, #{rgb_values[1]}, #{rgb_values[2]})") + end + end +end diff --git a/spec/fixtures/color_functions.glug b/spec/fixtures/color_functions.glug new file mode 100644 index 0000000..8cce2bc --- /dev/null +++ b/spec/fixtures/color_functions.glug @@ -0,0 +1,59 @@ +version 8 +name 'My first stylesheet' +source :shortbread, type: 'vector', url: 'https://vector.openstreetmap.org/shortbread_v1/tilejson.json' + +layer(:darken, source: :shortbread) do + line_color 0x998877.chroma(:darken, 20) +end + +layer(:darkenp, source: :shortbread) do + line_color 0x998877.chroma(:darkenp, 20) +end + +layer(:lighten, source: :shortbread) do + line_color 0x998877.chroma(:lighten, 20) +end + +layer(:lightenp, source: :shortbread) do + line_color 0x998877.chroma(:lightenp, 20) +end + +layer(:saturate, source: :shortbread) do + line_color 0x998877.chroma(:saturate, 20) +end + +layer(:saturatep, source: :shortbread) do + line_color 0x998877.chroma(:saturatep, 20) +end + +layer(:desaturate, source: :shortbread) do + line_color 0x998877.chroma(:desaturate, 20) +end + +layer(:desaturatep, source: :shortbread) do + line_color 0x998877.chroma(:desaturatep, 20) +end + +layer(:greyscale, source: :shortbread) do + line_color 0x998877.chroma(:greyscale) +end + +layer(:greyscalep, source: :shortbread) do + line_color 0x998877.chroma(:greyscalep) +end + +layer(:spin, source: :shortbread) do + line_color 0x998877.chroma(:spin, 20) +end + +layer(:spinp, source: :shortbread) do + line_color 0x998877.chroma(:spinp, 20) +end + +layer(:mix, source: :shortbread) do + line_color 0x998877.chroma(:mix, '#ffffff', 50) +end + +layer(:mixp, source: :shortbread) do + line_color 0x998877.chroma(:mixp, '#ffffff', 50) +end diff --git a/spec/fixtures/color_functions.json b/spec/fixtures/color_functions.json new file mode 100644 index 0000000..a10ab55 --- /dev/null +++ b/spec/fixtures/color_functions.json @@ -0,0 +1,110 @@ +{ + "version":8, + "name":"My first stylesheet", + "sources":{ + "shortbread":{ + "type":"vector", + "url":"https://vector.openstreetmap.org/shortbread_v1/tilejson.json" + } + }, + "layers":[ + { + "paint":{"line-color":"#615549"}, + "source":"shortbread", + "id":"darken", + "source-layer":"darken", + "type":"line" + }, + { + "paint":{"line-color":"#62574b"}, + "source":"shortbread", + "id":"darkenp", + "source-layer":"darkenp", + "type":"line" + }, + { + "paint":{"line-color":"#c5bbb1"}, + "source":"shortbread", + "id":"lighten", + "source-layer":"lighten", + "type":"line" + }, + { + "paint":{"line-color":"#d3bca5"}, + "source":"shortbread", + "id":"lightenp", + "source-layer":"lightenp", + "type":"line" + }, + { + "paint":{"line-color":"#b1885f"}, + "source":"shortbread", + "id":"saturate", + "source-layer":"saturate", + "type":"line" + }, + { + "paint":{"line-color":"#a28665"}, + "source":"shortbread", + "id":"saturatep", + "source-layer":"saturatep", + "type":"line" + }, + { + "paint":{"line-color":"#888888"}, + "source":"shortbread", + "id":"desaturate", + "source-layer":"desaturate", + "type":"line" + }, + { + "paint":{"line-color":"#8e8a87"}, + "source":"shortbread", + "id":"desaturatep", + "source-layer":"desaturatep", + "type":"line" + }, + { + "paint":{"line-color":"#888888"}, + "source":"shortbread", + "id":"greyscale", + "source-layer":"greyscale", + "type":"line" + }, + { + "paint":{"line-color":"#8b8b8b"}, + "source":"shortbread", + "id":"greyscalep", + "source-layer":"greyscalep", + "type":"line" + }, + { + "paint":{"line-color":"#999377"}, + "source":"shortbread", + "id":"spin", + "source-layer":"spin", + "type":"line" + }, + { + "paint":{"line-color":"#918b77"}, + "source":"shortbread", + "id":"spinp", + "source-layer":"spinp", + "type":"line" + }, + { + "paint":{"line-color":"#ccc4bb"}, + "source":"shortbread", + "id":"mix", + "source-layer":"mix", + "type":"line" + }, + { + "paint":{"line-color":"#cdc1be"}, + "source":"shortbread", + "id":"mixp", + "source-layer":"mixp", + "type":"line" + } + ] +} diff --git a/spec/lib/glug/stylesheet_spec.rb b/spec/lib/glug/stylesheet_spec.rb index f25811e..7c07093 100644 --- a/spec/lib/glug/stylesheet_spec.rb +++ b/spec/lib/glug/stylesheet_spec.rb @@ -113,5 +113,14 @@ output = File.read(File.join(fixture_dir, 'expression_ivars.json')) expect(stylesheet.to_json).to eql(output.strip) end + + it 'handles color functions' do + glug = File.read(File.join(fixture_dir, 'color_functions.glug')) + stylesheet = described_class.new(base_dir: fixture_dir) do + instance_eval(glug) + end + output = File.read(File.join(fixture_dir, 'color_functions.json')) + expect(stylesheet.to_json).to eql(output.strip) + end end end