immosquare-colors 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 731aff145269af33588fd72b5abef8720b45cb260de44d9083a4f8c1417b86de
4
- data.tar.gz: 35a426a289463a8c93d326c548217107e9e1932dc068be64d50d35bc8135e548
3
+ metadata.gz: 298cb3ad3c069e4f68cd9de80e71708a7cc96c4dec84809c7f23f4181ad80d1d
4
+ data.tar.gz: 40f23aae65b3b72611898ca2b52a1605781bf345e45bd62b6587c3cb17a8d09f
5
5
  SHA512:
6
- metadata.gz: 71b29cc00937ae63c5b228c65e0e1461ee3f784c545b6f2b326b792f897584ba2e54d04076949fdcc457ac6e8bec388deefa2532c13f5febba1302f45e23ea15
7
- data.tar.gz: 2d25bf197fbf33ef50b67f5ebddf84e7c6a5a2fa036478fd7b26d7db52d1b7843dd6d2e6847c1fcc3e045f39481a3a9538804ed580fbab0ced0258a833c9efe3
6
+ metadata.gz: 0d7207b2ff2ef514539cb159e4b05ea5830f38d4aed38bce7bf5f77ebdf5a04efcf1d04c745266b05fcf6904dcfb1843e92651f8d32d6d9863bc8d4967a07c08
7
+ data.tar.gz: e9f0713467cc5cc73ad32c32dd50f896be52cc07bfc836ea066768759e271adcf109143d8e7cddc5d6286e2eece9f5fe1bf454117e4e773ae86f119a2bb075d0
@@ -1,3 +1,3 @@
1
1
  module ImmosquareColors
2
- VERSION = "0.1.5".freeze
2
+ VERSION = "0.1.6".freeze
3
3
  end
@@ -5,26 +5,33 @@ module ImmosquareColors
5
5
 
6
6
  ##============================================================##
7
7
  ## To determine whether the complementary color should be
8
- ## black or white
8
+ ## black or white.
9
+ ##
10
+ ## The answer is the overlay that actually reads best, decided by
11
+ ## WCAG contrast ratio. A perceived-brightness cutoff does not
12
+ ## agree with that ratio around mid luminance: olives, mauves and
13
+ ## muted greens sit just under a 127.5 cutoff and are handed white,
14
+ ## while black reads better on them. `#6c8539` for instance scores
15
+ ## 4.16:1 against white and 5.05:1 against black.
16
+ ##
17
+ ## Passing `:luminance` keeps the former behaviour, for callers
18
+ ## that tuned their own brightness cutoff.
9
19
  ##============================================================##
10
20
  def get_complementary_color(color, options = {})
11
21
  begin
12
- luminance_std = (options[:luminance] || 127.5).to_f
13
- black = "#000000"
14
- white = "#FFFFFF"
15
- color_hex = color.start_with?("#") ? color : color_name_to_hex(color)
22
+ black = "#000000"
23
+ white = "#FFFFFF"
24
+ color_hex = color.start_with?("#") ? color : color_name_to_hex(color)
16
25
 
17
26
  raise("Not valid size") if ![7, 9].include?(color_hex.length)
18
27
 
28
+ if options[:luminance]
29
+ r, g, b = hex_to_rgba(color_hex)
30
+ brightness = Math.sqrt((0.299 * (r**2)) + (0.587 * (g**2)) + (0.114 * (b**2)))
31
+ return brightness > options[:luminance].to_f ? black : white
32
+ end
19
33
 
20
- color_rgba = hex_to_rgba(color_hex)
21
- r, g, b, a = color_rgba
22
-
23
- ##============================================================##
24
- ## luminance calculation
25
- ##============================================================##
26
- luminance = Math.sqrt((0.299 * (r**2)) + (0.587 * (g**2)) + (0.114 * (b**2)))
27
- luminance > luminance_std ? black : white
34
+ contrast_ratio(color_hex, white) >= contrast_ratio(color_hex, black) ? white : black
28
35
  rescue StandardError => e
29
36
  puts("=== Error! ===")
30
37
  puts(e.message)
@@ -33,6 +40,28 @@ module ImmosquareColors
33
40
  end
