Add blur and tint mode implementation.
Add blur and tint mode implementation
@@ -1,3 +1,5 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
protocol FilterProtocol {
|
||||
var name: String { get }
|
||||
var parameters: [String: Any] { get }
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import CoreImage
|
||||
|
||||
public enum ImageBlurType: FilterProtocol {
|
||||
case gaussian(radius: CGFloat)
|
||||
|
||||
public var name: String {
|
||||
switch self {
|
||||
case .gaussian:
|
||||
return "CIGaussianBlur"
|
||||
}
|
||||
}
|
||||
|
||||
public var parameters: [String: Any] {
|
||||
switch self {
|
||||
case let .gaussian(radius):
|
||||
return [kCIInputRadiusKey: radius]
|
||||
}
|
||||
}
|
||||
|
||||
public var imageFilter: ImageFilter {
|
||||
{ image in
|
||||
let parameters: [String: Any] = [
|
||||
kCIInputImageKey: image,
|
||||
]
|
||||
let filter = CIFilter(name: name, parameters: self.parameters.merging(parameters) { $1 })
|
||||
return filter?.outputImage
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,35 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import CoreImage
|
||||
|
||||
public enum ImageComposerType: FilterProtocol {
|
||||
case sourceAtop
|
||||
case sourceIn
|
||||
case darken
|
||||
case lighten
|
||||
case multiply
|
||||
case screen
|
||||
|
||||
public var name: String {
|
||||
switch self {
|
||||
case .sourceAtop:
|
||||
return "CISourceAtopCompositing"
|
||||
case .sourceIn:
|
||||
return "CISourceInCompositing"
|
||||
case .darken:
|
||||
return "CIDarkenBlendMode"
|
||||
case .lighten:
|
||||
return "CILightenBlendMode"
|
||||
case .multiply:
|
||||
return "CIMultiplyCompositing"
|
||||
case .screen:
|
||||
return "CIScreenBlendMode"
|
||||
}
|
||||
}
|
||||
|
||||
public var parameters: [String: Any] {
|
||||
switch self {
|
||||
case .sourceAtop:
|
||||
case .sourceAtop, .sourceIn, .darken, .lighten, .multiply, .screen:
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import CoreImage
|
||||
|
||||
public enum ImageCropType: FilterProtocol {
|
||||
case crop(rect: CIVector)
|
||||
|
||||
public var name: String {
|
||||
switch self {
|
||||
case .crop:
|
||||
return "CICrop"
|
||||
}
|
||||
}
|
||||
|
||||
public var parameters: [String: Any] {
|
||||
switch self {
|
||||
case let .crop(rect):
|
||||
return ["inputRectangle": rect]
|
||||
}
|
||||
}
|
||||
|
||||
public var imageFilter: ImageFilter {
|
||||
{ image in
|
||||
let parameters: [String: Any] = [
|
||||
kCIInputImageKey: image,
|
||||
]
|
||||
let filter = CIFilter(name: name, parameters: self.parameters.merging(parameters) { $1 })
|
||||
return filter?.outputImage
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2021 Yandex LLC. All rights reserved.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
import BaseTiny
|
||||
|
||||
public enum ImageEffect: Equatable {
|
||||
case blur(radius: CGFloat)
|
||||
case tint(color: RGBAColor, mode: TintMode?)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import CoreImage
|
||||
|
||||
public typealias ImageFilter = (CIImage) -> CIImage?
|
||||
public typealias ImageGenerator = () -> CIImage?
|
||||
public typealias ImageComposer = (CIImage) -> ImageFilter
|
||||
|
||||
public func combine(
|
||||
_ f1: @escaping ImageFilter,
|
||||
_ f2: @escaping ImageFilter
|
||||
) -> ImageFilter {
|
||||
{ image in f1(image).flatMap { f2($0) } }
|
||||
}
|
||||
|
||||
public func combine(
|
||||
_ f1: @escaping ImageFilter,
|
||||
_ f2: @escaping ImageFilter,
|
||||
_ f3: @escaping ImageFilter
|
||||
) -> ImageFilter {
|
||||
combine(combine(f1, f2), f3)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import CoreImage
|
||||
|
||||
public enum ImageGeneratorType: FilterProtocol {
|
||||
@@ -9,7 +9,7 @@ public final class Lazy<T> {
|
||||
|
||||
private enum State {
|
||||
case willLoad(getter: Getter, deferredActions: [Action])
|
||||
case loading
|
||||
case loading(deferredActions: [Action])
|
||||
case loaded(value: T)
|
||||
|
||||
var value: T? {
|
||||
@@ -74,8 +74,8 @@ public final class Lazy<T> {
|
||||
switch state {
|
||||
case let .willLoad(getter, deferredActions):
|
||||
state = .willLoad(getter: getter, deferredActions: deferredActions + [action])
|
||||
case .loading:
|
||||
assertionFailure()
|
||||
case let .loading(deferredActions):
|
||||
state = .loading(deferredActions: deferredActions + [action])
|
||||
case let .loaded(value):
|
||||
action(value)
|
||||
}
|
||||
@@ -85,8 +85,11 @@ public final class Lazy<T> {
|
||||
ensureIsValidThread()
|
||||
switch state {
|
||||
case let .willLoad(getter, deferredActions):
|
||||
state = .loading
|
||||
state = .loading(deferredActions: deferredActions)
|
||||
let value = getter()
|
||||
guard case let .loading(deferredActions) = state else {
|
||||
return assertionFailure()
|
||||
}
|
||||
state = .loaded(value: value)
|
||||
deferredActions.forEach { $0(value) }
|
||||
case .loading, .loaded:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Copyright 2018 Yandex LLC. All rights reserved.
|
||||
|
||||
public struct Observer<T> {
|
||||
public let action: (T) -> Void
|
||||
|
||||
|
||||
@@ -201,3 +201,18 @@ extension Property: ExpressibleByBooleanLiteral where T == Bool {
|
||||
self.init(initialValue: value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Property: ExpressibleByIntegerLiteral where T == Int {
|
||||
public init(integerLiteral value: Int) {
|
||||
self.init(initialValue: value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Property: ExpressibleByUnicodeScalarLiteral where T == String {}
|
||||
extension Property: ExpressibleByExtendedGraphemeClusterLiteral where T == String {}
|
||||
|
||||
extension Property: ExpressibleByStringLiteral where T == String {
|
||||
public init(stringLiteral value: String) {
|
||||
self.init(initialValue: value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ public final class RadialGradientView: UIView {
|
||||
+ [gradient.outerColor.cgColor]
|
||||
gradientLayer.locations =
|
||||
([0] + gradient.intermediatePoints.map { $0.location } + [1]) as [NSNumber]
|
||||
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
@@ -79,12 +78,20 @@ public final class RadialGradientView: UIView {
|
||||
}
|
||||
radius = min(startPoint.x.nearestDistance / kx, startPoint.y.nearestDistance / ky)
|
||||
case .nearestCorner:
|
||||
if startPoint.x.isOnBorder && startPoint.y.isOnBorder {
|
||||
if startPoint.x.isOnBorder, startPoint.y.isOnBorder {
|
||||
return CGPoint(x: startPoint.x + .ulpOfOne, y: startPoint.y + .ulpOfOne)
|
||||
}
|
||||
radius = sqrt(pow(startPoint.x.nearestDistance / kx, 2) + pow(startPoint.y.nearestDistance / ky, 2))
|
||||
radius =
|
||||
sqrt(
|
||||
pow(startPoint.x.nearestDistance / kx, 2) +
|
||||
pow(startPoint.y.nearestDistance / ky, 2)
|
||||
)
|
||||
case .farthestCorner:
|
||||
radius = sqrt(pow(startPoint.x.farthestDistance / kx, 2) + pow(startPoint.y.farthestDistance / ky, 2))
|
||||
radius =
|
||||
sqrt(
|
||||
pow(startPoint.x.farthestDistance / kx, 2) +
|
||||
pow(startPoint.y.farthestDistance / ky, 2)
|
||||
)
|
||||
case .farthestSide:
|
||||
radius = max(startPoint.x.farthestDistance / kx, startPoint.y.farthestDistance / ky)
|
||||
}
|
||||
@@ -104,24 +111,24 @@ public final class RadialGradientView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension CGFloat {
|
||||
var nearest: CGFloat {
|
||||
extension CGFloat {
|
||||
fileprivate var nearest: CGFloat {
|
||||
self < 0.5 ? 0 : 1
|
||||
}
|
||||
|
||||
var farthest: CGFloat {
|
||||
fileprivate var farthest: CGFloat {
|
||||
self > 0.5 ? 0 : 1
|
||||
}
|
||||
|
||||
var nearestDistance: CGFloat {
|
||||
fileprivate var nearestDistance: CGFloat {
|
||||
abs(self.nearest - self)
|
||||
}
|
||||
|
||||
var farthestDistance: CGFloat {
|
||||
fileprivate var farthestDistance: CGFloat {
|
||||
abs(self.farthest - self)
|
||||
}
|
||||
|
||||
var isOnBorder: Bool {
|
||||
fileprivate var isOnBorder: Bool {
|
||||
self == 0 || self == 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,25 +76,26 @@ extension SettingProperty where T: NSCoding {
|
||||
|
||||
extension SettingProperty where T: Codable {
|
||||
public static func codableStorage(_ storage: KeyValueStorage, key: String) -> Property<T?> {
|
||||
Property(getter: {
|
||||
guard let object = storage.object(forKey: key) else { return nil }
|
||||
guard let data = object as? Data else {
|
||||
assertionFailure()
|
||||
SettingProperty<Data>.storage(storage, key: key).decoded(T.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Property where T == Data? {
|
||||
public func decoded<U: Codable>(_: U.Type) -> Property<U?> {
|
||||
bimap(
|
||||
get: { $0.flatMap { try? SingleValueDecoder().decode(U.self, from: $0) } },
|
||||
set: {
|
||||
if let newValue = $0 {
|
||||
do {
|
||||
return try SingleValueEncoder().encode(newValue)
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
return try? SingleValueDecoder().decode(T.self, from: data)
|
||||
}, setter: {
|
||||
if let newValue = $0 {
|
||||
do {
|
||||
let encodedValue = try SingleValueEncoder().encode(newValue)
|
||||
storage.set(encodedValue, forKey: key)
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
} else {
|
||||
storage.removeObject(forKey: key)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Copyright 2018 Yandex LLC. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Signal<T> {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
public enum TintMode {
|
||||
case sourceIn
|
||||
case sourceAtop
|
||||
case darken
|
||||
case lighten
|
||||
case multiply
|
||||
case screen
|
||||
}
|
||||
@@ -42,6 +42,10 @@ public struct WeakCollection<T> {
|
||||
public func contains(_ object: T) -> Bool {
|
||||
array.contains(where: { $0.value === (object as AnyObject) })
|
||||
}
|
||||
|
||||
public mutating func removeAll() {
|
||||
array.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
extension WeakCollection: CustomStringConvertible {
|
||||
|
||||
@@ -15,6 +15,10 @@ public enum ArrayBuilder<Element> {
|
||||
element.map { [$0] } ?? []
|
||||
}
|
||||
|
||||
public static func buildExpression(_ component: Component) -> Component {
|
||||
component
|
||||
}
|
||||
|
||||
public static func buildOptional(_ component: Component?) -> Component {
|
||||
component ?? []
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ extension Comparable {
|
||||
}
|
||||
|
||||
extension Comparable where Self: Strideable, Self.Stride: SignedInteger {
|
||||
public func clamp(_ range: Range<Self>) -> Self {
|
||||
clamp(ClosedRange(range))
|
||||
public func clamp(_ range: Range<Self>) -> Self? {
|
||||
guard !range.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return clamp(ClosedRange(range))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,6 +345,25 @@ extension Dictionary where Key == String, Value == JSONObject {
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary where Key == String, Value == JSONObject {
|
||||
public func value(atPath path: JSONObject.Path) throws -> JSONObject {
|
||||
try JSONObject.object(self).value(atPath: path)
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary where Key == String, Value == JSONObject {
|
||||
public func flatten() -> JSONDictionary {
|
||||
reduce(into: [:]) { result, entry in
|
||||
let (key, value) = entry
|
||||
if case let .object(dict) = value {
|
||||
dict.flatten().forEach { result[key + "." + $0] = $1 }
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONObject {
|
||||
public struct Path: Codable, CustomDebugStringConvertible, ExpressibleByStringLiteral, Hashable {
|
||||
public typealias Component = JSONPathComponent
|
||||
|
||||
@@ -102,9 +102,15 @@ extension AccessibilityElement {
|
||||
|
||||
public static func button(
|
||||
strings: Strings,
|
||||
enabled: Bool = true,
|
||||
startsMediaSession: Bool = false
|
||||
) -> AccessibilityElement {
|
||||
AccessibilityElement(traits: .button, strings: strings, startsMediaSession: startsMediaSession)
|
||||
AccessibilityElement(
|
||||
traits: .button,
|
||||
strings: strings,
|
||||
enabled: enabled,
|
||||
startsMediaSession: startsMediaSession
|
||||
)
|
||||
}
|
||||
|
||||
public static func header(label: String?) -> AccessibilityElement? {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Copyright 2021 Yandex LLC. All rights reserved.
|
||||
|
||||
import CoreGraphics
|
||||
import CoreImage
|
||||
|
||||
import BaseTiny
|
||||
|
||||
@_implementationOnly import CoreImage
|
||||
|
||||
extension CGColor {
|
||||
public var rgba: RGBAColor {
|
||||
let color = CIColor(cgColor: self)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import CoreImage
|
||||
|
||||
public typealias ImageFilter = (CIImage) -> CIImage?
|
||||
public typealias ImageGenerator = () -> CIImage?
|
||||
public typealias ImageComposer = (CIImage) -> ImageFilter
|
||||
|
||||
public func combine(
|
||||
_ lhs: @escaping ImageFilter,
|
||||
_ rhs: @escaping ImageFilter
|
||||
) -> ImageFilter {
|
||||
{ image in lhs(image).flatMap { rhs($0) } }
|
||||
}
|
||||
@@ -31,6 +31,8 @@ extension UIDevice {
|
||||
public static let iPhone12mini = "iPhone13,1"
|
||||
public static let iPhone12Pro = "iPhone13,3"
|
||||
public static let iPhone12ProMax = "iPhone13,4"
|
||||
|
||||
public static let iPadAir_5gen = "iPad13,17"
|
||||
}
|
||||
|
||||
public var systemModelName: String {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import BaseUI
|
||||
|
||||
public struct ImageRedrawingStyle: Equatable {
|
||||
public static func ==(lhs: ImageRedrawingStyle, rhs: ImageRedrawingStyle) -> Bool {
|
||||
lhs.tintColor == rhs.tintColor &&
|
||||
lhs.tintMode == rhs.tintMode
|
||||
}
|
||||
|
||||
let tintColor: Color?
|
||||
let tintMode: TintMode?
|
||||
let effects: [ImageEffect]
|
||||
|
||||
public init(
|
||||
tintColor: Color?,
|
||||
tintMode: TintMode? = nil,
|
||||
effects: [ImageEffect]
|
||||
) {
|
||||
self.tintColor = tintColor
|
||||
self.tintMode = tintMode
|
||||
self.effects = effects
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
import Base
|
||||
@@ -29,22 +31,3 @@ public struct ImageViewAnimation {
|
||||
self.options = options
|
||||
}
|
||||
}
|
||||
|
||||
public struct ImageRedrawingStyle: Equatable {
|
||||
let tintColor: Color
|
||||
let tintMode: TintMode?
|
||||
|
||||
public init(tintColor: Color, tintMode: TintMode? = nil) {
|
||||
self.tintColor = tintColor
|
||||
self.tintMode = tintMode
|
||||
}
|
||||
|
||||
public enum TintMode {
|
||||
case sourceIn
|
||||
case sourceAtop
|
||||
case darken
|
||||
case lighten
|
||||
case multiply
|
||||
case screen
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import MetalKit
|
||||
import UIKit
|
||||
|
||||
@@ -18,8 +20,6 @@ public final class MetalImageView: UIView, RemoteImageViewContentProtocol {
|
||||
return mtkView
|
||||
}()
|
||||
|
||||
public var lastTexture: MTLTexture?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
@@ -32,12 +32,12 @@ public final class MetalImageView: UIView, RemoteImageViewContentProtocol {
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
metalView.frame = bounds
|
||||
image = redrawingImage(uiImage, bounds: bounds, imageRedrawingStyle: imageRedrawingStyle)
|
||||
}
|
||||
|
||||
public func setImage(_ image: UIImage?, animated: Bool?) {
|
||||
self.image = redrawingImage(image, imageRedrawingStyle: imageRedrawingStyle)
|
||||
uiImage = image
|
||||
metalView.setNeedsDisplay()
|
||||
setNeedsLayout()
|
||||
if let appearanceAnimation = appearanceAnimation, animated == true {
|
||||
self.alpha = appearanceAnimation.startAlpha
|
||||
UIView.animate(
|
||||
@@ -60,8 +60,8 @@ public final class MetalImageView: UIView, RemoteImageViewContentProtocol {
|
||||
|
||||
public var imageRedrawingStyle: ImageRedrawingStyle? {
|
||||
didSet {
|
||||
if oldValue != imageRedrawingStyle, let uiImage {
|
||||
image = redrawingImage(uiImage, imageRedrawingStyle: imageRedrawingStyle)
|
||||
if oldValue != imageRedrawingStyle {
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,56 +75,74 @@ public final class MetalImageView: UIView, RemoteImageViewContentProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage.Orientation {
|
||||
fileprivate var layerTransform: CGAffineTransform {
|
||||
switch self {
|
||||
case .up:
|
||||
return .identity
|
||||
case .upMirrored:
|
||||
return CGAffineTransform(scaleX: -1, y: 1)
|
||||
case .down:
|
||||
return CGAffineTransform(rotationAngle: .pi)
|
||||
case .downMirrored:
|
||||
return CGAffineTransform(rotationAngle: .pi).scaledBy(x: -1, y: 1)
|
||||
case .left:
|
||||
return CGAffineTransform(rotationAngle: -.pi / 2)
|
||||
case .leftMirrored:
|
||||
return CGAffineTransform(rotationAngle: -.pi / 2).scaledBy(x: -1, y: 1)
|
||||
case .right:
|
||||
return CGAffineTransform(rotationAngle: .pi / 2)
|
||||
case .rightMirrored:
|
||||
return CGAffineTransform(rotationAngle: .pi / 2).scaledBy(x: -1, y: 1)
|
||||
@unknown default:
|
||||
return .identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CALayer {
|
||||
fileprivate func setContents(_ source: Image?) {
|
||||
contents = source?.cgImage
|
||||
contentsScale = source?.scale ?? 1
|
||||
setAffineTransform(source?.imageOrientation.layerTransform ?? .identity)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func redrawingImage(
|
||||
_ image: UIImage?,
|
||||
bounds: CGRect,
|
||||
imageRedrawingStyle: ImageRedrawingStyle?
|
||||
) -> CIImage? {
|
||||
guard let image,
|
||||
let cgImage = image.cgImage else {
|
||||
return nil
|
||||
}
|
||||
let baseImage = CIImage(cgImage: cgImage)
|
||||
let ciImage = CIImage(cgImage: cgImage)
|
||||
let extent = ciImage.extent
|
||||
|
||||
guard let tintColor = imageRedrawingStyle?.tintColor.ciColor,
|
||||
let coloredImage = ImageGeneratorType.constantColor(color: tintColor).imageGenerator(),
|
||||
let ciImage = ImageComposerType.sourceAtop.imageComposer(baseImage)(coloredImage) else {
|
||||
return baseImage
|
||||
let identityFilter: ImageFilter = { $0 }
|
||||
|
||||
let tintModeFilter: ImageFilter
|
||||
|
||||
if let tintColor = imageRedrawingStyle?.tintColor,
|
||||
let coloredImage = ImageGeneratorType.constantColor(color: tintColor.ciColor)
|
||||
.imageGenerator() {
|
||||
let mode = imageRedrawingStyle?.tintMode ?? .sourceAtop
|
||||
tintModeFilter = { mode.composerType.imageComposer($0)(coloredImage) }
|
||||
} else {
|
||||
tintModeFilter = identityFilter
|
||||
}
|
||||
|
||||
return ciImage.cropped(to: baseImage.extent)
|
||||
let filter = imageRedrawingStyle?.effects.compactMap {
|
||||
switch $0 {
|
||||
case let .blur(radius: radius):
|
||||
let scaleX = bounds.width / extent.width
|
||||
let scaleY = bounds.height / extent.height
|
||||
let scale = max(scaleX, scaleY) * 2
|
||||
return ImageBlurType.gaussian(radius: radius / scale).imageFilter
|
||||
case let .tint(color: color, mode: mode):
|
||||
guard let mode,
|
||||
let coloredImage = ImageGeneratorType.constantColor(color: color.ciColor)
|
||||
.imageGenerator()
|
||||
else { return nil }
|
||||
return { mode.composerType.imageComposer($0)(coloredImage) }
|
||||
}
|
||||
}.reduce(identityFilter, combine) ?? identityFilter
|
||||
|
||||
let contentRect = CIVector(
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: extent.width,
|
||||
w: extent.height
|
||||
)
|
||||
|
||||
return combine(tintModeFilter, filter, ImageCropType.crop(rect: contentRect).imageFilter)(ciImage)
|
||||
}
|
||||
|
||||
extension TintMode {
|
||||
fileprivate var composerType: ImageComposerType {
|
||||
switch self {
|
||||
case .sourceIn:
|
||||
return .sourceIn
|
||||
case .sourceAtop:
|
||||
return .sourceAtop
|
||||
case .darken:
|
||||
return .darken
|
||||
case .lighten:
|
||||
return .lighten
|
||||
case .multiply:
|
||||
return .multiply
|
||||
case .screen:
|
||||
return .screen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MetalImageView: MTKViewDelegate {
|
||||
@@ -139,26 +157,18 @@ extension MetalImageView: MTKViewDelegate {
|
||||
let buffer = commandQueue.makeCommandBuffer()
|
||||
let drawableSize = view.drawableSize
|
||||
|
||||
let boundsSize: CGSize
|
||||
if imageContentMode.scale == .noScale {
|
||||
boundsSize = drawableSize
|
||||
} else {
|
||||
boundsSize = view.bounds.size
|
||||
}
|
||||
|
||||
let layout = layout(
|
||||
contentMode: imageContentMode,
|
||||
contentSize: image.extent.size,
|
||||
boundsSize: boundsSize
|
||||
boundsSize: view.bounds.size
|
||||
)
|
||||
|
||||
let ciContext = CIContext()
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
let bounds = CGRect(origin: CGPoint.zero, size: drawableSize)
|
||||
|
||||
let screenFactorX = drawableSize.width / boundsSize.width
|
||||
let screenFactorY = drawableSize.height / boundsSize.height
|
||||
let screenFactorX = drawableSize.width / view.bounds.width
|
||||
let screenFactorY = drawableSize.height / view.bounds.height
|
||||
let scaleX = layout.width / image.extent.width * screenFactorX
|
||||
let scaleY = layout.height / image.extent.height * screenFactorY
|
||||
|
||||
@@ -174,7 +184,15 @@ extension MetalImageView: MTKViewDelegate {
|
||||
y: layout.origin.y * screenFactorY
|
||||
))
|
||||
|
||||
ciContext.render(
|
||||
let ciContext: CIContext?
|
||||
|
||||
#if DEBUG
|
||||
ciContext = isSnapshotTest ? CIContext() : self.ciContext
|
||||
#else
|
||||
ciContext = self.ciContext
|
||||
#endif
|
||||
|
||||
ciContext?.render(
|
||||
scaledImage,
|
||||
to: drawable.texture,
|
||||
commandBuffer: buffer,
|
||||
@@ -242,3 +260,8 @@ private func makeOrigin(
|
||||
}
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private let isSnapshotTest = ProcessInfo.processInfo
|
||||
.environment["XCTestConfigurationFilePath"] != nil
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
import Base
|
||||
@@ -43,6 +45,7 @@ public final class RemoteImageView: UIView, RemoteImageViewContentProtocol {
|
||||
}
|
||||
}
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
public func setImage(_ image: UIImage?, animated: Bool?) {
|
||||
@@ -60,6 +63,8 @@ public final class RemoteImageView: UIView, RemoteImageViewContentProtocol {
|
||||
} else {
|
||||
updateContent()
|
||||
}
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
private var image: UIImage?
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 Yandex LLC. All rights reserved.
|
||||
// Copyright 2022 Yandex LLC. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public protocol RemoteImageViewContentProtocol: UIView, ImageViewProtocol {
|
||||
func setImage(_ image: UIImage?, animated: Bool?)
|
||||
func setImage(_ image: UIImage?, animated: Bool?)
|
||||
}
|
||||
|
||||
@@ -67,3 +67,47 @@ extension HTTPHeaders {
|
||||
extension HTTPHeaders {
|
||||
public static let yandexUID = "X-Yandex-RandomUID"
|
||||
}
|
||||
|
||||
extension HTTPHeaders {
|
||||
/**
|
||||
Creates a new instance, combining current headers with newHeaders.
|
||||
Conflicting keys get a value from newHeaders.
|
||||
"Cookies" field is merged separately, all cookies are stored,
|
||||
conflicting cookie keys get a value from newHeaders' cookies.
|
||||
**/
|
||||
public func merged(with newHeaders: HTTPHeaders) -> HTTPHeaders {
|
||||
let cookie = mergeCookies(self["Cookie"], newHeaders["Cookie"])
|
||||
return self + newHeaders +
|
||||
(cookie != "" ? HTTPHeaders(headersDictionary: ["Cookie": cookie]) : HTTPHeaders.empty)
|
||||
}
|
||||
|
||||
private func mergeCookies(_ cookies: String?, _ newCookies: String?) -> String {
|
||||
var result = parseCookies(cookies ?? "")
|
||||
parseCookies(newCookies ?? "").forEach { newCookie in
|
||||
if let index = result.firstIndex(where: { $0.name == newCookie.name }) {
|
||||
result[index] = newCookie
|
||||
} else {
|
||||
result.append(newCookie)
|
||||
}
|
||||
}
|
||||
return result.map { "\($0.name)=\($0.value)" }.joined(separator: "; ")
|
||||
}
|
||||
|
||||
private func parseCookies(_ cookies: String) -> [(name: String, value: String)] {
|
||||
cookies
|
||||
.components(separatedBy: "; ")
|
||||
.filter { $0 != "" }
|
||||
.map {
|
||||
let keyVal = $0.components(separatedByFirst: "=")
|
||||
return (name: keyVal[0], value: keyVal.count > 1 ? keyVal[1] : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
fileprivate func components(separatedByFirst delimiter: String) -> [String] {
|
||||
let comps = components(separatedBy: delimiter)
|
||||
guard comps.count > 1 else { return comps }
|
||||
return [comps[0], comps.suffix(from: 1).joined(separator: delimiter)]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,6 @@ extension RemoteImageHolder: CustomDebugStringConvertible {
|
||||
|
||||
extension RemoteImageHolder {
|
||||
public func reused(with placeholder: ImagePlaceholder?, remoteImageURL: URL?) -> ImageHolder? {
|
||||
(placeholder === placeholder && url == remoteImageURL) ? self : nil
|
||||
(self.placeholder === placeholder && url == remoteImageURL) ? self : nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,14 @@ cp $1/src/base/ios/yandex/storage/KeyValueDirectStoringSupporting.swift Base
|
||||
cp $1/src/base/ios/yandex/storage/KeyValueStorage.swift Base
|
||||
cp $1/src/base/ios/yandex/storage/SettingProperty.swift Base
|
||||
cp $1/src/base/ios/yandex/storage/SingleValueCoder.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/filters/FilterProtocol.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/filters/ImageComposer.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/filters/ImageFilters.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/filters/ImageGenerator.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/filters/TintMode.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/filters/ImageBlur.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/filters/ImageCrop.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/filters/ImageEffect.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/graphics/InternalImageDescriptor.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/types/color/ColorExtensions.swift Base
|
||||
cp $1/src/base/ios/yandex/thick_ui/types/color/RGBAColorExtensions.swift Base
|
||||
@@ -126,6 +134,11 @@ cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ImageContaining.swift
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ImageLayerLayout.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ObjectsReusability.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/RemoteImageView.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/RemoteImageViewContainer.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/RemoteImageViewContentProtocol.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/MetalImageView.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ImageViewProtocol.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ImageRedrawingStyle.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/Resetting.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/Theme.swift CommonCore
|
||||
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/UIStyles.swift CommonCore
|
||||
|
||||
@@ -9,7 +9,8 @@ import Networking
|
||||
extension DivBackground {
|
||||
func makeBlockBackground(
|
||||
with imageHolderFactory: ImageHolderFactory,
|
||||
expressionResolver: ExpressionResolver
|
||||
expressionResolver: ExpressionResolver,
|
||||
metalImageRenderingEnabled: Bool
|
||||
) -> LayoutKit.Background? {
|
||||
switch self {
|
||||
case let .divLinearGradient(gradient):
|
||||
@@ -38,7 +39,9 @@ extension DivBackground {
|
||||
imageBackground.resolveImageUrl(expressionResolver)
|
||||
),
|
||||
contentMode: imageBackground.resolveContentMode(expressionResolver),
|
||||
alpha: imageBackground.resolveAlpha(expressionResolver)
|
||||
alpha: imageBackground.resolveAlpha(expressionResolver),
|
||||
effects: imageBackground.makeEffects(with: expressionResolver),
|
||||
metalImageRenderingEnabled: metalImageRenderingEnabled
|
||||
)
|
||||
return .image(image)
|
||||
case let .divSolidBackground(solidBackground):
|
||||
@@ -56,3 +59,9 @@ extension DivBackground {
|
||||
}
|
||||
|
||||
extension DivImageBackground: DivImageContentMode {}
|
||||
|
||||
extension DivImageBackground {
|
||||
fileprivate func makeEffects(with resolver: ExpressionResolver) -> [ImageEffect] {
|
||||
filters?.compactMap { $0.makeImageEffect(with: resolver)} ?? []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ extension DivBase {
|
||||
background,
|
||||
to: block,
|
||||
imageHolderFactory: context.imageHolderFactory,
|
||||
expressionResolver: expressionResolver
|
||||
expressionResolver: expressionResolver,
|
||||
metalImageRenderingEnabled: context.flagsInfo.metalImageRenderingEnabled
|
||||
)
|
||||
.addingDecorations(
|
||||
boundary: border.makeBoundaryTrait(with: expressionResolver),
|
||||
@@ -222,7 +223,8 @@ extension DivBase {
|
||||
_ backgrounds: [DivBackground]?,
|
||||
to block: Block,
|
||||
imageHolderFactory: ImageHolderFactory,
|
||||
expressionResolver: ExpressionResolver
|
||||
expressionResolver: ExpressionResolver,
|
||||
metalImageRenderingEnabled: Bool
|
||||
) -> Block {
|
||||
guard let backgrounds = backgrounds else {
|
||||
return block
|
||||
@@ -232,7 +234,8 @@ extension DivBase {
|
||||
if backgrounds.count == 1 {
|
||||
guard let background = backgrounds[0].makeBlockBackground(
|
||||
with: imageHolderFactory,
|
||||
expressionResolver: expressionResolver
|
||||
expressionResolver: expressionResolver,
|
||||
metalImageRenderingEnabled: metalImageRenderingEnabled
|
||||
) else {
|
||||
return block
|
||||
}
|
||||
@@ -245,7 +248,8 @@ extension DivBase {
|
||||
let blockBackgrounds = backgrounds.compactMap {
|
||||
$0.makeBlockBackground(
|
||||
with: imageHolderFactory,
|
||||
expressionResolver: expressionResolver
|
||||
expressionResolver: expressionResolver,
|
||||
metalImageRenderingEnabled: metalImageRenderingEnabled
|
||||
)
|
||||
}
|
||||
guard let background = blockBackgrounds.composite() else {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import Base
|
||||
import CoreGraphics
|
||||
|
||||
extension DivFilter {
|
||||
func makeImageEffect(with resolver: ExpressionResolver) -> ImageEffect {
|
||||
switch self {
|
||||
case .divBlur(let blur):
|
||||
return .blur(radius: CGFloat(blur.resolveRadius(resolver) ?? 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
import BaseUI
|
||||
import CommonCore
|
||||
import Networking
|
||||
import LayoutKit
|
||||
@@ -39,8 +40,35 @@ extension DivImage: DivBlockModeling, DivImageProtocol {
|
||||
height: resolveHeight(context),
|
||||
contentMode: resolveContentMode(expressionResolver),
|
||||
tintColor: resolveTintColor(expressionResolver),
|
||||
tintMode: resolveTintMode(expressionResolver).tintMode,
|
||||
effects: makeEffects(with: expressionResolver),
|
||||
metalImageRenderingEnabled: context.flagsInfo.metalImageRenderingEnabled,
|
||||
appearanceAnimation: appearanceAnimation?.makeAppearanceAnimation(with: expressionResolver)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension DivBlendMode {
|
||||
fileprivate var tintMode: TintMode {
|
||||
switch self {
|
||||
case .sourceIn:
|
||||
return .sourceIn
|
||||
case .sourceAtop:
|
||||
return .sourceAtop
|
||||
case .darken:
|
||||
return .darken
|
||||
case .lighten:
|
||||
return .lighten
|
||||
case .multiply:
|
||||
return .multiply
|
||||
case .screen:
|
||||
return .screen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DivImage {
|
||||
fileprivate func makeEffects(with resolver: ExpressionResolver) -> [ImageEffect] {
|
||||
filters?.compactMap { $0.makeImageEffect(with: resolver)} ?? []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ public final class ImageExtensionHandler: DivExtensionHandler {
|
||||
widthTrait: block.widthTrait,
|
||||
height: block.height,
|
||||
contentMode: block.contentMode,
|
||||
tintColor: block.tintColor
|
||||
tintColor: block.tintColor,
|
||||
tintMode: block.tintMode,
|
||||
effects: block.effects
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ extension ImageBlock {
|
||||
height: height,
|
||||
contentMode: contentMode,
|
||||
tintColor: tintColor,
|
||||
tintMode: tintMode,
|
||||
effects: effects,
|
||||
metalImageRenderingEnabled: metalImageRenderingEnabled,
|
||||
accessibilityElement: accessibilityElement
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ public final class ImageBlock: ImageBaseBlock {
|
||||
public let height: ImageBlockHeight
|
||||
public let contentMode: ImageContentMode
|
||||
public let tintColor: Color?
|
||||
public let tintMode: TintMode?
|
||||
public let effects: [ImageEffect]
|
||||
public let accessibilityElement: AccessibilityElement?
|
||||
public let appearanceAnimation: TransitioningAnimation?
|
||||
public let metalImageRenderingEnabled: Bool
|
||||
@@ -20,6 +22,8 @@ public final class ImageBlock: ImageBaseBlock {
|
||||
height: ImageBlockHeight,
|
||||
contentMode: ImageContentMode,
|
||||
tintColor: Color?,
|
||||
tintMode: TintMode?,
|
||||
effects: [ImageEffect],
|
||||
metalImageRenderingEnabled: Bool = false,
|
||||
accessibilityElement: AccessibilityElement? = nil,
|
||||
appearanceAnimation: TransitioningAnimation? = nil
|
||||
@@ -29,6 +33,8 @@ public final class ImageBlock: ImageBaseBlock {
|
||||
self.height = height
|
||||
self.contentMode = contentMode
|
||||
self.tintColor = tintColor
|
||||
self.tintMode = tintMode
|
||||
self.effects = effects
|
||||
self.metalImageRenderingEnabled = metalImageRenderingEnabled
|
||||
self.accessibilityElement = accessibilityElement
|
||||
self.appearanceAnimation = appearanceAnimation
|
||||
@@ -40,6 +46,8 @@ public final class ImageBlock: ImageBaseBlock {
|
||||
heightTrait: LayoutTrait = .intrinsic,
|
||||
contentMode: ImageContentMode = .default,
|
||||
tintColor: Color? = nil,
|
||||
tintMode: TintMode? = nil,
|
||||
effects: [ImageEffect] = [],
|
||||
metalImageRenderingEnabled: Bool = false,
|
||||
accessibilityElement: AccessibilityElement? = nil,
|
||||
appearanceAnimation: TransitioningAnimation? = nil
|
||||
@@ -50,6 +58,8 @@ public final class ImageBlock: ImageBaseBlock {
|
||||
height: .trait(heightTrait),
|
||||
contentMode: contentMode,
|
||||
tintColor: tintColor,
|
||||
tintMode: tintMode,
|
||||
effects: effects,
|
||||
metalImageRenderingEnabled: metalImageRenderingEnabled,
|
||||
accessibilityElement: accessibilityElement,
|
||||
appearanceAnimation: appearanceAnimation
|
||||
@@ -61,6 +71,8 @@ public final class ImageBlock: ImageBaseBlock {
|
||||
size: CGSize,
|
||||
contentMode: ImageContentMode = .default,
|
||||
tintColor: Color? = nil,
|
||||
tintMode: TintMode? = nil,
|
||||
effects: [ImageEffect] = [],
|
||||
metalImageRenderingEnabled: Bool = false,
|
||||
accessibilityElement: AccessibilityElement? = nil,
|
||||
appearanceAnimation: TransitioningAnimation? = nil
|
||||
@@ -71,6 +83,8 @@ public final class ImageBlock: ImageBaseBlock {
|
||||
heightTrait: .fixed(size.height),
|
||||
contentMode: contentMode,
|
||||
tintColor: tintColor,
|
||||
tintMode: tintMode,
|
||||
effects: effects,
|
||||
metalImageRenderingEnabled: metalImageRenderingEnabled,
|
||||
accessibilityElement: accessibilityElement,
|
||||
appearanceAnimation: appearanceAnimation
|
||||
|
||||
@@ -21,9 +21,9 @@ extension ImageBlock {
|
||||
remoteImageViewContainer.imageHolder = imageHolder
|
||||
}
|
||||
remoteImageViewContainer.contentView.imageContentMode = contentMode
|
||||
if let tintColor = tintColor {
|
||||
remoteImageViewContainer.contentView.imageRedrawingStyle = .init(tintColor: tintColor)
|
||||
}
|
||||
remoteImageViewContainer.contentView.imageRedrawingStyle = ImageRedrawingStyle(tintColor: tintColor,
|
||||
tintMode: tintMode,
|
||||
effects: effects)
|
||||
remoteImageViewContainer.contentView.isUserInteractionEnabled = false
|
||||
remoteImageViewContainer.isUserInteractionEnabled = false
|
||||
remoteImageViewContainer.applyAccessibility(accessibilityElement)
|
||||
|
||||
@@ -88,8 +88,12 @@ extension BackgroundView {
|
||||
case .solidColor, .tiledImage:
|
||||
view = ColoredView()
|
||||
|
||||
case .image:
|
||||
view = RemoteImageViewContainer(contentView: RemoteImageView())
|
||||
case .image(let backgroundImage):
|
||||
if backgroundImage.metalImageRenderingEnabled {
|
||||
view = RemoteImageViewContainer(contentView: MetalImageView())
|
||||
} else {
|
||||
view = RemoteImageViewContainer(contentView: RemoteImageView())
|
||||
}
|
||||
|
||||
case .ninePatchImage:
|
||||
view = RemoteImageViewContainer(contentView: NinePatchImageView())
|
||||
@@ -148,6 +152,7 @@ extension BackgroundView {
|
||||
imageViewContainer.contentView.clipsToBounds = true
|
||||
imageViewContainer.contentView.alpha = CGFloat(image.alpha)
|
||||
imageViewContainer.contentView.imageContentMode = image.contentMode
|
||||
imageViewContainer.contentView.imageRedrawingStyle = ImageRedrawingStyle(tintColor: nil, effects: image.effects)
|
||||
if imageViewContainer.imageHolder != image.imageHolder {
|
||||
imageViewContainer.imageHolder = image.imageHolder
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import BaseUI
|
||||
import CommonCore
|
||||
|
||||
public struct BackgroundImage {
|
||||
let imageHolder: ImageHolder
|
||||
let contentMode: ImageContentMode
|
||||
let alpha: Double
|
||||
let effects: [ImageEffect]
|
||||
let metalImageRenderingEnabled: Bool
|
||||
|
||||
public init(
|
||||
imageHolder: ImageHolder,
|
||||
contentMode: ImageContentMode = .default,
|
||||
alpha: Double = 1.0
|
||||
alpha: Double = 1.0,
|
||||
effects: [ImageEffect],
|
||||
metalImageRenderingEnabled: Bool
|
||||
) {
|
||||
self.imageHolder = imageHolder
|
||||
self.contentMode = contentMode
|
||||
self.alpha = alpha
|
||||
self.effects = effects
|
||||
self.metalImageRenderingEnabled = metalImageRenderingEnabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +27,8 @@ extension BackgroundImage: Equatable {
|
||||
public static func ==(lhs: BackgroundImage, rhs: BackgroundImage) -> Bool {
|
||||
lhs.imageHolder == rhs.imageHolder &&
|
||||
lhs.contentMode == rhs.contentMode &&
|
||||
lhs.alpha == rhs.alpha
|
||||
lhs.alpha == rhs.alpha &&
|
||||
lhs.effects == rhs.effects
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 25 KiB |