Skip to content

Commit 01ce456

Browse files
authored
new view modifiers: brightness, contrast, saturation, huesaturation, colormultiply (#301)
.brightness(_:) Adjusts the brightness of the view .contrast(_:) Adjusts the contrast of the view .saturation(_:) Adjusts the color saturation of the view .hueRotation(_:) Rotates the hue of the view's colors by an angle .colorMultiply(_:) Multiplies the view's colors by a specified color float casting is annoying, could probably be made cleaner with something like let hueMatrix = ColorMatrix(values.map { Float($0) }.toFloatArray())
1 parent d13fd4a commit 01ce456

File tree

2 files changed

+209
-11
lines changed

2 files changed

+209
-11
lines changed

Sources/SkipUI/SkipUI/Compose/ComposeModifiers.swift

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,169 @@ class GrayscaleModifier : DrawModifier {
5757
}
5858
}
5959
}
60+
61+
class BrightnessModifier : DrawModifier {
62+
let amount: Double
63+
64+
init(amount: Double) {
65+
self.amount = amount
66+
}
67+
68+
// SKIP DECLARE: override fun ContentDrawScope.draw()
69+
override func draw() {
70+
// Brightness adjustment: shift RGB values by amount * 255
71+
// amount: -1.0 (black) to 1.0 (white), 0.0 = no change
72+
let shift = Float(amount * 255.0)
73+
let brightnessMatrix = ColorMatrix(floatArrayOf(
74+
Float(1), Float(0), Float(0), Float(0), shift,
75+
Float(0), Float(1), Float(0), Float(0), shift,
76+
Float(0), Float(0), Float(1), Float(0), shift,
77+
Float(0), Float(0), Float(0), Float(1), Float(0)
78+
))
79+
let brightnessFilter = ColorFilter.colorMatrix(brightnessMatrix)
80+
let paint = Paint().apply {
81+
colorFilter = brightnessFilter
82+
}
83+
drawIntoCanvas {
84+
$0.saveLayer(Rect(Float(0.0), Float(0.0), size.width, size.height), paint)
85+
drawContent()
86+
$0.restore()
87+
}
88+
}
89+
}
90+
91+
class ContrastModifier : DrawModifier {
92+
let amount: Double
93+
94+
init(amount: Double) {
95+
self.amount = amount
96+
}
97+
98+
// SKIP DECLARE: override fun ContentDrawScope.draw()
99+
override func draw() {
100+
// Contrast adjustment: scale RGB around 0.5 (128)
101+
// amount: 0.0 = gray, 1.0 = no change, >1.0 = increased contrast
102+
let scale = Float(amount)
103+
let translate = Float((1.0 - amount) * 127.5)
104+
let contrastMatrix = ColorMatrix(floatArrayOf(
105+
scale, Float(0), Float(0), Float(0), translate,
106+
Float(0), scale, Float(0), Float(0), translate,
107+
Float(0), Float(0), scale, Float(0), translate,
108+
Float(0), Float(0), Float(0), Float(1), Float(0)
109+
))
110+
let contrastFilter = ColorFilter.colorMatrix(contrastMatrix)
111+
let paint = Paint().apply {
112+
colorFilter = contrastFilter
113+
}
114+
drawIntoCanvas {
115+
$0.saveLayer(Rect(Float(0.0), Float(0.0), size.width, size.height), paint)
116+
drawContent()
117+
$0.restore()
118+
}
119+
}
120+
}
121+
122+
class SaturationModifier : DrawModifier {
123+
let amount: Double
124+
125+
init(amount: Double) {
126+
self.amount = amount
127+
}
128+
129+
// SKIP DECLARE: override fun ContentDrawScope.draw()
130+
override func draw() {
131+
// Saturation: 0.0 = grayscale, 1.0 = no change, >1.0 = oversaturated
132+
let saturationMatrix = ColorMatrix().apply { setToSaturation(Float(amount)) }
133+
let saturationFilter = ColorFilter.colorMatrix(saturationMatrix)
134+
let paint = Paint().apply {
135+
colorFilter = saturationFilter
136+
}
137+
drawIntoCanvas {
138+
$0.saveLayer(Rect(Float(0.0), Float(0.0), size.width, size.height), paint)
139+
drawContent()
140+
$0.restore()
141+
}
142+
}
143+
}
144+
145+
class HueRotationModifier : DrawModifier {
146+
let degrees: Double
147+
148+
init(degrees: Double) {
149+
self.degrees = degrees
150+
}
151+
152+
// SKIP DECLARE: override fun ContentDrawScope.draw()
153+
override func draw() {
154+
// Hue rotation using ColorMatrix
155+
let radians = Float(degrees * Double.pi / 180.0)
156+
let cos = kotlin.math.cos(radians)
157+
let sin = kotlin.math.sin(radians)
158+
159+
// Hue rotation matrix derived from rotation around the (1,1,1) axis in RGB space
160+
let lumR = Float(0.213)
161+
let lumG = Float(0.715)
162+
let lumB = Float(0.072)
163+
164+
let hueMatrix = ColorMatrix(floatArrayOf(
165+
lumR + cos * (Float(1) - lumR) + sin * (-lumR),
166+
lumG + cos * (-lumG) + sin * (-lumG),
167+
lumB + cos * (-lumB) + sin * (Float(1) - lumB),
168+
Float(0), Float(0),
169+
170+
lumR + cos * (-lumR) + sin * Float(0.143),
171+
lumG + cos * (Float(1) - lumG) + sin * Float(0.140),
172+
lumB + cos * (-lumB) + sin * Float(-0.283),
173+
Float(0), Float(0),
174+
175+
lumR + cos * (-lumR) + sin * (-(Float(1) - lumR)),
176+
lumG + cos * (-lumG) + sin * (lumG),
177+
lumB + cos * (Float(1) - lumB) + sin * (lumB),
178+
Float(0), Float(0),
179+
180+
Float(0), Float(0), Float(0), Float(1), Float(0)
181+
))
182+
let hueFilter = ColorFilter.colorMatrix(hueMatrix)
183+
let paint = Paint().apply {
184+
colorFilter = hueFilter
185+
}
186+
drawIntoCanvas {
187+
$0.saveLayer(Rect(Float(0.0), Float(0.0), Float(size.width), Float(size.height)), paint)
188+
drawContent()
189+
$0.restore()
190+
}
191+
}
192+
}
193+
194+
class ColorMultiplyModifier : DrawModifier {
195+
let color: androidx.compose.ui.graphics.Color
196+
197+
init(color: androidx.compose.ui.graphics.Color) {
198+
self.color = color
199+
}
200+
201+
// SKIP DECLARE: override fun ContentDrawScope.draw()
202+
override func draw() {
203+
// Color multiply: multiply each channel by the corresponding color channel
204+
let r = color.red
205+
let g = color.green
206+
let b = color.blue
207+
let a = color.alpha
208+
let multiplyMatrix = ColorMatrix(floatArrayOf(
209+
r, Float(0), Float(0), Float(0), Float(0),
210+
Float(0), g, Float(0), Float(0), Float(0),
211+
Float(0), Float(0), b, Float(0), Float(0),
212+
Float(0), Float(0), Float(0), a, Float(0)
213+
))
214+
let multiplyFilter = ColorFilter.colorMatrix(multiplyMatrix)
215+
let paint = Paint().apply {
216+
colorFilter = multiplyFilter
217+
}
218+
drawIntoCanvas {
219+
$0.saveLayer(Rect(Float(0.0), Float(0.0), size.width, size.height), paint)
220+
drawContent()
221+
$0.restore()
222+
}
223+
}
224+
}
60225
#endif