34
41
  end
35
42
 
43
+ ##============================================================##
44
+ ## WCAG 2.1 contrast ratio between two colors, from 1 (identical)
45
+ ## to 21 (black on white). Body text wants 4.5, large text and
46
+ ## graphical objects 3.
47
+ ##============================================================##
48
+ def contrast_ratio(color_one, color_two)
49
+ lighter, darker = [relative_luminance(color_one), relative_luminance(color_two)].minmax.reverse
50
+ (lighter + 0.05) / (darker + 0.05)
51
+ end
52
+
53
+ ##============================================================##
54
+ ## WCAG 2.1 relative luminance, between 0 (black) and 1 (white).
55
+ ## Channels are linearised before being weighted, which is what
56
+ ## separates it from a plain brightness average.
57
+ ##============================================================##
58
+ def relative_luminance(color)
59
+ color_hex = color.start_with?("#") ? color : color_name_to_hex(color)
60
+ channels = hex_to_rgba(color_hex).first(3).map {|channel| channel / 255.0 }
61
+ linear = channels.map {|channel| channel <= 0.03928 ? channel / 12.92 : (((channel + 0.055) / 1.055)**2.4) }
62
+ (0.2126 * linear[0]) + (0.7152 * linear[1]) + (0.0722 * linear[2])
63
+ end
64
+
36
65
  ##============================================================##
37
66
  ## To transform a hex color to rgba
38
67
  ##============================================================##
@@ -88,5 +117,94 @@ module ImmosquareColors
88
117
  rgba_to_hex([shaded_r, shaded_g, shaded_b, a])
89
118
  end
90
119
 
120
+ ##============================================================##
121
+ ## Mix two colors in sRGB, `weight` being the share of the second
122
+ ## one. Generalises tint (mix with white) and shade (mix with
123
+ ## black), and mirrors what CSS `color-mix(in srgb, ...)` computes,
124
+ ## so a value derived here matches what a browser would render.
125
+ ## Alpha is carried over from the first color.
126
+ ##============================================================##
127
+ def mix_colors(color_one, color_two, weight)
128
+ one_hex = color_one.start_with?("#") ? color_one : color_name_to_hex(color_one)
129
+ two_hex = color_two.start_with?("#") ? color_two : color_name_to_hex(color_two)
130
+ r1, g1, b1, a1 = hex_to_rgba(one_hex)
131
+ r2, g2, b2 = hex_to_rgba(two_hex)
132
+ mixed = [[r1, r2], [g1, g2], [b1, b2]].map {|from, to| (((1 - weight) * from) + (weight * to)).round }
133
+ rgba_to_hex(mixed + [a1].compact)
134
+ end
135
+
136
+ ##============================================================##
137
+ ## The color sitting opposite on the color wheel: same saturation
138
+ ## and lightness, hue rotated by 180°. Useful to derive a secondary
139
+ ## color that cannot be mistaken for a tint of the primary one.
140
+ ##============================================================##
141
+ def get_opposite_color(color)
142
+ h, s, l = hex_to_hsl(color)
143
+ hsl_to_hex([h + 180, s, l])
144
+ end
145
+
146
+ ##============================================================##
147
+ ## Whether two colors are far enough apart to be read together.
148
+ ## `:level` accepts :aa (4.5, body text), :aa_large (3, large text
149
+ ## and graphical objects such as icons or logos) and :aaa (7).
150
+ ##============================================================##
151
+ def accessible_contrast?(color_one, color_two, options = {})
152
+ thresholds = {:aa => 4.5, :aa_large => 3.0, :aaa => 7.0}
153
+ threshold = thresholds[(options[:level] || :aa).to_sym] || thresholds[:aa]
154
+ contrast_ratio(color_one, color_two) >= threshold
155
+ end
156
+
157
+ ##============================================================##
158
+ ## To transform a hex color to HSL, hue in degrees (0...360),
159
+ ## saturation and lightness between 0 and 1.
160
+ ##============================================================##
161
+ def hex_to_hsl(color)
162
+ color_hex = color.start_with?("#") ? color : color_name_to_hex(color)
163
+ r, g, b = hex_to_rgba(color_hex).first(3).map {|channel| channel / 255.0 }
164
+ high, low = [r, g, b].minmax.reverse
165
+ lightness = (high + low) / 2.0
166
+ return [0.0, 0.0, lightness.round(4)] if high == low
167
+
168
+ delta = high - low
169
+ saturation = lightness > 0.5 ? delta / (2.0 - high - low) : delta / (high + low)
170
+ hue = case high
171
+ when r then ((g - b) / delta) + (g < b ? 6.0 : 0.0)
172
+ when g then ((b - r) / delta) + 2.0
173
+ else ((r - g) / delta) + 4.0
174
+ end
175
+ [(hue * 60.0).round(2), saturation.round(4), lightness.round(4)]
176
+ end
177
+
178
+ ##============================================================##
179
+ ## To transform an HSL array back to hex. The hue wraps around, so
180
+ ## a rotation past 360° needs no clamping by the caller.
181
+ ##============================================================##
182
+ def hsl_to_hex(hsl)
183
+ hue, saturation, lightness = hsl
184
+ hue = (hue % 360) / 360.0
185
+ return rgba_to_hex([(lightness * 255).round] * 3) if saturation == 0
186
+
187
+ second = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - (lightness * saturation)
188
+ first = (2 * lightness) - second
189
+ rgba_to_hex([1.0 / 3, 0, -1.0 / 3].map {|offset| (hue_to_channel(first, second, hue + offset) * 255).round })
190
+ end
191
+
192
+
193
+ private
194
+
195
+ ##============================================================##
196
+ ## One channel of an HSL → RGB conversion, from the two
197
+ ## intermediate values and the hue shifted for that channel.
198
+ ##============================================================##
199
+ def hue_to_channel(first, second, hue)
200
+ hue += 1 if hue < 0
201
+ hue -= 1 if hue > 1
202
+ return first + ((second - first) * 6 * hue) if hue < 1.0 / 6
203
+ return second if hue < 1.0 / 2
204
+ return first + ((second - first) * ((2.0 / 3) - hue) * 6) if hue < 2.0 / 3
205
+
206
+ first
207
+ end
208
+
91
209
  end
