mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
929 lines
36 KiB
Swift
929 lines
36 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import MediaEditor
|
|
|
|
private let activeWidthFactor: CGFloat = 0.7
|
|
|
|
final class PenTool: DrawingElement {
|
|
class RenderView: UIView, DrawingRenderView {
|
|
private weak var element: PenTool?
|
|
private var isEraser = false
|
|
|
|
private var accumulationImage: UIImage?
|
|
private var activeView: ActiveView?
|
|
|
|
private var start = 0
|
|
private var segmentsCount = 0
|
|
|
|
private var drawScale = CGSize(width: 1.0, height: 1.0)
|
|
|
|
func setup(size: CGSize, screenSize: CGSize, isEraser: Bool) {
|
|
self.isEraser = isEraser
|
|
|
|
self.backgroundColor = .clear
|
|
self.isOpaque = false
|
|
self.contentMode = .redraw
|
|
|
|
let scale = CGSize(width: 0.33, height: 0.33)
|
|
let viewSize = CGSize(width: size.width * scale.width, height: size.height * scale.height)
|
|
|
|
self.drawScale = CGSize(width: size.width / viewSize.width, height: size.height / viewSize.height)
|
|
|
|
self.bounds = CGRect(origin: .zero, size: viewSize)
|
|
self.transform = CGAffineTransform(scaleX: self.drawScale.width, y: self.drawScale.height)
|
|
self.frame = CGRect(origin: .zero, size: size)
|
|
|
|
self.drawScale.height = self.drawScale.width
|
|
|
|
let activeView = ActiveView(frame: CGRect(origin: .zero, size: self.bounds.size))
|
|
activeView.backgroundColor = .clear
|
|
activeView.contentMode = .redraw
|
|
activeView.isOpaque = false
|
|
activeView.parent = self
|
|
self.addSubview(activeView)
|
|
self.activeView = activeView
|
|
}
|
|
|
|
func animateArrowPaths(start: CGPoint, direction: CGFloat, length: CGFloat, lineWidth: CGFloat, completion: @escaping () -> Void) {
|
|
let scale = min(self.drawScale.width, self.drawScale.height)
|
|
|
|
let arrowStart = CGPoint(x: start.x / scale, y: start.y / scale)
|
|
let arrowLeftPath = UIBezierPath()
|
|
arrowLeftPath.move(to: arrowStart)
|
|
arrowLeftPath.addLine(to: arrowStart.pointAt(distance: length / scale, angle: direction - 0.45))
|
|
|
|
let arrowRightPath = UIBezierPath()
|
|
arrowRightPath.move(to: arrowStart)
|
|
arrowRightPath.addLine(to: arrowStart.pointAt(distance: length / scale, angle: direction + 0.45))
|
|
|
|
let leftArrowShape = CAShapeLayer()
|
|
leftArrowShape.path = arrowLeftPath.cgPath
|
|
leftArrowShape.lineWidth = lineWidth / scale
|
|
leftArrowShape.strokeColor = self.element?.color.toCGColor()
|
|
leftArrowShape.lineCap = .round
|
|
leftArrowShape.frame = self.bounds
|
|
self.layer.addSublayer(leftArrowShape)
|
|
|
|
let rightArrowShape = CAShapeLayer()
|
|
rightArrowShape.path = arrowRightPath.cgPath
|
|
rightArrowShape.lineWidth = lineWidth / scale
|
|
rightArrowShape.strokeColor = self.element?.color.toCGColor()
|
|
rightArrowShape.lineCap = .round
|
|
rightArrowShape.frame = self.bounds
|
|
self.layer.addSublayer(rightArrowShape)
|
|
|
|
leftArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
rightArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, completion: { [weak leftArrowShape, weak rightArrowShape] _ in
|
|
completion()
|
|
|
|
leftArrowShape?.removeFromSuperlayer()
|
|
rightArrowShape?.removeFromSuperlayer()
|
|
})
|
|
}
|
|
|
|
var onDryingUp: () -> Void = {}
|
|
|
|
var isDryingUp = false {
|
|
didSet {
|
|
if !self.isDryingUp {
|
|
self.onDryingUp()
|
|
}
|
|
}
|
|
}
|
|
var dryingLayersCount: Int = 0 {
|
|
didSet {
|
|
if self.dryingLayersCount > 0 {
|
|
self.isDryingUp = true
|
|
} else {
|
|
self.isDryingUp = false
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate var displaySize: CGSize?
|
|
fileprivate func draw(element: PenTool, rect: CGRect) {
|
|
self.element = element
|
|
|
|
self.alpha = element.color.alpha
|
|
|
|
guard !rect.isInfinite && !rect.isEmpty && !rect.isNull else {
|
|
return
|
|
}
|
|
|
|
var rect: CGRect? = rect
|
|
|
|
let limit = 512
|
|
let activeCount = self.segmentsCount - self.start
|
|
if activeCount > limit {
|
|
rect = nil
|
|
let newStart = self.start + limit
|
|
let displaySize = self.displaySize ?? CGSize(width: round(self.bounds.size.width), height: round(self.bounds.size.height))
|
|
let image = generateImage(displaySize, contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: .zero, size: size))
|
|
|
|
if let accumulationImage = self.accumulationImage, let cgImage = accumulationImage.cgImage {
|
|
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
|
|
}
|
|
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
|
|
context.scaleBy(x: 1.0 / self.drawScale.width, y: 1.0 / self.drawScale.height)
|
|
|
|
context.setBlendMode(.copy)
|
|
element.drawSegments(in: context, from: self.start, to: newStart)
|
|
}, opaque: false)
|
|
self.accumulationImage = image
|
|
self.layer.contents = image?.cgImage
|
|
|
|
self.start = newStart
|
|
}
|
|
|
|
if element.hasAnimations {
|
|
let count = CGFloat(element.segments.count - self.segmentsCount)
|
|
if count > 0 {
|
|
let dryingPath = CGMutablePath()
|
|
var abFactor: CGFloat = activeWidthFactor * 1.35
|
|
let delta: CGFloat = (1.0 - abFactor) / count
|
|
for i in self.segmentsCount ..< element.segments.count {
|
|
let segmentPath = element.pathForSegment(element.segments[i], abFactor: abFactor, cdFactor: abFactor + delta)
|
|
dryingPath.addPath(segmentPath)
|
|
abFactor += delta
|
|
}
|
|
self.setupDrying(path: dryingPath)
|
|
}
|
|
}
|
|
|
|
self.segmentsCount = element.segments.count
|
|
|
|
if let rect = rect {
|
|
self.activeView?.setNeedsDisplay(rect.insetBy(dx: -40.0, dy: -40.0).applying(CGAffineTransform(scaleX: 1.0 / self.drawScale.width, y: 1.0 / self.drawScale.height)))
|
|
} else {
|
|
self.activeView?.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
private let dryingFactor: CGFloat = 0.4
|
|
func setupDrying(path: CGPath) {
|
|
guard let element = self.element else {
|
|
return
|
|
}
|
|
|
|
let dryingLayer = CAShapeLayer()
|
|
dryingLayer.contentsScale = 1.0
|
|
dryingLayer.fillColor = element.renderColor.cgColor
|
|
dryingLayer.strokeColor = element.renderColor.cgColor
|
|
dryingLayer.lineWidth = element.renderLineWidth * self.dryingFactor
|
|
dryingLayer.path = path
|
|
dryingLayer.animate(from: dryingLayer.lineWidth as NSNumber, to: 0.0 as NSNumber, keyPath: "lineWidth", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.4, removeOnCompletion: false, completion: { [weak dryingLayer] _ in
|
|
dryingLayer?.removeFromSuperlayer()
|
|
self.dryingLayersCount -= 1
|
|
})
|
|
dryingLayer.transform = CATransform3DMakeScale(1.0 / self.drawScale.width, 1.0 / self.drawScale.height, 1.0)
|
|
dryingLayer.frame = self.bounds
|
|
self.layer.addSublayer(dryingLayer)
|
|
|
|
self.dryingLayersCount += 1
|
|
}
|
|
|
|
private var isActiveDrying = false
|
|
func setupActiveSegmentsDrying() {
|
|
guard let element = self.element else {
|
|
return
|
|
}
|
|
|
|
if element.hasAnimations {
|
|
let dryingPath = CGMutablePath()
|
|
for segment in element.activeSegments {
|
|
let segmentPath = element.pathForSegment(segment)
|
|
dryingPath.addPath(segmentPath)
|
|
}
|
|
self.setupDrying(path: dryingPath)
|
|
self.isActiveDrying = true
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
class ActiveView: UIView {
|
|
weak var parent: RenderView?
|
|
override func draw(_ rect: CGRect) {
|
|
guard let context = UIGraphicsGetCurrentContext(), let parent = self.parent, let element = parent.element else {
|
|
return
|
|
}
|
|
|
|
parent.displaySize = rect.size
|
|
context.scaleBy(x: 1.0 / parent.drawScale.width, y: 1.0 / parent.drawScale.height)
|
|
element.drawSegments(in: context, from: parent.start, to: parent.segmentsCount)
|
|
|
|
if element.hasAnimations {
|
|
element.drawActiveSegments(in: context, strokeWidth: !parent.isActiveDrying ? element.renderLineWidth * parent.dryingFactor : nil)
|
|
} else {
|
|
element.drawActiveSegments(in: context, strokeWidth: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let uuid: UUID
|
|
let drawingSize: CGSize
|
|
let color: DrawingColor
|
|
let renderLineWidth: CGFloat
|
|
let renderMinLineWidth: CGFloat
|
|
let renderColor: UIColor
|
|
|
|
let hasArrow: Bool
|
|
let renderArrowLength: CGFloat
|
|
var renderArrowLineWidth: CGFloat
|
|
|
|
let isEraser: Bool
|
|
let isBlur: Bool
|
|
|
|
var arrowStart: CGPoint?
|
|
var arrowDirection: CGFloat?
|
|
var arrowLeftPath: UIBezierPath?
|
|
var arrowRightPath: UIBezierPath?
|
|
|
|
var translation: CGPoint = .zero
|
|
|
|
var blurredImage: UIImage?
|
|
|
|
private weak var currentRenderView: DrawingRenderView?
|
|
|
|
private var points: [Point] = Array(repeating: Point(location: .zero, width: 0.0), count: 4)
|
|
private var pointPtr = 0
|
|
|
|
private var smoothPoints: [Point] = []
|
|
private var activeSmoothPoints: [Point] = []
|
|
|
|
private var segments: [Segment] = []
|
|
private var activeSegments: [Segment] = []
|
|
|
|
private var previousActiveRect: CGRect?
|
|
|
|
private var previousRenderLineWidth: CGFloat?
|
|
|
|
private var segmentPaths: [Int: CGPath] = [:]
|
|
|
|
private var useCubicBezier = true
|
|
|
|
private let animationsEnabled: Bool
|
|
|
|
var hasAnimations: Bool {
|
|
return self.animationsEnabled && !self.isEraser && !self.isBlur
|
|
}
|
|
|
|
var isValid: Bool {
|
|
if self.hasArrow {
|
|
return self.arrowStart != nil && self.arrowDirection != nil
|
|
} else {
|
|
return self.segments.count > 0
|
|
}
|
|
}
|
|
|
|
var bounds: CGRect {
|
|
let segmentsBounds = boundingRect(from: 0, to: self.segments.count).insetBy(dx: -20.0, dy: -20.0)
|
|
var combinedBounds = segmentsBounds
|
|
if self.hasArrow, let arrowLeftPath, let arrowRightPath {
|
|
combinedBounds = combinedBounds.union(arrowLeftPath.bounds.insetBy(dx: -renderArrowLineWidth, dy: -renderArrowLineWidth)).union(arrowRightPath.bounds.insetBy(dx: -renderArrowLineWidth, dy: -renderArrowLineWidth)).insetBy(dx: -20.0, dy: -20.0)
|
|
}
|
|
return normalizeDrawingRect(combinedBounds, drawingSize: self.drawingSize)
|
|
}
|
|
|
|
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, hasArrow: Bool, isEraser: Bool, isBlur: Bool, blurredImage: UIImage?, animationsEnabled: Bool) {
|
|
self.uuid = UUID()
|
|
self.drawingSize = drawingSize
|
|
self.color = isEraser || isBlur ? DrawingColor(rgb: 0x000000) : color
|
|
self.hasArrow = hasArrow
|
|
self.isEraser = isEraser
|
|
self.isBlur = isBlur
|
|
self.blurredImage = blurredImage
|
|
self.animationsEnabled = animationsEnabled
|
|
|
|
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
|
|
|
|
let minRenderArrowLength = max(10.0, max(drawingSize.width, drawingSize.height) * 0.02)
|
|
|
|
self.renderLineWidth = lineWidth
|
|
self.renderMinLineWidth = isEraser || isBlur ? lineWidth : minLineWidth + (lineWidth - minLineWidth) * 0.2
|
|
self.renderArrowLength = max(minRenderArrowLength, lineWidth * 3.0)
|
|
self.renderArrowLineWidth = max(minLineWidth * 1.8, lineWidth * 0.75)
|
|
|
|
self.renderColor = color.withUpdatedAlpha(1.0).toUIColor()
|
|
}
|
|
|
|
var isFinishingArrow = false
|
|
func finishArrow(_ completion: @escaping () -> Void) {
|
|
if let arrowStart, let arrowDirection {
|
|
self.isFinishingArrow = true
|
|
(self.currentRenderView as? RenderView)?.animateArrowPaths(start: arrowStart, direction: arrowDirection, length: self.renderArrowLength, lineWidth: self.renderArrowLineWidth, completion: { [weak self] in
|
|
self?.isFinishingArrow = false
|
|
completion()
|
|
})
|
|
} else {
|
|
completion()
|
|
}
|
|
}
|
|
|
|
func setupRenderView(screenSize: CGSize) -> DrawingRenderView? {
|
|
let view = RenderView()
|
|
view.setup(size: self.drawingSize, screenSize: screenSize, isEraser: self.isEraser)
|
|
self.currentRenderView = view
|
|
return view
|
|
}
|
|
|
|
func setupRenderLayer() -> DrawingRenderLayer? {
|
|
return nil
|
|
}
|
|
|
|
func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) {
|
|
let result = self.addPoint(point, state: state, zoomScale: zoomScale)
|
|
let resetActiveRect = result?.0 ?? false
|
|
let updatedRect = result?.1
|
|
var combinedRect = updatedRect
|
|
if let previousActiveRect = self.previousActiveRect {
|
|
combinedRect = updatedRect?.union(previousActiveRect) ?? previousActiveRect
|
|
}
|
|
if resetActiveRect {
|
|
self.previousActiveRect = updatedRect
|
|
} else {
|
|
self.previousActiveRect = combinedRect
|
|
}
|
|
|
|
if let currentRenderView = self.currentRenderView as? RenderView, let combinedRect {
|
|
currentRenderView.draw(element: self, rect: combinedRect)
|
|
}
|
|
|
|
if state == .ended {
|
|
if !self.activeSegments.isEmpty {
|
|
(self.currentRenderView as? RenderView)?.setupActiveSegmentsDrying()
|
|
|
|
self.segments.append(contentsOf: self.activeSegments)
|
|
self.smoothPoints.append(contentsOf: self.activeSmoothPoints)
|
|
}
|
|
|
|
if self.hasArrow {
|
|
var direction: CGFloat?
|
|
if self.smoothPoints.count > 4 {
|
|
let p2 = self.smoothPoints[self.smoothPoints.count - 1].location
|
|
for i in 1 ..< min(self.smoothPoints.count - 2, 200) {
|
|
let p1 = self.smoothPoints[self.smoothPoints.count - 1 - i].location
|
|
if p1.distance(to: p2) > self.renderArrowLength * 0.5 {
|
|
direction = p2.angle(to: p1)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
self.arrowStart = self.smoothPoints.last?.location
|
|
self.arrowDirection = direction
|
|
self.maybeSetupArrow()
|
|
} else if self.segments.isEmpty {
|
|
let radius = self.renderLineWidth / 2.0
|
|
self.segments.append(
|
|
Segment(
|
|
a: CGPoint(x: point.x - radius, y: point.y),
|
|
b: CGPoint(x: point.x + radius, y: point.y),
|
|
c: CGPoint(x: point.x - radius, y: point.y + 0.1),
|
|
d: CGPoint(x: point.x + radius, y: point.y + 0.1),
|
|
radius1: radius,
|
|
radius2: radius,
|
|
abCenter: CGPoint(x: point.x, y: point.y),
|
|
cdCenter: CGPoint(x: point.x, y: point.y + 0.1),
|
|
perpendicular: .zero,
|
|
rect: CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius * 2.0, height: radius * 2.0))
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func maybeSetupArrow() {
|
|
if let start = self.arrowStart, let direction = self.arrowDirection {
|
|
let arrowLeftPath = UIBezierPath()
|
|
arrowLeftPath.move(to: start)
|
|
arrowLeftPath.addLine(to: start.pointAt(distance: self.renderArrowLength, angle: direction - 0.45))
|
|
|
|
let arrowRightPath = UIBezierPath()
|
|
arrowRightPath.move(to: start)
|
|
arrowRightPath.addLine(to: start.pointAt(distance: self.renderArrowLength, angle: direction + 0.45))
|
|
|
|
self.arrowLeftPath = arrowLeftPath
|
|
self.arrowRightPath = arrowRightPath
|
|
self.renderArrowLineWidth = self.smoothPoints.last?.width ?? self.renderArrowLineWidth
|
|
}
|
|
}
|
|
|
|
func draw(in context: CGContext, size: CGSize) {
|
|
guard !self.segments.isEmpty else {
|
|
return
|
|
}
|
|
|
|
context.saveGState()
|
|
|
|
if self.isEraser {
|
|
context.setBlendMode(.clear)
|
|
} else if self.isBlur {
|
|
context.setBlendMode(.normal)
|
|
} else {
|
|
context.setAlpha(self.color.alpha)
|
|
context.setBlendMode(.copy)
|
|
}
|
|
|
|
context.translateBy(x: self.translation.x, y: self.translation.y)
|
|
|
|
context.setShouldAntialias(true)
|
|
|
|
if self.isBlur, let blurredImage = self.blurredImage {
|
|
let maskContext = DrawingContext(size: size, scale: 0.5, clear: true)
|
|
maskContext?.withFlippedContext { maskContext in
|
|
self.drawSegments(in: maskContext, from: 0, to: self.segments.count)
|
|
}
|
|
if let maskImage = maskContext?.generateImage()?.cgImage, let blurredImage = blurredImage.cgImage {
|
|
context.clip(to: CGRect(origin: .zero, size: size), mask: maskImage)
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
context.draw(blurredImage, in: CGRect(origin: .zero, size: size))
|
|
}
|
|
|
|
} else {
|
|
self.drawSegments(in: context, from: 0, to: self.segments.count)
|
|
}
|
|
|
|
if let arrowLeftPath, let arrowRightPath {
|
|
context.setStrokeColor(self.renderColor.cgColor)
|
|
context.setLineWidth(self.renderArrowLineWidth)
|
|
context.setLineCap(.round)
|
|
|
|
context.addPath(arrowLeftPath.cgPath)
|
|
context.strokePath()
|
|
|
|
context.addPath(arrowRightPath.cgPath)
|
|
context.strokePath()
|
|
}
|
|
|
|
context.restoreGState()
|
|
|
|
self.segmentPaths = [:]
|
|
}
|
|
|
|
private struct Segment: Codable {
|
|
let a: CGPoint
|
|
let b: CGPoint
|
|
let c: CGPoint
|
|
let d: CGPoint
|
|
let radius1: CGFloat
|
|
let radius2: CGFloat
|
|
let abCenter: CGPoint
|
|
let cdCenter: CGPoint
|
|
let perpendicular: CGPoint
|
|
let rect: CGRect
|
|
|
|
init(
|
|
a: CGPoint,
|
|
b: CGPoint,
|
|
c: CGPoint,
|
|
d: CGPoint,
|
|
radius1: CGFloat,
|
|
radius2: CGFloat,
|
|
abCenter: CGPoint,
|
|
cdCenter: CGPoint,
|
|
perpendicular: CGPoint,
|
|
rect: CGRect
|
|
) {
|
|
self.a = a
|
|
self.b = b
|
|
self.c = c
|
|
self.d = d
|
|
self.radius1 = radius1
|
|
self.radius2 = radius2
|
|
self.abCenter = abCenter
|
|
self.cdCenter = cdCenter
|
|
self.perpendicular = perpendicular
|
|
self.rect = rect
|
|
}
|
|
|
|
func withMultiplied(abFactor: CGFloat, cdFactor: CGFloat) -> Segment {
|
|
let a = CGPoint(
|
|
x: self.abCenter.x + self.perpendicular.x * self.radius1 * abFactor,
|
|
y: self.abCenter.y + self.perpendicular.y * self.radius1 * abFactor
|
|
)
|
|
let b = CGPoint(
|
|
x: self.abCenter.x - self.perpendicular.x * self.radius1 * abFactor,
|
|
y: self.abCenter.y - self.perpendicular.y * self.radius1 * abFactor
|
|
)
|
|
let c = CGPoint(
|
|
x: self.cdCenter.x + self.perpendicular.x * self.radius2 * cdFactor,
|
|
y: self.cdCenter.y + self.perpendicular.y * self.radius2 * cdFactor
|
|
)
|
|
let d = CGPoint(
|
|
x: self.cdCenter.x - self.perpendicular.x * self.radius2 * cdFactor,
|
|
y: self.cdCenter.y - self.perpendicular.y * self.radius2 * cdFactor
|
|
)
|
|
|
|
return Segment(
|
|
a: a,
|
|
b: b,
|
|
c: c,
|
|
d: d,
|
|
radius1: self.radius1 * abFactor,
|
|
radius2: self.radius2 * cdFactor,
|
|
abCenter: self.abCenter,
|
|
cdCenter: self.cdCenter,
|
|
perpendicular: self.perpendicular,
|
|
rect: self.rect
|
|
)
|
|
}
|
|
}
|
|
|
|
private struct Point {
|
|
let location: CGPoint
|
|
let width: CGFloat
|
|
|
|
init(
|
|
location: CGPoint,
|
|
width: CGFloat
|
|
) {
|
|
self.location = location
|
|
self.width = width
|
|
}
|
|
}
|
|
|
|
private var currentVelocity: CGFloat?
|
|
private func addPoint(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) -> (Bool, CGRect)? {
|
|
let filterDistance: CGFloat = 8.0 / zoomScale
|
|
|
|
var velocity = point.velocity
|
|
if velocity.isZero {
|
|
velocity = 1000.0
|
|
}
|
|
self.currentVelocity = velocity
|
|
|
|
var renderLineWidth = max(self.renderMinLineWidth, min(self.renderLineWidth - (velocity / 200.0), self.renderLineWidth))
|
|
if let previousRenderLineWidth = self.previousRenderLineWidth {
|
|
renderLineWidth = renderLineWidth * 0.3 + previousRenderLineWidth * 0.7
|
|
}
|
|
self.previousRenderLineWidth = renderLineWidth
|
|
|
|
var resetActiveRect = false
|
|
var finalizedRect: CGRect?
|
|
if self.pointPtr == 0 {
|
|
self.points[0] = Point(location: point.location, width: renderLineWidth)
|
|
self.pointPtr += 1
|
|
} else {
|
|
let previousPoint = self.points[self.pointPtr - 1].location
|
|
guard previousPoint.distance(to: point.location) > filterDistance else {
|
|
return nil
|
|
}
|
|
|
|
if self.pointPtr >= 4 {
|
|
self.points[3] = Point(
|
|
location: self.points[2].location.point(to: point.location, t: 0.5),
|
|
width: self.points[2].width
|
|
)
|
|
if var smoothPoints = self.currentSmoothPoints(3) {
|
|
if let previousSmoothPoint = self.smoothPoints.last {
|
|
smoothPoints.insert(previousSmoothPoint, at: 0)
|
|
}
|
|
let (segments, rect) = self.segments(fromSmoothPoints: smoothPoints)
|
|
self.smoothPoints.append(contentsOf: smoothPoints)
|
|
self.segments.append(contentsOf: segments)
|
|
finalizedRect = rect
|
|
|
|
self.activeSmoothPoints.removeAll()
|
|
self.activeSegments.removeAll()
|
|
|
|
resetActiveRect = true
|
|
}
|
|
|
|
self.points[0] = self.points[3]
|
|
self.pointPtr = 1
|
|
}
|
|
|
|
let point = Point(location: point.location, width: renderLineWidth)
|
|
self.points[self.pointPtr] = point
|
|
self.pointPtr += 1
|
|
}
|
|
|
|
guard let smoothPoints = self.currentSmoothPoints(self.pointPtr - 1) else {
|
|
if let finalizedRect {
|
|
return (resetActiveRect, finalizedRect)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
let (segments, rect) = self.segments(fromSmoothPoints: smoothPoints)
|
|
self.activeSmoothPoints = smoothPoints
|
|
self.activeSegments = segments
|
|
|
|
var combinedRect: CGRect?
|
|
if let finalizedRect, let rect {
|
|
combinedRect = finalizedRect.union(rect)
|
|
} else {
|
|
combinedRect = rect ?? finalizedRect
|
|
}
|
|
if let combinedRect {
|
|
return (resetActiveRect, combinedRect)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func currentSmoothPoints(_ ctr: Int) -> [Point]? {
|
|
switch ctr {
|
|
case 0:
|
|
return nil//return [self.points[0]]
|
|
case 1:
|
|
return nil//return self.smoothPoints(.line(self.points[0], self.points[1]))
|
|
case 2:
|
|
return self.smoothPoints(.quad(self.points[0], self.points[1], self.points[2]))
|
|
case 3:
|
|
return self.smoothPoints(.cubic(self.points[0], self.points[1], self.points[2], self.points[3]))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private enum SmootherInput {
|
|
case line(Point, Point)
|
|
case quad(Point, Point, Point)
|
|
case cubic(Point, Point, Point, Point)
|
|
|
|
var start: Point {
|
|
switch self {
|
|
case let .line(start, _), let .quad(start, _, _), let .cubic(start, _, _, _):
|
|
return start
|
|
}
|
|
}
|
|
|
|
var end: Point {
|
|
switch self {
|
|
case let .line(_, end), let .quad(_, _, end), let .cubic(_, _, _, end):
|
|
return end
|
|
}
|
|
}
|
|
|
|
var distance: CGFloat {
|
|
return self.start.location.distance(to: self.end.location)
|
|
}
|
|
}
|
|
private func smoothPoints(_ input: SmootherInput) -> [Point] {
|
|
let segmentDistance: CGFloat = 6.0
|
|
let distance = input.distance
|
|
let numberOfSegments = min(48, max(floor(distance / segmentDistance), 24))
|
|
|
|
let step = 1.0 / numberOfSegments
|
|
|
|
var smoothPoints: [Point] = []
|
|
for t in stride(from: 0, to: 1, by: step) {
|
|
let point: Point
|
|
switch input {
|
|
case let .line(start, end):
|
|
point = Point(
|
|
location: start.location.linearBezierPoint(to: end.location, t: t),
|
|
width: CGPoint(x: start.width, y: 0.0).linearBezierPoint(to: CGPoint(x: end.width, y: 0.0), t: t).x
|
|
)
|
|
case let .quad(start, control, end):
|
|
let location = start.location.quadBezierPoint(to: end.location, controlPoint: control.location, t: t)
|
|
let width = CGPoint(x: start.width, y: 0.0).quadBezierPoint(to: CGPoint(x: end.width, y: 0.0), controlPoint: CGPoint(x: (start.width + end.width) / 2.0, y: 0.0), t: t).x
|
|
point = Point(
|
|
location: location,
|
|
width: width
|
|
)
|
|
case let .cubic(start, control1, control2, end):
|
|
let location = start.location.cubicBezierPoint(to: end.location, controlPoint1: control1.location, controlPoint2: control2.location, t: t)
|
|
let width = CGPoint(x: start.width, y: 0.0).cubicBezierPoint(to: CGPoint(x: end.width, y: 0.0), controlPoint1: CGPoint(x: (start.width + control1.width) / 2.0, y: 0.0), controlPoint2: CGPoint(x: (control2.width + end.width) / 2.0, y: 0.0), t: t).x
|
|
point = Point(
|
|
location: location,
|
|
width: width
|
|
)
|
|
}
|
|
smoothPoints.append(point)
|
|
}
|
|
smoothPoints.append(input.end)
|
|
return smoothPoints
|
|
}
|
|
|
|
fileprivate func boundingRect(from: Int, to: Int) -> CGRect {
|
|
var minX: CGFloat = .greatestFiniteMagnitude
|
|
var minY: CGFloat = .greatestFiniteMagnitude
|
|
var maxX: CGFloat = 0.0
|
|
var maxY: CGFloat = 0.0
|
|
|
|
for i in from ..< to {
|
|
let segment = self.segments[i]
|
|
|
|
if segment.rect.minX < minX {
|
|
minX = segment.rect.minX
|
|
}
|
|
if segment.rect.maxX > maxX {
|
|
maxX = segment.rect.maxX
|
|
}
|
|
if segment.rect.minY < minY {
|
|
minY = segment.rect.minY
|
|
}
|
|
if segment.rect.maxY > maxY {
|
|
maxY = segment.rect.maxY
|
|
}
|
|
}
|
|
|
|
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
|
|
}
|
|
|
|
private func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect?) {
|
|
var segments: [Segment] = []
|
|
var updateRect = CGRect.null
|
|
for i in 1 ..< smoothPoints.count {
|
|
let previousPoint = smoothPoints[i - 1].location
|
|
let previousWidth = smoothPoints[i - 1].width
|
|
let currentPoint = smoothPoints[i].location
|
|
let currentWidth = smoothPoints[i].width
|
|
let direction = CGPoint(
|
|
x: currentPoint.x - previousPoint.x,
|
|
y: currentPoint.y - previousPoint.y
|
|
)
|
|
|
|
guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else {
|
|
continue
|
|
}
|
|
|
|
var perpendicular = CGPoint(x: -direction.y, y: direction.x)
|
|
let length = perpendicular.length
|
|
if length > 0.0 {
|
|
perpendicular = CGPoint(
|
|
x: perpendicular.x / length,
|
|
y: perpendicular.y / length
|
|
)
|
|
}
|
|
|
|
let a = CGPoint(
|
|
x: previousPoint.x + perpendicular.x * previousWidth / 2.0,
|
|
y: previousPoint.y + perpendicular.y * previousWidth / 2.0
|
|
)
|
|
let b = CGPoint(
|
|
x: previousPoint.x - perpendicular.x * previousWidth / 2.0,
|
|
y: previousPoint.y - perpendicular.y * previousWidth / 2.0
|
|
)
|
|
let c = CGPoint(
|
|
x: currentPoint.x + perpendicular.x * currentWidth / 2.0,
|
|
y: currentPoint.y + perpendicular.y * currentWidth / 2.0
|
|
)
|
|
let d = CGPoint(
|
|
x: currentPoint.x - perpendicular.x * currentWidth / 2.0,
|
|
y: currentPoint.y - perpendicular.y * currentWidth / 2.0
|
|
)
|
|
|
|
let abCenter = CGPoint(
|
|
x: (a.x + b.x) / 2.0,
|
|
y: (a.y + b.y) / 2.0
|
|
)
|
|
let abRadius = CGPoint(
|
|
x: abCenter.x - b.x,
|
|
y: abCenter.y - b.y
|
|
)
|
|
let ab = CGPoint(
|
|
x: abCenter.x - abRadius.y,
|
|
y: abCenter.y + abRadius.x
|
|
)
|
|
|
|
let cdCenter = CGPoint(
|
|
x: (c.x + d.x) / 2.0,
|
|
y: (c.y + d.y) / 2.0
|
|
)
|
|
let cdRadius = CGPoint(
|
|
x: cdCenter.x - c.x,
|
|
y: cdCenter.y - c.y
|
|
)
|
|
let cd = CGPoint(
|
|
x: cdCenter.x - cdRadius.y,
|
|
y: cdCenter.y + cdRadius.x
|
|
)
|
|
|
|
let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x)
|
|
let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y)
|
|
let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x)
|
|
let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y)
|
|
|
|
let segmentRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
|
|
updateRect = updateRect.union(segmentRect)
|
|
|
|
let segment = Segment(
|
|
a: a,
|
|
b: b,
|
|
c: c,
|
|
d: d,
|
|
radius1: previousWidth / 2.0,
|
|
radius2: currentWidth / 2.0,
|
|
abCenter: abCenter,
|
|
cdCenter: cdCenter,
|
|
perpendicular: perpendicular,
|
|
rect: segmentRect
|
|
)
|
|
segments.append(segment)
|
|
}
|
|
return (segments, !updateRect.isNull ? updateRect : nil)
|
|
}
|
|
|
|
private func pathForSegment(_ segment: Segment, abFactor: CGFloat = 1.0, cdFactor: CGFloat = 1.0) -> CGPath {
|
|
var segment = segment
|
|
if abFactor != 1.0 || cdFactor != 1.0 {
|
|
segment = segment.withMultiplied(abFactor: abFactor, cdFactor: cdFactor)
|
|
}
|
|
|
|
let path = CGMutablePath()
|
|
path.move(to: segment.b)
|
|
|
|
let abStartAngle = atan2(
|
|
segment.b.y - segment.a.y,
|
|
segment.b.x - segment.a.x
|
|
)
|
|
path.addArc(
|
|
center: CGPoint(
|
|
x: (segment.a.x + segment.b.x) / 2,
|
|
y: (segment.a.y + segment.b.y) / 2
|
|
),
|
|
radius: segment.radius1,
|
|
startAngle: abStartAngle,
|
|
endAngle: abStartAngle + .pi,
|
|
clockwise: true
|
|
)
|
|
path.addLine(to: segment.c)
|
|
|
|
let cdStartAngle = atan2(
|
|
segment.c.y - segment.d.y,
|
|
segment.c.x - segment.d.x
|
|
)
|
|
path.addArc(
|
|
center: CGPoint(
|
|
x: (segment.c.x + segment.d.x) / 2,
|
|
y: (segment.c.y + segment.d.y) / 2
|
|
),
|
|
radius: segment.radius2,
|
|
startAngle: cdStartAngle,
|
|
endAngle: cdStartAngle + .pi,
|
|
clockwise: true
|
|
)
|
|
path.closeSubpath()
|
|
return path
|
|
}
|
|
|
|
func cachedPathForSegmentIndex(_ i: Int) -> CGPath {
|
|
var segmentPath: CGPath
|
|
if let current = self.segmentPaths[i] {
|
|
segmentPath = current
|
|
} else {
|
|
let segment = self.segments[i]
|
|
let path = self.pathForSegment(segment)
|
|
self.segmentPaths[i] = path
|
|
segmentPath = path
|
|
}
|
|
return segmentPath
|
|
}
|
|
|
|
private func drawSegments(in context: CGContext, from: Int, to: Int) {
|
|
context.setFillColor(self.renderColor.cgColor)
|
|
|
|
for i in from ..< to {
|
|
let segment = self.segments[i]
|
|
|
|
var segmentPath: CGPath
|
|
if let current = self.segmentPaths[i] {
|
|
segmentPath = current
|
|
} else {
|
|
let path = self.pathForSegment(segment)
|
|
self.segmentPaths[i] = path
|
|
segmentPath = path
|
|
}
|
|
|
|
context.addPath(segmentPath)
|
|
context.fillPath()
|
|
}
|
|
}
|
|
|
|
private func drawActiveSegments(in context: CGContext, strokeWidth: CGFloat?) {
|
|
context.setFillColor(self.renderColor.cgColor)
|
|
if let strokeWidth {
|
|
context.setStrokeColor(self.renderColor.cgColor)
|
|
context.setLineWidth(strokeWidth)
|
|
}
|
|
|
|
var abFactor: CGFloat = activeWidthFactor
|
|
let delta: CGFloat = (1.0 - activeWidthFactor) / CGFloat(self.activeSegments.count + 1)
|
|
for segment in self.activeSegments {
|
|
let path = self.pathForSegment(segment)
|
|
context.addPath(path)
|
|
if let _ = strokeWidth {
|
|
context.drawPath(using: .fillStroke)
|
|
} else {
|
|
context.fillPath()
|
|
}
|
|
abFactor += delta
|
|
}
|
|
}
|
|
}
|