import Foundation import UIKit import Display import SwiftSignalKit import ComponentFlow import LegacyComponents import AppBundle import ImageBlur import MediaEditor protocol DrawingRenderLayer: CALayer { } protocol DrawingRenderView: UIView { } protocol DrawingElement: AnyObject { var uuid: UUID { get } var translation: CGPoint { get set } var isValid: Bool { get } var bounds: CGRect { get } func setupRenderView(screenSize: CGSize) -> DrawingRenderView? func setupRenderLayer() -> DrawingRenderLayer? func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) func draw(in: CGContext, size: CGSize) } private enum DrawingOperation { case clearAll(CGRect) case slice(DrawingSlice) case addEntity(UUID) case removeEntity(DrawingEntity) } public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInteractionDelegate, TGPhotoDrawingView { public var zoomOut: () -> Void = {} public struct NavigationState { public let canUndo: Bool public let canRedo: Bool public let canClear: Bool public let canZoomOut: Bool public let isDrawing: Bool } public enum Action { case undo case redo case clear case zoomOut } enum Tool { case pen case arrow case marker case neon case eraser case blur } var tool: Tool = .pen var toolColor: DrawingColor = DrawingColor(color: .white) var toolBrushSize: CGFloat = 0.25 public var stateUpdated: (NavigationState) -> Void = { _ in } var shouldBegin: (CGPoint) -> Bool = { _ in return true } var getFullImage: () -> UIImage? = { return nil } var requestedColorPicker: () -> Void = {} var requestedEraserToggle: () -> Void = {} var requestedToolsToggle: () -> Void = {} private var undoStack: [DrawingOperation] = [] private var redoStack: [DrawingOperation] = [] fileprivate var uncommitedElement: DrawingElement? public private(set) var drawingImage: UIImage? private let renderer: UIGraphicsImageRenderer private var currentDrawingViewContainer: UIImageView private var currentDrawingRenderView: DrawingRenderView? private var currentDrawingLayer: DrawingRenderLayer? private var metalView: DrawingMetalView? private let brushSizePreviewLayer: SimpleShapeLayer let imageSize: CGSize private(set) var zoomScale: CGFloat = 1.0 private var drawingGesturePipeline: DrawingGesturePipeline? private var longPressGestureRecognizer: UILongPressGestureRecognizer? private var loadedTemplates: [UnistrokeTemplate] = [] private var previousStrokePoint: CGPoint? private var strokeRecognitionTimer: SwiftSignalKit.Timer? private var isDrawing = false private var drawingGestureStartTimestamp: Double? public var animationsEnabled = true private func loadTemplates() { func load(_ name: String) { if let url = getAppBundle().url(forResource: name, withExtension: "json"), let data = try? Data(contentsOf: url), let json = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any], let points = json["points"] as? [Any] { var strokePoints: [CGPoint] = [] for point in points { let x = (point as! [Any]).first as! Double let y = (point as! [Any]).last as! Double strokePoints.append(CGPoint(x: x, y: y)) } let template = UnistrokeTemplate(name: name, points: strokePoints) self.loadedTemplates.append(template) } } load("shape_rectangle") load("shape_circle") load("shape_star") load("shape_arrow") } private let hapticFeedback = HapticFeedback() public var screenSize: CGSize private var previousPointTimestamp: Double? private let pencilInteraction: UIInteraction? public init(size: CGSize, gestureView: UIView? = nil) { self.imageSize = size self.screenSize = size let format = UIGraphicsImageRendererFormat() format.scale = 1.0 if #available(iOS 12.0, *) { format.preferredRange = .standard } format.opaque = false self.renderer = UIGraphicsImageRenderer(size: size, format: format) self.currentDrawingViewContainer = UIImageView() self.currentDrawingViewContainer.frame = CGRect(origin: .zero, size: size) self.currentDrawingViewContainer.contentScaleFactor = 1.0 self.currentDrawingViewContainer.backgroundColor = .clear self.currentDrawingViewContainer.isUserInteractionEnabled = false self.brushSizePreviewLayer = SimpleShapeLayer() self.brushSizePreviewLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0)) self.brushSizePreviewLayer.strokeColor = UIColor(rgb: 0x919191).cgColor self.brushSizePreviewLayer.fillColor = UIColor.white.cgColor self.brushSizePreviewLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0)), transform: nil) self.brushSizePreviewLayer.opacity = 0.0 self.brushSizePreviewLayer.shadowColor = UIColor.black.cgColor self.brushSizePreviewLayer.shadowOpacity = 0.5 self.brushSizePreviewLayer.shadowOffset = CGSize(width: 0.0, height: 3.0) self.brushSizePreviewLayer.shadowRadius = 20.0 if #available(iOS 12.1, *) { let pencilInteraction = UIPencilInteraction() self.pencilInteraction = pencilInteraction } else { self.pencilInteraction = nil } super.init(frame: CGRect(origin: .zero, size: size)) if #available(iOS 12.1, *), let pencilInteraction = self.pencilInteraction as? UIPencilInteraction { pencilInteraction.delegate = self self.addInteraction(pencilInteraction) } self.backgroundColor = .clear self.contentScaleFactor = 1.0 self.isExclusiveTouch = true self.addSubview(self.currentDrawingViewContainer) self.layer.addSublayer(self.brushSizePreviewLayer) let drawingGesturePipeline = DrawingGesturePipeline(drawingView: self, gestureView: gestureView ?? self) drawingGesturePipeline.gestureRecognizer?.shouldBegin = { [weak self] point in if let strongSelf = self { if !strongSelf.shouldBegin(point) { return false } if strongSelf.undoStack.isEmpty && !strongSelf.hasOpaqueData && strongSelf.tool == .eraser { return false } if strongSelf.tool == .blur, strongSelf.preparedBlurredImage == nil { return false } if let uncommitedElement = strongSelf.uncommitedElement as? PenTool, uncommitedElement.isFinishingArrow { return false } return true } else { return false } } drawingGesturePipeline.onDrawing = { [weak self] state, point in guard let strongSelf = self else { return } let currentTimestamp = CACurrentMediaTime() switch state { case .began: strongSelf.isDrawing = true strongSelf.previousStrokePoint = nil strongSelf.drawingGestureStartTimestamp = currentTimestamp strongSelf.previousPointTimestamp = currentTimestamp if strongSelf.uncommitedElement != nil { strongSelf.finishDrawing(rect: CGRect(origin: .zero, size: strongSelf.imageSize), synchronous: true) } if case .marker = strongSelf.tool, let metalView = strongSelf.metalView { metalView.isHidden = false } guard let newElement = strongSelf.setupNewElement() else { return } if let renderView = newElement.setupRenderView(screenSize: strongSelf.screenSize) { if let currentDrawingView = strongSelf.currentDrawingRenderView { strongSelf.currentDrawingRenderView = nil currentDrawingView.removeFromSuperview() } if strongSelf.tool == .eraser { strongSelf.currentDrawingViewContainer.removeFromSuperview() strongSelf.currentDrawingViewContainer.backgroundColor = .white renderView.layer.compositingFilter = "xor" strongSelf.currentDrawingViewContainer.addSubview(renderView) strongSelf.mask = strongSelf.currentDrawingViewContainer } else if strongSelf.tool == .blur { strongSelf.currentDrawingViewContainer.mask = renderView strongSelf.currentDrawingViewContainer.image = strongSelf.preparedBlurredImage } else { strongSelf.currentDrawingViewContainer.addSubview(renderView) } strongSelf.currentDrawingRenderView = renderView } if let renderLayer = newElement.setupRenderLayer() { if let currentDrawingLayer = strongSelf.currentDrawingLayer { strongSelf.currentDrawingLayer = nil currentDrawingLayer.removeFromSuperlayer() } if strongSelf.tool == .eraser { strongSelf.currentDrawingViewContainer.removeFromSuperview() strongSelf.currentDrawingViewContainer.backgroundColor = .white renderLayer.compositingFilter = "xor" strongSelf.currentDrawingViewContainer.layer.addSublayer(renderLayer) strongSelf.mask = strongSelf.currentDrawingViewContainer } else if strongSelf.tool == .blur { strongSelf.currentDrawingViewContainer.layer.mask = renderLayer strongSelf.currentDrawingViewContainer.image = strongSelf.preparedBlurredImage } else { strongSelf.currentDrawingViewContainer.layer.addSublayer(renderLayer) } strongSelf.currentDrawingLayer = renderLayer } newElement.updatePath(point, state: state, zoomScale: strongSelf.zoomScale) strongSelf.uncommitedElement = newElement strongSelf.updateInternalState() case .changed: if let previousPointTimestamp = strongSelf.previousPointTimestamp, currentTimestamp - previousPointTimestamp < 0.016 { return } strongSelf.previousPointTimestamp = currentTimestamp strongSelf.uncommitedElement?.updatePath(point, state: state, zoomScale: strongSelf.zoomScale) // if case let .direct(point) = path, let lastPoint = line.points.last { // if let previousStrokePoint = strongSelf.previousStrokePoint, line.points.count > 10 { // let currentTimestamp = CACurrentMediaTime() // if lastPoint.location.distance(to: previousStrokePoint) > 10.0 { // strongSelf.previousStrokePoint = lastPoint.location // // strongSelf.strokeRecognitionTimer?.invalidate() // strongSelf.strokeRecognitionTimer = nil // } // // if strongSelf.strokeRecognitionTimer == nil, let startTimestamp = strongSelf.drawingGestureStartTimestamp, currentTimestamp - startTimestamp < 3.0 { // strongSelf.strokeRecognitionTimer = SwiftSignalKit.Timer(timeout: 0.85, repeat: false, completion: { [weak self] in // guard let strongSelf = self else { // return // } // if let previousStrokePoint = strongSelf.previousStrokePoint, lastPoint.location.distance(to: previousStrokePoint) <= 10.0 { // let strokeRecognizer = Unistroke(points: line.points.map { $0.location }) // if let template = strokeRecognizer.match(templates: strongSelf.loadedTemplates, minThreshold: 0.5) { // let edges = line.bounds // let bounds = CGRect(origin: edges.origin, size: CGSize(width: edges.width - edges.minX, height: edges.height - edges.minY)) // // var entity: DrawingEntity? // if template == "shape_rectangle" { // let shapeEntity = DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize) // shapeEntity.referenceDrawingSize = strongSelf.imageSize // shapeEntity.position = bounds.center // shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1) // entity = shapeEntity // } else if template == "shape_circle" { // let shapeEntity = DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize) // shapeEntity.referenceDrawingSize = strongSelf.imageSize // shapeEntity.position = bounds.center // shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1) // entity = shapeEntity // } else if template == "shape_star" { // let shapeEntity = DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize) // shapeEntity.referenceDrawingSize = strongSelf.imageSize // shapeEntity.position = bounds.center // shapeEntity.size = CGSize(width: max(bounds.width, bounds.height) * 1.1, height: max(bounds.width, bounds.height) * 1.1) // entity = shapeEntity // } else if template == "shape_arrow" { // let arrowEntity = DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize) // arrowEntity.referenceDrawingSize = strongSelf.imageSize // arrowEntity.start = line.points.first?.location ?? .zero // arrowEntity.end = line.points[line.points.count - 4].location // entity = arrowEntity // } // // if let entity = entity { // strongSelf.entitiesView?.add(entity) // strongSelf.entitiesView?.selectEntity(entity) // strongSelf.cancelDrawing() // strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = false // strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = true // } // } // } // strongSelf.strokeRecognitionTimer?.invalidate() // strongSelf.strokeRecognitionTimer = nil // }, queue: Queue.mainQueue()) // strongSelf.strokeRecognitionTimer?.start() // } // } else { // strongSelf.previousStrokePoint = lastPoint.location // } // } case .ended, .cancelled: strongSelf.isDrawing = false strongSelf.strokeRecognitionTimer?.invalidate() strongSelf.strokeRecognitionTimer = nil strongSelf.uncommitedElement?.updatePath(point, state: state, zoomScale: strongSelf.zoomScale) if strongSelf.uncommitedElement?.isValid == true { let bounds = strongSelf.uncommitedElement?.bounds Queue.mainQueue().after(0.05) { if let bounds = bounds { strongSelf.finishDrawing(rect: bounds, synchronous: true) } } } else { strongSelf.cancelDrawing() } strongSelf.updateInternalState() } } self.drawingGesturePipeline = drawingGesturePipeline let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) longPressGestureRecognizer.minimumPressDuration = 0.45 longPressGestureRecognizer.allowableMovement = 2.0 longPressGestureRecognizer.delegate = self self.addGestureRecognizer(longPressGestureRecognizer) self.longPressGestureRecognizer = longPressGestureRecognizer } public override var isUserInteractionEnabled: Bool { get { return super.isUserInteractionEnabled } set { super.isUserInteractionEnabled = newValue self.drawingGesturePipeline?.enabled = newValue } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.longPressTimer?.invalidate() self.strokeRecognitionTimer?.invalidate() } private var clearImage: UIImage? public func setup(withDrawing drawingData: Data?, storeAsClear: Bool = false) { self.undoStack = [] self.redoStack = [] if let drawingData = drawingData, let image = UIImage(data: drawingData) { if let context = DrawingContext(size: image.size, scale: 1.0, opaque: false) { context.withFlippedContext { context in context.clear(CGRect(origin: .zero, size: image.size)) if let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: image.size)) } } self.drawingImage = context.generateImage() ?? image } else { self.drawingImage = image } if storeAsClear { self.clearImage = self.drawingImage } else { self.hasOpaqueData = true } self.layer.contents = image.cgImage self.updateInternalState() } else { self.drawingImage = nil self.layer.contents = nil self.updateInternalState() } } public var emptyColor: UIColor? public func clearWithEmptyColor() { if let clearImage = self.clearImage { self.drawingImage = clearImage } else { if let context = DrawingContext(size: self.imageSize, scale: 1.0, opaque: false) { context.withFlippedContext { context in if let emptyColor = self.emptyColor { context.setFillColor(emptyColor.cgColor) context.fill(CGRect(origin: .zero, size: self.imageSize)) } else { context.clear(CGRect(origin: .zero, size: self.imageSize)) } } self.drawingImage = context.generateImage() ?? nil } } self.layer.contents = self.drawingImage?.cgImage self.updateInternalState() } var hasOpaqueData = false public var drawingData: Data? { guard !self.undoStack.isEmpty || self.hasOpaqueData else { return nil } return self.drawingImage?.pngData() } public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @available(iOS 12.1, *) public func pencilInteractionDidTap(_ interaction: UIPencilInteraction) { switch UIPencilInteraction.preferredTapAction { case .switchEraser: self.requestedEraserToggle() case .showColorPalette: self.requestedColorPicker() case .switchPrevious: self.requestedToolsToggle() default: break } } private var longPressTimer: SwiftSignalKit.Timer? private var fillCircleLayer: CALayer? @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { let location = gestureRecognizer.location(in: self) switch gestureRecognizer.state { case .began: self.longPressTimer?.invalidate() self.longPressTimer = nil if self.longPressTimer == nil { var toolColor = self.toolColor var blurredImage: UIImage? if self.tool == .marker { toolColor = toolColor.withUpdatedAlpha(toolColor.alpha * 0.7) } else if self.tool == .eraser { toolColor = DrawingColor.clear } else if self.tool == .blur { blurredImage = self.preparedBlurredImage } self.hapticFeedback.prepareImpact(.medium) let fillCircleLayer = SimpleShapeLayer() self.longPressTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self, weak fillCircleLayer] in if let strongSelf = self { strongSelf.cancelDrawing() let action = { let newElement = FillTool(drawingSize: strongSelf.imageSize, color: toolColor, blur: blurredImage != nil, blurredImage: blurredImage) strongSelf.uncommitedElement = newElement strongSelf.finishDrawing(rect: CGRect(origin: .zero, size: strongSelf.imageSize), synchronous: true) } if [.eraser, .blur].contains(strongSelf.tool) { UIView.transition(with: strongSelf, duration: 0.2, options: .transitionCrossDissolve) { action() } } else { action() } strongSelf.fillCircleLayer = nil fillCircleLayer?.removeFromSuperlayer() strongSelf.hapticFeedback.impact(.medium) } }, queue: Queue.mainQueue()) self.longPressTimer?.start() if [.eraser, .blur].contains(self.tool) { } else { fillCircleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0)) fillCircleLayer.position = location fillCircleLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0))).cgPath fillCircleLayer.fillColor = toolColor.toCGColor() self.layer.addSublayer(fillCircleLayer) self.fillCircleLayer = fillCircleLayer fillCircleLayer.animateScale(from: 0.01, to: 12.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] _ in if let strongSelf = self { if let fillCircleLayer = strongSelf.fillCircleLayer { strongSelf.fillCircleLayer = nil fillCircleLayer.removeFromSuperlayer() } } }) } } case .ended, .cancelled: self.longPressTimer?.invalidate() self.longPressTimer = nil if let fillCircleLayer = self.fillCircleLayer { self.fillCircleLayer = nil fillCircleLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fillCircleLayer] _ in fillCircleLayer?.removeFromSuperlayer() }) } default: break } } private let queue = Queue() private func commit(interactive: Bool = false, synchronous: Bool = true, completion: @escaping () -> Void = {}) { let currentImage = self.drawingImage let uncommitedElement = self.uncommitedElement let imageSize = self.imageSize let action = { let updatedImage = self.renderer.image { context in context.cgContext.setBlendMode(.copy) context.cgContext.clear(CGRect(origin: .zero, size: imageSize)) if let image = currentImage { image.draw(at: .zero) } if let uncommitedElement = uncommitedElement { context.cgContext.setBlendMode(.normal) uncommitedElement.draw(in: context.cgContext, size: imageSize) } } Queue.mainQueue().async { self.drawingImage = updatedImage self.layer.contents = updatedImage.cgImage if let currentDrawingRenderView = self.currentDrawingRenderView { if case .eraser = self.tool { currentDrawingRenderView.removeFromSuperview() self.mask = nil self.insertSubview(self.currentDrawingViewContainer, at: 0) self.currentDrawingViewContainer.backgroundColor = .clear } else if case .blur = self.tool { self.currentDrawingViewContainer.mask = nil self.currentDrawingViewContainer.image = nil } else { if let renderView = currentDrawingRenderView as? PenTool.RenderView, renderView.isDryingUp { renderView.onDryingUp = { [weak renderView] in renderView?.removeFromSuperview() } } else { currentDrawingRenderView.removeFromSuperview() } } self.currentDrawingRenderView = nil } if let currentDrawingLayer = self.currentDrawingLayer { if case .eraser = self.tool { currentDrawingLayer.removeFromSuperlayer() self.mask = nil self.insertSubview(self.currentDrawingViewContainer, at: 0) self.currentDrawingViewContainer.backgroundColor = .clear } else if case .blur = self.tool { self.currentDrawingViewContainer.layer.mask = nil self.currentDrawingViewContainer.image = nil } else { currentDrawingLayer.removeFromSuperlayer() } self.currentDrawingLayer = nil } if self.tool == .marker { //self.metalView?.clear() self.metalView?.isHidden = true } completion() } } if synchronous { action() } else { self.queue.async { action() } } } fileprivate func cancelDrawing() { self.uncommitedElement = nil if let currentDrawingRenderView = self.currentDrawingRenderView { if case .eraser = self.tool { currentDrawingRenderView.removeFromSuperview() self.mask = nil self.insertSubview(self.currentDrawingViewContainer, at: 0) self.currentDrawingViewContainer.backgroundColor = .clear } else if case .blur = self.tool { self.currentDrawingViewContainer.mask = nil self.currentDrawingViewContainer.image = nil } else { currentDrawingRenderView.removeFromSuperview() } self.currentDrawingRenderView = nil } if let currentDrawingLayer = self.currentDrawingLayer { if self.tool == .eraser { currentDrawingLayer.removeFromSuperlayer() self.mask = nil self.insertSubview(self.currentDrawingViewContainer, at: 0) self.currentDrawingViewContainer.backgroundColor = .clear } else if self.tool == .blur { self.currentDrawingViewContainer.mask = nil self.currentDrawingViewContainer.image = nil } else { currentDrawingLayer.removeFromSuperlayer() } self.currentDrawingLayer = nil } if case .marker = self.tool { self.metalView?.isHidden = true } } private func slice(for rect: CGRect) -> DrawingSlice? { if let subImage = self.drawingImage?.cgImage?.cropping(to: rect) { return DrawingSlice(image: subImage, rect: rect) } return nil } fileprivate func finishDrawing(rect: CGRect, synchronous: Bool = false) { let complete: (Bool) -> Void = { synchronous in if let uncommitedElement = self.uncommitedElement, !uncommitedElement.isValid { self.uncommitedElement = nil } if !self.undoStack.isEmpty || self.hasOpaqueData, let slice = self.slice(for: rect) { self.undoStack.append(.slice(slice)) } else { self.undoStack.append(.clearAll(rect)) } self.commit(interactive: true, synchronous: synchronous) self.redoStack.removeAll() self.uncommitedElement = nil self.updateInternalState() } if let uncommitedElement = self.uncommitedElement as? PenTool { if uncommitedElement.hasArrow { uncommitedElement.finishArrow { complete(true) } } else { complete(true) } } else { complete(synchronous) } } weak var entitiesView: DrawingEntitiesView? func clear() { self.entitiesView?.removeAll() self.uncommitedElement = nil self.undoStack.removeAll() self.redoStack.removeAll() self.hasOpaqueData = false let snapshotView = UIImageView(image: self.drawingImage) snapshotView.frame = self.bounds self.addSubview(snapshotView) self.drawingImage = nil self.layer.contents = nil Queue.mainQueue().justDispatch { snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } self.updateInternalState() self.updateBlurredImage() } private func applySlice(_ slice: DrawingSlice) { let updatedImage = self.renderer.image { context in context.cgContext.clear(CGRect(origin: .zero, size: imageSize)) context.cgContext.setBlendMode(.copy) if let image = self.drawingImage { image.draw(at: .zero) } if let image = slice.image { context.cgContext.translateBy(x: imageSize.width / 2.0, y: imageSize.height / 2.0) context.cgContext.scaleBy(x: 1.0, y: -1.0) context.cgContext.translateBy(x: -imageSize.width / 2.0, y: -imageSize.height / 2.0) context.cgContext.translateBy(x: slice.rect.minX, y: imageSize.height - slice.rect.maxY) context.cgContext.draw(image, in: CGRect(origin: .zero, size: slice.rect.size)) } } self.drawingImage = updatedImage self.layer.contents = updatedImage.cgImage } var canUndo: Bool { return !self.undoStack.isEmpty } private func undo() { guard let lastOperation = self.undoStack.last else { return } switch lastOperation { case let .clearAll(rect): if let slice = self.slice(for: rect) { self.redoStack.append(.slice(slice)) } UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { if let _ = self.emptyColor { self.clearWithEmptyColor() } else { self.drawingImage = nil self.layer.contents = nil } } self.updateBlurredImage() case let .slice(slice): if let slice = self.slice(for: slice.rect) { self.redoStack.append(.slice(slice)) } UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { self.applySlice(slice) } self.updateBlurredImage() case let .addEntity(uuid): if let entityView = self.entitiesView?.getView(for: uuid) { self.entitiesView?.remove(uuid: uuid, animated: true, announce: false) self.redoStack.append(.removeEntity(entityView.entity)) } case let .removeEntity(entity): if let view = self.entitiesView?.add(entity, announce: false) { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) if !(entity is DrawingVectorEntity) { view.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) } } self.redoStack.append(.addEntity(entity.uuid)) } self.undoStack.removeLast() self.updateInternalState() } private func redo() { guard let lastOperation = self.redoStack.last else { return } switch lastOperation { case .clearAll: break case let .slice(slice): if !self.undoStack.isEmpty || self.hasOpaqueData, let slice = self.slice(for: slice.rect) { self.undoStack.append(.slice(slice)) } else { self.undoStack.append(.clearAll(slice.rect)) } UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { self.applySlice(slice) } self.updateBlurredImage() case let .addEntity(uuid): if let entityView = self.entitiesView?.getView(for: uuid) { self.entitiesView?.remove(uuid: uuid, animated: true, announce: false) self.undoStack.append(.removeEntity(entityView.entity)) } case let .removeEntity(entity): if let view = self.entitiesView?.add(entity, announce: false) { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) if !(entity is DrawingVectorEntity) { view.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) } } self.undoStack.append(.addEntity(entity.uuid)) } self.redoStack.removeLast() self.updateInternalState() } func onEntityAdded(_ entity: DrawingEntity) { self.redoStack.removeAll() self.undoStack.append(.addEntity(entity.uuid)) self.updateInternalState() } func onEntityRemoved(_ entity: DrawingEntity) { self.redoStack.removeAll() self.undoStack.append(.removeEntity(entity)) self.updateInternalState() } private var preparedBlurredImage: UIImage? public var appliedToolState: DrawingToolState? { switch self.tool { case .pen: return .pen(DrawingToolState.BrushState(color: self.toolColor, size: self.toolBrushSize)) case .arrow: return .arrow(DrawingToolState.BrushState(color: self.toolColor, size: self.toolBrushSize)) case .marker: return .marker(DrawingToolState.BrushState(color: self.toolColor, size: self.toolBrushSize)) case .neon: return .neon(DrawingToolState.BrushState(color: self.toolColor, size: self.toolBrushSize)) case .blur: return .blur(DrawingToolState.EraserState(size: self.toolBrushSize)) case .eraser: return .eraser(DrawingToolState.EraserState(size: self.toolBrushSize)) } } public func updateToolState(_ state: DrawingToolState) { let previousTool = self.tool switch state { case let .pen(brushState): self.tool = .pen self.toolColor = brushState.color self.toolBrushSize = brushState.size case let .arrow(brushState): self.tool = .arrow self.toolColor = brushState.color self.toolBrushSize = brushState.size case let .marker(brushState): self.tool = .marker self.toolColor = brushState.color self.toolBrushSize = brushState.size var size = self.imageSize if Int(size.width) % 16 != 0 { size.width = ceil(size.width / 16.0) * 16.0 } if self.metalView == nil, let metalView = DrawingMetalView(size: size) { metalView.transform = self.currentDrawingViewContainer.transform if size.width != self.imageSize.width { let scaledSize = size.preciseAspectFilled(self.currentDrawingViewContainer.frame.size) metalView.frame = CGRect(origin: .zero, size: scaledSize) } else { metalView.frame = self.currentDrawingViewContainer.frame } self.insertSubview(metalView, aboveSubview: self.currentDrawingViewContainer) self.metalView = metalView } case let .neon(brushState): self.tool = .neon self.toolColor = brushState.color self.toolBrushSize = brushState.size case let .blur(blurState): self.tool = .blur self.toolBrushSize = blurState.size case let .eraser(eraserState): self.tool = .eraser self.toolBrushSize = eraserState.size } if self.tool != previousTool { self.updateBlurredImage() } } func updateBlurredImage() { if case .blur = self.tool { Queue.concurrentDefaultQueue().async { if let image = self.getFullImage() { Queue.mainQueue().async { self.preparedBlurredImage = image } } } } else { self.preparedBlurredImage = nil } } public func performAction(_ action: Action) { switch action { case .undo: self.undo() case .redo: self.redo() case .clear: self.clear() case .zoomOut: self.zoomOut() } } public var internalState: NavigationState { return NavigationState( canUndo: !self.undoStack.isEmpty, canRedo: !self.redoStack.isEmpty, canClear: !self.undoStack.isEmpty || self.hasOpaqueData || (self.entitiesView?.hasEntities ?? false), canZoomOut: self.zoomScale > 1.0 + .ulpOfOne, isDrawing: self.isDrawing ) } private func updateInternalState() { self.stateUpdated(self.internalState) } public func updateZoomScale(_ scale: CGFloat) { self.cancelDrawing() self.zoomScale = scale self.updateInternalState() } private func setupNewElement() -> DrawingElement? { let scale = 1.0 / self.zoomScale let element: DrawingElement? switch self.tool { case .pen: let penTool = PenTool( drawingSize: self.imageSize, color: self.toolColor, lineWidth: self.toolBrushSize * scale, hasArrow: false, isEraser: false, isBlur: false, blurredImage: nil, animationsEnabled: self.animationsEnabled ) element = penTool case .arrow: let penTool = PenTool( drawingSize: self.imageSize, color: self.toolColor, lineWidth: self.toolBrushSize * scale, hasArrow: true, isEraser: false, isBlur: false, blurredImage: nil, animationsEnabled: self.animationsEnabled ) element = penTool case .marker: let markerTool = MarkerTool( drawingSize: self.imageSize, color: self.toolColor, lineWidth: self.toolBrushSize * scale ) markerTool.metalView = self.metalView element = markerTool case .neon: element = NeonTool( drawingSize: self.imageSize, color: self.toolColor, lineWidth: self.toolBrushSize * scale ) case .blur: let penTool = PenTool( drawingSize: self.imageSize, color: self.toolColor, lineWidth: self.toolBrushSize * scale, hasArrow: false, isEraser: false, isBlur: true, blurredImage: self.preparedBlurredImage, animationsEnabled: self.animationsEnabled ) element = penTool case .eraser: let penTool = PenTool( drawingSize: self.imageSize, color: self.toolColor, lineWidth: self.toolBrushSize * scale, hasArrow: false, isEraser: true, isBlur: false, blurredImage: nil, animationsEnabled: self.animationsEnabled ) element = penTool } return element } func setBrushSizePreview(_ size: CGFloat?) { let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let size = size { let minLineWidth = max(1.0, max(self.frame.width, self.frame.height) * 0.002) let maxLineWidth = max(10.0, max(self.frame.width, self.frame.height) * 0.07) let minBrushSize = minLineWidth let maxBrushSize = maxLineWidth let brushSize = minBrushSize + (maxBrushSize - minBrushSize) * size self.brushSizePreviewLayer.transform = CATransform3DMakeScale(brushSize / 100.0, brushSize / 100.0, 1.0) transition.setAlpha(layer: self.brushSizePreviewLayer, alpha: 1.0) } else { transition.setAlpha(layer: self.brushSizePreviewLayer, alpha: 0.0) } } public override func layoutSubviews() { super.layoutSubviews() let scale = self.scale let transform = CGAffineTransformMakeScale(scale, scale) self.currentDrawingViewContainer.transform = transform self.currentDrawingViewContainer.frame = self.bounds self.drawingGesturePipeline?.transform = CGAffineTransformMakeScale(1.0 / scale, 1.0 / scale) if let metalView = self.metalView { var size = self.imageSize if Int(size.width) % 16 != 0 { size.width = ceil(size.width / 16.0) * 16.0 } metalView.transform = transform if size.width != self.imageSize.width { let scaledSize = size.preciseAspectFilled(self.currentDrawingViewContainer.frame.size) metalView.frame = CGRect(origin: .zero, size: scaledSize) } else { metalView.frame = self.currentDrawingViewContainer.frame } } self.brushSizePreviewLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) } public var isEmpty: Bool { return self.undoStack.isEmpty && !self.hasOpaqueData } public var scale: CGFloat { return self.bounds.width / self.imageSize.width } public var isTracking: Bool { return self.uncommitedElement != nil } } private extension CGSize { func preciseAspectFilled(_ size: CGSize) -> CGSize { let scale = max(size.width / max(1.0, self.width), size.height / max(1.0, self.height)) return CGSize(width: self.width * scale, height: self.height * scale) } } private class DrawingSlice { private static let queue = Queue() var _image: CGImage? let uuid: UUID var image: CGImage? { if let image = self._image { return image } else if let data = try? Data(contentsOf: URL(fileURLWithPath: self.path)) { return UIImage(data: data)?.cgImage } else { return nil } } let rect: CGRect let path: String init(image: CGImage, rect: CGRect) { self.uuid = UUID() self._image = image self.rect = rect self.path = NSTemporaryDirectory() + "/drawing_\(uuid.hashValue).slice" DrawingSlice.queue.after(2.0) { let image = UIImage(cgImage: image) if let data = image.pngData() as? NSData { try? data.write(toFile: self.path) Queue.mainQueue().async { self._image = nil } } } } deinit { try? FileManager.default.removeItem(atPath: self.path) } }