92
210
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: immosquare-colors
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
- - IMMO SQUARE
8
- autorequire:
7
+ - immosquare
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-10-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: immosquare-constants
@@ -30,8 +29,8 @@ dependencies:
30
29
  - - ">="
31
30
  - !ruby/object:Gem::Version
32
31
  version: 0.1.3
33
- description: Provides methods to suggest complementary colors based on luminance,
34
- convert HEX to RGBA, and map named colors to HEX
32
+ description: Provides methods to derive a black/white complementary color from luminance,
33
+ tint or shade a color, convert HEX <-> RGBA, and resolve named colors to HEX.
35
34
  email:
36
35
  - jules@immosquare.com
37
36
  executables: []
@@ -40,11 +39,10 @@ extra_rdoc_files: []
40
39
  files:
41
40
  - lib/immosquare-colors.rb
42
41
  - lib/immosquare-colors/version.rb
43
- homepage: https://github.com/IMMOSQUARE/immosquare-colors
42
+ homepage: https://github.com/immosquare/immosquare-colors
44
43
  licenses:
45
44
  - MIT
46
45
  metadata: {}
47
- post_install_message:
48
46
  rdoc_options: []
49
47
  require_paths:
50
48
  - lib
@@ -52,15 +50,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
52
50
  requirements:
53
51
  - - ">="
54
52
  - !ruby/object:Gem::Version
55
- version: 2.7.2
53
+ version: 3.2.6
56
54
  required_rubygems_version: !ruby/object:Gem::Requirement
57
55
  requirements:
58
56
  - - ">="
59
57
  - !ruby/object:Gem::Version
60
58
  version: '0'
61
59
  requirements: []
62
- rubygems_version: 3.4.13
63
- signing_key:
60
+ rubygems_version: 4.0.20
64
61
  specification_version: 4
65
- summary: Ruby utility for complementary color derivation and color conversions.
62
+ summary: Ruby utility for complementary color derivation, tinting, shading and color
63
+ conversions.
66
64
  test_files: []