Commit fa078b67 by Dmitriy Stepanets

- Removed Palette library

- Added previous native average color logic
parent e085a688
......@@ -6,19 +6,18 @@
//
import UIKit
import Palette
extension UIImage {
var averageColor: UIColor? {
let palette = self.createPalette()
var vibrantSwatch = palette.mutedSwatch
if nil == vibrantSwatch {
vibrantSwatch = palette.darkMutedSwatch
}
if nil == vibrantSwatch {
vibrantSwatch = palette.lightMutedSwatch
}
guard let inputImage = CIImage(image: self) else { return nil }
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
guard let outputImage = filter.outputImage else { return nil }
return vibrantSwatch?.color
var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: kCFNull!])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
}
}
......@@ -119,21 +119,12 @@ class ShortsManager {
}
avgColorQueue.addOperation {
guard let color = shortImage.averageColor else {
guard let color = shortImage.averageColor?.darker(by: 20) else {
completion(nil, short.id)
return
}
self.colorCache.setObject(color, forKey: short.id as NSString)
completion(color, short.id)
}
// shortImage.averageColor { avgColor in
// guard let color = avgColor else {
// completion(nil)
// return
// }
// self.colorCache.setObject(color, forKey: short.id as NSString)
// completion(color)
// }
}
}
......@@ -10,7 +10,7 @@ import OneWeatherCore
import Nuke
protocol ShortsItemCellDelegate: AnyObject {
func averageColor(forImage image:UIImage, identifier:String) -> UIColor?
func averageColor(for short: ShortsItem, shortImage: UIImage, completion: @escaping (_ avgColor: UIColor?,_ shortId: String?) -> Void)
func didSelectLike(onItem item: ShortsItem)
func didSelectMore(onItem item: ShortsItem)
}
......@@ -35,7 +35,7 @@ class ShortsItemCell: UITableViewCell {
private var shareButton = UIButton()
private var likeButton = UIButton()
private let swipeDownView = UIView()
private var currentShorts:ShortsItem?
private var currentShort:ShortsItem?
//Public
weak var delegate:ShortsItemCellDelegate?
......@@ -68,33 +68,32 @@ class ShortsItemCell: UITableViewCell {
//Public
func configure(shortsItem:ShortsItem, tableWidth:CGFloat) {
currentShorts = shortsItem
currentShort = shortsItem
if let backgroundImage = shortsItem.getOverlayImage(for: tableWidth) {
Nuke.loadImage(with: backgroundImage.url, into: backgroundImageView) {[weak self] result in
guard let self = self else { return }
switch result {
case .success(let imageResponse):
onMain {
self.backgroundImageView.image = imageResponse.image
self.ctaButton.isHidden = false
self.likeButton.isHidden = false
//Update ImageView size
let aspect = imageResponse.image.size.width / imageResponse.image.size.height
self.backgroundImageView.snp.remakeConstraints { remake in
remake.left.top.right.equalToSuperview()
remake.width.equalTo(self.backgroundImageView.snp.height).multipliedBy(aspect)
}
self.delegate?.averageColor(for: shortsItem, shortImage: imageResponse.image, completion: {[weak self] avgColor, shortId in
guard self?.currentShort?.id == shortId else { return }
if let cachedColor = self.delegate?.averageColor(forImage: imageResponse.image,
identifier: backgroundImage.url.absoluteString) {
self.contentView.backgroundColor = cachedColor
onMain {
self?.ctaButton.isHidden = false
self?.likeButton.isHidden = false
//Update ImageView size
let aspect = imageResponse.image.size.width / imageResponse.image.size.height
self?.backgroundImageView.snp.remakeConstraints { remake in
remake.left.top.right.equalToSuperview()
remake.width.equalTo(self!.backgroundImageView.snp.height).multipliedBy(aspect)
}
if let color = avgColor {
self?.contentView.backgroundColor = color
}
}
else {
self.contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
}
}
})
default:
self.ctaButton.isHidden = true
self.likeButton.isHidden = true
......@@ -151,12 +150,12 @@ class ShortsItemCell: UITableViewCell {
//Private
@objc private func handleCtaButton() {
guard let shorts = currentShorts else { return }
guard let shorts = currentShort else { return }
delegate?.didSelectMore(onItem: shorts)
}
@objc private func handleLikeButton() {
guard let shorts = currentShorts else { return }
guard let shorts = currentShort else { return }
delegate?.didSelectLike(onItem: shorts)
}
}
......
......@@ -26,7 +26,9 @@ class ShortsViewController: UIViewController {
case weatherFacts = "weather_facts"
}
private let colorCache = NSCache<NSString, UIColor>()
private let kAnimationKey = "com.oneWeather.scrollView.snappingAnimation"
private let avgColorQueue = OperationQueue()
private let coordinator:ShortsCoordinator
private let viewModel = ShortsViewModel(shortsManager: ShortsManager.shared)
private let tableView = UITableView()
......@@ -404,19 +406,23 @@ extension ShortsViewController: ShortsItemCellDelegate {
return UIImage(cgImage: crop, scale: UIScreen.main.scale, orientation: .up)
}
func averageColor(forImage image: UIImage, identifier: String) -> UIColor? {
if let cachedColor = self.averageColorCache[identifier] {
return cachedColor
func averageColor(for short:ShortsItem, shortImage: UIImage, completion: @escaping (UIColor?, String?) -> Void) {
if let cachedColor = colorCache.object(forKey: short.id as NSString) {
completion(cachedColor, short.id)
return
}
else {
if let bottomPartImage = self.bottomPart(of: image),
let color = bottomPartImage.averageColor {
self.averageColorCache[identifier] = color
return color
}
avgColorQueue.addOperation {
guard
let bottomPartImage = self.bottomPart(of: shortImage),
let color = bottomPartImage.averageColor
else {
return nil
completion(nil, short.id)
return
}
self.colorCache.setObject(color, forKey: short.id as NSString)
completion(color, short.id)
}
}
......
......@@ -9,12 +9,6 @@ import UIKit
import Nuke
import OneWeatherCore
protocol ShortsCollectionCellDelegate: AnyObject {
func averageColor(forImage: UIImage,
identifier: String,
completion:@escaping (_ color: UIColor?,_ shortId: String?) -> Void)
}
class ShortsCollectionViewCell: UICollectionViewCell {
//Private
private let imageView = UIImageView()
......@@ -22,9 +16,6 @@ class ShortsCollectionViewCell: UICollectionViewCell {
private let footerLabel = UILabel()
private var currentShortId: String?
//Public
weak var delegate: ShortsCollectionCellDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
......@@ -59,23 +50,6 @@ class ShortsCollectionViewCell: UICollectionViewCell {
break
}
}
// Nuke.loadImage(with: imageRequest, into: imageView) {[weak self] result in
// switch result {
// case .success(let imageResponse):
// ShortsManager.shared.backgroundColor(for: shortsItem, shortImage: imageResponse.image) { avgColor, shortId in
// guard self?.currentShortId == shortId else { return }
//
// onMain {
// if let color = avgColor {
// self?.footerView.backgroundColor = color
// }
// }
// }
// default:
// break
// }
// }
}
else {
imageView.image = nil
......
......@@ -78,7 +78,6 @@ extension ShortsView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ShortsCollectionViewCell.kIdentifier,
for: indexPath) as! ShortsCollectionViewCell
cell.delegate = self
cell.configure(shortsItem: shorts[indexPath.row])
return cell
}
......@@ -134,45 +133,3 @@ extension ShortsView: UICollectionViewDataSourcePrefetching {
prefetcher.stopPrefetching(with: urls)
}
}
//MARK:- ShortsCollectionCell Delegate
extension ShortsView: ShortsCollectionCellDelegate {
func averageColor(forImage: UIImage,
identifier: String,
completion: @escaping (UIColor?, String?) -> Void
) {
// self.avgColorReadQueue.addOperation {
// if let cachedColor = self.averageColorCache[identifier] {
// completion(cachedColor, identifier)
// }
// else {
// self.avgColorProcessQueue.addOperation {
// if let color = forImage.averageColor {
// self.avgColorReadQueue.addOperation {
// self.averageColorCache[identifier] = color.isDarkColor ? color : color.darker(by: 30)
// }
// completion(color, identifier)
// }
// else {
// completion(nil, identifier)
// }
// }
// }
// }
if let cachedColor = self.averageColorCache[identifier] {
completion(cachedColor, identifier)
}
else {
self.avgColorProcessQueue.addOperation {
if let color = forImage.averageColor {
self.averageColorCache[identifier] = color.isDarkColor ? color : color.darker(by: 30)
completion(color, identifier)
}
else {
completion(nil, identifier)
}
}
}
}
}
......@@ -41,7 +41,6 @@ def application_pods
pod 'SnapKit'
pod 'BezierKit'
pod 'lottie-ios'
pod 'Palette', :git => 'https://github.com/galandezzz/ios-Palette.git'
pod 'Cirque', :git => 'https://github.com/StepanetsDmtry/Cirque.git'
pod 'AlgoliaSearchClient', '~> 8.2'
pod 'PKHUD', '~> 5.0'
......
......@@ -182,7 +182,6 @@ PODS:
- Nuke-WebP-Plugin (5.0.0):
- libwebp (= 1.1.0)
- Nuke (~> 9.0)
- Palette (1.0.6)
- PKHUD (5.3.0)
- pop (1.0.11)
- PromisesObjC (2.0.0)
......@@ -211,7 +210,6 @@ DEPENDENCIES:
- MORichNotification
- Nuke
- Nuke-WebP-Plugin
- Palette (from `https://github.com/galandezzz/ios-Palette.git`)
- PKHUD (~> 5.0)
- pop (from `https://github.com/facebook/pop.git`)
- SnapKit
......@@ -262,8 +260,6 @@ SPEC REPOS:
EXTERNAL SOURCES:
Cirque:
:git: https://github.com/StepanetsDmtry/Cirque.git
Palette:
:git: https://github.com/galandezzz/ios-Palette.git
pop:
:git: https://github.com/facebook/pop.git
Swarm:
......@@ -274,9 +270,6 @@ CHECKOUT OPTIONS:
Cirque:
:commit: ceb7ba910a35973cbcd41c73a62be6305aed4d13
:git: https://github.com/StepanetsDmtry/Cirque.git
Palette:
:commit: 9b5b2831367fb32fac0ec97551724f77978a7e85
:git: https://github.com/galandezzz/ios-Palette.git
pop:
:commit: 87d1f8b74cdaa4699d7a9f6be1ff5202014c581f
:git: https://github.com/facebook/pop.git
......@@ -320,7 +313,6 @@ SPEC CHECKSUMS:
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
Nuke: 6f400a4ea957e09149ec335a3c6acdcc814d89e4
Nuke-WebP-Plugin: a79a97be508453ce5c36b78989595cbbc19c2deb
Palette: aa21bf41603ef85b1d735ae7e17f7685aa21f192
PKHUD: 98f3e4bc904b9c916f1c5bb6d765365b5357291b
pop: ae3ae187018759968252242e175c21f7f9be5dd2
PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58
......@@ -328,6 +320,6 @@ SPEC CHECKSUMS:
Swarm: 95393cd52715744c94e3a8475bc20b4de5d79f35
XMLCoder: f884dfa894a6f8b7dce465e4f6c02963bf17e028
PODFILE CHECKSUM: 23d1daf4e648e9812f22d209e2a82814a30ccaeb
PODFILE CHECKSUM: 361ffeef3b27080f70a30707521c689c5841509c
COCOAPODS: 1.10.2
{
"name": "Palette",
"version": "1.0.6",
"summary": "Color palette generation from image written in Swift",
"homepage": "https://github.com/galandezzz/Palette",
"license": "MIT",
"authors": {
"Egor Snitsar": "fearum@icloud.com"
},
"platforms": {
"ios": "9.0"
},
"swift_versions": "5.0",
"source": {
"git": "https://github.com/galandezzz/Palette.git",
"tag": "v1.0.6"
},
"source_files": [
"Source/*",
"Source/*/*"
],
"swift_version": "5.0"
}
......@@ -182,7 +182,6 @@ PODS:
- Nuke-WebP-Plugin (5.0.0):
- libwebp (= 1.1.0)
- Nuke (~> 9.0)
- Palette (1.0.6)
- PKHUD (5.3.0)
- pop (1.0.11)
- PromisesObjC (2.0.0)
......@@ -211,7 +210,6 @@ DEPENDENCIES:
- MORichNotification
- Nuke
- Nuke-WebP-Plugin
- Palette (from `https://github.com/galandezzz/ios-Palette.git`)
- PKHUD (~> 5.0)
- pop (from `https://github.com/facebook/pop.git`)
- SnapKit
......@@ -262,8 +260,6 @@ SPEC REPOS:
EXTERNAL SOURCES:
Cirque:
:git: https://github.com/StepanetsDmtry/Cirque.git
Palette:
:git: https://github.com/galandezzz/ios-Palette.git
pop:
:git: https://github.com/facebook/pop.git
Swarm:
......@@ -274,9 +270,6 @@ CHECKOUT OPTIONS:
Cirque:
:commit: ceb7ba910a35973cbcd41c73a62be6305aed4d13
:git: https://github.com/StepanetsDmtry/Cirque.git
Palette:
:commit: 9b5b2831367fb32fac0ec97551724f77978a7e85
:git: https://github.com/galandezzz/ios-Palette.git
pop:
:commit: 87d1f8b74cdaa4699d7a9f6be1ff5202014c581f
:git: https://github.com/facebook/pop.git
......@@ -320,7 +313,6 @@ SPEC CHECKSUMS:
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
Nuke: 6f400a4ea957e09149ec335a3c6acdcc814d89e4
Nuke-WebP-Plugin: a79a97be508453ce5c36b78989595cbbc19c2deb
Palette: aa21bf41603ef85b1d735ae7e17f7685aa21f192
PKHUD: 98f3e4bc904b9c916f1c5bb6d765365b5357291b
pop: ae3ae187018759968252242e175c21f7f9be5dd2
PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58
......@@ -328,6 +320,6 @@ SPEC CHECKSUMS:
Swarm: 95393cd52715744c94e3a8475bc20b4de5d79f35
XMLCoder: f884dfa894a6f8b7dce465e4f6c02963bf17e028
PODFILE CHECKSUM: 23d1daf4e648e9812f22d209e2a82814a30ccaeb
PODFILE CHECKSUM: 361ffeef3b27080f70a30707521c689c5841509c
COCOAPODS: 1.10.2
MIT License
Copyright (c) 2019 Egor Snitsar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Palette
Color palette generation from image written in Swift.
## Installation
**Cocoapods:**
`pod 'Palette', :git => 'https://github.com/galandezzz/ios-Palette.git'`
**Carthage:**
`github "galandezzz/Palette" ~> 1.0`
## Usage
### Targets
There are six built-in targets for palette generation:
- Light vibrant
- Vibrant
- Dark vibrant
- Light muted
- Muted
- Dark muted
You can also create your own targets using `Target.Builder` class:
```
let target = Target.Builder()
.with(targetSaturation: 0.7)
.with(targetLightness: 0.7)
.build()
```
### Synchronous Palette generation
```
let palette = Palette.from(image: YOUR_IMAGE).generate()
view.backgroundColor = palette.vibrantColor
```
or simply
```
view.backgroundColor = YOUR_IMAGE.createPalette().vibrantColor
```
### Asynchornous Palette generation
```
Palette.from(image: YOUR_IMAGE).generate { view.backgroundColor = $0.vibrantColor }
```
or using extension on `UIImage`
```
YOUR_IMAGE.createPalette { view.backgroudColor = $0.vibrantColor }
```
## License
Palette is available under the MIT license. See the LICENSE file for more info.
//
// Color.swift
// Palette
//
// Created by Egor Snitsar on 08.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import UIKit
extension UIColor {
internal convenience init(_ color: Color) {
self.init(red: CGFloat(color.red) / 255.0,
green: CGFloat(color.green) / 255.0,
blue: CGFloat(color.blue) / 255.0,
alpha: 1.0)
}
}
internal struct Color: Hashable, Comparable, CustomDebugStringConvertible {
internal enum Width: Int {
case normal = 8
case quantized = 5
}
internal init(_ storage: Int, width: Width = .normal) {
self.storage = storage
self.width = width
}
internal init(_ components: [Int], width: Width = .normal) {
self.storage = ColorConverter.packColor(components: components, width: width.rawValue)
self.width = width
}
internal init(reducingAlpha components: [Int], width: Width = .normal) {
let alpha = components[3]
let cs = components[0...2].map { ColorConverter.reduceAlpha(for: $0, alpha: alpha) }
self.init(cs, width: width)
}
internal init(_ components: [UInt8], width: Width = .normal) {
self.init(components.map { Int($0) }, width: width)
}
internal init(reducingAlpha components: [UInt8], width: Width = .normal) {
self.init(reducingAlpha: components.map { Int($0) }, width: width)
}
internal var red: Int {
return (storage >> (width.rawValue * 2)) & mask
}
internal var green: Int {
return (storage >> width.rawValue) & mask
}
internal var blue: Int {
return storage & mask
}
internal var hsl: HSL {
return ColorConverter.colorToHSL(self)
}
internal var rgb: RGB {
return (red, green, blue)
}
internal var quantized: Color {
return color(with: .quantized)
}
internal var normalized: Color {
return color(with: .normal)
}
internal let width: Width
// MARK: - CustomDebugStringConvertible
var debugDescription: String {
return """
Red: \(red), Green: \(green), Blue: \(blue)
Hue: \(hsl.h), Saturation: \(hsl.s), Brightness: \(hsl.l)
"""
}
// MARK: - Comparable
internal static func < (lhs: Color, rhs: Color) -> Bool {
return lhs.storage < rhs.storage
}
// MARK: - Private
private let storage: Int
private var mask: Int {
return (1 << width.rawValue) - 1
}
private func color(with width: Width) -> Color {
let r = ColorConverter.modifyWordWidth(red, currentWidth: self.width.rawValue, targetWidth: width.rawValue)
let g = ColorConverter.modifyWordWidth(green, currentWidth: self.width.rawValue, targetWidth: width.rawValue)
let b = ColorConverter.modifyWordWidth(blue, currentWidth: self.width.rawValue, targetWidth: width.rawValue)
return Color([r, g, b], width: width)
}
}
//
// ColorUtils.swift
// Palette
//
// Created by Egor Snitsar on 10.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import Foundation
internal struct ColorConverter {
internal static func colorToHSL(_ color: Color) -> HSL {
let r = CGFloat(color.red) / 255.0
let g = CGFloat(color.green) / 255.0
let b = CGFloat(color.blue) / 255.0
let cmin = min(r, g, b)
let cmax = max(r, g, b)
let delta = cmax - cmin
var h: CGFloat = 0.0
var s: CGFloat = 0.0
let l = (cmax + cmin) / 2.0
if cmax != cmin {
switch cmax {
case r:
h = ((g - b) / delta).truncatingRemainder(dividingBy: 6.0)
case g:
h = ((b - r) / delta) + 2.0
default:
h = ((r - g) / delta) + 4.0
}
s = delta / (1 - abs(2 * l - 1))
}
h = (h * 60.0).truncatingRemainder(dividingBy: 360.0)
if h.isLess(than: .zero) {
h += 360.0
}
return (
h.rounded().limited(.zero, 360.0),
s.limited(.zero, 1.0),
l.limited(.zero, 1.0)
)
}
internal static func reduceAlpha(for value: Int, alpha: Int) -> Int {
guard alpha > .zero else {
return value
}
return Int(CGFloat(value) / CGFloat(alpha) * 255.0)
}
internal static func packColor(components: [Int], width: Int) -> Int {
let mask: Int = (1 << width) - 1
let r = components[0]
let g = components[1]
let b = components[2]
return ((r & mask) << (width * 2)) | ((g & mask) << width) | (b & mask)
}
internal static func packColor(components: [UInt8], width: Int) -> Int {
return packColor(components: components.map { Int($0) }, width: width)
}
internal static func modifyWordWidth(_ value: Int, currentWidth: Int, targetWidth: Int) -> Int {
guard currentWidth != targetWidth else {
return value
}
let newValue: Int
if targetWidth > currentWidth {
newValue = value << (targetWidth - currentWidth)
} else {
newValue = value >> (currentWidth - targetWidth)
}
return newValue & ((1 << targetWidth) - 1)
}
}
private extension Comparable {
func limited(_ lowerBound: Self, _ upperBound: Self) -> Self {
return min(max(lowerBound, self), upperBound)
}
}
//
// ColorCutQuantizer.swift
// Palette
//
// Created by Egor Snitsar on 06.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import UIKit
internal final class ColorCutQuantizer {
internal var quantizedColors = [Palette.Swatch]()
internal init(colors: [Color], maxColorsCount: Int, filters: [PaletteFilter]) {
self.filters = filters
let hist = CountedSet(
colors
.map { $0.quantized }
.filter { !shouldIgnoreColor($0.normalized) }
)
var distinctColors = hist.allObjects
if distinctColors.count <= maxColorsCount {
quantizedColors = distinctColors.map { Palette.Swatch(color: $0.normalized, population: hist.count(for: $0)) }
} else {
quantizedColors = quantizePixels(maxColorsCount: maxColorsCount, colors: &distinctColors, histogram: hist)
}
}
private let filters: [PaletteFilter]
private func shouldIgnoreColor(_ swatch: Palette.Swatch) -> Bool {
return shouldIgnoreColor(swatch._color)
}
private func shouldIgnoreColor(_ color: Color) -> Bool {
return filters.contains { !$0.isAllowed(rgb: color.rgb, hsl: color.hsl) }
}
private func quantizePixels(maxColorsCount: Int, colors: inout [Color], histogram: CountedSet<Color>) -> [Palette.Swatch] {
var queue = PriorityQueue<VBox>() { $0.volume > $1.volume }
queue.enqueue(VBox(lowerIndex: colors.startIndex, upperIndex: colors.index(before: colors.endIndex), colors: colors, histogram: histogram))
splitBoxes(queue: &queue, maxSize: maxColorsCount, colors: &colors, histogram: histogram)
return generateAverageColors(from: queue.elements, colors: colors, histogram: histogram)
}
private func splitBoxes(queue: inout PriorityQueue<VBox>, maxSize: Int, colors: inout [Color], histogram: CountedSet<Color>) {
while queue.count < maxSize {
if let vbox = queue.dequeue(), vbox.canSplit {
if let newBox = vbox.splitBox(colors: &colors, histogram: histogram) {
queue.enqueue(newBox)
}
queue.enqueue(vbox)
} else {
return
}
}
}
private func generateAverageColors(from boxes: [VBox], colors: [Color], histogram: CountedSet<Color>) -> [Palette.Swatch] {
return boxes.compactMap {
let swatch = $0.averageColor(colors: colors, histogram: histogram)
guard !shouldIgnoreColor(swatch) else {
return nil
}
return swatch
}
}
private class VBox {
internal init(lowerIndex: Int, upperIndex: Int, colors: [Color], histogram: CountedSet<Color>) {
self.lowerIndex = lowerIndex
self.upperIndex = upperIndex
fitBox(colors: colors, histogram: histogram)
}
internal var volume: Int {
return (maxRed - minRed + 1) * (maxGreen - minGreen + 1) * (maxBlue - minBlue + 1)
}
internal var canSplit: Bool {
return colorCount > 1
}
internal func splitBox(colors: inout [Color], histogram: CountedSet<Color>) -> VBox? {
guard canSplit else {
return nil
}
let splitPoint = findSplitPoint(colors: &colors, histogram: histogram)
let newBox = VBox(lowerIndex: splitPoint + 1, upperIndex: upperIndex, colors: colors, histogram: histogram)
upperIndex = splitPoint
fitBox(colors: colors, histogram: histogram)
return newBox
}
internal func averageColor(colors: [Color], histogram: CountedSet<Color>) -> Palette.Swatch {
var redSum = 0, greenSum = 0, blueSum = 0, totalCount = 0
colors[lowerIndex...upperIndex].forEach {
let (r, g, b) = $0.rgb
let count = histogram.count(for: $0)
totalCount += count
redSum += count * Int(r)
greenSum += count * Int(g)
blueSum += count * Int(b)
}
let mean: (Int) -> Int = { Int((CGFloat($0) / CGFloat(totalCount)).rounded()) }
let redMean = mean(redSum)
let greenMean = mean(greenSum)
let blueMean = mean(blueSum)
let color = Color([redMean, greenMean, blueMean], width: .quantized)
return Palette.Swatch(color: color.normalized, population: totalCount)
}
// MARK: - Private
private enum Component {
case red
case green
case blue
}
private let lowerIndex: Int
private var upperIndex: Int
private var population = 0
private var minRed = 0, maxRed = 0
private var minGreen = 0, maxGreen = 0
private var minBlue = 0, maxBlue = 0
private var colorCount: Int {
return upperIndex - lowerIndex + 1
}
private func fitBox(colors: [Color], histogram: CountedSet<Color>) {
minRed = Int.max
minGreen = Int.max
minBlue = Int.max
maxRed = Int.min
maxGreen = Int.min
maxBlue = Int.min
for i in (lowerIndex...upperIndex) {
let color = colors[i]
population += histogram.count(for: color)
let r = Int(color.red)
let g = Int(color.green)
let b = Int(color.blue)
if r > maxRed { maxRed = r }
if r < minRed { minRed = r }
if g > maxGreen { maxGreen = g }
if g < minGreen { minGreen = g }
if b > maxBlue { maxBlue = b }
if b < minBlue { minBlue = b }
}
}
private func findLongestComponent() -> Component {
let redLength = maxRed - minRed
let greenLength = maxGreen - minGreen
let blueLength = maxBlue - minBlue
if redLength >= greenLength && redLength >= blueLength {
return .red
} else if greenLength >= redLength && greenLength >= blueLength {
return .green
} else {
return .blue
}
}
private func findSplitPoint(colors: inout [Color], histogram: CountedSet<Color>) -> Int {
let longestComponent = findLongestComponent()
modifySignificantOctet(for: &colors, component: longestComponent, lower: lowerIndex, upper: upperIndex)
colors[lowerIndex...upperIndex].sort()
modifySignificantOctet(for: &colors, component: longestComponent, lower: lowerIndex, upper: upperIndex)
let midPoint = population / 2
var count = 0
for i in (lowerIndex...upperIndex) {
count += histogram.count(for: colors[i])
if count >= midPoint {
return min(upperIndex - 1, i)
}
}
return lowerIndex
}
private func modifySignificantOctet(for colors: inout [Color], component: Component, lower: Int, upper: Int) {
switch component {
case .red:
break
case .green:
for i in (lower...upper) {
let (r, g, b) = colors[i].rgb
colors[i] = Color([g, r, b], width: colors[i].width)
}
case .blue:
for i in (lower...upper) {
let (r, g, b) = colors[i].rgb
colors[i] = Color([b, g, r], width: colors[i].width)
}
}
}
}
}
//
// CountedSet.swift
// Palette
//
// Created by Egor Snitsar on 08.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import Foundation
internal struct CountedSet<T: Hashable> {
internal init(_ array: [T] = []) {
self.storage = NSCountedSet(array: array)
}
internal var allObjects: [T] {
return storage.allObjects as! [T]
}
internal var countedObjects: [T: Int] {
let values = allObjects.map { ($0, count(for: $0)) }
return Dictionary(uniqueKeysWithValues: values)
}
internal func contains(_ object: T) -> Bool {
return storage.contains(object)
}
internal func insert(_ object: T) {
storage.add(object)
}
internal func remove(_ object: T) {
storage.remove(object)
}
internal func count(for object: T) -> Int {
return storage.count(for: object)
}
private let storage: NSCountedSet
}
//
// Heap.swift
// Palette
//
// Created by Egor Snitsar on 07.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import Foundation
internal struct Heap<T> {
/**
* Creates an empty heap.
* The sort function determines whether this is a min-heap or max-heap.
* For comparable data types, > makes a max-heap, < makes a min-heap.
*/
internal init(sort: @escaping (T, T) -> Bool) {
self.orderCriteria = sort
}
/**
* Creates a heap from an array. The order of the array does not matter;
* the elements are inserted into the heap in the order determined by the
* sort function. For comparable data types, '>' makes a max-heap,
* '<' makes a min-heap.
*/
internal init(array: [T], sort: @escaping (T, T) -> Bool) {
self.orderCriteria = sort
configureHeap(from: array)
}
/**
* Configures the max-heap or min-heap from an array, in a bottom-up manner.
* Performance: This runs pretty much in O(n).
*/
internal mutating func configureHeap(from array: [T]) {
nodes = array
for i in stride(from: (nodes.count / 2 - 1), through: 0, by: -1) {
shiftDown(i)
}
}
/** The array that stores the heap's nodes. */
internal var nodes = [T]()
internal var isEmpty: Bool {
return nodes.isEmpty
}
internal var count: Int {
return nodes.count
}
/**
* Returns the index of the parent of the element at index i.
* The element at index 0 is the root of the tree and has no parent.
*/
@inline(__always) internal func parentIndex(ofIndex i: Int) -> Int {
return (i - 1) / 2
}
/**
* Returns the index of the left child of the element at index i.
* Note that this index can be greater than the heap size, in which case
* there is no left child.
*/
@inline(__always) internal func leftChildIndex(ofIndex i: Int) -> Int {
return 2*i + 1
}
/**
* Returns the index of the right child of the element at index i.
* Note that this index can be greater than the heap size, in which case
* there is no right child.
*/
@inline(__always) internal func rightChildIndex(ofIndex i: Int) -> Int {
return 2*i + 2
}
/**
* Returns the maximum value in the heap (for a max-heap) or the minimum
* value (for a min-heap).
*/
internal func peek() -> T? {
return nodes.first
}
/**
* Adds a new value to the heap. This reorders the heap so that the max-heap
* or min-heap property still holds. Performance: O(log n).
*/
internal mutating func insert(_ value: T) {
nodes.append(value)
shiftUp(nodes.count - 1)
}
/**
* Adds a sequence of values to the heap. This reorders the heap so that
* the max-heap or min-heap property still holds. Performance: O(log n).
*/
internal mutating func insert<S: Sequence>(_ sequence: S) where S.Iterator.Element == T {
for value in sequence {
insert(value)
}
}
/**
* Allows you to change an element. This reorders the heap so that
* the max-heap or min-heap property still holds.
*/
internal mutating func replace(index i: Int, value: T) {
guard i < nodes.count else {
return
}
remove(at: i)
insert(value)
}
/**
* Removes the root node from the heap. For a max-heap, this is the maximum
* value; for a min-heap it is the minimum value. Performance: O(log n).
*/
@discardableResult internal mutating func remove() -> T? {
guard !nodes.isEmpty else {
return nil
}
if nodes.count == 1 {
return nodes.removeLast()
} else {
// Use the last node to replace the first one, then fix the heap by
// shifting this new first node into its proper position.
let value = nodes[0]
nodes[0] = nodes.removeLast()
shiftDown(0)
return value
}
}
/**
* Removes an arbitrary node from the heap. Performance: O(log n).
* Note that you need to know the node's index.
*/
@discardableResult internal mutating func remove(at index: Int) -> T? {
guard index < nodes.count else {
return nil
}
let size = nodes.count - 1
if index != size {
nodes.swapAt(index, size)
shiftDown(from: index, until: size)
shiftUp(index)
}
return nodes.removeLast()
}
/**
* Takes a child node and looks at its parents; if a parent is not larger
* (max-heap) or not smaller (min-heap) than the child, we exchange them.
*/
internal mutating func shiftUp(_ index: Int) {
var childIndex = index
let child = nodes[childIndex]
var parentIndex = self.parentIndex(ofIndex: childIndex)
while childIndex > 0 && orderCriteria(child, nodes[parentIndex]) {
nodes[childIndex] = nodes[parentIndex]
childIndex = parentIndex
parentIndex = self.parentIndex(ofIndex: childIndex)
}
nodes[childIndex] = child
}
/**
* Looks at a parent node and makes sure it is still larger (max-heap) or
* smaller (min-heap) than its childeren.
*/
internal mutating func shiftDown(from index: Int, until endIndex: Int) {
let leftChildIndex = self.leftChildIndex(ofIndex: index)
let rightChildIndex = leftChildIndex + 1
// Figure out which comes first if we order them by the sort function:
// the parent, the left child, or the right child. If the parent comes
// first, we're done. If not, that element is out-of-place and we make
// it "float down" the tree until the heap property is restored.
var first = index
if leftChildIndex < endIndex && orderCriteria(nodes[leftChildIndex], nodes[first]) {
first = leftChildIndex
}
if rightChildIndex < endIndex && orderCriteria(nodes[rightChildIndex], nodes[first]) {
first = rightChildIndex
}
if first == index { return }
nodes.swapAt(index, first)
shiftDown(from: first, until: endIndex)
}
internal mutating func shiftDown(_ index: Int) {
shiftDown(from: index, until: nodes.count)
}
/**
* Determines how to compare two nodes in the heap.
* Use '>' for a max-heap or '<' for a min-heap,
* or provide a comparing method if the heap is made
* of custom elements, for example tuples.
*/
private var orderCriteria: (T, T) -> Bool
}
// MARK: - Searching
extension Heap where T: Equatable {
/** Get the index of a node in the heap. Performance: O(n). */
internal func index(of node: T) -> Int? {
return nodes.firstIndex { $0 == node }
}
/** Removes the first occurrence of a node from the heap. Performance: O(n log n). */
@discardableResult internal mutating func remove(node: T) -> T? {
guard let index = index(of: node) else {
return nil
}
return remove(at: index)
}
}
//
// PriorityQueue.swift
// Palette
//
// Created by Egor Snitsar on 07.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import Foundation
internal struct PriorityQueue<T> {
private var heap: Heap<T>
internal var elements: [T] {
return heap.nodes
}
/*
To create a max-priority queue, supply a > sort function. For a min-priority
queue, use <.
*/
internal init(sort: @escaping (T, T) -> Bool) {
heap = Heap(sort: sort)
}
internal var isEmpty: Bool {
return heap.isEmpty
}
internal var count: Int {
return heap.count
}
internal func peek() -> T? {
return heap.peek()
}
internal mutating func enqueue(_ element: T) {
heap.insert(element)
}
internal mutating func dequeue() -> T? {
return heap.remove()
}
/*
Allows you to change the priority of an element. In a max-priority queue,
the new priority should be larger than the old one; in a min-priority queue
it should be smaller.
*/
internal mutating func changePriority(index i: Int, value: T) {
return heap.replace(index: i, value: value)
}
}
extension PriorityQueue where T: Equatable {
internal func index(of element: T) -> Int? {
return heap.index(of: element)
}
}
//
// Palette.swift
// Palette
//
// Created by Egor Snitsar on 05.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import UIKit
public final class Palette {
// MARK: - Public
public let swatches: [Swatch]
public class func from(image: UIImage) -> Builder {
return Builder(image: image)
}
public var lightVibrantSwatch: Swatch? {
return swatch(for: .lightVibrant)
}
public var lightVibrantColor: UIColor? {
return lightVibrantSwatch?.color
}
public var vibrantSwatch: Swatch? {
return swatch(for: .vibrant)
}
public var vibrantColor: UIColor? {
return vibrantSwatch?.color
}
public var darkVibrantSwatch: Swatch? {
return swatch(for: .darkVibrant)
}
public var darkVibrantColor: UIColor? {
return darkVibrantSwatch?.color
}
public var lightMutedSwatch: Swatch? {
return swatch(for: .lightMuted)
}
public var lightMutedColor: UIColor? {
return lightMutedSwatch?.color
}
public var mutedSwatch: Swatch? {
return swatch(for: .muted)
}
public var mutedColor: UIColor? {
return mutedSwatch?.color
}
public var darkMutedSwatch: Swatch? {
return swatch(for: .darkMuted)
}
public var darkMutedColor:UIColor? {
return darkMutedSwatch?.color
}
public private(set) lazy var dominantSwatch: Swatch? = {
return swatches.max { $0.population < $1.population }
}()
public var dominantColor: UIColor? {
return dominantSwatch?.color
}
public func swatch(for target: Target) -> Swatch? {
return selectedSwatches[target]
}
public func color(for target: Target) -> UIColor? {
return swatch(for: target)?.color
}
// MARK: - Internal
internal init(swatches: [Swatch], targets: [Target]) {
self.swatches = swatches
self.targets = targets
}
internal func generate() {
targets.forEach {
$0.normalizeWeights()
selectedSwatches[$0] = scoredSwatch(for: $0)
}
usedColors.removeAll()
}
// MARK: - Private
private let targets: [Target]
private var selectedSwatches = [Target: Swatch]()
private var usedColors = Set<Color>()
private func scoredSwatch(for target: Target) -> Swatch? {
guard let swatch = maxScoredSwatch(for: target) else {
return nil
}
if target.isExclusive {
usedColors.insert(swatch._color)
}
return swatch
}
private func maxScoredSwatch(for target: Target) -> Swatch? {
let result = swatches
.filter { shouldBeScored($0, for: target) }
.map { (swatch: $0, score: score($0, target: target)) }
.max { $0.score < $1.score }
return result?.swatch
}
private func shouldBeScored(_ swatch: Swatch, for target: Target) -> Bool {
let hsl = swatch.hsl
return (target.minimumSaturation...target.maximumSaturation).contains(hsl.s)
&& (target.minimumLightness...target.maximumLightness).contains(hsl.l)
&& !usedColors.contains(swatch._color)
}
private func score(_ swatch: Swatch, target: Target) -> CGFloat {
let hsl = swatch.hsl
let maxPopulation = CGFloat(dominantSwatch?.population ?? 1)
let saturationScore = target.saturationWeight * (1 - abs(hsl.s - target.targetSaturation))
let lightnessScore = target.lightnessWeight * (1 - abs(hsl.l - target.targetLightness))
let populationScore = target.populationWeight * CGFloat(swatch.population) / maxPopulation
return saturationScore + lightnessScore + populationScore
}
}
//
// PaletteBuilder.swift
// Palette
//
// Created by Egor Snitsar on 05.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import UIKit
extension Palette {
public final class Builder {
// MARK: - Public
public func with(maximumColorsCount: Int) -> Builder {
self.maxColorsCount = maximumColorsCount
return self
}
public func with(resizeArea: CGFloat) -> Builder {
self.resizeArea = resizeArea
return self
}
public func byRemovingFilters() -> Builder {
self.filters.removeAll()
return self
}
public func byAddingFilter(_ filter: PaletteFilter) -> Builder {
self.filters.append(filter)
return self
}
public func byRemovingTargets() -> Builder {
self.targets.removeAll()
return self
}
public func byAddingTarget(_ target: Target) -> Builder {
self.targets.append(target)
return self
}
public func generate() -> Palette {
let swatches: [Swatch]
if let image = image {
let scaledImage = scaleDownImage(image, to: resizeArea)
let colors = calculateColors(from: scaledImage)
let quantizer = ColorCutQuantizer(colors: colors, maxColorsCount: maxColorsCount, filters: filters)
swatches = quantizer.quantizedColors
} else {
swatches = self.swatches
}
let p = Palette(swatches: swatches, targets: targets)
p.generate()
return p
}
public func generate(_ completion: @escaping (Palette) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let palette = self.generate()
DispatchQueue.main.async {
completion(palette)
}
}
}
// MARK: - Internal
internal init(image: UIImage) {
self.image = image
self.filters.append(DefaultFilter())
self.targets.append(.lightVibrant)
self.targets.append(.vibrant)
self.targets.append(.darkVibrant)
self.targets.append(.lightMuted)
self.targets.append(.muted)
self.targets.append(.darkMuted)
}
internal init(swatches: [Swatch]) {
self.image = nil
self.filters.append(DefaultFilter())
self.swatches = swatches
}
// MARK: - Private
private struct Constants {
static let defaultMaxColorsCount = 16
static let defaultResizeBitmapArea: CGFloat = 112.0 * 112.0
}
private var maxColorsCount = Constants.defaultMaxColorsCount
private var resizeArea = Constants.defaultResizeBitmapArea
private let image: UIImage?
private var swatches = [Swatch]()
private var targets = [Target]()
private var filters = [PaletteFilter]()
private func scaleDownImage(_ image: UIImage, to resizeArea: CGFloat) -> UIImage {
let bitmapArea = image.size.width * image.size.height
guard bitmapArea > resizeArea else {
return image
}
let ratio = sqrt(resizeArea / bitmapArea)
let width = ceil(ratio * image.size.width)
let height = ceil(ratio * image.size.height)
let size = CGSize(width: width, height: height)
UIGraphicsBeginImageContext(size)
image.draw(in: CGRect(origin: .zero, size: size))
let resultImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return resultImage ?? image
}
private func calculateColors(from image: UIImage) -> [Color] {
guard let cgImage = image.cgImage else {
return []
}
let width = cgImage.width
let height = cgImage.height
let bytesPerRow = width * 4
let bytesCount = bytesPerRow * height
let colorSpace = CGColorSpaceCreateDeviceRGB()
var data = Array(repeating: UInt8(0), count: bytesCount)
let context = CGContext(data: &data,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
let size = CGSize(width: width, height: height)
let rect = CGRect(origin: .zero, size: size)
context?.draw(cgImage, in: rect)
return data.chunk(into: 4).map { Color(reducingAlpha: $0) }
}
}
}
private extension Collection where Index: Strideable {
func chunk(into size: Index.Stride) -> [[Element]] {
return stride(from: startIndex, to: endIndex, by: size).map {
Array(self[$0 ..< Swift.min($0.advanced(by: size), endIndex)])
}
}
}
//
// PaletteFilter.swift
// Palette
//
// Created by Egor Snitsar on 06.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import UIKit
public protocol PaletteFilter {
func isAllowed(rgb: RGB, hsl: HSL) -> Bool
}
internal struct DefaultFilter: PaletteFilter {
private struct Constants {
static let blackMaxLightness: CGFloat = 0.05
static let whiteMinLightness: CGFloat = 0.95
static let iLineHueRange: ClosedRange<CGFloat> = (10...37)
static let iLineSaturationRange: ClosedRange<CGFloat> = (0...0.82)
}
func isAllowed(rgb: RGB, hsl: HSL) -> Bool {
return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl)
}
private func isBlack(_ hsl: HSL) -> Bool {
return hsl.l <= Constants.blackMaxLightness
}
private func isWhite(_ hsl: HSL) -> Bool {
return hsl.l >= Constants.whiteMinLightness
}
private func isNearRedILine(_ hsl: HSL) -> Bool {
return Constants.iLineHueRange.contains(hsl.h) && Constants.iLineSaturationRange.contains(hsl.s)
}
}
//
// PaletteSwatch.swift
// Palette
//
// Created by Egor Snitsar on 06.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import UIKit
public typealias RGB = (r: Int, g: Int, b: Int)
public typealias HSL = (h: CGFloat, s: CGFloat, l: CGFloat)
extension Palette {
public final class Swatch: CustomDebugStringConvertible {
public private(set) lazy var color = UIColor(_color)
public private(set) lazy var hsl: HSL = _color.hsl
public private(set) lazy var rgb: RGB = _color.rgb
public let population: Int
public var debugDescription: String {
return """
Color: \(String(describing: _color))
Population: \(population)
"""
}
internal init(color: Color, population: Int) {
self._color = color
self.population = population
}
internal let _color: Color
}
}
//
// Target.swift
// Palette
//
// Created by Egor Snitsar on 05.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import UIKit
extension Target {
public static let lightVibrant: Target = {
var result = Target()
result.setDefaultLightLightnessValues()
result.setDefaultVibrantSaturationValues()
return result
}()
public static let vibrant: Target = {
var result = Target()
result.setDefaultNormalLightnessValues()
result.setDefaultVibrantSaturationValues()
return result
}()
public static let darkVibrant: Target = {
var result = Target()
result.setDefaultDarkLightnessValues()
result.setDefaultVibrantSaturationValues()
return result
}()
public static let lightMuted: Target = {
var result = Target()
result.setDefaultLightLightnessValues()
result.setDefaultMutedSaturationValues()
return result
}()
public static let muted: Target = {
var result = Target()
result.setDefaultNormalLightnessValues()
result.setDefaultMutedSaturationValues()
return result
}()
public static let darkMuted: Target = {
var result = Target()
result.setDefaultDarkLightnessValues()
result.setDefaultMutedSaturationValues()
return result
}()
}
public final class Target: Hashable {
// MARK: - Public
public internal(set) var minimumSaturation: CGFloat {
get {
return saturation.min
}
set {
saturation.min = newValue
}
}
public internal(set) var targetSaturation: CGFloat {
get {
return saturation.target
}
set {
saturation.target = newValue
}
}
public internal(set) var maximumSaturation: CGFloat {
get {
return saturation.max
}
set {
saturation.max = newValue
}
}
public internal(set) var minimumLightness: CGFloat {
get {
return lightness.min
}
set {
lightness.min = newValue
}
}
public internal(set) var targetLightness: CGFloat {
get {
return lightness.target
}
set {
lightness.target = newValue
}
}
public internal(set) var maximumLightness: CGFloat {
get {
return lightness.max
}
set {
lightness.max = newValue
}
}
public internal(set) var saturationWeight: CGFloat {
get {
return weights.saturation
}
set {
weights.saturation = newValue
}
}
public internal(set) var lightnessWeight: CGFloat {
get {
return weights.lightness
}
set {
weights.lightness = newValue
}
}
public internal(set) var populationWeight: CGFloat {
get {
return weights.population
}
set {
weights.population = newValue
}
}
public internal(set) var isExclusive: Bool = true
// MARK: - Internal
internal init() {}
internal init(_ other: Target) {
self.saturation = other.saturation
self.lightness = other.lightness
self.weights = other.weights
}
// MARK: - Hashable
public static func == (lhs: Target, rhs: Target) -> Bool {
return lhs.saturation == rhs.saturation && lhs.lightness == rhs.lightness
}
public func hash(into hasher: inout Hasher) {
hasher.combine(saturation)
hasher.combine(lightness)
}
// MARK: - Internal
internal func normalizeWeights() {
let sum = weights.saturation + weights.lightness + weights.population
guard sum > 0 else {
return
}
weights.saturation /= sum
weights.lightness /= sum
weights.population /= sum
}
// MARK: - Private
private struct Value: Hashable {
var min: CGFloat = 0.0
var target: CGFloat = 0.5
var max: CGFloat = 1.0
}
private struct Weights: Hashable {
var saturation: CGFloat = 0.24
var lightness: CGFloat = 0.52
var population: CGFloat = 0.24
}
private var saturation = Value()
private var lightness = Value()
private var weights = Weights()
private func setDefaultLightLightnessValues() {
lightness.min = 0.55
lightness.target = 0.74
}
private func setDefaultNormalLightnessValues() {
lightness.min = 0.3
lightness.target = 0.5
lightness.max = 0.7
}
private func setDefaultDarkLightnessValues() {
lightness.target = 0.26
lightness.max = 0.45
}
private func setDefaultVibrantSaturationValues() {
saturation.min = 0.35
saturation.target = 1.0
}
private func setDefaultMutedSaturationValues() {
saturation.target = 0.3
saturation.max = 0.4
}
}
//
// TargetBuilder.swift
// Palette
//
// Created by Egor Snitsar on 10.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import Foundation
extension Target {
public final class Builder {
public init() {
self.target = Target()
}
public init(_ target: Target) {
self.target = Target(target)
}
public func with(minimumSaturation: CGFloat) -> Builder {
target.minimumSaturation = minimumSaturation
return self
}
public func with(targetSaturation: CGFloat) -> Builder {
target.targetSaturation = targetSaturation
return self
}
public func with(maximumSaturation: CGFloat) -> Builder {
target.maximumSaturation = maximumSaturation
return self
}
public func with(minimumLightness: CGFloat) -> Builder {
target.minimumLightness = minimumLightness
return self
}
public func with(targetLightness: CGFloat) -> Builder {
target.targetLightness = targetLightness
return self
}
public func with(maximumLightness: CGFloat) -> Builder {
target.maximumLightness = maximumLightness
return self
}
public func with(saturationWeight: CGFloat) -> Builder {
target.saturationWeight = saturationWeight
return self
}
public func with(lightnessWeight: CGFloat) -> Builder {
target.lightnessWeight = lightnessWeight
return self
}
public func with(populationWeight: CGFloat) -> Builder {
target.populationWeight = populationWeight
return self
}
public func with(exclusive: Bool) -> Builder {
target.isExclusive = exclusive
return self
}
public func build() -> Target {
return target
}
private let target: Target
}
}
//
// UIImage+Palette.swift
// Palette
//
// Created by Egor Snitsar on 09.08.2019.
// Copyright © 2019 Egor Snitsar. All rights reserved.
//
import Foundation
extension UIImage {
public func createPalette() -> Palette {
return Palette.from(image: self).generate()
}
public func createPalette(_ completion: @escaping (Palette) -> Void) {
return Palette.from(image: self).generate(completion)
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.6</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${CURRENT_PROJECT_VERSION}</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
#import <Foundation/Foundation.h>
@interface PodsDummy_Palette : NSObject
@end
@implementation PodsDummy_Palette
@end
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
FOUNDATION_EXPORT double PaletteVersionNumber;
FOUNDATION_EXPORT const unsigned char PaletteVersionString[];
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Palette
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/Palette
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
framework module Palette {
umbrella header "Palette-umbrella.h"
export *
module * { export * }
}
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Palette
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/Palette
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
......@@ -2756,31 +2756,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Palette
MIT License
Copyright (c) 2019 Egor Snitsar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## SnapKit
Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
......
......@@ -2979,37 +2979,6 @@ SOFTWARE.</string>
</dict>
<dict>
<key>FooterText</key>
<string>MIT License
Copyright (c) 2019 Egor Snitsar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</string>
<key>License</key>
<string>MIT</string>
<key>Title</key>
<string>Palette</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
Permission is hereby granted, free of charge, to any person obtaining a copy
......
......@@ -25,7 +25,6 @@ ${BUILT_PRODUCTS_DIR}/Logging/Logging.framework
${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework
${BUILT_PRODUCTS_DIR}/Nuke-WebP-Plugin/NukeWebPPlugin.framework
${BUILT_PRODUCTS_DIR}/PKHUD/PKHUD.framework
${BUILT_PRODUCTS_DIR}/Palette/Palette.framework
${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
${BUILT_PRODUCTS_DIR}/Swarm/Swarm.framework
${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework
......
......@@ -24,7 +24,6 @@ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Logging.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NukeWebPPlugin.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PKHUD.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Palette.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swarm.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework
......
......@@ -25,7 +25,6 @@ ${BUILT_PRODUCTS_DIR}/Logging/Logging.framework
${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework
${BUILT_PRODUCTS_DIR}/Nuke-WebP-Plugin/NukeWebPPlugin.framework
${BUILT_PRODUCTS_DIR}/PKHUD/PKHUD.framework
${BUILT_PRODUCTS_DIR}/Palette/Palette.framework
${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
${BUILT_PRODUCTS_DIR}/Swarm/Swarm.framework
${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework
......
......@@ -24,7 +24,6 @@ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Logging.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NukeWebPPlugin.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PKHUD.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Palette.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swarm.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework
......
......@@ -201,7 +201,6 @@ if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Nuke-WebP-Plugin/NukeWebPPlugin.framework"
install_framework "${BUILT_PRODUCTS_DIR}/PKHUD/PKHUD.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Palette/Palette.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Swarm/Swarm.framework"
install_framework "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework"
......@@ -238,7 +237,6 @@ if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Nuke-WebP-Plugin/NukeWebPPlugin.framework"
install_framework "${BUILT_PRODUCTS_DIR}/PKHUD/PKHUD.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Palette/Palette.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Swarm/Swarm.framework"
install_framework "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment