sass-embedded 1.79.3 → 1.79.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/exe/sass +1 -9
  3. data/ext/sass/Rakefile +49 -16
  4. data/ext/sass/package.json +1 -1
  5. data/lib/sass/compiler/connection.rb +1 -9
  6. data/lib/sass/compiler/host/protofier.rb +16 -55
  7. data/lib/sass/elf.rb +4 -4
  8. data/lib/sass/embedded/version.rb +1 -1
  9. data/lib/sass/value/color/channel.rb +79 -0
  10. data/lib/sass/value/color/conversions.rb +464 -0
  11. data/lib/sass/value/color/gamut_map_method/clip.rb +45 -0
  12. data/lib/sass/value/color/gamut_map_method/local_minde.rb +94 -0
  13. data/lib/sass/value/color/gamut_map_method.rb +45 -0
  14. data/lib/sass/value/color/interpolation_method.rb +51 -0
  15. data/lib/sass/value/color/space/a98_rgb.rb +57 -0
  16. data/lib/sass/value/color/space/display_p3.rb +57 -0
  17. data/lib/sass/value/color/space/hsl.rb +65 -0
  18. data/lib/sass/value/color/space/hwb.rb +70 -0
  19. data/lib/sass/value/color/space/lab.rb +77 -0
  20. data/lib/sass/value/color/space/lch.rb +53 -0
  21. data/lib/sass/value/color/space/lms.rb +129 -0
  22. data/lib/sass/value/color/space/oklab.rb +66 -0
  23. data/lib/sass/value/color/space/oklch.rb +54 -0
  24. data/lib/sass/value/color/space/prophoto_rgb.rb +59 -0
  25. data/lib/sass/value/color/space/rec2020.rb +69 -0
  26. data/lib/sass/value/color/space/rgb.rb +52 -0
  27. data/lib/sass/value/color/space/srgb.rb +141 -0
  28. data/lib/sass/value/color/space/srgb_linear.rb +72 -0
  29. data/lib/sass/value/color/space/utils.rb +86 -0
  30. data/lib/sass/value/color/space/xyz_d50.rb +100 -0
  31. data/lib/sass/value/color/space/xyz_d65.rb +57 -0
  32. data/lib/sass/value/color/space.rb +198 -0
  33. data/lib/sass/value/color.rb +537 -162
  34. data/lib/sass/value/fuzzy_math.rb +30 -3
  35. data/lib/sass/value/string.rb +1 -1
  36. metadata +29 -5
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'color/channel'
4
+ require_relative 'color/conversions'
5
+ require_relative 'color/gamut_map_method'
6
+ require_relative 'color/interpolation_method'
7
+ require_relative 'color/space'
8
+
3
9
  module Sass
4
10
  module Value
5
11
  # Sass's color type.
@@ -18,90 +24,149 @@ module Sass
18
24
  # @param lightness [Numeric]
19
25
  # @param whiteness [Numeric]
20
26
  # @param blackness [Numeric]
27
+ # @param a [Numeric]
28
+ # @param b [Numeric]
29
+ # @param chroma [Numeric]
30
+ # @param x [Numeric]
31
+ # @param y [Numeric]
32
+ # @param z [Numeric]
21
33
  # @param alpha [Numeric]