Sources/SkipUI/SkipUI/View/AdditionalViewModifiers.swift

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,15 @@ extension View {
208208
#endif
209209
}
210210

211-
@available(*, unavailable)
212-
public func brightness(_ amount: Double) -> some View {
211+
// SKIP @bridge
212+
public func brightness(_ amount: Double) -> any View {
213+
#if SKIP
214+
return ModifiedContent(content: self, modifier: RenderModifier {
215+
return $0.modifier.then(BrightnessModifier(amount: amount))
216+
})
217+
#else
213218
return self
219+
#endif
214220
}
215221

216222
public func clipShape(_ shape: any Shape, style: FillStyle = FillStyle()) -> any View {
@@ -250,9 +256,15 @@ extension View {
250256
#endif
251257
}
252258

253-
@available(*, unavailable)
254-
public func colorMultiply(_ color: Color) -> some View {
259+
// SKIP @bridge
260+
public func colorMultiply(_ color: Color) -> any View {
261+
#if SKIP
262+
return ModifiedContent(content: self, modifier: RenderModifier { context in
263+
return context.modifier.then(ColorMultiplyModifier(color: color.colorImpl()))
264+
})
265+
#else
255266
return self
267+
#endif
256268
}
257269

258270
// SKIP @bridge
@@ -315,9 +327,15 @@ extension View {
315327
return self
316328
}
317329

318-
@available(*, unavailable)
319-
public func contrast(_ amount: Double) -> some View {
330+
// SKIP @bridge
331+
public func contrast(_ amount: Double) -> any View {
332+
#if SKIP
333+
return ModifiedContent(content: self, modifier: RenderModifier {
334+
return $0.modifier.then(ContrastModifier(amount: amount))
335+
})
336+
#else
320337
return self
338+
#endif
321339
}
322340

323341
@available(*, unavailable)
@@ -543,10 +561,19 @@ extension View {
543561
return self
544562
}
545563

546-
@available(*, unavailable)
547-
public func hueRotation(_ angle: Angle) -> some View {
548-
// NOTE: animatable property
564+
public func hueRotation(_ angle: Angle) -> any View {
565+
#if SKIP
566+
return ModifiedContent(content: self, modifier: RenderModifier {
567+
return $0.modifier.then(HueRotationModifier(degrees: angle.degrees))
568+
})
569+
#else
549570
return self
571+
#endif
572+
}
573+
574+
// SKIP @bridge
575+
public func hueRotation(bridgedAngle: Double) -> any View {
576+
return hueRotation(.radians(bridgedAngle))
550577
}
551578

552579
// SKIP @bridge
@@ -1012,9 +1039,15 @@ extension View {
10121039
return rotation3DEffect(.radians(bridgedAngle), axis: axis, anchor: UnitPoint(x: anchorX, y: anchorY), anchorZ: anchorZ, perspective: perspective)
10131040
}
10141041

1015-
@available(*, unavailable)
1016-
public func saturation(_ amount: Double) -> some View {
1042+
// SKIP @bridge
1043+
public func saturation(_ amount: Double) -> any View {
1044+
#if SKIP
1045+
return ModifiedContent(content: self, modifier: RenderModifier {
1046+
return $0.modifier.then(SaturationModifier(amount: amount))
1047+
})
1048+
#else
10171049
return self
1050+
#endif
10181051
}
10191052

10201053
// No need to @bridge because we define in terms of `.aspectRatio`

0 commit comments

Comments
 (0)