|
|
|
@ -62,6 +62,118 @@ public class ImageEditorColor: NSObject {
|
|
|
|
|
|
|
|
|
|
// MARK: -
|
|
|
|
|
|
|
|
|
|
private class PalettePreviewView: OWSLayerView {
|
|
|
|
|
|
|
|
|
|
private static let innerRadius: CGFloat = 32
|
|
|
|
|
private static let shadowMargin: CGFloat = 0
|
|
|
|
|
// The distance from the "inner circle" to the "teardrop".
|
|
|
|
|
private static let circleMargin: CGFloat = 3
|
|
|
|
|
private static let teardropTipRadius: CGFloat = 4
|
|
|
|
|
private static let teardropPointiness: CGFloat = 12
|
|
|
|
|
|
|
|
|
|
private let teardropColor = UIColor.white
|
|
|
|
|
public var selectedColor = UIColor.white {
|
|
|
|
|
didSet {
|
|
|
|
|
circleLayer.fillColor = selectedColor.cgColor
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private let circleLayer: CAShapeLayer
|
|
|
|
|
private let teardropLayer: CAShapeLayer
|
|
|
|
|
|
|
|
|
|
override init() {
|
|
|
|
|
let circleLayer = CAShapeLayer()
|
|
|
|
|
let teardropLayer = CAShapeLayer()
|
|
|
|
|
self.circleLayer = circleLayer
|
|
|
|
|
self.teardropLayer = teardropLayer
|
|
|
|
|
|
|
|
|
|
super.init()
|
|
|
|
|
|
|
|
|
|
circleLayer.strokeColor = nil
|
|
|
|
|
teardropLayer.strokeColor = nil
|
|
|
|
|
// Layer order matters.
|
|
|
|
|
layer.addSublayer(teardropLayer)
|
|
|
|
|
layer.addSublayer(circleLayer)
|
|
|
|
|
|
|
|
|
|
teardropLayer.fillColor = teardropColor.cgColor
|
|
|
|
|
teardropLayer.shadowColor = UIColor.black.cgColor
|
|
|
|
|
teardropLayer.shadowRadius = 2.0
|
|
|
|
|
teardropLayer.shadowOpacity = 0.33
|
|
|
|
|
teardropLayer.shadowOffset = .zero
|
|
|
|
|
|
|
|
|
|
layoutCallback = { (view) in
|
|
|
|
|
PalettePreviewView.updateLayers(view: view,
|
|
|
|
|
circleLayer: circleLayer,
|
|
|
|
|
teardropLayer: teardropLayer)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The bounding rect of the teardrop + shadow is non-trivial, so
|
|
|
|
|
// we use a generous size that reserves plenty of space.
|
|
|
|
|
//
|
|
|
|
|
// The size doesn't matter since this view is
|
|
|
|
|
// mostly transparent and isn't hot.
|
|
|
|
|
autoSetDimensions(to: CGSize(width: PalettePreviewView.innerRadius * 4,
|
|
|
|
|
height: PalettePreviewView.innerRadius * 4))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@available(*, unavailable, message: "use other init() instead.")
|
|
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
|
|
|
notImplemented()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func updateLayers(view: UIView,
|
|
|
|
|
circleLayer: CAShapeLayer,
|
|
|
|
|
teardropLayer: CAShapeLayer) {
|
|
|
|
|
let bounds = view.bounds
|
|
|
|
|
let outerRadius = innerRadius + circleMargin
|
|
|
|
|
let rightEdge = CGPoint(x: bounds.width,
|
|
|
|
|
y: bounds.height * 0.5)
|
|
|
|
|
let teardropTipCenter = rightEdge.minus(CGPoint(x: teardropTipRadius + shadowMargin, y: 0))
|
|
|
|
|
let circleCenter = teardropTipCenter.minus(CGPoint(x: teardropPointiness + innerRadius, y: 0))
|
|
|
|
|
|
|
|
|
|
// The "teardrop" shape is bounded by 2 circles, joined by their tangents.
|
|
|
|
|
//
|
|
|
|
|
// UIBezierPath can be used to draw this using 2 arcs, if we
|
|
|
|
|
// have the angle of the tangents.
|
|
|
|
|
//
|
|
|
|
|
// Finding the tangent between two circles of known distance + radius
|
|
|
|
|
// is pretty straightforward. We solve for the right triangle that
|
|
|
|
|
// defines the tangents and atan() that triangle to get the angle.
|
|
|
|
|
//
|
|
|
|
|
// 1. Find the length of the hypotenuse.
|
|
|
|
|
let circleCenterDistance = teardropTipCenter.minus(circleCenter).length
|
|
|
|
|
// 2. Fine the length of the first side.
|
|
|
|
|
let radiusDiff = outerRadius - teardropTipRadius
|
|
|
|
|
// 2. Fine the length of the second side.
|
|
|
|
|
let tangentLength = (circleCenterDistance.square - radiusDiff.square).squareRoot()
|
|
|
|
|
let angle = atan2(tangentLength, radiusDiff)
|
|
|
|
|
|
|
|
|
|
let teardropPath = UIBezierPath()
|
|
|
|
|
teardropPath.addArc(withCenter: circleCenter,
|
|
|
|
|
radius: outerRadius,
|
|
|
|
|
startAngle: +angle,
|
|
|
|
|
endAngle: -angle,
|
|
|
|
|
clockwise: true)
|
|
|
|
|
teardropPath.addArc(withCenter: teardropTipCenter,
|
|
|
|
|
radius: teardropTipRadius,
|
|
|
|
|
startAngle: -angle,
|
|
|
|
|
endAngle: +angle,
|
|
|
|
|
clockwise: true)
|
|
|
|
|
|
|
|
|
|
teardropLayer.path = teardropPath.cgPath
|
|
|
|
|
teardropLayer.frame = bounds
|
|
|
|
|
|
|
|
|
|
let innerCircleSize = CGSize(width: innerRadius * 2,
|
|
|
|
|
height: innerRadius * 2)
|
|
|
|
|
let circleFrame = CGRect(origin: circleCenter.minus(innerCircleSize.asPoint.times(0.5)),
|
|
|
|
|
size: innerCircleSize)
|
|
|
|
|
circleLayer.path = UIBezierPath(ovalIn: circleFrame).cgPath
|
|
|
|
|
circleLayer.frame = bounds
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: -
|
|
|
|
|
|
|
|
|
|
public class ImageEditorPaletteView: UIView {
|
|
|
|
|
|
|
|
|
|
public weak var delegate: ImageEditorPaletteViewDelegate?
|
|
|
|
@ -89,6 +201,8 @@ public class ImageEditorPaletteView: UIView {
|
|
|
|
|
private let imageWrapper = OWSLayerView()
|
|
|
|
|
private let shadowView = UIView()
|
|
|
|
|
private var selectionConstraint: NSLayoutConstraint?
|
|
|
|
|
private let previewView = PalettePreviewView()
|
|
|
|
|
private var previewConstraint: NSLayoutConstraint?
|
|
|
|
|
|
|
|
|
|
private func createContents() {
|
|
|
|
|
self.backgroundColor = .clear
|
|
|
|
@ -140,6 +254,14 @@ public class ImageEditorPaletteView: UIView {
|
|
|
|
|
selectionConstraint.autoInstall()
|
|
|
|
|
self.selectionConstraint = selectionConstraint
|
|
|
|
|
|
|
|
|
|
previewView.isHidden = true
|
|
|
|
|
addSubview(previewView)
|
|
|
|
|
previewView.autoPinEdge(.trailing, to: .leading, of: imageView, withOffset: -24)
|
|
|
|
|
let previewConstraint = NSLayoutConstraint(item: previewView,
|
|
|
|
|
attribute: .centerY, relatedBy: .equal, toItem: imageWrapper, attribute: .top, multiplier: 1, constant: 0)
|
|
|
|
|
previewConstraint.autoInstall()
|
|
|
|
|
self.previewConstraint = previewConstraint
|
|
|
|
|
|
|
|
|
|
isUserInteractionEnabled = true
|
|
|
|
|
addGestureRecognizer(PaletteGestureRecognizer(target: self, action: #selector(didTouch)))
|
|
|
|
|
|
|
|
|
@ -208,6 +330,7 @@ public class ImageEditorPaletteView: UIView {
|
|
|
|
|
|
|
|
|
|
private func updateState() {
|
|
|
|
|
selectionView.backgroundColor = selectedValue.color
|
|
|
|
|
previewView.selectedColor = selectedValue.color
|
|
|
|
|
|
|
|
|
|
guard let selectionConstraint = selectionConstraint else {
|
|
|
|
|
owsFailDebug("Missing selectionConstraint.")
|
|
|
|
@ -215,6 +338,12 @@ public class ImageEditorPaletteView: UIView {
|
|
|
|
|
}
|
|
|
|
|
let selectionY = imageWrapper.height() * selectedValue.palettePhase
|
|
|
|
|
selectionConstraint.constant = selectionY
|
|
|
|
|
|
|
|
|
|
guard let previewConstraint = previewConstraint else {
|
|
|
|
|
owsFailDebug("Missing previewConstraint.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
previewConstraint.constant = selectionY
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Events
|
|
|
|
@ -222,9 +351,12 @@ public class ImageEditorPaletteView: UIView {
|
|
|
|
|
@objc
|
|
|
|
|
func didTouch(gesture: UIGestureRecognizer) {
|
|
|
|
|
switch gesture.state {
|
|
|
|
|
case .began, .changed, .ended:
|
|
|
|
|
break
|
|
|
|
|
case .began, .changed:
|
|
|
|
|
previewView.isHidden = false
|
|
|
|
|
case .ended:
|
|
|
|
|
previewView.isHidden = true
|
|
|
|
|
default:
|
|
|
|
|
previewView.isHidden = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|