22
- def initialize(red: nil,
23
- green: nil,
24
- blue: nil,
25
- hue: nil,
26
- saturation: nil,
27
- lightness: nil,
28
- whiteness: nil,
29
- blackness: nil,
30
- alpha: 1)
31
- @alpha = alpha.nil? ? 1 : FuzzyMath.assert_between(alpha, 0, 1, 'alpha')
32
- if red && green && blue
33
- @red = FuzzyMath.assert_between(FuzzyMath.round(red), 0, 255, 'red')
34
- @green = FuzzyMath.assert_between(FuzzyMath.round(green), 0, 255, 'green')
35
- @blue = FuzzyMath.assert_between(FuzzyMath.round(blue), 0, 255, 'blue')
36
- elsif hue && saturation && lightness
37
- @hue = hue % 360
38
- @saturation = FuzzyMath.assert_between(saturation, 0, 100, 'saturation')
39
- @lightness = FuzzyMath.assert_between(lightness, 0, 100, 'lightness')
40
- elsif hue && whiteness && blackness
41
- @hue = hue % 360
42
- @whiteness = FuzzyMath.assert_between(whiteness, 0, 100, 'whiteness')
43
- @blackness = FuzzyMath.assert_between(blackness, 0, 100, 'blackness')
44
- hwb_to_rgb
45
- @whiteness = @blackness = nil
46
- else
47
- raise Sass::ScriptError, 'Invalid Color'
34
+ # @param space [::String]
35
+ # @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'rgb')
36
+ # @overload initialize(hue: nil, saturation: nil, lightness: nil, alpha: nil, space: 'hsl')
37
+ # @overload initialize(hue: nil, whiteness: nil, blackness: nil, alpha: nil, space: 'hwb')
38
+ # @overload initialize(lightness: nil, a: nil, b: nil, alpha: nil, space: 'lab')
39
+ # @overload initialize(lightness: nil, a: nil, b: nil, alpha: nil, space: 'oklab')
40
+ # @overload initialize(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'lch')
41
+ # @overload initialize(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'oklch')
42
+ # @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'a98-rgb')
43
+ # @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'display-p3')
44
+ # @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'prophoto-rgb')
45
+ # @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'rec2020')
46
+ # @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb')
47
+ # @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb-linear')
48
+ # @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz')
49
+ # @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d50')
50
+ # @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d65')
51
+ def initialize(**options)
52
+ unless options.key?(:space)
53
+ options[:space] = case options
54
+ in {red: _, green: _, blue: _}
55
+ 'rgb'
56
+ in {hue: _, saturation: _, lightness: _}
57
+ 'hsl'
58
+ in {hue: _, whiteness: _, blackness: _}
59
+ 'hwb'
60
+ else
61
+ raise Sass::ScriptError.new('No color space found', 'space')
62
+ end
48
63
  end
49
- end
50
64
 
51
- # @return [Integer]
52
- def red
53
- hsl_to_rgb unless defined?(@red)
65
+ space = Space.from_name(options[:space])
54
66
 
55
- @red
67
+ keys = _assert_options(space, options)
68
+
69
+ _initialize_for_space_internal(space,
70
+ options[keys[0]],
71
+ options[keys[1]],
72
+ options[keys[2]],
73
+ options.fetch(:alpha, 1))
56
74
  end
57
75
 
58
- # @return [Integer]
59
- def green
60
- hsl_to_rgb unless defined?(@green)
76
+ # @return [::String]
77
+ def space
78
+ _space.name
79
+ end
61
80
 
62
- @green
81
+ # @param space [::String]
82
+ # @return [Color]
83
+ def to_space(space)
84
+ _to_space(Space.from_name(space))
63
85
  end
64
86
 
65
- # @return [Integer]
66
- def blue
67
- hsl_to_rgb unless defined?(@blue)
87
+ # @param space [::String]
88
+ # @return [::Boolean]
89
+ def in_gamut?(space = nil)
90
+ return to_space(space)._in_gamut? unless space.nil?
68
91
 
69
- @blue
92
+ _in_gamut?
70
93
  end
71
94
 
72
- # @return [Numeric]
73
- def hue
74
- rgb_to_hsl unless defined?(@hue)
95
+ # @param method [::String]
96
+ # @param space [::String]
97
+ # @return [Color]
98
+ def to_gamut(method:, space: nil)
99
+ return to_space(space).to_gamut(method:)._to_space(_space) unless space.nil?
75
100
 
76
- @hue
101
+ _to_gamut(GamutMapMethod.from_name(method, 'method'))
77
102
  end
78
103
 
79
- # @return [Numeric]
80
- def saturation
81
- rgb_to_hsl unless defined?(@saturation)
104
+ # @return [Array<Numeric>]
105
+ def channels_or_nil
106
+ [channel0_or_nil, channel1_or_nil, channel2_or_nil].freeze
107
+ end
82
108
 
83
- @saturation
109
+ # @return [Array<Numeric>]
110
+ def channels
111
+ [channel0, channel1, channel2].freeze
84
112
  end
85
113
 
114
+ # @param channel [::String]
115
+ # @param space [::String]
86
116
  # @return [Numeric]
