Skip to content

Commit

Permalink
Merge pull request #9 from bryanjclark/master
Browse files Browse the repository at this point in the history
Allow tighter Config controls over color variation, and ensure all shapes and colors are included.
  • Loading branch information
onmyway133 authored Oct 9, 2017
2 parents 511ca6e + b9afc06 commit 46fb62f
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 31 deletions.
56 changes: 54 additions & 2 deletions CheersTests/CheerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import XCTest
class CheerTests: XCTestCase {
func testImageGenerators() {
let generator = ImageGenerator()
XCTAssertNotNil(generator.rectangle())
XCTAssertNotNil(generator.circle())
let allShapes = Particle.ConfettiShape.all
allShapes.forEach { shape in
XCTAssertNotNil(generator.confetti(shape: shape), "Shape \(shape) was nil.")
}
}

func testCheerView() {
Expand All @@ -21,4 +23,54 @@ class CheerTests: XCTestCase {
cheerView.stop()
XCTAssertNotNil(cheerView.emitter)
}

func testShuffle() {
let originalArray = [1, 2, 3, 4, 5]
let shuffled = originalArray.shuffled()

// Ensure that they have equal count
XCTAssertEqual(originalArray.count, shuffled.count)

// Ensure that no entries were added/removed — only moved around.
// 1: first, verify that the test data was well-formed.
XCTAssertEqual(
originalArray.count,
Set(originalArray).count,
"Test data failure: originalArray must only contain unique values for this test to work properly."
)
// 2: now, verify that the shuffled entries are the same as the non-shuffled entries.
let uniqueShuffled = Set(shuffled)
XCTAssertEqual(uniqueShuffled, Set(originalArray))
}

func testCombineEntries() {
let lhs = [1, 2, 3, 4]
let rhs = ["a", "b", "c"]
let combinations = Array<(Int, String)>.createAllCombinations(from: lhs, and: rhs)

// Ensure that we have the right count.
XCTAssertEqual(lhs.count * rhs.count, combinations.count)

// Ensure that all possible combinations are included.
let allPossibleCombinations: [(Int, String)] = [
(1, "a"),
(1, "b"),
(1, "c"),
(2, "a"),
(2, "b"),
(2, "c"),
(3, "a"),
(3, "b"),
(3, "c"),
(4, "a"),
(4, "b"),
(4, "c"),
]
allPossibleCombinations.forEach { testCombination in
let match = combinations.first(where: { (int: Int, str: String) -> Bool in
return int == testCombination.0 && str == testCombination.1
})
XCTAssertNotNil(match, "No match for testCombination \(testCombination)")
}
}
}
31 changes: 18 additions & 13 deletions Sources/Cheers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ public class CheerView: UIView {
emitter.emitterSize = CGSize(width: bounds.width, height: 1)
emitter.renderMode = kCAEmitterLayerAdditive

let colors = config.colors.shuffled()
var cells = [CAEmitterCell]()
// This combination will ensure that all color/image combinations are evenly distributed.
// For example, if you have only one color, then we still want to make sure
// that all "allowed" particle types are represented in the result.
let combinations = Array<(UIColor, UIImage)>.createAllCombinations(
from: config.colors,
and: pickImages()
)

zip(pickImages(), colors.shuffled()).forEach { image, color in
let cells: [CAEmitterCell] = combinations.reduce([]) { (accum, combination) in
let cell = CAEmitterCell()
cell.birthRate = 20
cell.lifetime = 20.0
Expand All @@ -36,16 +41,16 @@ public class CheerView: UIView {
cell.spinRange = 5
cell.scale = 0.3
cell.scaleRange = 0.2
cell.color = color.cgColor
cell.color = combination.0.cgColor
cell.alphaSpeed = -0.1
cell.contents = image.cgImage
cell.contents = combination.1.cgImage
cell.xAcceleration = 20
cell.yAcceleration = 50
cell.redRange = 0.8
cell.greenRange = 0.8
cell.blueRange = 0.8
cell.redRange = config.colorRange
cell.greenRange = config.colorRange
cell.blueRange = config.colorRange

cells.append(cell)
return accum + [cell]
}

emitter.emitterCells = cells
Expand All @@ -66,10 +71,10 @@ public class CheerView: UIView {
let generator = ImageGenerator()

switch config.particle {
case .confetti:
return [generator.rectangle(), generator.circle(),
generator.triangle(), generator.curvedQuadrilateral()]
.flatMap({ $0 })
case .confetti(let allowedShapes):
return allowedShapes
.map { generator.confetti(shape: $0) }
.flatMap({ $0 })
case .image(let images):
return images
case .text(let size, let strings):
Expand Down
21 changes: 19 additions & 2 deletions Sources/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,26 @@ import UIKit
/// - image: An array of images
/// - text: An array of texts
public enum Particle {
case confetti
case confetti(allowedShapes: [ConfettiShape])
case image([UIImage])
case text(CGSize, [NSAttributedString])

/// The shape of a piece of confetti.
public enum ConfettiShape {
case rectangle, circle, triangle, curvedQuadrilateral
public static var all: [ConfettiShape] = [
.rectangle,
.circle,
.triangle,
.curvedQuadrilateral
]
}
}

/// Used to configure CheerView
public struct Config {
/// Specify the particle shapes
public var particle: Particle = .confetti
public var particle: Particle = .confetti(allowedShapes: Particle.ConfettiShape.all)

/// The list of available colors. This will be shuffled
public var colors: [UIColor] = [
Expand All @@ -27,6 +38,12 @@ public struct Config {
UIColor.cyan
]

/// The allowed "color range" for RGB values in each particle.
/// For example, a value of 0.8 would allow the `CAEmitterCell().redRange`, `.greenRange`, and `.blueRange` to each vary by 0.8.
/// If you want to tightly-specify the color of your particles, use a small value.
/// This can be any value between 0.0 and 1.0 — see the documentation for `CAEmitterCell().redRange` for more information.
public var colorRange: Float = 0.8

/// Customize the cells
public var customize: (([CAEmitterCell]) -> Void)?

Expand Down
14 changes: 14 additions & 0 deletions Sources/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,18 @@ extension Array {
}
return list
}

/// Creates an array containing all combinations of two arrays.
static func createAllCombinations<T, U>(
from lhs: Array<T>,
and rhs: Array<U>
) -> Array<(T, U)> {
let result: [(T, U)] = lhs.reduce([]) { (accum, t) in
let innerResult: [(T, U)] = rhs.reduce([]) { (innerAccum, u) in
return innerAccum + [(t, u)]
}
return accum + innerResult
}
return result
}
}
37 changes: 23 additions & 14 deletions Sources/ImageGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,35 @@ import UIKit

class ImageGenerator {
private let size = CGSize(width: 20, height: 20)

private func generate(block: (CGContext?) -> Void) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
let context = UIGraphicsGetCurrentContext()
block(context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

return image
}

func generate(size: CGSize, string: NSAttributedString) -> UIImage? {
return generate { context in
let rect = CGRect(origin: .zero, size: size)
context?.clear(rect)
string.draw(in: rect)
}
}

func rectangle() -> UIImage? {

func confetti(shape: Particle.ConfettiShape) -> UIImage? {
switch shape {
case .rectangle: return rectangle()
case .circle: return circle()
case .triangle: return triangle()
case .curvedQuadrilateral: return curvedQuadrilateral()
}
}

private func rectangle() -> UIImage? {
return generate { context in
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height/2)
let path = UIBezierPath(rect: rect)
Expand All @@ -30,8 +39,8 @@ class ImageGenerator {
context?.fillPath()
}
}

func circle() -> UIImage? {
private func circle() -> UIImage? {
return generate { context in
let rect = CGRect(origin: .zero, size: size)
let path = UIBezierPath(ovalIn: rect)
Expand All @@ -40,8 +49,8 @@ class ImageGenerator {
context?.fillPath()
}
}

func triangle() -> UIImage? {
private func triangle() -> UIImage? {
return generate { context in
let path = UIBezierPath()
path.move(to: CGPoint(x: size.width/2, y: 0))
Expand All @@ -53,22 +62,22 @@ class ImageGenerator {
context?.fillPath()
}
}

func curvedQuadrilateral() -> UIImage? {
private func curvedQuadrilateral() -> UIImage? {
return generate { context in
let path = UIBezierPath()
let rightPoint = CGPoint(x: size.width - 5, y: 5)
let leftPoint = CGPoint(x: size.width * 0.5, y: size.height - 8)

// top left
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: size.width * 0.3, y: 0))

// bottom right
path.addQuadCurve(to: CGPoint(x: size.width, y: size.height),
controlPoint: rightPoint)
path.addLine(to: CGPoint(x: size.width * 0.7, y: size.height))

// close to top left
path.addQuadCurve(to: CGPoint.zero,
controlPoint: leftPoint)
Expand Down

0 comments on commit 46fb62f

Please sign in to comment.