import Foundation import UIKit import Display import SwiftSignalKit import ComponentFlow import LegacyComponents import AppBundle import ImageBlur protocol DrawingElement: AnyObject { var uuid: UUID { get } var translation: CGPoint { get set } var isValid: Bool { get } func setupRenderLayer() -> DrawingRenderLayer? func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) func draw(in: CGContext, size: CGSize) } enum DrawingOperation { case element(DrawingElement) case addEntity(UUID) case removeEntity(DrawingEntity) } public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDrawingView { public var zoomOut: () -> Void = {} struct NavigationState { let canUndo: Bool let canRedo: Bool let canClear: Bool let canZoomOut: Bool let isDrawing: Bool } 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 var stateUpdated: (NavigationState) -> Void = { _ in } var shouldBegin: (CGPoint) -> Bool = { _ in return true } var getFullImage: () -> UIImage? = { return nil } private var elements: [DrawingElement] = [] private var undoStack: [DrawingOperation] = [] private var redoStack: [DrawingOperation] = [] fileprivate var uncommitedElement: DrawingElement? private(set) var drawingImage: UIImage? private let renderer: UIGraphicsImageRenderer private var currentDrawingView: UIImageView private var currentDrawingLayer: DrawingRenderLayer? private var pannedSelectionView: UIView 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? 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() init(size: CGSize) { self.imageSize = size let format = UIGraphicsImageRendererFormat() format.scale = 1.0 self.renderer = UIGraphicsImageRenderer(size: size, format: format) self.currentDrawingView = UIImageView() self.currentDrawingView.frame = CGRect(origin: .zero, size: size) self.currentDrawingView.contentScaleFactor = 1.0 self.currentDrawingView.backgroundColor = .clear self.currentDrawingView.isUserInteractionEnabled = false self.pannedSelectionView = UIView() self.pannedSelectionView.frame = CGRect(origin: .zero, size: size) self.pannedSelectionView.contentScaleFactor = 1.0 self.pannedSelectionView.backgroundColor = .clear self.pannedSelectionView.isUserInteractionEnabled = false self.metalView = DrawingMetalView(size: size)! self.metalView.isHidden = true 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 super.init(frame: CGRect(origin: .zero, size: size)) Queue.mainQueue().async { self.loadTemplates() } self.backgroundColor = .clear self.contentScaleFactor = 1.0 self.isExclusiveTouch = true self.addSubview(self.currentDrawingView) self.addSubview(self.metalView) self.layer.addSublayer(self.brushSizePreviewLayer) let drawingGesturePipeline = DrawingGesturePipeline(view: self) drawingGesturePipeline.gestureRecognizer?.shouldBegin = { [weak self] point in if let strongSelf = self { if !strongSelf.shouldBegin(point) { return false } if strongSelf.elements.isEmpty && !strongSelf.hasOpaqueData && strongSelf.tool == .eraser { return false } if let uncommitedElement = strongSelf.uncommitedElement as? PenTool, uncommitedElement.isFinishingArrow { return false } return true } else { return false } } drawingGesturePipeline.onDrawing = { [weak self] state, path in guard let strongSelf = self else { return } switch state { case .began: strongSelf.isDrawing = true strongSelf.previousStrokePoint = nil strongSelf.drawingGestureStartTimestamp = CACurrentMediaTime() if strongSelf.uncommitedElement != nil { strongSelf.finishDrawing() } guard let newElement = strongSelf.prepareNewElement() else { return } if newElement is MarkerTool { strongSelf.metalView.isHidden = false } if let renderLayer = newElement.setupRenderLayer() { if let currentDrawingLayer = strongSelf.currentDrawingLayer { strongSelf.currentDrawingLayer = nil currentDrawingLayer.removeFromSuperlayer() } if strongSelf.tool == .eraser { strongSelf.currentDrawingView.removeFromSuperview() strongSelf.currentDrawingView.backgroundColor = .white renderLayer.compositingFilter = "xor" strongSelf.currentDrawingView.layer.addSublayer(renderLayer) strongSelf.mask = strongSelf.currentDrawingView } else if strongSelf.tool == .blur { strongSelf.currentDrawingView.layer.mask = renderLayer strongSelf.currentDrawingView.image = strongSelf.preparedBlurredImage } else { strongSelf.currentDrawingView.layer.addSublayer(renderLayer) } strongSelf.currentDrawingLayer = renderLayer } newElement.updatePath(path, state: state) strongSelf.uncommitedElement = newElement strongSelf.updateInternalState() case .changed: strongSelf.uncommitedElement?.updatePath(path, state: state) if case let .polyline(line) = 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: strongSelf.isDrawing = false strongSelf.strokeRecognitionTimer?.invalidate() strongSelf.strokeRecognitionTimer = nil strongSelf.uncommitedElement?.updatePath(path, state: state) Queue.mainQueue().after(0.05) { strongSelf.finishDrawing() } strongSelf.updateInternalState() case .cancelled: strongSelf.isDrawing = false strongSelf.strokeRecognitionTimer?.invalidate() strongSelf.strokeRecognitionTimer = nil 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 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.longPressTimer?.invalidate() self.strokeRecognitionTimer?.invalidate() } public func setup(withDrawing drawingData: Data!) { if let drawingData = drawingData, let image = UIImage(data: drawingData) { self.hasOpaqueData = true self.drawingImage = image self.layer.contents = image.cgImage //let codableElements = try? JSONDecoder().decode([CodableDrawingElement].self, from: drawingData) { //self.elements = codableElements.map { $0.element } //self.commit(reset: true) self.updateInternalState() } } var hasOpaqueData = false var drawingData: Data? { guard !self.elements.isEmpty || self.hasOpaqueData else { return nil } return self.drawingImage?.pngData() // let codableElements = self.elements.compactMap({ CodableDrawingElement(element: $0) }) // if let data = try? JSONEncoder().encode(codableElements) { // return data // } else { // return nil // } } public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } 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.tap() 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(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 var skipDrawing = Set() private func commit(reset: Bool = false, interactive: Bool = false, synchronous: Bool = true, completion: @escaping () -> Void = {}) { let currentImage = self.drawingImage let uncommitedElement = self.uncommitedElement let imageSize = self.imageSize let skipDrawing = self.skipDrawing let action = { let updatedImage = self.renderer.image { context in if !reset { context.cgContext.clear(CGRect(origin: .zero, size: imageSize)) if let image = currentImage { image.draw(at: .zero) } if let uncommitedElement = uncommitedElement { uncommitedElement.draw(in: context.cgContext, size: imageSize) } } else { context.cgContext.clear(CGRect(origin: .zero, size: imageSize)) for element in self.elements { if !skipDrawing.contains(element.uuid) { element.draw(in: context.cgContext, size: imageSize) } } } } Queue.mainQueue().async { self.drawingImage = updatedImage self.layer.contents = updatedImage.cgImage if let currentDrawingLayer = self.currentDrawingLayer { if case .eraser = self.tool { currentDrawingLayer.removeFromSuperlayer() self.mask = nil self.insertSubview(self.currentDrawingView, at: 0) self.currentDrawingView.backgroundColor = .clear } else if case .blur = self.tool { self.currentDrawingView.layer.mask = nil self.currentDrawingView.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() } } } private func updateSelectionContent() { let selectionImage = self.renderer.image { context in for element in self.elements { if self.skipDrawing.contains(element.uuid) { element.draw(in: context.cgContext, size: self.imageSize) } } } self.pannedSelectionView.layer.contents = selectionImage.cgImage } fileprivate func cancelDrawing() { self.uncommitedElement = nil if let currentDrawingLayer = self.currentDrawingLayer { if self.tool == .eraser { currentDrawingLayer.removeFromSuperlayer() self.mask = nil self.insertSubview(self.currentDrawingView, at: 0) self.currentDrawingView.backgroundColor = .clear } else if self.tool == .blur { self.currentDrawingView.mask = nil } else { currentDrawingLayer.removeFromSuperlayer() } self.currentDrawingLayer = nil } } fileprivate func finishDrawing(synchronous: Bool = false) { let complete: (Bool) -> Void = { synchronous in if let uncommitedElement = self.uncommitedElement, !uncommitedElement.isValid { self.uncommitedElement = nil } self.commit(interactive: true, synchronous: synchronous) self.redoStack.removeAll() if let uncommitedElement = self.uncommitedElement { self.elements.append(uncommitedElement) self.undoStack.append(.element(uncommitedElement)) self.uncommitedElement = nil } self.updateInternalState() } if let uncommitedElement = self.uncommitedElement as? PenTool, uncommitedElement.hasArrow { uncommitedElement.finishArrow({ complete(true) }) } else { complete(synchronous) } } weak var entitiesView: DrawingEntitiesView? func clear() { self.entitiesView?.removeAll() self.uncommitedElement = nil self.elements.removeAll() 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.commit(reset: true) 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() } var canUndo: Bool { return !self.undoStack.isEmpty } private func undo() { guard let lastOperation = self.undoStack.last else { return } switch lastOperation { case let .element(element): self.uncommitedElement = nil self.redoStack.append(.element(element)) self.elements.removeAll(where: { $0.uuid == element.uuid }) UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { self.commit(reset: true) } 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 let .element(element): self.uncommitedElement = nil self.elements.append(element) self.undoStack.append(.element(element)) self.uncommitedElement = element UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { self.commit(reset: false) } self.uncommitedElement = nil 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? func updateToolState(_ state: DrawingToolState) { let previousTool = self.tool switch state { case let .pen(brushState): self.drawingGesturePipeline?.mode = .direct self.tool = .pen self.toolColor = brushState.color self.toolBrushSize = brushState.size case let .arrow(brushState): self.drawingGesturePipeline?.mode = .direct self.tool = .arrow self.toolColor = brushState.color self.toolBrushSize = brushState.size case let .marker(brushState): self.drawingGesturePipeline?.mode = .location self.tool = .marker self.toolColor = brushState.color self.toolBrushSize = brushState.size case let .neon(brushState): self.drawingGesturePipeline?.mode = .smoothCurve self.tool = .neon self.toolColor = brushState.color self.toolBrushSize = brushState.size case let .blur(blurState): self.tool = .blur self.drawingGesturePipeline?.mode = .direct self.toolBrushSize = blurState.size case let .eraser(eraserState): self.tool = .eraser self.drawingGesturePipeline?.mode = .direct self.toolBrushSize = eraserState.size } if self.tool != previousTool { self.updateBlurredImage() } else { self.preparedBlurredImage = nil } } 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 } } func performAction(_ action: Action) { switch action { case .undo: self.undo() case .redo: self.redo() case .clear: self.clear() case .zoomOut: self.zoomOut() } } private func updateInternalState() { self.stateUpdated(NavigationState( canUndo: !self.undoStack.isEmpty, canRedo: !self.redoStack.isEmpty, canClear: !self.elements.isEmpty || self.hasOpaqueData || !(self.entitiesView?.entities.isEmpty ?? true), canZoomOut: self.zoomScale > 1.0 + .ulpOfOne, isDrawing: self.isDrawing )) } public func updateZoomScale(_ scale: CGFloat) { self.cancelDrawing() self.zoomScale = scale self.updateInternalState() } private func prepareNewElement() -> 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 ) 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 ) 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 ) 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 ) element = penTool } return element } func removeElement(_ element: DrawingElement) { self.elements.removeAll(where: { $0 === element }) self.commit(reset: true) } func removeElements(_ elements: [UUID]) { self.elements.removeAll(where: { elements.contains($0.uuid) }) self.commit(reset: true) } 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.currentDrawingView.transform = transform self.currentDrawingView.frame = self.bounds self.drawingGesturePipeline?.transform = CGAffineTransformMakeScale(1.0 / scale, 1.0 / scale) self.metalView.transform = transform self.metalView.frame = self.bounds self.brushSizePreviewLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) } public var isEmpty: Bool { return self.elements.isEmpty } public var scale: CGFloat { return self.bounds.width / self.imageSize.width } public var isTracking: Bool { return self.uncommitedElement != nil } }