mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Various improvements
This commit is contained in:
parent
1d0f77fc3e
commit
0b37adc8b2
@ -490,8 +490,6 @@ class DrawingGesturePipeline {
|
||||
return gf
|
||||
}
|
||||
|
||||
/// Calculates the Gram Polynomial ( s = 0 ), or its s'th
|
||||
/// derivative evaluated at i, order k, over 2m + 1 points
|
||||
private static func gramPoly(_ index: Int, _ window: Int, _ order: Int, _ derivative: Int) -> CGFloat {
|
||||
var gp_val: CGFloat
|
||||
|
||||
@ -513,8 +511,6 @@ class DrawingGesturePipeline {
|
||||
return gp_val
|
||||
}
|
||||
|
||||
/// calculates the weight of the i'th data point for the t'th Least-square
|
||||
/// point of the s'th derivative, over 2m + 1 points, order n
|
||||
private static func calcWeight(_ index: Int, _ windowLoc: Int, _ windowSize: Int, _ order: Int, _ derivative: Int) -> CGFloat {
|
||||
var sum: CGFloat = 0.0
|
||||
|
||||
@ -1002,10 +998,10 @@ struct Polyline {
|
||||
}
|
||||
}
|
||||
|
||||
public internal(set) var isComplete: Bool
|
||||
public let touchIdentifier: String
|
||||
public var points: [Point]
|
||||
public var bounds: CGRect {
|
||||
var isComplete: Bool
|
||||
let touchIdentifier: String
|
||||
var points: [Point]
|
||||
var bounds: CGRect {
|
||||
return self.points.reduce(.null) { partialResult, point -> CGRect in
|
||||
return CGRect(x: min(partialResult.origin.x, point.x),
|
||||
y: min(partialResult.origin.y, point.y),
|
||||
@ -1144,7 +1140,7 @@ private class BezierBuilder {
|
||||
private class Smoother {
|
||||
let smoothFactor: CGFloat
|
||||
|
||||
init(smoothFactor: CGFloat = 0.7) {
|
||||
init(smoothFactor: CGFloat = 1.0) {
|
||||
self.smoothFactor = smoothFactor
|
||||
}
|
||||
|
||||
|
@ -180,8 +180,8 @@ struct DrawingState: Equatable {
|
||||
return DrawingState(
|
||||
selectedTool: .pen,
|
||||
tools: [
|
||||
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff453a), size: 0.2)),
|
||||
.arrow(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff8a00), size: 0.2)),
|
||||
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff453a), size: 0.23)),
|
||||
.arrow(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff8a00), size: 0.23)),
|
||||
.marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xffd60a), size: 0.75)),
|
||||
.neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34c759), size: 0.4)),
|
||||
.eraser(DrawingToolState.EraserState(size: 0.5)),
|
||||
@ -374,7 +374,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
|
||||
self.currentMode = .drawing
|
||||
self.drawingState = .initial
|
||||
self.drawingViewState = DrawingView.NavigationState(canUndo: false, canRedo: false, canClear: false, canZoomOut: false)
|
||||
self.drawingViewState = DrawingView.NavigationState(canUndo: false, canRedo: false, canClear: false, canZoomOut: false, isDrawing: false)
|
||||
self.currentColor = self.drawingState.tools.first?.color ?? DrawingColor(rgb: 0xffffff)
|
||||
|
||||
self.updateToolState.invoke(self.drawingState.currentToolState)
|
||||
@ -692,6 +692,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
|
||||
font: DrawingTextFont(font: textEntity.font),
|
||||
isEmojiKeyboard: false,
|
||||
tag: textSettingsTag,
|
||||
toggleStyle: { [weak state, weak textEntity] in
|
||||
guard let textEntity = textEntity else {
|
||||
return
|
||||
@ -1064,6 +1065,9 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
if let sizeValue {
|
||||
state.lastSize = sizeValue
|
||||
}
|
||||
if state.drawingViewState.isDrawing {
|
||||
sizeSliderVisible = false
|
||||
}
|
||||
|
||||
let topInset = environment.safeInsets.top + 31.0
|
||||
let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : 145.0
|
||||
@ -1730,8 +1734,17 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
entitiesView.add(entity)
|
||||
entitiesView.selectEntity(entity)
|
||||
|
||||
if let entityView = entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView {
|
||||
entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView)
|
||||
if let entityView = entitiesView.getView(for: entity.uuid) {
|
||||
if let textEntityView = entityView as? DrawingTextEntityView {
|
||||
textEntityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView)
|
||||
} else {
|
||||
entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
entityView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
|
||||
if let selectionView = entityView.selectionView {
|
||||
selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1943,7 +1956,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
if let view = self.componentHost.findTaggedView(tag: tag) {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: delay)
|
||||
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: delay)
|
||||
delay += 0.025
|
||||
delay += 0.02
|
||||
}
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
|
||||
@ -2122,6 +2135,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
|
||||
font: DrawingTextFont(font: textEntity.font),
|
||||
isEmojiKeyboard: entityView.textView.inputView != nil,
|
||||
tag: nil,
|
||||
presentColorPicker: { [weak self] in
|
||||
guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else {
|
||||
return
|
||||
|
@ -198,6 +198,9 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
||||
self.renderImage = UIImage(data: renderImageData)
|
||||
}
|
||||
if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) {
|
||||
self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity }
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -226,6 +229,10 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
if let renderImage, let data = renderImage.pngData() {
|
||||
try container.encode(data, forKey: .renderImage)
|
||||
}
|
||||
if let renderSubEntities = self.renderSubEntities {
|
||||
let codableEntities: [CodableDrawingEntity] = renderSubEntities.map { .sticker($0) }
|
||||
try container.encode(codableEntities, forKey: .renderSubEntities)
|
||||
}
|
||||
}
|
||||
|
||||
public func duplicate() -> DrawingEntity {
|
||||
@ -247,6 +254,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
|
||||
public func prepareForRender() {
|
||||
self.renderImage = (self.currentEntityView as? DrawingTextEntityView)?.getRenderImage()
|
||||
self.renderSubEntities = (self.currentEntityView as? DrawingTextEntityView)?.getRenderSubEntities()
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,8 +291,6 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
self.textView.delegate = self
|
||||
self.addSubview(self.textView)
|
||||
|
||||
self.update(animated: false)
|
||||
|
||||
self.emojiViewProvider = { [weak self] emoji in
|
||||
guard let strongSelf = self else {
|
||||
return UIView()
|
||||
@ -293,6 +299,8 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
let pointSize: CGFloat = 128.0
|
||||
return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: strongSelf.context.animationCache, renderer: strongSelf.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
|
||||
}
|
||||
|
||||
self.update(animated: false)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -312,11 +320,13 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
self.endEditing()
|
||||
}
|
||||
|
||||
private var emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
func updateEntities() {
|
||||
self.textView.drawingLayoutManager.ensureLayout(for: self.textView.textContainer)
|
||||
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
|
||||
var shouldRepeat = false
|
||||
if let attributedText = self.textView.attributedText {
|
||||
let beginning = self.textView.beginningOfDocument
|
||||
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
|
||||
@ -324,6 +334,9 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
if let start = self.textView.position(from: beginning, offset: range.location), let end = self.textView.position(from: start, offset: range.length), let textRange = self.textView.textRange(from: start, to: end) {
|
||||
let rect = self.textView.firstRect(for: textRange)
|
||||
customEmojiRects.append((rect, value))
|
||||
if rect.origin.x.isInfinite {
|
||||
shouldRepeat = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -342,7 +355,8 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
}
|
||||
|
||||
if !customEmojiRects.isEmpty {
|
||||
self.emojiRects = customEmojiRects
|
||||
if !customEmojiRects.isEmpty && !shouldRepeat {
|
||||
let customEmojiContainerView: CustomEmojiContainerView
|
||||
if let current = self.customEmojiContainerView {
|
||||
customEmojiContainerView = current
|
||||
@ -354,15 +368,22 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
return emojiViewProvider(emoji)
|
||||
})
|
||||
customEmojiContainerView.isUserInteractionEnabled = false
|
||||
self.textView.addSubview(customEmojiContainerView)
|
||||
customEmojiContainerView.center = customEmojiContainerView.center.offsetBy(dx: 0.0, dy: 10.0)
|
||||
self.addSubview(customEmojiContainerView)
|
||||
self.customEmojiContainerView = customEmojiContainerView
|
||||
}
|
||||
|
||||
customEmojiContainerView.update(fontSize: self.displayFontSize, textColor: textColor, emojiRects: customEmojiRects)
|
||||
customEmojiContainerView.update(fontSize: self.displayFontSize * 0.8, textColor: textColor, emojiRects: customEmojiRects)
|
||||
} else if let customEmojiContainerView = self.customEmojiContainerView {
|
||||
customEmojiContainerView.removeFromSuperview()
|
||||
self.customEmojiContainerView = nil
|
||||
}
|
||||
|
||||
if shouldRepeat {
|
||||
Queue.mainQueue().after(0.01) {
|
||||
self.updateEntities()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func beginEditing(accessoryView: UIView?) {
|
||||
@ -526,9 +547,6 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
super.layoutSubviews()
|
||||
|
||||
var rect = self.bounds
|
||||
// CGFloat correction = _textView.font.pointSize * _font.sizeCorrection;
|
||||
// rect.origin.y += correction;
|
||||
// rect.size.height -= correction;
|
||||
rect = rect.offsetBy(dx: 0.0, dy: 10.0) // CGRectOffset(rect, 0.0f, 10.0f);
|
||||
self.textView.frame = rect
|
||||
}
|
||||
@ -636,7 +654,6 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
self.updateEntities()
|
||||
}
|
||||
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
@ -671,11 +688,35 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
func getRenderImage() -> UIImage? {
|
||||
let rect = self.bounds
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0)
|
||||
self.drawHierarchy(in: rect, afterScreenUpdates: false)
|
||||
self.textView.drawHierarchy(in: rect.offsetBy(dx: 0.0, dy: 10.0), afterScreenUpdates: true)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image
|
||||
}
|
||||
|
||||
func getRenderSubEntities() -> [DrawingStickerEntity] {
|
||||
let textSize = self.textView.bounds.size
|
||||
let textPosition = self.textEntity.position
|
||||
|
||||
let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.8 / 17.0)
|
||||
|
||||
var entities: [DrawingStickerEntity] = []
|
||||
for (emojiRect, emojiAttribute) in self.emojiRects {
|
||||
guard let file = emojiAttribute.file else {
|
||||
continue
|
||||
}
|
||||
let emojiTextPosition = emojiRect.offsetBy(dx: 0.0, dy: 10.0).center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0)
|
||||
|
||||
let entity = DrawingStickerEntity(file: file)
|
||||
entity.referenceDrawingSize = CGSize(width: itemSize * 2.5, height: itemSize * 2.5)
|
||||
entity.scale = self.textEntity.scale
|
||||
entity.position = textPosition.offsetBy(dx: emojiTextPosition.x, dy: emojiTextPosition.y)
|
||||
entity.rotation = self.textEntity.rotation
|
||||
|
||||
entities.append(entity)
|
||||
}
|
||||
return entities
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate {
|
||||
|
@ -267,27 +267,6 @@ final class NeonTool: DrawingElement {
|
||||
|
||||
self.path = bezierPath
|
||||
|
||||
// if self.arrow && polyline.isComplete, polyline.points.count > 2 {
|
||||
// let lastPoint = lastPosition
|
||||
// var secondPoint = polyline.points[polyline.points.count - 2]
|
||||
// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
|
||||
// secondPoint = polyline.points[polyline.points.count - 3]
|
||||
// }
|
||||
// let angle = lastPoint.angle(to: secondPoint.location)
|
||||
// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
|
||||
// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
|
||||
//
|
||||
// let arrowPath = UIBezierPath()
|
||||
// arrowPath.move(to: point2)
|
||||
// arrowPath.addLine(to: lastPoint)
|
||||
// arrowPath.addLine(to: point1)
|
||||
// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
//
|
||||
// combinedPath.usesEvenOddFillRule = false
|
||||
// combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
|
||||
// }
|
||||
|
||||
|
||||
let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
self.renderPath = cgPath
|
||||
|
||||
|
@ -49,6 +49,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
let canRedo: Bool
|
||||
let canClear: Bool
|
||||
let canZoomOut: Bool
|
||||
let isDrawing: Bool
|
||||
}
|
||||
|
||||
enum Action {
|
||||
@ -107,6 +108,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
private var previousStrokePoint: CGPoint?
|
||||
private var strokeRecognitionTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var isDrawing = false
|
||||
|
||||
private func loadTemplates() {
|
||||
func load(_ name: String) {
|
||||
if let url = getAppBundle().url(forResource: name, withExtension: "json"),
|
||||
@ -249,6 +252,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
} else {
|
||||
switch state {
|
||||
case .began:
|
||||
strongSelf.isDrawing = true
|
||||
strongSelf.previousStrokePoint = nil
|
||||
|
||||
if strongSelf.uncommitedElement != nil {
|
||||
@ -269,6 +273,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
}
|
||||
newElement.updatePath(path, state: state)
|
||||
strongSelf.uncommitedElement = newElement
|
||||
strongSelf.updateInternalState()
|
||||
case .changed:
|
||||
strongSelf.uncommitedElement?.updatePath(path, state: state)
|
||||
|
||||
@ -339,16 +344,20 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -572,9 +581,20 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
self.uncommitedElement = nil
|
||||
self.elements.removeAll()
|
||||
self.redoElements.removeAll()
|
||||
|
||||
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.lassoView.reset()
|
||||
@ -587,8 +607,18 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
self.uncommitedElement = nil
|
||||
self.redoElements.append(lastElement)
|
||||
self.elements.removeLast()
|
||||
|
||||
let snapshotView = UIImageView(image: self.drawingImage)
|
||||
snapshotView.frame = self.bounds
|
||||
self.addSubview(snapshotView)
|
||||
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()
|
||||
}
|
||||
|
||||
@ -600,6 +630,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
self.elements.append(lastElement)
|
||||
self.redoElements.removeLast()
|
||||
self.uncommitedElement = lastElement
|
||||
|
||||
self.commit(reset: false)
|
||||
self.uncommitedElement = nil
|
||||
|
||||
@ -611,13 +642,13 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
func updateToolState(_ state: DrawingToolState) {
|
||||
switch state {
|
||||
case let .pen(brushState):
|
||||
self.drawingGesturePipeline?.mode = .location
|
||||
self.drawingGesturePipeline?.mode = .polyline
|
||||
self.tool = .pen
|
||||
self.toolColor = brushState.color
|
||||
self.toolBrushSize = brushState.size
|
||||
self.toolHasArrow = false
|
||||
case let .arrow(brushState):
|
||||
self.drawingGesturePipeline?.mode = .location
|
||||
self.drawingGesturePipeline?.mode = .polyline
|
||||
self.tool = .pen
|
||||
self.toolColor = brushState.color
|
||||
self.toolBrushSize = brushState.size
|
||||
@ -691,7 +722,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
canUndo: !self.elements.isEmpty,
|
||||
canRedo: !self.redoElements.isEmpty,
|
||||
canClear: !self.elements.isEmpty,
|
||||
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne
|
||||
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
|
||||
isDrawing: self.isDrawing
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -2,73 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
struct PointWeighted {
|
||||
let point: CGPoint
|
||||
let weight: CGFloat
|
||||
|
||||
static let zero = PointWeighted(point: CGPoint.zero, weight: 0)
|
||||
}
|
||||
|
||||
struct LineSegment {
|
||||
let start: CGPoint
|
||||
let end: CGPoint
|
||||
|
||||
var length: CGFloat {
|
||||
return start.distance(to: end)
|
||||
}
|
||||
|
||||
func average(with line: LineSegment) -> LineSegment {
|
||||
return LineSegment(start: start.average(with: line.start), end: end.average(with: line.end))
|
||||
}
|
||||
|
||||
func normalLine(from weightedPoint: PointWeighted) -> LineSegment {
|
||||
return normalLine(withMiddle: weightedPoint.point, weight: weightedPoint.weight)
|
||||
}
|
||||
|
||||
func normalLine(withMiddle middle: CGPoint, weight: CGFloat) -> LineSegment {
|
||||
let relativeEnd = start.diff(to: end)
|
||||
|
||||
guard weight != 0 && relativeEnd != CGPoint.zero else {
|
||||
return LineSegment(start: middle, end: middle)
|
||||
}
|
||||
|
||||
let moddle = weight / 2
|
||||
let lengthK = moddle / length
|
||||
|
||||
let k = CGPoint(x: relativeEnd.x * lengthK, y: relativeEnd.y * lengthK)
|
||||
|
||||
var normalLineStart = CGPoint(x: k.y, y: -k.x)
|
||||
var normalLineEnd = CGPoint(x: -k.y, y: k.x)
|
||||
|
||||
normalLineStart.x += middle.x;
|
||||
normalLineStart.y += middle.y;
|
||||
|
||||
normalLineEnd.x += middle.x;
|
||||
normalLineEnd.y += middle.y;
|
||||
|
||||
return LineSegment(start: normalLineStart, end: normalLineEnd)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension CGPoint {
|
||||
func average(with point: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: (x + point.x) * 0.5, y: (y + point.y) * 0.5)
|
||||
}
|
||||
|
||||
func diff(to point: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: point.x - x, y: point.y - y)
|
||||
}
|
||||
|
||||
func forward(to point: CGPoint, by: CGFloat) -> CGPoint {
|
||||
let diff = diff(to: point)
|
||||
let distance = sqrt(pow(diff.x, 2) + pow(diff.y, 2))
|
||||
let k = by / distance
|
||||
|
||||
return CGPoint(x: point.x + diff.x * k, y: point.y + diff.y * k)
|
||||
}
|
||||
}
|
||||
|
||||
final class PenTool: DrawingElement {
|
||||
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
func setup(size: CGSize) {
|
||||
@ -79,15 +12,12 @@ final class PenTool: DrawingElement {
|
||||
self.frame = bounds
|
||||
}
|
||||
|
||||
private var paths: [UIBezierPath] = []
|
||||
private var tempPath: UIBezierPath?
|
||||
|
||||
private var color: UIColor?
|
||||
fileprivate func draw(paths: [UIBezierPath], tempPath: UIBezierPath?, color: UIColor, rect: CGRect) {
|
||||
self.paths = paths
|
||||
self.tempPath = tempPath
|
||||
private var line: StrokeLine?
|
||||
fileprivate func draw(line: StrokeLine, color: UIColor, rect: CGRect) {
|
||||
self.line = line
|
||||
self.color = color
|
||||
|
||||
self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
|
||||
}
|
||||
|
||||
@ -118,21 +48,7 @@ final class PenTool: DrawingElement {
|
||||
}
|
||||
|
||||
override func draw(in ctx: CGContext) {
|
||||
guard let color = self.color else {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.setFillColor(color.cgColor)
|
||||
|
||||
for path in self.paths {
|
||||
ctx.addPath(path.cgPath)
|
||||
ctx.fillPath()
|
||||
}
|
||||
|
||||
if let tempPath = self.tempPath {
|
||||
ctx.addPath(tempPath.cgPath)
|
||||
ctx.fillPath()
|
||||
}
|
||||
self.line?.drawInContext(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,14 +62,12 @@ final class PenTool: DrawingElement {
|
||||
var path: Polyline?
|
||||
var boundingBox: CGRect?
|
||||
|
||||
var didSetupArrow = false
|
||||
private var renderLine: StrokeLine
|
||||
let renderLineWidth: CGFloat
|
||||
let renderArrowLength: CGFloat
|
||||
let renderArrowLineWidth: CGFloat
|
||||
|
||||
var bezierPaths: [UIBezierPath] = []
|
||||
var tempBezierPath: UIBezierPath?
|
||||
|
||||
var didSetupArrow = false
|
||||
var arrowLeftPath: UIBezierPath?
|
||||
var arrowLeftPoint: CGPoint?
|
||||
var arrowRightPath: UIBezierPath?
|
||||
@ -167,37 +81,20 @@ final class PenTool: DrawingElement {
|
||||
return self.path?.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
|
||||
}
|
||||
|
||||
var _points: [Polyline.Point] = []
|
||||
|
||||
var points: [Polyline.Point] {
|
||||
guard let linePath = self.path else {
|
||||
return []
|
||||
}
|
||||
var points: [Polyline.Point] = []
|
||||
var lastPoint: Polyline.Point?
|
||||
for point in self._points {
|
||||
for point in linePath.points {
|
||||
points.append(point.offsetBy(self.translation))
|
||||
lastPoint = point
|
||||
}
|
||||
if let arrowLeftPoint, let lastPoint {
|
||||
points.append(lastPoint.withLocation(arrowLeftPoint.offsetBy(self.translation)))
|
||||
}
|
||||
if let arrowRightPoint, let lastPoint {
|
||||
points.append(lastPoint.withLocation(arrowRightPoint.offsetBy(self.translation)))
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
private let pointsPerLine: Int = 4
|
||||
private var nextPointIndex: Int = 0
|
||||
private var drawPoints = [PointWeighted](repeating: PointWeighted.zero, count: 4)
|
||||
|
||||
private var arrowParams: (CGPoint, CGFloat)?
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
for path in self.bezierPaths {
|
||||
if path.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
@ -220,15 +117,15 @@ final class PenTool: DrawingElement {
|
||||
self.lineWidth = lineWidth
|
||||
self.arrow = arrow
|
||||
|
||||
let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.0015)
|
||||
let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.05)
|
||||
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.renderLineWidth = lineWidth
|
||||
self.renderArrowLength = lineWidth * 7.0
|
||||
self.renderArrowLineWidth = lineWidth * 2.0
|
||||
self.renderArrowLength = lineWidth * 3.0
|
||||
self.renderArrowLineWidth = lineWidth
|
||||
|
||||
self.path = Polyline(points: [])
|
||||
self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth + (lineWidth - minLineWidth) * 0.3, lineWidth: lineWidth)
|
||||
}
|
||||
|
||||
func finishArrow(_ completion: @escaping () -> Void) {
|
||||
@ -248,57 +145,52 @@ final class PenTool: DrawingElement {
|
||||
return layer
|
||||
}
|
||||
|
||||
var lastPoint: CGPoint?
|
||||
func updateWithLocation(_ point: CGPoint, ended: Bool = false) {
|
||||
if ended {
|
||||
self.lastPoint = self.drawPoints[self.nextPointIndex - 1].point
|
||||
|
||||
if let path = tempBezierPath {
|
||||
bezierPaths.last?.append(path)
|
||||
}
|
||||
tempBezierPath = nil
|
||||
nextPointIndex = 0
|
||||
} else {
|
||||
addPoint(point)
|
||||
}
|
||||
}
|
||||
|
||||
var previousPoint: CGPoint?
|
||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
guard case let .location(point) = path else {
|
||||
guard case let .polyline(line) = path, let point = line.points.last else {
|
||||
return
|
||||
}
|
||||
|
||||
self._points.append(point)
|
||||
self.path?.points.append(point)
|
||||
self.path = line
|
||||
|
||||
switch state {
|
||||
case .began:
|
||||
addPoint(point.location)
|
||||
case .changed:
|
||||
if self._points.count > 1 {
|
||||
self.updateTouchPoints(point: self._points[self._points.count - 1].location, previousPoint: self._points[self._points.count - 2].location)
|
||||
self.updateWithLocation(point.location)
|
||||
}
|
||||
case .ended:
|
||||
self.updateTouchPoints(point: self._points[self._points.count - 1].location, previousPoint: self._points[self._points.count - 2].location)
|
||||
self.updateWithLocation(point.location, ended: true)
|
||||
|
||||
let filterDistance: CGFloat
|
||||
if point.velocity > 1200 {
|
||||
filterDistance = 75.0
|
||||
} else {
|
||||
filterDistance = 35.0
|
||||
}
|
||||
|
||||
if let previousPoint, point.location.distance(to: previousPoint) < filterDistance, state == .changed, self.renderLine.ready {
|
||||
return
|
||||
}
|
||||
print("vel: \(point.velocity)")
|
||||
if let previousPoint {
|
||||
print("dist: \(point.location.distance(to: previousPoint))")
|
||||
}
|
||||
self.previousPoint = point.location
|
||||
|
||||
let rect = self.renderLine.draw(at: point)
|
||||
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
currentRenderLayer.draw(line: self.renderLine, color: self.color.toUIColor(), rect: rect)
|
||||
}
|
||||
|
||||
|
||||
if state == .ended {
|
||||
if self.arrow {
|
||||
let points = self.path?.points ?? []
|
||||
var direction: CGFloat?
|
||||
|
||||
let p2 = points[points.count - 1].location
|
||||
for i in 1 ..< min(points.count - 2, 12) {
|
||||
let p1 = points[points.count - 1 - i].location
|
||||
if p1.distance(to: p2) > renderArrowLength * 0.5 {
|
||||
direction = p2.angle(to: p1)
|
||||
break
|
||||
var direction: CGFloat?
|
||||
if points.count > 4 {
|
||||
let p2 = points[points.count - 1].location
|
||||
for i in 1 ..< min(points.count - 2, 12) {
|
||||
let p1 = points[points.count - 1 - i].location
|
||||
if p1.distance(to: p2) > renderArrowLength * 0.5 {
|
||||
direction = p2.angle(to: p1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let point = self.lastPoint, let direction {
|
||||
self.arrowParams = (point, direction)
|
||||
|
||||
if let point = points.last?.location, let direction {
|
||||
let arrowLeftPath = UIBezierPath()
|
||||
arrowLeftPath.move(to: point)
|
||||
let leftPoint = point.pointAt(distance: self.renderArrowLength, angle: direction - 0.45)
|
||||
@ -316,127 +208,7 @@ final class PenTool: DrawingElement {
|
||||
self.arrowRightPoint = rightPoint
|
||||
}
|
||||
}
|
||||
case .cancelled:
|
||||
break
|
||||
}
|
||||
|
||||
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
currentRenderLayer.draw(paths: self.bezierPaths, tempPath: self.tempBezierPath, color: self.color.toUIColor(), rect: CGRect(origin: .zero, size: self.drawingSize))
|
||||
}
|
||||
}
|
||||
|
||||
private let minDistance: CGFloat = 2
|
||||
|
||||
private func addPoint(_ point: CGPoint) {
|
||||
if isFirstPoint {
|
||||
startNewLine(from: PointWeighted(point: point, weight: 2.0))
|
||||
} else {
|
||||
let previousPoint = self.drawPoints[nextPointIndex - 1].point
|
||||
guard previousPoint.distance(to: point) >= minDistance else {
|
||||
return
|
||||
}
|
||||
if isStartOfNextLine {
|
||||
finalizeBezier(nextLineStartPoint: point)
|
||||
startNewLine(from: self.drawPoints[3])
|
||||
}
|
||||
|
||||
let weightedPoint = PointWeighted(point: point, weight: weightForLine(between: previousPoint, and: point))
|
||||
addPoint(point: weightedPoint)
|
||||
}
|
||||
|
||||
let newBezier = generateBezierPath(withPointIndex: nextPointIndex - 1)
|
||||
self.tempBezierPath = newBezier
|
||||
}
|
||||
|
||||
private var isFirstPoint: Bool {
|
||||
return nextPointIndex == 0
|
||||
}
|
||||
|
||||
private var isStartOfNextLine: Bool {
|
||||
return nextPointIndex >= pointsPerLine
|
||||
}
|
||||
|
||||
private func startNewLine(from weightedPoint: PointWeighted) {
|
||||
drawPoints[0] = weightedPoint
|
||||
nextPointIndex = 1
|
||||
}
|
||||
|
||||
private func addPoint(point: PointWeighted) {
|
||||
drawPoints[nextPointIndex] = point
|
||||
nextPointIndex += 1
|
||||
}
|
||||
|
||||
private func finalizeBezier(nextLineStartPoint: CGPoint) {
|
||||
let touchPoint2 = drawPoints[2].point
|
||||
let newTouchPoint3 = touchPoint2.average(with: nextLineStartPoint)
|
||||
drawPoints[3] = PointWeighted(point: newTouchPoint3, weight: weightForLine(between: touchPoint2, and: newTouchPoint3))
|
||||
|
||||
guard let bezier = generateBezierPath(withPointIndex: 3) else {
|
||||
return
|
||||
}
|
||||
self.bezierPaths.append(bezier)
|
||||
|
||||
}
|
||||
|
||||
private func generateBezierPath(withPointIndex index: Int) -> UIBezierPath? {
|
||||
switch index {
|
||||
case 0:
|
||||
return UIBezierPath.dot(with: drawPoints[0])
|
||||
case 1:
|
||||
return UIBezierPath.curve(withPointA: drawPoints[0], pointB: drawPoints[1])
|
||||
case 2:
|
||||
return UIBezierPath.curve(withPointA: drawPoints[0], pointB: drawPoints[1], pointC: drawPoints[2])
|
||||
case 3:
|
||||
return UIBezierPath.curve(withPointA: drawPoints[0], pointB: drawPoints[1], pointC: drawPoints[2], pointD: drawPoints[3])
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func weightForLine(between pointA: CGPoint, and pointB: CGPoint) -> CGFloat {
|
||||
let length = pointA.distance(to: pointB)
|
||||
|
||||
let limitRange: CGFloat = 50
|
||||
|
||||
var lowerer: CGFloat = 0.2
|
||||
var constant: CGFloat = 2
|
||||
|
||||
let toolWidth = self.renderLineWidth
|
||||
|
||||
constant = toolWidth - 3.0
|
||||
lowerer = 0.25 * toolWidth / 10.0
|
||||
|
||||
|
||||
let r = min(limitRange, length)
|
||||
|
||||
return (r * lowerer) + constant
|
||||
}
|
||||
|
||||
public var firstPoint: CGPoint = .zero
|
||||
public var currentPoint: CGPoint = .zero
|
||||
private var previousPoint: CGPoint = .zero
|
||||
private var previousPreviousPoint: CGPoint = .zero
|
||||
|
||||
private func setTouchPoints(point: CGPoint, previousPoint: CGPoint) {
|
||||
self.previousPoint = previousPoint
|
||||
self.previousPreviousPoint = previousPoint
|
||||
self.currentPoint = point
|
||||
}
|
||||
|
||||
private func updateTouchPoints(point: CGPoint, previousPoint: CGPoint) {
|
||||
self.previousPreviousPoint = self.previousPoint
|
||||
self.previousPoint = previousPoint
|
||||
self.currentPoint = point
|
||||
}
|
||||
|
||||
private func calculateMidPoint(_ p1 : CGPoint, p2 : CGPoint) -> CGPoint {
|
||||
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
|
||||
}
|
||||
|
||||
private func getMidPoints() -> (CGPoint, CGPoint) {
|
||||
let mid1 : CGPoint = calculateMidPoint(previousPoint, p2: previousPreviousPoint)
|
||||
let mid2 : CGPoint = calculateMidPoint(currentPoint, p2: previousPoint)
|
||||
return (mid1, mid2)
|
||||
}
|
||||
|
||||
func draw(in context: CGContext, size: CGSize) {
|
||||
@ -445,12 +217,8 @@ final class PenTool: DrawingElement {
|
||||
context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
|
||||
context.setShouldAntialias(true)
|
||||
|
||||
context.setFillColor(self.color.toCGColor())
|
||||
for path in self.bezierPaths {
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
}
|
||||
|
||||
self.renderLine.drawInContext(context)
|
||||
|
||||
if let arrowLeftPath, let arrowRightPath {
|
||||
context.setStrokeColor(self.color.toCGColor())
|
||||
@ -468,101 +236,210 @@ final class PenTool: DrawingElement {
|
||||
}
|
||||
}
|
||||
|
||||
extension UIBezierPath {
|
||||
|
||||
class func dot(with weightedPoint: PointWeighted) -> UIBezierPath {
|
||||
let path = UIBezierPath()
|
||||
path.addArc(withCenter: weightedPoint.point, radius: weightedPoint.weight / 2.0, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
class func curve(withPointA pointA: PointWeighted, pointB: PointWeighted) -> UIBezierPath {
|
||||
let lines = normalToLine(from: pointA, to: pointB)
|
||||
|
||||
let path = UIBezierPath()
|
||||
path.move(to: lines.0.start)
|
||||
path.addLine(to: lines.1.start)
|
||||
let arcA = lines.1.start
|
||||
let arcB = lines.1.end
|
||||
path.addQuadCurve(to: arcB, controlPoint: pointA.point.forward(to: pointB.point, by: arcA.distance(to: arcB) / 1.1))
|
||||
path.addLine(to: lines.0.end)
|
||||
path.close()
|
||||
|
||||
return path
|
||||
private class StrokeLine {
|
||||
struct Segment {
|
||||
let a: CGPoint
|
||||
let b: CGPoint
|
||||
let c: CGPoint
|
||||
let d: CGPoint
|
||||
let abWidth: CGFloat
|
||||
let cdWidth: CGFloat
|
||||
}
|
||||
|
||||
class func curve(withPointA pointA: PointWeighted, pointB: PointWeighted, pointC: PointWeighted) -> UIBezierPath {
|
||||
let linesAB = normalToLine(from: pointA, to: pointB)
|
||||
let linesBC = normalToLine(from: pointB, to: pointC)
|
||||
struct Point {
|
||||
let position: CGPoint
|
||||
let width: CGFloat
|
||||
|
||||
let lineA = linesAB.0
|
||||
let lineB = linesAB.1.average(with: linesBC.0)
|
||||
let lineC = linesBC.1
|
||||
|
||||
let path = UIBezierPath()
|
||||
path.move(to: lineA.start)
|
||||
path.addQuadCurve(to: lineC.start, controlPoint: lineB.start)
|
||||
let arcA = lineC.start
|
||||
let arcB = lineC.end
|
||||
|
||||
path.addQuadCurve(to: arcB, controlPoint: pointB.point.forward(to: pointC.point, by: arcA.distance(to: arcB) / 1.1))
|
||||
path.addQuadCurve(to: lineA.end, controlPoint: lineB.end)
|
||||
path.close()
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
class func line(withPointA pointA: PointWeighted, pointB: PointWeighted, pointC: PointWeighted, prevLineSegment: LineSegment, roundedEnd: Bool = true) -> (UIBezierPath, LineSegment) {
|
||||
let linesAB = normalToLine(from: pointA, to: pointB)
|
||||
let linesBC = normalToLine(from: pointB, to: pointC)
|
||||
|
||||
// let lineA = linesAB.0
|
||||
let lineB = linesAB.1.average(with: linesBC.0)
|
||||
let lineC = linesBC.1
|
||||
|
||||
let path = UIBezierPath()
|
||||
path.move(to: prevLineSegment.start)
|
||||
path.addQuadCurve(to: lineC.start, controlPoint: lineB.start)
|
||||
if roundedEnd {
|
||||
let arcA = lineC.start
|
||||
let arcB = lineC.end
|
||||
|
||||
path.addQuadCurve(to: arcB, controlPoint: pointB.point.forward(to: pointC.point, by: arcA.distance(to: arcB) / 1.1))
|
||||
} else {
|
||||
path.addLine(to: lineC.end)
|
||||
init(position: CGPoint, width: CGFloat) {
|
||||
self.position = position
|
||||
self.width = width
|
||||
}
|
||||
path.addQuadCurve(to: prevLineSegment.end, controlPoint: lineB.end)
|
||||
path.close()
|
||||
}
|
||||
|
||||
private(set) var points: [Point] = []
|
||||
private var smoothPoints: [Point] = []
|
||||
private var segments: [Segment] = []
|
||||
|
||||
return (path, lineC)
|
||||
private let minLineWidth: CGFloat
|
||||
let lineWidth: CGFloat
|
||||
private var lastWidth: CGFloat?
|
||||
|
||||
var ready = false
|
||||
|
||||
let color: UIColor
|
||||
|
||||
init(color: UIColor, minLineWidth: CGFloat, lineWidth: CGFloat) {
|
||||
self.color = color
|
||||
self.minLineWidth = minLineWidth
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
class func curve(withPointA pointA: PointWeighted, pointB: PointWeighted, pointC: PointWeighted, pointD: PointWeighted) -> UIBezierPath {
|
||||
let linesAB = normalToLine(from: pointA, to: pointB)
|
||||
let linesBC = normalToLine(from: pointB, to: pointC)
|
||||
let linesCD = normalToLine(from: pointC, to: pointD)
|
||||
func draw(at point: Polyline.Point) -> CGRect {
|
||||
var velocity = point.velocity
|
||||
if velocity.isZero {
|
||||
velocity = 600.0
|
||||
}
|
||||
let width = extractLineWidth(from: velocity)
|
||||
self.lastWidth = width
|
||||
|
||||
let lineA = linesAB.0
|
||||
let lineB = linesAB.1.average(with: linesBC.0)
|
||||
let lineC = linesBC.1.average(with: linesCD.0)
|
||||
let lineD = linesCD.1
|
||||
|
||||
let path = UIBezierPath()
|
||||
path.move(to: lineA.start)
|
||||
path.addCurve(to: lineD.start, controlPoint1: lineB.start, controlPoint2: lineC.start)
|
||||
let arcA = lineD.start
|
||||
let arcB = lineD.end
|
||||
path.addQuadCurve(to: arcB, controlPoint: pointC.point.forward(to: pointD.point, by: arcA.distance(to: arcB) / 1.1))
|
||||
path.addCurve(to: lineA.end, controlPoint1: lineC.end, controlPoint2: lineB.end)
|
||||
path.close()
|
||||
|
||||
return path
|
||||
let point = Point(position: point.location, width: width)
|
||||
return appendPoint(point)
|
||||
}
|
||||
|
||||
class func normalToLine(from pointA: PointWeighted, to pointB: PointWeighted) -> (LineSegment, LineSegment) {
|
||||
let line = LineSegment(start: pointA.point, end: pointB.point)
|
||||
func drawInContext(_ context: CGContext) {
|
||||
self.drawSegments(self.segments, inContext: context)
|
||||
}
|
||||
|
||||
func extractLineWidth(from velocity: CGFloat) -> CGFloat {
|
||||
let minValue = self.minLineWidth
|
||||
let maxValue = self.lineWidth
|
||||
|
||||
return (line.normalLine(from: pointA), line.normalLine(from: pointB))
|
||||
var width = max(minValue, min(maxValue + 1.0 - (velocity / 180.0), maxValue))
|
||||
if let lastWidth = self.lastWidth {
|
||||
width = width * 0.2 + lastWidth * 0.8
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func appendPoint(_ point: Point) -> CGRect {
|
||||
self.points.append(point)
|
||||
|
||||
guard self.points.count > 2 else { return .null }
|
||||
|
||||
let index = self.points.count - 1
|
||||
let point0 = self.points[index - 2]
|
||||
let point1 = self.points[index - 1]
|
||||
let point2 = self.points[index]
|
||||
|
||||
let newSmoothPoints = smoothPoints(
|
||||
fromPoint0: point0,
|
||||
point1: point1,
|
||||
point2: point2
|
||||
)
|
||||
|
||||
let lastOldSmoothPoint = smoothPoints.last
|
||||
smoothPoints.append(contentsOf: newSmoothPoints)
|
||||
|
||||
guard smoothPoints.count > 1 else { return .null }
|
||||
|
||||
let newSegments: ([Segment], CGRect) = {
|
||||
guard let lastOldSmoothPoint = lastOldSmoothPoint else {
|
||||
return segments(fromSmoothPoints: newSmoothPoints)
|
||||
}
|
||||
return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints)
|
||||
}()
|
||||
segments.append(contentsOf: newSegments.0)
|
||||
|
||||
self.ready = true
|
||||
|
||||
return newSegments.1
|
||||
}
|
||||
|
||||
func smoothPoints(fromPoint0 point0: Point, point1: Point, point2: Point) -> [Point] {
|
||||
var smoothPoints = [Point]()
|
||||
|
||||
let midPoint1 = (point0.position + point1.position) * 0.5
|
||||
let midPoint2 = (point1.position + point2.position) * 0.5
|
||||
|
||||
let segmentDistance = 3.0
|
||||
let distance = midPoint1.distance(to: midPoint2)
|
||||
let numberOfSegments = min(128, max(floor(distance / segmentDistance), 32))
|
||||
|
||||
let step = 1.0 / numberOfSegments
|
||||
for t in stride(from: 0, to: 1, by: step) {
|
||||
let position = midPoint1 * pow(1 - t, 2) + point1.position * 2 * (1 - t) * t + midPoint2 * t * t
|
||||
let size = pow(1 - t, 2) * ((point0.width + point1.width) * 0.5) + 2 * (1 - t) * t * point1.width + t * t * ((point1.width + point2.width) * 0.5)
|
||||
let point = Point(position: position, width: size)
|
||||
smoothPoints.append(point)
|
||||
}
|
||||
|
||||
let finalPoint = Point(position: midPoint2, width: (point1.width + point2.width) * 0.5)
|
||||
smoothPoints.append(finalPoint)
|
||||
|
||||
return smoothPoints
|
||||
}
|
||||
|
||||
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].position
|
||||
let previousWidth = smoothPoints[i - 1].width
|
||||
let currentPoint = smoothPoints[i].position
|
||||
let currentWidth = smoothPoints[i].width
|
||||
let direction = currentPoint - previousPoint
|
||||
|
||||
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 = perpendicular / length
|
||||
}
|
||||
|
||||
let a = previousPoint + perpendicular * previousWidth / 2
|
||||
let b = previousPoint - perpendicular * previousWidth / 2
|
||||
let c = currentPoint + perpendicular * currentWidth / 2
|
||||
let d = currentPoint - perpendicular * currentWidth / 2
|
||||
|
||||
let ab: CGPoint = {
|
||||
let center = (b + a) / 2
|
||||
let radius = center - b
|
||||
return .init(x: center.x - radius.y, y: center.y + radius.x)
|
||||
}()
|
||||
let cd: CGPoint = {
|
||||
let center = (c + d) / 2
|
||||
let radius = center - c
|
||||
return .init(x: center.x + radius.y, y: center.y - radius.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)
|
||||
|
||||
updateRect = updateRect.union(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY))
|
||||
|
||||
segments.append(Segment(a: a, b: b, c: c, d: d, abWidth: previousWidth, cdWidth: currentWidth))
|
||||
}
|
||||
return (segments, updateRect)
|
||||
}
|
||||
|
||||
func drawSegments(_ segments: [Segment], inContext context: CGContext) {
|
||||
for segment in segments {
|
||||
context.beginPath()
|
||||
|
||||
//let color = [UIColor.red, UIColor.green, UIColor.blue, UIColor.yellow].randomElement()!
|
||||
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setFillColor(color.cgColor)
|
||||
|
||||
context.move(to: segment.b)
|
||||
|
||||
let abStartAngle = atan2(segment.b.y - segment.a.y, segment.b.x - segment.a.x)
|
||||
context.addArc(
|
||||
center: (segment.a + segment.b)/2,
|
||||
radius: segment.abWidth/2,
|
||||
startAngle: abStartAngle,
|
||||
endAngle: abStartAngle + .pi,
|
||||
clockwise: true
|
||||
)
|
||||
context.addLine(to: segment.c)
|
||||
|
||||
let cdStartAngle = atan2(segment.c.y - segment.d.y, segment.c.x - segment.d.x)
|
||||
context.addArc(
|
||||
center: (segment.c + segment.d) / 2,
|
||||
radius: segment.cdWidth/2,
|
||||
startAngle: cdStartAngle,
|
||||
endAngle: cdStartAngle + .pi,
|
||||
clockwise: true
|
||||
)
|
||||
context.closePath()
|
||||
|
||||
context.fillPath()
|
||||
context.strokePath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -362,6 +362,7 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
let alignment: DrawingTextAlignment
|
||||
let font: DrawingTextFont
|
||||
let isEmojiKeyboard: Bool
|
||||
let tag: AnyObject?
|
||||
|
||||
let presentColorPicker: () -> Void
|
||||
let presentFastColorPicker: (GenericComponentViewTag) -> Void
|
||||
@ -378,6 +379,7 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
alignment: DrawingTextAlignment,
|
||||
font: DrawingTextFont,
|
||||
isEmojiKeyboard: Bool,
|
||||
tag: AnyObject?,
|
||||
presentColorPicker: @escaping () -> Void = {},
|
||||
presentFastColorPicker: @escaping (GenericComponentViewTag) -> Void = { _ in },
|
||||
updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in },
|
||||
@ -392,6 +394,7 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
self.alignment = alignment
|
||||
self.font = font
|
||||
self.isEmojiKeyboard = isEmojiKeyboard
|
||||
self.tag = tag
|
||||
self.presentColorPicker = presentColorPicker
|
||||
self.presentFastColorPicker = presentFastColorPicker
|
||||
self.updateFastColorPickerPan = updateFastColorPickerPan
|
||||
@ -460,6 +463,25 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
State()
|
||||
}
|
||||
|
||||
final class View: UIView, ComponentTaggedView {
|
||||
var componentTag: AnyObject?
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let componentTag = self.componentTag {
|
||||
let tag = tag as AnyObject
|
||||
if componentTag === tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
let view = View()
|
||||
view.componentTag = self.tag
|
||||
return view
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let colorButton = Child(ColorSwatchComponent.self)
|
||||
let colorButtonTag = GenericComponentViewTag()
|
||||
@ -723,6 +745,7 @@ final class TextSizeSliderComponent: Component {
|
||||
if let size = self.validSize, let component = self.component {
|
||||
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
self.released()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user