87
- def lightness
88
- rgb_to_hsl unless defined?(@lightness)
117
+ def channel(channel, space: nil)
118
+ return to_space(space).channel(channel) unless space.nil?
119
+
120
+ channels = _space.channels
121
+ return channel0 if channel == channels[0].name
122
+ return channel1 if channel == channels[1].name
123
+ return channel2 if channel == channels[2].name
124
+ return alpha if channel == 'alpha'
89
125
 
90
- @lightness
126
+ raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
91
127
  end
92
128
 
93
- # @return [Numeric]
94
- def whiteness
95
- @whiteness ||= Rational([red, green, blue].min, 255) * 100
129
+ # @param channel [::String]
130
+ # @return [::Boolean]
131
+ def channel_missing?(channel)
132
+ channels = _space.channels
133
+ return channel0_missing? if channel == channels[0].name
134
+ return channel1_missing? if channel == channels[1].name
135
+ return channel2_missing? if channel == channels[2].name
136
+ return alpha_missing? if channel == 'alpha'
137
+
138
+ raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
96
139
  end
97
140
 
98
- # @return [Numeric]
99
- def blackness
100
- @blackness ||= 100 - (Rational([red, green, blue].max, 255) * 100)
141
+ # @param channel [::String]
142
+ # @param space [::String]
143
+ # @return [::Boolean]
144
+ def channel_powerless?(channel, space: nil)
145
+ return to_space(space).channel_powerless?(channel) unless space.nil?
146
+
147
+ channels = _space.channels
148
+ return channel0_powerless? if channel == channels[0].name
149
+ return channel1_powerless? if channel == channels[1].name
150
+ return channel2_powerless? if channel == channels[2].name
151
+ return false if channel == 'alpha'
152
+
153
+ raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
101
154
  end
102
155
 
103
- # @return [Numeric]
104
- attr_reader :alpha
156
+ # @param other [Color]
157
+ # @param method [::String]
158
+ # @param weight [Numeric]
159
+ # @return [Color]
160
+ def interpolate(other, method: nil, weight: nil)
161
+ interpolation_method = if !method.nil?
162
+ InterpolationMethod.new(_space, HueInterpolationMethod.from_name(method))
163
+ elsif !_space.polar?
164
+ InterpolationMethod.new(_space)
165
+ else
166
+ InterpolationMethod.new(_space, :shorter)
167
+ end
168
+ _interpolate(other, interpolation_method, weight:)
169
+ end
105
170
 
106
171
  # @param red [Numeric]
107
172
  # @param green [Numeric]
@@ -111,52 +176,175 @@ module Sass
111
176
  # @param lightness [Numeric]
112
177
  # @param whiteness [Numeric]
113
178
  # @param blackness [Numeric]
179
+ # @param a [Numeric]
180
+ # @param b [Numeric]
181
+ # @param chroma [Numeric]
182
+ # @param x [Numeric]
183
+ # @param y [Numeric]
184
+ # @param z [Numeric]
114
185
  # @param alpha [Numeric]
186
+ # @param space [::String]
187
+ # @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'rgb')
188
+ # @overload change(hue: nil, saturation: nil, lightness: nil, alpha: nil, space: 'hsl')
189
+ # @overload change(hue: nil, whiteness: nil, blackness: nil, alpha: nil, space: 'hwb')
190
+ # @overload change(lightness: nil, a: nil, b: nil, alpha: nil, space: 'lab')
191
+ # @overload change(lightness: nil, a: nil, b: nil, alpha: nil, space: 'oklab')
192
+ # @overload change(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'lch')
193
+ # @overload change(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'oklch')
194
+ # @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'a98-rgb')
195
+ # @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'display-p3')
196
+ # @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'prophoto-rgb')
197
+ # @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'rec2020')
198
+ # @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb')
199
+ # @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb-linear')
200
+ # @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz')
201
+ # @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d50')
202
+ # @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d65')
115
203
  # @return [Color]
