import Foundation import UIKit import Display import MediaEditor final class NeonTool: DrawingElement { class RenderView: UIView, DrawingRenderView { private weak var element: NeonTool? private var drawScale = CGSize(width: 1.0, height: 1.0) let shadowLayer = SimpleShapeLayer() let borderLayer = SimpleShapeLayer() let fillLayer = SimpleShapeLayer() func setup(element: NeonTool, size: CGSize, screenSize: CGSize) { self.element = element self.backgroundColor = .clear self.isOpaque = false self.contentScaleFactor = 1.0 let shadowRadius = element.renderShadowRadius let strokeWidth = element.renderStrokeWidth var shadowColor = element.color.toUIColor() var fillColor: UIColor = .white if shadowColor.lightness < 0.01 { fillColor = shadowColor shadowColor = UIColor(rgb: 0x440881) } let bounds = CGRect(origin: .zero, size: size) self.frame = bounds self.shadowLayer.frame = bounds self.shadowLayer.contentsScale = 1.0 self.shadowLayer.backgroundColor = UIColor.clear.cgColor self.shadowLayer.lineWidth = strokeWidth * 0.5 self.shadowLayer.lineCap = .round self.shadowLayer.lineJoin = .round self.shadowLayer.fillColor = fillColor.cgColor self.shadowLayer.strokeColor = fillColor.cgColor self.shadowLayer.shadowColor = shadowColor.cgColor self.shadowLayer.shadowRadius = shadowRadius self.shadowLayer.shadowOpacity = 1.0 self.shadowLayer.shadowOffset = .zero self.borderLayer.frame = bounds self.borderLayer.contentsScale = 1.0 self.borderLayer.lineWidth = strokeWidth self.borderLayer.lineCap = .round self.borderLayer.lineJoin = .round self.borderLayer.fillColor = UIColor.clear.cgColor self.borderLayer.strokeColor = fillColor.mixedWith(shadowColor, alpha: 0.25).cgColor self.fillLayer.frame = bounds self.fillLayer.contentsScale = 1.0 self.fillLayer.fillColor = fillColor.cgColor self.layer.addSublayer(self.shadowLayer) self.layer.addSublayer(self.borderLayer) self.layer.addSublayer(self.fillLayer) } fileprivate func updatePath(_ path: CGPath, shadowPath: CGPath) { self.shadowLayer.path = path self.shadowLayer.shadowPath = shadowPath self.borderLayer.path = path self.fillLayer.path = path } } let uuid: UUID let drawingSize: CGSize let color: DrawingColor let renderStrokeWidth: CGFloat let renderShadowRadius: CGFloat let renderLineWidth: CGFloat let renderColor: UIColor private var pathStarted = false private let path = UIBezierPath() private var activePath: UIBezierPath? private var addedPaths = 0 fileprivate var renderPath: CGPath? fileprivate var shadowRenderPath: CGPath? var translation: CGPoint = .zero private weak var currentRenderView: DrawingRenderView? var isValid: Bool { return self.renderPath != nil } var bounds: CGRect { if let renderPath = self.shadowRenderPath { return normalizeDrawingRect(renderPath.boundingBoxOfPath.insetBy(dx: -self.renderShadowRadius - 30.0, dy: -self.renderShadowRadius - 30.0), drawingSize: self.drawingSize) } else { return .zero } } required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) { self.uuid = UUID() self.drawingSize = drawingSize self.color = color let strokeWidth = min(drawingSize.width, drawingSize.height) * 0.008 let shadowRadius = min(drawingSize.width, drawingSize.height) * 0.02 let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.002) let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.07) let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth self.renderStrokeWidth = strokeWidth self.renderShadowRadius = shadowRadius self.renderLineWidth = lineWidth self.renderColor = color.withUpdatedAlpha(1.0).toUIColor() } func setupRenderView(screenSize: CGSize) -> DrawingRenderView? { let view = RenderView() view.setup(element: self, size: self.drawingSize, screenSize: screenSize) self.currentRenderView = view return view } func setupRenderLayer() -> DrawingRenderLayer? { return nil } func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) { guard self.addPoint(point, state: state, zoomScale: zoomScale) || state == .ended else { return } if let currentRenderView = self.currentRenderView as? RenderView { let path = self.path.cgPath.mutableCopy() if let activePath { path?.addPath(activePath.cgPath) } if let renderPath = path?.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0), let shadowRenderPath = path?.copy(strokingWithWidth: self.renderLineWidth * 2.0, lineCap: .round, lineJoin: .round, miterLimit: 0.0) { self.renderPath = renderPath self.shadowRenderPath = shadowRenderPath currentRenderView.updatePath(renderPath, shadowPath: shadowRenderPath) } } if state == .ended { if let activePath = self.activePath { self.path.append(activePath) self.renderPath = self.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) } else if self.addedPaths == 0, let point = self.points.first { self.renderPath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: point.x - self.renderLineWidth / 2.0, y: point.y - self.renderLineWidth / 2.0), size: CGSize(width: self.renderLineWidth, height: self.renderLineWidth)), transform: nil) } } } func draw(in context: CGContext, size: CGSize) { guard let path = self.renderPath, let shadowPath = self.shadowRenderPath else { return } context.saveGState() context.translateBy(x: self.translation.x, y: self.translation.y) context.setShouldAntialias(true) context.setBlendMode(.normal) var shadowColor = self.color.toUIColor() var fillColor: UIColor = .white if shadowColor.lightness < 0.01 { fillColor = shadowColor shadowColor = UIColor(rgb: 0x440881) } let shadowOffset = CGSize(width: 3000.0, height: 3000.0) context.translateBy(x: -shadowOffset.width, y: -shadowOffset.height) context.addPath(shadowPath) context.setLineCap(.round) context.setFillColor(fillColor.cgColor) context.setStrokeColor(fillColor.cgColor) context.setLineWidth(self.renderStrokeWidth * 0.5) context.setShadow(offset: shadowOffset, blur: self.renderShadowRadius * 1.9, color: shadowColor.withAlphaComponent(0.87).cgColor) context.drawPath(using: .fillStroke) context.translateBy(x: shadowOffset.width, y: shadowOffset.height) context.addPath(path) context.setShadow(offset: .zero, blur: 0.0, color: UIColor.clear.cgColor) context.setLineWidth(self.renderStrokeWidth) context.setStrokeColor(fillColor.mixedWith(shadowColor, alpha: 0.25).cgColor) context.strokePath() context.addPath(path) context.setFillColor(fillColor.cgColor) context.fillPath() context.restoreGState() } private var points: [CGPoint] = Array(repeating: .zero, count: 4) private var pointPtr = 0 private func addPoint(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) -> Bool { let filterDistance: CGFloat = 10.0 / zoomScale if self.pointPtr == 0 { self.points[0] = point.location self.pointPtr += 1 } else { let previousPoint = self.points[self.pointPtr - 1] guard previousPoint.distance(to: point.location) > filterDistance else { return false } if self.pointPtr >= 4 { self.points[3] = self.points[2].point(to: point.location, t: 0.5) if let bezierPath = self.currentBezierPath(3) { self.path.append(bezierPath) self.addedPaths += 1 self.activePath = nil } self.points[0] = self.points[3] self.pointPtr = 1 } self.points[self.pointPtr] = point.location self.pointPtr += 1 } guard let bezierPath = self.currentBezierPath(self.pointPtr - 1) else { return false } self.activePath = bezierPath return true } private func currentBezierPath(_ ctr: Int) -> UIBezierPath? { switch ctr { case 0: return nil case 1: let path = UIBezierPath() path.move(to: self.points[0]) path.addLine(to: self.points[1]) return path case 2: let path = UIBezierPath() path.move(to: self.points[0]) path.addQuadCurve(to: self.points[2], controlPoint: self.points[1]) return path case 3: let path = UIBezierPath() path.move(to: self.points[0]) path.addCurve(to: self.points[3], controlPoint1: self.points[1], controlPoint2: self.points[2]) return path default: return nil } } }