unmagic-color 0.1.0
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 +7 -0
- data/README.md +274 -0
- data/data/rgb.txt +164 -0
- data/lib/unmagic/color/hsl.rb +421 -0
- data/lib/unmagic/color/oklch.rb +433 -0
- data/lib/unmagic/color/rgb/hex.rb +104 -0
- data/lib/unmagic/color/rgb/named.rb +112 -0
- data/lib/unmagic/color/rgb.rb +390 -0
- data/lib/unmagic/color/string/hash_function.rb +316 -0
- data/lib/unmagic/color/util/percentage.rb +143 -0
- data/lib/unmagic/color.rb +402 -0
- data/lib/unmagic_color.rb +3 -0
- metadata +55 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
class Color
|
|
5
|
+
# `HSL` (Hue, Saturation, Lightness) color representation.
|
|
6
|
+
#
|
|
7
|
+
# ## Understanding HSL
|
|
8
|
+
#
|
|
9
|
+
# While {RGB} describes colors as mixing light, HSL describes colors in a way that's
|
|
10
|
+
# more intuitive to humans. It separates the "what color" from "how vibrant" and "how bright."
|
|
11
|
+
#
|
|
12
|
+
# ## The Three Components
|
|
13
|
+
#
|
|
14
|
+
# 1. **Hue** (`0-360°`): The actual color on the color wheel
|
|
15
|
+
# - `0°/360°` = Red
|
|
16
|
+
# - `60°` = Yellow
|
|
17
|
+
# - `120°` = Green
|
|
18
|
+
# - `180°` = Cyan
|
|
19
|
+
# - `240°` = Blue
|
|
20
|
+
# - `300°` = Magenta
|
|
21
|
+
#
|
|
22
|
+
# Think of it as rotating around a circle of colors.
|
|
23
|
+
#
|
|
24
|
+
# 2. **Saturation** (`0-100%`): How pure/intense the color is
|
|
25
|
+
# - `0%` = Gray (no color, just brightness)
|
|
26
|
+
# - `50%` = Moderate color
|
|
27
|
+
# - `100%` = Full, vivid color
|
|
28
|
+
#
|
|
29
|
+
# Think of it as "how much color" vs "how much gray."
|
|
30
|
+
#
|
|
31
|
+
# 3. **Lightness** (`0-100%`): How bright the color is
|
|
32
|
+
# - `0%` = Black (no light)
|
|
33
|
+
# - `50%` = Pure color
|
|
34
|
+
# - `100%` = White (full light)
|
|
35
|
+
#
|
|
36
|
+
# Think of it as a dimmer switch.
|
|
37
|
+
#
|
|
38
|
+
# ## Why HSL is Useful
|
|
39
|
+
#
|
|
40
|
+
# HSL makes it easy to:
|
|
41
|
+
#
|
|
42
|
+
# - Create color variations (keep hue, adjust saturation/lightness)
|
|
43
|
+
# - Generate color schemes (change hue by fixed amounts)
|
|
44
|
+
# - Make colors lighter/darker without changing their "color-ness"
|
|
45
|
+
#
|
|
46
|
+
# ## Common Patterns
|
|
47
|
+
#
|
|
48
|
+
# - **Pastel colors**: High lightness, medium-low saturation (`70-80% L`, `30-50% S`)
|
|
49
|
+
# - **Vibrant colors**: Medium lightness, high saturation (`50% L`, `80-100% S`)
|
|
50
|
+
# - **Dark colors**: Low lightness, any saturation (`20-30% L`)
|
|
51
|
+
# - **Muted colors**: Medium lightness and saturation (`40-60% L`, `30-50% S`)
|
|
52
|
+
#
|
|
53
|
+
# ## Examples
|
|
54
|
+
#
|
|
55
|
+
# # Parse HSL colors
|
|
56
|
+
# color = Unmagic::Color::HSL.parse("hsl(120, 100%, 50%)") # Pure green
|
|
57
|
+
# color = Unmagic::Color::HSL.parse("240, 50%, 75%") # Light blue
|
|
58
|
+
#
|
|
59
|
+
# # Create directly
|
|
60
|
+
# red = Unmagic::Color::HSL.new(hue: 0, saturation: 100, lightness: 50)
|
|
61
|
+
# pastel = Unmagic::Color::HSL.new(hue: 180, saturation: 40, lightness: 80)
|
|
62
|
+
#
|
|
63
|
+
# # Access components
|
|
64
|
+
# color.hue.value #=> 120 (degrees)
|
|
65
|
+
# color.saturation.value #=> 100 (percent)
|
|
66
|
+
# color.lightness.value #=> 50 (percent)
|
|
67
|
+
#
|
|
68
|
+
# # Easy color variations
|
|
69
|
+
# lighter = color.lighten(0.2) # Increase lightness
|
|
70
|
+
# muted = color.desaturate(0.3) # Reduce saturation
|
|
71
|
+
#
|
|
72
|
+
# # Generate color from text
|
|
73
|
+
# Unmagic::Color::HSL.derive("user@example.com".hash) # Consistent color
|
|
74
|
+
class HSL < Color
|
|
75
|
+
# Error raised when parsing HSL color strings fails
|
|
76
|
+
class ParseError < Color::Error; end
|
|
77
|
+
|
|
78
|
+
attr_reader :hue, :saturation, :lightness
|
|
79
|
+
|
|
80
|
+
# Create a new HSL color.
|
|
81
|
+
#
|
|
82
|
+
# @param hue [Numeric] Hue in degrees (0-360), wraps around if outside range
|
|
83
|
+
# @param saturation [Numeric] Saturation percentage (0-100), clamped to range
|
|
84
|
+
# @param lightness [Numeric] Lightness percentage (0-100), clamped to range
|
|
85
|
+
#
|
|
86
|
+
# @example Create a pure red
|
|
87
|
+
# HSL.new(hue: 0, saturation: 100, lightness: 50)
|
|
88
|
+
#
|
|
89
|
+
# @example Create a pastel blue
|
|
90
|
+
# HSL.new(hue: 240, saturation: 40, lightness: 80)
|
|
91
|
+
def initialize(hue:, saturation:, lightness:)
|
|
92
|
+
super()
|
|
93
|
+
@hue = Color::Hue.new(value: hue)
|
|
94
|
+
@saturation = Color::Saturation.new(saturation)
|
|
95
|
+
@lightness = Color::Lightness.new(lightness)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class << self
|
|
99
|
+
# Parse an HSL color from a string.
|
|
100
|
+
#
|
|
101
|
+
# Accepts formats:
|
|
102
|
+
# - CSS format: "hsl(120, 100%, 50%)"
|
|
103
|
+
# - Raw values: "120, 100%, 50%" or "120, 100, 50"
|
|
104
|
+
# - Percentages optional for saturation and lightness
|
|
105
|
+
#
|
|
106
|
+
# @param input [String] The HSL color string to parse
|
|
107
|
+
# @return [HSL] The parsed HSL color
|
|
108
|
+
# @raise [ParseError] If the input format is invalid or values are out of range
|
|
109
|
+
#
|
|
110
|
+
# @example Parse CSS format
|
|
111
|
+
# HSL.parse("hsl(120, 100%, 50%)")
|
|
112
|
+
#
|
|
113
|
+
# @example Parse without function wrapper
|
|
114
|
+
# HSL.parse("240, 50%, 75%")
|
|
115
|
+
def parse(input)
|
|
116
|
+
raise ParseError, "Input must be a string" unless input.is_a?(::String)
|
|
117
|
+
|
|
118
|
+
# Remove hsl() wrapper if present
|
|
119
|
+
clean = input.gsub(/^hsl\s*\(\s*|\s*\)$/, "").strip
|
|
120
|
+
|
|
121
|
+
# Split and parse values
|
|
122
|
+
parts = clean.split(/\s*,\s*/)
|
|
123
|
+
unless parts.length == 3
|
|
124
|
+
raise ParseError, "Expected 3 HSL values, got #{parts.length}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Check if hue is numeric
|
|
128
|
+
h_str = parts[0].strip
|
|
129
|
+
unless h_str.match?(/\A\d+(\.\d+)?\z/)
|
|
130
|
+
raise ParseError, "Invalid hue value: #{h_str.inspect} (must be a number)"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if saturation and lightness are numeric (with optional %)
|
|
134
|
+
s_str = parts[1].gsub("%", "").strip
|
|
135
|
+
l_str = parts[2].gsub("%", "").strip
|
|
136
|
+
|
|
137
|
+
unless s_str.match?(/\A\d+(\.\d+)?\z/)
|
|
138
|
+
raise ParseError, "Invalid saturation value: #{parts[1].inspect} (must be a number with optional %)"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
unless l_str.match?(/\A\d+(\.\d+)?\z/)
|
|
142
|
+
raise ParseError, "Invalid lightness value: #{parts[2].inspect} (must be a number with optional %)"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
h = h_str.to_f
|
|
146
|
+
s = s_str.to_f
|
|
147
|
+
l = l_str.to_f
|
|
148
|
+
|
|
149
|
+
# Validate ranges
|
|
150
|
+
if h < 0 || h > 360
|
|
151
|
+
raise ParseError, "Hue must be between 0 and 360, got #{h}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if s < 0 || s > 100
|
|
155
|
+
raise ParseError, "Saturation must be between 0 and 100, got #{s}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
if l < 0 || l > 100
|
|
159
|
+
raise ParseError, "Lightness must be between 0 and 100, got #{l}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
new(hue: h, saturation: s, lightness: l)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Generate a deterministic HSL color from an integer seed.
|
|
166
|
+
#
|
|
167
|
+
# Creates visually distinct, consistent colors from hash values. Particularly
|
|
168
|
+
# useful because HSL naturally spreads colors evenly around the color wheel.
|
|
169
|
+
#
|
|
170
|
+
# @param seed [Integer] The seed value (typically from a hash function)
|
|
171
|
+
# @param lightness [Numeric] Fixed lightness percentage (0-100, default 50)
|
|
172
|
+
# @param saturation_range [Range] Range for saturation variation (default 40..80)
|
|
173
|
+
# @return [HSL] A deterministic color based on the seed
|
|
174
|
+
# @raise [ArgumentError] If seed is not an integer
|
|
175
|
+
#
|
|
176
|
+
# @example Generate user avatar color
|
|
177
|
+
# user_color = HSL.derive("alice@example.com".hash)
|
|
178
|
+
#
|
|
179
|
+
# @example Generate lighter colors
|
|
180
|
+
# HSL.derive(12345, lightness: 70)
|
|
181
|
+
#
|
|
182
|
+
# @example Generate muted colors
|
|
183
|
+
# HSL.derive(12345, saturation_range: (20..40))
|
|
184
|
+
def derive(seed, lightness: 50, saturation_range: (40..80))
|
|
185
|
+
raise ArgumentError, "Seed must be an integer" unless seed.is_a?(Integer)
|
|
186
|
+
|
|
187
|
+
h32 = seed & 0xFFFFFFFF # Ensure 32-bit
|
|
188
|
+
|
|
189
|
+
# Hue: distribute evenly across the color wheel
|
|
190
|
+
h = (h32 % 360).to_f
|
|
191
|
+
|
|
192
|
+
# Saturation: map a byte into the provided range
|
|
193
|
+
s = saturation_range.begin + ((h32 >> 8) & 0xFF) / 255.0 * (saturation_range.end - saturation_range.begin)
|
|
194
|
+
|
|
195
|
+
new(hue: h, saturation: s, lightness: lightness)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Convert to HSL color space.
|
|
200
|
+
#
|
|
201
|
+
# Since this is already an HSL color, returns self.
|
|
202
|
+
#
|
|
203
|
+
# @return [HSL] self
|
|
204
|
+
def to_hsl
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Convert to RGB color space.
|
|
209
|
+
#
|
|
210
|
+
# @return [RGB] The color in RGB color space
|
|
211
|
+
def to_rgb
|
|
212
|
+
rgb = hsl_to_rgb
|
|
213
|
+
require_relative "rgb"
|
|
214
|
+
Unmagic::Color::RGB.new(red: rgb[0], green: rgb[1], blue: rgb[2])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Convert to OKLCH color space.
|
|
218
|
+
#
|
|
219
|
+
# Converts via RGB as an intermediate step.
|
|
220
|
+
#
|
|
221
|
+
# @return [OKLCH] The color in OKLCH color space
|
|
222
|
+
def to_oklch
|
|
223
|
+
to_rgb.to_oklch
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Calculate the relative luminance.
|
|
227
|
+
#
|
|
228
|
+
# Converts to RGB first, then calculates luminance.
|
|
229
|
+
#
|
|
230
|
+
# @return [Float] Luminance from 0.0 (black) to 1.0 (white)
|
|
231
|
+
def luminance
|
|
232
|
+
to_rgb.luminance
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Blend this color with another color in HSL space.
|
|
236
|
+
#
|
|
237
|
+
# Blending in HSL can produce different results than RGB blending,
|
|
238
|
+
# often creating more natural-looking color transitions.
|
|
239
|
+
#
|
|
240
|
+
# @param other [Color] The color to blend with (automatically converted to HSL)
|
|
241
|
+
# @param amount [Float] How much of the other color to mix in (0.0-1.0)
|
|
242
|
+
# @return [HSL] A new HSL color that is a blend of the two
|
|
243
|
+
#
|
|
244
|
+
# @example Create a color halfway between red and blue
|
|
245
|
+
# red = HSL.new(hue: 0, saturation: 100, lightness: 50)
|
|
246
|
+
# blue = HSL.new(hue: 240, saturation: 100, lightness: 50)
|
|
247
|
+
# purple = red.blend(blue, 0.5)
|
|
248
|
+
def blend(other, amount = 0.5)
|
|
249
|
+
amount = amount.to_f.clamp(0, 1)
|
|
250
|
+
other_hsl = other.respond_to?(:to_hsl) ? other.to_hsl : other
|
|
251
|
+
|
|
252
|
+
# Blend in HSL space
|
|
253
|
+
new_hue = @hue.value * (1 - amount) + other_hsl.hue.value * amount
|
|
254
|
+
new_saturation = @saturation.value * (1 - amount) + other_hsl.saturation.value * amount
|
|
255
|
+
new_lightness = @lightness.value * (1 - amount) + other_hsl.lightness.value * amount
|
|
256
|
+
|
|
257
|
+
Unmagic::Color::HSL.new(hue: new_hue, saturation: new_saturation, lightness: new_lightness)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Create a lighter version by increasing lightness.
|
|
261
|
+
#
|
|
262
|
+
# In HSL, lightening moves the color toward white while preserving the hue.
|
|
263
|
+
# The amount determines how far to move from the current lightness toward 100%.
|
|
264
|
+
#
|
|
265
|
+
# @param amount [Float] How much to lighten (0.0-1.0, default 0.1)
|
|
266
|
+
# @return [HSL] A lighter version of this color
|
|
267
|
+
#
|
|
268
|
+
# @example Make a color 30% lighter
|
|
269
|
+
# dark = HSL.new(hue: 240, saturation: 80, lightness: 30)
|
|
270
|
+
# light = dark.lighten(0.3)
|
|
271
|
+
def lighten(amount = 0.1)
|
|
272
|
+
amount = amount.to_f.clamp(0, 1)
|
|
273
|
+
new_lightness = @lightness.value + (100 - @lightness.value) * amount
|
|
274
|
+
Unmagic::Color::HSL.new(hue: @hue.value, saturation: @saturation.value, lightness: new_lightness)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Create a darker version by decreasing lightness.
|
|
278
|
+
#
|
|
279
|
+
# In HSL, darkening moves the color toward black while preserving the hue.
|
|
280
|
+
# The amount determines how much to reduce the current lightness toward 0%.
|
|
281
|
+
#
|
|
282
|
+
# @param amount [Float] How much to darken (0.0-1.0, default 0.1)
|
|
283
|
+
# @return [HSL] A darker version of this color
|
|
284
|
+
#
|
|
285
|
+
# @example Make a color 20% darker
|
|
286
|
+
# bright = HSL.new(hue: 60, saturation: 100, lightness: 70)
|
|
287
|
+
# subdued = bright.darken(0.2)
|
|
288
|
+
def darken(amount = 0.1)
|
|
289
|
+
amount = amount.to_f.clamp(0, 1)
|
|
290
|
+
new_lightness = @lightness.value * (1 - amount)
|
|
291
|
+
Unmagic::Color::HSL.new(hue: @hue.value, saturation: @saturation.value, lightness: new_lightness)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Check if two HSL colors are equal.
|
|
295
|
+
#
|
|
296
|
+
# @param other [Object] The object to compare with
|
|
297
|
+
# @return [Boolean] true if both colors have the same HSL values
|
|
298
|
+
def ==(other)
|
|
299
|
+
other.is_a?(Unmagic::Color::HSL) &&
|
|
300
|
+
lightness == other.lightness &&
|
|
301
|
+
saturation == other.saturation &&
|
|
302
|
+
hue == other.hue
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Generate a progression of colors by varying lightness and saturation.
|
|
306
|
+
#
|
|
307
|
+
# This creates an array of related colors, useful for color scales in UI design
|
|
308
|
+
# (like shades of blue from light to dark).
|
|
309
|
+
#
|
|
310
|
+
# The lightness and saturation can be provided as:
|
|
311
|
+
# - Array: Specific values for each step (last value repeats if array is shorter)
|
|
312
|
+
# - Proc: Dynamic calculation based on the base color and step index
|
|
313
|
+
#
|
|
314
|
+
# @param steps [Integer] Number of colors to generate (must be at least 1)
|
|
315
|
+
# @param lightness [Array<Numeric>, Proc] Lightness values or calculation function
|
|
316
|
+
# @param saturation [Array<Numeric>, Proc, nil] Optional saturation values or function
|
|
317
|
+
# @return [Array<HSL>] Array of HSL colors in the progression
|
|
318
|
+
# @raise [ArgumentError] If steps < 1 or lightness/saturation are invalid types
|
|
319
|
+
#
|
|
320
|
+
# @example Create a 5-step lightness progression
|
|
321
|
+
# base = Unmagic::Color::HSL.new(hue: 240, saturation: 80, lightness: 50)
|
|
322
|
+
# base.progression(steps: 5, lightness: [20, 35, 50, 65, 80])
|
|
323
|
+
#
|
|
324
|
+
# @example Dynamic lightness calculation
|
|
325
|
+
# base = Unmagic::Color::HSL.new(hue: 240, saturation: 80, lightness: 50)
|
|
326
|
+
# base.progression(steps: 7, lightness: ->(hsl, i) { 20 + (i * 12) })
|
|
327
|
+
#
|
|
328
|
+
# @example Vary both lightness and saturation
|
|
329
|
+
# base = Unmagic::Color::HSL.new(hue: 240, saturation: 80, lightness: 50)
|
|
330
|
+
# base.progression(steps: 5, lightness: [30, 45, 60, 75, 90], saturation: [100, 80, 60, 40, 20])
|
|
331
|
+
def progression(steps:, lightness:, saturation: nil)
|
|
332
|
+
raise ArgumentError, "steps must be at least 1" if steps < 1
|
|
333
|
+
raise ArgumentError, "lightness must be a proc or array" unless lightness.respond_to?(:call) || lightness.is_a?(Array)
|
|
334
|
+
raise ArgumentError, "saturation must be a proc or array" if saturation && !saturation.respond_to?(:call) && !saturation.is_a?(Array)
|
|
335
|
+
|
|
336
|
+
colors = []
|
|
337
|
+
|
|
338
|
+
(0...steps).each do |i|
|
|
339
|
+
# Calculate new lightness using the provided proc or array
|
|
340
|
+
new_lightness = if lightness.is_a?(Array)
|
|
341
|
+
# Use array value at index i, or last value if beyond array length
|
|
342
|
+
lightness[i] || lightness.last
|
|
343
|
+
else
|
|
344
|
+
lightness.call(self, i)
|
|
345
|
+
end
|
|
346
|
+
new_lightness = new_lightness.to_f.clamp(0, 100)
|
|
347
|
+
|
|
348
|
+
# Calculate new saturation using the provided proc/array or keep current
|
|
349
|
+
new_saturation = if saturation
|
|
350
|
+
if saturation.is_a?(Array)
|
|
351
|
+
# Use array value at index i, or last value if beyond array length
|
|
352
|
+
(saturation[i] || saturation.last).to_f.clamp(0, 100)
|
|
353
|
+
else
|
|
354
|
+
saturation.call(self, i).to_f.clamp(0, 100)
|
|
355
|
+
end
|
|
356
|
+
else
|
|
357
|
+
@saturation.value
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Create new HSL color with computed values
|
|
361
|
+
color = self.class.new(hue: @hue.value, saturation: new_saturation, lightness: new_lightness)
|
|
362
|
+
colors << color
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
colors
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Convert to string representation.
|
|
369
|
+
#
|
|
370
|
+
# Returns the CSS hsl() function format.
|
|
371
|
+
#
|
|
372
|
+
# @return [String] HSL string like "hsl(240, 80%, 50%)"
|
|
373
|
+
#
|
|
374
|
+
# @example
|
|
375
|
+
# color = HSL.new(hue: 240, saturation: 80, lightness: 50)
|
|
376
|
+
# color.to_s
|
|
377
|
+
# # => "hsl(240, 80.0%, 50.0%)"
|
|
378
|
+
def to_s
|
|
379
|
+
"hsl(#{@hue.value.round}, #{@saturation.value}%, #{@lightness.value}%)"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private
|
|
383
|
+
|
|
384
|
+
def hsl_to_rgb
|
|
385
|
+
h = @hue.value / 360.0
|
|
386
|
+
s = @saturation.to_ratio # Convert percentage to 0-1
|
|
387
|
+
l = @lightness.to_ratio # Convert percentage to 0-1
|
|
388
|
+
|
|
389
|
+
if s == 0
|
|
390
|
+
# Achromatic
|
|
391
|
+
gray = (l * 255).round
|
|
392
|
+
[gray, gray, gray]
|
|
393
|
+
else
|
|
394
|
+
q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
|
395
|
+
p = 2 * l - q
|
|
396
|
+
|
|
397
|
+
r = hue_to_rgb(p, q, h + 1 / 3.0)
|
|
398
|
+
g = hue_to_rgb(p, q, h)
|
|
399
|
+
b = hue_to_rgb(p, q, h - 1 / 3.0)
|
|
400
|
+
|
|
401
|
+
[(r * 255).round, (g * 255).round, (b * 255).round]
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def hue_to_rgb(p, q, t)
|
|
406
|
+
t += 1 if t < 0
|
|
407
|
+
t -= 1 if t > 1
|
|
408
|
+
|
|
409
|
+
if t < 1 / 6.0
|
|
410
|
+
p + (q - p) * 6 * t
|
|
411
|
+
elsif t < 1 / 2.0
|
|
412
|
+
q
|
|
413
|
+
elsif t < 2 / 3.0
|
|
414
|
+
p + (q - p) * (2 / 3.0 - t) * 6
|
|
415
|
+
else
|
|
416
|
+
p
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|