116
- def change(red: nil,
117
- green: nil,
118
- blue: nil,
119
- hue: nil,
120
- saturation: nil,
121
- lightness: nil,
122
- whiteness: nil,
123
- blackness: nil,
124
- alpha: nil)
125
- if whiteness || blackness
126
- Sass::Value::Color.new(hue: hue || self.hue,
127
- whiteness: whiteness || self.whiteness,
128
- blackness: blackness || self.blackness,
129
- alpha: alpha || self.alpha)
130
- elsif hue || saturation || lightness
131
- Sass::Value::Color.new(hue: hue || self.hue,
132
- saturation: saturation || self.saturation,
133
- lightness: lightness || self.lightness,
134
- alpha: alpha || self.alpha)
135
- elsif red || green || blue
136
- Sass::Value::Color.new(red: red ? FuzzyMath.round(red) : self.red,
137
- green: green ? FuzzyMath.round(green) : self.green,
138
- blue: blue ? FuzzyMath.round(blue) : self.blue,
139
- alpha: alpha || self.alpha)
140
- else
141
- dup.instance_eval do
142
- @alpha = FuzzyMath.assert_between(alpha, 0, 1, 'alpha')
143
- self
204
+ def change(**options)
205
+ space_set_explictly = !options[:space].nil?
206
+ space = space_set_explictly ? Space.from_name(options[:space]) : _space
207
+
208
+ if legacy? && !space_set_explictly
209
+ case options
210
+ in {whiteness: _} | {blackness: _}
211
+ space = Space::HWB
212
+ in {saturation: _} | {lightness: _}
213
+ space = Space::HSL
214
+ in {hue: _}
215
+ space = if _space == Space::HWB
216
+ Space::HWB
217
+ else
218
+ Space::HSL
219
+ end
220
+ in {red: _} | {blue: _} | {green: _}
221
+ space = Space::RGB
222
+ else
223
+ end
224
+
225
+ if space != _space
226
+ # deprecated
144
227
  end
145
228
  end
229
+
230
+ keys = _assert_options(space, options)
231
+
232
+ color = _to_space(space)
233
+
234
+ changed_color = if space_set_explictly
235
+ Color.send(:for_space_internal,
236
+ space,
237
+ options.fetch(keys[0], color.channel0_or_nil),
238
+ options.fetch(keys[1], color.channel1_or_nil),
239
+ options.fetch(keys[2], color.channel2_or_nil),
240
+ options.fetch(:alpha, color.alpha_or_nil))
241
+ else
242
+ changed_channel0_or_nil = options[keys[0]]
243
+ changed_channel1_or_nil = options[keys[1]]
244
+ changed_channel2_or_nil = options[keys[2]]
245
+ changed_alpha_or_nil = options[:alpha]
246
+ Color.send(:for_space_internal,
247
+ space,
248
+ changed_channel0_or_nil.nil? ? color.channel0_or_nil : changed_channel0_or_nil,
249
+ changed_channel1_or_nil.nil? ? color.channel1_or_nil : changed_channel1_or_nil,
250
+ changed_channel2_or_nil.nil? ? color.channel2_or_nil : changed_channel2_or_nil,
251
+ changed_alpha_or_nil.nil? ? color.alpha_or_nil : changed_alpha_or_nil)
252
+ end
253
+
254
+ changed_color._to_space(_space)
255
+ end
256
+
257
+ # return [Numeric]
258
+ def alpha
259
+ @alpha_or_nil.nil? ? 0 : @alpha_or_nil
260
+ end
261
+
262
+ # return [::Boolean]
263
+ def legacy?
264
+ _space.legacy?
265
+ end
266
+
267
+ # @deprecated
268
+ # @return [Numeric]
269
+ def red
270
+ _to_space(Space::RGB).channel('red').round
271
+ end
272
+
273
+ # @deprecated
274
+ # @return [Numeric]
275
+ def green
276
+ _to_space(Space::RGB).channel('green').round
277
+ end
278
+
279
+ # @deprecated
280
+ # @return [Numeric]
281
+ def blue
282
+ _to_space(Space::RGB).channel('blue').round
283
+ end
284
+
285
+ # @deprecated
286
+ # @return [Numeric]
287
+ def hue
288
+ _to_space(Space::HSL).channel('hue')
289
+ end
290
+
291
+ # @deprecated
292
+ # @return [Numeric]
293
+ def saturation
294
+ _to_space(Space::HSL).channel('saturation')
295
+ end
296
+
297
+ # @deprecated
298
+ # @return [Numeric]
299
+ def lightness
300
+ _to_space(Space::HSL).channel('lightness')
301
+ end
302
+
303
+ # @deprecated
304
+ # @return [Numeric]
305
+ def whiteness
306
+ _to_space(Space::HWB).channel('whiteness')
307
+ end
308
+
309
+ # @deprecated
310
+ # @return [Numeric]
311
+ def blackness
312
+ _to_space(Space::HWB).channel('blackness')
146
313
  end
