Skip to content

Commit

Permalink
Merge pull request zalando#57 from dirtydanee/master
Browse files Browse the repository at this point in the history
Adding customisable visualisation support for the paws.
  • Loading branch information
wojciechczerski authored Feb 13, 2018
2 parents 547ac3c + c40526c commit 17801e7
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 49 deletions.
59 changes: 59 additions & 0 deletions SwiftMonkeyPaws/Configuration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// Configuration.swift
// SwiftMonkeyPaws
//
// Created by Daniel.Metzing on 11.02.18.
//

import UIKit

public struct Configuration {
// Customise the appearance of the paws
public struct Paws {

/// Define the colour of the Paws
///
/// - randomized: random colour for each paw
/// - constant: same colour for the paws
public enum Color {
case randomized
case constant(UIColor)
}
// Colour of the paws
public let color: Color

// Brightness of a particular paw
public let brightness: CGFloat

// Maximum visible paws at one time
public let maxShown: Int

public init(colour: Color = .randomized, brightness: CGFloat = 0.5, maxShown: Int = 15) {
self.color = colour
self.brightness = brightness
self.maxShown = maxShown
}
}

public struct Radius {

/// Radius of the cross draw upon canceling a touch event
public let cross: CGFloat

/// Radius of the circle draw upon ending a touch event
public let circle: CGFloat

public init(cross: CGFloat = 7, circle: CGFloat = 7) {
self.cross = cross
self.circle = circle
}
}

public let paws: Paws
public let radius: Radius

public init(paws: Paws = Paws(), radius: Radius = Radius()) {
self.paws = paws
self.radius = radius
}
}
37 changes: 37 additions & 0 deletions SwiftMonkeyPaws/MonkeyPawDrawer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// MonkeyPawDrawer.swift
// SwiftMonkeyPaws
//
// Created by Daniel.Metzing on 04.02.18.
//

import UIKit

public final class MonkeyPawDrawer {

public static func monkeyHandPath() -> UIBezierPath {
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: -5.91, y: 8.76))
bezierPath.addCurve(to: CGPoint(x: -10.82, y: 2.15), controlPoint1: CGPoint(x: -9.18, y: 7.11), controlPoint2: CGPoint(x: -8.09, y: 4.9))
bezierPath.addCurve(to: CGPoint(x: -16.83, y: -1.16), controlPoint1: CGPoint(x: -13.56, y: -0.6), controlPoint2: CGPoint(x: -14.65, y: 0.5))
bezierPath.addCurve(to: CGPoint(x: -14.65, y: -6.11), controlPoint1: CGPoint(x: -19.02, y: -2.81), controlPoint2: CGPoint(x: -19.57, y: -6.66))
bezierPath.addCurve(to: CGPoint(x: -8.09, y: -2.81), controlPoint1: CGPoint(x: -9.73, y: -5.56), controlPoint2: CGPoint(x: -8.64, y: -0.05))
bezierPath.addCurve(to: CGPoint(x: -11.37, y: -13.82), controlPoint1: CGPoint(x: -7.54, y: -5.56), controlPoint2: CGPoint(x: -7, y: -8.32))
bezierPath.addCurve(to: CGPoint(x: -7.54, y: -17.13), controlPoint1: CGPoint(x: -15.74, y: -19.33), controlPoint2: CGPoint(x: -9.73, y: -20.98))
bezierPath.addCurve(to: CGPoint(x: -4.27, y: -8.87), controlPoint1: CGPoint(x: -5.36, y: -13.27), controlPoint2: CGPoint(x: -6.45, y: -7.76))
bezierPath.addCurve(to: CGPoint(x: -4.27, y: -18.23), controlPoint1: CGPoint(x: -2.08, y: -9.97), controlPoint2: CGPoint(x: -3.72, y: -12.72))
bezierPath.addCurve(to: CGPoint(x: 0.65, y: -18.23), controlPoint1: CGPoint(x: -4.81, y: -23.74), controlPoint2: CGPoint(x: 0.65, y: -25.39))
bezierPath.addCurve(to: CGPoint(x: 1.2, y: -8.32), controlPoint1: CGPoint(x: 0.65, y: -11.07), controlPoint2: CGPoint(x: -0.74, y: -9.29))
bezierPath.addCurve(to: CGPoint(x: 3.93, y: -18.78), controlPoint1: CGPoint(x: 2.29, y: -7.76), controlPoint2: CGPoint(x: 3.93, y: -9.3))
bezierPath.addCurve(to: CGPoint(x: 8.3, y: -16.03), controlPoint1: CGPoint(x: 3.93, y: -23.19), controlPoint2: CGPoint(x: 9.96, y: -21.86))
bezierPath.addCurve(to: CGPoint(x: 5.57, y: -6.11), controlPoint1: CGPoint(x: 7.76, y: -14.1), controlPoint2: CGPoint(x: 3.93, y: -6.66))
bezierPath.addCurve(to: CGPoint(x: 9.4, y: -10.52), controlPoint1: CGPoint(x: 7.21, y: -5.56), controlPoint2: CGPoint(x: 9.16, y: -10.09))
bezierPath.addCurve(to: CGPoint(x: 12.13, y: -6.66), controlPoint1: CGPoint(x: 12.13, y: -15.48), controlPoint2: CGPoint(x: 15.41, y: -9.42))
bezierPath.addCurve(to: CGPoint(x: 8.3, y: -1.16), controlPoint1: CGPoint(x: 8.85, y: -3.91), controlPoint2: CGPoint(x: 8.85, y: -3.91))
bezierPath.addCurve(to: CGPoint(x: 8.3, y: 7.11), controlPoint1: CGPoint(x: 7.76, y: 1.6), controlPoint2: CGPoint(x: 9.4, y: 4.35))
bezierPath.addCurve(to: CGPoint(x: -5.91, y: 8.76), controlPoint1: CGPoint(x: 7.21, y: 9.86), controlPoint2: CGPoint(x: -2.63, y: 10.41))
bezierPath.close()

