diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorView.swift b/SignalMessaging/Views/ImageEditor/ImageEditorView.swift index 269b1fed4..4f09e5003 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorView.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorView.swift @@ -201,15 +201,6 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate { self.currentStrokeSamples.removeAll() } - let referenceView = layersView - let unitSampleForGestureLocation = { () -> CGPoint in - // TODO: Smooth touch samples before converting into stroke samples. - let location = gestureRecognizer.location(in: referenceView) - let x = CGFloatClamp01(CGFloatInverseLerp(location.x, 0, referenceView.bounds.width)) - let y = CGFloatClamp01(CGFloatInverseLerp(location.y, 0, referenceView.bounds.height)) - return CGPoint(x: x, y: y) - } - // TODO: Color picker. let strokeColor = UIColor.blue // TODO: Tune stroke width. @@ -219,14 +210,14 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate { case .began: removeCurrentStroke() - currentStrokeSamples.append(unitSampleForGestureLocation()) + currentStrokeSamples.append(unitSampleForGestureLocation(gestureRecognizer)) let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth) model.append(item: stroke) currentStroke = stroke case .changed, .ended: - currentStrokeSamples.append(unitSampleForGestureLocation()) + currentStrokeSamples.append(unitSampleForGestureLocation(gestureRecognizer)) guard let lastStroke = self.currentStroke else { owsFailDebug("Missing last stroke.") @@ -250,67 +241,104 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate { } } + private func unitSampleForGestureLocation(_ gestureRecognizer: UIGestureRecognizer) -> CGPoint { + let referenceView = layersView + // TODO: Smooth touch samples before converting into stroke samples. + let location = gestureRecognizer.location(in: referenceView) + let x = CGFloatClamp01(CGFloatInverseLerp(location.x, 0, referenceView.bounds.width)) + let y = CGFloatClamp01(CGFloatInverseLerp(location.y, 0, referenceView.bounds.height)) + return CGPoint(x: x, y: y) + } + // MARK: - Crop + private var cropStartUnit = CGPoint.zero + private var cropEndUnit = CGPoint.zero + private var cropLayer1 = CAShapeLayer() + private var cropLayer2 = CAShapeLayer() + private var cropLayers: [CAShapeLayer] { + return [cropLayer1, cropLayer2] + } + @objc public func handleCropGesture(_ gestureRecognizer: UIGestureRecognizer) { AssertIsOnMainThread() -// let removeCurrentStroke = { -// if let stroke = self.currentStroke { -// self.model.remove(item: stroke) -// } -// self.currentStroke = nil -// self.currentStrokeSamples.removeAll() -// } -// -// let referenceView = self -// let unitSampleForGestureLocation = { () -> CGPoint in -// // TODO: Smooth touch samples before converting into stroke samples. -// let location = gestureRecognizer.location(in: referenceView) -// let x = CGFloatClamp01(CGFloatInverseLerp(location.x, 0, referenceView.bounds.width)) -// let y = CGFloatClamp01(CGFloatInverseLerp(location.y, 0, referenceView.bounds.height)) -// return CGPoint(x: x, y: y) -// } -// -// // TODO: Color picker. -// let strokeColor = UIColor.blue -// // TODO: Tune stroke width. -// let unitStrokeWidth = ImageEditorStrokeItem.defaultUnitStrokeWidth() -// -// switch gestureRecognizer.state { -// case .began: -// removeCurrentStroke() -// -// currentStrokeSamples.append(unitSampleForGestureLocation()) -// -// let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth) -// model.append(item: stroke) -// currentStroke = stroke -// -// case .changed, .ended: -// currentStrokeSamples.append(unitSampleForGestureLocation()) -// -// guard let lastStroke = self.currentStroke else { -// owsFailDebug("Missing last stroke.") -// removeCurrentStroke() -// return -// } -// -// // Model items are immutable; we _replace_ the -// // stroke item rather than modify it. -// let stroke = ImageEditorStrokeItem(itemId: lastStroke.itemId, color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth) -// model.replace(item: stroke, suppressUndo: true) -// -// if gestureRecognizer.state == .ended { -// currentStroke = nil -// currentStrokeSamples.removeAll() -// } else { -// currentStroke = stroke -// } -// default: -// removeCurrentStroke() -// } + let kCropDashLength: CGFloat = 3 + let cancelCrop = { + for cropLayer in self.cropLayers { + cropLayer.removeFromSuperlayer() + cropLayer.removeAllAnimations() + } + } + let updateCropLayer = { (cropLayer: CAShapeLayer) in + cropLayer.fillColor = nil + cropLayer.lineWidth = 1.0 + cropLayer.lineDashPattern = [NSNumber(value: Double(kCropDashLength)), NSNumber(value: Double(kCropDashLength))] + + let viewSize = self.layersView.bounds.size + cropLayer.frame = CGRect(origin: .zero, size: viewSize) + + // Find the upper-left and bottom-right corners of the + // crop rectangle, in unit coordinates. + let unitMin = CGPointMin(self.cropStartUnit, self.cropEndUnit) + let unitMax = CGPointMax(self.cropStartUnit, self.cropEndUnit) + + let transformSampleToPoint = { (unitSample: CGPoint) -> CGPoint in + return CGPoint(x: viewSize.width * unitSample.x, + y: viewSize.height * unitSample.y) + } + + // Convert from unit coordinates to view coordinates. + let pointMin = transformSampleToPoint(unitMin) + let pointMax = transformSampleToPoint(unitMax) + let cropRect = CGRect(x: pointMin.x, + y: pointMin.y, + width: pointMax.x - pointMin.x, + height: pointMax.y - pointMin.y) + let bezierPath = UIBezierPath(rect: cropRect) + cropLayer.path = bezierPath.cgPath + } + let updateCrop = { + updateCropLayer(self.cropLayer1) + updateCropLayer(self.cropLayer2) + self.cropLayer1.strokeColor = UIColor.white.cgColor + self.cropLayer2.strokeColor = UIColor.black.cgColor + self.cropLayer1.lineDashPhase = 0 + self.cropLayer2.lineDashPhase = self.cropLayer1.lineDashPhase + kCropDashLength + } + let startCrop = { + for cropLayer in self.cropLayers { + self.layersView.layer.addSublayer(cropLayer) + } + + updateCrop() + } + let endCrop = { + updateCrop() + + for cropLayer in self.cropLayers { + cropLayer.removeFromSuperlayer() + cropLayer.removeAllAnimations() + } + } + + switch gestureRecognizer.state { + case .began: + let unitSample = unitSampleForGestureLocation(gestureRecognizer) + cropStartUnit = unitSample + cropEndUnit = unitSample + startCrop() + + case .changed: + cropEndUnit = unitSampleForGestureLocation(gestureRecognizer) + updateCrop() + case .ended: + cropEndUnit = unitSampleForGestureLocation(gestureRecognizer) + endCrop() + default: + cancelCrop() + } } // MARK: - ImageEditorModelDelegate @@ -515,6 +543,7 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate { shapeLayer.path = bezierPath.cgPath shapeLayer.fillColor = nil shapeLayer.lineCap = kCALineCapRound + shapeLayer.lineJoin = kCALineJoinRound return shapeLayer } diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 2597fe93d..0d397644e 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -194,6 +194,16 @@ CG_INLINE CGFloat CGPointDistance(CGPoint left, CGPoint right) return sqrt(delta.x * delta.x + delta.y * delta.y); } +CG_INLINE CGPoint CGPointMin(CGPoint left, CGPoint right) +{ + return CGPointMake(MIN(left.x, right.x), MIN(left.y, right.y)); +} + +CG_INLINE CGPoint CGPointMax(CGPoint left, CGPoint right) +{ + return CGPointMake(MAX(left.x, right.x), MAX(left.y, right.y)); +} + CG_INLINE CGSize CGSizeScale(CGSize size, CGFloat factor) { return CGSizeMake(size.width * factor, size.height * factor); @@ -204,6 +214,14 @@ CG_INLINE CGSize CGSizeAdd(CGSize left, CGSize right) return CGSizeMake(left.width + right.width, left.height + right.height); } +CG_INLINE CGRect CGRectScale(CGRect rect, CGFloat factor) +{ + CGRect result; + result.origin = CGPointScale(rect.origin, factor); + result.size = CGSizeScale(rect.size, factor); + return result; +} + CGFloat CGHairlineWidth(void); NS_ASSUME_NONNULL_END