147
314
 
148
315
  # @return [::Boolean]
149
316
  def ==(other)
150
- other.is_a?(Sass::Value::Color) &&
151
- other.red == red &&
152
- other.green == green &&
153
- other.blue == blue &&
154
- other.alpha == alpha
317
+ return false unless other.is_a?(Sass::Value::Color)
318
+
319
+ if legacy?
320
+ return false unless other.legacy?
321
+ return false unless FuzzyMath.equals_nilable(other.alpha_or_nil, alpha_or_nil)
322
+
323
+ if _space == other._space
324
+ FuzzyMath.equals_nilable(other.channel0_or_nil, channel0_or_nil) &&
325
+ FuzzyMath.equals_nilable(other.channel1_or_nil, channel1_or_nil) &&
326
+ FuzzyMath.equals_nilable(other.channel2_or_nil, channel2_or_nil)
327
+ else
328
+ _to_space(Space::RGB) == other._to_space(Space::RGB)
329
+ end
330
+ else
331
+ other._space == _space &&
332
+ FuzzyMath.equals_nilable(other.channel0_or_nil, channel0_or_nil) &&
333
+ FuzzyMath.equals_nilable(other.channel1_or_nil, channel1_or_nil) &&
334
+ FuzzyMath.equals_nilable(other.channel2_or_nil, channel2_or_nil) &&
335
+ FuzzyMath.equals_nilable(other.alpha_or_nil, alpha_or_nil)
336
+ end
155
337
  end
156
338
 
157
339
  # @return [Integer]
158
340
  def hash
159
- @hash ||= [red, green, blue, alpha].hash
341
+ @hash ||= [
342
+ _space,
343
+ channel0_or_nil&.finite? ? (channel0_or_nil * FuzzyMath::INVERSE_EPSILON).round : channel0_or_nil, # rubocop:disable Security/CompoundHash
344
+ channel1_or_nil&.finite? ? (channel1_or_nil * FuzzyMath::INVERSE_EPSILON).round : channel1_or_nil, # rubocop:disable Security/CompoundHash
345
+ channel2_or_nil&.finite? ? (channel2_or_nil * FuzzyMath::INVERSE_EPSILON).round : channel2_or_nil, # rubocop:disable Security/CompoundHash
346
+ alpha_or_nil&.finite? ? (alpha_or_nil * FuzzyMath::INVERSE_EPSILON).round : alpha_or_nil # rubocop:disable Security/CompoundHash
347
+ ].hash
160
348
  end
161
349
 
162
350
  # @return [Color]
@@ -164,89 +352,276 @@ module Sass
164
352
  self
165
353
  end
166
354
 
