mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
964 lines
40 KiB
Swift
964 lines
40 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import ComponentFlow
|
|
import LegacyComponents
|
|
import AppBundle
|
|
import ImageBlur
|
|
|
|
protocol DrawingRenderLayer: CALayer {
|
|
|
|
}
|
|
|
|
protocol DrawingRenderView: UIView {
|
|
|
|
}
|
|
|
|
protocol DrawingElement: AnyObject {
|
|
var uuid: UUID { get }
|
|
var translation: CGPoint { get set }
|
|
var isValid: Bool { get }
|
|
|
|
func setupRenderView(screenSize: CGSize) -> DrawingRenderView?
|
|
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 currentDrawingViewContainer: UIImageView
|
|
private var currentDrawingRenderView: DrawingRenderView?
|
|
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.currentDrawingViewContainer = UIImageView()
|
|
self.currentDrawingViewContainer.frame = CGRect(origin: .zero, size: size)
|
|
self.currentDrawingViewContainer.contentScaleFactor = 1.0
|
|
self.currentDrawingViewContainer.backgroundColor = .clear
|
|
self.currentDrawingViewContainer.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.currentDrawingViewContainer)
|
|
//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 renderView = newElement.setupRenderView(screenSize: CGSize(width: 414.0, height: 414.0)) {
|
|
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(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()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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<UUID>()
|
|
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 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 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()
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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.currentDrawingViewContainer.transform = transform
|
|
self.currentDrawingViewContainer.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 && !self.hasOpaqueData
|
|
}
|
|
|
|
public var scale: CGFloat {
|
|
return self.bounds.width / self.imageSize.width
|
|
}
|
|
|
|
public var isTracking: Bool {
|
|
return self.uncommitedElement != nil
|
|
}
|
|
}
|
|
|
|
private class UndoSlice {
|
|
let uuid: UUID
|
|
|
|
// let data: Data
|
|
// let bounds: CGRect
|
|
|
|
let path: String
|
|
|
|
init(context: DrawingContext, bounds: CGRect) {
|
|
self.uuid = UUID()
|
|
|
|
self.path = NSTemporaryDirectory() + "/drawing_\(uuid.hashValue).slice"
|
|
}
|
|
|
|
deinit {
|
|
try? FileManager.default.removeItem(atPath: self.path)
|
|
}
|
|
}
|