return bezierPath
}
}
105 changes: 56 additions & 49 deletions SwiftMonkeyPaws/MonkeyPaws.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@

import UIKit

private let maxGesturesShown: Int = 15
private let crossRadius: CGFloat = 7
private let circleRadius: CGFloat = 7

/**
A class that visualises input events as an overlay over
your regular UI. To use, simply instantiate it and
Expand All @@ -35,10 +31,16 @@ private let circleRadius: CGFloat = 7
}
```
*/

public class MonkeyPaws: NSObject, CALayerDelegate {

public typealias BezierPathDrawer = () -> UIBezierPath

private var gestures: [(hash: Int?, gesture: Gesture)] = []
private weak var view: UIView?

let configuration: Configuration
let bezierPathDrawer: BezierPathDrawer
let layer = CALayer()

fileprivate static var tappingTracks: [WeakReference<MonkeyPaws>] = []
Expand All @@ -55,8 +57,18 @@ public class MonkeyPaws: NSObject, CALayerDelegate {
intercept events so that it can visualise them.
If you do not want this, pass `false` here and
provide it with events manually.
- parameter configuration: Configure the visual appearance
of the Monkey paws. By default it uses the built in visual
parameters.
- parameter bezierPathDrawer: Create your own visualisation by
defining a bezier path drawer closure
*/
public init(view: UIView, tapUIApplication: Bool = true) {
public init(view: UIView,
tapUIApplication: Bool = true,
configuration: Configuration = Configuration(),
bezierPathDrawer: @escaping BezierPathDrawer = MonkeyPawDrawer.monkeyHandPath) {
self.configuration = configuration
self.bezierPathDrawer = bezierPathDrawer
super.init()
self.view = view

Expand Down Expand Up @@ -112,14 +124,16 @@ public class MonkeyPaws: NSObject, CALayerDelegate {
gesture.extend(to: point)
}
} else {
if gestures.count > maxGesturesShown {
if gestures.count > configuration.paws.maxShown {
gestures.removeFirst()
}

gestures.append((hash: touchHash, gesture: Gesture(from: point, inLayer: layer)))
gestures.append((hash: touchHash, gesture: Gesture(from: point, inLayer: layer, configuration: configuration, bezierPathDrawer: bezierPathDrawer)))

for i in 0 ..< gestures.count {
gestures[i].gesture.number = gestures.count - i
let number = gestures.count - i
let gesture = gestures[i].gesture
gesture.number = number
}
}
}
Expand Down Expand Up @@ -173,18 +187,21 @@ private class Gesture {
var pathLayer: CAShapeLayer?
var endLayer: CAShapeLayer?

let configuration: Configuration

private static var counter: Int = 0

init(from: CGPoint, inLayer: CALayer) {
init(from: CGPoint, inLayer: CALayer, configuration: Configuration, bezierPathDrawer: @escaping MonkeyPaws.BezierPathDrawer) {
self.points = [from]
self.configuration = configuration

let counter = Gesture.counter
Gesture.counter += 1

let angle = 45 * (CGFloat(fmod(Float(counter) * 0.279, 1)) * 2 - 1)
let mirrored = counter % 2 == 0
let colour = UIColor(hue: CGFloat(fmod(Float(counter) * 0.391, 1)), saturation: 1, brightness: 0.5, alpha: 1)
startLayer.path = monkeyHandPath(angle: angle, scale: 1, mirrored: mirrored).cgPath
let colour: UIColor = pawsColor(configuration: configuration.paws, seed: counter)

startLayer.path = customize(path: bezierPathDrawer(), seed: counter).cgPath

startLayer.strokeColor = colour.cgColor
startLayer.fillColor = nil
startLayer.position = from
Expand All @@ -211,7 +228,7 @@ private class Gesture {
didSet {
numberLayer.string = String(number)

let fraction = Float(number - 1) / Float(maxGesturesShown)
let fraction = Float(number - 1) / Float(configuration.paws.maxShown)
let alpha = sqrt(1 - fraction)
containerLayer.opacity = alpha
}
Expand Down Expand Up @@ -269,7 +286,7 @@ private class Gesture {
layer.fillColor = nil
layer.position = at

let path = circlePath()
let path = circlePath(radius: configuration.radius.circle)
layer.path = path.cgPath

containerLayer.addSublayer(layer)
Expand All @@ -289,56 +306,46 @@ private class Gesture {
layer.fillColor = nil
layer.position = at

let path = crossPath()
let path = crossPath(radius: configuration.radius.cross)
layer.path = path.cgPath

containerLayer.addSublayer(layer)
endLayer = layer
}

func pawsColor(configuration: Configuration.Paws, seed: Int) -> UIColor {
switch configuration.color {
case .randomized:
return UIColor(hue: CGFloat(fmod(Float(seed) * 0.391, 1)),
saturation: 1,
brightness: configuration.brightness,
alpha: 1)
case .constant(let constantColour):
return constantColour.color(WithBrightness: configuration.brightness)
}
}
}

private func monkeyHandPath(angle: CGFloat, scale: CGFloat, mirrored: Bool) -> UIBezierPath {
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: -5.91, y: 8.76))
bezierPath.addCurve(to: CGPoint(x: -10.82, y: 2.15), controlPoint1: CGPoint(x: -9.18, y: 7.11), controlPoint2: CGPoint(x: -8.09, y: 4.9))
bezierPath.addCurve(to: CGPoint(x: -16.83, y: -1.16), controlPoint1: CGPoint(x: -13.56, y: -0.6), controlPoint2: CGPoint(x: -14.65, y: 0.5))
bezierPath.addCurve(to: CGPoint(x: -14.65, y: -6.11), controlPoint1: CGPoint(x: -19.02, y: -2.81), controlPoint2: CGPoint(x: -19.57, y: -6.66))
bezierPath.addCurve(to: CGPoint(x: -8.09, y: -2.81), controlPoint1: CGPoint(x: -9.73, y: -5.56), controlPoint2: CGPoint(x: -8.64, y: -0.05))
bezierPath.addCurve(to: CGPoint(x: -11.37, y: -13.82), controlPoint1: CGPoint(x: -7.54, y: -5.56), controlPoint2: CGPoint(x: -7, y: -8.32))
bezierPath.addCurve(to: CGPoint(x: -7.54, y: -17.13), controlPoint1: CGPoint(x: -15.74, y: -19.33), controlPoint2: CGPoint(x: -9.73, y: -20.98))
bezierPath.addCurve(to: CGPoint(x: -4.27, y: -8.87), controlPoint1: CGPoint(x: -5.36, y: -13.27), controlPoint2: CGPoint(x: -6.45, y: -7.76))
bezierPath.addCurve(to: CGPoint(x: -4.27, y: -18.23), controlPoint1: CGPoint(x: -2.08, y: -9.97), controlPoint2: CGPoint(x: -3.72, y: -12.72))
bezierPath.addCurve(to: CGPoint(x: 0.65, y: -18.23), controlPoint1: CGPoint(x: -4.81, y: -23.74), controlPoint2: CGPoint(x: 0.65, y: -25.39))
bezierPath.addCurve(to: CGPoint(x: 1.2, y: -8.32), controlPoint1: CGPoint(x: 0.65, y: -11.07), controlPoint2: CGPoint(x: -0.74, y: -9.29))
bezierPath.addCurve(to: CGPoint(x: 3.93, y: -18.78), controlPoint1: CGPoint(x: 2.29, y: -7.76), controlPoint2: CGPoint(x: 3.93, y: -9.3))
bezierPath.addCurve(to: CGPoint(x: 8.3, y: -16.03), controlPoint1: CGPoint(x: 3.93, y: -23.19), controlPoint2: CGPoint(x: 9.96, y: -21.86))
bezierPath.addCurve(to: CGPoint(x: 5.57, y: -6.11), controlPoint1: CGPoint(x: 7.76, y: -14.1), controlPoint2: CGPoint(x: 3.93, y: -6.66))
bezierPath.addCurve(to: CGPoint(x: 9.4, y: -10.52), controlPoint1: CGPoint(x: 7.21, y: -5.56), controlPoint2: CGPoint(x: 9.16, y: -10.09))
bezierPath.addCurve(to: CGPoint(x: 12.13, y: -6.66), controlPoint1: CGPoint(x: 12.13, y: -15.48), controlPoint2: CGPoint(x: 15.41, y: -9.42))
bezierPath.addCurve(to: CGPoint(x: 8.3, y: -1.16), controlPoint1: CGPoint(x: 8.85, y: -3.91), controlPoint2: CGPoint(x: 8.85, y: -3.91))
bezierPath.addCurve(to: CGPoint(x: 8.3, y: 7.11), controlPoint1: CGPoint(x: 7.76, y: 1.6), controlPoint2: CGPoint(x: 9.4, y: 4.35))
bezierPath.addCurve(to: CGPoint(x: -5.91, y: 8.76), controlPoint1: CGPoint(x: 7.21, y: 9.86), controlPoint2: CGPoint(x: -2.63, y: 10.41))
bezierPath.close()

bezierPath.apply(CGAffineTransform(translationX: 0.5, y: 0))

bezierPath.apply(CGAffineTransform(scaleX: scale, y: scale))
private func customize(path: UIBezierPath, seed: Int) -> UIBezierPath {

let angle = 45 * (CGFloat(fmod(Float(seed) * 0.279, 1)) * 2 - 1)
let mirrored = seed % 2 == 0

if mirrored {
bezierPath.apply(CGAffineTransform(scaleX: -1, y: 1))
path.apply(CGAffineTransform(scaleX: -1, y: 1))
}

bezierPath.apply(CGAffineTransform(rotationAngle: angle / 180 * CGFloat.pi))
path.apply(CGAffineTransform(rotationAngle: angle / 180 * CGFloat.pi))

return bezierPath
return path
}

private func circlePath() -> UIBezierPath {
return UIBezierPath(ovalIn: CGRect(centre: CGPoint.zero, size: CGSize(width: circleRadius * 2, height: circleRadius * 2)))
private func circlePath(radius: CGFloat) -> UIBezierPath {
return UIBezierPath(ovalIn: CGRect(centre: CGPoint.zero, size: CGSize(width: radius * 2, height: radius * 2)))
}

private func crossPath() -> UIBezierPath {
let rect = CGRect(centre: CGPoint.zero, size: CGSize(width: crossRadius * 2, height: crossRadius * 2))
private func crossPath(radius: CGFloat) -> UIBezierPath {
let rect = CGRect(centre: CGPoint.zero, size: CGSize(width: radius * 2, height: radius * 2))
let cross = UIBezierPath()
cross.move(to: CGPoint(x: rect.minX, y: rect.minY))
cross.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
Expand Down
12 changes: 12 additions & 0 deletions SwiftMonkeyPaws/SwiftMonkeyPaws.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
objects = {

/* Begin PBXBuildFile section */
1810357020337681005D6D35 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1810356E20337680005D6D35 /* Configuration.swift */; };
1810357120337681005D6D35 /* UIColor+MonkeyPaws.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1810356F20337681005D6D35 /* UIColor+MonkeyPaws.swift */; };
189807B920279AD800B9F544 /* MonkeyPawDrawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189807B820279AD800B9F544 /* MonkeyPawDrawer.swift */; };
C929B55C1DD0B7C9004B256F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C929B55B1DD0B7C9004B256F /* UIKit.framework */; };
OBJ_18 /* MonkeyPaws.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* MonkeyPaws.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
1810356E20337680005D6D35 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = SOURCE_ROOT; };
1810356F20337681005D6D35 /* UIColor+MonkeyPaws.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+MonkeyPaws.swift"; sourceTree = SOURCE_ROOT; };
189807B820279AD800B9F544 /* MonkeyPawDrawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonkeyPawDrawer.swift; sourceTree = SOURCE_ROOT; };
C929B55B1DD0B7C9004B256F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
C943263D1DDB123A0038A891 /* SwiftMonkeyPaws.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; name = SwiftMonkeyPaws.podspec; path = ../SwiftMonkeyPaws.podspec; sourceTree = "<group>"; };
OBJ_12 /* SwiftMonkeyPaws.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftMonkeyPaws.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -78,6 +84,9 @@
isa = PBXGroup;
children = (
OBJ_9 /* MonkeyPaws.swift */,
1810356E20337680005D6D35 /* Configuration.swift */,
189807B820279AD800B9F544 /* MonkeyPawDrawer.swift */,
1810356F20337681005D6D35 /* UIColor+MonkeyPaws.swift */,
);
name = SwiftMonkeyPaws;
path = .;
Expand Down Expand Up @@ -138,6 +147,9 @@
buildActionMask = 0;
files = (
OBJ_18 /* MonkeyPaws.swift in Sources */,
1810357020337681005D6D35 /* Configuration.swift in Sources */,
1810357120337681005D6D35 /* UIColor+MonkeyPaws.swift in Sources */,
189807B920279AD800B9F544 /* MonkeyPawDrawer.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
26 changes: 26 additions & 0 deletions SwiftMonkeyPaws/UIColor+MonkeyPaws.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// UIColor+MonkeyPaws.swift
// SwiftMonkeyPaws
//
// Created by Daniel.Metzing on 01.02.18.
//

import UIKit

extension UIColor {
func color(WithBrightness brightness: CGFloat) -> UIColor {
var H: CGFloat = 0
var S: CGFloat = 0
var B: CGFloat = 0
var A: CGFloat = 0

guard getHue(&H, saturation: &S, brightness: &B, alpha: &A) else {
return self
}

B += (brightness - 1.0)
B = max(min(B, 1.0), 0.0)

return UIColor(hue: H, saturation: S, brightness: B, alpha: A)
}
}

0 comments on commit 17801e7

Please sign in to comment.