Skip to content

Commit f292c3c

Browse files
authored
Merge pull request #80 from onevcat/feature/hsl-color-support
Add HSL Color Support
2 parents a58bc98 + f9f513e commit f292c3c

File tree

6 files changed

+399
-3
lines changed

6 files changed

+399
-3
lines changed

Playground/main.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ print("\("春色满园".hex("#ea517f", to: .bit24))关不住,\("一枝红杏".
2020

2121
print("")
2222

23+
print("\("天街".hsl(0, 0, 70))小雨润如酥,\("草色".hsl(120, 20, 80))遥看近却无")
24+
print("\("最是".hsl(90, 60, 70))一年春好处,绝胜\("烟柳".hsl(120, 30, 75))满皇都")
25+
26+
print("")
27+
2328
let output = "The quick brown fox jumps over the lazy dog"
2429
.applyingCodes(Color.red, BackgroundColor.yellow, Style.bold)
2530
print(output)

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ You could know more information on how to use Swift Package Manager in Apple's [
6767

6868
## Performance Optimization
6969

70-
Rainbow v4.2.0+ includes significant performance optimizations for high-frequency styling operations.
70+
Rainbow v4.2.0+ includes performance optimizations for high-frequency styling operations.
7171

72-
### Builder Pattern (578% faster for complex chaining)
72+
### Builder Pattern
7373

7474
For complex styling with multiple chained calls, use the builder pattern:
7575

@@ -81,7 +81,7 @@ let traditional = "Hello".red.bold.underline.onBlue
8181
let optimized = "Hello".styled.red.bold.underline.onBlue.build()
8282
```
8383

84-
### Batch Operations (264% faster for multiple styles)
84+
### Batch Operations
8585

8686
Apply multiple styles in a single operation:
8787

@@ -152,6 +152,17 @@ print("\("春色满园".hex("#ea517f", to: .bit24))关不住,\("一枝红杏".
152152

153153
![](https://user-images.githubusercontent.com/1019875/110496210-9d2c2600-8138-11eb-803d-15a745ef1dfb.png)
154154

155+
### HSL Colors
156+
157+
HSL (Hue, Saturation, Lightness) colors are also supported:
158+
159+
```swift
160+
print("天街小雨润如酥,草色遥看近却无".hsl(120, 20, 80))
161+
print("最是一年春好处,绝胜烟柳满皇都".hsl(90, 60, 70))
162+
```
163+
164+
> Format: `hue` (0-360°), `saturation` (0-100%), `lightness` (0-100%)
165+
155166
### Output Target
156167

157168
By default, Rainbow should be smart enough to detect the output target, to determine if it is a tty. For example, it

Rainbow.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@
6464
D1C5D8DE1C2ACA7A001CB619 /* ModesExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C5D8DD1C2ACA7A001CB619 /* ModesExtractor.swift */; };
6565
D1C5D8E01C2AD38A001CB619 /* StringGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C5D8DF1C2AD38A001CB619 /* StringGenerator.swift */; };
6666
D1C5F61A2E25614A00004F50 /* EdgeCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C5F6192E25614A00004F50 /* EdgeCaseTests.swift */; };
67+
D1F077292E27C68100EF60B9 /* HSLColorConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F077282E27C68100EF60B9 /* HSLColorConverter.swift */; };
68+
D1F0772A2E27C68100EF60B9 /* HSLColorConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F077282E27C68100EF60B9 /* HSLColorConverter.swift */; };
69+
D1F0772B2E27C68100EF60B9 /* HSLColorConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F077282E27C68100EF60B9 /* HSLColorConverter.swift */; };
70+
D1F0772C2E27C68100EF60B9 /* HSLColorConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F077282E27C68100EF60B9 /* HSLColorConverter.swift */; };
71+
D1F077302E27C6DF00EF60B9 /* HSLColorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F0772F2E27C6DF00EF60B9 /* HSLColorTests.swift */; };
6772
/* End PBXBuildFile section */
6873

6974
/* Begin PBXContainerItemProxy section */
@@ -104,6 +109,8 @@
104109
D1C5D8DD1C2ACA7A001CB619 /* ModesExtractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModesExtractor.swift; sourceTree = "<group>"; };
105110
D1C5D8DF1C2AD38A001CB619 /* StringGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringGenerator.swift; sourceTree = "<group>"; };
106111
D1C5F6192E25614A00004F50 /* EdgeCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EdgeCaseTests.swift; path = RainbowTests/EdgeCaseTests.swift; sourceTree = "<group>"; };
112+
D1F077282E27C68100EF60B9 /* HSLColorConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HSLColorConverter.swift; sourceTree = "<group>"; };
113+
D1F0772F2E27C6DF00EF60B9 /* HSLColorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HSLColorTests.swift; path = RainbowTests/HSLColorTests.swift; sourceTree = "<group>"; };
107114
/* End PBXFileReference section */
108115

109116
/* Begin PBXFrameworksBuildPhase section */
@@ -177,6 +184,7 @@
177184
D17B42EF25E891D5002914A0 /* ConsoleTextParserTests.swift */,
178185
D1A98FB01C299DF0000DDF20 /* ConsoleStringTests.swift */,
179186
D1B428C825F1CBA0009F1DD1 /* ColorApproximatedTests.swift */,
187+
D1F0772F2E27C6DF00EF60B9 /* HSLColorTests.swift */,
180188
D13C24C02E20F85B000690E1 /* PerformanceTests.swift */,
181189
D13C24C92E20FC16000690E1 /* StyledStringBuilderTests.swift */,
182190
D1C5F6192E25614A00004F50 /* EdgeCaseTests.swift */,
@@ -195,6 +203,7 @@
195203
4BC821A91C2928CF00F68E70 /* Sources */ = {
196204
isa = PBXGroup;
197205
children = (
206+
D1F077282E27C68100EF60B9 /* HSLColorConverter.swift */,
198207
D13C24C22E20F8A7000690E1 /* StyledStringBuilder.swift */,
199208
D1C5D8DB1C2ACA1D001CB619 /* CodesParser.swift */,
200209
D1C5D8DD1C2ACA7A001CB619 /* ModesExtractor.swift */,
@@ -410,6 +419,7 @@
410419
4B1929201C2BBC98005B79AB /* ControlCode.swift in Sources */,
411420
4B1929211C2BBC98005B79AB /* String+Rainbow.swift in Sources */,
412421
4B1929221C2BBC98005B79AB /* OutputTarget.swift in Sources */,
422+
D1F0772B2E27C68100EF60B9 /* HSLColorConverter.swift in Sources */,
413423
);
414424
runOnlyForDeploymentPostprocessing = 0;
415425
};
@@ -429,6 +439,7 @@
429439
4B1929381C2BBE06005B79AB /* ControlCode.swift in Sources */,
430440
4B1929391C2BBE06005B79AB /* String+Rainbow.swift in Sources */,
431441
4B19293A1C2BBE06005B79AB /* OutputTarget.swift in Sources */,
442+
D1F0772A2E27C68100EF60B9 /* HSLColorConverter.swift in Sources */,
432443
);
433444
runOnlyForDeploymentPostprocessing = 0;
434445
};
@@ -448,6 +459,7 @@
448459
4B1929611C2BBEFE005B79AB /* ControlCode.swift in Sources */,
449460
4B1929621C2BBEFE005B79AB /* String+Rainbow.swift in Sources */,
450461
4B1929631C2BBEFE005B79AB /* OutputTarget.swift in Sources */,
462+
D1F077292E27C68100EF60B9 /* HSLColorConverter.swift in Sources */,
451463
);
452464
runOnlyForDeploymentPostprocessing = 0;
453465
};
@@ -467,6 +479,7 @@
467479
D1C5D8DE1C2ACA7A001CB619 /* ModesExtractor.swift in Sources */,
468480
4BA75FAA1C2939EC00B1037A /* Rainbow.swift in Sources */,
469481
4BC026EF1C292F70009FD2FD /* Style.swift in Sources */,
482+
D1F0772C2E27C68100EF60B9 /* HSLColorConverter.swift in Sources */,
470483
);
471484
runOnlyForDeploymentPostprocessing = 0;
472485
};
@@ -479,6 +492,7 @@
479492
D1C5F61A2E25614A00004F50 /* EdgeCaseTests.swift in Sources */,
480493
D13C24C12E20F85B000690E1 /* PerformanceTests.swift in Sources */,
481494
D118F24F25EF13B700C7B074 /* ColorTests.swift in Sources */,
495+
D1F077302E27C6DF00EF60B9 /* HSLColorTests.swift in Sources */,
482496
D13C24CA2E20FC16000690E1 /* StyledStringBuilderTests.swift in Sources */,
483497
D1A98FB11C299DF0000DDF20 /* ConsoleStringTests.swift in Sources */,
484498
4BC821A11C29276C00F68E70 /* RainbowTests.swift in Sources */,

Sources/HSLColorConverter.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// HSLColorConverter.swift
3+
// Rainbow
4+
//
5+
// Created by Rainbow on 16/7/25.
6+
//
7+
8+
import Foundation
9+
10+
public typealias HSL = (hue: Double, saturation: Double, lightness: Double)
11+
12+
struct HSLColorConverter {
13+
14+
let hsl: HSL
15+
16+
init(hue: Double, saturation: Double, lightness: Double) {
17+
// Normalize inputs
18+
// Hue: wrap around 360 degrees
19+
let normalizedHue = hue.truncatingRemainder(dividingBy: 360)
20+
let h = normalizedHue < 0 ? normalizedHue + 360 : normalizedHue
21+
22+
// Saturation and Lightness: clamp to 0-100
23+
let s = min(max(saturation, 0), 100)
24+
let l = min(max(lightness, 0), 100)
25+
26+
self.hsl = HSL(hue: h, saturation: s, lightness: l)
27+
}
28+
29+
init(hsl: HSL) {
30+
self.init(hue: hsl.hue, saturation: hsl.saturation, lightness: hsl.lightness)
31+
}
32+
33+
func toRGB() -> RGB {
34+
let h = hsl.hue / 360.0
35+
let s = hsl.saturation / 100.0
36+
let l = hsl.lightness / 100.0
37+
38+
// Achromatic (gray)
39+
if s == 0 {
40+
let gray = UInt8(round(l * 255))
41+
return (gray, gray, gray)
42+
}
43+
44+
let q = l < 0.5 ? l * (1 + s) : l + s - l * s
45+
let p = 2 * l - q
46+
47+
let r = hueToRGB(p: p, q: q, t: h + 1/3)
48+
let g = hueToRGB(p: p, q: q, t: h)
49+
let b = hueToRGB(p: p, q: q, t: h - 1/3)
50+
51+
return (
52+
UInt8(round(r * 255)),
53+
UInt8(round(g * 255)),
54+
UInt8(round(b * 255))
55+
)
56+
}
57+
58+
private func hueToRGB(p: Double, q: Double, t: Double) -> Double {
59+
var t = t
60+
if t < 0 { t += 1 }
61+
if t > 1 { t -= 1 }
62+
63+
if t < 1/6 { return p + (q - p) * 6 * t }
64+
if t < 1/2 { return q }
65+
if t < 2/3 { return p + (q - p) * (2/3 - t) * 6 }
66+
67+
return p
68+
}
69+
}

Sources/String+Rainbow.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,43 @@ extension String {
230230
guard let converter = ColorApproximation(color: color) else { return self }
231231
return applyingColor(converter.convert(to: target))
232232
}
233+
234+
/// String with an HSL color applied to the text. The exact color which will be used is determined by the `target`.
235+
///
236+
/// - Parameters:
237+
/// - hue: The hue component in degrees (0-360). Values outside this range will be wrapped around.
238+
/// - saturation: The saturation component as a percentage (0-100). Values outside will be clamped.
239+
/// - lightness: The lightness component as a percentage (0-100). Values outside will be clamped.
240+
/// - target: The conversion target of this color. If `target` is `.bit8Approximated`, an approximated 8-bit
241+
/// color will be used; If `.bit24`, a 24-bit color is used.
242+
/// Default is `.bit8Approximated`.
243+
/// - Returns: The formatted string with the HSL color applied.
244+
public func hsl(_ hue: Double, _ saturation: Double, _ lightness: Double, to target: HexColorTarget = .bit8Approximated) -> String {
245+
let converter = HSLColorConverter(hue: hue, saturation: saturation, lightness: lightness)
246+
let rgb = converter.toRGB()
247+
248+
switch target {
249+
case .bit8Approximated:
250+
// Convert RGB to hex and use ColorApproximation for 8-bit conversion
251+
let hexValue = (UInt32(rgb.0) << 16) | (UInt32(rgb.1) << 8) | UInt32(rgb.2)
252+
guard let approximator = ColorApproximation(color: hexValue) else { return self }
253+
return applyingColor(approximator.convert(to: .bit8Approximated))
254+
case .bit24:
255+
return applyingColor(.bit24(rgb))
256+
}
257+
}
258+
259+
/// String with an HSL color applied to the text. The exact color which will be used is determined by the `target`.
260+
///
261+
/// - Parameters:
262+
/// - hsl: The HSL color as a tuple (hue: 0-360, saturation: 0-100, lightness: 0-100).
263+
/// - target: The conversion target of this color. If `target` is `.bit8Approximated`, an approximated 8-bit
264+
/// color will be used; If `.bit24`, a 24-bit color is used.
265+
/// Default is `.bit8Approximated`.
266+
/// - Returns: The formatted string with the HSL color applied.
267+
public func hsl(_ hsl: HSL, to target: HexColorTarget = .bit8Approximated) -> String {
268+
return self.hsl(hsl.hue, hsl.saturation, hsl.lightness, to: target)
269+
}
233270
}
234271

235272
// MARK: - Background Colors Shorthand
@@ -304,6 +341,43 @@ extension String {
304341
guard let converter = ColorApproximation(color: color) else { return self }
305342
return applyingBackgroundColor(converter.convert(to: target))
306343
}
344+
345+
/// String with an HSL color applied to the background. The exact color which will be used is determined by the `target`.
346+
///
347+
/// - Parameters:
348+
/// - hue: The hue component in degrees (0-360). Values outside this range will be wrapped around.
349+
/// - saturation: The saturation component as a percentage (0-100). Values outside will be clamped.
350+
/// - lightness: The lightness component as a percentage (0-100). Values outside will be clamped.
351+
/// - target: The conversion target of this color. If `target` is `.bit8Approximated`, an approximated 8-bit
352+
/// color will be used; If `.bit24`, a 24-bit color is used.
353+
/// Default is `.bit8Approximated`.
354+
/// - Returns: The formatted string with the HSL color applied to the background.
355+
public func onHsl(_ hue: Double, _ saturation: Double, _ lightness: Double, to target: HexColorTarget = .bit8Approximated) -> String {
356+
let converter = HSLColorConverter(hue: hue, saturation: saturation, lightness: lightness)
357+
let rgb = converter.toRGB()
358+
359+
switch target {
360+
case .bit8Approximated:
361+
// Convert RGB to hex and use ColorApproximation for 8-bit conversion
362+
let hexValue = (UInt32(rgb.0) << 16) | (UInt32(rgb.1) << 8) | UInt32(rgb.2)
363+
guard let approximator = ColorApproximation(color: hexValue) else { return self }
364+
return applyingBackgroundColor(approximator.convert(to: target))
365+
case .bit24:
366+
return applyingBackgroundColor(.bit24(rgb))
367+
}
368+
}
369+
370+
/// String with an HSL color applied to the background. The exact color which will be used is determined by the `target`.
371+
///
372+
/// - Parameters:
373+
/// - hsl: The HSL color as a tuple (hue: 0-360, saturation: 0-100, lightness: 0-100).
374+
/// - target: The conversion target of this color. If `target` is `.bit8Approximated`, an approximated 8-bit
375+
/// color will be used; If `.bit24`, a 24-bit color is used.
376+
/// Default is `.bit8Approximated`.
377+
/// - Returns: The formatted string with the HSL color applied to the background.
378+
public func onHsl(_ hsl: HSL, to target: HexColorTarget = .bit8Approximated) -> String {
379+
return self.onHsl(hsl.hue, hsl.saturation, hsl.lightness, to: target)
380+
}
307381
}
308382

309383
// MARK: - Styles Shorthand

0 commit comments

Comments
 (0)