355
+ protected
356
+
357
+ attr_reader :channel0_or_nil, :channel1_or_nil, :channel2_or_nil, :alpha_or_nil
358
+
359
+ def channel0
360
+ @channel0_or_nil.nil? ? 0 : @channel0_or_nil
361
+ end
362
+
363
+ def channel0_missing?
364
+ @channel0_or_nil.nil?
365
+ end
366
+
367
+ def channel0_powerless?
368
+ case _space
369
+ when Space::HSL
370
+ FuzzyMath.equals(channel1, 0)
371
+ when Space::HWB
372
+ FuzzyMath.greater_than_or_equals(channel1 + channel2, 100)
373
+ else
374
+ false
375
+ end
376
+ end
377
+
378
+ def channel1
379
+ @channel1_or_nil.nil? ? 0 : @channel1_or_nil
380
+ end
381
+
382
+ def channel1_missing?
383
+ @channel1_or_nil.nil?
384
+ end
385
+
386
+ def channel1_powerless?
387
+ false
388
+ end
389
+
390
+ def channel2
391
+ @channel2_or_nil.nil? ? 0 : @channel2_or_nil
392
+ end
393
+
394
+ def channel2_missing?
395
+ @channel2_or_nil.nil?
396
+ end
397
+
398
+ def channel2_powerless?
399
+ case _space
400
+ when Space::LCH, Space::OKLCH
401
+ FuzzyMath.equals(channel1, 0)
402
+ else
403
+ false
404
+ end
405
+ end
406
+
407
+ def alpha_missing?
408
+ @alpha_or_nil.nil?
409
+ end
410
+
411
+ def _space
412
+ @space
413
+ end
414
+
415
+ def _to_space(space)
416
+ return self if _space == space
417
+
418
+ _space.convert(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
419
+ end
420
+
421
+ def _in_gamut?
422
+ return true unless _space.bounded?
423
+
424
+ _is_channel_in_gamut(channel0, _space.channels[0]) &&
425
+ _is_channel_in_gamut(channel1, _space.channels[1]) &&
426
+ _is_channel_in_gamut(channel2, _space.channels[2])
427
+ end
428
+
429
+ def _to_gamut(method)
430
+ _in_gamut? ? self : method.map(self)
431
+ end
432
+
167
433
  private
168
434
 
169
- def rgb_to_hsl
170
- scaled_red = Rational(red, 255)
171
- scaled_green = Rational(green, 255)
172
- scaled_blue = Rational(blue, 255)
173
-
174
- max = [scaled_red, scaled_green, scaled_blue].max
175
- min = [scaled_red, scaled_green, scaled_blue].min
176
- delta = max - min
177
-
178
- if max == min
179
- @hue = 0
180
- elsif max == scaled_red
181
- @hue = ((scaled_green - scaled_blue) * 60 / delta) % 360
182
- elsif max == scaled_green
183
- @hue = (((scaled_blue - scaled_red) * 60 / delta) + 120) % 360
184
- elsif max == scaled_blue
185
- @hue = (((scaled_red - scaled_green) * 60 / delta) + 240) % 360
435
+ def _assert_options(space, options)
436
+ keys = space.channels.map do |channel|
437
+ channel.name.to_sym
438
+ end << :alpha << :space
439
+ options.each_key do |key|
440
+ unless keys.include?(key)
441
+ raise Sass::ScriptError.new("`#{key}` is not a valid channel in `#{space.name}`.", key)
442
+ end
186
443
  end
444
+ keys
445
+ end
187
446
 
188
- lightness = @lightness = (max + min) * 50
189
-
190
- @saturation = if max == min
191
- 0
192
- elsif lightness < 50
193
- delta * 100 / (max + min)
194
- else
195
- delta * 100 / (2 - max - min)
196
- end
197
- end
198
-
199
- def hsl_to_rgb
200
- scaled_hue = Rational(hue, 360)
201
- scaled_saturation = Rational(saturation, 100)
202
- scaled_lightness = Rational(lightness, 100)
203
-
204
- tmp2 = if scaled_lightness <= 0.5
205
- scaled_lightness * (scaled_saturation + 1)
206
- else
207
- scaled_lightness + scaled_saturation - (scaled_lightness * scaled_saturation)
208
- end
209
- tmp1 = (scaled_lightness * 2) - tmp2
210
- @red = FuzzyMath.round(hsl_hue_to_rgb(tmp1, tmp2, scaled_hue + Rational(1, 3)) * 255)
211
- @green = FuzzyMath.round(hsl_hue_to_rgb(tmp1, tmp2, scaled_hue) * 255)
212
- @blue = FuzzyMath.round(hsl_hue_to_rgb(tmp1, tmp2, scaled_hue - Rational(1, 3)) * 255)
213
- end
214
-
215
- def hsl_hue_to_rgb(tmp1, tmp2, hue)
216
- hue += 1 if hue.negative?
217
- hue -= 1 if hue > 1
218
-
219
- if hue < Rational(1, 6)
220
- tmp1 + ((tmp2 - tmp1) * hue * 6)
221
- elsif hue < Rational(1, 2)
222
- tmp2
223
- elsif hue < Rational(2, 3)
224
- tmp1 + ((tmp2 - tmp1) * (Rational(2, 3) - hue) * 6)
447
+ def _initialize_for_space_internal(space, channel0, channel1, channel2, alpha = 1)
448
+ case space
449
+ when Space::HSL
450
+ _initialize_for_space(
451
+ space,
452
+ _normalize_hue(channel0, invert: !channel1.nil? && FuzzyMath.less_than(channel1, 0)),
453
+ channel1&.abs,
454
+ channel2,
455
+ alpha
456
+ )
457
+ when Space::HWB
458
+ _initialize_for_space(space, _normalize_hue(channel0, invert: false), channel1, channel2, alpha)
459
+ when Space::LCH, Space::OKLCH
460
+ _initialize_for_space(
461
+ space,
462
+ channel0,
463
+ channel1&.abs,
464
+ _normalize_hue(channel2, invert: !channel1.nil? && FuzzyMath.less_than(channel1, 0)),
465
+ alpha
466
+ )
225
467
  else
226
- tmp1
468
+ _initialize_for_space(space, channel0, channel1, channel2, alpha)
227
469
  end
228
470
  end
229
471
 
230
- def hwb_to_rgb
231
- scaled_hue = Rational(hue, 360)
232
- scaled_whiteness = Rational(whiteness, 100)
233
- scaled_blackness = Rational(blackness, 100)
472
+ def _initialize_for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
473
+ @space = space
474
+ @channel0_or_nil = channel0_or_nil
475
+ @channel1_or_nil = channel1_or_nil
476
+ @channel2_or_nil = channel2_or_nil
477
+ @alpha_or_nil = alpha
478
+
479
+ FuzzyMath.assert_between(@alpha_or_nil, 0, 1, 'alpha') unless @alpha_or_nil.nil?
480
+ end
481
+
482
+ def _normalize_hue(hue, invert:)
483
+ return hue if hue.nil?
234
484
 
235
- sum = scaled_whiteness + scaled_blackness
236
- if sum > 1
237
- scaled_whiteness /= sum
238
- scaled_blackness /= sum
485
+ ((hue % 360) + 360 + (invert ? 180 : 0)) % 360
486
+ end
487
+
488
+ def _is_channel_in_gamut(value, channel)
489
+ case channel
490
+ when LinearChannel
491
+ FuzzyMath.less_than_or_equals(value, channel.max) && FuzzyMath.greater_than_or_equals(value, channel.min)
492
+ else
493
+ true
239
494
  end
495
+ end
240
496
 
241
- factor = 1 - scaled_whiteness - scaled_blackness
242
- @red = hwb_hue_to_rgb(factor, scaled_whiteness, scaled_hue + Rational(1, 3))
243
- @green = hwb_hue_to_rgb(factor, scaled_whiteness, scaled_hue)
244
- @blue = hwb_hue_to_rgb(factor, scaled_whiteness, scaled_hue - Rational(1, 3))
497
+ def _interpolate(other, method, weight: nil)
498
+ weight = 0.5 if weight.nil?
499
+ if weight.negative? || weight > 1
500
+ raise Sass::ScriptError.new("Expected #{wieght} to be within 0 and 1.", 'weight')
501
+ end
502
+
503
+ return other if FuzzyMath.equals(weight, 0)
504
+ return self if FuzzyMath.equals(weight, 1)
505
+
506
+ color1 = _to_space(method.space)
507
+ color2 = other._to_space(method.space)
508
+
509
+ c1_missing0 = _analogous_channel_missing?(self, color1, 0)
510
+ c1_missing1 = _analogous_channel_missing?(self, color1, 1)
511
+ c1_missing2 = _analogous_channel_missing?(self, color1, 2)
512
+ c2_missing0 = _analogous_channel_missing?(other, color2, 0)
513
+ c2_missing1 = _analogous_channel_missing?(other, color2, 1)
514
+ c2_missing2 = _analogous_channel_missing?(other, color2, 2)
515
+ c1_channel0 = (c1_missing0 ? color2 : color1).channel0
516
+ c1_channel1 = (c1_missing1 ? color2 : color1).channel1
517
+ c1_channel2 = (c1_missing2 ? color2 : color1).channel2
518
+ c2_channel0 = (c2_missing0 ? color1 : color2).channel0
519
+ c2_channel1 = (c2_missing1 ? color1 : color2).channel1
520
+ c2_channel2 = (c2_missing2 ? color1 : color2).channel2
521
+ c1_alpha = alpha_or_nil.nil? ? other.alpha : alpha_or_nil
522
+ c2_alpha = other.alpha_or_nil.nil? ? alpha : other.alpha_or_nil
523
+
524
+ c1_multiplier = (alpha_or_nil.nil? ? 1 : alpha_or_nil) * weight
525
+ c2_multiplier = (other.alpha_or_nil.nil? ? 1 : other.alpha_or_nil) * (1 - weight)
526
+ mixed_alpha = alpha_missing? && other.alpha_missing? ? nil : (c1_alpha * weight) + (c2_alpha * (1 - weight))
527
+ mixed0 = if c1_missing0 && c2_missing0
528
+ nil
529
+ else
530
+ ((c1_channel0 * c1_multiplier) + (c2_channel0 * c2_multiplier)) /
531
+ (mixed_alpha.nil? ? 1 : mixed_alpha)
532
+ end
533
+ mixed1 = if c1_missing1 && c2_missing1
534
+ nil
535
+ else
536
+ ((c1_channel1 * c1_multiplier) + (c2_channel1 * c2_multiplier)) /
537
+ (mixed_alpha.nil? ? 1 : mixed_alpha)
538
+ end
539
+ mixed2 = if c1_missing2 && c2_missing2
540
+ nil
541
+ else
542
+ ((c1_channel2 * c1_multiplier) + (c2_channel2 * c2_multiplier)) /
543
+ (mixed_alpha.nil? ? 1 : mixed_alpha)
544
+ end
545
+
546
+ case method.space
547
+ when Space::HSL, Space::HWB
548
+ Color.send(:for_space_internal,
549
+ method.space,
550
+ c1_missing0 && c2_missing0 ? nil : _interpolate_hues(c1_channel0, c2_channel0, method.hue, weight),
551
+ mixed1,
552
+ mixed2,
553
+ mixed_alpha)
554
+ when Space::LCH, Space::OKLCH
555
+ Color.send(:for_space_internal,
556
+ method.space,
557
+ mixed0,
558
+ mixed1,
559
+ c1_missing2 && c2_missing2 ? nil : _interpolate_hues(c1_channel2, c2_channel2, method.hue, weight),
560
+ mixed_alpha)
561
+ else
562
+ Color.send(:_for_space,
563
+ method.space, mixed0, mixed1, mixed2, mixed_alpha)
564
+ end._to_space(_space)
245
565
  end
246
566
 
247
- def hwb_hue_to_rgb(factor, scaled_whiteness, scaled_hue)
248
- channel = (hsl_hue_to_rgb(0, 1, scaled_hue) * factor) + scaled_whiteness
249
- FuzzyMath.round(channel * 255)
567
+ def _analogous_channel_missing?(original, output, output_channel_index)
568
+ return true if output.channels_or_nil[output_channel_index].nil?
569
+
570
+ return false if original.equal?(output)
571
+
572
+ output_channel = output.space.channels[output_channel_index]
573
+ original_channel = original.space.channels.find do |channel|
574
+ output_channel.analogous?(channel)
575
+ end
576
+
577
+ return false if original_channel.nil?
578
+
579
+ original.channel_missing?(original_channel.name)
580
+ end
581
+
582
+ def _interpolate_hues(hue1, hue2, method, weight)
583
+ case method
584
+ when :shorter
585
+ diff = hue2 - hue1
586
+ if diff > 180
587
+ hue1 += 360
588
+ elsif diff < -180
589
+ hue2 += 360
590
+ end
591
+ when :longer
592
+ diff = hue2 - hue1
593
+ if diff.positive? && diff < 180
594
+ hue2 += 360
595
+ elsif diff > -180 && diff <= 0
596
+ hue1 += 360
597
+ end
598
+ when :increasing
599
+ hue2 += 360 if hue2 < hue1
600
+ when :decreasing
601
+ hue1 += 360 if hue1 < hue2
602
+ end
603
+
604
+ (hue1 * weight) + (hue2 * (1 - weight))
605
+ end
606
+
607
+ class << self
608
+ private
609
+
610
+ def for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
611
+ _for_space(Space.from_name(space), channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
612
+ end
613
+
614
+ def for_space_internal(space, channel0, channel1, channel2, alpha)
615
+ o = allocate
616
+ o.send(:_initialize_for_space_internal, space, channel0, channel1, channel2, alpha)
617
+ o
618
+ end
619
+
620
+ def _for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
621
+ o = allocate
622
+ o.send(:_initialize_for_space, space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
623
+ o
624
+ end
250
625
  end
251
626
  end
252
627
  end