Drawing
@@ -345,6 +345,7 @@ swift_library(
|
||||
"//submodules/PasswordSetupUI:PasswordSetupUIResources",
|
||||
"//submodules/PasswordSetupUI:PasswordSetupUIAssets",
|
||||
"//submodules/PremiumUI:PremiumUIResources",
|
||||
"//submodules/DrawingUI:DrawingUIResources",
|
||||
"//submodules/TelegramUI:TelegramUIResources",
|
||||
"//submodules/TelegramUI:TelegramUIAssets",
|
||||
":GeneratedPresentationStrings/Resources/PresentationStrings.data",
|
||||
|
||||
@@ -35,11 +35,25 @@ public extension Transition.AppearWithGuide {
|
||||
}
|
||||
|
||||
public extension Transition.Disappear {
|
||||
static let `default` = Transition.Disappear { view, transition, completion in
|
||||
static func `default`(scale: Bool = false, alpha: Bool = true) -> Transition.Disappear {
|
||||
return Transition.Disappear { view, transition, completion in
|
||||
if scale {
|
||||
transition.setScale(view: view, scale: 0.01, completion: { _ in
|
||||
if !alpha {
|
||||
completion()
|
||||
}
|
||||
})
|
||||
}
|
||||
if alpha {
|
||||
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
if !alpha && !scale {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.DisappearWithGuide {
|
||||
|
||||
@@ -391,11 +391,11 @@ public struct Transition {
|
||||
}
|
||||
}
|
||||
|
||||
public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
self.setAlpha(layer: view.layer, alpha: alpha, completion: completion)
|
||||
public func setAlpha(view: UIView, alpha: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
self.setAlpha(layer: view.layer, alpha: alpha, delay: delay, completion: completion)
|
||||
}
|
||||
|
||||
public func setAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
public func setAlpha(layer: CALayer, alpha: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
if layer.opacity == Float(alpha) {
|
||||
completion?(true)
|
||||
return
|
||||
@@ -408,15 +408,15 @@ public struct Transition {
|
||||
case .curve:
|
||||
let previousAlpha = layer.presentation()?.opacity ?? layer.opacity
|
||||
layer.opacity = Float(alpha)
|
||||
self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, completion: completion)
|
||||
self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, delay: delay, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
public func setScale(view: UIView, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
self.setScale(layer: view.layer, scale: scale, completion: completion)
|
||||
public func setScale(view: UIView, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
self.setScale(layer: view.layer, scale: scale, delay: delay, completion: completion)
|
||||
}
|
||||
|
||||
public func setScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
public func setScale(layer: CALayer, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = layer.presentation()?.transform ?? layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
if currentScale == scale {
|
||||
@@ -435,7 +435,7 @@ public struct Transition {
|
||||
to: scale as NSNumber,
|
||||
keyPath: "transform.scale",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
delay: delay,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: false,
|
||||
@@ -476,19 +476,23 @@ public struct Transition {
|
||||
}
|
||||
|
||||
public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||
self.setSublayerTransform(layer: view.layer, transform: transform, completion: completion)
|
||||
}
|
||||
|
||||
public func setSublayerTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
view.layer.sublayerTransform = transform
|
||||
layer.sublayerTransform = transform
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
let previousValue: CATransform3D
|
||||
if let presentation = view.layer.presentation() {
|
||||
if let presentation = layer.presentation() {
|
||||
previousValue = presentation.sublayerTransform
|
||||
} else {
|
||||
previousValue = view.layer.sublayerTransform
|
||||
previousValue = layer.sublayerTransform
|
||||
}
|
||||
view.layer.sublayerTransform = transform
|
||||
view.layer.animate(
|
||||
layer.sublayerTransform = transform
|
||||
layer.animate(
|
||||
from: NSValue(caTransform3D: previousValue),
|
||||
to: NSValue(caTransform3D: transform),
|
||||
keyPath: "sublayerTransform",
|
||||
@@ -502,7 +506,7 @@ public struct Transition {
|
||||
}
|
||||
}
|
||||
|
||||
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
@@ -512,7 +516,7 @@ public struct Transition {
|
||||
to: toValue as NSNumber,
|
||||
keyPath: "transform.scale",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
delay: delay,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
@@ -540,11 +544,11 @@ public struct Transition {
|
||||
}
|
||||
}
|
||||
|
||||
public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animateAlpha(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion)
|
||||
public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animateAlpha(layer: view.layer, from: fromValue, to: toValue, delay: delay, additive: additive, completion: completion)
|
||||
}
|
||||
|
||||
public func animateAlpha(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
public func animateAlpha(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
@@ -554,7 +558,7 @@ public struct Transition {
|
||||
to: toValue as NSNumber,
|
||||
keyPath: "opacity",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
delay: delay,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
@@ -709,6 +713,28 @@ public struct Transition {
|
||||
}
|
||||
}
|
||||
|
||||
public func setShapeLayerLineWidth(layer: CAShapeLayer, lineWidth: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
layer.lineWidth = lineWidth
|
||||
case let .curve(duration, curve):
|
||||
let previousLineWidth = layer.lineWidth
|
||||
layer.lineWidth = lineWidth
|
||||
|
||||
layer.animate(
|
||||
from: previousLineWidth as NSNumber,
|
||||
to: lineWidth as NSNumber,
|
||||
keyPath: "lineWidth",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: false,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func setShapeLayerLineDashPattern(layer: CAShapeLayer, pattern: [NSNumber], completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
|
||||
@@ -6,11 +6,13 @@ public final class Button: Component {
|
||||
public let minSize: CGSize?
|
||||
public let tag: AnyObject?
|
||||
public let automaticHighlight: Bool
|
||||
public let isEnabled: Bool
|
||||
public let action: () -> Void
|
||||
public let holdAction: (() -> Void)?
|
||||
|
||||
convenience public init(
|
||||
content: AnyComponent<Empty>,
|
||||
isEnabled: Bool = true,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.init(
|
||||
@@ -18,6 +20,7 @@ public final class Button: Component {
|
||||
minSize: nil,
|
||||
tag: nil,
|
||||
automaticHighlight: true,
|
||||
isEnabled: isEnabled,
|
||||
action: action,
|
||||
holdAction: nil
|
||||
)
|
||||
@@ -28,6 +31,7 @@ public final class Button: Component {
|
||||
minSize: CGSize? = nil,
|
||||
tag: AnyObject? = nil,
|
||||
automaticHighlight: Bool = true,
|
||||
isEnabled: Bool = true,
|
||||
action: @escaping () -> Void,
|
||||
holdAction: (() -> Void)?
|
||||
) {
|
||||
@@ -35,6 +39,7 @@ public final class Button: Component {
|
||||
self.minSize = minSize
|
||||
self.tag = tag
|
||||
self.automaticHighlight = automaticHighlight
|
||||
self.isEnabled = isEnabled
|
||||
self.action = action
|
||||
self.holdAction = holdAction
|
||||
}
|
||||
@@ -45,6 +50,7 @@ public final class Button: Component {
|
||||
minSize: minSize,
|
||||
tag: self.tag,
|
||||
automaticHighlight: self.automaticHighlight,
|
||||
isEnabled: self.isEnabled,
|
||||
action: self.action,
|
||||
holdAction: self.holdAction
|
||||
)
|
||||
@@ -56,6 +62,7 @@ public final class Button: Component {
|
||||
minSize: self.minSize,
|
||||
tag: self.tag,
|
||||
automaticHighlight: self.automaticHighlight,
|
||||
isEnabled: self.isEnabled,
|
||||
action: self.action,
|
||||
holdAction: holdAction
|
||||
)
|
||||
@@ -67,6 +74,7 @@ public final class Button: Component {
|
||||
minSize: self.minSize,
|
||||
tag: tag,
|
||||
automaticHighlight: self.automaticHighlight,
|
||||
isEnabled: self.isEnabled,
|
||||
action: self.action,
|
||||
holdAction: self.holdAction
|
||||
)
|
||||
@@ -85,6 +93,9 @@ public final class Button: Component {
|
||||
if lhs.automaticHighlight != rhs.automaticHighlight {
|
||||
return false
|
||||
}
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -98,11 +109,28 @@ public final class Button: Component {
|
||||
return
|
||||
}
|
||||
if self.currentIsHighlighted != oldValue {
|
||||
self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0
|
||||
self.updateAlpha(transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAlpha(transition: Transition) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let alpha: CGFloat
|
||||
if component.isEnabled {
|
||||
if component.automaticHighlight {
|
||||
alpha = self.currentIsHighlighted ? 0.6 : 1.0
|
||||
} else {
|
||||
alpha = 1.0
|
||||
}
|
||||
} else {
|
||||
alpha = 0.4
|
||||
}
|
||||
transition.setAlpha(view: self.contentView, alpha: alpha)
|
||||
}
|
||||
|
||||
private var holdActionTriggerred: Bool = false
|
||||
private var holdActionTimer: Timer?
|
||||
|
||||
@@ -218,6 +246,9 @@ public final class Button: Component {
|
||||
|
||||
self.component = component
|
||||
|
||||
self.updateAlpha(transition: transition)
|
||||
self.isEnabled = component.isEnabled
|
||||
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize), completion: nil)
|
||||
|
||||
return size
|
||||
|
||||
@@ -20,10 +20,27 @@ public final class LottieAnimationComponent: Component {
|
||||
|
||||
public var name: String
|
||||
public var mode: Mode
|
||||
public var range: (CGFloat, CGFloat)?
|
||||
|
||||
public init(name: String, mode: Mode) {
|
||||
public init(name: String, mode: Mode, range: (CGFloat, CGFloat)? = nil) {
|
||||
self.name = name
|
||||
self.mode = mode
|
||||
self.range = range
|
||||
}
|
||||
|
||||
public static func == (lhs: LottieAnimationComponent.AnimationItem, rhs: LottieAnimationComponent.AnimationItem) -> Bool {
|
||||
if lhs.name != rhs.name {
|
||||
return false
|
||||
}
|
||||
if lhs.mode != rhs.mode {
|
||||
return false
|
||||
}
|
||||
if let lhsRange = lhs.range, let rhsRange = rhs.range, lhsRange != rhsRange {
|
||||
return false
|
||||
} else if (lhs.range == nil) != (rhs.range == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,10 +265,16 @@ public final class LottieAnimationComponent: Component {
|
||||
if updatePlayback {
|
||||
if case .animating = component.animation.mode {
|
||||
if !animationView.isAnimationPlaying {
|
||||
if let range = component.animation.range {
|
||||
animationView.play(fromProgress: range.0, toProgress: range.1, completion: { [weak self] _ in
|
||||
self?.currentCompletion?()
|
||||
})
|
||||
} else {
|
||||
animationView.play { [weak self] _ in
|
||||
self?.currentCompletion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if case let .still(position) = component.animation.mode {
|
||||
switch position {
|
||||
|
||||
@@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -3,15 +3,18 @@ import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class SheetComponentEnvironment: Equatable {
|
||||
public let isDisplaying: Bool
|
||||
public let isCentered: Bool
|
||||
public let hasInputHeight: Bool
|
||||
public let dismiss: (Bool) -> Void
|
||||
|
||||
public init(isDisplaying: Bool, isCentered: Bool, dismiss: @escaping (Bool) -> Void) {
|
||||
public init(isDisplaying: Bool, isCentered: Bool, hasInputHeight: Bool, dismiss: @escaping (Bool) -> Void) {
|
||||
self.isDisplaying = isDisplaying
|
||||
self.isCentered = isCentered
|
||||
self.hasInputHeight = hasInputHeight
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
@@ -22,6 +25,9 @@ public final class SheetComponentEnvironment: Equatable {
|
||||
if lhs.isCentered != rhs.isCentered {
|
||||
return false
|
||||
}
|
||||
if lhs.hasInputHeight != rhs.hasInputHeight {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -29,11 +35,21 @@ public final class SheetComponentEnvironment: Equatable {
|
||||
public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment)
|
||||
|
||||
public enum BackgroundColor: Equatable {
|
||||
public enum BlurStyle: Equatable {
|
||||
case light
|
||||
case dark
|
||||
}
|
||||
|
||||
case color(UIColor)
|
||||
case blur(BlurStyle)
|
||||
}
|
||||
|
||||
public let content: AnyComponent<ChildEnvironmentType>
|
||||
public let backgroundColor: UIColor
|
||||
public let backgroundColor: BackgroundColor
|
||||
public let animateOut: ActionSlot<Action<()>>
|
||||
|
||||
public init(content: AnyComponent<ChildEnvironmentType>, backgroundColor: UIColor, animateOut: ActionSlot<Action<()>>) {
|
||||
public init(content: AnyComponent<ChildEnvironmentType>, backgroundColor: BackgroundColor, animateOut: ActionSlot<Action<()>>) {
|
||||
self.content = content
|
||||
self.backgroundColor = backgroundColor
|
||||
self.animateOut = animateOut
|
||||
@@ -53,20 +69,36 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
private class ScrollView: UIScrollView {
|
||||
var ignoreScroll = false
|
||||
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
|
||||
guard !self.ignoreScroll else {
|
||||
return
|
||||
}
|
||||
if animated && abs(contentOffset.y - self.contentOffset.y) > 200.0 {
|
||||
return
|
||||
}
|
||||
super.setContentOffset(contentOffset, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UIScrollViewDelegate {
|
||||
private let dimView: UIView
|
||||
private let scrollView: UIScrollView
|
||||
private let scrollView: ScrollView
|
||||
private let backgroundView: UIView
|
||||
private var effectView: UIVisualEffectView?
|
||||
private let contentView: ComponentHostView<ChildEnvironmentType>
|
||||
|
||||
private var previousIsDisplaying: Bool = false
|
||||
private var dismiss: ((Bool) -> Void)?
|
||||
|
||||
private var keyboardWillShowObserver: AnyObject?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.dimView = UIView()
|
||||
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
|
||||
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.delaysContentTouches = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
@@ -76,7 +108,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
self.backgroundView = UIView()
|
||||
self.backgroundView.layer.cornerRadius = 10.0
|
||||
self.backgroundView.layer.cornerRadius = 12.0
|
||||
self.backgroundView.layer.masksToBounds = true
|
||||
|
||||
self.contentView = ComponentHostView<ChildEnvironmentType>()
|
||||
@@ -91,12 +123,27 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimViewTapGesture(_:))))
|
||||
|
||||
self.keyboardWillShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil, using: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.scrollView.ignoreScroll = true
|
||||
Queue.mainQueue().after(0.1, {
|
||||
strongSelf.scrollView.ignoreScroll = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let keyboardFrameChangeObserver = self.keyboardWillShowObserver {
|
||||
NotificationCenter.default.removeObserver(keyboardFrameChangeObserver)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dimViewTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.dismiss?(true)
|
||||
@@ -183,8 +230,10 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private var currentHasInputHeight = false
|
||||
private var currentAvailableSize: CGSize?
|
||||
func update(component: SheetComponent<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
let previousHasInputHeight = self.currentHasInputHeight
|
||||
let sheetEnvironment = environment[SheetComponentEnvironment.self].value
|
||||
component.animateOut.connect { [weak self] completion in
|
||||
guard let strongSelf = self else {
|
||||
@@ -195,8 +244,23 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if self.backgroundView.backgroundColor != component.backgroundColor {
|
||||
self.backgroundView.backgroundColor = component.backgroundColor
|
||||
self.currentHasInputHeight = sheetEnvironment.hasInputHeight
|
||||
|
||||
switch component.backgroundColor {
|
||||
case let .blur(style):
|
||||
self.backgroundView.isHidden = true
|
||||
if self.effectView == nil {
|
||||
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: style == .dark ? .dark : .light))
|
||||
effectView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
|
||||
effectView.layer.masksToBounds = true
|
||||
self.backgroundView.superview?.insertSubview(effectView, aboveSubview: self.backgroundView)
|
||||
self.effectView = effectView
|
||||
}
|
||||
case let .color(color):
|
||||
self.backgroundView.backgroundColor = color
|
||||
self.backgroundView.isHidden = false
|
||||
self.effectView?.removeFromSuperview()
|
||||
self.effectView = nil
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
|
||||
@@ -226,9 +290,15 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0)
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
|
||||
if let effectView = self.effectView {
|
||||
transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
|
||||
}
|
||||
} else {
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil)
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
|
||||
if let effectView = self.effectView {
|
||||
transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
|
||||
|
||||
@@ -239,6 +309,10 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
if let currentAvailableSize = self.currentAvailableSize, currentAvailableSize.height != availableSize.height {
|
||||
self.scrollView.contentOffset = CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height))
|
||||
}
|
||||
if self.currentHasInputHeight != previousHasInputHeight {
|
||||
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: self.scrollView.bounds.size))
|
||||
}
|
||||
|
||||
self.currentAvailableSize = availableSize
|
||||
|
||||
if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
|
||||
|
||||
@@ -769,6 +769,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
additionalVisibleOffsetY += reactionContextNode.visibleExtensionDistance
|
||||
}
|
||||
if case .reference = self.source {
|
||||
if actionsFrame.maxY > layout.size.height {
|
||||
actionsFrame.origin.y = contentRect.minY - actionsSize.height - contentActionsSpacing
|
||||
}
|
||||
}
|
||||
if case .center = actionsHorizontalAlignment {
|
||||
actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0)
|
||||
if actionsFrame.maxX > layout.size.width - actionsEdgeInset {
|
||||
@@ -807,6 +812,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
||||
actionsFrame.origin.x = actionsEdgeInset
|
||||
}
|
||||
}
|
||||
|
||||
if case let .reference(reference) = self.source, let transitionInfo = reference.transitionInfo(), let customPosition = transitionInfo.customPosition {
|
||||
actionsFrame = actionsFrame.offsetBy(dx: customPosition.x, dy: customPosition.y)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame.offsetBy(dx: 0.0, dy: additionalVisibleOffsetY), beginWithCurrentState: true)
|
||||
|
||||
if let contentNode = contentNode {
|
||||
@@ -1167,11 +1177,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
||||
if case .center = actionsHorizontalAlignment {
|
||||
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX
|
||||
}
|
||||
|
||||
if case .reference = self.source {
|
||||
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX
|
||||
}
|
||||
|
||||
let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing
|
||||
self.actionsStackNode.layer.animate(
|
||||
from: NSValue(cgPoint: CGPoint()),
|
||||
|
||||
93
submodules/DrawingUI/BUILD
Normal file
@@ -0,0 +1,93 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
load(
|
||||
"@build_bazel_rules_apple//apple:resources.bzl",
|
||||
"apple_resource_bundle",
|
||||
"apple_resource_group",
|
||||
)
|
||||
load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
"plist_fragment",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "DrawingUIMetalResources",
|
||||
srcs = glob([
|
||||
"MetalResources/**/*.*",
|
||||
]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "DrawingUIBundleInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.telegram.DrawingUI</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>PremiumUI</string>
|
||||
"""
|
||||
)
|
||||
|
||||
apple_resource_bundle(
|
||||
name = "DrawingUIBundle",
|
||||
infoplists = [
|
||||
":DrawingUIBundleInfoPlist",
|
||||
],
|
||||
resources = [
|
||||
":DrawingUIMetalResources",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "DrawingUIResources",
|
||||
srcs = glob([
|
||||
"Resources/**/*",
|
||||
], exclude = ["Resources/**/.*"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "DrawingUI",
|
||||
module_name = "DrawingUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
data = [
|
||||
":DrawingUIBundle",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/LegacyUI:LegacyUI",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/SegmentedControlNode:SegmentedControlNode",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/HexColor:HexColor",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||
"//submodules/Components/SheetComponent:SheetComponent",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/ImageBlur:ImageBlur",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
84
submodules/DrawingUI/MetalResources/Drawing.metal
Normal file
@@ -0,0 +1,84 @@
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct Vertex {
|
||||
float4 position [[position]];
|
||||
float2 tex_coord;
|
||||
};
|
||||
|
||||
struct Uniforms {
|
||||
float4x4 scaleMatrix;
|
||||
};
|
||||
|
||||
struct Point {
|
||||
float4 position [[position]];
|
||||
float4 color;
|
||||
float angle;
|
||||
float size [[point_size]];
|
||||
};
|
||||
|
||||
vertex Vertex vertex_render_target(constant Vertex *vertexes [[ buffer(0) ]],
|
||||
constant Uniforms &uniforms [[ buffer(1) ]],
|
||||
uint vid [[vertex_id]])
|
||||
{
|
||||
Vertex out = vertexes[vid];
|
||||
out.position = uniforms.scaleMatrix * out.position;
|
||||
return out;
|
||||
};
|
||||
|
||||
fragment float4 fragment_render_target(Vertex vertex_data [[ stage_in ]],
|
||||
texture2d<float> tex2d [[ texture(0) ]])
|
||||
{
|
||||
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||
float4 color = float4(tex2d.sample(textureSampler, vertex_data.tex_coord));
|
||||
return color;
|
||||
};
|
||||
|
||||
float2 transformPointCoord(float2 pointCoord, float a, float2 anchor) {
|
||||
float2 point20 = pointCoord - anchor;
|
||||
float x = point20.x * cos(a) - point20.y * sin(a);
|
||||
float y = point20.x * sin(a) + point20.y * cos(a);
|
||||
return float2(x, y) + anchor;
|
||||
}
|
||||
|
||||
|
||||
vertex Point vertex_point_func(constant Point *points [[ buffer(0) ]],
|
||||
constant Uniforms &uniforms [[ buffer(1) ]],
|
||||
uint vid [[ vertex_id ]])
|
||||
{
|
||||
Point out = points[vid];
|
||||
float2 pos = float2(out.position.x, out.position.y);
|
||||
out.position = uniforms.scaleMatrix * float4(pos, 0, 1);
|
||||
out.size = out.size;
|
||||
return out;
|
||||
};
|
||||
|
||||
fragment float4 fragment_point_func(Point point_data [[ stage_in ]],
|
||||
texture2d<float> tex2d [[ texture(0) ]],
|
||||
float2 pointCoord [[ point_coord ]])
|
||||
{
|
||||
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||
float2 tex_coord = transformPointCoord(pointCoord, point_data.angle, float2(0.5));
|
||||
float4 color = float4(tex2d.sample(textureSampler, tex_coord));
|
||||
return float4(point_data.color.rgb, color.a * point_data.color.a);
|
||||
};
|
||||
|
||||
// franment shader that applys original color of the texture
|
||||
fragment half4 fragment_point_func_original(Point point_data [[ stage_in ]],
|
||||
texture2d<float> tex2d [[ texture(0) ]],
|
||||
float2 pointCoord [[ point_coord ]])
|
||||
{
|
||||
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||
half4 color = half4(tex2d.sample(textureSampler, pointCoord));
|
||||
return half4(color.rgb, color.a * point_data.color.a);
|
||||
};
|
||||
|
||||
fragment float4 fragment_point_func_without_texture(Point point_data [[ stage_in ]],
|
||||
float2 pointCoord [[ point_coord ]])
|
||||
{
|
||||
float dist = length(pointCoord - float2(0.5));
|
||||
if (dist >= 0.5) {
|
||||
return float4(0);
|
||||
}
|
||||
return point_data.color;
|
||||
}
|
||||
BIN
submodules/DrawingUI/Resources/marker.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
submodules/DrawingUI/Resources/pencil.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
1
submodules/DrawingUI/Resources/shape_arrow.json
Normal file
@@ -0,0 +1 @@
|
||||
{"name" : "Arrow", "points" : [[68,222],[70,220],[73,218],[75,217],[77,215],[80,213],[82,212],[84,210],[87,209],[89,208],[92,206],[95,204],[101,201],[106,198],[112,194],[118,191],[124,187],[127,186],[132,183],[138,181],[141,180],[146,178],[154,173],[159,171],[161,170],[166,167],[168,167],[171,166],[174,164],[177,162],[180,160],[182,158],[183,156],[181,154],[178,153],[171,153],[164,153],[160,153],[150,154],[147,155],[141,157],[137,158],[135,158],[137,158],[140,157],[143,156],[151,154],[160,152],[170,149],[179,147],[185,145],[192,144],[196,144],[198,144],[200,144],[201,147],[199,149],[194,157],[191,160],[186,167],[180,176],[177,179],[171,187],[169,189],[165,194],[164,196]]}
|
||||
1
submodules/DrawingUI/Resources/shape_circle.json
Normal file
@@ -0,0 +1 @@
|
||||
{"name" : "Circle", "points" : [[127,141],[124,140],[120,139],[118,139],[116,139],[111,140],[109,141],[104,144],[100,147],[96,152],[93,157],[90,163],[87,169],[85,175],[83,181],[82,190],[82,195],[83,200],[84,205],[88,213],[91,216],[96,219],[103,222],[108,224],[111,224],[120,224],[133,223],[142,222],[152,218],[160,214],[167,210],[173,204],[178,198],[179,196],[182,188],[182,177],[178,167],[170,150],[163,138],[152,130],[143,129],[140,131],[129,136],[126,139]]}
|
||||
1
submodules/DrawingUI/Resources/shape_rectangle.json
Normal file
@@ -0,0 +1 @@
|
||||
{"name" : "Rectangle", "points" : [[78,149],[78,153],[78,157],[78,160],[79,162],[79,164],[79,167],[79,169],[79,173],[79,178],[79,183],[80,189],[80,193],[80,198],[80,202],[81,208],[81,210],[81,216],[82,222],[82,224],[82,227],[83,229],[83,231],[85,230],[88,232],[90,233],[92,232],[94,233],[99,232],[102,233],[106,233],[109,234],[117,235],[123,236],[126,236],[135,237],[142,238],[145,238],[152,238],[154,239],[165,238],[174,237],[179,236],[186,235],[191,235],[195,233],[197,233],[200,233],[201,235],[201,233],[199,231],[198,226],[198,220],[196,207],[195,195],[195,181],[195,173],[195,163],[194,155],[192,145],[192,143],[192,138],[191,135],[191,133],[191,130],[190,128],[188,129],[186,129],[181,132],[173,131],[162,131],[151,132],[149,132],[138,132],[136,132],[122,131],[120,131],[109,130],[107,130],[90,132],[81,133],[76,133]]}
|
||||
1
submodules/DrawingUI/Resources/shape_star.json
Normal file
@@ -0,0 +1 @@
|
||||
{"name" : "Star", "points" : [[75,250],[75,247],[77,244],[78,242],[79,239],[80,237],[82,234],[82,232],[84,229],[85,225],[87,222],[88,219],[89,216],[91,212],[92,208],[94,204],[95,201],[96,196],[97,194],[98,191],[100,185],[102,178],[104,173],[104,171],[105,164],[106,158],[107,156],[107,152],[108,145],[109,141],[110,139],[112,133],[113,131],[116,127],[117,125],[119,122],[121,121],[123,120],[125,122],[125,125],[127,130],[128,133],[131,143],[136,153],[140,163],[144,172],[145,175],[151,189],[156,201],[161,213],[166,225],[169,233],[171,236],[174,243],[177,247],[178,249],[179,251],[180,253],[180,255],[179,257],[177,257],[174,255],[169,250],[164,247],[160,245],[149,238],[138,230],[127,221],[124,220],[112,212],[110,210],[96,201],[84,195],[74,190],[64,182],[55,175],[51,172],[49,170],[51,169],[56,169],[66,169],[78,168],[92,166],[107,164],[123,161],[140,162],[156,162],[171,160],[173,160],[186,160],[195,160],[198,161],[203,163],[208,163],[206,164],[200,167],[187,172],[174,179],[172,181],[153,192],[137,201],[123,211],[112,220],[99,229],[90,237],[80,244],[73,250],[69,254],[69,252]]}
|
||||
2482
submodules/DrawingUI/Sources/ColorPickerScreen.swift
Normal file
308
submodules/DrawingUI/Sources/ConcaveHull.swift
Normal file
@@ -0,0 +1,308 @@
|
||||
import Foundation
|
||||
|
||||
private func intersect(seg1: [CGPoint], seg2: [CGPoint]) -> Bool {
|
||||
func ccw(_ seg1: CGPoint, _ seg2: CGPoint, _ seg3: CGPoint) -> Bool {
|
||||
let ccw = ((seg3.y - seg1.y) * (seg2.x - seg1.x)) - ((seg2.y - seg1.y) * (seg3.x - seg1.x))
|
||||
return ccw > 0 ? true : ccw < 0 ? false : true
|
||||
}
|
||||
let segment1 = seg1[0]
|
||||
let segment2 = seg1[1]
|
||||
let segment3 = seg2[0]
|
||||
let segment4 = seg2[1]
|
||||
return ccw(segment1, segment3, segment4) != ccw(segment2, segment3, segment4)
|
||||
&& ccw(segment1, segment2, segment3) != ccw(segment1, segment2, segment4)
|
||||
}
|
||||
|
||||
private func convex(points: [CGPoint]) -> [CGPoint] {
|
||||
func cross(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double {
|
||||
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
|
||||
}
|
||||
|
||||
func upperTangent(_ points: [CGPoint]) -> [CGPoint] {
|
||||
var lower: [CGPoint] = []
|
||||
for point in points {
|
||||
while lower.count >= 2 && (cross(lower[lower.count - 2], lower[lower.count - 1], point) <= 0) {
|
||||
_ = lower.popLast()
|
||||
}
|
||||
lower.append(point)
|
||||
}
|
||||
_ = lower.popLast()
|
||||
return lower
|
||||
}
|
||||
|
||||
func lowerTangent(_ points: [CGPoint]) -> [CGPoint] {
|
||||
let reversed = points.reversed()
|
||||
var upper: [CGPoint] = []
|
||||
for point in reversed {
|
||||
while upper.count >= 2 && (cross(upper[upper.count - 2], upper[upper.count - 1], point) <= 0) {
|
||||
_ = upper.popLast()
|
||||
}
|
||||
upper.append(point)
|
||||
}
|
||||
_ = upper.popLast()
|
||||
return upper
|
||||
}
|
||||
|
||||
var convex: [CGPoint] = []
|
||||
convex.append(contentsOf: upperTangent(points))
|
||||
convex.append(contentsOf: lowerTangent(points))
|
||||
return convex
|
||||
}
|
||||
|
||||
private class Grid {
|
||||
var cells = [Int: [Int: [CGPoint]]]()
|
||||
var cellSize: Double = 0
|
||||
|
||||
init(_ points: [CGPoint], _ cellSize: Double) {
|
||||
self.cellSize = cellSize
|
||||
for point in points {
|
||||
let cellXY = point2CellXY(point)
|
||||
let x = cellXY[0]
|
||||
let y = cellXY[1]
|
||||
if self.cells[x] == nil {
|
||||
self.cells[x] = [Int: [CGPoint]]()
|
||||
}
|
||||
if self.cells[x]![y] == nil {
|
||||
self.cells[x]![y] = [CGPoint]()
|
||||
}
|
||||
self.cells[x]![y]!.append(point)
|
||||
}
|
||||
}
|
||||
|
||||
func point2CellXY(_ point: CGPoint) -> [Int] {
|
||||
let x = Int(point.x / self.cellSize)
|
||||
let y = Int(point.y / self.cellSize)
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
func extendBbox(_ bbox: [Double], _ scaleFactor: Double) -> [Double] {
|
||||
return [
|
||||
bbox[0] - (scaleFactor * self.cellSize),
|
||||
bbox[1] - (scaleFactor * self.cellSize),
|
||||
bbox[2] + (scaleFactor * self.cellSize),
|
||||
bbox[3] + (scaleFactor * self.cellSize)
|
||||
]
|
||||
}
|
||||
|
||||
func removePoint(_ point: CGPoint) {
|
||||
let cellXY = point2CellXY(point)
|
||||
let cell = self.cells[cellXY[0]]![cellXY[1]]!
|
||||
var pointIdxInCell = 0
|
||||
for idx in 0 ..< cell.count {
|
||||
if cell[idx].x == point.x && cell[idx].y == point.y {
|
||||
pointIdxInCell = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
self.cells[cellXY[0]]![cellXY[1]]!.remove(at: pointIdxInCell)
|
||||
}
|
||||
|
||||
func rangePoints(_ bbox: [Double]) -> [CGPoint] {
|
||||
let tlCellXY = point2CellXY(CGPoint(x: bbox[0], y: bbox[1]))
|
||||
let brCellXY = point2CellXY(CGPoint(x: bbox[2], y: bbox[3]))
|
||||
var points: [CGPoint] = []
|
||||
for x in tlCellXY[0]..<brCellXY[0]+1 {
|
||||
for y in tlCellXY[1]..<brCellXY[1]+1 {
|
||||
points += cellPoints(x, y)
|
||||
}
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
func cellPoints(_ xAbs: Int, _ yOrd: Int) -> [CGPoint] {
|
||||
if let x = self.cells[xAbs] {
|
||||
if let y = x[yOrd] {
|
||||
return y
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private let maxConcaveAngleCos = cos(90.0 / (180.0 / Double.pi))
|
||||
|
||||
private func filterDuplicates(_ pointSet: [CGPoint]) -> [CGPoint] {
|
||||
let sortedSet = sortByX(pointSet)
|
||||
return sortedSet.filter { (point: CGPoint) -> Bool in
|
||||
let index = pointSet.firstIndex(where: {(idx: CGPoint) -> Bool in
|
||||
return idx.x == point.x && idx.y == point.y
|
||||
})
|
||||
if index == 0 {
|
||||
return true
|
||||
} else {
|
||||
let prevEl = pointSet[index! - 1]
|
||||
if prevEl.x != point.x || prevEl.y != point.y {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sortByX(_ pointSet: [CGPoint]) -> [CGPoint] {
|
||||
return pointSet.sorted(by: { (lhs, rhs) -> Bool in
|
||||
if lhs.x == rhs.x {
|
||||
return lhs.y < rhs.y
|
||||
} else {
|
||||
return lhs.x < rhs.x
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func squaredLength(_ a: CGPoint, _ b: CGPoint) -> Double {
|
||||
return pow(b.x - a.x, 2) + pow(b.y - a.y, 2)
|
||||
}
|
||||
|
||||
private func cosFunc(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double {
|
||||
let aShifted = [a.x - o.x, a.y - o.y]
|
||||
let bShifted = [b.x - o.x, b.y - o.y]
|
||||
let sqALen = squaredLength(o, a)
|
||||
let sqBLen = squaredLength(o, b)
|
||||
let dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1]
|
||||
return dot / sqrt(sqALen * sqBLen)
|
||||
}
|
||||
|
||||
private func intersectFunc(_ segment: [CGPoint], _ pointSet: [CGPoint]) -> Bool {
|
||||
for idx in 0..<pointSet.count - 1 {
|
||||
let seg = [pointSet[idx], pointSet[idx + 1]]
|
||||
if segment[0].x == seg[0].x && segment[0].y == seg[0].y ||
|
||||
segment[0].x == seg[1].x && segment[0].y == seg[1].y {
|
||||
continue
|
||||
}
|
||||
if intersect(seg1: segment, seg2: seg) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func occupiedAreaFunc(_ points: [CGPoint]) -> CGPoint {
|
||||
var minX = Double.infinity
|
||||
var minY = Double.infinity
|
||||
var maxX = -Double.infinity
|
||||
var maxY = -Double.infinity
|
||||
for idx in 0 ..< points.reversed().count {
|
||||
if points[idx].x < minX {
|
||||
minX = points[idx].x
|
||||
}
|
||||
if points[idx].y < minY {
|
||||
minY = points[idx].y
|
||||
}
|
||||
if points[idx].x > maxX {
|
||||
maxX = points[idx].x
|
||||
}
|
||||
if points[idx].y > maxY {
|
||||
maxY = points[idx].y
|
||||
}
|
||||
}
|
||||
return CGPoint(x: maxX - minX, y: maxY - minY)
|
||||
}
|
||||
|
||||
private func bBoxAroundFunc(_ edge: [CGPoint]) -> [Double] {
|
||||
return [min(edge[0].x, edge[1].x),
|
||||
min(edge[0].y, edge[1].y),
|
||||
max(edge[0].x, edge[1].x),
|
||||
max(edge[0].y, edge[1].y)]
|
||||
}
|
||||
|
||||
private func midPointFunc(_ edge: [CGPoint], _ innerPoints: [CGPoint], _ convex: [CGPoint]) -> CGPoint? {
|
||||
var point: CGPoint?
|
||||
var angle1Cos = maxConcaveAngleCos
|
||||
var angle2Cos = maxConcaveAngleCos
|
||||
var a1Cos: Double = 0
|
||||
var a2Cos: Double = 0
|
||||
for innerPoint in innerPoints {
|
||||
a1Cos = cosFunc(edge[0], edge[1], innerPoint)
|
||||
a2Cos = cosFunc(edge[1], edge[0], innerPoint)
|
||||
if a1Cos > angle1Cos &&
|
||||
a2Cos > angle2Cos &&
|
||||
!intersectFunc([edge[0], innerPoint], convex) &&
|
||||
!intersectFunc([edge[1], innerPoint], convex) {
|
||||
angle1Cos = a1Cos
|
||||
angle2Cos = a2Cos
|
||||
point = innerPoint
|
||||
}
|
||||
}
|
||||
return point
|
||||
}
|
||||
|
||||
private func concaveFunc(_ convex: inout [CGPoint], _ maxSqEdgeLen: Double, _ maxSearchArea: [Double], _ grid: Grid, _ edgeSkipList: inout [String: Bool]) -> [CGPoint] {
|
||||
var edge: [CGPoint]
|
||||
var keyInSkipList: String = ""
|
||||
var scaleFactor: Double
|
||||
var midPoint: CGPoint?
|
||||
var bBoxAround: [Double]
|
||||
var bBoxWidth: Double = 0
|
||||
var bBoxHeight: Double = 0
|
||||
var midPointInserted: Bool = false
|
||||
|
||||
for idx in 0..<convex.count - 1 {
|
||||
edge = [convex[idx], convex[idx+1]]
|
||||
keyInSkipList = edge[0].key.appending(", ").appending(edge[1].key)
|
||||
|
||||
scaleFactor = 0
|
||||
bBoxAround = bBoxAroundFunc(edge)
|
||||
|
||||
if squaredLength(edge[0], edge[1]) < maxSqEdgeLen || edgeSkipList[keyInSkipList] == true {
|
||||
continue
|
||||
}
|
||||
|
||||
repeat {
|
||||
bBoxAround = grid.extendBbox(bBoxAround, scaleFactor)
|
||||
bBoxWidth = bBoxAround[2] - bBoxAround[0]
|
||||
bBoxHeight = bBoxAround[3] - bBoxAround[1]
|
||||
midPoint = midPointFunc(edge, grid.rangePoints(bBoxAround), convex)
|
||||
scaleFactor += 1
|
||||
} while midPoint == nil && (maxSearchArea[0] > bBoxWidth || maxSearchArea[1] > bBoxHeight)
|
||||
|
||||
if bBoxWidth >= maxSearchArea[0] && bBoxHeight >= maxSearchArea[1] {
|
||||
edgeSkipList[keyInSkipList] = true
|
||||
}
|
||||
if let midPoint = midPoint {
|
||||
convex.insert(midPoint, at: idx + 1)
|
||||
grid.removePoint(midPoint)
|
||||
midPointInserted = true
|
||||
}
|
||||
}
|
||||
|
||||
if midPointInserted {
|
||||
return concaveFunc(&convex, maxSqEdgeLen, maxSearchArea, grid, &edgeSkipList)
|
||||
}
|
||||
|
||||
return convex
|
||||
}
|
||||
|
||||
private extension CGPoint {
|
||||
var key: String {
|
||||
return "\(self.x),\(self.y)"
|
||||
}
|
||||
}
|
||||
|
||||
func getHull(_ points: [CGPoint], concavity: Double) -> [CGPoint] {
|
||||
let points = filterDuplicates(points)
|
||||
let occupiedArea = occupiedAreaFunc(points)
|
||||
let maxSearchArea: [Double] = [
|
||||
occupiedArea.x * 0.6,
|
||||
occupiedArea.y * 0.6
|
||||
]
|
||||
|
||||
var convex = convex(points: points)
|
||||
|
||||
var innerPoints = points.filter { (point: CGPoint) -> Bool in
|
||||
let idx = convex.firstIndex(where: { (idx: CGPoint) -> Bool in
|
||||
return idx.x == point.x && idx.y == point.y
|
||||
})
|
||||
return idx == nil
|
||||
}
|
||||
|
||||
innerPoints.sort(by: { (lhs: CGPoint, rhs: CGPoint) -> Bool in
|
||||
return lhs.x == rhs.x ? lhs.y > rhs.y : lhs.x > rhs.x
|
||||
})
|
||||
|
||||
let cellSize = ceil(occupiedArea.x * occupiedArea.y / Double(points.count))
|
||||
let grid = Grid(innerPoints, cellSize)
|
||||
|
||||
var skipList: [String: Bool] = [String: Bool]()
|
||||
return concaveFunc(&convex, pow(concavity, 2), maxSearchArea, grid, &skipList)
|
||||
}
|
||||
389
submodules/DrawingUI/Sources/DrawingBubbleEntity.swift
Normal file
@@ -0,0 +1,389 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AccountContext
|
||||
|
||||
final class DrawingBubbleEntity: DrawingEntity {
|
||||
public enum DrawType {
|
||||
case fill
|
||||
case stroke
|
||||
}
|
||||
|
||||
let uuid: UUID
|
||||
let isAnimated: Bool
|
||||
|
||||
var drawType: DrawType
|
||||
var color: DrawingColor
|
||||
var lineWidth: CGFloat
|
||||
|
||||
var referenceDrawingSize: CGSize
|
||||
var position: CGPoint
|
||||
var size: CGSize
|
||||
var rotation: CGFloat
|
||||
var tailPosition: CGPoint
|
||||
|
||||
init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = false
|
||||
|
||||
self.drawType = drawType
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
|
||||
self.referenceDrawingSize = .zero
|
||||
self.position = .zero
|
||||
self.size = CGSize(width: 1.0, height: 1.0)
|
||||
self.rotation = 0.0
|
||||
self.tailPosition = CGPoint(x: 0.16, y: 0.18)
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
newEntity.position = self.position
|
||||
newEntity.size = self.size
|
||||
newEntity.rotation = self.rotation
|
||||
return newEntity
|
||||
}
|
||||
|
||||
weak var currentEntityView: DrawingEntityView?
|
||||
func makeView(context: AccountContext) -> DrawingEntityView {
|
||||
let entityView = DrawingBubbleEntityView(context: context, entity: self)
|
||||
self.currentEntityView = entityView
|
||||
return entityView
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingBubbleEntityView: DrawingEntityView {
|
||||
private var bubbleEntity: DrawingBubbleEntity {
|
||||
return self.entity as! DrawingBubbleEntity
|
||||
}
|
||||
|
||||
private var currentSize: CGSize?
|
||||
private var currentTailPosition: CGPoint?
|
||||
|
||||
private let shapeLayer = SimpleShapeLayer()
|
||||
|
||||
init(context: AccountContext, entity: DrawingBubbleEntity) {
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.layer.addSublayer(self.shapeLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func update(animated: Bool) {
|
||||
let size = self.bubbleEntity.size
|
||||
|
||||
self.center = self.bubbleEntity.position
|
||||
self.bounds = CGRect(origin: .zero, size: size)
|
||||
self.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation)
|
||||
|
||||
if size != self.currentSize || self.bubbleEntity.tailPosition != self.currentTailPosition {
|
||||
self.currentSize = size
|
||||
self.currentTailPosition = self.bubbleEntity.tailPosition
|
||||
self.shapeLayer.frame = self.bounds
|
||||
|
||||
let cornerRadius = max(10.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.066)
|
||||
let smallCornerRadius = max(5.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.016)
|
||||
let tailWidth = max(5.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1)
|
||||
|
||||
self.shapeLayer.path = CGPath.bubble(in: CGRect(origin: .zero, size: size), cornerRadius: cornerRadius, smallCornerRadius: smallCornerRadius, tailPosition: self.bubbleEntity.tailPosition, tailWidth: tailWidth)
|
||||
}
|
||||
|
||||
switch self.bubbleEntity.drawType {
|
||||
case .fill:
|
||||
self.shapeLayer.fillColor = self.bubbleEntity.color.toCGColor()
|
||||
self.shapeLayer.strokeColor = UIColor.clear.cgColor
|
||||
case .stroke:
|
||||
let minLineWidth = max(10.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.02)
|
||||
let maxLineWidth = max(10.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1)
|
||||
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.bubbleEntity.lineWidth
|
||||
|
||||
self.shapeLayer.fillColor = UIColor.clear.cgColor
|
||||
self.shapeLayer.strokeColor = self.bubbleEntity.color.toCGColor()
|
||||
self.shapeLayer.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
fileprivate var visualLineWidth: CGFloat {
|
||||
return self.shapeLayer.lineWidth
|
||||
}
|
||||
|
||||
override func precisePoint(inside point: CGPoint) -> Bool {
|
||||
if case .stroke = self.bubbleEntity.drawType, var path = self.shapeLayer.path {
|
||||
path = path.copy(strokingWithWidth: 20.0, lineCap: .square, lineJoin: .bevel, miterLimit: 0.0)
|
||||
if path.contains(point) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return super.precisePoint(inside: point)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateSelectionView() {
|
||||
super.updateSelectionView()
|
||||
|
||||
guard let selectionView = self.selectionView as? DrawingBubbleEntititySelectionView else {
|
||||
return
|
||||
}
|
||||
|
||||
// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
// selectionView.scale = scale
|
||||
|
||||
selectionView.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation)
|
||||
selectionView.setNeedsLayout()
|
||||
}
|
||||
|
||||
override func makeSelectionView() -> DrawingEntitySelectionView {
|
||||
if let selectionView = self.selectionView {
|
||||
return selectionView
|
||||
}
|
||||
let selectionView = DrawingBubbleEntititySelectionView()
|
||||
selectionView.entityView = self
|
||||
return selectionView
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate {
|
||||
private let leftHandle = SimpleShapeLayer()
|
||||
private let topLeftHandle = SimpleShapeLayer()
|
||||
private let topHandle = SimpleShapeLayer()
|
||||
private let topRightHandle = SimpleShapeLayer()
|
||||
private let rightHandle = SimpleShapeLayer()
|
||||
private let bottomLeftHandle = SimpleShapeLayer()
|
||||
private let bottomHandle = SimpleShapeLayer()
|
||||
private let bottomRightHandle = SimpleShapeLayer()
|
||||
private let tailHandle = SimpleShapeLayer()
|
||||
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
|
||||
let handles = [
|
||||
self.leftHandle,
|
||||
self.topLeftHandle,
|
||||
self.topHandle,
|
||||
self.topRightHandle,
|
||||
self.rightHandle,
|
||||
self.bottomLeftHandle,
|
||||
self.bottomHandle,
|
||||
self.bottomRightHandle,
|
||||
self.tailHandle
|
||||
]
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
self.isOpaque = false
|
||||
|
||||
for handle in handles {
|
||||
handle.bounds = handleBounds
|
||||
if handle === self.tailHandle {
|
||||
handle.fillColor = UIColor(rgb: 0x00ff00).cgColor
|
||||
} else {
|
||||
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
|
||||
}
|
||||
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
|
||||
handle.rasterizationScale = UIScreen.main.scale
|
||||
handle.shouldRasterize = true
|
||||
|
||||
self.layer.addSublayer(handle)
|
||||
}
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var scale: CGFloat = 1.0 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override var selectionInset: CGFloat {
|
||||
return 5.5
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
|
||||
return
|
||||
}
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
|
||||
var updatedSize = entity.size
|
||||
var updatedPosition = entity.position
|
||||
var updatedTailPosition = entity.tailPosition
|
||||
|
||||
if self.currentHandle === self.leftHandle {
|
||||
updatedSize.width -= delta.x
|
||||
updatedPosition.x -= delta.x * -0.5
|
||||
} else if self.currentHandle === self.rightHandle {
|
||||
updatedSize.width += delta.x
|
||||
updatedPosition.x += delta.x * 0.5
|
||||
} else if self.currentHandle === self.topHandle {
|
||||
updatedSize.height -= delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.bottomHandle {
|
||||
updatedSize.height += delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.topLeftHandle {
|
||||
updatedSize.width -= delta.x
|
||||
updatedPosition.x -= delta.x * -0.5
|
||||
updatedSize.height -= delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.topRightHandle {
|
||||
updatedSize.width += delta.x
|
||||
updatedPosition.x += delta.x * 0.5
|
||||
updatedSize.height -= delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.bottomLeftHandle {
|
||||
updatedSize.width -= delta.x
|
||||
updatedPosition.x -= delta.x * -0.5
|
||||
updatedSize.height += delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.bottomRightHandle {
|
||||
updatedSize.width += delta.x
|
||||
updatedPosition.x += delta.x * 0.5
|
||||
updatedSize.height += delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.tailHandle {
|
||||
updatedTailPosition = CGPoint(x: max(0.0, min(1.0, updatedTailPosition.x + delta.x / updatedSize.width)), y: max(0.0, min(updatedSize.height, updatedTailPosition.y + delta.y)))
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
}
|
||||
|
||||
entity.size = updatedSize
|
||||
entity.position = updatedPosition
|
||||
entity.tailPosition = updatedTailPosition
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale)
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let rotation = gestureRecognizer.rotation
|
||||
entity.rotation += rotation
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.rotation = 0.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) || self.tailHandle.frame.contains(point)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
let inset = self.selectionInset
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
|
||||
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
|
||||
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
|
||||
let lineWidth = (1.0 + UIScreenPixel) / self.scale
|
||||
|
||||
let handles = [
|
||||
self.leftHandle,
|
||||
self.topLeftHandle,
|
||||
self.topHandle,
|
||||
self.topRightHandle,
|
||||
self.rightHandle,
|
||||
self.bottomLeftHandle,
|
||||
self.bottomHandle,
|
||||
self.bottomRightHandle,
|
||||
self.tailHandle
|
||||
]
|
||||
|
||||
for handle in handles {
|
||||
handle.path = handlePath
|
||||
handle.bounds = bounds
|
||||
handle.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
self.topLeftHandle.position = CGPoint(x: inset, y: inset)
|
||||
self.topHandle.position = CGPoint(x: self.bounds.midX, y: inset)
|
||||
self.topRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: inset)
|
||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||
self.bottomLeftHandle.position = CGPoint(x: inset, y: self.bounds.maxY - inset)
|
||||
self.bottomHandle.position = CGPoint(x: self.bounds.midX, y: self.bounds.maxY - inset)
|
||||
self.bottomRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.maxY - inset)
|
||||
|
||||
let selectionScale = (self.bounds.width - inset * 2.0) / (max(0.001, entity.size.width))
|
||||
self.tailHandle.position = CGPoint(x: inset + (self.bounds.width - inset * 2.0) * entity.tailPosition.x, y: self.bounds.height - inset + entity.tailPosition.y * selectionScale)
|
||||
}
|
||||
|
||||
var isTracking: Bool {
|
||||
return gestureIsTracking(self.panGestureRecognizer)
|
||||
}
|
||||
}
|
||||
477
submodules/DrawingUI/Sources/DrawingEntitiesView.swift
Normal file
@@ -0,0 +1,477 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import LegacyComponents
|
||||
import AccountContext
|
||||
|
||||
protocol DrawingEntity: AnyObject {
|
||||
var uuid: UUID { get }
|
||||
var isAnimated: Bool { get }
|
||||
var center: CGPoint { get }
|
||||
|
||||
var lineWidth: CGFloat { get set }
|
||||
var color: DrawingColor { get set }
|
||||
|
||||
func duplicate() -> DrawingEntity
|
||||
|
||||
var currentEntityView: DrawingEntityView? { get }
|
||||
func makeView(context: AccountContext) -> DrawingEntityView
|
||||
}
|
||||
|
||||
public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
private let context: AccountContext
|
||||
private let size: CGSize
|
||||
|
||||
weak var selectionContainerView: DrawingSelectionContainerView?
|
||||
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer!
|
||||
private(set) var selectedEntityView: DrawingEntityView?
|
||||
|
||||
public var hasSelectionChanged: (Bool) -> Void = { _ in }
|
||||
var selectionChanged: (DrawingEntity?) -> Void = { _ in }
|
||||
var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in }
|
||||
|
||||
init(context: AccountContext, size: CGSize, entities: [DrawingEntity] = []) {
|
||||
self.context = context
|
||||
self.size = size
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
for entity in entities {
|
||||
self.add(entity)
|
||||
}
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(tapGestureRecognizer)
|
||||
self.tapGestureRecognizer = tapGestureRecognizer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var entities: [DrawingEntity] {
|
||||
var entities: [DrawingEntity] = []
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
entities.append(view.entity)
|
||||
}
|
||||
return entities
|
||||
}
|
||||
|
||||
private func startPosition(relativeTo entity: DrawingEntity?) -> CGPoint {
|
||||
let offsetLength = round(self.size.width * 0.1)
|
||||
let offset = CGPoint(x: offsetLength, y: offsetLength)
|
||||
if let entity = entity {
|
||||
return entity.center.offsetBy(dx: offset.x, dy: offset.y)
|
||||
} else {
|
||||
let minimalDistance: CGFloat = round(offsetLength * 0.5)
|
||||
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0) // place good here
|
||||
|
||||
while true {
|
||||
var occupied = false
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
let location = view.entity.center
|
||||
let distance = sqrt(pow(location.x - position.x, 2) + pow(location.y - position.y, 2))
|
||||
if distance < minimalDistance {
|
||||
occupied = true
|
||||
}
|
||||
}
|
||||
if !occupied {
|
||||
break
|
||||
} else {
|
||||
position = position.offsetBy(dx: offset.x, dy: offset.y)
|
||||
}
|
||||
}
|
||||
return position
|
||||
}
|
||||
}
|
||||
|
||||
private func newEntitySize() -> CGSize {
|
||||
let width = round(self.size.width * 0.5)
|
||||
|
||||
return CGSize(width: width, height: width)
|
||||
}
|
||||
|
||||
func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
||||
let center = self.startPosition(relativeTo: relativeTo)
|
||||
if let shape = entity as? DrawingSimpleShapeEntity {
|
||||
shape.position = center
|
||||
|
||||
if setup {
|
||||
let size = self.newEntitySize()
|
||||
shape.referenceDrawingSize = self.size
|
||||
if shape.shapeType == .star {
|
||||
shape.size = size
|
||||
} else {
|
||||
shape.size = CGSize(width: size.width, height: round(size.height * 0.75))
|
||||
}
|
||||
}
|
||||
} else if let vector = entity as? DrawingVectorEntity {
|
||||
if setup {
|
||||
vector.drawingSize = self.size
|
||||
vector.referenceDrawingSize = self.size
|
||||
vector.start = CGPoint(x: center.x * 0.5, y: center.y)
|
||||
vector.mid = (0.5, 0.0)
|
||||
vector.end = CGPoint(x: center.x * 1.5, y: center.y)
|
||||
vector.type = .oneSidedArrow
|
||||
}
|
||||
} else if let sticker = entity as? DrawingStickerEntity {
|
||||
sticker.position = center
|
||||
if setup {
|
||||
sticker.referenceDrawingSize = self.size
|
||||
sticker.scale = 1.0
|
||||
}
|
||||
} else if let bubble = entity as? DrawingBubbleEntity {
|
||||
bubble.position = center
|
||||
if setup {
|
||||
let size = self.newEntitySize()
|
||||
bubble.referenceDrawingSize = self.size
|
||||
bubble.size = CGSize(width: size.width, height: round(size.height * 0.7))
|
||||
bubble.tailPosition = CGPoint(x: 0.16, y: size.height * 0.18)
|
||||
}
|
||||
} else if let text = entity as? DrawingTextEntity {
|
||||
text.position = center
|
||||
if setup {
|
||||
text.referenceDrawingSize = self.size
|
||||
text.width = floor(self.size.width * 0.9)
|
||||
text.fontSize = 0.4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func add(_ entity: DrawingEntity) -> DrawingEntityView {
|
||||
let view = entity.makeView(context: self.context)
|
||||
view.containerView = self
|
||||
view.update()
|
||||
self.addSubview(view)
|
||||
return view
|
||||
}
|
||||
|
||||
func duplicate(_ entity: DrawingEntity) -> DrawingEntity {
|
||||
let newEntity = entity.duplicate()
|
||||
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity)
|
||||
|
||||
let view = newEntity.makeView(context: self.context)
|
||||
view.containerView = self
|
||||
view.update()
|
||||
self.addSubview(view)
|
||||
return newEntity
|
||||
}
|
||||
|
||||
func remove(uuid: UUID) {
|
||||
if let view = self.getView(for: uuid) {
|
||||
if self.selectedEntityView === view {
|
||||
self.selectedEntityView?.removeFromSuperview()
|
||||
self.selectedEntityView = nil
|
||||
self.selectionChanged(nil)
|
||||
self.hasSelectionChanged(false)
|
||||
}
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
self.selectionChanged(nil)
|
||||
self.hasSelectionChanged(false)
|
||||
}
|
||||
|
||||
func bringToFront(uuid: UUID) {
|
||||
if let view = self.getView(for: uuid) {
|
||||
self.bringSubviewToFront(view)
|
||||
}
|
||||
}
|
||||
|
||||
func getView(for uuid: UUID) -> DrawingEntityView? {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
if view.entity.uuid == uuid {
|
||||
return view
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func play() {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.play()
|
||||
}
|
||||
}
|
||||
|
||||
public func pause() {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.pause()
|
||||
}
|
||||
}
|
||||
|
||||
public func seek(to timestamp: Double) {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.seek(to: timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
public func resetToStart() {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.resetToStart()
|
||||
}
|
||||
}
|
||||
|
||||
public func updateVisibility(_ visibility: Bool) {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.updateVisibility(visibility)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognzier: UITapGestureRecognizer) {
|
||||
let location = gestureRecognzier.location(in: self)
|
||||
|
||||
var intersectedViews: [DrawingEntityView] = []
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
if view.precisePoint(inside: self.convert(location, to: view)) {
|
||||
intersectedViews.append(view)
|
||||
}
|
||||
}
|
||||
|
||||
if let entityView = intersectedViews.last {
|
||||
self.selectEntity(entityView.entity)
|
||||
}
|
||||
}
|
||||
|
||||
func selectEntity(_ entity: DrawingEntity?) {
|
||||
if entity !== self.selectedEntityView?.entity {
|
||||
if let selectedEntityView = self.selectedEntityView {
|
||||
if let textEntityView = selectedEntityView as? DrawingTextEntityView, textEntityView.isEditing {
|
||||
if entity == nil {
|
||||
textEntityView.endEditing()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.selectedEntityView = nil
|
||||
if let selectionView = selectedEntityView.selectionView {
|
||||
selectedEntityView.selectionView = nil
|
||||
selectionView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let entity = entity, let entityView = self.getView(for: entity.uuid) {
|
||||
self.selectedEntityView = entityView
|
||||
|
||||
let selectionView = entityView.makeSelectionView()
|
||||
selectionView.tapped = { [weak self, weak entityView] in
|
||||
if let strongSelf = self, let entityView = entityView {
|
||||
strongSelf.requestedMenuForEntityView(entityView, strongSelf.subviews.last === entityView)
|
||||
}
|
||||
}
|
||||
entityView.selectionView = selectionView
|
||||
self.selectionContainerView?.addSubview(selectionView)
|
||||
entityView.update()
|
||||
}
|
||||
|
||||
self.selectionChanged(self.selectedEntityView?.entity)
|
||||
self.hasSelectionChanged(self.selectedEntityView != nil)
|
||||
}
|
||||
|
||||
var isTrackingAnyEntity: Bool {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
if view.isTracking {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return super.point(inside: point, with: event)
|
||||
}
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result === self {
|
||||
return nil
|
||||
}
|
||||
if let result = result as? DrawingEntityView, !result.precisePoint(inside: self.convert(point, to: result)) {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func clearSelection() {
|
||||
self.selectEntity(nil)
|
||||
}
|
||||
|
||||
public func onZoom() {
|
||||
self.selectedEntityView?.updateSelectionView()
|
||||
}
|
||||
|
||||
public var hasSelection: Bool {
|
||||
return self.selectedEntityView != nil
|
||||
}
|
||||
|
||||
public func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer!) {
|
||||
if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView {
|
||||
selectionView.handlePinch(gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
public func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer!) {
|
||||
if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView {
|
||||
selectionView.handleRotate(gestureRecognizer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DrawingEntityView: UIView {
|
||||
let context: AccountContext
|
||||
let entity: DrawingEntity
|
||||
var isTracking = false
|
||||
|
||||
weak var selectionView: DrawingEntitySelectionView?
|
||||
weak var containerView: DrawingEntitiesView?
|
||||
|
||||
init(context: AccountContext, entity: DrawingEntity) {
|
||||
self.context = context
|
||||
self.entity = entity
|
||||
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let selectionView = self.selectionView {
|
||||
selectionView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
var selectionBounds: CGRect {
|
||||
return self.bounds
|
||||
}
|
||||
|
||||
func play() {
|
||||
|
||||
}
|
||||
|
||||
func pause() {
|
||||
|
||||
}
|
||||
|
||||
func seek(to timestamp: Double) {
|
||||
|
||||
}
|
||||
|
||||
func resetToStart() {
|
||||
|
||||
}
|
||||
|
||||
func updateVisibility(_ visibility: Bool) {
|
||||
|
||||
}
|
||||
|
||||
func update(animated: Bool = false) {
|
||||
self.updateSelectionView()
|
||||
}
|
||||
|
||||
func updateSelectionView() {
|
||||
guard let selectionView = self.selectionView else {
|
||||
return
|
||||
}
|
||||
self.pushIdentityTransformForMeasurement()
|
||||
|
||||
selectionView.transform = .identity
|
||||
let bounds = self.selectionBounds
|
||||
let center = bounds.center
|
||||
|
||||
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
selectionView.center = self.convert(center, to: selectionView.superview)
|
||||
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: bounds.width * scale + selectionView.selectionInset * 2.0, height: bounds.height * scale + selectionView.selectionInset * 2.0))
|
||||
|
||||
self.popIdentityTransformForMeasurement()
|
||||
}
|
||||
|
||||
private var realTransform: CGAffineTransform?
|
||||
func pushIdentityTransformForMeasurement() {
|
||||
guard self.realTransform == nil else {
|
||||
return
|
||||
}
|
||||
self.realTransform = self.transform
|
||||
self.transform = .identity
|
||||
}
|
||||
|
||||
func popIdentityTransformForMeasurement() {
|
||||
guard let realTransform = self.realTransform else {
|
||||
return
|
||||
}
|
||||
self.transform = realTransform
|
||||
self.realTransform = nil
|
||||
}
|
||||
|
||||
public func precisePoint(inside point: CGPoint) -> Bool {
|
||||
return self.point(inside: point, with: nil)
|
||||
}
|
||||
|
||||
func makeSelectionView() -> DrawingEntitySelectionView {
|
||||
if let selectionView = self.selectionView {
|
||||
return selectionView
|
||||
}
|
||||
return DrawingEntitySelectionView()
|
||||
}
|
||||
}
|
||||
|
||||
let entitySelectionViewHandleSize = CGSize(width: 44.0, height: 44.0)
|
||||
public class DrawingEntitySelectionView: UIView {
|
||||
weak var entityView: DrawingEntityView?
|
||||
|
||||
var tapped: () -> Void = { }
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
self.tapped()
|
||||
}
|
||||
|
||||
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
||||
}
|
||||
|
||||
@objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
|
||||
}
|
||||
|
||||
var selectionInset: CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
public class DrawingSelectionContainerView: UIView {
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result === self {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
let result = super.point(inside: point, with: event)
|
||||
if !result {
|
||||
for subview in self.subviews {
|
||||
let subpoint = self.convert(point, to: subview)
|
||||
if subview.point(inside: subpoint, with: event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
1270
submodules/DrawingUI/Sources/DrawingGesture.swift
Normal file
554
submodules/DrawingUI/Sources/DrawingMetalView.swift
Normal file
@@ -0,0 +1,554 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import QuartzCore
|
||||
import MetalKit
|
||||
import AppBundle
|
||||
|
||||
public final class DrawingMetalView: MTKView {
|
||||
private let size: CGSize
|
||||
|
||||
private let commandQueue: MTLCommandQueue
|
||||
fileprivate let library: MTLLibrary
|
||||
private var pipelineState: MTLRenderPipelineState!
|
||||
|
||||
fileprivate var drawable: Drawable?
|
||||
|
||||
private var render_target_vertex: MTLBuffer!
|
||||
private var render_target_uniform: MTLBuffer!
|
||||
|
||||
private var penBrush: Brush?
|
||||
private var markerBrush: Brush?
|
||||
private var pencilBrush: Brush?
|
||||
|
||||
public init?(size: CGSize) {
|
||||
var size = size
|
||||
if Int(size.width) % 16 != 0 {
|
||||
size.width = round(size.width / 16.0) * 16.0
|
||||
}
|
||||
|
||||
let mainBundle = Bundle(for: DrawingView.self)
|
||||
guard let path = mainBundle.path(forResource: "DrawingUIBundle", ofType: "bundle") else {
|
||||
return nil
|
||||
}
|
||||
guard let bundle = Bundle(path: path) else {
|
||||
return nil
|
||||
}
|
||||
guard let device = MTLCreateSystemDefaultDevice() else {
|
||||
return nil
|
||||
}
|
||||
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
return nil
|
||||
}
|
||||
self.library = defaultLibrary
|
||||
|
||||
guard let commandQueue = device.makeCommandQueue() else {
|
||||
return nil
|
||||
}
|
||||
self.commandQueue = commandQueue
|
||||
|
||||
self.size = size
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: size), device: device)
|
||||
|
||||
self.isOpaque = false
|
||||
self.drawableSize = self.size
|
||||
|
||||
self.setup()
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func makeTexture(with data: Data) -> MTLTexture? {
|
||||
let textureLoader = MTKTextureLoader(device: device!)
|
||||
return try? textureLoader.newTexture(data: data, options: [.SRGB : false])
|
||||
}
|
||||
|
||||
func makeTexture(with image: UIImage) -> MTLTexture? {
|
||||
if let data = image.pngData() {
|
||||
return makeTexture(with: data)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func drawInContext(_ cgContext: CGContext) {
|
||||
guard let texture = self.drawable?.texture, let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: CGColorSpaceCreateDeviceRGB()])?.oriented(forExifOrientation: 1) else {
|
||||
return
|
||||
}
|
||||
let context = CIContext(cgContext: cgContext)
|
||||
let rect = CGRect(origin: .zero, size: ciImage.extent.size)
|
||||
context.draw(ciImage, in: rect, from: rect)
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
self.drawable = Drawable(size: self.size, pixelFormat: self.colorPixelFormat, device: device)
|
||||
|
||||
let size = self.size
|
||||
let w = size.width, h = size.height
|
||||
let vertices = [
|
||||
Vertex(position: CGPoint(x: 0 , y: 0), texCoord: CGPoint(x: 0, y: 0)),
|
||||
Vertex(position: CGPoint(x: w , y: 0), texCoord: CGPoint(x: 1, y: 0)),
|
||||
Vertex(position: CGPoint(x: 0 , y: h), texCoord: CGPoint(x: 0, y: 1)),
|
||||
Vertex(position: CGPoint(x: w , y: h), texCoord: CGPoint(x: 1, y: 1)),
|
||||
]
|
||||
self.render_target_vertex = self.device?.makeBuffer(bytes: vertices, length: MemoryLayout<Vertex>.stride * vertices.count, options: .cpuCacheModeWriteCombined)
|
||||
|
||||
let matrix = Matrix.identity
|
||||
matrix.scaling(x: 2.0 / Float(size.width), y: -2.0 / Float(size.height), z: 1)
|
||||
matrix.translation(x: -1, y: 1, z: 0)
|
||||
self.render_target_uniform = self.device?.makeBuffer(bytes: matrix.m, length: MemoryLayout<Float>.size * 16, options: [])
|
||||
|
||||
let vertexFunction = self.library.makeFunction(name: "vertex_render_target")
|
||||
let fragmentFunction = self.library.makeFunction(name: "fragment_render_target")
|
||||
let pipelineDescription = MTLRenderPipelineDescriptor()
|
||||
pipelineDescription.vertexFunction = vertexFunction
|
||||
pipelineDescription.fragmentFunction = fragmentFunction
|
||||
pipelineDescription.colorAttachments[0].pixelFormat = colorPixelFormat
|
||||
|
||||
do {
|
||||
self.pipelineState = try self.device?.makeRenderPipelineState(descriptor: pipelineDescription)
|
||||
} catch {
|
||||
fatalError(error.localizedDescription)
|
||||
}
|
||||
|
||||
self.penBrush = Brush(texture: nil, target: self, rotation: .ahead)
|
||||
|
||||
if let url = getAppBundle().url(forResource: "marker", withExtension: "png"), let data = try? Data(contentsOf: url) {
|
||||
self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(0.0))
|
||||
}
|
||||
|
||||
if let url = getAppBundle().url(forResource: "pencil", withExtension: "png"), let data = try? Data(contentsOf: url) {
|
||||
self.pencilBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .random)
|
||||
}
|
||||
}
|
||||
|
||||
override public func draw(_ rect: CGRect) {
|
||||
super.draw(rect)
|
||||
|
||||
guard let drawable = self.drawable, let texture = drawable.texture else {
|
||||
return
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
let attachment = renderPassDescriptor.colorAttachments[0]
|
||||
attachment?.clearColor = clearColor
|
||||
attachment?.texture = self.currentDrawable?.texture
|
||||
attachment?.loadAction = .clear
|
||||
attachment?.storeAction = .store
|
||||
|
||||
let commandBuffer = self.commandQueue.makeCommandBuffer()
|
||||
let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
|
||||
|
||||
commandEncoder?.setRenderPipelineState(self.pipelineState)
|
||||
|
||||
commandEncoder?.setVertexBuffer(self.render_target_vertex, offset: 0, index: 0)
|
||||
commandEncoder?.setVertexBuffer(self.render_target_uniform, offset: 0, index: 1)
|
||||
commandEncoder?.setFragmentTexture(texture, index: 0)
|
||||
commandEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
||||
|
||||
commandEncoder?.endEncoding()
|
||||
if let drawable = self.currentDrawable {
|
||||
commandBuffer?.present(drawable)
|
||||
}
|
||||
commandBuffer?.commit()
|
||||
}
|
||||
|
||||
func clear() {
|
||||
guard let drawable = self.drawable else {
|
||||
return
|
||||
}
|
||||
|
||||
drawable.updateBuffer(with: self.size)
|
||||
drawable.clear()
|
||||
|
||||
drawable.commit()
|
||||
}
|
||||
|
||||
enum BrushType {
|
||||
case pen
|
||||
case marker
|
||||
case pencil
|
||||
}
|
||||
|
||||
func updated(_ point: Polyline.Point, state: DrawingGesturePipeline.DrawingGestureState, brush: BrushType, color: DrawingColor, size: CGFloat) {
|
||||
switch brush {
|
||||
case .pen:
|
||||
self.penBrush?.updated(point, color: color, state: state, size: size)
|
||||
case .marker:
|
||||
self.markerBrush?.updated(point, color: color, state: state, size: size)
|
||||
case .pencil:
|
||||
self.pencilBrush?.updated(point, color: color, state: state, size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Drawable {
|
||||
public private(set) var texture: MTLTexture?
|
||||
|
||||
internal var pixelFormat: MTLPixelFormat = .bgra8Unorm
|
||||
internal var size: CGSize
|
||||
internal var uniform_buffer: MTLBuffer!
|
||||
internal var renderPassDescriptor: MTLRenderPassDescriptor?
|
||||
internal var commandBuffer: MTLCommandBuffer?
|
||||
internal var commandQueue: MTLCommandQueue?
|
||||
internal var device: MTLDevice?
|
||||
|
||||
public init(size: CGSize, pixelFormat: MTLPixelFormat, device: MTLDevice?) {
|
||||
self.size = size
|
||||
self.pixelFormat = pixelFormat
|
||||
self.device = device
|
||||
self.texture = self.makeColorTexture(DrawingColor.clear)
|
||||
self.commandQueue = device?.makeCommandQueue()
|
||||
|
||||
self.renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
let attachment = self.renderPassDescriptor?.colorAttachments[0]
|
||||
attachment?.texture = self.texture
|
||||
attachment?.loadAction = .load
|
||||
attachment?.storeAction = .store
|
||||
|
||||
self.updateBuffer(with: size)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
self.texture = self.makeColorTexture(DrawingColor.clear)
|
||||
self.renderPassDescriptor?.colorAttachments[0].texture = self.texture
|
||||
self.commit()
|
||||
}
|
||||
|
||||
internal func updateBuffer(with size: CGSize) {
|
||||
self.size = size
|
||||
|
||||
let matrix = Matrix.identity
|
||||
self.uniform_buffer = device?.makeBuffer(bytes: matrix.m, length: MemoryLayout<Float>.size * 16, options: [])
|
||||
}
|
||||
|
||||
internal func prepareForDraw() {
|
||||
if self.commandBuffer == nil {
|
||||
self.commandBuffer = commandQueue?.makeCommandBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
internal func makeCommandEncoder() -> MTLRenderCommandEncoder? {
|
||||
guard let commandBuffer = self.commandBuffer, let rpd = renderPassDescriptor else {
|
||||
return nil
|
||||
}
|
||||
return commandBuffer.makeRenderCommandEncoder(descriptor: rpd)
|
||||
}
|
||||
|
||||
internal func commit() {
|
||||
self.commandBuffer?.commit()
|
||||
self.commandBuffer = nil
|
||||
}
|
||||
|
||||
internal func makeColorTexture(_ color: DrawingColor) -> MTLTexture? {
|
||||
guard self.size.width * self.size.height > 0 else {
|
||||
return nil
|
||||
}
|
||||
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||
pixelFormat: self.pixelFormat,
|
||||
width: Int(self.size.width),
|
||||
height: Int(self.size.height),
|
||||
mipmapped: false
|
||||
)
|
||||
textureDescriptor.usage = [.renderTarget, .shaderRead]
|
||||
guard let texture = device?.makeTexture(descriptor: textureDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
let region = MTLRegion(
|
||||
origin: MTLOrigin(x: 0, y: 0, z: 0),
|
||||
size: MTLSize(width: texture.width, height: texture.height, depth: 1)
|
||||
)
|
||||
let bytesPerRow = 4 * texture.width
|
||||
let data = Data(capacity: Int(bytesPerRow * texture.height))
|
||||
if let bytes = data.withUnsafeBytes({ $0.baseAddress }) {
|
||||
texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: bytesPerRow)
|
||||
}
|
||||
return texture
|
||||
}
|
||||
}
|
||||
|
||||
private class Brush {
|
||||
private(set) var texture: MTLTexture?
|
||||
private(set) var pipelineState: MTLRenderPipelineState!
|
||||
|
||||
weak var target: DrawingMetalView?
|
||||
|
||||
public enum Rotation {
|
||||
case fixed(CGFloat)
|
||||
case random
|
||||
case ahead
|
||||
}
|
||||
|
||||
var rotation: Rotation
|
||||
|
||||
required public init(texture: MTLTexture?, target: DrawingMetalView, rotation: Rotation) {
|
||||
self.texture = texture
|
||||
self.target = target
|
||||
self.rotation = rotation
|
||||
|
||||
self.setupPipeline()
|
||||
}
|
||||
|
||||
private func setupPipeline() {
|
||||
guard let target = self.target, let device = target.device else {
|
||||
return
|
||||
}
|
||||
|
||||
let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||
if let vertex_func = target.library.makeFunction(name: "vertex_point_func") {
|
||||
renderPipelineDescriptor.vertexFunction = vertex_func
|
||||
}
|
||||
if let _ = self.texture {
|
||||
if let fragment_func = target.library.makeFunction(name: "fragment_point_func") {
|
||||
renderPipelineDescriptor.fragmentFunction = fragment_func
|
||||
}
|
||||
} else {
|
||||
if let fragment_func = target.library.makeFunction(name: "fragment_point_func_without_texture") {
|
||||
renderPipelineDescriptor.fragmentFunction = fragment_func
|
||||
}
|
||||
}
|
||||
renderPipelineDescriptor.colorAttachments[0].pixelFormat = target.colorPixelFormat
|
||||
|
||||
let attachment = renderPipelineDescriptor.colorAttachments[0]
|
||||
attachment?.isBlendingEnabled = true
|
||||
|
||||
attachment?.rgbBlendOperation = .add
|
||||
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
attachment?.alphaBlendOperation = .add
|
||||
attachment?.sourceAlphaBlendFactor = .one
|
||||
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
self.pipelineState = try! device.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
|
||||
}
|
||||
|
||||
func render(stroke: Stroke, in drawable: Drawable? = nil) {
|
||||
let drawable = drawable ?? target?.drawable
|
||||
|
||||
guard stroke.lines.count > 0, let target = drawable else {
|
||||
return
|
||||
}
|
||||
|
||||
target.prepareForDraw()
|
||||
|
||||
let commandEncoder = target.makeCommandEncoder()
|
||||
|
||||
commandEncoder?.setRenderPipelineState(self.pipelineState)
|
||||
|
||||
if let vertex_buffer = stroke.preparedBuffer(rotation: self.rotation) {
|
||||
commandEncoder?.setVertexBuffer(vertex_buffer, offset: 0, index: 0)
|
||||
commandEncoder?.setVertexBuffer(target.uniform_buffer, offset: 0, index: 1)
|
||||
if let texture = texture {
|
||||
commandEncoder?.setFragmentTexture(texture, index: 0)
|
||||
}
|
||||
commandEncoder?.drawPrimitives(type: .point, vertexStart: 0, vertexCount: stroke.vertexCount)
|
||||
}
|
||||
|
||||
commandEncoder?.endEncoding()
|
||||
}
|
||||
|
||||
private let bezier = BezierGenerator()
|
||||
func updated(_ point: Polyline.Point, color: DrawingColor, state: DrawingGesturePipeline.DrawingGestureState, size: CGFloat) {
|
||||
let point = point.location
|
||||
switch state {
|
||||
case .began:
|
||||
self.bezier.begin(with: point)
|
||||
self.pushPoint(point, color: color, size: size, isEnd: false)
|
||||
case .changed:
|
||||
if self.bezier.points.count > 0 && point != lastRenderedPoint {
|
||||
self.pushPoint(point, color: color, size: size, isEnd: false)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
if self.bezier.points.count >= 3 {
|
||||
self.pushPoint(point, color: color, size: size, isEnd: true)
|
||||
}
|
||||
self.bezier.finish()
|
||||
self.lastRenderedPoint = nil
|
||||
}
|
||||
}
|
||||
|
||||
private var lastRenderedPoint: CGPoint?
|
||||
func pushPoint(_ point: CGPoint, color: DrawingColor, size: CGFloat, isEnd: Bool) {
|
||||
var pointStep: CGFloat
|
||||
if case .random = self.rotation {
|
||||
pointStep = size * 0.1
|
||||
} else {
|
||||
pointStep = 2.0
|
||||
}
|
||||
|
||||
var lines: [Line] = []
|
||||
let points = self.bezier.pushPoint(point)
|
||||
guard points.count >= 2 else {
|
||||
return
|
||||
}
|
||||
var previousPoint = self.lastRenderedPoint ?? points[0]
|
||||
for i in 1 ..< points.count {
|
||||
let p = points[i]
|
||||
if (isEnd && i == points.count - 1) || pointStep <= 1 || (pointStep > 1 && previousPoint.distance(to: p) >= pointStep) {
|
||||
let line = Line(start: previousPoint, end: p, pointSize: size, pointStep: pointStep)
|
||||
lines.append(line)
|
||||
previousPoint = p
|
||||
}
|
||||
}
|
||||
|
||||
if let drawable = self.target?.drawable {
|
||||
let stroke = Stroke(color: color, lines: lines, target: drawable)
|
||||
|
||||
self.render(stroke: stroke, in: drawable)
|
||||
|
||||
drawable.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Stroke {
|
||||
private weak var target: Drawable?
|
||||
|
||||
let color: DrawingColor
|
||||
var lines: [Line] = []
|
||||
|
||||
private(set) var vertexCount: Int = 0
|
||||
private var vertex_buffer: MTLBuffer?
|
||||
|
||||
init(color: DrawingColor, lines: [Line] = [], target: Drawable) {
|
||||
self.color = color
|
||||
self.lines = lines
|
||||
self.target = target
|
||||
|
||||
let _ = self.preparedBuffer(rotation: .fixed(0))
|
||||
}
|
||||
|
||||
func append(_ lines: [Line]) {
|
||||
self.lines.append(contentsOf: lines)
|
||||
self.vertex_buffer = nil
|
||||
}
|
||||
|
||||
func preparedBuffer(rotation: Brush.Rotation) -> MTLBuffer? {
|
||||
guard !self.lines.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var vertexes: [Point] = []
|
||||
|
||||
self.lines.forEach { (line) in
|
||||
let count = max(line.length / line.pointStep, 1)
|
||||
|
||||
let overlapping = max(1, line.pointSize / line.pointStep)
|
||||
var renderingColor = self.color
|
||||
renderingColor.alpha = renderingColor.alpha / overlapping * 2.5
|
||||
|
||||
for i in 0 ..< Int(count) {
|
||||
let index = CGFloat(i)
|
||||
let x = line.start.x + (line.end.x - line.start.x) * (index / count)
|
||||
let y = line.start.y + (line.end.y - line.start.y) * (index / count)
|
||||
|
||||
var angle: CGFloat = 0
|
||||
switch rotation {
|
||||
case let .fixed(a):
|
||||
angle = a
|
||||
case .random:
|
||||
angle = CGFloat.random(in: -CGFloat.pi ... CGFloat.pi)
|
||||
case .ahead:
|
||||
angle = line.angle
|
||||
}
|
||||
|
||||
vertexes.append(Point(x: x, y: y, color: renderingColor, size: line.pointSize, angle: angle))
|
||||
}
|
||||
}
|
||||
|
||||
self.vertexCount = vertexes.count
|
||||
self.vertex_buffer = self.target?.device?.makeBuffer(bytes: vertexes, length: MemoryLayout<Point>.stride * vertexCount, options: .cpuCacheModeWriteCombined)
|
||||
|
||||
return self.vertex_buffer
|
||||
}
|
||||
}
|
||||
|
||||
class BezierGenerator {
|
||||
init() {
|
||||
}
|
||||
|
||||
init(beginPoint: CGPoint) {
|
||||
begin(with: beginPoint)
|
||||
}
|
||||
|
||||
func begin(with point: CGPoint) {
|
||||
step = 0
|
||||
points.removeAll()
|
||||
points.append(point)
|
||||
}
|
||||
|
||||
func pushPoint(_ point: CGPoint) -> [CGPoint] {
|
||||
if point == points.last {
|
||||
return []
|
||||
}
|
||||
points.append(point)
|
||||
if points.count < 3 {
|
||||
return []
|
||||
}
|
||||
step += 1
|
||||
let result = genericPathPoints()
|
||||
return result
|
||||
}
|
||||
|
||||
func finish() {
|
||||
step = 0
|
||||
points.removeAll()
|
||||
}
|
||||
|
||||
var points: [CGPoint] = []
|
||||
|
||||
private var step = 0
|
||||
private func genericPathPoints() -> [CGPoint] {
|
||||
var begin: CGPoint
|
||||
var control: CGPoint
|
||||
let end = CGPoint.middle(p1: points[step], p2: points[step + 1])
|
||||
|
||||
var vertices: [CGPoint] = []
|
||||
if step == 1 {
|
||||
begin = points[0]
|
||||
let middle1 = CGPoint.middle(p1: points[0], p2: points[1])
|
||||
control = CGPoint.middle(p1: middle1, p2: points[1])
|
||||
} else {
|
||||
begin = CGPoint.middle(p1: points[step - 1], p2: points[step])
|
||||
control = points[step]
|
||||
}
|
||||
|
||||
let distance = begin.distance(to: end)
|
||||
let segements = max(Int(distance / 5), 2)
|
||||
|
||||
for i in 0 ..< segements {
|
||||
let t = CGFloat(i) / CGFloat(segements)
|
||||
let x = pow(1 - t, 2) * begin.x + 2.0 * (1 - t) * t * control.x + t * t * end.x
|
||||
let y = pow(1 - t, 2) * begin.y + 2.0 * (1 - t) * t * control.y + t * t * end.y
|
||||
vertices.append(CGPoint(x: x, y: y))
|
||||
}
|
||||
vertices.append(end)
|
||||
return vertices
|
||||
}
|
||||
}
|
||||
|
||||
private struct Line {
|
||||
var start: CGPoint
|
||||
var end: CGPoint
|
||||
|
||||
var pointSize: CGFloat
|
||||
var pointStep: CGFloat
|
||||
|
||||
init(start: CGPoint, end: CGPoint, pointSize: CGFloat, pointStep: CGFloat) {
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.pointSize = pointSize
|
||||
self.pointStep = pointStep
|
||||
}
|
||||
|
||||
var length: CGFloat {
|
||||
return self.start.distance(to: self.end)
|
||||
}
|
||||
|
||||
var angle: CGFloat {
|
||||
return self.end.angle(to: self.start)
|
||||
}
|
||||
}
|
||||
2442
submodules/DrawingUI/Sources/DrawingScreen.swift
Normal file
433
submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift
Normal file
@@ -0,0 +1,433 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AccountContext
|
||||
|
||||
final class DrawingSimpleShapeEntity: DrawingEntity {
|
||||
public enum ShapeType {
|
||||
case rectangle
|
||||
case ellipse
|
||||
case star
|
||||
}
|
||||
|
||||
public enum DrawType {
|
||||
case fill
|
||||
case stroke
|
||||
}
|
||||
|
||||
let uuid: UUID
|
||||
let isAnimated: Bool
|
||||
|
||||
var shapeType: ShapeType
|
||||
var drawType: DrawType
|
||||
var color: DrawingColor
|
||||
var lineWidth: CGFloat
|
||||
|
||||
var referenceDrawingSize: CGSize
|
||||
var position: CGPoint
|
||||
var size: CGSize
|
||||
var rotation: CGFloat
|
||||
|
||||
init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = false
|
||||
|
||||
self.shapeType = shapeType
|
||||
self.drawType = drawType
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
|
||||
self.referenceDrawingSize = .zero
|
||||
self.position = .zero
|
||||
self.size = CGSize(width: 1.0, height: 1.0)
|
||||
self.rotation = 0.0
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
newEntity.position = self.position
|
||||
newEntity.size = self.size
|
||||
newEntity.rotation = self.rotation
|
||||
return newEntity
|
||||
}
|
||||
|
||||
weak var currentEntityView: DrawingEntityView?
|
||||
func makeView(context: AccountContext) -> DrawingEntityView {
|
||||
let entityView = DrawingSimpleShapeEntityView(context: context, entity: self)
|
||||
self.currentEntityView = entityView
|
||||
return entityView
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingSimpleShapeEntityView: DrawingEntityView {
|
||||
private var shapeEntity: DrawingSimpleShapeEntity {
|
||||
return self.entity as! DrawingSimpleShapeEntity
|
||||
}
|
||||
|
||||
private var currentShape: DrawingSimpleShapeEntity.ShapeType?
|
||||
private var currentSize: CGSize?
|
||||
|
||||
private let shapeLayer = SimpleShapeLayer()
|
||||
|
||||
init(context: AccountContext, entity: DrawingSimpleShapeEntity) {
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.layer.addSublayer(self.shapeLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func update(animated: Bool) {
|
||||
let shapeType = self.shapeEntity.shapeType
|
||||
let size = self.shapeEntity.size
|
||||
|
||||
self.center = self.shapeEntity.position
|
||||
self.bounds = CGRect(origin: .zero, size: size)
|
||||
self.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation)
|
||||
|
||||
if shapeType != self.currentShape || size != self.currentSize {
|
||||
self.currentShape = shapeType
|
||||
self.currentSize = size
|
||||
self.shapeLayer.frame = self.bounds
|
||||
|
||||
switch shapeType {
|
||||
case .rectangle:
|
||||
self.shapeLayer.path = CGPath(rect: CGRect(origin: .zero, size: size), transform: nil)
|
||||
case .ellipse:
|
||||
self.shapeLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: size), transform: nil)
|
||||
case .star:
|
||||
self.shapeLayer.path = CGPath.star(in: CGRect(origin: .zero, size: size), extrusion: size.width * 0.2, points: 5)
|
||||
}
|
||||
}
|
||||
|
||||
switch self.shapeEntity.drawType {
|
||||
case .fill:
|
||||
self.shapeLayer.fillColor = self.shapeEntity.color.toCGColor()
|
||||
self.shapeLayer.strokeColor = UIColor.clear.cgColor
|
||||
case .stroke:
|
||||
let minLineWidth = max(10.0, min(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.02)
|
||||
let maxLineWidth = max(10.0, min(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.1)
|
||||
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.shapeEntity.lineWidth
|
||||
|
||||
self.shapeLayer.fillColor = UIColor.clear.cgColor
|
||||
self.shapeLayer.strokeColor = self.shapeEntity.color.toCGColor()
|
||||
self.shapeLayer.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
fileprivate var visualLineWidth: CGFloat {
|
||||
return self.shapeLayer.lineWidth
|
||||
}
|
||||
|
||||
override func precisePoint(inside point: CGPoint) -> Bool {
|
||||
if case .stroke = self.shapeEntity.drawType, var path = self.shapeLayer.path {
|
||||
path = path.copy(strokingWithWidth: 20.0, lineCap: .square, lineJoin: .bevel, miterLimit: 0.0)
|
||||
if path.contains(point) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return super.precisePoint(inside: point)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateSelectionView() {
|
||||
super.updateSelectionView()
|
||||
|
||||
guard let selectionView = self.selectionView as? DrawingSimpleShapeEntititySelectionView else {
|
||||
return
|
||||
}
|
||||
|
||||
// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
// selectionView.scale = scale
|
||||
|
||||
selectionView.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation)
|
||||
}
|
||||
|
||||
override func makeSelectionView() -> DrawingEntitySelectionView {
|
||||
if let selectionView = self.selectionView {
|
||||
return selectionView
|
||||
}
|
||||
let selectionView = DrawingSimpleShapeEntititySelectionView()
|
||||
selectionView.entityView = self
|
||||
return selectionView
|
||||
}
|
||||
}
|
||||
|
||||
func gestureIsTracking(_ gestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
return [.began, .changed].contains(gestureRecognizer.state)
|
||||
}
|
||||
|
||||
final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate {
|
||||
private let leftHandle = SimpleShapeLayer()
|
||||
private let topLeftHandle = SimpleShapeLayer()
|
||||
private let topHandle = SimpleShapeLayer()
|
||||
private let topRightHandle = SimpleShapeLayer()
|
||||
private let rightHandle = SimpleShapeLayer()
|
||||
private let bottomLeftHandle = SimpleShapeLayer()
|
||||
private let bottomHandle = SimpleShapeLayer()
|
||||
private let bottomRightHandle = SimpleShapeLayer()
|
||||
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
|
||||
let handles = [
|
||||
self.leftHandle,
|
||||
self.topLeftHandle,
|
||||
self.topHandle,
|
||||
self.topRightHandle,
|
||||
self.rightHandle,
|
||||
self.bottomLeftHandle,
|
||||
self.bottomHandle,
|
||||
self.bottomRightHandle
|
||||
]
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
self.isOpaque = false
|
||||
|
||||
for handle in handles {
|
||||
handle.bounds = handleBounds
|
||||
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
|
||||
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
|
||||
handle.rasterizationScale = UIScreen.main.scale
|
||||
handle.shouldRasterize = true
|
||||
|
||||
self.layer.addSublayer(handle)
|
||||
}
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var scale: CGFloat = 1.0 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override var selectionInset: CGFloat {
|
||||
return 5.5
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
|
||||
return
|
||||
}
|
||||
let isAspectLocked = [.star].contains(entity.shapeType)
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
|
||||
var updatedSize = entity.size
|
||||
var updatedPosition = entity.position
|
||||
|
||||
if self.currentHandle === self.leftHandle {
|
||||
let deltaX = delta.x * cos(entity.rotation)
|
||||
let deltaY = delta.x * sin(entity.rotation)
|
||||
|
||||
updatedSize.width -= deltaX
|
||||
updatedPosition.x -= deltaX * -0.5
|
||||
updatedPosition.y -= deltaY * -0.5
|
||||
|
||||
if isAspectLocked {
|
||||
updatedSize.height -= delta.x
|
||||
}
|
||||
} else if self.currentHandle === self.rightHandle {
|
||||
let deltaX = delta.x * cos(entity.rotation)
|
||||
let deltaY = delta.x * sin(entity.rotation)
|
||||
|
||||
updatedSize.width += deltaX
|
||||
updatedPosition.x += deltaX * 0.5
|
||||
updatedPosition.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.height += delta.x
|
||||
}
|
||||
} else if self.currentHandle === self.topHandle {
|
||||
let deltaX = delta.y * sin(entity.rotation)
|
||||
let deltaY = delta.y * cos(entity.rotation)
|
||||
|
||||
updatedSize.height -= deltaY
|
||||
updatedPosition.x += deltaX * 0.5
|
||||
updatedPosition.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.width -= delta.y
|
||||
}
|
||||
} else if self.currentHandle === self.bottomHandle {
|
||||
let deltaX = delta.y * sin(entity.rotation)
|
||||
let deltaY = delta.y * cos(entity.rotation)
|
||||
|
||||
updatedSize.height += deltaY
|
||||
updatedPosition.x += deltaX * 0.5
|
||||
updatedPosition.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.width += delta.y
|
||||
}
|
||||
} else if self.currentHandle === self.topLeftHandle {
|
||||
var delta = delta
|
||||
if isAspectLocked {
|
||||
delta = CGPoint(x: delta.x, y: delta.x)
|
||||
}
|
||||
updatedSize.width -= delta.x
|
||||
updatedPosition.x -= delta.x * -0.5
|
||||
updatedSize.height -= delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.topRightHandle {
|
||||
var delta = delta
|
||||
if isAspectLocked {
|
||||
delta = CGPoint(x: delta.x, y: -delta.x)
|
||||
}
|
||||
updatedSize.width += delta.x
|
||||
updatedPosition.x += delta.x * 0.5
|
||||
updatedSize.height -= delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.bottomLeftHandle {
|
||||
var delta = delta
|
||||
if isAspectLocked {
|
||||
delta = CGPoint(x: delta.x, y: -delta.x)
|
||||
}
|
||||
updatedSize.width -= delta.x
|
||||
updatedPosition.x -= delta.x * -0.5
|
||||
updatedSize.height += delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.bottomRightHandle {
|
||||
var delta = delta
|
||||
if isAspectLocked {
|
||||
delta = CGPoint(x: delta.x, y: delta.x)
|
||||
}
|
||||
updatedSize.width += delta.x
|
||||
updatedPosition.x += delta.x * 0.5
|
||||
updatedSize.height += delta.y
|
||||
updatedPosition.y += delta.y * 0.5
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
}
|
||||
|
||||
entity.size = updatedSize
|
||||
entity.position = updatedPosition
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale)
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let rotation = gestureRecognizer.rotation
|
||||
entity.rotation += rotation
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.rotation = 0.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
var inset = self.selectionInset
|
||||
if let entityView = self.entityView as? DrawingSimpleShapeEntityView, let entity = entityView.entity as? DrawingSimpleShapeEntity, case .star = entity.shapeType {
|
||||
inset -= entityView.visualLineWidth / 2.0
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
|
||||
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
|
||||
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
|
||||
let lineWidth = (1.0 + UIScreenPixel) / self.scale
|
||||
|
||||
let handles = [
|
||||
self.leftHandle,
|
||||
self.topLeftHandle,
|
||||
self.topHandle,
|
||||
self.topRightHandle,
|
||||
self.rightHandle,
|
||||
self.bottomLeftHandle,
|
||||
self.bottomHandle,
|
||||
self.bottomRightHandle
|
||||
]
|
||||
|
||||
for handle in handles {
|
||||
handle.path = handlePath
|
||||
handle.bounds = bounds
|
||||
handle.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
self.topLeftHandle.position = CGPoint(x: inset, y: inset)
|
||||
self.topHandle.position = CGPoint(x: self.bounds.midX, y: inset)
|
||||
self.topRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: inset)
|
||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||
self.bottomLeftHandle.position = CGPoint(x: inset, y: self.bounds.maxY - inset)
|
||||
self.bottomHandle.position = CGPoint(x: self.bounds.midX, y: self.bounds.maxY - inset)
|
||||
self.bottomRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.maxY - inset)
|
||||
}
|
||||
|
||||
var isTracking: Bool {
|
||||
return gestureIsTracking(self.panGestureRecognizer)
|
||||
}
|
||||
}
|
||||
458
submodules/DrawingUI/Sources/DrawingStickerEntity.swift
Normal file
@@ -0,0 +1,458 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import StickerResources
|
||||
import AccountContext
|
||||
|
||||
final class DrawingStickerEntity: DrawingEntity {
|
||||
let uuid: UUID
|
||||
let isAnimated: Bool
|
||||
let file: TelegramMediaFile
|
||||
|
||||
var referenceDrawingSize: CGSize
|
||||
var position: CGPoint
|
||||
var scale: CGFloat
|
||||
var rotation: CGFloat
|
||||
var mirrored: Bool
|
||||
|
||||
var color: DrawingColor = DrawingColor.clear
|
||||
var lineWidth: CGFloat = 0.0
|
||||
|
||||
init(file: TelegramMediaFile) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = file.isAnimatedSticker
|
||||
|
||||
self.file = file
|
||||
|
||||
self.referenceDrawingSize = .zero
|
||||
self.position = CGPoint()
|
||||
self.scale = 1.0
|
||||
self.rotation = 0.0
|
||||
self.mirrored = false
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingStickerEntity(file: self.file)
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
newEntity.position = self.position
|
||||
newEntity.scale = self.scale
|
||||
newEntity.rotation = self.rotation
|
||||
newEntity.mirrored = self.mirrored
|
||||
return newEntity
|
||||
}
|
||||
|
||||
weak var currentEntityView: DrawingEntityView?
|
||||
func makeView(context: AccountContext) -> DrawingEntityView {
|
||||
let entityView = DrawingStickerEntityView(context: context, entity: self)
|
||||
self.currentEntityView = entityView
|
||||
return entityView
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingStickerEntityView: DrawingEntityView {
|
||||
private var stickerEntity: DrawingStickerEntity {
|
||||
return self.entity as! DrawingStickerEntity
|
||||
}
|
||||
|
||||
var started: ((Double) -> Void)?
|
||||
|
||||
private var currentSize: CGSize?
|
||||
private var dimensions: CGSize?
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
|
||||
private var didSetUpAnimationNode = false
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
private let cachedDisposable = MetaDisposable()
|
||||
|
||||
private var isVisible = true
|
||||
private var isPlaying = false
|
||||
|
||||
init(context: AccountContext, entity: DrawingStickerEntity) {
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.addSubview(self.imageNode.view)
|
||||
|
||||
self.setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
self.cachedDisposable.dispose()
|
||||
}
|
||||
|
||||
private var file: TelegramMediaFile {
|
||||
return (self.entity as! DrawingStickerEntity).file
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
if let dimensions = self.file.dimensions {
|
||||
if self.file.isAnimatedSticker || self.file.isVideoSticker {
|
||||
if self.animationNode == nil {
|
||||
let animationNode = DefaultAnimatedStickerNodeImpl()
|
||||
animationNode.autoplay = false
|
||||
self.animationNode = animationNode
|
||||
animationNode.started = { [weak self, weak animationNode] in
|
||||
self?.imageNode.isHidden = true
|
||||
|
||||
if let animationNode = animationNode {
|
||||
let _ = (animationNode.status
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
self?.started?(status.duration)
|
||||
})
|
||||
}
|
||||
}
|
||||
self.addSubnode(animationNode)
|
||||
}
|
||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: self.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: stickerPackFileReference(self.file), resource: self.file.resource).start())
|
||||
} else {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.visibility = false
|
||||
self.animationNode = nil
|
||||
animationNode.removeFromSupernode()
|
||||
self.imageNode.isHidden = false
|
||||
self.didSetUpAnimationNode = false
|
||||
}
|
||||
self.imageNode.setSignal(chatMessageSticker(account: self.context.account, file: self.file, small: false, synchronousLoad: false))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: stickerPackFileReference(self.file), resource: chatMessageStickerResource(file: self.file, small: false)).start())
|
||||
}
|
||||
|
||||
self.dimensions = dimensions.cgSize
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override func play() {
|
||||
self.isVisible = true
|
||||
self.applyVisibility()
|
||||
}
|
||||
|
||||
override func pause() {
|
||||
self.isVisible = false
|
||||
self.applyVisibility()
|
||||
}
|
||||
|
||||
override func seek(to timestamp: Double) {
|
||||
self.isVisible = false
|
||||
self.isPlaying = false
|
||||
self.animationNode?.seekTo(.timestamp(timestamp))
|
||||
}
|
||||
|
||||
override func resetToStart() {
|
||||
self.isVisible = false
|
||||
self.isPlaying = false
|
||||
self.animationNode?.seekTo(.timestamp(0.0))
|
||||
}
|
||||
|
||||
override func updateVisibility(_ visibility: Bool) {
|
||||
self.isVisible = visibility
|
||||
self.applyVisibility()
|
||||
}
|
||||
|
||||
private func applyVisibility() {
|
||||
let isPlaying = self.isVisible
|
||||
if self.isPlaying != isPlaying {
|
||||
self.isPlaying = isPlaying
|
||||
|
||||
if isPlaying && !self.didSetUpAnimationNode {
|
||||
self.didSetUpAnimationNode = true
|
||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker)
|
||||
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
|
||||
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
||||
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
||||
}
|
||||
self.animationNode?.visibility = isPlaying
|
||||
}
|
||||
}
|
||||
|
||||
private var didApplyVisibility = false
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if size.width > 0 && self.currentSize != size {
|
||||
self.currentSize = size
|
||||
|
||||
let sideSize: CGFloat = size.width
|
||||
let boundingSize = CGSize(width: sideSize, height: sideSize)
|
||||
|
||||
if let dimensions = self.dimensions {
|
||||
let imageSize = dimensions.aspectFitted(boundingSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
|
||||
if !self.didApplyVisibility {
|
||||
self.didApplyVisibility = true
|
||||
self.applyVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func update(animated: Bool) {
|
||||
guard let dimensions = self.stickerEntity.file.dimensions?.cgSize else {
|
||||
return
|
||||
}
|
||||
self.center = self.stickerEntity.position
|
||||
|
||||
let size = max(10.0, min(self.stickerEntity.referenceDrawingSize.width, self.stickerEntity.referenceDrawingSize.height) * 0.45)
|
||||
|
||||
self.bounds = CGRect(origin: .zero, size: dimensions.fitted(CGSize(width: size, height: size)))
|
||||
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale)
|
||||
|
||||
var transform = CATransform3DIdentity
|
||||
|
||||
if self.stickerEntity.mirrored {
|
||||
transform = CATransform3DRotate(transform, .pi, 0.0, 1.0, 0.0)
|
||||
transform.m34 = -1.0 / self.imageNode.frame.width
|
||||
}
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.25, delay: 0.0) {
|
||||
self.imageNode.transform = transform
|
||||
self.animationNode?.transform = transform
|
||||
}
|
||||
} else {
|
||||
self.imageNode.transform = transform
|
||||
self.animationNode?.transform = transform
|
||||
}
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
override func updateSelectionView() {
|
||||
guard let selectionView = self.selectionView as? DrawingStickerEntititySelectionView else {
|
||||
return
|
||||
}
|
||||
self.pushIdentityTransformForMeasurement()
|
||||
|
||||
selectionView.transform = .identity
|
||||
let bounds = self.selectionBounds
|
||||
let center = bounds.center
|
||||
|
||||
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
selectionView.center = self.convert(center, to: selectionView.superview)
|
||||
|
||||
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0))
|
||||
selectionView.transform = CGAffineTransformMakeRotation(self.stickerEntity.rotation)
|
||||
|
||||
self.popIdentityTransformForMeasurement()
|
||||
}
|
||||
|
||||
override func makeSelectionView() -> DrawingEntitySelectionView {
|
||||
if let selectionView = self.selectionView {
|
||||
return selectionView
|
||||
}
|
||||
let selectionView = DrawingStickerEntititySelectionView()
|
||||
selectionView.entityView = self
|
||||
return selectionView
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate {
|
||||
private let border = SimpleShapeLayer()
|
||||
private let leftHandle = SimpleShapeLayer()
|
||||
private let rightHandle = SimpleShapeLayer()
|
||||
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
|
||||
let handles = [
|
||||
self.leftHandle,
|
||||
self.rightHandle
|
||||
]
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
self.isOpaque = false
|
||||
|
||||
self.border.lineCap = .round
|
||||
self.border.fillColor = UIColor.clear.cgColor
|
||||
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
|
||||
self.layer.addSublayer(self.border)
|
||||
|
||||
for handle in handles {
|
||||
handle.bounds = handleBounds
|
||||
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
|
||||
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
|
||||
handle.rasterizationScale = UIScreen.main.scale
|
||||
handle.shouldRasterize = true
|
||||
|
||||
self.layer.addSublayer(handle)
|
||||
}
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var scale: CGFloat = 1.0 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override var selectionInset: CGFloat {
|
||||
return 18.0
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else {
|
||||
return
|
||||
}
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
|
||||
var updatedPosition = entity.position
|
||||
var updatedScale = entity.scale
|
||||
var updatedRotation = entity.rotation
|
||||
if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle {
|
||||
var deltaX = gestureRecognizer.translation(in: self).x
|
||||
if self.currentHandle === self.leftHandle {
|
||||
deltaX *= -1.0
|
||||
}
|
||||
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
|
||||
updatedScale *= scaleDelta
|
||||
|
||||
let deltaAngle: CGFloat
|
||||
if self.currentHandle === self.leftHandle {
|
||||
deltaAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x)
|
||||
} else {
|
||||
deltaAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x)
|
||||
}
|
||||
updatedRotation = deltaAngle
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
}
|
||||
|
||||
entity.position = updatedPosition
|
||||
entity.scale = updatedScale
|
||||
entity.rotation = updatedRotation
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.scale = entity.scale * scale
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let rotation = gestureRecognizer.rotation
|
||||
entity.rotation += rotation
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.rotation = 0.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
let inset = self.selectionInset - 10.0
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
|
||||
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
|
||||
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
|
||||
let lineWidth = (1.0 + UIScreenPixel) / self.scale
|
||||
|
||||
let handles = [
|
||||
self.leftHandle,
|
||||
self.rightHandle
|
||||
]
|
||||
|
||||
for handle in handles {
|
||||
handle.path = handlePath
|
||||
handle.bounds = bounds
|
||||
handle.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||
|
||||
self.border.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber]
|
||||
self.border.lineWidth = 2.0 / self.scale
|
||||
self.border.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0))).cgPath
|
||||
}
|
||||
}
|
||||
935
submodules/DrawingUI/Sources/DrawingTextEntity.swift
Normal file
@@ -0,0 +1,935 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AccountContext
|
||||
|
||||
final class DrawingTextEntity: DrawingEntity {
|
||||
enum Style {
|
||||
case regular
|
||||
case filled
|
||||
case semi
|
||||
case stroke
|
||||
|
||||
init(style: DrawingTextEntity.Style) {
|
||||
switch style {
|
||||
case .regular:
|
||||
self = .regular
|
||||
case .filled:
|
||||
self = .filled
|
||||
case .semi:
|
||||
self = .semi
|
||||
case .stroke:
|
||||
self = .stroke
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Font {
|
||||
case sanFrancisco
|
||||
case newYork
|
||||
case monospaced
|
||||
case round
|
||||
|
||||
init(font: DrawingTextEntity.Font) {
|
||||
switch font {
|
||||
case .sanFrancisco:
|
||||
self = .sanFrancisco
|
||||
case .newYork:
|
||||
self = .newYork
|
||||
case .monospaced:
|
||||
self = .monospaced
|
||||
case .round:
|
||||
self = .round
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Alignment {
|
||||
case left
|
||||
case center
|
||||
case right
|
||||
|
||||
init(font: DrawingTextEntity.Alignment) {
|
||||
switch font {
|
||||
case .left:
|
||||
self = .left
|
||||
case .center:
|
||||
self = .center
|
||||
case .right:
|
||||
self = .right
|
||||
}
|
||||
}
|
||||
|
||||
var alignment: NSTextAlignment {
|
||||
switch self {
|
||||
case .left:
|
||||
return .left
|
||||
case .center:
|
||||
return .center
|
||||
case .right:
|
||||
return .right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var uuid: UUID
|
||||
let isAnimated: Bool
|
||||
|
||||
var text: String
|
||||
var style: Style
|
||||
var font: Font
|
||||
var alignment: Alignment
|
||||
var fontSize: CGFloat
|
||||
var color: DrawingColor
|
||||
var lineWidth: CGFloat = 0.0
|
||||
|
||||
var referenceDrawingSize: CGSize
|
||||
var position: CGPoint
|
||||
var width: CGFloat
|
||||
var rotation: CGFloat
|
||||
|
||||
init(text: String, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = false
|
||||
|
||||
self.text = text
|
||||
self.style = style
|
||||
self.font = font
|
||||
self.alignment = alignment
|
||||
self.fontSize = fontSize
|
||||
self.color = color
|
||||
|
||||
self.referenceDrawingSize = .zero
|
||||
self.position = .zero
|
||||
self.width = 100.0
|
||||
self.rotation = 0.0
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingTextEntity(text: self.text, style: self.style, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color)
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
newEntity.position = self.position
|
||||
newEntity.width = self.width
|
||||
newEntity.rotation = self.rotation
|
||||
return newEntity
|
||||
}
|
||||
|
||||
weak var currentEntityView: DrawingEntityView?
|
||||
func makeView(context: AccountContext) -> DrawingEntityView {
|
||||
let entityView = DrawingTextEntityView(context: context, entity: self)
|
||||
self.currentEntityView = entityView
|
||||
return entityView
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
private var textEntity: DrawingTextEntity {
|
||||
return self.entity as! DrawingTextEntity
|
||||
}
|
||||
|
||||
private let textView: DrawingTextView
|
||||
|
||||
init(context: AccountContext, entity: DrawingTextEntity) {
|
||||
self.textView = DrawingTextView(frame: .zero)
|
||||
self.textView.clipsToBounds = false
|
||||
|
||||
self.textView.backgroundColor = .clear
|
||||
self.textView.text = entity.text
|
||||
self.textView.isEditable = false
|
||||
self.textView.isSelectable = false
|
||||
self.textView.contentInset = .zero
|
||||
self.textView.showsHorizontalScrollIndicator = false
|
||||
self.textView.showsVerticalScrollIndicator = false
|
||||
self.textView.scrollsToTop = false
|
||||
self.textView.isScrollEnabled = false
|
||||
self.textView.textContainerInset = .zero
|
||||
self.textView.minimumZoomScale = 1.0
|
||||
self.textView.maximumZoomScale = 1.0
|
||||
self.textView.keyboardAppearance = .dark
|
||||
self.textView.autocorrectionType = .no
|
||||
self.textView.spellCheckingType = .no
|
||||
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.textView.delegate = self
|
||||
self.addSubview(self.textView)
|
||||
|
||||
self.update(animated: false)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isSuspended = false
|
||||
private var _isEditing = false
|
||||
var isEditing: Bool {
|
||||
return self._isEditing || self.isSuspended
|
||||
}
|
||||
|
||||
private var previousEntity: DrawingTextEntity?
|
||||
private var fadeView: UIView?
|
||||
|
||||
@objc private func fadePressed() {
|
||||
self.endEditing()
|
||||
}
|
||||
|
||||
func beginEditing(accessoryView: UIView?) {
|
||||
self._isEditing = true
|
||||
if !self.textEntity.text.isEmpty {
|
||||
let previousEntity = self.textEntity.duplicate() as? DrawingTextEntity
|
||||
previousEntity?.uuid = self.textEntity.uuid
|
||||
self.previousEntity = previousEntity
|
||||
}
|
||||
|
||||
self.update(animated: false)
|
||||
|
||||
if let superview = self.superview {
|
||||
let fadeView = UIButton(frame: CGRect(origin: .zero, size: superview.frame.size))
|
||||
fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
||||
fadeView.addTarget(self, action: #selector(self.fadePressed), for: .touchUpInside)
|
||||
superview.insertSubview(fadeView, belowSubview: self)
|
||||
self.fadeView = fadeView
|
||||
fadeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
|
||||
self.textView.inputAccessoryView = accessoryView
|
||||
|
||||
self.textView.isEditable = true
|
||||
self.textView.isSelectable = true
|
||||
|
||||
self.textView.window?.makeKey()
|
||||
self.textView.becomeFirstResponder()
|
||||
|
||||
UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0.0) {
|
||||
self.transform = .identity
|
||||
if let superview = self.superview {
|
||||
self.center = CGPoint(x: superview.bounds.width / 2.0, y: superview.bounds.height / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
if let selectionView = self.selectionView as? DrawingTextEntititySelectionView {
|
||||
selectionView.alpha = 0.0
|
||||
if !self.textEntity.text.isEmpty {
|
||||
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func endEditing(reset: Bool = false) {
|
||||
self._isEditing = false
|
||||
self.textView.resignFirstResponder()
|
||||
|
||||
self.textView.isEditable = false
|
||||
self.textView.isSelectable = false
|
||||
|
||||
if reset {
|
||||
if let previousEntity = self.previousEntity {
|
||||
self.textEntity.color = previousEntity.color
|
||||
self.textEntity.style = previousEntity.style
|
||||
self.textEntity.alignment = previousEntity.alignment
|
||||
self.textEntity.font = previousEntity.font
|
||||
self.textEntity.text = previousEntity.text
|
||||
|
||||
self.previousEntity = nil
|
||||
} else {
|
||||
self.containerView?.remove(uuid: self.textEntity.uuid)
|
||||
}
|
||||
} else {
|
||||
self.textEntity.text = self.textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if self.textEntity.text.isEmpty {
|
||||
self.containerView?.remove(uuid: self.textEntity.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
self.textView.text = self.textEntity.text
|
||||
|
||||
if let fadeView = self.fadeView {
|
||||
self.fadeView = nil
|
||||
fadeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fadeView] _ in
|
||||
fadeView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0.0) {
|
||||
self.transform = CGAffineTransformMakeRotation(self.textEntity.rotation)
|
||||
self.center = self.textEntity.position
|
||||
}
|
||||
self.update(animated: false)
|
||||
|
||||
if let selectionView = self.selectionView as? DrawingTextEntititySelectionView {
|
||||
selectionView.alpha = 1.0
|
||||
selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
func suspendEditing() {
|
||||
self.isSuspended = true
|
||||
self.textView.resignFirstResponder()
|
||||
|
||||
if let fadeView = self.fadeView {
|
||||
fadeView.alpha = 0.0
|
||||
fadeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
func resumeEditing() {
|
||||
self.isSuspended = false
|
||||
self.textView.becomeFirstResponder()
|
||||
|
||||
if let fadeView = self.fadeView {
|
||||
fadeView.alpha = 1.0
|
||||
fadeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
self.sizeToFit()
|
||||
self.textView.setNeedsDisplay()
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
var result = self.textView.sizeThatFits(CGSize(width: self.textEntity.width, height: .greatestFiniteMagnitude))
|
||||
result.width = max(224.0, ceil(result.width) + 20.0)
|
||||
result.height = ceil(result.height) //+ 20.0 + (self.textView.font?.pointSize ?? 0.0) // * _font.sizeCorrection;
|
||||
return result;
|
||||
}
|
||||
|
||||
override func sizeToFit() {
|
||||
let center = self.center
|
||||
let transform = self.transform
|
||||
self.transform = .identity
|
||||
super.sizeToFit()
|
||||
self.center = center
|
||||
self.transform = transform
|
||||
|
||||
//entity changed
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
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
|
||||
}
|
||||
|
||||
override func update(animated: Bool = false) {
|
||||
if !self.isEditing {
|
||||
self.center = self.textEntity.position
|
||||
self.transform = CGAffineTransformMakeRotation(self.textEntity.rotation)
|
||||
}
|
||||
|
||||
let minFontSize = max(10.0, min(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.05)
|
||||
let maxFontSize = max(10.0, min(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.45)
|
||||
let fontSize = minFontSize + (maxFontSize - minFontSize) * self.textEntity.fontSize
|
||||
|
||||
switch self.textEntity.font {
|
||||
case .sanFrancisco:
|
||||
self.textView.font = Font.with(size: fontSize, design: .regular, weight: .semibold)
|
||||
case .newYork:
|
||||
self.textView.font = Font.with(size: fontSize, design: .serif, weight: .semibold)
|
||||
case .monospaced:
|
||||
self.textView.font = Font.with(size: fontSize, design: .monospace, weight: .semibold)
|
||||
case .round:
|
||||
self.textView.font = Font.with(size: fontSize, design: .round, weight: .semibold)
|
||||
}
|
||||
self.textView.textAlignment = self.textEntity.alignment.alignment
|
||||
|
||||
let color = self.textEntity.color.toUIColor()
|
||||
switch self.textEntity.style {
|
||||
case .regular:
|
||||
self.textView.textColor = color
|
||||
self.textView.strokeColor = nil
|
||||
self.textView.frameColor = nil
|
||||
case .filled:
|
||||
self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
self.textView.strokeColor = nil
|
||||
self.textView.frameColor = color
|
||||
case .semi:
|
||||
self.textView.textColor = color
|
||||
self.textView.strokeColor = nil
|
||||
self.textView.frameColor = UIColor(rgb: 0xffffff, alpha: 0.75)
|
||||
case .stroke:
|
||||
self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
self.textView.strokeColor = color
|
||||
self.textView.frameColor = nil
|
||||
}
|
||||
|
||||
if case .regular = self.textEntity.style {
|
||||
self.textView.layer.shadowColor = UIColor.black.cgColor
|
||||
self.textView.layer.shadowOffset = CGSize(width: 0.0, height: 4.0)
|
||||
self.textView.layer.shadowOpacity = 0.4
|
||||
self.textView.layer.shadowRadius = 4.0
|
||||
} else {
|
||||
self.textView.layer.shadowColor = nil
|
||||
self.textView.layer.shadowOffset = .zero
|
||||
self.textView.layer.shadowOpacity = 0.0
|
||||
self.textView.layer.shadowRadius = 0.0
|
||||
}
|
||||
|
||||
self.sizeToFit()
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
override func updateSelectionView() {
|
||||
super.updateSelectionView()
|
||||
|
||||
guard let selectionView = self.selectionView as? DrawingTextEntititySelectionView else {
|
||||
return
|
||||
}
|
||||
|
||||
// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
// selectionView.scale = scale
|
||||
|
||||
selectionView.transform = CGAffineTransformMakeRotation(self.textEntity.rotation)
|
||||
}
|
||||
|
||||
override func makeSelectionView() -> DrawingEntitySelectionView {
|
||||
if let selectionView = self.selectionView {
|
||||
return selectionView
|
||||
}
|
||||
let selectionView = DrawingTextEntititySelectionView()
|
||||
selectionView.entityView = self
|
||||
return selectionView
|
||||
}
|
||||
|
||||
func getRenderImage() -> UIImage? {
|
||||
let rect = self.bounds
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0)
|
||||
self.drawHierarchy(in: rect, afterScreenUpdates: false)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate {
|
||||
private let border = SimpleShapeLayer()
|
||||
private let leftHandle = SimpleShapeLayer()
|
||||
private let rightHandle = SimpleShapeLayer()
|
||||
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
|
||||
let handles = [
|
||||
self.leftHandle,
|
||||
self.rightHandle
|
||||
]
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
self.isOpaque = false
|
||||
|
||||
self.border.lineCap = .round
|
||||
self.border.fillColor = UIColor.clear.cgColor
|
||||
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
|
||||
self.layer.addSublayer(self.border)
|
||||
|
||||
for handle in handles {
|
||||
handle.bounds = handleBounds
|
||||
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
|
||||
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
|
||||
handle.rasterizationScale = UIScreen.main.scale
|
||||
handle.shouldRasterize = true
|
||||
|
||||
self.layer.addSublayer(handle)
|
||||
}
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var scale: CGFloat = 1.0 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override var selectionInset: CGFloat {
|
||||
return 15.0
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else {
|
||||
return
|
||||
}
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
|
||||
var updatedFontSize = entity.fontSize
|
||||
var updatedPosition = entity.position
|
||||
var updatedRotation = entity.rotation
|
||||
|
||||
if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle {
|
||||
var deltaX = gestureRecognizer.translation(in: self).x
|
||||
if self.currentHandle === self.leftHandle {
|
||||
deltaX *= -1.0
|
||||
}
|
||||
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
|
||||
updatedFontSize = max(0.0, min(1.0, updatedFontSize * scaleDelta))
|
||||
|
||||
let deltaAngle: CGFloat
|
||||
if self.currentHandle === self.leftHandle {
|
||||
deltaAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x)
|
||||
} else {
|
||||
deltaAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x)
|
||||
}
|
||||
updatedRotation = deltaAngle
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
}
|
||||
|
||||
entity.fontSize = updatedFontSize
|
||||
entity.position = updatedPosition
|
||||
entity.rotation = updatedRotation
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.fontSize = max(0.0, min(1.0, entity.fontSize * scale))
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let rotation = gestureRecognizer.rotation
|
||||
entity.rotation += rotation
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.rotation = 0.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
let inset = self.selectionInset - 10.0
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
|
||||
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
|
||||
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
|
||||
let lineWidth = (1.0 + UIScreenPixel) / self.scale
|
||||
|
||||
let handles = [
|
||||
self.leftHandle,
|
||||
self.rightHandle
|
||||
]
|
||||
|
||||
for handle in handles {
|
||||
handle.path = handlePath
|
||||
handle.bounds = bounds
|
||||
handle.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||
|
||||
self.border.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber]
|
||||
self.border.lineWidth = 2.0 / self.scale
|
||||
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0)), cornerRadius: 12.0 / self.scale).cgPath
|
||||
}
|
||||
}
|
||||
|
||||
private class DrawingTextLayoutManager: NSLayoutManager {
|
||||
var radius: CGFloat
|
||||
var maxIndex: Int = 0
|
||||
|
||||
private(set) var path: UIBezierPath?
|
||||
var rectArray: [CGRect] = []
|
||||
|
||||
var strokeColor: UIColor?
|
||||
var strokeWidth: CGFloat = 0.0
|
||||
var strokeOffset: CGPoint = .zero
|
||||
|
||||
var frameColor: UIColor?
|
||||
var frameWidthInset: CGFloat = 0.0
|
||||
|
||||
override init() {
|
||||
self.radius = 8.0
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func prepare() {
|
||||
self.path = nil
|
||||
self.rectArray.removeAll()
|
||||
|
||||
self.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: ((self.textStorage?.string ?? "") as NSString).length)) { rect, usedRect, textContainer, glyphRange, _ in
|
||||
var ignoreRange = false
|
||||
let charecterRange = self.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
|
||||
let substring = ((self.textStorage?.string ?? "") as NSString).substring(with: charecterRange)
|
||||
if substring.trimmingCharacters(in: .newlines).isEmpty {
|
||||
ignoreRange = true
|
||||
}
|
||||
|
||||
if !ignoreRange {
|
||||
let newRect = CGRect(origin: CGPoint(x: usedRect.minX - self.frameWidthInset, y: usedRect.minY), size: CGSize(width: usedRect.width + self.frameWidthInset * 2.0, height: usedRect.height))
|
||||
self.rectArray.append(newRect)
|
||||
}
|
||||
}
|
||||
|
||||
self.preprocess()
|
||||
}
|
||||
|
||||
private func preprocess() {
|
||||
self.maxIndex = 0
|
||||
if self.rectArray.count < 2 {
|
||||
return
|
||||
}
|
||||
for i in 1 ..< self.rectArray.count {
|
||||
self.maxIndex = i
|
||||
self.processRectIndex(i)
|
||||
}
|
||||
}
|
||||
|
||||
private func processRectIndex(_ index: Int) {
|
||||
guard self.rectArray.count >= 2 && index > 0 && index <= self.maxIndex else {
|
||||
return
|
||||
}
|
||||
|
||||
let last = self.rectArray[index - 1]
|
||||
let cur = self.rectArray[index]
|
||||
|
||||
self.radius = cur.height * 0.18
|
||||
|
||||
let t1 = ((cur.minX - last.minX < 2.0 * self.radius) && (cur.minX > last.minX)) || ((cur.maxX - last.maxX > -2.0 * self.radius) && (cur.maxX < last.maxX))
|
||||
let t2 = ((last.minX - cur.minX < 2.0 * self.radius) && (last.minX > cur.minX)) || ((last.maxX - cur.maxX > -2.0 * self.radius) && (last.maxX < cur.maxX))
|
||||
|
||||
if t2 {
|
||||
let newRect = CGRect(origin: CGPoint(x: cur.minX, y: last.minY), size: CGSize(width: cur.width, height: last.height))
|
||||
self.rectArray[index - 1] = newRect
|
||||
self.processRectIndex(index - 1)
|
||||
}
|
||||
if t1 {
|
||||
let newRect = CGRect(origin: CGPoint(x: last.minX, y: cur.minY), size: CGSize(width: last.width, height: cur.height))
|
||||
self.rectArray[index] = newRect
|
||||
self.processRectIndex(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
override func showCGGlyphs(_ glyphs: UnsafePointer<CGGlyph>, positions: UnsafePointer<CGPoint>, count glyphCount: Int, font: UIFont, matrix textMatrix: CGAffineTransform, attributes: [NSAttributedString.Key : Any] = [:], in graphicsContext: CGContext) {
|
||||
if let strokeColor = self.strokeColor {
|
||||
graphicsContext.setStrokeColor(strokeColor.cgColor)
|
||||
graphicsContext.setLineJoin(.round)
|
||||
|
||||
let lineWidth = self.strokeWidth > 0.0 ? self.strokeWidth : font.pointSize / 9.0
|
||||
graphicsContext.setLineWidth(lineWidth)
|
||||
graphicsContext.setTextDrawingMode(.stroke)
|
||||
|
||||
graphicsContext.saveGState()
|
||||
graphicsContext.translateBy(x: self.strokeOffset.x, y: self.strokeOffset.y)
|
||||
|
||||
super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: attributes, in: graphicsContext)
|
||||
|
||||
graphicsContext.restoreGState()
|
||||
|
||||
let textColor: UIColor = attributes[NSAttributedString.Key.foregroundColor] as? UIColor ?? UIColor.white
|
||||
|
||||
graphicsContext.setFillColor(textColor.cgColor)
|
||||
graphicsContext.setTextDrawingMode(.fill)
|
||||
}
|
||||
super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: attributes, in: graphicsContext)
|
||||
}
|
||||
|
||||
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
|
||||
if let frameColor = self.frameColor, let context = UIGraphicsGetCurrentContext() {
|
||||
context.saveGState()
|
||||
|
||||
context.translateBy(x: origin.x, y: origin.y)
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(frameColor.cgColor)
|
||||
context.setStrokeColor(frameColor.cgColor)
|
||||
|
||||
self.prepare()
|
||||
self.preprocess()
|
||||
|
||||
let path = UIBezierPath()
|
||||
|
||||
var last: CGRect = .null
|
||||
for i in 0 ..< self.rectArray.count {
|
||||
let cur = self.rectArray[i]
|
||||
self.radius = cur.height * 0.18
|
||||
|
||||
path.append(UIBezierPath(roundedRect: cur, cornerRadius: self.radius))
|
||||
if i == 0 {
|
||||
last = cur
|
||||
} else if i > 0 && abs(last.maxY - cur.minY) < 10.0 {
|
||||
let a = cur.origin
|
||||
let b = CGPoint(x: cur.maxX, y: cur.minY)
|
||||
let c = CGPoint(x: last.minX, y: last.maxY)
|
||||
let d = CGPoint(x: last.maxX, y: last.maxY)
|
||||
|
||||
if a.x - c.x >= 2.0 * self.radius {
|
||||
let addPath = UIBezierPath(arcCenter: CGPoint(x: a.x - self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi * 0.5 * 3.0, endAngle: 0.0, clockwise: true)
|
||||
addPath.append(
|
||||
UIBezierPath(arcCenter: CGPoint(x: a.x + self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi, endAngle: 3.0 * .pi * 0.5, clockwise: true)
|
||||
)
|
||||
addPath.addLine(to: CGPoint(x: a.x - self.radius, y: a.y))
|
||||
path.append(addPath)
|
||||
}
|
||||
if a.x == c.x {
|
||||
path.move(to: CGPoint(x: a.x, y: a.y - self.radius))
|
||||
path.addLine(to: CGPoint(x: a.x, y: a.y + self.radius))
|
||||
path.addArc(withCenter: CGPoint(x: a.x + self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi, endAngle: .pi * 0.5 * 3.0, clockwise: true)
|
||||
path.addArc(withCenter: CGPoint(x: a.x + self.radius, y: a.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true)
|
||||
}
|
||||
if d.x - b.x >= 2.0 * self.radius {
|
||||
let addPath = UIBezierPath(arcCenter: CGPoint(x: b.x + self.radius, y: b.y + self.radius), radius: self.radius, startAngle: .pi * 0.5 * 3.0, endAngle: .pi, clockwise: false)
|
||||
addPath.append(
|
||||
UIBezierPath(arcCenter: CGPoint(x: b.x - self.radius, y: b.y + self.radius), radius: self.radius, startAngle: 0.0, endAngle: 3.0 * .pi * 0.5, clockwise: false)
|
||||
)
|
||||
addPath.addLine(to: CGPoint(x: b.x + self.radius, y: b.y))
|
||||
path.append(addPath)
|
||||
}
|
||||
if d.x == b.x {
|
||||
path.move(to: CGPoint(x: b.x, y: b.y - self.radius))
|
||||
path.addLine(to: CGPoint(x: b.x, y: b.y + self.radius))
|
||||
path.addArc(withCenter: CGPoint(x: b.x - self.radius, y: b.y + self.radius), radius: self.radius, startAngle: 0.0, endAngle: 3.0 * .pi * 0.5, clockwise: false)
|
||||
path.addArc(withCenter: CGPoint(x: b.x - self.radius, y: b.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: 0.0, clockwise: false)
|
||||
}
|
||||
if c.x - a.x >= 2.0 * self.radius {
|
||||
let addPath = UIBezierPath(arcCenter: CGPoint(x: c.x - self.radius, y: c.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: 0.0, clockwise: false)
|
||||
addPath.append(
|
||||
UIBezierPath(arcCenter: CGPoint(x: c.x + self.radius, y: c.y - self.radius), radius: self.radius, startAngle: .pi, endAngle: .pi * 0.5, clockwise: false)
|
||||
)
|
||||
addPath.addLine(to: CGPoint(x: c.x - self.radius, y: c.y))
|
||||
path.append(addPath)
|
||||
}
|
||||
if b.x - d.x >= 2.0 * self.radius {
|
||||
let addPath = UIBezierPath(arcCenter: CGPoint(x: d.x + self.radius, y: d.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true)
|
||||
addPath.append(
|
||||
UIBezierPath(arcCenter: CGPoint(x: d.x - self.radius, y: d.y - self.radius), radius: self.radius, startAngle: 0.0, endAngle: .pi * 0.5, clockwise: true)
|
||||
)
|
||||
addPath.addLine(to: CGPoint(x: d.x + self.radius, y: d.y))
|
||||
path.append(addPath)
|
||||
}
|
||||
|
||||
last = cur
|
||||
}
|
||||
}
|
||||
self.path = path
|
||||
|
||||
self.path?.fill()
|
||||
self.path?.stroke()
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DrawingTextStorage: NSTextStorage {
|
||||
let impl: NSTextStorage
|
||||
|
||||
override init() {
|
||||
self.impl = NSTextStorage()
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var string: String {
|
||||
return self.impl.string
|
||||
}
|
||||
|
||||
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
|
||||
return self.impl.attributes(at: location, effectiveRange: range)
|
||||
}
|
||||
|
||||
override func replaceCharacters(in range: NSRange, with str: String) {
|
||||
self.beginEditing()
|
||||
self.impl.replaceCharacters(in: range, with: str)
|
||||
self.edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
|
||||
self.endEditing()
|
||||
}
|
||||
|
||||
override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
|
||||
self.beginEditing()
|
||||
self.impl.setAttributes(attrs, range: range)
|
||||
self.edited(.editedAttributes, range: range, changeInLength: 0)
|
||||
self.endEditing()
|
||||
}
|
||||
}
|
||||
|
||||
private class DrawingTextView: UITextView {
|
||||
var drawingLayoutManager: DrawingTextLayoutManager {
|
||||
return self.layoutManager as! DrawingTextLayoutManager
|
||||
}
|
||||
|
||||
var strokeColor: UIColor? {
|
||||
didSet {
|
||||
self.drawingLayoutManager.strokeColor = self.strokeColor
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var strokeWidth: CGFloat = 0.0 {
|
||||
didSet {
|
||||
self.drawingLayoutManager.strokeWidth = self.strokeWidth
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var strokeOffset: CGPoint = .zero {
|
||||
didSet {
|
||||
self.drawingLayoutManager.strokeOffset = self.strokeOffset
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var frameColor: UIColor? {
|
||||
didSet {
|
||||
self.drawingLayoutManager.frameColor = self.frameColor
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var frameWidthInset: CGFloat = 0.0 {
|
||||
didSet {
|
||||
self.drawingLayoutManager.frameWidthInset = self.frameWidthInset
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
override var font: UIFont? {
|
||||
get {
|
||||
return super.font
|
||||
}
|
||||
set {
|
||||
super.font = newValue
|
||||
if let font = newValue {
|
||||
self.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(font.pointSize * 0.24)
|
||||
}
|
||||
|
||||
self.fixTypingAttributes()
|
||||
}
|
||||
}
|
||||
|
||||
override var textColor: UIColor? {
|
||||
get {
|
||||
return super.textColor
|
||||
}
|
||||
set {
|
||||
super.textColor = newValue
|
||||
self.fixTypingAttributes()
|
||||
}
|
||||
}
|
||||
|
||||
init(frame: CGRect) {
|
||||
let textStorage = DrawingTextStorage()
|
||||
let layoutManager = DrawingTextLayoutManager()
|
||||
|
||||
let textContainer = NSTextContainer(size: CGSize(width: 0.0, height: .greatestFiniteMagnitude))
|
||||
textContainer.widthTracksTextView = true
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
|
||||
super.init(frame: frame, textContainer: textContainer)
|
||||
|
||||
self.tintColor = UIColor.white
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func caretRect(for position: UITextPosition) -> CGRect {
|
||||
var rect = super.caretRect(for: position)
|
||||
rect.size.width = floorToScreenPixels(rect.size.height / 25.0)
|
||||
return rect
|
||||
}
|
||||
|
||||
override func insertText(_ text: String) {
|
||||
self.fixTypingAttributes()
|
||||
super.insertText(text)
|
||||
self.fixTypingAttributes()
|
||||
}
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
self.fixTypingAttributes()
|
||||
super.paste(sender)
|
||||
self.fixTypingAttributes()
|
||||
}
|
||||
|
||||
private func fixTypingAttributes() {
|
||||
var attributes: [NSAttributedString.Key: Any] = [:]
|
||||
if let font = self.font {
|
||||
attributes[NSAttributedString.Key.font] = font
|
||||
}
|
||||
if let textColor = self.textColor {
|
||||
attributes[NSAttributedString.Key.foregroundColor] = textColor
|
||||
}
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = self.textAlignment
|
||||
attributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle
|
||||
self.typingAttributes = attributes
|
||||
}
|
||||
}
|
||||
1231
submodules/DrawingUI/Sources/DrawingTools.swift
Normal file
950
submodules/DrawingUI/Sources/DrawingUtils.swift
Normal file
@@ -0,0 +1,950 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import QuartzCore
|
||||
import simd
|
||||
|
||||
struct DrawingColor: Equatable {
|
||||
public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
|
||||
public var red: CGFloat
|
||||
public var green: CGFloat
|
||||
public var blue: CGFloat
|
||||
public var alpha: CGFloat
|
||||
|
||||
public var position: CGPoint?
|
||||
|
||||
var isClear: Bool {
|
||||
return self.red.isZero && self.green.isZero && self.blue.isZero && self.alpha.isZero
|
||||
}
|
||||
|
||||
public init(
|
||||
red: CGFloat,
|
||||
green: CGFloat,
|
||||
blue: CGFloat,
|
||||
alpha: CGFloat = 1.0,
|
||||
position: CGPoint? = nil
|
||||
) {
|
||||
self.red = red
|
||||
self.green = green
|
||||
self.blue = blue
|
||||
self.alpha = alpha
|
||||
self.position = position
|
||||
}
|
||||
|
||||
public init(color: UIColor) {
|
||||
var red: CGFloat = 0.0
|
||||
var green: CGFloat = 0.0
|
||||
var blue: CGFloat = 0.0
|
||||
var alpha: CGFloat = 1.0
|
||||
if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
|
||||
self.init(red: red, green: green, blue: blue, alpha: alpha)
|
||||
} else if color.getWhite(&red, alpha: &alpha) {
|
||||
self.init(red: red, green: red, blue: red, alpha: alpha)
|
||||
} else {
|
||||
self.init(red: 0.0, green: 0.0, blue: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
public init(rgb: UInt32) {
|
||||
self.init(color: UIColor(rgb: rgb))
|
||||
}
|
||||
|
||||
func withUpdatedRed(_ red: CGFloat) -> DrawingColor {
|
||||
return DrawingColor(
|
||||
red: red,
|
||||
green: self.green,
|
||||
blue: self.blue,
|
||||
alpha: self.alpha
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedGreen(_ green: CGFloat) -> DrawingColor {
|
||||
return DrawingColor(
|
||||
red: self.red,
|
||||
green: green,
|
||||
blue: self.blue,
|
||||
alpha: self.alpha
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedBlue(_ blue: CGFloat) -> DrawingColor {
|
||||
return DrawingColor(
|
||||
red: self.red,
|
||||
green: self.green,
|
||||
blue: blue,
|
||||
alpha: self.alpha
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedAlpha(_ alpha: CGFloat) -> DrawingColor {
|
||||
return DrawingColor(
|
||||
red: self.red,
|
||||
green: self.green,
|
||||
blue: self.blue,
|
||||
alpha: alpha,
|
||||
position: self.position
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedPosition(_ position: CGPoint) -> DrawingColor {
|
||||
return DrawingColor(
|
||||
red: self.red,
|
||||
green: self.green,
|
||||
blue: self.blue,
|
||||
alpha: self.alpha,
|
||||
position: position
|
||||
)
|
||||
}
|
||||
|
||||
func toUIColor() -> UIColor {
|
||||
return UIColor(
|
||||
red: self.red,
|
||||
green: self.green,
|
||||
blue: self.blue,
|
||||
alpha: self.alpha
|
||||
)
|
||||
}
|
||||
|
||||
func toCGColor() -> CGColor {
|
||||
return self.toUIColor().cgColor
|
||||
}
|
||||
|
||||
func toFloat4() -> vector_float4 {
|
||||
return [
|
||||
simd_float1(self.red),
|
||||
simd_float1(self.green),
|
||||
simd_float1(self.blue),
|
||||
simd_float1(self.alpha)
|
||||
]
|
||||
}
|
||||
|
||||
public static func ==(lhs: DrawingColor, rhs: DrawingColor) -> Bool {
|
||||
if lhs.red != rhs.red {
|
||||
return false
|
||||
}
|
||||
if lhs.green != rhs.green {
|
||||
return false
|
||||
}
|
||||
if lhs.blue != rhs.blue {
|
||||
return false
|
||||
}
|
||||
if lhs.alpha != rhs.alpha {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension UIBezierPath {
|
||||
convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) {
|
||||
self.init()
|
||||
|
||||
let path = CGMutablePath()
|
||||
|
||||
let topLeft = rect.origin
|
||||
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
|
||||
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
|
||||
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
|
||||
|
||||
if topLeftRadius != .zero {
|
||||
path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
|
||||
} else {
|
||||
path.move(to: CGPoint(x: topLeft.x, y: topLeft.y))
|
||||
}
|
||||
|
||||
if topRightRadius != .zero {
|
||||
path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y))
|
||||
path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: topRight.x, y: topRight.y))
|
||||
}
|
||||
|
||||
if bottomRightRadius != .zero {
|
||||
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius))
|
||||
path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y))
|
||||
}
|
||||
|
||||
if bottomLeftRadius != .zero {
|
||||
path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y))
|
||||
path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y))
|
||||
}
|
||||
|
||||
if topLeftRadius != .zero {
|
||||
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius))
|
||||
path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y))
|
||||
}
|
||||
|
||||
path.closeSubpath()
|
||||
self.cgPath = path
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint {
|
||||
func isEqual(to point: CGPoint, epsilon: CGFloat) -> Bool {
|
||||
if x - epsilon <= point.x && point.x <= x + epsilon && y - epsilon <= point.y && point.y <= y + epsilon {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static public func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
|
||||
}
|
||||
|
||||
static public func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
|
||||
}
|
||||
|
||||
static public func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
|
||||
}
|
||||
|
||||
static public func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: lhs.x / rhs, y: lhs.y / rhs)
|
||||
}
|
||||
|
||||
var length: CGFloat {
|
||||
return sqrt(self.x * self.x + self.y * self.y)
|
||||
}
|
||||
|
||||
static func middle(p1: CGPoint, p2: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5)
|
||||
}
|
||||
|
||||
func distance(to point: CGPoint) -> CGFloat {
|
||||
return sqrt(pow((point.x - self.x), 2) + pow((point.y - self.y), 2))
|
||||
}
|
||||
|
||||
func distanceSquared(to point: CGPoint) -> CGFloat {
|
||||
return pow((point.x - self.x), 2) + pow((point.y - self.y), 2)
|
||||
}
|
||||
|
||||
func angle(to point: CGPoint) -> CGFloat {
|
||||
return atan2((point.y - self.y), (point.x - self.x))
|
||||
}
|
||||
|
||||
func pointAt(distance: CGFloat, angle: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: distance * cos(angle) + self.x, y: distance * sin(angle) + self.y)
|
||||
}
|
||||
|
||||
func point(to point: CGPoint, t: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: self.x + t * (point.x - self.x), y: self.y + t * (point.y - self.y))
|
||||
}
|
||||
|
||||
func perpendicularPointOnLine(start: CGPoint, end: CGPoint) -> CGPoint {
|
||||
let l2 = start.distanceSquared(to: end)
|
||||
if l2.isZero {
|
||||
return start
|
||||
}
|
||||
let t = ((self.x - start.x) * (end.x - start.x) + (self.y - start.y) * (end.y - start.y)) / l2
|
||||
return CGPoint(x: start.x + t * (end.x - start.x), y: start.y + t * (end.y - start.y))
|
||||
}
|
||||
|
||||
func linearBezierPoint(to: CGPoint, t: CGFloat) -> CGPoint {
|
||||
let dx = to.x - x;
|
||||
let dy = to.y - y;
|
||||
|
||||
let px = x + (t * dx);
|
||||
let py = y + (t * dy);
|
||||
|
||||
return CGPoint(x: px, y: py)
|
||||
}
|
||||
|
||||
fileprivate func _cubicBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat {
|
||||
let _t = 1 - t;
|
||||
let _t2 = _t * _t;
|
||||
let _t3 = _t * _t * _t ;
|
||||
let t2 = t * t;
|
||||
let t3 = t * t * t;
|
||||
|
||||
return _t3 * start +
|
||||
3.0 * _t2 * t * c1 +
|
||||
3.0 * _t * t2 * c2 +
|
||||
t3 * end;
|
||||
}
|
||||
|
||||
func cubicBezierPoint(to: CGPoint, controlPoint1 c1: CGPoint, controlPoint2 c2: CGPoint, t: CGFloat) -> CGPoint {
|
||||
let x = _cubicBezier(t, self.x, c1.x, c2.x, to.x);
|
||||
let y = _cubicBezier(t, self.y, c1.y, c2.y, to.y);
|
||||
|
||||
return CGPoint(x: x, y: y);
|
||||
}
|
||||
|
||||
fileprivate func _quadBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ end: CGFloat) -> CGFloat {
|
||||
let _t = 1 - t;
|
||||
let _t2 = _t * _t;
|
||||
let t2 = t * t;
|
||||
|
||||
return _t2 * start +
|
||||
2 * _t * t * c1 +
|
||||
t2 * end;
|
||||
}
|
||||
|
||||
func quadBezierPoint(to: CGPoint, controlPoint: CGPoint, t: CGFloat) -> CGPoint {
|
||||
let x = _quadBezier(t, self.x, controlPoint.x, to.x);
|
||||
let y = _quadBezier(t, self.y, controlPoint.y, to.y);
|
||||
|
||||
return CGPoint(x: x, y: y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension CGPath {
|
||||
static func star(in rect: CGRect, extrusion: CGFloat, points: Int = 5) -> CGPath {
|
||||
func pointFrom(angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: radius * cos(angle) + offset.x, y: radius * sin(angle) + offset.y)
|
||||
}
|
||||
|
||||
let path = CGMutablePath()
|
||||
|
||||
let center = rect.center.offsetBy(dx: 0.0, dy: rect.height * 0.05)
|
||||
var angle: CGFloat = -CGFloat(.pi / 2.0)
|
||||
let angleIncrement = CGFloat(.pi * 2.0 / Double(points))
|
||||
let radius = rect.width / 2.0
|
||||
|
||||
var firstPoint = true
|
||||
for _ in 1 ... points {
|
||||
let point = center.pointAt(distance: radius, angle: angle)
|
||||
let nextPoint = center.pointAt(distance: radius, angle: angle + angleIncrement)
|
||||
let midPoint = center.pointAt(distance: extrusion, angle: angle + angleIncrement * 0.5)
|
||||
|
||||
if firstPoint {
|
||||
firstPoint = false
|
||||
path.move(to: point)
|
||||
}
|
||||
path.addLine(to: midPoint)
|
||||
path.addLine(to: nextPoint)
|
||||
|
||||
angle += angleIncrement
|
||||
}
|
||||
path.closeSubpath()
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
static func arrow(from point: CGPoint, controlPoint: CGPoint, width: CGFloat, height: CGFloat, isOpen: Bool) -> CGPath {
|
||||
let angle = atan2(point.y - controlPoint.y, point.x - controlPoint.x)
|
||||
let angleAdjustment = atan2(width, -height)
|
||||
let distance = hypot(width, height)
|
||||
|
||||
let path = CGMutablePath()
|
||||
path.move(to: point)
|
||||
path.addLine(to: point.pointAt(distance: distance, angle: angle - angleAdjustment))
|
||||
if isOpen {
|
||||
path.addLine(to: point)
|
||||
}
|
||||
path.addLine(to: point.pointAt(distance: distance, angle: angle + angleAdjustment))
|
||||
if isOpen {
|
||||
path.addLine(to: point)
|
||||
} else {
|
||||
path.closeSubpath()
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
static func curve(start: CGPoint, end: CGPoint, mid: CGPoint, lineWidth: CGFloat?, arrowSize: CGSize?, twoSided: Bool = false) -> CGPath {
|
||||
let linePath = CGMutablePath()
|
||||
|
||||
let controlPoints = configureControlPoints(data: [start, mid, end])
|
||||
var lineStart = start
|
||||
if let arrowSize = arrowSize, twoSided {
|
||||
lineStart = start.pointAt(distance: -arrowSize.height * 0.5, angle: controlPoints[0].ctrl1.angle(to: start))
|
||||
}
|
||||
linePath.move(to: lineStart)
|
||||
linePath.addCurve(to: mid, control1: controlPoints[0].ctrl1, control2: controlPoints[0].ctrl2)
|
||||
|
||||
var lineEnd = end
|
||||
if let arrowSize = arrowSize {
|
||||
lineEnd = end.pointAt(distance: -arrowSize.height * 0.5, angle: controlPoints[1].ctrl1.angle(to: end))
|
||||
}
|
||||
linePath.addCurve(to: lineEnd, control1: controlPoints[1].ctrl1, control2: controlPoints[1].ctrl2)
|
||||
|
||||
let path: CGMutablePath
|
||||
if let lineWidth = lineWidth, let mutablePath = linePath.copy(strokingWithWidth: lineWidth, lineCap: .square, lineJoin: .round, miterLimit: 0.0).mutableCopy() {
|
||||
path = mutablePath
|
||||
} else {
|
||||
path = linePath
|
||||
}
|
||||
|
||||
if let arrowSize = arrowSize {
|
||||
let arrowPath = arrow(from: end, controlPoint: controlPoints[1].ctrl1, width: arrowSize.width, height: arrowSize.height, isOpen: false)
|
||||
path.addPath(arrowPath)
|
||||
|
||||
if twoSided {
|
||||
let secondArrowPath = arrow(from: start, controlPoint: controlPoints[0].ctrl1, width: arrowSize.width, height: arrowSize.height, isOpen: false)
|
||||
path.addPath(secondArrowPath)
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
static func bubble(in rect: CGRect, cornerRadius: CGFloat, smallCornerRadius: CGFloat, tailPosition: CGPoint, tailWidth: CGFloat) -> CGPath {
|
||||
let r1 = min(cornerRadius, min(rect.width, rect.height) / 3.0)
|
||||
let r2 = min(smallCornerRadius, min(rect.width, rect.height) / 10.0)
|
||||
|
||||
let ax = tailPosition.x * rect.width
|
||||
let ay = tailPosition.y
|
||||
|
||||
let width = min(max(tailWidth, ay / 2.0), rect.width / 4.0)
|
||||
let angle = atan2(ay, width)
|
||||
let h = r2 / tan(angle / 2.0)
|
||||
|
||||
let r1a = min(r1, min(rect.maxX - ax, ax - rect.minX) * 0.5)
|
||||
let r2a = min(r2, min(rect.maxX - ax, ax - rect.minX) * 0.2)
|
||||
|
||||
let path = CGMutablePath()
|
||||
path.addArc(center: CGPoint(x: rect.minX + r1, y: rect.minY + r1), radius: r1, startAngle: .pi, endAngle: .pi * 3.0 / 2.0, clockwise: false)
|
||||
path.addArc(center: CGPoint(x: rect.maxX - r1, y: rect.minY + r1), radius: r1, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false)
|
||||
|
||||
if ax > rect.width / 2.0 {
|
||||
if ax < rect.width - 1 {
|
||||
path.addArc(center: CGPoint(x: rect.maxX - r1a, y: rect.maxY - r1a), radius: r1a, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false)
|
||||
path.addArc(center: CGPoint(x: rect.minX + ax + r2a, y: rect.maxY + r2a), radius: r2a, startAngle: .pi * 3.0 / 2.0, endAngle: .pi, clockwise: true)
|
||||
}
|
||||
path.addLine(to: CGPoint(x: rect.minX + ax, y: rect.maxY + ay))
|
||||
path.addArc(center: CGPoint(x: rect.minX + ax - width - r2, y: rect.maxY + h), radius: h, startAngle: -(CGFloat.pi / 2 - angle), endAngle: CGFloat.pi * 3 / 2, clockwise: true)
|
||||
path.addArc(center: CGPoint(x: rect.minX + r1, y: rect.maxY - r1), radius: r1, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false)
|
||||
} else {
|
||||
path.addArc(center: CGPoint(x: rect.maxX - r1, y: rect.maxY - r1), radius: r1, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false)
|
||||
path.addArc(center: CGPoint(x: rect.minX + ax + width + r2, y: rect.maxY + h), radius: h, startAngle: CGFloat.pi * 3 / 2, endAngle: CGFloat.pi * 3 / 2 - angle, clockwise: true)
|
||||
path.addLine(to: CGPoint(x: rect.minX + ax, y: rect.maxY + ay))
|
||||
if ax > 1 {
|
||||
path.addArc(center: CGPoint(x: rect.minX + ax - r2a, y: rect.maxY + r2a), radius: r2a, startAngle: 0, endAngle: CGFloat.pi * 3 / 2, clockwise: true)
|
||||
path.addArc(center: CGPoint(x: rect.minX + r1a, y: rect.maxY - r1a), radius: r1a, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false)
|
||||
}
|
||||
}
|
||||
|
||||
path.closeSubpath()
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
private func configureControlPoints(data: [CGPoint]) -> [(ctrl1: CGPoint, ctrl2: CGPoint)] {
|
||||
let segments = data.count - 1
|
||||
|
||||
if segments == 1 {
|
||||
let p0 = data[0]
|
||||
let p3 = data[1]
|
||||
|
||||
return [(p0, p3)]
|
||||
} else if segments > 1 {
|
||||
var ad: [CGFloat] = []
|
||||
var d: [CGFloat] = []
|
||||
var bd: [CGFloat] = []
|
||||
|
||||
var rhsArray: [CGPoint] = []
|
||||
|
||||
for i in 0 ..< segments {
|
||||
var rhsXValue: CGFloat = 0.0
|
||||
var rhsYValue: CGFloat = 0.0
|
||||
|
||||
let p0 = data[i]
|
||||
let p3 = data[i + 1]
|
||||
|
||||
if i == 0 {
|
||||
bd.append(0.0)
|
||||
d.append(2.0)
|
||||
ad.append(1.0)
|
||||
|
||||
rhsXValue = p0.x + 2.0 * p3.x
|
||||
rhsYValue = p0.y + 2.0 * p3.y
|
||||
} else if i == segments - 1 {
|
||||
bd.append(2.0)
|
||||
d.append(7.0)
|
||||
ad.append(0.0)
|
||||
|
||||
rhsXValue = 8.0 * p0.x + p3.x
|
||||
rhsYValue = 8.0 * p0.y + p3.y
|
||||
} else {
|
||||
bd.append(1.0)
|
||||
d.append(4.0)
|
||||
ad.append(1.0)
|
||||
|
||||
rhsXValue = 4.0 * p0.x + 2.0 * p3.x
|
||||
rhsYValue = 4.0 * p0.y + 2.0 * p3.y
|
||||
}
|
||||
|
||||
rhsArray.append(CGPoint(x: rhsXValue, y: rhsYValue))
|
||||
}
|
||||
|
||||
var firstControlPoints: [CGPoint?] = []
|
||||
var secondControlPoints: [CGPoint?] = []
|
||||
|
||||
var controlPoints : [(CGPoint, CGPoint)] = []
|
||||
|
||||
var solutionSet1 = [CGPoint?]()
|
||||
solutionSet1 = Array(repeating: nil, count: segments)
|
||||
|
||||
ad[0] = ad[0] / d[0]
|
||||
rhsArray[0].x = rhsArray[0].x / d[0]
|
||||
rhsArray[0].y = rhsArray[0].y / d[0]
|
||||
|
||||
if segments > 2 {
|
||||
for i in 1...segments - 2 {
|
||||
let rhsValueX = rhsArray[i].x
|
||||
let prevRhsValueX = rhsArray[i - 1].x
|
||||
|
||||
let rhsValueY = rhsArray[i].y
|
||||
let prevRhsValueY = rhsArray[i - 1].y
|
||||
|
||||
ad[i] = ad[i] / (d[i] - bd[i] * ad[i - 1]);
|
||||
|
||||
let exp1x = (rhsValueX - (bd[i] * prevRhsValueX))
|
||||
let exp1y = (rhsValueY - (bd[i] * prevRhsValueY))
|
||||
let exp2 = (d[i] - bd[i] * ad[i - 1])
|
||||
|
||||
rhsArray[i].x = exp1x / exp2
|
||||
rhsArray[i].y = exp1y / exp2
|
||||
}
|
||||
}
|
||||
|
||||
let lastElementIndex = segments - 1
|
||||
let exp1 = (rhsArray[lastElementIndex].x - bd[lastElementIndex] * rhsArray[lastElementIndex - 1].x)
|
||||
let exp1y = (rhsArray[lastElementIndex].y - bd[lastElementIndex] * rhsArray[lastElementIndex - 1].y)
|
||||
let exp2 = (d[lastElementIndex] - bd[lastElementIndex] * ad[lastElementIndex - 1])
|
||||
rhsArray[lastElementIndex].x = exp1 / exp2
|
||||
rhsArray[lastElementIndex].y = exp1y / exp2
|
||||
|
||||
solutionSet1[lastElementIndex] = rhsArray[lastElementIndex]
|
||||
|
||||
for i in (0..<lastElementIndex).reversed() {
|
||||
let controlPointX = rhsArray[i].x - (ad[i] * solutionSet1[i + 1]!.x)
|
||||
let controlPointY = rhsArray[i].y - (ad[i] * solutionSet1[i + 1]!.y)
|
||||
|
||||
solutionSet1[i] = CGPoint(x: controlPointX, y: controlPointY)
|
||||
}
|
||||
|
||||
firstControlPoints = solutionSet1
|
||||
|
||||
for i in (0..<segments) {
|
||||
if i == (segments - 1) {
|
||||
|
||||
let lastDataPoint = data[i + 1]
|
||||
let p1 = firstControlPoints[i]
|
||||
guard let controlPoint1 = p1 else { continue }
|
||||
|
||||
let controlPoint2X = 0.5 * (lastDataPoint.x + controlPoint1.x)
|
||||
let controlPoint2y = 0.5 * (lastDataPoint.y + controlPoint1.y)
|
||||
|
||||
let controlPoint2 = CGPoint(x: controlPoint2X, y: controlPoint2y)
|
||||
secondControlPoints.append(controlPoint2)
|
||||
}else {
|
||||
|
||||
let dataPoint = data[i+1]
|
||||
let p1 = firstControlPoints[i+1]
|
||||
guard let controlPoint1 = p1 else { continue }
|
||||
|
||||
let controlPoint2X = 2*dataPoint.x - controlPoint1.x
|
||||
let controlPoint2Y = 2*dataPoint.y - controlPoint1.y
|
||||
|
||||
secondControlPoints.append(CGPoint(x: controlPoint2X, y: controlPoint2Y))
|
||||
}
|
||||
}
|
||||
|
||||
for i in (0..<segments) {
|
||||
guard let firstControlPoint = firstControlPoints[i] else { continue }
|
||||
guard let secondControlPoint = secondControlPoints[i] else { continue }
|
||||
|
||||
controlPoints.append((firstControlPoint, secondControlPoint))
|
||||
}
|
||||
|
||||
return controlPoints
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class FPSCounter: NSObject {
|
||||
|
||||
/// Helper class that relays display link updates to the FPSCounter
|
||||
///
|
||||
/// This is necessary because CADisplayLink retains its target. Thus
|
||||
/// if the FPSCounter class would be the target of the display link
|
||||
/// it would create a retain cycle. The delegate has a weak reference
|
||||
/// to its parent FPSCounter, thus preventing this.
|
||||
///
|
||||
internal class DisplayLinkProxy: NSObject {
|
||||
|
||||
/// A weak ref to the parent FPSCounter instance.
|
||||
@objc weak var parentCounter: FPSCounter?
|
||||
|
||||
/// Notify the parent FPSCounter of a CADisplayLink update.
|
||||
///
|
||||
/// This method is automatically called by the CADisplayLink.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - displayLink: The display link that updated
|
||||
///
|
||||
@objc func updateFromDisplayLink(_ displayLink: CADisplayLink) {
|
||||
parentCounter?.updateFromDisplayLink(displayLink)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
private let displayLink: CADisplayLink
|
||||
private let displayLinkProxy: DisplayLinkProxy
|
||||
|
||||
/// Create a new FPSCounter.
|
||||
///
|
||||
/// To start receiving FPS updates you need to start tracking with the
|
||||
/// `startTracking(inRunLoop:mode:)` method.
|
||||
///
|
||||
public override init() {
|
||||
self.displayLinkProxy = DisplayLinkProxy()
|
||||
self.displayLink = CADisplayLink(
|
||||
target: self.displayLinkProxy,
|
||||
selector: #selector(DisplayLinkProxy.updateFromDisplayLink(_:))
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
self.displayLinkProxy.parentCounter = self
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.displayLink.invalidate()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// The delegate that should receive FPS updates.
|
||||
public weak var delegate: FPSCounterDelegate?
|
||||
|
||||
/// Delay between FPS updates. Longer delays mean more averaged FPS numbers.
|
||||
@objc public var notificationDelay: TimeInterval = 1.0
|
||||
|
||||
|
||||
// MARK: - Tracking
|
||||
|
||||
private var runloop: RunLoop?
|
||||
private var mode: RunLoop.Mode?
|
||||
|
||||
/// Start tracking FPS updates.
|
||||
///
|
||||
/// You can specify wich runloop to use for tracking, as well as the runloop modes.
|
||||
/// Usually you'll want the main runloop (default), and either the common run loop modes
|
||||
/// (default), or the tracking mode (`RunLoop.Mode.tracking`).
|
||||
///
|
||||
/// When the counter is already tracking, it's stopped first.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - runloop: The runloop to start tracking in
|
||||
/// - mode: The mode(s) to track in the runloop
|
||||
///
|
||||
@objc public func startTracking(inRunLoop runloop: RunLoop = .main, mode: RunLoop.Mode = .common) {
|
||||
self.stopTracking()
|
||||
|
||||
self.runloop = runloop
|
||||
self.mode = mode
|
||||
self.displayLink.add(to: runloop, forMode: mode)
|
||||
}
|
||||
|
||||
/// Stop tracking FPS updates.
|
||||
///
|
||||
/// This method does nothing if the counter is not currently tracking.
|
||||
///
|
||||
@objc public func stopTracking() {
|
||||
guard let runloop = self.runloop, let mode = self.mode else { return }
|
||||
|
||||
self.displayLink.remove(from: runloop, forMode: mode)
|
||||
self.runloop = nil
|
||||
self.mode = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Handling Frame Updates
|
||||
|
||||
private var lastNotificationTime: CFAbsoluteTime = 0.0
|
||||
private var numberOfFrames = 0
|
||||
|
||||
private func updateFromDisplayLink(_ displayLink: CADisplayLink) {
|
||||
if self.lastNotificationTime == 0.0 {
|
||||
self.lastNotificationTime = CFAbsoluteTimeGetCurrent()
|
||||
return
|
||||
}
|
||||
|
||||
self.numberOfFrames += 1
|
||||
|
||||
let currentTime = CFAbsoluteTimeGetCurrent()
|
||||
let elapsedTime = currentTime - self.lastNotificationTime
|
||||
|
||||
if elapsedTime >= self.notificationDelay {
|
||||
self.notifyUpdateForElapsedTime(elapsedTime)
|
||||
self.lastNotificationTime = 0.0
|
||||
self.numberOfFrames = 0
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyUpdateForElapsedTime(_ elapsedTime: CFAbsoluteTime) {
|
||||
let fps = Int(round(Double(self.numberOfFrames) / elapsedTime))
|
||||
self.delegate?.fpsCounter(self, didUpdateFramesPerSecond: fps)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The delegate protocol for the FPSCounter class.
|
||||
///
|
||||
/// Implement this protocol if you want to receive updates from a `FPSCounter`.
|
||||
///
|
||||
protocol FPSCounterDelegate: NSObjectProtocol {
|
||||
|
||||
/// Called in regular intervals while the counter is tracking FPS.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - counter: The FPSCounter that sent the update
|
||||
/// - fps: The current FPS of the application
|
||||
///
|
||||
func fpsCounter(_ counter: FPSCounter, didUpdateFramesPerSecond fps: Int)
|
||||
}
|
||||
|
||||
class BezierPath {
|
||||
struct Element {
|
||||
enum ElementType {
|
||||
case moveTo
|
||||
case addLine
|
||||
case cubicCurve
|
||||
case quadCurve
|
||||
}
|
||||
|
||||
let type: ElementType
|
||||
|
||||
var startPoint: Polyline.Point
|
||||
var endPoint: Polyline.Point
|
||||
var controlPoints: [CGPoint]
|
||||
|
||||
var lengthRange: ClosedRange<CGFloat>?
|
||||
var calculatedLength: CGFloat?
|
||||
|
||||
func point(at t: CGFloat) -> CGPoint {
|
||||
switch self.type {
|
||||
case .addLine:
|
||||
return self.startPoint.location.linearBezierPoint(to: self.endPoint.location, t: t)
|
||||
case .cubicCurve:
|
||||
return self.startPoint.location.cubicBezierPoint(to: self.endPoint.location, controlPoint1: self.controlPoints[0], controlPoint2: self.controlPoints[1], t: t)
|
||||
case .quadCurve:
|
||||
return self.startPoint.location.quadBezierPoint(to: self.endPoint.location, controlPoint: self.controlPoints[0], t: t)
|
||||
default:
|
||||
return .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let path = UIBezierPath()
|
||||
|
||||
var elements: [Element] = []
|
||||
var elementCount: Int {
|
||||
return self.elements.count
|
||||
}
|
||||
|
||||
func element(at t: CGFloat) -> (element: Element, innerT: CGFloat)? {
|
||||
let t = min(max(0.0, t), 1.0)
|
||||
|
||||
for element in elements {
|
||||
if let lengthRange = element.lengthRange, lengthRange.contains(t) {
|
||||
let innerT = (t - lengthRange.lowerBound) / (lengthRange.upperBound - lengthRange.lowerBound)
|
||||
return (element, innerT)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var points: [Int: CGPoint] = [:]
|
||||
func point(at t: CGFloat) -> CGPoint? {
|
||||
if let (element, innerT) = self.element(at: t) {
|
||||
return element.point(at: innerT)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func append(_ element: Element) {
|
||||
self.elements.append(element)
|
||||
switch element.type {
|
||||
case .moveTo:
|
||||
self.move(to: element.startPoint.location)
|
||||
case .addLine:
|
||||
self.addLine(to: element.endPoint.location)
|
||||
case .cubicCurve:
|
||||
self.addCurve(to: element.endPoint.location, controlPoint1: element.controlPoints[0], controlPoint2: element.controlPoints[1])
|
||||
case .quadCurve:
|
||||
self.addQuadCurve(to: element.endPoint.location, controlPoint: element.controlPoints[0])
|
||||
}
|
||||
}
|
||||
|
||||
private func move(to point: CGPoint) {
|
||||
self.path.move(to: point)
|
||||
}
|
||||
|
||||
private func addLine(to point: CGPoint) {
|
||||
self.path.addLine(to: point)
|
||||
}
|
||||
|
||||
private func addQuadCurve(to point: CGPoint, controlPoint: CGPoint) {
|
||||
self.path.addQuadCurve(to: point, controlPoint: controlPoint)
|
||||
}
|
||||
|
||||
private func addCurve(to point: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) {
|
||||
self.path.addCurve(to: point, controlPoint1: controlPoint1, controlPoint2: controlPoint2)
|
||||
}
|
||||
|
||||
private func close() {
|
||||
self.path.close()
|
||||
}
|
||||
|
||||
func trimming(to elementIndex: Int) -> BezierPath {
|
||||
let outputPath = BezierPath()
|
||||
for element in self.elements[0 ... elementIndex] {
|
||||
outputPath.append(element)
|
||||
}
|
||||
return outputPath
|
||||
}
|
||||
|
||||
func closedCopy() -> BezierPath {
|
||||
let outputPath = BezierPath()
|
||||
for element in self.elements {
|
||||
outputPath.append(element)
|
||||
}
|
||||
outputPath.close()
|
||||
return outputPath
|
||||
}
|
||||
}
|
||||
|
||||
func concaveHullPath(points: [CGPoint]) -> CGPath {
|
||||
let hull = getHull(points, concavity: 1000.0)
|
||||
let hullPath = CGMutablePath()
|
||||
var moved = true
|
||||
for point in hull {
|
||||
if moved {
|
||||
hullPath.move(to: point)
|
||||
moved = false
|
||||
} else {
|
||||
hullPath.addLine(to: point)
|
||||
}
|
||||
}
|
||||
hullPath.closeSubpath()
|
||||
|
||||
return hullPath
|
||||
}
|
||||
|
||||
func expandPath(_ path: CGPath, width: CGFloat) -> CGPath {
|
||||
let expandedPath = path.copy(strokingWithWidth: width * 2.0, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
|
||||
class UserInfo {
|
||||
let outputPath = CGMutablePath()
|
||||
var passedFirst = false
|
||||
}
|
||||
var userInfo = UserInfo()
|
||||
|
||||
withUnsafeMutablePointer(to: &userInfo) { userInfoPointer in
|
||||
expandedPath.apply(info: userInfoPointer) { (userInfo, nextElementPointer) in
|
||||
let element = nextElementPointer.pointee
|
||||
let userInfoPointer = userInfo!.assumingMemoryBound(to: UserInfo.self)
|
||||
let userInfo = userInfoPointer.pointee
|
||||
|
||||
if !userInfo.passedFirst {
|
||||
if case .closeSubpath = element.type {
|
||||
userInfo.passedFirst = true
|
||||
}
|
||||
} else {
|
||||
switch element.type {
|
||||
case .moveToPoint:
|
||||
userInfo.outputPath.move(to: element.points[0])
|
||||
case .addLineToPoint:
|
||||
userInfo.outputPath.addLine(to: element.points[0])
|
||||
case .addQuadCurveToPoint:
|
||||
userInfo.outputPath.addQuadCurve(to: element.points[1], control: element.points[0])
|
||||
case .addCurveToPoint:
|
||||
userInfo.outputPath.addCurve(to: element.points[2], control1: element.points[0], control2: element.points[1])
|
||||
case .closeSubpath:
|
||||
userInfo.outputPath.closeSubpath()
|
||||
@unknown default:
|
||||
userInfo.outputPath.closeSubpath()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return userInfo.outputPath
|
||||
}
|
||||
|
||||
class Matrix {
|
||||
private(set) var m: [Float]
|
||||
|
||||
private init() {
|
||||
m = [
|
||||
1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
]
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func translation(x: Float, y: Float, z: Float) -> Matrix {
|
||||
m[12] = x
|
||||
m[13] = y
|
||||
m[14] = z
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func scaling(x: Float, y: Float, z: Float) -> Matrix {
|
||||
m[0] = x
|
||||
m[5] = y
|
||||
m[10] = z
|
||||
return self
|
||||
}
|
||||
|
||||
static var identity = Matrix()
|
||||
}
|
||||
|
||||
struct Vertex {
|
||||
var position: vector_float4
|
||||
var texCoord: vector_float2
|
||||
|
||||
init(position: CGPoint, texCoord: CGPoint) {
|
||||
self.position = position.toFloat4()
|
||||
self.texCoord = texCoord.toFloat2()
|
||||
}
|
||||
}
|
||||
|
||||
struct Point {
|
||||
var position: vector_float4
|
||||
var color: vector_float4
|
||||
var angle: Float
|
||||
var size: Float
|
||||
|
||||
init(x: CGFloat, y: CGFloat, color: DrawingColor, size: CGFloat, angle: CGFloat = 0) {
|
||||
self.position = vector_float4(Float(x), Float(y), 0, 1)
|
||||
self.size = Float(size)
|
||||
self.color = color.toFloat4()
|
||||
self.angle = Float(angle)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint {
|
||||
func toFloat4(z: CGFloat = 0, w: CGFloat = 1) -> vector_float4 {
|
||||
return [Float(x), Float(y), Float(z) ,Float(w)]
|
||||
}
|
||||
|
||||
func toFloat2() -> vector_float2 {
|
||||
return [Float(x), Float(y)]
|
||||
}
|
||||
|
||||
func offsetBy(_ offset: CGPoint) -> CGPoint {
|
||||
return self.offsetBy(dx: offset.x, dy: offset.y)
|
||||
}
|
||||
}
|
||||
327
submodules/DrawingUI/Sources/DrawingVectorEntity.swift
Normal file
@@ -0,0 +1,327 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AccountContext
|
||||
|
||||
final class DrawingVectorEntity: DrawingEntity {
|
||||
public enum VectorType {
|
||||
case line
|
||||
case oneSidedArrow
|
||||
case twoSidedArrow
|
||||
}
|
||||
|
||||
let uuid: UUID
|
||||
let isAnimated: Bool
|
||||
|
||||
var type: VectorType
|
||||
var color: DrawingColor
|
||||
var lineWidth: CGFloat
|
||||
|
||||
var drawingSize: CGSize
|
||||
var referenceDrawingSize: CGSize
|
||||
var start: CGPoint
|
||||
var mid: (CGFloat, CGFloat)
|
||||
var end: CGPoint
|
||||
|
||||
var _cachedMidPoint: (start: CGPoint, end: CGPoint, midLength: CGFloat, midHeight: CGFloat, midPoint: CGPoint)?
|
||||
var midPoint: CGPoint {
|
||||
if let (start, end, midLength, midHeight, midPoint) = self._cachedMidPoint, start == self.start, end == self.end, midLength == self.mid.0, midHeight == self.mid.1 {
|
||||
return midPoint
|
||||
} else {
|
||||
let midPoint = midPointPositionFor(start: self.start, end: self.end, length: self.mid.0, height: self.mid.1)
|
||||
self._cachedMidPoint = (self.start, self.end, self.mid.0, self.mid.1, midPoint)
|
||||
return midPoint
|
||||
}
|
||||
}
|
||||
|
||||
init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = false
|
||||
|
||||
self.type = type
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
|
||||
self.drawingSize = .zero
|
||||
self.referenceDrawingSize = .zero
|
||||
self.start = CGPoint()
|
||||
self.mid = (0.5, 0.0)
|
||||
self.end = CGPoint()
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.start
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth)
|
||||
newEntity.drawingSize = self.drawingSize
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
newEntity.start = self.start
|
||||
newEntity.mid = self.mid
|
||||
newEntity.end = self.end
|
||||
return newEntity
|
||||
}
|
||||
|
||||
weak var currentEntityView: DrawingEntityView?
|
||||
func makeView(context: AccountContext) -> DrawingEntityView {
|
||||
let entityView = DrawingVectorEntityView(context: context, entity: self)
|
||||
self.currentEntityView = entityView
|
||||
return entityView
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingVectorEntityView: DrawingEntityView {
|
||||
private var vectorEntity: DrawingVectorEntity {
|
||||
return self.entity as! DrawingVectorEntity
|
||||
}
|
||||
|
||||
fileprivate let shapeLayer = SimpleShapeLayer()
|
||||
|
||||
init(context: AccountContext, entity: DrawingVectorEntity) {
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.layer.addSublayer(self.shapeLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var selectionBounds: CGRect {
|
||||
return self.shapeLayer.path?.boundingBox ?? self.bounds
|
||||
}
|
||||
|
||||
override func update(animated: Bool) {
|
||||
self.center = CGPoint(x: self.vectorEntity.drawingSize.width * 0.5, y: self.vectorEntity.drawingSize.height * 0.5)
|
||||
self.bounds = CGRect(origin: .zero, size: self.vectorEntity.drawingSize)
|
||||
|
||||
let minLineWidth = max(10.0, min(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.02)
|
||||
let maxLineWidth = max(10.0, min(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.1)
|
||||
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.vectorEntity.lineWidth
|
||||
|
||||
self.shapeLayer.path = CGPath.curve(
|
||||
start: self.vectorEntity.start,
|
||||
end: self.vectorEntity.end,
|
||||
mid: self.vectorEntity.midPoint,
|
||||
lineWidth: lineWidth,
|
||||
arrowSize: self.vectorEntity.type == .line ? nil : CGSize(width: lineWidth * 1.5, height: lineWidth * 3.0),
|
||||
twoSided: self.vectorEntity.type == .twoSidedArrow
|
||||
)
|
||||
self.shapeLayer.fillColor = self.vectorEntity.color.toCGColor()
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
override func updateSelectionView() {
|
||||
guard let selectionView = self.selectionView as? DrawingVectorEntititySelectionView else {
|
||||
return
|
||||
}
|
||||
|
||||
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
|
||||
let drawingSize = self.vectorEntity.drawingSize
|
||||
selectionView.bounds = CGRect(origin: .zero, size: drawingSize)
|
||||
selectionView.center = CGPoint(x: drawingSize.width * 0.5 * scale, y: drawingSize.height * 0.5 * scale)
|
||||
selectionView.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
selectionView.scale = scale
|
||||
}
|
||||
|
||||
override func precisePoint(inside point: CGPoint) -> Bool {
|
||||
if let path = self.shapeLayer.path {
|
||||
if path.contains(point) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return super.precisePoint(inside: point)
|
||||
}
|
||||
}
|
||||
|
||||
override func makeSelectionView() -> DrawingEntitySelectionView {
|
||||
if let selectionView = self.selectionView {
|
||||
return selectionView
|
||||
}
|
||||
let selectionView = DrawingVectorEntititySelectionView()
|
||||
selectionView.entityView = self
|
||||
return selectionView
|
||||
}
|
||||
}
|
||||
|
||||
private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, height: CGFloat) -> CGPoint {
|
||||
let distance = end.distance(to: start)
|
||||
let angle = start.angle(to: end)
|
||||
let p1 = start.pointAt(distance: distance * length, angle: angle)
|
||||
let p2 = p1.pointAt(distance: distance * height, angle: angle + .pi * 0.5)
|
||||
return p2
|
||||
}
|
||||
|
||||
final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate {
|
||||
private let startHandle = SimpleShapeLayer()
|
||||
private let midHandle = SimpleShapeLayer()
|
||||
private let endHandle = SimpleShapeLayer()
|
||||
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer!
|
||||
|
||||
var scale: CGFloat = 1.0 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
|
||||
self.startHandle.bounds = handleBounds
|
||||
self.startHandle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
|
||||
self.startHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor
|
||||
self.startHandle.rasterizationScale = UIScreen.main.scale
|
||||
self.startHandle.shouldRasterize = true
|
||||
|
||||
self.midHandle.bounds = handleBounds
|
||||
self.midHandle.fillColor = UIColor(rgb: 0x00ff00).cgColor
|
||||
self.midHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor
|
||||
self.midHandle.rasterizationScale = UIScreen.main.scale
|
||||
self.midHandle.shouldRasterize = true
|
||||
|
||||
self.endHandle.bounds = handleBounds
|
||||
self.endHandle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
|
||||
self.endHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor
|
||||
self.endHandle.rasterizationScale = UIScreen.main.scale
|
||||
self.endHandle.shouldRasterize = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
self.isOpaque = false
|
||||
|
||||
self.layer.addSublayer(self.startHandle)
|
||||
self.layer.addSublayer(self.midHandle)
|
||||
self.layer.addSublayer(self.endHandle)
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingVectorEntity else {
|
||||
return
|
||||
}
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView)
|
||||
|
||||
var updatedStart = entity.start
|
||||
var updatedMid = entity.mid
|
||||
var updatedEnd = entity.end
|
||||
|
||||
if self.currentHandle === self.startHandle {
|
||||
updatedStart.x += delta.x
|
||||
updatedStart.y += delta.y
|
||||
} else if self.currentHandle === self.endHandle {
|
||||
updatedEnd.x += delta.x
|
||||
updatedEnd.y += delta.y
|
||||
} else if self.currentHandle === self.midHandle {
|
||||
var updatedMidPoint = entity.midPoint
|
||||
updatedMidPoint.x += delta.x
|
||||
updatedMidPoint.y += delta.y
|
||||
|
||||
let distance = updatedStart.distance(to: updatedEnd)
|
||||
let pointOnLine = updatedMidPoint.perpendicularPointOnLine(start: updatedStart, end: updatedEnd)
|
||||
|
||||
let angle = updatedStart.angle(to: updatedEnd)
|
||||
let midAngle = updatedStart.angle(to: updatedMidPoint)
|
||||
var height = updatedMidPoint.distance(to: pointOnLine) / distance
|
||||
var deltaAngle = midAngle - angle
|
||||
if deltaAngle > .pi {
|
||||
deltaAngle = angle - 2 * .pi
|
||||
} else if deltaAngle < -.pi {
|
||||
deltaAngle = angle + 2 * .pi
|
||||
}
|
||||
if deltaAngle < 0.0 {
|
||||
height *= -1.0
|
||||
}
|
||||
let length = updatedStart.distance(to: pointOnLine) / distance
|
||||
updatedMid = (length, height)
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedStart.x += delta.x
|
||||
updatedStart.y += delta.y
|
||||
updatedEnd.x += delta.x
|
||||
updatedEnd.y += delta.y
|
||||
}
|
||||
|
||||
entity.start = updatedStart
|
||||
entity.mid = updatedMid
|
||||
entity.end = updatedEnd
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
if self.startHandle.frame.contains(point) || self.midHandle.frame.contains(point) || self.endHandle.frame.contains(point) {
|
||||
return true
|
||||
} else if let entityView = self.entityView as? DrawingVectorEntityView, let path = entityView.shapeLayer.path {
|
||||
return path.contains(self.convert(point, to: entityView))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
guard let entityView = self.entityView as? DrawingVectorEntityView, let entity = entityView.entity as? DrawingVectorEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
|
||||
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
|
||||
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
|
||||
let lineWidth = (1.0 + UIScreenPixel) / self.scale
|
||||
|
||||
self.startHandle.path = handlePath
|
||||
self.startHandle.position = entity.start
|
||||
self.startHandle.bounds = bounds
|
||||
self.startHandle.lineWidth = lineWidth
|
||||
|
||||
self.midHandle.path = handlePath
|
||||
self.midHandle.position = entity.midPoint
|
||||
self.midHandle.bounds = bounds
|
||||
self.midHandle.lineWidth = lineWidth
|
||||
|
||||
self.endHandle.path = handlePath
|
||||
self.endHandle.position = entity.end
|
||||
self.endHandle.bounds = bounds
|
||||
self.endHandle.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
var isTracking: Bool {
|
||||
return gestureIsTracking(self.panGestureRecognizer)
|
||||
}
|
||||
}
|
||||
963
submodules/DrawingUI/Sources/DrawingView.swift
Normal file
@@ -0,0 +1,963 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import ComponentFlow
|
||||
import LegacyComponents
|
||||
import AppBundle
|
||||
import ImageBlur
|
||||
|
||||
protocol DrawingElement: AnyObject {
|
||||
var uuid: UUID { get }
|
||||
var bounds: CGRect { get }
|
||||
var points: [Polyline.Point] { get }
|
||||
|
||||
var translation: CGPoint { get set }
|
||||
|
||||
var renderLineWidth: CGFloat { get }
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool
|
||||
|
||||
init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool)
|
||||
|
||||
func setupRenderLayer() -> DrawingRenderLayer?
|
||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState)
|
||||
|
||||
func draw(in: CGContext, size: CGSize)
|
||||
}
|
||||
|
||||
enum DrawingCommand {
|
||||
enum DrawingElementTransform {
|
||||
case move(offset: CGPoint)
|
||||
}
|
||||
|
||||
case addStroke(DrawingElement)
|
||||
case updateStrokes([UUID], DrawingElementTransform)
|
||||
case removeStroke(DrawingElement)
|
||||
case addEntity(DrawingEntity)
|
||||
case updateEntity(UUID, DrawingEntity)
|
||||
case removeEntity(DrawingEntity)
|
||||
case updateEntityZOrder(UUID, Int32)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
enum Action {
|
||||
case undo
|
||||
case redo
|
||||
case clear
|
||||
case zoomOut
|
||||
}
|
||||
|
||||
enum Tool {
|
||||
case pen
|
||||
case marker
|
||||
case neon
|
||||
case pencil
|
||||
case eraser
|
||||
case lasso
|
||||
case objectRemover
|
||||
case blur
|
||||
}
|
||||
|
||||
var tool: Tool = .pen
|
||||
var toolColor: DrawingColor = DrawingColor(color: .white)
|
||||
var toolBrushSize: CGFloat = 0.15
|
||||
var toolHasArrow: Bool = false
|
||||
|
||||
var stateUpdated: (NavigationState) -> Void = { _ in }
|
||||
|
||||
var shouldBegin: (CGPoint) -> Bool = { _ in return true }
|
||||
var requestMenu: ([UUID], CGRect) -> Void = { _, _ in }
|
||||
var getFullImage: (Bool) -> UIImage? = { _ in return nil }
|
||||
|
||||
private var elements: [DrawingElement] = []
|
||||
private var redoElements: [DrawingElement] = []
|
||||
fileprivate var uncommitedElement: DrawingElement?
|
||||
|
||||
private(set) var drawingImage: UIImage?
|
||||
private let renderer: UIGraphicsImageRenderer
|
||||
|
||||
private var currentDrawingView: UIView
|
||||
private var currentDrawingLayer: DrawingRenderLayer?
|
||||
|
||||
private var selectionImage: UIImage?
|
||||
private var pannedSelectionView: UIView
|
||||
|
||||
var lassoView: DrawingLassoView
|
||||
private var metalView: DrawingMetalView
|
||||
|
||||
private let brushSizePreviewLayer: SimpleShapeLayer
|
||||
|
||||
let imageSize: CGSize
|
||||
private 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 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")
|
||||
}
|
||||
|
||||
public init(size: CGSize) {
|
||||
self.imageSize = size
|
||||
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = 1.0
|
||||
self.renderer = UIGraphicsImageRenderer(size: size, format: format)
|
||||
|
||||
self.currentDrawingView = UIView()
|
||||
self.currentDrawingView.frame = CGRect(origin: .zero, size: size)
|
||||
self.currentDrawingView.contentScaleFactor = 1.0
|
||||
self.currentDrawingView.backgroundColor = .clear
|
||||
self.currentDrawingView.isUserInteractionEnabled = false
|
||||
|
||||
self.pannedSelectionView = UIView()
|
||||
self.pannedSelectionView.frame = CGRect(origin: .zero, size: size)
|
||||
self.pannedSelectionView.contentScaleFactor = 1.0
|
||||
self.pannedSelectionView.backgroundColor = .clear
|
||||
self.pannedSelectionView.isUserInteractionEnabled = false
|
||||
|
||||
self.lassoView = DrawingLassoView(size: size)
|
||||
self.lassoView.isHidden = true
|
||||
|
||||
self.metalView = DrawingMetalView(size: size)!
|
||||
|
||||
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.addSubview(self.currentDrawingView)
|
||||
self.addSubview(self.metalView)
|
||||
self.lassoView.addSubview(self.pannedSelectionView)
|
||||
self.addSubview(self.lassoView)
|
||||
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.lassoView.isHidden && strongSelf.lassoView.point(inside: strongSelf.convert(point, to: strongSelf.lassoView), with: nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
drawingGesturePipeline.onDrawing = { [weak self] state, path in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if case .objectRemover = strongSelf.tool {
|
||||
if case let .location(point) = path {
|
||||
var elementsToRemove: [DrawingElement] = []
|
||||
for element in strongSelf.elements {
|
||||
if element.containsPoint(point.location) {
|
||||
elementsToRemove.append(element)
|
||||
}
|
||||
}
|
||||
|
||||
for element in elementsToRemove {
|
||||
strongSelf.removeElement(element)
|
||||
}
|
||||
}
|
||||
} else if case .lasso = strongSelf.tool {
|
||||
if case let .smoothCurve(bezierPath) = path {
|
||||
let scale = strongSelf.bounds.width / strongSelf.imageSize.width
|
||||
|
||||
switch state {
|
||||
case .began:
|
||||
strongSelf.lassoView.setup(scale: scale)
|
||||
strongSelf.lassoView.updatePath(bezierPath)
|
||||
case .changed:
|
||||
strongSelf.lassoView.updatePath(bezierPath)
|
||||
case .ended:
|
||||
let closedPath = bezierPath.closedCopy()
|
||||
|
||||
var selectedElements: [DrawingElement] = []
|
||||
var selectedPoints: [CGPoint] = []
|
||||
var maxLineWidth: CGFloat = 0.0
|
||||
for element in strongSelf.elements {
|
||||
if element.hasPointsInsidePath(closedPath.path) {
|
||||
maxLineWidth = max(maxLineWidth, element.renderLineWidth)
|
||||
selectedElements.append(element)
|
||||
selectedPoints.append(contentsOf: element.points.map { $0.location })
|
||||
}
|
||||
}
|
||||
|
||||
if selectedPoints.count > 0 {
|
||||
strongSelf.lassoView.apply(scale: scale, points: selectedPoints, selectedElements: selectedElements.map { $0.uuid }, expand: maxLineWidth)
|
||||
} else {
|
||||
strongSelf.lassoView.reset()
|
||||
}
|
||||
case .cancelled:
|
||||
strongSelf.lassoView.reset()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch state {
|
||||
case .began:
|
||||
strongSelf.previousStrokePoint = nil
|
||||
|
||||
if strongSelf.uncommitedElement != nil {
|
||||
strongSelf.finishDrawing()
|
||||
}
|
||||
|
||||
guard let newElement = strongSelf.prepareNewElement() else {
|
||||
return
|
||||
}
|
||||
if let renderLayer = newElement.setupRenderLayer() {
|
||||
strongSelf.currentDrawingView.layer.addSublayer(renderLayer)
|
||||
strongSelf.currentDrawingLayer = renderLayer
|
||||
}
|
||||
newElement.updatePath(path, state: state)
|
||||
strongSelf.uncommitedElement = newElement
|
||||
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 {
|
||||
if lastPoint.location.distance(to: previousStrokePoint) > 10.0 {
|
||||
strongSelf.previousStrokePoint = lastPoint.location
|
||||
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
}
|
||||
|
||||
if strongSelf.strokeRecognitionTimer == nil {
|
||||
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: 0.25)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = bounds.size
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_circle" {
|
||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = bounds.size
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_star" {
|
||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = CGSize(width: max(bounds.width, bounds.height), height: max(bounds.width, bounds.height))
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_arrow" {
|
||||
let arrowEntity = DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.toolColor, lineWidth: 0.2)
|
||||
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.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.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
strongSelf.uncommitedElement?.updatePath(path, state: state)
|
||||
Queue.mainQueue().after(0.05) {
|
||||
strongSelf.finishDrawing()
|
||||
}
|
||||
case .cancelled:
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
strongSelf.cancelDrawing()
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
self.lassoView.requestMenu = { [weak self] elements, rect in
|
||||
if let strongSelf = self {
|
||||
strongSelf.requestMenu(elements, rect)
|
||||
}
|
||||
}
|
||||
|
||||
self.lassoView.panBegan = { [weak self] elements in
|
||||
if let strongSelf = self {
|
||||
strongSelf.skipDrawing = Set(elements)
|
||||
strongSelf.commit(reset: true)
|
||||
strongSelf.updateSelectionImage()
|
||||
}
|
||||
}
|
||||
|
||||
self.lassoView.panChanged = { [weak self] elements, offset in
|
||||
if let strongSelf = self {
|
||||
let offset = CGPoint(x: offset.x * -1.0, y: offset.y * -1.0)
|
||||
strongSelf.lassoView.bounds = CGRect(origin: offset, size: strongSelf.lassoView.bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
self.lassoView.panEnded = { [weak self] elements, offset in
|
||||
if let strongSelf = self {
|
||||
let elementsSet = Set(elements)
|
||||
for element in strongSelf.elements {
|
||||
if elementsSet.contains(element.uuid) {
|
||||
element.translation = element.translation.offsetBy(offset)
|
||||
}
|
||||
}
|
||||
strongSelf.skipDrawing = Set()
|
||||
strongSelf.commit(reset: true)
|
||||
strongSelf.selectionImage = nil
|
||||
strongSelf.pannedSelectionView.layer.contents = nil
|
||||
|
||||
strongSelf.lassoView.bounds = CGRect(origin: .zero, size: strongSelf.lassoView.bounds.size)
|
||||
strongSelf.lassoView.translate(offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.longPressTimer?.invalidate()
|
||||
self.strokeRecognitionTimer?.invalidate()
|
||||
}
|
||||
|
||||
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer === self.longPressGestureRecognizer, !self.lassoView.isHidden {
|
||||
return false
|
||||
}
|
||||
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 {
|
||||
self.longPressTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.cancelDrawing()
|
||||
|
||||
let newElement = FillTool(drawingSize: strongSelf.imageSize, color: strongSelf.toolColor, lineWidth: 0.0, arrow: false)
|
||||
strongSelf.uncommitedElement = newElement
|
||||
strongSelf.finishDrawing()
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longPressTimer?.start()
|
||||
|
||||
let fillCircleLayer = SimpleShapeLayer()
|
||||
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 = self.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 var skipDrawing = Set<UUID>()
|
||||
private func commit(reset: Bool = false) {
|
||||
let currentImage = self.drawingImage
|
||||
self.drawingImage = self.renderer.image { context in
|
||||
if !reset {
|
||||
context.cgContext.clear(CGRect(origin: .zero, size: self.imageSize))
|
||||
if let image = currentImage {
|
||||
image.draw(at: .zero)
|
||||
}
|
||||
if let uncommitedElement = self.uncommitedElement {
|
||||
uncommitedElement.draw(in: context.cgContext, size: self.imageSize)
|
||||
}
|
||||
} else {
|
||||
context.cgContext.clear(CGRect(origin: .zero, size: self.imageSize))
|
||||
for element in self.elements {
|
||||
if !self.skipDrawing.contains(element.uuid) {
|
||||
element.draw(in: context.cgContext, size: self.imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.layer.contents = self.drawingImage?.cgImage
|
||||
|
||||
if let currentDrawingLayer = self.currentDrawingLayer {
|
||||
self.currentDrawingLayer = nil
|
||||
currentDrawingLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
self.metalView.clear()
|
||||
}
|
||||
|
||||
private func updateSelectionImage() {
|
||||
self.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 = self.selectionImage?.cgImage
|
||||
}
|
||||
|
||||
fileprivate func cancelDrawing() {
|
||||
self.uncommitedElement = nil
|
||||
|
||||
if let currentDrawingLayer = self.currentDrawingLayer {
|
||||
self.currentDrawingLayer = nil
|
||||
currentDrawingLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func finishDrawing() {
|
||||
self.commit()
|
||||
|
||||
self.redoElements.removeAll()
|
||||
if let uncommitedElement = self.uncommitedElement {
|
||||
self.elements.append(uncommitedElement)
|
||||
self.uncommitedElement = nil
|
||||
}
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
|
||||
weak var entitiesView: DrawingEntitiesView?
|
||||
func clear() {
|
||||
self.entitiesView?.removeAll()
|
||||
|
||||
self.uncommitedElement = nil
|
||||
self.elements.removeAll()
|
||||
self.redoElements.removeAll()
|
||||
self.drawingImage = nil
|
||||
self.commit(reset: true)
|
||||
|
||||
self.updateInternalState()
|
||||
|
||||
self.lassoView.reset()
|
||||
}
|
||||
|
||||
private func undo() {
|
||||
guard let lastElement = self.elements.last else {
|
||||
return
|
||||
}
|
||||
self.uncommitedElement = nil
|
||||
self.redoElements.append(lastElement)
|
||||
self.elements.removeLast()
|
||||
self.commit(reset: true)
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
|
||||
private func redo() {
|
||||
guard let lastElement = self.redoElements.last else {
|
||||
return
|
||||
}
|
||||
self.uncommitedElement = nil
|
||||
self.elements.append(lastElement)
|
||||
self.redoElements.removeLast()
|
||||
self.uncommitedElement = lastElement
|
||||
self.commit(reset: false)
|
||||
self.uncommitedElement = nil
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
|
||||
private var preparredEraserImage: UIImage?
|
||||
|
||||
func updateToolState(_ state: DrawingToolState) {
|
||||
switch state {
|
||||
case let .pen(brushState):
|
||||
self.drawingGesturePipeline?.mode = .polyline
|
||||
self.tool = .pen
|
||||
self.toolColor = brushState.color
|
||||
self.toolBrushSize = brushState.size
|
||||
self.toolHasArrow = brushState.mode == .arrow
|
||||
case let .marker(brushState):
|
||||
self.drawingGesturePipeline?.mode = .location
|
||||
self.tool = .marker
|
||||
self.toolColor = brushState.color
|
||||
self.toolBrushSize = brushState.size
|
||||
self.toolHasArrow = brushState.mode == .arrow
|
||||
case let .neon(brushState):
|
||||
self.drawingGesturePipeline?.mode = .smoothCurve
|
||||
self.tool = .neon
|
||||
self.toolColor = brushState.color
|
||||
self.toolBrushSize = brushState.size
|
||||
self.toolHasArrow = brushState.mode == .arrow
|
||||
case let .pencil(brushState):
|
||||
self.drawingGesturePipeline?.mode = .location
|
||||
self.tool = .pencil
|
||||
self.toolColor = brushState.color
|
||||
self.toolBrushSize = brushState.size
|
||||
self.toolHasArrow = brushState.mode == .arrow
|
||||
case .lasso:
|
||||
self.drawingGesturePipeline?.mode = .smoothCurve
|
||||
self.tool = .lasso
|
||||
case let .eraser(eraserState):
|
||||
switch eraserState.mode {
|
||||
case .bitmap:
|
||||
self.tool = .eraser
|
||||
self.drawingGesturePipeline?.mode = .smoothCurve
|
||||
case .vector:
|
||||
self.tool = .objectRemover
|
||||
self.drawingGesturePipeline?.mode = .location
|
||||
case .blur:
|
||||
self.tool = .blur
|
||||
self.drawingGesturePipeline?.mode = .smoothCurve
|
||||
}
|
||||
self.toolBrushSize = eraserState.size
|
||||
}
|
||||
|
||||
if [.eraser, .blur].contains(self.tool) {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
if let image = self.getFullImage(self.tool == .blur) {
|
||||
if case .eraser = self.tool {
|
||||
Queue.mainQueue().async {
|
||||
self.preparredEraserImage = image
|
||||
}
|
||||
} else {
|
||||
// let format = UIGraphicsImageRendererFormat()
|
||||
// format.scale = 1.0
|
||||
// let size = image.size.fitted(CGSize(width: 256, height: 256))
|
||||
// let renderer = UIGraphicsImageRenderer(size: size, format: format)
|
||||
// let scaledImage = renderer.image { _ in
|
||||
// image.draw(in: CGRect(origin: .zero, size: size))
|
||||
// }
|
||||
|
||||
let blurredImage = blurredImage(image, radius: 60.0)
|
||||
Queue.mainQueue().async {
|
||||
self.preparredEraserImage = blurredImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.preparredEraserImage = 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.elements.isEmpty,
|
||||
canRedo: !self.redoElements.isEmpty,
|
||||
canClear: !self.elements.isEmpty,
|
||||
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne
|
||||
))
|
||||
}
|
||||
|
||||
public func updateZoomScale(_ scale: CGFloat) {
|
||||
self.zoomScale = scale
|
||||
self.updateInternalState()
|
||||
}
|
||||
|
||||
private func prepareNewElement() -> DrawingElement? {
|
||||
let element: DrawingElement?
|
||||
switch self.tool {
|
||||
case .pen:
|
||||
let penTool = PenTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize,
|
||||
arrow: self.toolHasArrow
|
||||
)
|
||||
element = penTool
|
||||
case .marker:
|
||||
let markerTool = MarkerTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize,
|
||||
arrow: self.toolHasArrow
|
||||
)
|
||||
markerTool.metalView = self.metalView
|
||||
element = markerTool
|
||||
case .neon:
|
||||
element = NeonTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize,
|
||||
arrow: self.toolHasArrow
|
||||
)
|
||||
case .pencil:
|
||||
let pencilTool = PencilTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize,
|
||||
arrow: self.toolHasArrow
|
||||
)
|
||||
pencilTool.metalView = self.metalView
|
||||
element = pencilTool
|
||||
case .blur:
|
||||
let blurTool = BlurTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize,
|
||||
arrow: false)
|
||||
blurTool.getFullImage = { [weak self] in
|
||||
return self?.preparredEraserImage
|
||||
}
|
||||
element = blurTool
|
||||
case .eraser:
|
||||
let eraserTool = EraserTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize,
|
||||
arrow: false)
|
||||
eraserTool.getFullImage = { [weak self] in
|
||||
return self?.preparredEraserImage
|
||||
}
|
||||
element = eraserTool
|
||||
default:
|
||||
element = nil
|
||||
}
|
||||
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)
|
||||
|
||||
self.lassoView.reset()
|
||||
}
|
||||
|
||||
func setBrushSizePreview(_ size: CGFloat?) {
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
if let size = size {
|
||||
let minBrushSize = 2.0
|
||||
let maxBrushSize = 28.0
|
||||
let brushSize = minBrushSize + (maxBrushSize - minBrushSize) * size
|
||||
|
||||
self.brushSizePreviewLayer.transform = CATransform3DMakeScale(brushSize / 100.0, brushSize / 100.0, 1.0)
|
||||
transition.setAlpha(layer: self.brushSizePreviewLayer, alpha: 1.0)
|
||||
} else {
|
||||
transition.setAlpha(layer: self.brushSizePreviewLayer, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let scale = self.scale
|
||||
let transform = CGAffineTransformMakeScale(scale, scale)
|
||||
self.currentDrawingView.transform = transform
|
||||
self.currentDrawingView.frame = self.bounds
|
||||
|
||||
self.drawingGesturePipeline?.transform = CGAffineTransformMakeScale(1.0 / scale, 1.0 / scale)
|
||||
|
||||
self.lassoView.transform = transform
|
||||
self.lassoView.frame = self.bounds
|
||||
|
||||
self.metalView.transform = transform
|
||||
self.metalView.frame = self.bounds
|
||||
|
||||
self.brushSizePreviewLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||
}
|
||||
|
||||
public var isEmpty: Bool {
|
||||
return self.elements.isEmpty
|
||||
}
|
||||
|
||||
public var scale: CGFloat {
|
||||
return self.bounds.width / self.imageSize.width
|
||||
}
|
||||
|
||||
public var isTracking: Bool {
|
||||
return self.uncommitedElement != nil
|
||||
}
|
||||
}
|
||||
|
||||
class DrawingLassoView: UIView {
|
||||
private var lassoBlackLayer: SimpleShapeLayer
|
||||
private var lassoWhiteLayer: SimpleShapeLayer
|
||||
|
||||
var requestMenu: ([UUID], CGRect) -> Void = { _, _ in }
|
||||
|
||||
var panBegan: ([UUID]) -> Void = { _ in }
|
||||
var panChanged: ([UUID], CGPoint) -> Void = { _, _ in }
|
||||
var panEnded: ([UUID], CGPoint) -> Void = { _, _ in }
|
||||
|
||||
private var currentScale: CGFloat = 1.0
|
||||
private var currentPoints: [CGPoint] = []
|
||||
private var selectedElements: [UUID] = []
|
||||
private var currentExpand: CGFloat = 0.0
|
||||
|
||||
init(size: CGSize) {
|
||||
self.lassoBlackLayer = SimpleShapeLayer()
|
||||
self.lassoBlackLayer.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
self.lassoWhiteLayer = SimpleShapeLayer()
|
||||
self.lassoWhiteLayer.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
self.layer.addSublayer(self.lassoBlackLayer)
|
||||
self.layer.addSublayer(self.lassoWhiteLayer)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
tapGestureRecognizer.numberOfTouchesRequired = 1
|
||||
self.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
panGestureRecognizer.maximumNumberOfTouches = 1
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup(scale: CGFloat) {
|
||||
self.isHidden = false
|
||||
|
||||
let dash: CGFloat = 5.0 / scale
|
||||
|
||||
self.lassoBlackLayer.opacity = 0.5
|
||||
self.lassoBlackLayer.fillColor = UIColor.clear.cgColor
|
||||
self.lassoBlackLayer.strokeColor = UIColor.black.cgColor
|
||||
self.lassoBlackLayer.lineWidth = 2.0 / scale
|
||||
self.lassoBlackLayer.lineJoin = .round
|
||||
self.lassoBlackLayer.lineCap = .round
|
||||
self.lassoBlackLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber]
|
||||
|
||||
let blackAnimation = CABasicAnimation(keyPath: "lineDashPhase")
|
||||
blackAnimation.fromValue = dash * 3.5
|
||||
blackAnimation.toValue = 0
|
||||
blackAnimation.duration = 0.45
|
||||
blackAnimation.repeatCount = .infinity
|
||||
self.lassoBlackLayer.add(blackAnimation, forKey: "lineDashPhase")
|
||||
|
||||
self.lassoWhiteLayer.opacity = 0.5
|
||||
self.lassoWhiteLayer.fillColor = UIColor.clear.cgColor
|
||||
self.lassoWhiteLayer.strokeColor = UIColor.white.cgColor
|
||||
self.lassoWhiteLayer.lineWidth = 2.0 / scale
|
||||
self.lassoWhiteLayer.lineJoin = .round
|
||||
self.lassoWhiteLayer.lineCap = .round
|
||||
self.lassoWhiteLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber]
|
||||
|
||||
let whiteAnimation = CABasicAnimation(keyPath: "lineDashPhase")
|
||||
whiteAnimation.fromValue = dash * 3.5 + dash * 1.75
|
||||
whiteAnimation.toValue = dash * 1.75
|
||||
whiteAnimation.duration = 0.45
|
||||
whiteAnimation.repeatCount = .infinity
|
||||
self.lassoWhiteLayer.add(whiteAnimation, forKey: "lineDashPhase")
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard let path = self.lassoBlackLayer.path else {
|
||||
return
|
||||
}
|
||||
self.requestMenu(self.selectedElements, path.boundingBox)
|
||||
}
|
||||
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let translation = gestureRecognizer.translation(in: self)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.panBegan(self.selectedElements)
|
||||
case .changed:
|
||||
self.panChanged(self.selectedElements, translation)
|
||||
case .ended:
|
||||
self.panEnded(self.selectedElements, translation)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
if let path = self.lassoBlackLayer.path {
|
||||
return path.contains(point)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func updatePath(_ bezierPath: BezierPath) {
|
||||
self.lassoBlackLayer.path = bezierPath.path.cgPath
|
||||
self.lassoWhiteLayer.path = bezierPath.path.cgPath
|
||||
}
|
||||
|
||||
func translate(_ offset: CGPoint) {
|
||||
let updatedPoints = self.currentPoints.map { $0.offsetBy(offset) }
|
||||
|
||||
self.apply(scale: self.currentScale, points: updatedPoints, selectedElements: self.selectedElements, expand: self.currentExpand)
|
||||
}
|
||||
|
||||
func apply(scale: CGFloat, points: [CGPoint], selectedElements: [UUID], expand: CGFloat) {
|
||||
self.currentScale = scale
|
||||
self.currentPoints = points
|
||||
self.selectedElements = selectedElements
|
||||
self.currentExpand = expand
|
||||
|
||||
let dash: CGFloat = 5.0 / scale
|
||||
|
||||
let hullPath = concaveHullPath(points: points)
|
||||
let expandedPath = expandPath(hullPath, width: expand)
|
||||
self.lassoBlackLayer.path = expandedPath
|
||||
self.lassoWhiteLayer.path = expandedPath
|
||||
|
||||
self.lassoBlackLayer.removeAllAnimations()
|
||||
self.lassoWhiteLayer.removeAllAnimations()
|
||||
|
||||
let blackAnimation = CABasicAnimation(keyPath: "lineDashPhase")
|
||||
blackAnimation.fromValue = 0
|
||||
blackAnimation.toValue = dash * 3.5
|
||||
blackAnimation.duration = 0.45
|
||||
blackAnimation.repeatCount = .infinity
|
||||
self.lassoBlackLayer.add(blackAnimation, forKey: "lineDashPhase")
|
||||
|
||||
self.lassoWhiteLayer.fillColor = UIColor.clear.cgColor
|
||||
self.lassoWhiteLayer.strokeColor = UIColor.white.cgColor
|
||||
self.lassoWhiteLayer.lineWidth = 2.0 / scale
|
||||
self.lassoWhiteLayer.lineJoin = .round
|
||||
self.lassoWhiteLayer.lineCap = .round
|
||||
self.lassoWhiteLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber]
|
||||
|
||||
let whiteAnimation = CABasicAnimation(keyPath: "lineDashPhase")
|
||||
whiteAnimation.fromValue = dash * 1.75
|
||||
whiteAnimation.toValue = dash * 3.5 + dash * 1.75
|
||||
whiteAnimation.duration = 0.45
|
||||
whiteAnimation.repeatCount = .infinity
|
||||
self.lassoWhiteLayer.add(whiteAnimation, forKey: "lineDashPhase")
|
||||
}
|
||||
|
||||
func reset() {
|
||||
self.bounds = CGRect(origin: .zero, size: self.bounds.size)
|
||||
|
||||
self.selectedElements = []
|
||||
|
||||
self.isHidden = true
|
||||
self.lassoBlackLayer.path = nil
|
||||
self.lassoWhiteLayer.path = nil
|
||||
self.lassoBlackLayer.removeAllAnimations()
|
||||
self.lassoWhiteLayer.removeAllAnimations()
|
||||
}
|
||||
}
|
||||
233
submodules/DrawingUI/Sources/EyedropperView.swift
Normal file
@@ -0,0 +1,233 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
|
||||
private let size = CGSize(width: 148.0, height: 148.0)
|
||||
private let outerWidth: CGFloat = 12.0
|
||||
private let ringWidth: CGFloat = 5.0
|
||||
private let selectionWidth: CGFloat = 4.0
|
||||
|
||||
private func generateShadowImage(size: CGSize) -> UIImage? {
|
||||
let inset: CGFloat = 60.0
|
||||
let imageSize = CGSize(width: size.width + inset * 2.0, height: size.height + inset * 2.0)
|
||||
return generateImage(imageSize, rotatedContext: { imageSize, context in
|
||||
context.clear(CGRect(origin: .zero, size: imageSize))
|
||||
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 40.0, color: UIColor(rgb: 0x000000, alpha: 0.9).cgColor)
|
||||
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.1).cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: inset, y: inset), size: size))
|
||||
})
|
||||
}
|
||||
|
||||
private func generateGridImage(size: CGSize, light: Bool) -> UIImage? {
|
||||
return generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
context.setFillColor(light ? UIColor.white.cgColor : UIColor(rgb: 0x505050).cgColor)
|
||||
|
||||
let lineWidth: CGFloat = 1.0
|
||||
var offset: CGFloat = 7.0
|
||||
for _ in 0 ..< 8 {
|
||||
context.fill(CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: size.width, height: lineWidth)))
|
||||
context.fill(CGRect(origin: CGPoint(x: offset, y: 0.0), size: CGSize(width: lineWidth, height: size.height)))
|
||||
|
||||
offset += 14.0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
final class EyedropperView: UIView {
|
||||
private weak var drawingView: DrawingView?
|
||||
|
||||
private let containerView: UIView
|
||||
private let shadowLayer: SimpleLayer
|
||||
private let clipView: UIView
|
||||
private let zoomedView: UIImageView
|
||||
|
||||
private let gridLayer: SimpleLayer
|
||||
|
||||
private let outerColorLayer: SimpleLayer
|
||||
private let ringLayer: SimpleLayer
|
||||
private let selectionLayer: SimpleLayer
|
||||
|
||||
private let sourceImage: (data: Data, size: CGSize, bytesPerRow: Int, info: CGBitmapInfo)?
|
||||
|
||||
var completed: (DrawingColor) -> Void = { _ in }
|
||||
|
||||
init(containerSize: CGSize, drawingView: DrawingView, sourceImage: UIImage) {
|
||||
self.drawingView = drawingView
|
||||
|
||||
self.zoomedView = UIImageView(image: sourceImage)
|
||||
self.zoomedView.isOpaque = true
|
||||
self.zoomedView.layer.magnificationFilter = .nearest
|
||||
|
||||
if let cgImage = sourceImage.cgImage, let pixelData = cgImage.dataProvider?.data as? Data {
|
||||
self.sourceImage = (pixelData, sourceImage.size, cgImage.bytesPerRow, cgImage.bitmapInfo)
|
||||
} else {
|
||||
self.sourceImage = nil
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
|
||||
self.containerView = UIView()
|
||||
self.containerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - size.width) / 2.0), y: floorToScreenPixels((containerSize.height - size.height) / 2.0)), size: size)
|
||||
|
||||
self.shadowLayer = SimpleLayer()
|
||||
self.shadowLayer.contents = generateShadowImage(size: size)?.cgImage
|
||||
self.shadowLayer.frame = bounds.insetBy(dx: -60.0, dy: -60.0)
|
||||
|
||||
let clipFrame = bounds.insetBy(dx: outerWidth + ringWidth, dy: outerWidth + ringWidth)
|
||||
self.clipView = UIView()
|
||||
self.clipView.clipsToBounds = true
|
||||
self.clipView.frame = bounds.insetBy(dx: outerWidth + ringWidth, dy: outerWidth + ringWidth)
|
||||
self.clipView.layer.cornerRadius = size.width / 2.0 - outerWidth - ringWidth
|
||||
if #available(iOS 13.0, *) {
|
||||
self.clipView.layer.cornerCurve = .circular
|
||||
}
|
||||
self.clipView.addSubview(self.zoomedView)
|
||||
|
||||
self.gridLayer = SimpleLayer()
|
||||
self.gridLayer.opacity = 0.6
|
||||
|
||||
self.gridLayer.frame = self.clipView.bounds
|
||||
self.gridLayer.contents = generateGridImage(size: clipFrame.size, light: true)?.cgImage
|
||||
|
||||
self.outerColorLayer = SimpleLayer()
|
||||
self.outerColorLayer.rasterizationScale = UIScreen.main.scale
|
||||
self.outerColorLayer.shouldRasterize = true
|
||||
self.outerColorLayer.frame = bounds
|
||||
self.outerColorLayer.cornerRadius = self.outerColorLayer.frame.width / 2.0
|
||||
self.outerColorLayer.borderWidth = outerWidth
|
||||
|
||||
self.ringLayer = SimpleLayer()
|
||||
self.ringLayer.rasterizationScale = UIScreen.main.scale
|
||||
self.ringLayer.shouldRasterize = true
|
||||
self.ringLayer.borderColor = UIColor.white.cgColor
|
||||
self.ringLayer.frame = bounds.insetBy(dx: outerWidth, dy: outerWidth)
|
||||
self.ringLayer.cornerRadius = self.ringLayer.frame.width / 2.0
|
||||
self.ringLayer.borderWidth = ringWidth
|
||||
|
||||
self.selectionLayer = SimpleLayer()
|
||||
self.selectionLayer.borderColor = UIColor.white.cgColor
|
||||
self.selectionLayer.borderWidth = selectionWidth
|
||||
self.selectionLayer.cornerRadius = 2.0
|
||||
self.selectionLayer.frame = CGRect(origin: CGPoint(x: clipFrame.minX + 48.0, y: clipFrame.minY + 48.0), size: CGSize(width: 17.0, height: 17.0)).insetBy(dx: -UIScreenPixel, dy: -UIScreenPixel)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
self.clipView.layer.addSublayer(self.gridLayer)
|
||||
|
||||
self.containerView.layer.addSublayer(self.shadowLayer)
|
||||
self.containerView.addSubview(self.clipView)
|
||||
self.containerView.layer.addSublayer(self.ringLayer)
|
||||
self.containerView.layer.addSublayer(self.outerColorLayer)
|
||||
self.containerView.layer.addSublayer(self.selectionLayer)
|
||||
|
||||
self.containerView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.updateColor()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var gridIsLight = true
|
||||
private var currentColor: DrawingColor?
|
||||
func setColor(_ color: UIColor) {
|
||||
self.currentColor = DrawingColor(color: color)
|
||||
self.outerColorLayer.borderColor = color.cgColor
|
||||
self.selectionLayer.backgroundColor = color.cgColor
|
||||
|
||||
if color.lightness > 0.9 {
|
||||
self.ringLayer.borderColor = UIColor(rgb: 0x999999).cgColor
|
||||
if self.gridIsLight {
|
||||
self.gridIsLight = false
|
||||
self.gridLayer.contents = generateGridImage(size: self.clipView.frame.size, light: false)?.cgImage
|
||||
}
|
||||
} else {
|
||||
self.ringLayer.borderColor = UIColor.white.cgColor
|
||||
if !self.gridIsLight {
|
||||
self.gridIsLight = true
|
||||
self.gridLayer.contents = generateGridImage(size: self.clipView.frame.size, light: true)?.cgImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getColorAt(_ point: CGPoint) -> UIColor? {
|
||||
guard var sourceImage = self.sourceImage, point.x >= 0 && point.x < sourceImage.size.width && point.y >= 0 && point.y < sourceImage.size.height else {
|
||||
return UIColor.black
|
||||
}
|
||||
|
||||
let x = Int(point.x)
|
||||
let y = Int(point.y)
|
||||
|
||||
var color: UIColor?
|
||||
sourceImage.data.withUnsafeMutableBytes { buffer in
|
||||
guard let bytes = buffer.assumingMemoryBound(to: UInt8.self).baseAddress else {
|
||||
return
|
||||
}
|
||||
|
||||
let srcLine = bytes.advanced(by: y * sourceImage.bytesPerRow)
|
||||
let srcPixel = srcLine + x * 4
|
||||
let r = srcPixel.pointee
|
||||
let g = srcPixel.advanced(by: 1).pointee
|
||||
let b = srcPixel.advanced(by: 2).pointee
|
||||
|
||||
if sourceImage.info.contains(.byteOrder32Little) {
|
||||
color = UIColor(red: CGFloat(b) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(r) / 255.0, alpha: 1.0)
|
||||
} else {
|
||||
color = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
private func updateColor() {
|
||||
guard let drawingView = self.drawingView else {
|
||||
return
|
||||
}
|
||||
var point = self.convert(self.containerView.center, to: drawingView)
|
||||
point.x /= drawingView.scale
|
||||
point.y /= drawingView.scale
|
||||
|
||||
let scale: CGFloat = 15.0
|
||||
self.zoomedView.transform = CGAffineTransformMakeScale(scale, scale)
|
||||
self.zoomedView.center = CGPoint(x: self.clipView.frame.width / 2.0 + (self.zoomedView.bounds.width / 2.0 - point.x) * scale, y: self.clipView.frame.height / 2.0 + (self.zoomedView.bounds.height / 2.0 - point.y) * scale)
|
||||
|
||||
if let color = self.getColorAt(point) {
|
||||
self.setColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
switch gestureRecognizer.state {
|
||||
case .changed:
|
||||
let translation = gestureRecognizer.translation(in: self)
|
||||
self.containerView.center = self.containerView.center.offsetBy(dx: translation.x, dy: translation.y)
|
||||
gestureRecognizer.setTranslation(.zero, in: self)
|
||||
|
||||
self.updateColor()
|
||||
case .ended, .cancelled:
|
||||
if let color = currentColor {
|
||||
self.containerView.alpha = 0.0
|
||||
self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
||||
self?.removeFromSuperview()
|
||||
})
|
||||
self.containerView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
||||
|
||||
self.completed(color)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
266
submodules/DrawingUI/Sources/ModeAndSizeComponent.swift
Normal file
@@ -0,0 +1,266 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import LegacyComponents
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SegmentedControlNode
|
||||
|
||||
private func generateMaskPath(size: CGSize, leftRadius: CGFloat, rightRadius: CGFloat) -> UIBezierPath {
|
||||
let path = UIBezierPath()
|
||||
path.addArc(withCenter: CGPoint(x: leftRadius, y: size.height / 2.0), radius: leftRadius, startAngle: .pi * 0.5, endAngle: -.pi * 0.5, clockwise: true)
|
||||
path.addArc(withCenter: CGPoint(x: size.width - rightRadius, y: size.height / 2.0), radius: rightRadius, startAngle: -.pi * 0.5, endAngle: .pi * 0.5, clockwise: true)
|
||||
path.close()
|
||||
return path
|
||||
}
|
||||
|
||||
private func generateKnobImage() -> UIImage? {
|
||||
let side: CGFloat = 28.0
|
||||
let margin: CGFloat = 10.0
|
||||
|
||||
let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side)))
|
||||
})
|
||||
return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5))
|
||||
}
|
||||
|
||||
class ModeAndSizeComponent: Component {
|
||||
let values: [String]
|
||||
let sizeValue: CGFloat
|
||||
let isEditing: Bool
|
||||
let isEnabled: Bool
|
||||
let rightInset: CGFloat
|
||||
let tag: AnyObject?
|
||||
let selectedIndex: Int
|
||||
let selectionChanged: (Int) -> Void
|
||||
let sizeUpdated: (CGFloat) -> Void
|
||||
let sizeReleased: () -> Void
|
||||
|
||||
init(values: [String], sizeValue: CGFloat, isEditing: Bool, isEnabled: Bool, rightInset: CGFloat, tag: AnyObject?, selectedIndex: Int, selectionChanged: @escaping (Int) -> Void, sizeUpdated: @escaping (CGFloat) -> Void, sizeReleased: @escaping () -> Void) {
|
||||
self.values = values
|
||||
self.sizeValue = sizeValue
|
||||
self.isEditing = isEditing
|
||||
self.isEnabled = isEnabled
|
||||
self.rightInset = rightInset
|
||||
self.tag = tag
|
||||
self.selectedIndex = selectedIndex
|
||||
self.selectionChanged = selectionChanged
|
||||
self.sizeUpdated = sizeUpdated
|
||||
self.sizeReleased = sizeReleased
|
||||
}
|
||||
|
||||
static func ==(lhs: ModeAndSizeComponent, rhs: ModeAndSizeComponent) -> Bool {
|
||||
if lhs.values != rhs.values {
|
||||
return false
|
||||
}
|
||||
if lhs.sizeValue != rhs.sizeValue {
|
||||
return false
|
||||
}
|
||||
if lhs.isEditing != rhs.isEditing {
|
||||
return false
|
||||
}
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
return false
|
||||
}
|
||||
if lhs.rightInset != rhs.rightInset {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedIndex != rhs.selectedIndex {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let node: SegmentedControlNode
|
||||
|
||||
private var knob: UIImageView
|
||||
|
||||
private let maskLayer = SimpleShapeLayer()
|
||||
|
||||
private var isEditing: Bool?
|
||||
private var isControlEnabled: Bool?
|
||||
private var sliderWidth: CGFloat = 0.0
|
||||
|
||||
fileprivate var updated: (CGFloat) -> Void = { _ in }
|
||||
fileprivate var released: () -> Void = { }
|
||||
|
||||
private var component: ModeAndSizeComponent?
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
let tag = tag as AnyObject
|
||||
if componentTag === tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3))
|
||||
self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0, cornerRadius: 16.0)
|
||||
|
||||
self.knob = UIImageView(image: generateKnobImage())
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.allowsGroupOpacity = true
|
||||
|
||||
self.addSubview(self.backgroundNode.view)
|
||||
self.addSubview(self.node.view)
|
||||
self.addSubview(self.knob)
|
||||
|
||||
self.backgroundNode.layer.mask = self.maskLayer
|
||||
|
||||
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
|
||||
pressGestureRecognizer.minimumPressDuration = 0.01
|
||||
pressGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(pressGestureRecognizer)
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
||||
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
|
||||
guard self.frame.width > 0.0, case .began = gestureRecognizer.state else {
|
||||
return
|
||||
}
|
||||
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
|
||||
self.updated(value)
|
||||
}
|
||||
|
||||
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
switch gestureRecognizer.state {
|
||||
case .changed:
|
||||
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
|
||||
guard self.frame.width > 0.0 else {
|
||||
return
|
||||
}
|
||||
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
|
||||
self.updated(value)
|
||||
case .ended, .cancelled:
|
||||
self.released()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled {
|
||||
return isEditing && isControlEnabled
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
self.node.alpha = 0.0
|
||||
self.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
|
||||
self.backgroundNode.alpha = 0.0
|
||||
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
|
||||
func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
self.updated = component.sizeUpdated
|
||||
self.released = component.sizeReleased
|
||||
|
||||
let previousIsEditing = self.isEditing
|
||||
self.isEditing = component.isEditing
|
||||
self.isControlEnabled = component.isEnabled
|
||||
|
||||
if component.isEditing {
|
||||
self.sliderWidth = availableSize.width
|
||||
}
|
||||
|
||||
self.node.items = component.values.map { SegmentedControlItem(title: $0) }
|
||||
self.node.setSelectedIndex(component.selectedIndex, animated: !transition.animation.isImmediate)
|
||||
let selectionChanged = component.selectionChanged
|
||||
self.node.selectedIndexChanged = { [weak self] index in
|
||||
self?.window?.endEditing(true)
|
||||
selectionChanged(index)
|
||||
}
|
||||
|
||||
let nodeSize = self.node.updateLayout(.stretchToFill(width: availableSize.width + component.rightInset), transition: transition.containedViewLayoutTransition)
|
||||
let size = CGSize(width: availableSize.width, height: nodeSize.height)
|
||||
transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: nodeSize))
|
||||
|
||||
var isDismissingEditing = false
|
||||
if component.isEditing != previousIsEditing && !component.isEditing {
|
||||
isDismissingEditing = true
|
||||
}
|
||||
|
||||
self.knob.alpha = component.isEditing ? 1.0 : 0.0
|
||||
if !isDismissingEditing {
|
||||
self.knob.frame = CGRect(origin: CGPoint(x: -12.0 + floorToScreenPixels((self.sliderWidth + 24.0 - self.knob.frame.size.width) * component.sizeValue), y: floorToScreenPixels((size.height - self.knob.frame.size.height) / 2.0)), size: self.knob.frame.size)
|
||||
}
|
||||
|
||||
if component.isEditing != previousIsEditing {
|
||||
let containedTransition = transition.containedViewLayoutTransition
|
||||
let maskPath: UIBezierPath
|
||||
if component.isEditing {
|
||||
maskPath = generateMaskPath(size: size, leftRadius: 2.0, rightRadius: 11.5)
|
||||
let selectionFrame = self.node.animateSelection(to: self.knob.center, transition: containedTransition)
|
||||
containedTransition.animateFrame(layer: self.knob.layer, from: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
|
||||
|
||||
self.knob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
} else {
|
||||
maskPath = generateMaskPath(size: size, leftRadius: 16.0, rightRadius: 16.0)
|
||||
if previousIsEditing != nil {
|
||||
let selectionFrame = self.node.animateSelection(from: self.knob.center, transition: containedTransition)
|
||||
containedTransition.animateFrame(layer: self.knob.layer, from: self.knob.frame, to: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
|
||||
self.knob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath)
|
||||
}
|
||||
|
||||
transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize))
|
||||
|
||||
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
if let screenTransition = transition.userData(DrawingScreenTransition.self) {
|
||||
switch screenTransition {
|
||||
case .animateIn:
|
||||
self.animateIn()
|
||||
case .animateOut:
|
||||
self.animateOut()
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
428
submodules/DrawingUI/Sources/PenTool.swift
Normal file
@@ -0,0 +1,428 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
final class PenTool: DrawingElement {
|
||||
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
func setup(size: CGSize) {
|
||||
self.shouldRasterize = true
|
||||
self.contentsScale = 1.0
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
self.frame = bounds
|
||||
}
|
||||
|
||||
private var line: StrokeLine?
|
||||
fileprivate func draw(line: StrokeLine, rect: CGRect) {
|
||||
self.line = line
|
||||
self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
|
||||
}
|
||||
|
||||
override func draw(in ctx: CGContext) {
|
||||
self.line?.drawInContext(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
let uuid = UUID()
|
||||
|
||||
let drawingSize: CGSize
|
||||
let color: DrawingColor
|
||||
let lineWidth: CGFloat
|
||||
let arrow: Bool
|
||||
|
||||
var path: Polyline?
|
||||
var boundingBox: CGRect?
|
||||
|
||||
private var renderLine: StrokeLine
|
||||
var didSetupArrow = false
|
||||
private var renderLineArrow1: StrokeLine?
|
||||
private var renderLineArrow2: StrokeLine?
|
||||
let renderLineWidth: CGFloat
|
||||
|
||||
var translation = CGPoint()
|
||||
|
||||
private var currentRenderLayer: DrawingRenderLayer?
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.path?.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
|
||||
}
|
||||
|
||||
var points: [Polyline.Point] {
|
||||
guard let linePath = self.path else {
|
||||
return []
|
||||
}
|
||||
var points: [Polyline.Point] = []
|
||||
for point in linePath.points {
|
||||
points.append(point.offsetBy(self.translation))
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
return false
|
||||
// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
if let linePath = self.path {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for point in linePath.points {
|
||||
if path.contains(point.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
self.drawingSize = drawingSize
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.arrow = arrow
|
||||
|
||||
let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003)
|
||||
let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09)
|
||||
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
|
||||
|
||||
self.renderLineWidth = lineWidth
|
||||
|
||||
self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth)
|
||||
if arrow {
|
||||
self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
|
||||
self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
|
||||
}
|
||||
}
|
||||
|
||||
func setupRenderLayer() -> DrawingRenderLayer? {
|
||||
let layer = RenderLayer()
|
||||
layer.setup(size: self.drawingSize)
|
||||
self.currentRenderLayer = layer
|
||||
return layer
|
||||
}
|
||||
|
||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
guard case let .polyline(line) = path, let point = line.points.last else {
|
||||
return
|
||||
}
|
||||
self.path = line
|
||||
|
||||
let rect = self.renderLine.draw(at: point)
|
||||
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
currentRenderLayer.draw(line: self.renderLine, rect: rect)
|
||||
}
|
||||
// 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
|
||||
|
||||
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
// currentRenderLayer.updatePath(cgPath)
|
||||
// }
|
||||
}
|
||||
|
||||
func draw(in context: CGContext, size: CGSize) {
|
||||
context.saveGState()
|
||||
|
||||
context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
|
||||
context.setShouldAntialias(true)
|
||||
|
||||
if self.arrow, let path = self.path, let lastPoint = path.points.last {
|
||||
var lastPointWithVelocity: Polyline.Point?
|
||||
for point in path.points.reversed() {
|
||||
if point.velocity > 0.0 {
|
||||
lastPointWithVelocity = point
|
||||
break
|
||||
}
|
||||
}
|
||||
if !self.didSetupArrow, let lastPointWithVelocity = lastPointWithVelocity {
|
||||
let w = self.renderLineWidth
|
||||
var dist: CGFloat = 18.0 * sqrt(w)
|
||||
let spread: CGFloat = .pi * max(0.05, 0.03 * sqrt(w))
|
||||
|
||||
let suffix = path.points.suffix(100).reversed()
|
||||
|
||||
var p0 = suffix.first!
|
||||
|
||||
var p2 = suffix.last!
|
||||
var d: CGFloat = 0
|
||||
for p in suffix {
|
||||
d += hypot(p0.location.x - p.location.x, p0.location.y - p.location.y)
|
||||
if d >= dist {
|
||||
p2 = p
|
||||
break
|
||||
}
|
||||
p0 = p
|
||||
}
|
||||
|
||||
p0 = suffix.first!
|
||||
dist = min(dist, hypot(p0.location.x - p2.location.x, p0.location.y - p2.location.y))
|
||||
|
||||
var i = 0
|
||||
for spread in [-spread, spread] {
|
||||
var points: [CGPoint] = []
|
||||
points.append(lastPoint.location)
|
||||
|
||||
p0 = suffix.first!
|
||||
var prev = p0.location
|
||||
d = 0
|
||||
for p in suffix {
|
||||
let d1 = hypot(p0.location.x - p.location.x, p0.location.y - p.location.y)
|
||||
d += d1
|
||||
if d >= dist {
|
||||
break
|
||||
}
|
||||
let d2 = d1 / cos(spread)
|
||||
let angle = atan2(p.location.y - p0.location.y, p.location.x - p0.location.x)
|
||||
let cur = CGPoint(x: prev.x + d2 * cos(angle + spread), y: prev.y + d2 * sin(angle + spread))
|
||||
|
||||
points.append(
|
||||
cur
|
||||
)
|
||||
|
||||
p0 = p
|
||||
prev = cur
|
||||
}
|
||||
|
||||
for point in points {
|
||||
if i == 0 {
|
||||
let _ = self.renderLineArrow1?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint))
|
||||
} else if i == 1 {
|
||||
let _ = self.renderLineArrow2?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint))
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
self.didSetupArrow = true
|
||||
}
|
||||
self.renderLineArrow1?.drawInContext(context)
|
||||
self.renderLineArrow2?.drawInContext(context)
|
||||
}
|
||||
|
||||
self.renderLine.drawInContext(context)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
private class StrokeLine {
|
||||
struct Segment {
|
||||
let a: CGPoint
|
||||
let b: CGPoint
|
||||
let c: CGPoint
|
||||
let d: CGPoint
|
||||
let abWidth: CGFloat
|
||||
let cdWidth: CGFloat
|
||||
}
|
||||
|
||||
struct Point {
|
||||
let position: CGPoint
|
||||
let width: CGFloat
|
||||
|
||||
init(position: CGPoint, width: CGFloat) {
|
||||
self.position = position
|
||||
self.width = width
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var points: [Point] = []
|
||||
private var smoothPoints: [Point] = []
|
||||
private var segments: [Segment] = []
|
||||
private var lastWidth: CGFloat?
|
||||
|
||||
private let minLineWidth: CGFloat
|
||||
let lineWidth: CGFloat
|
||||
|
||||
let color: UIColor
|
||||
|
||||
init(color: UIColor, minLineWidth: CGFloat, lineWidth: CGFloat) {
|
||||
self.color = color
|
||||
self.minLineWidth = minLineWidth
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
func draw(at point: Polyline.Point) -> CGRect {
|
||||
let width = extractLineWidth(from: point.velocity)
|
||||
self.lastWidth = width
|
||||
|
||||
let point = Point(position: point.location, width: width)
|
||||
return appendPoint(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
|
||||
|
||||
var size = max(minValue, min(maxValue + 1 - (velocity / 150), maxValue))
|
||||
if let lastWidth = self.lastWidth {
|
||||
size = size * 0.2 + lastWidth * 0.8
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 = 2.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()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
861
submodules/DrawingUI/Sources/StickerPickerScreen.swift
Normal file
@@ -0,0 +1,861 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import EntityKeyboard
|
||||
import PagerComponent
|
||||
|
||||
private final class StickerSelectionComponent: Component {
|
||||
public typealias EnvironmentType = Empty
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let deviceMetrics: DeviceMetrics
|
||||
public let stickerContent: EmojiPagerContentComponent
|
||||
public let backgroundColor: UIColor
|
||||
public let separatorColor: UIColor
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
stickerContent: EmojiPagerContentComponent,
|
||||
backgroundColor: UIColor,
|
||||
separatorColor: UIColor
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.deviceMetrics = deviceMetrics
|
||||
self.stickerContent = stickerContent
|
||||
self.backgroundColor = backgroundColor
|
||||
self.separatorColor = separatorColor
|
||||
}
|
||||
|
||||
public static func ==(lhs: StickerSelectionComponent, rhs: StickerSelectionComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings != rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.deviceMetrics != rhs.deviceMetrics {
|
||||
return false
|
||||
}
|
||||
if lhs.stickerContent != rhs.stickerContent {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.separatorColor != rhs.separatorColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let keyboardView: ComponentView<Empty>
|
||||
private let keyboardClippingView: UIView
|
||||
private let panelHostView: PagerExternalTopPanelContainer
|
||||
private let panelBackgroundView: BlurredBackgroundView
|
||||
private let panelSeparatorView: UIView
|
||||
|
||||
private var component: StickerSelectionComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.keyboardView = ComponentView<Empty>()
|
||||
self.keyboardClippingView = UIView()
|
||||
self.panelHostView = PagerExternalTopPanelContainer()
|
||||
self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.panelSeparatorView = UIView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.keyboardClippingView)
|
||||
self.addSubview(self.panelBackgroundView)
|
||||
self.addSubview(self.panelSeparatorView)
|
||||
self.addSubview(self.panelHostView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func update(component: StickerSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.backgroundColor = component.backgroundColor
|
||||
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
|
||||
self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate)
|
||||
self.panelSeparatorView.backgroundColor = component.separatorColor
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let topPanelHeight: CGFloat = 42.0
|
||||
|
||||
let keyboardSize = self.keyboardView.update(
|
||||
transition: transition,//.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
|
||||
component: AnyComponent(EntityKeyboardComponent(
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
isContentInFocus: true,
|
||||
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
|
||||
emojiContent: nil,
|
||||
stickerContent: component.stickerContent,
|
||||
gifContent: nil,
|
||||
hasRecentGifs: false,
|
||||
availableGifSearchEmojies: [],
|
||||
defaultToEmojiTab: false,
|
||||
externalTopPanelContainer: self.panelHostView,
|
||||
topPanelExtensionUpdated: { _, _ in },
|
||||
hideInputUpdated: { _, _, _ in },
|
||||
hideTopPanelUpdated: { _, _ in },
|
||||
switchToTextInput: {},
|
||||
switchToGifSubject: { _ in },
|
||||
reorderItems: { _, _ in },
|
||||
makeSearchContainerNode: { _ in return nil },
|
||||
deviceMetrics: component.deviceMetrics,
|
||||
hiddenInputHeight: 0.0,
|
||||
inputHeight: 0.0,
|
||||
displayBottomPanel: false,
|
||||
isExpanded: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let keyboardComponentView = self.keyboardView.view {
|
||||
if keyboardComponentView.superview == nil {
|
||||
self.keyboardClippingView.addSubview(keyboardComponentView)
|
||||
}
|
||||
|
||||
if panelBackgroundColor.alpha < 0.01 {
|
||||
self.keyboardClippingView.clipsToBounds = true
|
||||
} else {
|
||||
self.keyboardClippingView.clipsToBounds = false
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight)))
|
||||
|
||||
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize))
|
||||
transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0)))
|
||||
|
||||
transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight)))
|
||||
self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel)))
|
||||
transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public class StickerPickerScreen: ViewController {
|
||||
final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private var presentationData: PresentationData
|
||||
private weak var controller: StickerPickerScreen?
|
||||
private let theme: PresentationTheme
|
||||
|
||||
let dim: ASDisplayNode
|
||||
let wrappingView: UIView
|
||||
let containerView: UIView
|
||||
let scrollView: UIScrollView
|
||||
let hostView: ComponentHostView<Empty>
|
||||
|
||||
private var stickerContent: EmojiPagerContentComponent?
|
||||
private let stickerContentDisposable = MetaDisposable()
|
||||
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
|
||||
|
||||
private(set) var isExpanded = false
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer?
|
||||
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)?
|
||||
|
||||
private var currentIsVisible: Bool = false
|
||||
private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
||||
|
||||
fileprivate var temporaryDismiss = false
|
||||
|
||||
init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.controller = controller
|
||||
self.theme = theme
|
||||
|
||||
self.dim = ASDisplayNode()
|
||||
self.dim.alpha = 0.0
|
||||
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||
|
||||
self.wrappingView = UIView()
|
||||
self.containerView = UIView()
|
||||
self.scrollView = UIScrollView()
|
||||
self.hostView = ComponentHostView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
self.containerView.clipsToBounds = true
|
||||
self.containerView.backgroundColor = .clear
|
||||
|
||||
self.addSubnode(self.dim)
|
||||
|
||||
self.view.addSubview(self.wrappingView)
|
||||
self.wrappingView.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.scrollView)
|
||||
self.scrollView.addSubview(self.hostView)
|
||||
|
||||
self.stickerContentDisposable.set((
|
||||
EmojiPagerContentComponent.stickerInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
|
||||
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
|
||||
chatPeerId: context.account.peerId
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] content in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateContent(content)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerContentDisposable.dispose()
|
||||
}
|
||||
|
||||
|
||||
func updateContent(_ content: EmojiPagerContentComponent) {
|
||||
self.stickerContent = content
|
||||
content.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self] _, item, _, _, _, _ in
|
||||
guard let strongSelf = self, let file = item.itemFile else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.completion(file)
|
||||
strongSelf.controller?.dismiss(animated: true)
|
||||
},
|
||||
deleteBackwards: {},
|
||||
openStickerSettings: {
|
||||
// guard let controllerInteraction = controllerInteraction else {
|
||||
// return
|
||||
// }
|
||||
// let controller = installedStickerPacksController(context: context, mode: .modal)
|
||||
// controller.navigationPresentation = .modal
|
||||
// controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
},
|
||||
openFeatured: {
|
||||
// guard let controllerInteraction = controllerInteraction else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen(
|
||||
// context: context,
|
||||
// highlightedPackId: nil,
|
||||
// sendSticker: { [weak controllerInteraction] fileReference, sourceNode, sourceRect in
|
||||
// guard let controllerInteraction = controllerInteraction else {
|
||||
// return false
|
||||
// }
|
||||
// return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
// }
|
||||
// ))
|
||||
},
|
||||
openSearch: {},
|
||||
addGroupAction: { [weak self] groupId, isPremiumLocked in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
|
||||
if isPremiumLocked {
|
||||
// let controller = PremiumIntroScreen(context: context, source: .stickers)
|
||||
// controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
||||
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { views in
|
||||
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
return
|
||||
}
|
||||
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
||||
if featuredStickerPack.info.id == collectionId {
|
||||
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|
||||
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||
switch result {
|
||||
case let .result(info, items, installed):
|
||||
if installed {
|
||||
return .complete()
|
||||
} else {
|
||||
return context.engine.stickers.addStickerPackInteractively(info: info, items: items)
|
||||
}
|
||||
case .fetching:
|
||||
break
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
clearGroup: { [weak self] groupId in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
if groupId == AnyHashable("popular") {
|
||||
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
|
||||
var items: [ActionSheetItem] = []
|
||||
let context = controller.context
|
||||
items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true))
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular"))
|
||||
let _ = context.engine.stickers.clearRecentlyUsedReactions().start()
|
||||
}))
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
|
||||
}
|
||||
},
|
||||
pushController: { c in },
|
||||
presentController: { c in },
|
||||
presentGlobalOverlayController: { c in },
|
||||
navigationController: { [weak self] in
|
||||
return self?.controller?.navigationController as? NavigationController
|
||||
},
|
||||
requestUpdate: { _ in },
|
||||
updateSearchQuery: { _, _ in },
|
||||
chatPeerId: nil,
|
||||
peekBehavior: nil,
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
useOpaqueTheme: false
|
||||
)
|
||||
|
||||
if let (layout, navigationHeight) = self.currentLayout {
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
panRecognizer.delegate = self
|
||||
panRecognizer.delaysTouchesBegan = false
|
||||
panRecognizer.cancelsTouchesInView = true
|
||||
self.panGestureRecognizer = panRecognizer
|
||||
self.wrappingView.addGestureRecognizer(panRecognizer)
|
||||
|
||||
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
|
||||
self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
|
||||
}
|
||||
|
||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.controller?.completion(nil)
|
||||
self.controller?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let (layout, _) = self.currentLayout {
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let contentOffset = self.scrollView.contentOffset.y
|
||||
self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var isDismissing = false
|
||||
func animateIn() {
|
||||
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
|
||||
|
||||
let targetPosition = self.containerView.center
|
||||
let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height)
|
||||
|
||||
self.containerView.center = startPosition
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
transition.animateView(allowUserInteraction: true, {
|
||||
self.containerView.center = targetPosition
|
||||
}, completion: { _ in
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void = {}) {
|
||||
self.isDismissing = true
|
||||
|
||||
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in
|
||||
self?.controller?.dismiss(animated: false, completion: completion)
|
||||
})
|
||||
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0)
|
||||
|
||||
if !self.temporaryDismiss {
|
||||
self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition)
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
|
||||
self.currentLayout = (layout, navigationHeight)
|
||||
|
||||
self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0))
|
||||
|
||||
var effectiveExpanded = self.isExpanded
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
effectiveExpanded = true
|
||||
}
|
||||
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset
|
||||
let topInset: CGFloat
|
||||
if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments {
|
||||
if effectiveExpanded {
|
||||
topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset))
|
||||
} else {
|
||||
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
|
||||
}
|
||||
} else {
|
||||
topInset = effectiveExpanded ? 0.0 : edgeTopInset
|
||||
}
|
||||
transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil)
|
||||
|
||||
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset)
|
||||
self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let clipFrame: CGRect
|
||||
if layout.metrics.widthClass == .compact {
|
||||
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25)
|
||||
if isLandscape {
|
||||
self.containerView.layer.cornerRadius = 0.0
|
||||
} else {
|
||||
self.containerView.layer.cornerRadius = 10.0
|
||||
}
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
if layout.safeInsets.bottom.isZero {
|
||||
self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
} else {
|
||||
self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
}
|
||||
}
|
||||
|
||||
if isLandscape {
|
||||
clipFrame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
} else {
|
||||
let coveredByModalTransition: CGFloat = 0.0
|
||||
var containerTopInset: CGFloat = 10.0
|
||||
if let statusBarHeight = layout.statusBarHeight {
|
||||
containerTopInset += statusBarHeight
|
||||
}
|
||||
|
||||
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset))
|
||||
let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width
|
||||
let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
|
||||
let maxScaledTopInset: CGFloat = containerTopInset - 10.0
|
||||
let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
|
||||
let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
|
||||
|
||||
clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height)
|
||||
}
|
||||
} else {
|
||||
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
||||
self.containerView.layer.cornerRadius = 10.0
|
||||
|
||||
let verticalInset: CGFloat = 44.0
|
||||
|
||||
let maxSide = max(layout.size.width, layout.size.height)
|
||||
let minSide = min(layout.size.width, layout.size.height)
|
||||
let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
|
||||
clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.containerView, frame: clipFrame)
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil)
|
||||
|
||||
if let stickerContent = self.stickerContent {
|
||||
var stickersTransition: Transition = transition
|
||||
if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint {
|
||||
self.scheduledEmojiContentAnimationHint = nil
|
||||
let contentAnimation = scheduledEmojiContentAnimationHint
|
||||
stickersTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation)
|
||||
}
|
||||
|
||||
var contentSize = self.hostView.update(
|
||||
transition: stickersTransition,
|
||||
component: AnyComponent(
|
||||
StickerSelectionComponent(
|
||||
theme: self.theme,
|
||||
strings: self.presentationData.strings,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
stickerContent: stickerContent,
|
||||
backgroundColor: self.theme.list.itemBlocksBackgroundColor,
|
||||
separatorColor: self.theme.list.blocksBackgroundColor
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
forceUpdate: true,
|
||||
containerSize: CGSize(width: clipFrame.size.width, height: clipFrame.height)
|
||||
)
|
||||
contentSize.height = max(layout.size.height - navigationHeight, contentSize.height)
|
||||
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil)
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
}
|
||||
|
||||
private var didPlayAppearAnimation = false
|
||||
func updateIsVisible(isVisible: Bool) {
|
||||
if self.currentIsVisible == isVisible {
|
||||
return
|
||||
}
|
||||
self.currentIsVisible = isVisible
|
||||
|
||||
guard let currentLayout = self.currentLayout else {
|
||||
return
|
||||
}
|
||||
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate)
|
||||
|
||||
if !self.didPlayAppearAnimation {
|
||||
self.didPlayAppearAnimation = true
|
||||
self.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
private var defaultTopInset: CGFloat {
|
||||
guard let (layout, _) = self.currentLayout else{
|
||||
return 210.0
|
||||
}
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
var factor: CGFloat = 0.2488
|
||||
if layout.size.width <= 320.0 {
|
||||
factor = 0.15
|
||||
}
|
||||
return floor(max(layout.size.width, layout.size.height) * factor)
|
||||
} else {
|
||||
return 210.0
|
||||
}
|
||||
}
|
||||
|
||||
private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? {
|
||||
if let view = view {
|
||||
if let view = view as? UIScrollView, view.contentSize.width < view.contentSize.height {
|
||||
return (view, nil)
|
||||
}
|
||||
if let node = view.asyncdisplaykit_node as? ListView {
|
||||
return (node.scroller, node)
|
||||
}
|
||||
return findScrollView(view: view.superview)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard let (layout, navigationHeight) = self.currentLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
let point = recognizer.location(in: self.view)
|
||||
let currentHitView = self.hitTest(point, with: nil)
|
||||
|
||||
var scrollViewAndListNode = self.findScrollView(view: currentHitView)
|
||||
if scrollViewAndListNode?.0.frame.height == self.frame.width {
|
||||
scrollViewAndListNode = nil
|
||||
}
|
||||
let scrollView = scrollViewAndListNode?.0
|
||||
let listNode = scrollViewAndListNode?.1
|
||||
|
||||
let topInset: CGFloat
|
||||
if self.isExpanded {
|
||||
topInset = 0.0
|
||||
} else {
|
||||
topInset = edgeTopInset
|
||||
}
|
||||
|
||||
self.panGestureArguments = (topInset, 0.0, scrollView, listNode)
|
||||
case .changed:
|
||||
guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
|
||||
return
|
||||
}
|
||||
let visibleContentOffset = listNode?.visibleContentOffset()
|
||||
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
||||
|
||||
var translation = recognizer.translation(in: self.view).y
|
||||
|
||||
var currentOffset = topInset + translation
|
||||
|
||||
let epsilon = 1.0
|
||||
if case let .known(value) = visibleContentOffset, value <= epsilon {
|
||||
if let scrollView = scrollView {
|
||||
scrollView.bounces = false
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false)
|
||||
}
|
||||
} else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon {
|
||||
scrollView.bounces = false
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
||||
} else if let scrollView = scrollView {
|
||||
translation = panOffset
|
||||
currentOffset = topInset + translation
|
||||
if self.isExpanded {
|
||||
recognizer.setTranslation(CGPoint(), in: self.view)
|
||||
} else if currentOffset > 0.0 {
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.panGestureArguments = (topInset, translation, scrollView, listNode)
|
||||
|
||||
if !self.isExpanded {
|
||||
if currentOffset > 0.0, let scrollView = scrollView {
|
||||
scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = self.bounds
|
||||
if self.isExpanded {
|
||||
bounds.origin.y = -max(0.0, translation - edgeTopInset)
|
||||
} else {
|
||||
bounds.origin.y = -translation
|
||||
}
|
||||
bounds.origin.y = min(0.0, bounds.origin.y)
|
||||
self.bounds = bounds
|
||||
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||
case .ended:
|
||||
guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
|
||||
return
|
||||
}
|
||||
self.panGestureArguments = nil
|
||||
|
||||
let visibleContentOffset = listNode?.visibleContentOffset()
|
||||
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
||||
|
||||
let translation = recognizer.translation(in: self.view).y
|
||||
var velocity = recognizer.velocity(in: self.view)
|
||||
|
||||
if self.isExpanded {
|
||||
if case let .known(value) = visibleContentOffset, value > 0.1 {
|
||||
velocity = CGPoint()
|
||||
} else if case .unknown = visibleContentOffset {
|
||||
velocity = CGPoint()
|
||||
} else if contentOffset > 0.1 {
|
||||
velocity = CGPoint()
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = self.bounds
|
||||
if self.isExpanded {
|
||||
bounds.origin.y = -max(0.0, translation - edgeTopInset)
|
||||
} else {
|
||||
bounds.origin.y = -translation
|
||||
}
|
||||
bounds.origin.y = min(0.0, bounds.origin.y)
|
||||
|
||||
scrollView?.bounces = true
|
||||
|
||||
let offset = currentTopInset + panOffset
|
||||
let topInset: CGFloat = edgeTopInset
|
||||
|
||||
var dismissing = false
|
||||
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) {
|
||||
self.controller?.dismiss(animated: true, completion: nil)
|
||||
dismissing = true
|
||||
} else if self.isExpanded {
|
||||
if velocity.y > 300.0 || offset > topInset / 2.0 {
|
||||
self.isExpanded = false
|
||||
if let listNode = listNode {
|
||||
listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
||||
} else if let scrollView = scrollView {
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
||||
}
|
||||
|
||||
let distance = topInset - offset
|
||||
let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance)
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
|
||||
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
|
||||
} else {
|
||||
self.isExpanded = true
|
||||
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut)))
|
||||
}
|
||||
} else if (velocity.y < -300.0 || offset < topInset / 2.0) {
|
||||
if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode {
|
||||
DispatchQueue.main.async {
|
||||
listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset)
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
|
||||
self.isExpanded = true
|
||||
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
|
||||
} else {
|
||||
if let listNode = listNode {
|
||||
listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
||||
} else if let scrollView = scrollView {
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
||||
}
|
||||
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut)))
|
||||
}
|
||||
|
||||
if !dismissing {
|
||||
var bounds = self.bounds
|
||||
let previousBounds = bounds
|
||||
bounds.origin.y = 0.0
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
}
|
||||
case .cancelled:
|
||||
self.panGestureArguments = nil
|
||||
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||
guard isExpanded != self.isExpanded else {
|
||||
return
|
||||
}
|
||||
self.isExpanded = isExpanded
|
||||
|
||||
guard let (layout, navigationHeight) = self.currentLayout else {
|
||||
return
|
||||
}
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
|
||||
var node: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let theme: PresentationTheme
|
||||
|
||||
private var currentLayout: ContainerViewLayout?
|
||||
|
||||
public var pushController: (ViewController) -> Void = { _ in }
|
||||
public var presentController: (ViewController) -> Void = { _ in }
|
||||
|
||||
var completion: (TelegramMediaFile?) -> Void = { _ in }
|
||||
|
||||
public init(context: AccountContext) {
|
||||
self.context = context
|
||||
self.theme = defaultDarkColorPresentationTheme
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
override open func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, controller: self, theme: self.theme)
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
self.view.endEditing(true)
|
||||
if flag {
|
||||
self.node.animateOut(completion: {
|
||||
super.dismiss(animated: false, completion: {})
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
super.dismiss(animated: false, completion: {})
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
override open func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.node.updateIsVisible(isVisible: true)
|
||||
}
|
||||
|
||||
override open func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.node.updateIsVisible(isVisible: false)
|
||||
}
|
||||
|
||||
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.currentLayout = layout
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
let navigationHeight: CGFloat = 56.0
|
||||
|
||||
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
710
submodules/DrawingUI/Sources/TextSettingsComponent.swift
Normal file
@@ -0,0 +1,710 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import LegacyComponents
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
|
||||
enum DrawingTextStyle: Equatable {
|
||||
case regular
|
||||
case filled
|
||||
case semi
|
||||
case stroke
|
||||
|
||||
init(style: DrawingTextEntity.Style) {
|
||||
switch style {
|
||||
case .regular:
|
||||
self = .regular
|
||||
case .filled:
|
||||
self = .filled
|
||||
case .semi:
|
||||
self = .semi
|
||||
case .stroke:
|
||||
self = .stroke
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DrawingTextAlignment: Equatable {
|
||||
case left
|
||||
case center
|
||||
case right
|
||||
|
||||
init(alignment: DrawingTextEntity.Alignment) {
|
||||
switch alignment {
|
||||
case .left:
|
||||
self = .left
|
||||
case .center:
|
||||
self = .center
|
||||
case .right:
|
||||
self = .right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DrawingTextFont: Equatable, CaseIterable {
|
||||
case sanFrancisco
|
||||
case newYork
|
||||
case monospaced
|
||||
case round
|
||||
|
||||
init(font: DrawingTextEntity.Font) {
|
||||
switch font {
|
||||
case .sanFrancisco:
|
||||
self = .sanFrancisco
|
||||
case .newYork:
|
||||
self = .newYork
|
||||
case .monospaced:
|
||||
self = .monospaced
|
||||
case .round:
|
||||
self = .round
|
||||
}
|
||||
}
|
||||
|
||||
var font: DrawingTextEntity.Font {
|
||||
switch self {
|
||||
case .sanFrancisco:
|
||||
return .sanFrancisco
|
||||
case .newYork:
|
||||
return .newYork
|
||||
case .monospaced:
|
||||
return .monospaced
|
||||
case .round:
|
||||
return .round
|
||||
}
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .sanFrancisco:
|
||||
return "San Francisco"
|
||||
case .newYork:
|
||||
return "New York"
|
||||
case .monospaced:
|
||||
return "Monospaced"
|
||||
case .round:
|
||||
return "Rounded"
|
||||
}
|
||||
}
|
||||
|
||||
var uiFont: UIFont {
|
||||
switch self {
|
||||
case .sanFrancisco:
|
||||
return Font.semibold(13.0)
|
||||
case .newYork:
|
||||
return Font.with(size: 13.0, design: .serif, weight: .semibold)
|
||||
case .monospaced:
|
||||
return Font.with(size: 13.0, design: .monospace, weight: .semibold)
|
||||
case .round:
|
||||
return Font.with(size: 13.0, design: .round, weight: .semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class TextAlignmentComponent: Component {
|
||||
let alignment: DrawingTextAlignment
|
||||
|
||||
init(alignment: DrawingTextAlignment) {
|
||||
self.alignment = alignment
|
||||
}
|
||||
|
||||
static func == (lhs: TextAlignmentComponent, rhs: TextAlignmentComponent) -> Bool {
|
||||
return lhs.alignment == rhs.alignment
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let line1 = SimpleLayer()
|
||||
private let line2 = SimpleLayer()
|
||||
private let line3 = SimpleLayer()
|
||||
private let line4 = SimpleLayer()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
let lines = [self.line1, self.line2, self.line3, self.line4]
|
||||
lines.forEach { line in
|
||||
line.backgroundColor = UIColor.white.cgColor
|
||||
line.cornerRadius = 1.0
|
||||
line.masksToBounds = true
|
||||
self.layer.addSublayer(line)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: TextAlignmentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let height = 2.0 - UIScreenPixel
|
||||
let spacing: CGFloat = 3.0 + UIScreenPixel
|
||||
let long = 21.0
|
||||
let short = 13.0
|
||||
|
||||
let size = CGSize(width: long, height: 18.0)
|
||||
|
||||
switch component.alignment {
|
||||
case .left:
|
||||
transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: long, height: height)))
|
||||
transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing), size: CGSize(width: short, height: height)))
|
||||
transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing + height + spacing), size: CGSize(width: long, height: height)))
|
||||
transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height)))
|
||||
case .center:
|
||||
transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - long) / 2.0), y: 0.0), size: CGSize(width: long, height: height)))
|
||||
transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - short) / 2.0), y: height + spacing), size: CGSize(width: short, height: height)))
|
||||
transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - long) / 2.0), y: height + spacing + height + spacing), size: CGSize(width: long, height: height)))
|
||||
transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - short) / 2.0), y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height)))
|
||||
case .right:
|
||||
transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: size.width - long, y: 0.0), size: CGSize(width: long, height: height)))
|
||||
transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: size.width - short, y: height + spacing), size: CGSize(width: short, height: height)))
|
||||
transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: size.width - long, y: height + spacing + height + spacing), size: CGSize(width: long, height: height)))
|
||||
transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: size.width - short, y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height)))
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class TextFontComponent: Component {
|
||||
let values: [DrawingTextFont]
|
||||
let selectedValue: DrawingTextFont
|
||||
let updated: (DrawingTextFont) -> Void
|
||||
|
||||
init(values: [DrawingTextFont], selectedValue: DrawingTextFont, updated: @escaping (DrawingTextFont) -> Void) {
|
||||
self.values = values
|
||||
self.selectedValue = selectedValue
|
||||
self.updated = updated
|
||||
}
|
||||
|
||||
static func == (lhs: TextFontComponent, rhs: TextFontComponent) -> Bool {
|
||||
return lhs.values == rhs.values && lhs.selectedValue == rhs.selectedValue
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var buttons: [DrawingTextFont: HighlightableButton] = [:]
|
||||
private let scrollView = UIScrollView()
|
||||
private let scrollMask = UIView()
|
||||
private let maskLeft = SimpleGradientLayer()
|
||||
private let maskCenter = SimpleLayer()
|
||||
private let maskRight = SimpleGradientLayer()
|
||||
|
||||
private var updated: (DrawingTextFont) -> Void = { _ in }
|
||||
|
||||
override init(frame: CGRect) {
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.decelerationRate = .fast
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.mask = self.scrollMask
|
||||
|
||||
self.maskLeft.type = .axial
|
||||
self.maskLeft.startPoint = CGPoint(x: 0.0, y: 0.5)
|
||||
self.maskLeft.endPoint = CGPoint(x: 1.0, y: 0.5)
|
||||
self.maskLeft.colors = [UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.cgColor]
|
||||
self.maskLeft.locations = [0.0, 1.0]
|
||||
|
||||
self.maskCenter.backgroundColor = UIColor.white.cgColor
|
||||
|
||||
self.maskRight.type = .axial
|
||||
self.maskRight.startPoint = CGPoint(x: 0.0, y: 0.5)
|
||||
self.maskRight.endPoint = CGPoint(x: 1.0, y: 0.5)
|
||||
self.maskRight.colors = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
|
||||
self.maskRight.locations = [0.0, 1.0]
|
||||
|
||||
self.scrollMask.layer.addSublayer(self.maskLeft)
|
||||
self.scrollMask.layer.addSublayer(self.maskCenter)
|
||||
self.scrollMask.layer.addSublayer(self.maskRight)
|
||||
|
||||
self.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed(_ sender: HighlightableButton) {
|
||||
for (font, button) in self.buttons {
|
||||
if button === sender {
|
||||
self.updated(font)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var previousValue: DrawingTextFont?
|
||||
func update(component: TextFontComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.updated = component.updated
|
||||
|
||||
var contentWidth: CGFloat = 0.0
|
||||
|
||||
for value in component.values {
|
||||
contentWidth += 12.0
|
||||
let button: HighlightableButton
|
||||
if let current = self.buttons[value] {
|
||||
button = current
|
||||
} else {
|
||||
button = HighlightableButton()
|
||||
button.setTitle(value.title, for: .normal)
|
||||
button.titleLabel?.font = value.uiFont
|
||||
button.sizeToFit()
|
||||
button.frame = CGRect(origin: .zero, size: CGSize(width: button.frame.width + 16.0, height: 30.0))
|
||||
button.layer.cornerRadius = 11.0
|
||||
button.addTarget(self, action: #selector(self.pressed(_:)), for: .touchUpInside)
|
||||
|
||||
self.buttons[value] = button
|
||||
|
||||
self.scrollView.addSubview(button)
|
||||
}
|
||||
|
||||
if value == component.selectedValue {
|
||||
button.layer.borderWidth = 1.0 - UIScreenPixel
|
||||
button.layer.borderColor = UIColor.white.cgColor
|
||||
} else {
|
||||
button.layer.borderWidth = UIScreenPixel
|
||||
button.layer.borderColor = UIColor.white.withAlphaComponent(0.5).cgColor
|
||||
}
|
||||
|
||||
button.frame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: button.frame.size)
|
||||
contentWidth += button.frame.width
|
||||
}
|
||||
contentWidth += 12.0
|
||||
|
||||
if self.scrollView.contentSize.width != contentWidth {
|
||||
self.scrollView.contentSize = CGSize(width: contentWidth, height: 30.0)
|
||||
}
|
||||
self.scrollView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
self.scrollMask.frame = CGRect(origin: .zero, size: availableSize)
|
||||
self.maskLeft.frame = CGRect(origin: .zero, size: CGSize(width: 12.0, height: 30.0))
|
||||
self.maskCenter.frame = CGRect(origin: CGPoint(x: 12.0, y: 0.0), size: CGSize(width: availableSize.width - 24.0, height: 30.0))
|
||||
self.maskRight.frame = CGRect(origin: CGPoint(x: availableSize.width - 12.0, y: 0.0), size: CGSize(width: 12.0, height: 30.0))
|
||||
|
||||
if component.selectedValue != self.previousValue {
|
||||
self.previousValue = component.selectedValue
|
||||
|
||||
if let button = self.buttons[component.selectedValue] {
|
||||
self.scrollView.scrollRectToVisible(button.frame.insetBy(dx: -48.0, dy: 0.0), animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class TextSettingsComponent: CombinedComponent {
|
||||
let color: DrawingColor?
|
||||
let style: DrawingTextStyle
|
||||
let alignment: DrawingTextAlignment
|
||||
let font: DrawingTextFont
|
||||
|
||||
let presentColorPicker: () -> Void
|
||||
let presentFastColorPicker: (GenericComponentViewTag) -> Void
|
||||
let updateFastColorPickerPan: (CGPoint) -> Void
|
||||
let dismissFastColorPicker: () -> Void
|
||||
let toggleStyle: () -> Void
|
||||
let toggleAlignment: () -> Void
|
||||
let updateFont: (DrawingTextFont) -> Void
|
||||
|
||||
init(
|
||||
color: DrawingColor?,
|
||||
style: DrawingTextStyle,
|
||||
alignment: DrawingTextAlignment,
|
||||
font: DrawingTextFont,
|
||||
presentColorPicker: @escaping () -> Void = {},
|
||||
presentFastColorPicker: @escaping (GenericComponentViewTag) -> Void = { _ in },
|
||||
updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in },
|
||||
dismissFastColorPicker: @escaping () -> Void = {},
|
||||
toggleStyle: @escaping () -> Void,
|
||||
toggleAlignment: @escaping () -> Void,
|
||||
updateFont: @escaping (DrawingTextFont) -> Void
|
||||
) {
|
||||
self.color = color
|
||||
self.style = style
|
||||
self.alignment = alignment
|
||||
self.font = font
|
||||
self.presentColorPicker = presentColorPicker
|
||||
self.presentFastColorPicker = presentFastColorPicker
|
||||
self.updateFastColorPickerPan = updateFastColorPickerPan
|
||||
self.dismissFastColorPicker = dismissFastColorPicker
|
||||
self.toggleStyle = toggleStyle
|
||||
self.toggleAlignment = toggleAlignment
|
||||
self.updateFont = updateFont
|
||||
}
|
||||
|
||||
static func ==(lhs: TextSettingsComponent, rhs: TextSettingsComponent) -> Bool {
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.style != rhs.style {
|
||||
return false
|
||||
}
|
||||
if lhs.alignment != rhs.alignment {
|
||||
return false
|
||||
}
|
||||
if lhs.font != rhs.font {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
enum ImageKey: Hashable {
|
||||
case regular
|
||||
case filled
|
||||
case semi
|
||||
case stroke
|
||||
}
|
||||
private var cachedImages: [ImageKey: UIImage] = [:]
|
||||
func image(_ key: ImageKey) -> UIImage {
|
||||
if let image = self.cachedImages[key] {
|
||||
return image
|
||||
} else {
|
||||
var image: UIImage
|
||||
switch key {
|
||||
case .regular:
|
||||
image = UIImage(bundleImageName: "Media Editor/TextDefault")!
|
||||
case .filled:
|
||||
image = UIImage(bundleImageName: "Media Editor/TextFilled")!
|
||||
case .semi:
|
||||
image = UIImage(bundleImageName: "Media Editor/TextSemi")!
|
||||
case .stroke:
|
||||
image = UIImage(bundleImageName: "Media Editor/TextStroke")!
|
||||
}
|
||||
cachedImages[key] = image
|
||||
return image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
State()
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let colorButton = Child(ColorSwatchComponent.self)
|
||||
let colorButtonTag = GenericComponentViewTag()
|
||||
|
||||
let alignmentButton = Child(Button.self)
|
||||
let styleButton = Child(Button.self)
|
||||
let font = Child(TextFontComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
let state = context.state
|
||||
|
||||
let toggleStyle = component.toggleStyle
|
||||
let toggleAlignment = component.toggleAlignment
|
||||
let updateFont = component.updateFont
|
||||
|
||||
var offset: CGFloat = 6.0
|
||||
if let color = component.color {
|
||||
let presentColorPicker = component.presentColorPicker
|
||||
let presentFastColorPicker = component.presentFastColorPicker
|
||||
let updateFastColorPickerPan = component.updateFastColorPickerPan
|
||||
let dismissFastColorPicker = component.dismissFastColorPicker
|
||||
|
||||
let colorButton = colorButton.update(
|
||||
component: ColorSwatchComponent(
|
||||
type: .main,
|
||||
color: color,
|
||||
tag: colorButtonTag,
|
||||
action: {
|
||||
presentColorPicker()
|
||||
},
|
||||
holdAction: {
|
||||
presentFastColorPicker(colorButtonTag)
|
||||
},
|
||||
pan: { point in
|
||||
updateFastColorPickerPan(point)
|
||||
},
|
||||
release: {
|
||||
dismissFastColorPicker()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(colorButton
|
||||
.position(CGPoint(x: colorButton.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
offset += 44.0
|
||||
}
|
||||
|
||||
let styleImage: UIImage
|
||||
switch component.style {
|
||||
case .regular:
|
||||
styleImage = state.image(.regular)
|
||||
case .filled:
|
||||
styleImage = state.image(.filled)
|
||||
case .semi:
|
||||
styleImage = state.image(.semi)
|
||||
case .stroke:
|
||||
styleImage = state.image(.stroke)
|
||||
}
|
||||
|
||||
let styleButton = styleButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(
|
||||
image: styleImage
|
||||
)
|
||||
),
|
||||
action: {
|
||||
toggleStyle()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(styleButton
|
||||
.position(CGPoint(x: offset + styleButton.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
.update(Transition.Update { _, view, transition in
|
||||
if let snapshot = view.snapshotView(afterScreenUpdates: false) {
|
||||
transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in
|
||||
snapshot?.removeFromSuperview()
|
||||
})
|
||||
snapshot.frame = view.frame
|
||||
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
|
||||
view.superview?.addSubview(snapshot)
|
||||
}
|
||||
})
|
||||
)
|
||||
offset += 44.0
|
||||
|
||||
let alignmentButton = alignmentButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
TextAlignmentComponent(
|
||||
alignment: component.alignment
|
||||
)
|
||||
),
|
||||
action: {
|
||||
toggleAlignment()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
availableSize: context.availableSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(alignmentButton
|
||||
.position(CGPoint(x: offset + alignmentButton.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
offset += 44.0
|
||||
|
||||
let font = font.update(
|
||||
component: TextFontComponent(
|
||||
values: DrawingTextFont.allCases,
|
||||
selectedValue: component.font,
|
||||
updated: { font in
|
||||
updateFont(font)
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - offset, height: 30.0),
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(font
|
||||
.position(CGPoint(x: offset + font.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
offset += 44.0
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generateMaskPath(size: CGSize, topRadius: CGFloat, bottomRadius: CGFloat) -> UIBezierPath {
|
||||
let path = UIBezierPath()
|
||||
path.addArc(withCenter: CGPoint(x: size.width / 2.0, y: topRadius), radius: topRadius, startAngle: .pi, endAngle: 0, clockwise: true)
|
||||
path.addArc(withCenter: CGPoint(x: size.width / 2.0, y: size.height - bottomRadius), radius: bottomRadius, startAngle: 0, endAngle: .pi, clockwise: true)
|
||||
path.close()
|
||||
return path
|
||||
}
|
||||
|
||||
private func generateKnobImage() -> UIImage? {
|
||||
let side: CGFloat = 32.0
|
||||
let margin: CGFloat = 10.0
|
||||
|
||||
let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side)))
|
||||
})
|
||||
return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5))
|
||||
}
|
||||
|
||||
final class TextSizeSliderComponent: Component {
|
||||
let value: CGFloat
|
||||
let updated: (CGFloat) -> Void
|
||||
|
||||
public init(
|
||||
value: CGFloat,
|
||||
updated: @escaping (CGFloat) -> Void
|
||||
) {
|
||||
self.value = value
|
||||
self.updated = updated
|
||||
}
|
||||
|
||||
public static func ==(lhs: TextSizeSliderComponent, rhs: TextSizeSliderComponent) -> Bool {
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UIGestureRecognizerDelegate {
|
||||
private var validSize: CGSize?
|
||||
private var component: TextSizeSliderComponent?
|
||||
|
||||
private let backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3))
|
||||
private let maskLayer = SimpleShapeLayer()
|
||||
|
||||
private let knobContainer = SimpleLayer()
|
||||
private let knob = SimpleLayer()
|
||||
|
||||
fileprivate var updated: (CGFloat) -> Void = { _ in }
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect())
|
||||
|
||||
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
|
||||
pressGestureRecognizer.minimumPressDuration = 0.01
|
||||
pressGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(pressGestureRecognizer)
|
||||
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))))
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
private var isTracking: Bool?
|
||||
private var isPanning = false
|
||||
private var isPressing = false
|
||||
|
||||
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
||||
guard self.frame.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.isPressing = true
|
||||
if let size = self.validSize, let component = self.component {
|
||||
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
let location = gestureRecognizer.location(in: self).offsetBy(dx: 0.0, dy: -12.0)
|
||||
let value = 1.0 - max(0.0, min(1.0, location.y / (self.frame.height - 24.0)))
|
||||
self.updated(value)
|
||||
case .ended, .cancelled:
|
||||
self.isPressing = false
|
||||
if let size = self.validSize, let component = self.component {
|
||||
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard self.frame.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
self.isPanning = true
|
||||
if let size = self.validSize, let component = self.component {
|
||||
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
let location = gestureRecognizer.location(in: self).offsetBy(dx: 0.0, dy: -12.0)
|
||||
let value = 1.0 - max(0.0, min(1.0, location.y / (self.frame.height - 24.0)))
|
||||
self.updated(value)
|
||||
case .ended, .cancelled:
|
||||
self.isPanning = false
|
||||
if let size = self.validSize, let component = self.component {
|
||||
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, component: TextSizeSliderComponent, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let previousSize = self.validSize
|
||||
self.validSize = size
|
||||
|
||||
if self.backgroundNode.view.superview == nil {
|
||||
self.addSubview(self.backgroundNode.view)
|
||||
}
|
||||
if self.knobContainer.superlayer == nil {
|
||||
self.layer.addSublayer(self.knobContainer)
|
||||
}
|
||||
if self.knob.superlayer == nil {
|
||||
self.knob.contents = generateKnobImage()?.cgImage
|
||||
self.knobContainer.addSublayer(self.knob)
|
||||
}
|
||||
|
||||
let isTracking = self.isPanning || self.isPressing
|
||||
if self.isTracking != isTracking {
|
||||
self.isTracking = isTracking
|
||||
transition.setSublayerTransform(view: self, transform: isTracking ? CATransform3DMakeTranslation(8.0, 0.0, 0.0) : CATransform3DMakeTranslation(-size.width / 2.0, 0.0, 0.0))
|
||||
transition.setSublayerTransform(layer: self.knobContainer, transform: isTracking ? CATransform3DIdentity : CATransform3DMakeTranslation(4.0, 0.0, 0.0))
|
||||
}
|
||||
|
||||
let knobSize = CGSize(width: 52.0, height: 52.0)
|
||||
self.knob.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - knobSize.width) / 2.0), y: -12.0 + floorToScreenPixels((size.height + 24.0 - knobSize.height) * (1.0 - component.value))), size: knobSize)
|
||||
|
||||
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
transition.setFrame(layer: self.knobContainer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if previousSize != size {
|
||||
transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: size))
|
||||
self.maskLayer.path = generateMaskPath(size: size, topRadius: 15.0, bottomRadius: 3.0).cgPath
|
||||
self.backgroundNode.layer.mask = self.maskLayer
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
view.updated = self.updated
|
||||
return view.updateLayout(size: availableSize, component: self, transition: transition)
|
||||
}
|
||||
}
|
||||
630
submodules/DrawingUI/Sources/ToolsComponent.swift
Normal file
@@ -0,0 +1,630 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import LegacyComponents
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
|
||||
private let toolSize = CGSize(width: 40.0, height: 176.0)
|
||||
|
||||
private class ToolView: UIView, UIGestureRecognizerDelegate {
|
||||
let type: DrawingToolState.Key
|
||||
|
||||
var isSelected = false
|
||||
var isToolFocused = false
|
||||
var isVisible = false
|
||||
private var currentSize: CGFloat?
|
||||
private var currentEraserMode: DrawingToolState.EraserState.Mode?
|
||||
|
||||
private let tip: UIImageView
|
||||
private let background: SimpleLayer
|
||||
private let band: SimpleGradientLayer
|
||||
private let eraserType: SimpleLayer
|
||||
|
||||
var pressed: (DrawingToolState.Key) -> Void = { _ in }
|
||||
var swiped: (DrawingToolState.Key, CGFloat) -> Void = { _, _ in }
|
||||
var released: () -> Void = { }
|
||||
|
||||
init(type: DrawingToolState.Key) {
|
||||
self.type = type
|
||||
self.tip = UIImageView()
|
||||
self.tip.isUserInteractionEnabled = false
|
||||
|
||||
self.background = SimpleLayer()
|
||||
|
||||
self.band = SimpleGradientLayer()
|
||||
self.band.cornerRadius = 2.0
|
||||
self.band.type = .axial
|
||||
self.band.startPoint = CGPoint(x: 0.0, y: 0.5)
|
||||
self.band.endPoint = CGPoint(x: 1.0, y: 0.5)
|
||||
self.band.masksToBounds = true
|
||||
|
||||
self.eraserType = SimpleLayer()
|
||||
self.eraserType.opacity = 0.0
|
||||
self.eraserType.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
|
||||
|
||||
let backgroundImage: UIImage?
|
||||
let tipImage: UIImage?
|
||||
|
||||
var tipAbove = true
|
||||
var hasBand = true
|
||||
var hasEraserType = false
|
||||
|
||||
switch type {
|
||||
case .pen:
|
||||
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolPen")
|
||||
tipImage = UIImage(bundleImageName: "Media Editor/ToolPenTip")?.withRenderingMode(.alwaysTemplate)
|
||||
case .marker:
|
||||
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolMarker")
|
||||
tipImage = UIImage(bundleImageName: "Media Editor/ToolMarkerTip")?.withRenderingMode(.alwaysTemplate)
|
||||
tipAbove = false
|
||||
case .neon:
|
||||
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolNeon")
|
||||
tipImage = UIImage(bundleImageName: "Media Editor/ToolNeonTip")?.withRenderingMode(.alwaysTemplate)
|
||||
tipAbove = false
|
||||
case .pencil:
|
||||
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolPencil")
|
||||
tipImage = UIImage(bundleImageName: "Media Editor/ToolPencilTip")?.withRenderingMode(.alwaysTemplate)
|
||||
case .lasso:
|
||||
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolLasso")
|
||||
tipImage = nil
|
||||
hasBand = false
|
||||
case .eraser:
|
||||
self.eraserType.contents = UIImage(bundleImageName: "Media Editor/EraserRemove")?.cgImage
|
||||
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolEraser")
|
||||
tipImage = nil
|
||||
hasBand = false
|
||||
hasEraserType = true
|
||||
}
|
||||
|
||||
self.tip.image = tipImage
|
||||
self.background.contents = backgroundImage?.cgImage
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: toolSize))
|
||||
|
||||
self.tip.frame = CGRect(origin: .zero, size: toolSize)
|
||||
self.background.frame = CGRect(origin: .zero, size: toolSize)
|
||||
|
||||
self.band.frame = CGRect(origin: CGPoint(x: 3.0, y: 64.0), size: CGSize(width: toolSize.width - 6.0, height: toolSize.width - 16.0))
|
||||
self.band.anchorPoint = CGPoint(x: 0.5, y: 0.0)
|
||||
|
||||
self.eraserType.position = CGPoint(x: 20.0, y: 56.0)
|
||||
self.eraserType.bounds = CGRect(origin: .zero, size: CGSize(width: 16.0, height: 16.0))
|
||||
|
||||
if tipAbove {
|
||||
self.layer.addSublayer(self.background)
|
||||
self.addSubview(self.tip)
|
||||
} else {
|
||||
self.addSubview(self.tip)
|
||||
self.layer.addSublayer(self.background)
|
||||
}
|
||||
|
||||
if hasBand {
|
||||
self.layer.addSublayer(self.band)
|
||||
}
|
||||
|
||||
if hasEraserType {
|
||||
self.layer.addSublayer(self.eraserType)
|
||||
}
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is UIPanGestureRecognizer {
|
||||
if self.isSelected && !self.isToolFocused {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return self.isVisible
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
self.pressed(self.type)
|
||||
}
|
||||
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let size = self.currentSize else {
|
||||
return
|
||||
}
|
||||
switch gestureRecognizer.state {
|
||||
case .changed:
|
||||
let translation = gestureRecognizer.translation(in: self)
|
||||
gestureRecognizer.setTranslation(.zero, in: self)
|
||||
|
||||
let updatedSize = max(0.0, min(1.0, size - translation.y / 200.0))
|
||||
self.swiped(self.type, updatedSize)
|
||||
case .ended, .cancelled:
|
||||
self.released()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(animated: Bool, delay: Double = 0.0) {
|
||||
let layout = {
|
||||
self.bounds = CGRect(origin: .zero, size: self.bounds.size)
|
||||
}
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.5, delay: delay, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, animations: layout)
|
||||
} else {
|
||||
layout()
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(animated: Bool, delay: Double = 0.0, completion: @escaping () -> Void = {}) {
|
||||
let layout = {
|
||||
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: -140.0), size: self.bounds.size)
|
||||
}
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.5, delay: delay, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, animations: layout, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
} else {
|
||||
layout()
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func update(state: DrawingToolState) {
|
||||
if let _ = self.tip.image {
|
||||
let color = state.color?.toUIColor()
|
||||
self.tip.tintColor = color
|
||||
|
||||
self.currentSize = state.size
|
||||
|
||||
guard let color = color else {
|
||||
return
|
||||
}
|
||||
var locations: [NSNumber] = [0.0, 1.0]
|
||||
var colors: [CGColor] = []
|
||||
switch self.type {
|
||||
case .pen:
|
||||
locations = [0.0, 0.15, 0.85, 1.0]
|
||||
colors = [
|
||||
color.withMultipliedBrightnessBy(0.7).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withMultipliedBrightnessBy(0.7).cgColor
|
||||
]
|
||||
case .marker:
|
||||
locations = [0.0, 0.15, 0.85, 1.0]
|
||||
colors = [
|
||||
color.withMultipliedBrightnessBy(0.7).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withMultipliedBrightnessBy(0.7).cgColor
|
||||
]
|
||||
case .neon:
|
||||
locations = [0.0, 0.15, 0.85, 1.0]
|
||||
colors = [
|
||||
color.withMultipliedBrightnessBy(0.7).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withMultipliedBrightnessBy(0.7).cgColor
|
||||
]
|
||||
case .pencil:
|
||||
locations = [0.0, 0.25, 0.25, 0.75, 0.75, 1.0]
|
||||
colors = [
|
||||
color.withMultipliedBrightnessBy(0.85).cgColor,
|
||||
color.withMultipliedBrightnessBy(0.85).cgColor,
|
||||
color.withMultipliedBrightnessBy(1.15).cgColor,
|
||||
color.withMultipliedBrightnessBy(1.15).cgColor,
|
||||
color.withMultipliedBrightnessBy(0.85).cgColor,
|
||||
color.withMultipliedBrightnessBy(0.85).cgColor
|
||||
]
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
self.band.transform = CATransform3DMakeScale(1.0, 0.08 + 0.92 * (state.size ?? 1.0), 1.0)
|
||||
|
||||
self.band.locations = locations
|
||||
self.band.colors = colors
|
||||
}
|
||||
|
||||
if case .eraser = self.type {
|
||||
let previousEraserMode = self.currentEraserMode
|
||||
self.currentEraserMode = state.eraserMode
|
||||
|
||||
let transition = Transition(animation: Transition.Animation.curve(duration: 0.2, curve: .easeInOut))
|
||||
if [.vector, .blur].contains(state.eraserMode) {
|
||||
if !self.eraserType.opacity.isZero && (previousEraserMode != self.currentEraserMode) {
|
||||
let snapshot = SimpleShapeLayer()
|
||||
snapshot.contents = self.eraserType.contents
|
||||
snapshot.frame = self.eraserType.frame
|
||||
self.layer.addSublayer(snapshot)
|
||||
|
||||
snapshot.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak snapshot] _ in
|
||||
snapshot?.removeFromSuperlayer()
|
||||
})
|
||||
snapshot.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
|
||||
|
||||
self.eraserType.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.eraserType.animateScale(from: 0.001, to: 1.0, duration: 0.2)
|
||||
} else {
|
||||
transition.setAlpha(layer: self.eraserType, alpha: 1.0)
|
||||
transition.setScale(layer: self.eraserType, scale: 1.0)
|
||||
}
|
||||
|
||||
self.eraserType.contents = UIImage(bundleImageName: state.eraserMode == .vector ? "Media Editor/EraserRemove" : "Media Editor/BrushBlur")?.cgImage
|
||||
} else {
|
||||
transition.setAlpha(layer: self.eraserType, alpha: 0.0)
|
||||
transition.setScale(layer: self.eraserType, scale: 0.001)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ToolsComponent: Component {
|
||||
let state: DrawingState
|
||||
let isFocused: Bool
|
||||
let tag: AnyObject?
|
||||
let toolPressed: (DrawingToolState.Key) -> Void
|
||||
let toolResized: (DrawingToolState.Key, CGFloat) -> Void
|
||||
let sizeReleased: () -> Void
|
||||
|
||||
init(state: DrawingState, isFocused: Bool, tag: AnyObject?, toolPressed: @escaping (DrawingToolState.Key) -> Void, toolResized: @escaping (DrawingToolState.Key, CGFloat) -> Void, sizeReleased: @escaping () -> Void) {
|
||||
self.state = state
|
||||
self.isFocused = isFocused
|
||||
self.tag = tag
|
||||
self.toolPressed = toolPressed
|
||||
self.toolResized = toolResized
|
||||
self.sizeReleased = sizeReleased
|
||||
}
|
||||
|
||||
static func == (lhs: ToolsComponent, rhs: ToolsComponent) -> Bool {
|
||||
return lhs.state == rhs.state && lhs.isFocused == rhs.isFocused
|
||||
}
|
||||
|
||||
public final class View: UIView, ComponentTaggedView {
|
||||
private let toolViews: [ToolView]
|
||||
private let maskImageView: UIImageView
|
||||
|
||||
private var isToolFocused: Bool?
|
||||
|
||||
private var component: ToolsComponent?
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
let tag = tag as AnyObject
|
||||
if componentTag === tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
var toolViews: [ToolView] = []
|
||||
for type in DrawingToolState.Key.allCases {
|
||||
toolViews.append(ToolView(type: type))
|
||||
}
|
||||
self.toolViews = toolViews
|
||||
|
||||
self.maskImageView = UIImageView()
|
||||
self.maskImageView.image = generateGradientImage(size: CGSize(width: 1.0, height: 120.0), colors: [UIColor.white, UIColor.white, UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 0.88, 1.0], direction: .vertical)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.mask = self.maskImageView
|
||||
|
||||
toolViews.forEach { self.addSubview($0) }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result === self {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func animateIn(completion: @escaping () -> Void) {
|
||||
var delay = 0.0
|
||||
for i in 0 ..< self.toolViews.count {
|
||||
let view = self.toolViews[i]
|
||||
view.animateOut(animated: false)
|
||||
view.animateIn(animated: true, delay: delay)
|
||||
delay += 0.025
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
var delay = 0.0
|
||||
for i in 0 ..< self.toolViews.count {
|
||||
let view = self.toolViews[i]
|
||||
view.animateOut(animated: true, delay: delay, completion: i == self.toolViews.count - 1 ? completion : {})
|
||||
delay += 0.025
|
||||
|
||||
transition.setPosition(view: view, position: CGPoint(x: view.center.x, y: toolSize.height / 2.0 - 30.0 + 34.0))
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ToolsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let wasFocused = self.isToolFocused
|
||||
|
||||
self.isToolFocused = component.isFocused
|
||||
|
||||
let toolPressed = component.toolPressed
|
||||
let toolResized = component.toolResized
|
||||
let toolSizeReleased = component.sizeReleased
|
||||
|
||||
let spacing: CGFloat = 44.0
|
||||
let totalWidth = spacing * CGFloat(self.toolViews.count - 1)
|
||||
|
||||
let left = (availableSize.width - totalWidth) / 2.0
|
||||
var xPositions: [CGFloat] = []
|
||||
|
||||
var selectedIndex = 0
|
||||
let isFocused = component.isFocused
|
||||
|
||||
for i in 0 ..< self.toolViews.count {
|
||||
xPositions.append(left + spacing * CGFloat(i))
|
||||
|
||||
if self.toolViews[i].type == component.state.selectedTool {
|
||||
selectedIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
if isFocused {
|
||||
let originalFocusedToolPosition = xPositions[selectedIndex]
|
||||
xPositions[selectedIndex] = availableSize.width / 2.0
|
||||
|
||||
let delta = availableSize.width / 2.0 - originalFocusedToolPosition
|
||||
|
||||
for i in 0 ..< xPositions.count {
|
||||
if i != selectedIndex {
|
||||
xPositions[i] += delta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var offset: CGFloat = 100.0
|
||||
for i in 0 ..< self.toolViews.count {
|
||||
let view = self.toolViews[i]
|
||||
|
||||
var scale = 0.5
|
||||
var verticalOffset: CGFloat = 34.0
|
||||
if i == selectedIndex {
|
||||
if isFocused {
|
||||
scale = 1.0
|
||||
verticalOffset = 30.0
|
||||
} else {
|
||||
verticalOffset = 18.0
|
||||
}
|
||||
view.isSelected = true
|
||||
view.isToolFocused = isFocused
|
||||
view.isVisible = true
|
||||
} else {
|
||||
view.isSelected = false
|
||||
view.isToolFocused = false
|
||||
view.isVisible = !isFocused
|
||||
}
|
||||
view.isUserInteractionEnabled = view.isVisible
|
||||
|
||||
let layout = {
|
||||
view.center = CGPoint(x: xPositions[i], y: toolSize.height / 2.0 - 30.0 + verticalOffset)
|
||||
view.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
}
|
||||
if case .curve = transition.animation {
|
||||
UIView.animate(
|
||||
withDuration: 0.7,
|
||||
delay: 0.0,
|
||||
usingSpringWithDamping: 0.6,
|
||||
initialSpringVelocity: 0.0,
|
||||
options: .allowUserInteraction,
|
||||
animations: layout)
|
||||
} else {
|
||||
layout()
|
||||
}
|
||||
|
||||
view.update(state: component.state.toolState(for: view.type))
|
||||
|
||||
view.pressed = { type in
|
||||
toolPressed(type)
|
||||
}
|
||||
view.swiped = { type, size in
|
||||
toolResized(type, size)
|
||||
}
|
||||
view.released = {
|
||||
toolSizeReleased()
|
||||
}
|
||||
|
||||
offset += 44.0
|
||||
}
|
||||
|
||||
|
||||
if wasFocused != nil && wasFocused != component.isFocused {
|
||||
var animated = false
|
||||
if case .curve = transition.animation {
|
||||
animated = true
|
||||
}
|
||||
if isFocused {
|
||||
var delay = 0.0
|
||||
for i in (selectedIndex + 1 ..< self.toolViews.count).reversed() {
|
||||
let view = self.toolViews[i]
|
||||
view.animateOut(animated: animated, delay: delay)
|
||||
delay += 0.025
|
||||
}
|
||||
delay = 0.0
|
||||
for i in (0 ..< selectedIndex) {
|
||||
let view = self.toolViews[i]
|
||||
view.animateOut(animated: animated, delay: delay)
|
||||
delay += 0.025
|
||||
}
|
||||
} else {
|
||||
var delay = 0.0
|
||||
for i in (selectedIndex + 1 ..< self.toolViews.count) {
|
||||
let view = self.toolViews[i]
|
||||
view.animateIn(animated: animated, delay: delay)
|
||||
delay += 0.025
|
||||
}
|
||||
delay = 0.0
|
||||
for i in (0 ..< selectedIndex).reversed() {
|
||||
let view = self.toolViews[i]
|
||||
view.animateIn(animated: animated, delay: delay)
|
||||
delay += 0.025
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.maskImageView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
if let screenTransition = transition.userData(DrawingScreenTransition.self) {
|
||||
switch screenTransition {
|
||||
case .animateIn:
|
||||
self.animateIn(completion: {})
|
||||
case .animateOut:
|
||||
self.animateOut(completion: {})
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class BrushButtonContent: CombinedComponent {
|
||||
let title: String
|
||||
let image: UIImage
|
||||
|
||||
init(
|
||||
title: String,
|
||||
image: UIImage
|
||||
) {
|
||||
self.title = title
|
||||
self.image = image
|
||||
}
|
||||
|
||||
static func ==(lhs: BrushButtonContent, rhs: BrushButtonContent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.image !== rhs.image {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let title = Child(Text.self)
|
||||
let image = Child(Image.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let title = title.update(
|
||||
component: Text(
|
||||
text: component.title,
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let image = image.update(
|
||||
component: Image(image: component.image),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(image
|
||||
.position(CGPoint(x: context.availableSize.width - image.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width - image.size.width - title.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ZoomOutButtonContent: CombinedComponent {
|
||||
let title: String
|
||||
let image: UIImage
|
||||
|
||||
init(
|
||||
title: String,
|
||||
image: UIImage
|
||||
) {
|
||||
self.title = title
|
||||
self.image = image
|
||||
}
|
||||
|
||||
static func ==(lhs: ZoomOutButtonContent, rhs: ZoomOutButtonContent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.image !== rhs.image {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let title = Child(Text.self)
|
||||
let image = Child(Image.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let title = title.update(
|
||||
component: Text(
|
||||
text: component.title,
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let image = image.update(
|
||||
component: Image(image: component.image),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let spacing: CGFloat = 2.0
|
||||
let width = title.size.width + spacing + image.size.width
|
||||
context.add(image
|
||||
.position(CGPoint(x: image.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(title
|
||||
.position(CGPoint(x: image.size.width + spacing + title.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return CGSize(width: width, height: context.availableSize.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
241
submodules/DrawingUI/Sources/Unistroke.swift
Normal file
@@ -0,0 +1,241 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private let pointsCount: Int = 64
|
||||
private let squareSize: Double = 250.0
|
||||
private let diagonal = sqrt(squareSize * squareSize + squareSize * squareSize)
|
||||
private let halfDiagonal = diagonal * 0.5
|
||||
private let angleRange: Double = .pi / 4.0
|
||||
private let anglePrecision: Double = .pi / 90.0
|
||||
|
||||
class Unistroke {
|
||||
let points: [CGPoint]
|
||||
|
||||
init(points: [CGPoint]) {
|
||||
var points = resample(points: points, totalPoints: pointsCount)
|
||||
let radians = indicativeAngle(points: points)
|
||||
points = rotate(points: points, byRadians: -radians)
|
||||
points = scale(points: points, toSize: squareSize)
|
||||
points = translate(points: points, to: .zero)
|
||||
self.points = points
|
||||
}
|
||||
|
||||
func match(templates: [UnistrokeTemplate], minThreshold: Double = 0.8) -> String? {
|
||||
var bestDistance = Double.infinity
|
||||
var bestTemplate: UnistrokeTemplate?
|
||||
for template in templates {
|
||||
let templateDistance = distanceAtBestAngle(points: self.points, strokeTemplate: template.points, fromAngle: -angleRange, toAngle: angleRange, threshold: anglePrecision)
|
||||
if templateDistance < bestDistance {
|
||||
bestDistance = templateDistance
|
||||
bestTemplate = template
|
||||
}
|
||||
}
|
||||
|
||||
if let bestTemplate = bestTemplate {
|
||||
bestDistance = 1.0 - bestDistance / halfDiagonal
|
||||
if bestDistance < minThreshold {
|
||||
return nil
|
||||
}
|
||||
return bestTemplate.name
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnistrokeTemplate : Unistroke {
|
||||
var name: String
|
||||
|
||||
init(name: String, points: [CGPoint]) {
|
||||
self.name = name
|
||||
super.init(points: points)
|
||||
}
|
||||
}
|
||||
|
||||
private struct Edge {
|
||||
var minX: Double
|
||||
var minY: Double
|
||||
var maxX: Double
|
||||
var maxY: Double
|
||||
|
||||
init(minX: Double, maxX: Double, minY: Double, maxY: Double) {
|
||||
self.minX = minX
|
||||
self.minY = minY
|
||||
self.maxX = maxX
|
||||
self.maxY = maxY
|
||||
}
|
||||
|
||||
mutating func addPoint(value: CGPoint) {
|
||||
self.minX = min(self.minX,value.x)
|
||||
self.maxX = max(self.maxX,value.x)
|
||||
self.minY = min(self.minY,value.y)
|
||||
self.maxY = max(self.maxY,value.y)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension Double {
|
||||
func toRadians() -> Double {
|
||||
let res = self * .pi / 180.0
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
private func resample(points: [CGPoint], totalPoints: Int) -> [CGPoint] {
|
||||
var initialPoints = points
|
||||
let interval = pathLength(points: initialPoints) / Double(totalPoints - 1)
|
||||
var totalLength: Double = 0.0
|
||||
var newPoints: [CGPoint] = [points[0]]
|
||||
for i in 1 ..< initialPoints.count {
|
||||
let currentLength = initialPoints[i - 1].distance(to: initialPoints[i])
|
||||
if totalLength + currentLength >= interval {
|
||||
let newPoint = CGPoint(
|
||||
x: initialPoints[i - 1].x + ((interval - totalLength) / currentLength) * (initialPoints[i].x - initialPoints[i - 1].x),
|
||||
y: initialPoints[i - 1].y + ((interval - totalLength) / currentLength) * (initialPoints[i].y - initialPoints[i - 1].y)
|
||||
)
|
||||
newPoints.append(newPoint)
|
||||
initialPoints.insert(newPoint, at: i)
|
||||
totalLength = 0.0
|
||||
} else {
|
||||
totalLength += currentLength
|
||||
}
|
||||
}
|
||||
if newPoints.count == totalPoints - 1 {
|
||||
newPoints.append(points.last!)
|
||||
}
|
||||
return newPoints
|
||||
}
|
||||
|
||||
private func pathLength(points: [CGPoint]) -> Double {
|
||||
var distance: Double = 0.0
|
||||
for index in 1 ..< points.count {
|
||||
distance += points[index - 1].distance(to: points[index])
|
||||
}
|
||||
return distance
|
||||
}
|
||||
|
||||
private func pathDistance(path1: [CGPoint], path2: [CGPoint]) -> Double {
|
||||
var d: Double = 0.0
|
||||
for idx in 0 ..< min(path1.count, path2.count) {
|
||||
d += path1[idx].distance(to: path2[idx])
|
||||
}
|
||||
return d / Double(path1.count)
|
||||
}
|
||||
|
||||
private func centroid(points: [CGPoint]) -> CGPoint {
|
||||
var centroidPoint: CGPoint = .zero
|
||||
for point in points {
|
||||
centroidPoint.x = centroidPoint.x + point.x
|
||||
centroidPoint.y = centroidPoint.y + point.y
|
||||
}
|
||||
centroidPoint.x = (centroidPoint.x / Double(points.count))
|
||||
centroidPoint.y = (centroidPoint.y / Double(points.count))
|
||||
return centroidPoint
|
||||
}
|
||||
|
||||
private func boundingBox(points: [CGPoint]) -> CGRect {
|
||||
var edge = Edge(minX: +Double.infinity, maxX: -Double.infinity, minY: +Double.infinity, maxY: -Double.infinity)
|
||||
for point in points {
|
||||
edge.addPoint(value: point)
|
||||
}
|
||||
return CGRect(x: edge.minX, y: edge.minY, width: (edge.maxX - edge.minX), height: (edge.maxY - edge.minY))
|
||||
}
|
||||
|
||||
private func rotate(points: [CGPoint], byRadians radians: Double) -> [CGPoint] {
|
||||
let centroid = centroid(points: points)
|
||||
let cosinus = cos(radians)
|
||||
let sinus = sin(radians)
|
||||
var result: [CGPoint] = []
|
||||
for point in points {
|
||||
result.append(
|
||||
CGPoint(
|
||||
x: (point.x - centroid.x) * cosinus - (point.y - centroid.y) * sinus + centroid.x,
|
||||
y: (point.x - centroid.x) * sinus + (point.y - centroid.y) * cosinus + centroid.y
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func scale(points: [CGPoint], toSize size: Double) -> [CGPoint] {
|
||||
let boundingBox = boundingBox(points: points)
|
||||
var result: [CGPoint] = []
|
||||
for point in points {
|
||||
result.append(
|
||||
CGPoint(
|
||||
x: point.x * (size / boundingBox.width),
|
||||
y: point.y * (size / boundingBox.height)
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func translate(points: [CGPoint], to pt: CGPoint) -> [CGPoint] {
|
||||
let centroidPoint = centroid(points: points)
|
||||
var newPoints: [CGPoint] = []
|
||||
for point in points {
|
||||
newPoints.append(
|
||||
CGPoint(
|
||||
x: point.x + pt.x - centroidPoint.x,
|
||||
y: point.y + pt.y - centroidPoint.y
|
||||
)
|
||||
)
|
||||
}
|
||||
return newPoints
|
||||
}
|
||||
|
||||
private func vectorize(points: [CGPoint]) -> [Double] {
|
||||
var sum: Double = 0.0
|
||||
var vector: [Double] = []
|
||||
for point in points {
|
||||
vector.append(point.x)
|
||||
vector.append(point.y)
|
||||
sum += (point.x * point.x) + (point.y * point.y)
|
||||
}
|
||||
let magnitude = sqrt(sum)
|
||||
for i in 0 ..< vector.count {
|
||||
vector[i] = vector[i] / magnitude
|
||||
}
|
||||
return vector
|
||||
}
|
||||
|
||||
private func indicativeAngle(points: [CGPoint]) -> Double {
|
||||
let centroid = centroid(points: points)
|
||||
return atan2(centroid.y - points[0].y, centroid.x - points[0].x)
|
||||
}
|
||||
|
||||
private func distanceAtBestAngle(points: [CGPoint], strokeTemplate: [CGPoint], fromAngle: Double, toAngle: Double, threshold: Double) -> Double {
|
||||
func distanceAtAngle(points: [CGPoint], strokeTemplate: [CGPoint], radians: Double) -> Double {
|
||||
let rotatedPoints = rotate(points: points, byRadians: radians)
|
||||
return pathDistance(path1: rotatedPoints, path2: strokeTemplate)
|
||||
}
|
||||
|
||||
let phi: Double = (0.5 * (-1.0 + sqrt(5.0)))
|
||||
|
||||
var fromAngle = fromAngle
|
||||
var toAngle = toAngle
|
||||
|
||||
var x1 = phi * fromAngle + (1.0 - phi) * toAngle
|
||||
var f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1)
|
||||
|
||||
var x2 = (1.0 - phi) * fromAngle + phi * toAngle
|
||||
var f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2)
|
||||
|
||||
while abs(toAngle - fromAngle) > threshold {
|
||||
if f1 < f2 {
|
||||
toAngle = x2
|
||||
x2 = x1
|
||||
f2 = f1
|
||||
x1 = phi * fromAngle + (1.0 - phi) * toAngle
|
||||
f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1)
|
||||
} else {
|
||||
fromAngle = x1
|
||||
x1 = x2
|
||||
f1 = f2
|
||||
x2 = (1.0 - phi) * fromAngle + phi * toAngle
|
||||
f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2)
|
||||
}
|
||||
}
|
||||
return min(f1, f2)
|
||||
}
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
@protocol TGMediaEditAdjustments;
|
||||
|
||||
@protocol TGPhotoEditorTabProtocol
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@interface TGPhotoEditorTabController : TGViewController
|
||||
{
|
||||
bool _dismissing;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol TGPhotoPaintStickerRenderView <NSObject>
|
||||
|
||||
@property (nonatomic, copy) void(^started)(double);
|
||||
@@ -56,6 +57,65 @@
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol TGPhotoDrawingView <NSObject>
|
||||
|
||||
@property (nonatomic, readonly) BOOL isTracking;
|
||||
|
||||
@property (nonatomic, copy) void(^zoomOut)(void);
|
||||
|
||||
- (void)updateZoomScale:(CGFloat)scale;
|
||||
|
||||
@end
|
||||
|
||||
@protocol TGPhotoDrawingEntitiesView <NSObject>
|
||||
|
||||
@property (nonatomic, copy) void(^hasSelectionChanged)(bool);
|
||||
@property (nonatomic, readonly) BOOL hasSelection;
|
||||
|
||||
- (void)play;
|
||||
- (void)pause;
|
||||
- (void)seekTo:(double)timestamp;
|
||||
- (void)resetToStart;
|
||||
- (void)updateVisibility:(BOOL)visibility;
|
||||
- (void)clearSelection;
|
||||
- (void)onZoom;
|
||||
|
||||
- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer;
|
||||
- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer;
|
||||
|
||||
@end
|
||||
|
||||
@protocol TGPhotoDrawingInterfaceController <NSObject>
|
||||
|
||||
@property (nonatomic, copy) void(^requestDismiss)(void);
|
||||
@property (nonatomic, copy) void(^requestApply)(void);
|
||||
@property (nonatomic, copy) UIImage *(^getCurrentImage)(void);
|
||||
|
||||
- (TGPaintingData *)generateResultData;
|
||||
- (void)animateOut:(void(^)(void))completion;
|
||||
|
||||
- (void)adapterContainerLayoutUpdatedSize:(CGSize)size
|
||||
intrinsicInsets:(UIEdgeInsets)intrinsicInsets
|
||||
safeInsets:(UIEdgeInsets)safeInsets
|
||||
statusBarHeight:(CGFloat)statusBarHeight
|
||||
inputHeight:(CGFloat)inputHeight
|
||||
animated:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol TGPhotoDrawingAdapter <NSObject>
|
||||
|
||||
@property (nonatomic, readonly) id<TGPhotoDrawingView> drawingView;
|
||||
@property (nonatomic, readonly) id<TGPhotoDrawingEntitiesView> drawingEntitiesView;
|
||||
@property (nonatomic, readonly) UIView * selectionContainerView;
|
||||
@property (nonatomic, readonly) UIView * contentWrapperView;
|
||||
@property (nonatomic, readonly) id<TGPhotoDrawingInterfaceController> interfaceController;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol TGPhotoPaintStickersContext <NSObject>
|
||||
|
||||
- (int64_t)documentIdForDocument:(id)document;
|
||||
@@ -67,4 +127,6 @@
|
||||
|
||||
@property (nonatomic, copy) id<TGCaptionPanelView>(^captionPanelView)(void);
|
||||
|
||||
- (id<TGPhotoDrawingAdapter>)drawingAdapter:(CGSize)size;
|
||||
|
||||
@end
|
||||
|
||||
@@ -23,3 +23,10 @@ typedef enum {
|
||||
- (instancetype)initWithText:(NSString *)text font:(TGPhotoPaintFont *)font swatch:(TGPaintSwatch *)swatch baseFontSize:(CGFloat)baseFontSize maxWidth:(CGFloat)maxWidth style:(TGPhotoPaintTextEntityStyle)style;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface TGPhotoPaintStaticEntity : TGPhotoPaintEntity
|
||||
|
||||
@property (nonatomic, strong) UIImage *renderImage;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#import "TGPhotoEditorTabController.h"
|
||||
|
||||
#import <LegacyComponents/LegacyComponentsContext.h>
|
||||
|
||||
@class PGPhotoEditor;
|
||||
@class TGPhotoEditorPreviewView;
|
||||
|
||||
@protocol TGPhotoPaintStickersContext;
|
||||
|
||||
@interface TGPhotoDrawingController : TGPhotoEditorTabController
|
||||
|
||||
@property (nonatomic, copy) void (^requestDismiss)(void);
|
||||
@property (nonatomic, copy) void (^requestApply)(void);
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext;
|
||||
|
||||
- (TGPaintingData *)paintingData;
|
||||
|
||||
@end
|
||||
893
submodules/LegacyComponents/Sources/TGPhotoDrawingController.m
Normal file
@@ -0,0 +1,893 @@
|
||||
#import "TGPhotoDrawingController.h"
|
||||
#import "TGPhotoPaintController.h"
|
||||
|
||||
#import "LegacyComponentsInternal.h"
|
||||
|
||||
#import <LegacyComponents/UIImage+TG.h>
|
||||
|
||||
#import <LegacyComponents/TGPaintUtils.h>
|
||||
#import <LegacyComponents/TGPhotoEditorUtils.h>
|
||||
#import <LegacyComponents/TGPhotoEditorAnimation.h>
|
||||
#import "TGPhotoEditorInterfaceAssets.h"
|
||||
#import <LegacyComponents/TGObserverProxy.h>
|
||||
|
||||
#import <LegacyComponents/TGMenuView.h>
|
||||
#import <LegacyComponents/TGModernButton.h>
|
||||
|
||||
#import <LegacyComponents/TGMediaAsset.h>
|
||||
#import <LegacyComponents/TGMediaAssetImageSignals.h>
|
||||
|
||||
#import <LegacyComponents/TGPaintingData.h>
|
||||
|
||||
#import "TGPaintingWrapperView.h"
|
||||
#import "TGPhotoEditorSparseView.h"
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
|
||||
#import "PGPhotoEditor.h"
|
||||
#import "TGPhotoEditorPreviewView.h"
|
||||
|
||||
|
||||
|
||||
|
||||
#import "TGPaintCanvas.h"
|
||||
#import "TGPainting.h"
|
||||
|
||||
@interface TGPhotoDrawingController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
|
||||
{
|
||||
id<LegacyComponentsContext> _context;
|
||||
id<TGPhotoPaintStickersContext> _stickersContext;
|
||||
id<TGPhotoDrawingAdapter> _drawingAdapter;
|
||||
|
||||
TGModernGalleryZoomableScrollView *_scrollView;
|
||||
UIView *_scrollContentView;
|
||||
UIView *_scrollContainerView;
|
||||
|
||||
TGPaintingWrapperView *_paintingWrapperView;
|
||||
UIView<TGPhotoDrawingView> *_drawingView;
|
||||
|
||||
UIPinchGestureRecognizer *_entityPinchGestureRecognizer;
|
||||
UIRotationGestureRecognizer *_entityRotationGestureRecognizer;
|
||||
|
||||
UIView *_entitiesOutsideContainerView;
|
||||
UIView *_entitiesWrapperView;
|
||||
UIView<TGPhotoDrawingEntitiesView> *_entitiesView;
|
||||
|
||||
UIView *_selectionContainerView;
|
||||
|
||||
TGPhotoEditorSparseView *_interfaceWrapperView;
|
||||
UIViewController<TGPhotoDrawingInterfaceController> *_interfaceController;
|
||||
|
||||
CGSize _previousSize;
|
||||
CGFloat _keyboardHeight;
|
||||
TGObserverProxy *_keyboardWillChangeFrameProxy;
|
||||
|
||||
TGPainting *_painting;
|
||||
|
||||
bool _skipEntitiesSetup;
|
||||
bool _entitiesReady;
|
||||
|
||||
TGPaintingData *_resultData;
|
||||
}
|
||||
|
||||
@property (nonatomic, weak) PGPhotoEditor *photoEditor;
|
||||
@property (nonatomic, weak) TGPhotoEditorPreviewView *previewView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TGPhotoDrawingController
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext
|
||||
{
|
||||
self = [super initWithContext:context];
|
||||
if (self != nil)
|
||||
{
|
||||
_context = context;
|
||||
_stickersContext = stickersContext;
|
||||
|
||||
CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
_drawingAdapter = [_stickersContext drawingAdapter:size];
|
||||
_interfaceController = (UIViewController<TGPhotoDrawingInterfaceController> *)_drawingAdapter.interfaceController;
|
||||
|
||||
__weak TGPhotoDrawingController *weakSelf = self;
|
||||
_interfaceController.requestDismiss = ^{
|
||||
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
strongSelf.requestDismiss();
|
||||
};
|
||||
_interfaceController.requestApply = ^{
|
||||
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
strongSelf.requestApply();
|
||||
};
|
||||
_interfaceController.getCurrentImage = ^UIImage *{
|
||||
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return nil;
|
||||
|
||||
return [strongSelf.photoEditor currentResultImage];
|
||||
};
|
||||
|
||||
self.photoEditor = photoEditor;
|
||||
self.previewView = previewView;
|
||||
|
||||
_keyboardWillChangeFrameProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
_scrollView = [[TGModernGalleryZoomableScrollView alloc] initWithFrame:self.view.bounds hasDoubleTap:false];
|
||||
if (@available(iOS 11.0, *)) {
|
||||
_scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||
}
|
||||
_scrollView.contentInset = UIEdgeInsetsZero;
|
||||
_scrollView.delegate = self;
|
||||
_scrollView.showsHorizontalScrollIndicator = false;
|
||||
_scrollView.showsVerticalScrollIndicator = false;
|
||||
[self.view addSubview:_scrollView];
|
||||
|
||||
_scrollContentView = [[UIView alloc] initWithFrame:self.view.bounds];
|
||||
[_scrollView addSubview:_scrollContentView];
|
||||
|
||||
_scrollContainerView = _drawingAdapter.contentWrapperView;
|
||||
_scrollContainerView.clipsToBounds = true;
|
||||
// [_scrollContainerView addTarget:self action:@selector(containerPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_scrollContentView addSubview:_scrollContainerView];
|
||||
|
||||
_entityPinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
|
||||
_entityPinchGestureRecognizer.delegate = self;
|
||||
[_scrollContentView addGestureRecognizer:_entityPinchGestureRecognizer];
|
||||
|
||||
_entityRotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotate:)];
|
||||
_entityRotationGestureRecognizer.delegate = self;
|
||||
[_scrollContentView addGestureRecognizer:_entityRotationGestureRecognizer];
|
||||
|
||||
__weak TGPhotoDrawingController *weakSelf = self;
|
||||
_paintingWrapperView = [[TGPaintingWrapperView alloc] init];
|
||||
_paintingWrapperView.clipsToBounds = true;
|
||||
_paintingWrapperView.shouldReceiveTouch = ^bool
|
||||
{
|
||||
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
[_scrollContainerView addSubview:_paintingWrapperView];
|
||||
|
||||
_entitiesOutsideContainerView = [[TGPhotoEditorSparseView alloc] init];
|
||||
_entitiesOutsideContainerView.clipsToBounds = true;
|
||||
[_scrollContainerView addSubview:_entitiesOutsideContainerView];
|
||||
|
||||
_entitiesWrapperView = [[TGPhotoEditorSparseView alloc] init];
|
||||
[_entitiesOutsideContainerView addSubview:_entitiesWrapperView];
|
||||
|
||||
if (_entitiesView == nil) {
|
||||
_entitiesView = (UIView<TGPhotoDrawingEntitiesView> *)[_drawingAdapter drawingEntitiesView];
|
||||
}
|
||||
if (!_skipEntitiesSetup) {
|
||||
[_entitiesWrapperView addSubview:_entitiesView];
|
||||
}
|
||||
|
||||
_selectionContainerView = [_drawingAdapter selectionContainerView];
|
||||
_selectionContainerView.clipsToBounds = false;
|
||||
[_scrollContainerView addSubview:_selectionContainerView];
|
||||
|
||||
_interfaceWrapperView = [[TGPhotoEditorSparseView alloc] initWithFrame:CGRectZero];
|
||||
[self.view addSubview:_interfaceWrapperView];
|
||||
|
||||
TGPhotoEditorPreviewView *previewView = _previewView;
|
||||
previewView.userInteractionEnabled = false;
|
||||
previewView.hidden = true;
|
||||
|
||||
[_interfaceWrapperView addSubview:_interfaceController.view];
|
||||
|
||||
if (![self _updateControllerInset:false])
|
||||
[self controllerInsetUpdated:UIEdgeInsetsZero];
|
||||
}
|
||||
|
||||
- (void)setupCanvas
|
||||
{
|
||||
__weak TGPhotoDrawingController *weakSelf = self;
|
||||
if (_drawingView == nil) {
|
||||
_drawingView = (UIView<TGPhotoDrawingView> *)_drawingAdapter.drawingView;
|
||||
_drawingView.zoomOut = ^{
|
||||
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
[strongSelf->_scrollView setZoomScale:strongSelf->_scrollView.normalZoomScale animated:true];
|
||||
};
|
||||
[_paintingWrapperView addSubview:_drawingView];
|
||||
}
|
||||
|
||||
_entitiesView.hasSelectionChanged = ^(bool hasSelection) {
|
||||
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
strongSelf->_scrollView.pinchGestureRecognizer.enabled = !hasSelection;
|
||||
};
|
||||
|
||||
[self.view setNeedsLayout];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
// PGPhotoEditor *photoEditor = _photoEditor;
|
||||
// if (!_skipEntitiesSetup) {
|
||||
// [_entitiesView setupWithPaintingData:photoEditor.paintingData];
|
||||
// }
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
[self transitionIn];
|
||||
}
|
||||
|
||||
- (void)containerPressed {
|
||||
[_entitiesView clearSelection];
|
||||
}
|
||||
|
||||
- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
[_entitiesView handlePinch:gestureRecognizer];
|
||||
}
|
||||
|
||||
- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
[_entitiesView handleRotate:gestureRecognizer];
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)__unused gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer == _entityPinchGestureRecognizer && !_entitiesView.hasSelection) {
|
||||
return false;
|
||||
}
|
||||
return !_drawingView.isTracking;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#pragma mark - Tab Bar
|
||||
|
||||
- (TGPhotoEditorTab)availableTabs
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (TGPhotoEditorTab)activeTab
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma mark - Undo & Redo
|
||||
|
||||
|
||||
- (TGPaintingData *)_prepareResultData
|
||||
{
|
||||
TGPaintingData *resultData = _resultData;
|
||||
if (_resultData == nil) {
|
||||
resultData = [_interfaceController generateResultData];
|
||||
_resultData = resultData;
|
||||
}
|
||||
return resultData;
|
||||
}
|
||||
|
||||
- (UIImage *)image
|
||||
{
|
||||
TGPaintingData *paintingData = [self _prepareResultData];
|
||||
return paintingData.image;
|
||||
}
|
||||
|
||||
- (TGPaintingData *)paintingData
|
||||
{
|
||||
return [self _prepareResultData];
|
||||
}
|
||||
|
||||
#pragma mark - Scroll View
|
||||
|
||||
- (CGSize)fittedContentSize
|
||||
{
|
||||
return [TGPhotoPaintController fittedContentSize:_photoEditor.cropRect orientation:_photoEditor.cropOrientation originalSize:_photoEditor.originalSize];
|
||||
}
|
||||
|
||||
+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize {
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
CGFloat scale = fittedOriginalSize.width / originalSize.width;
|
||||
|
||||
CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale);
|
||||
if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight)
|
||||
size = CGSizeMake(size.height, size.width);
|
||||
|
||||
return CGSizeMake(floor(size.width), floor(size.height));
|
||||
}
|
||||
|
||||
- (CGRect)fittedCropRect:(bool)originalSize
|
||||
{
|
||||
return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:originalSize];
|
||||
}
|
||||
|
||||
+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)keepOriginalSize {
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
CGFloat scale = fittedOriginalSize.width / originalSize.width;
|
||||
|
||||
CGSize size = fittedOriginalSize;
|
||||
if (!keepOriginalSize)
|
||||
size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale);
|
||||
|
||||
return CGRectMake(-cropRect.origin.x * scale, -cropRect.origin.y * scale, size.width, size.height);
|
||||
}
|
||||
|
||||
- (CGPoint)fittedCropCenterScale:(CGFloat)scale
|
||||
{
|
||||
return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect centerScale:scale];
|
||||
}
|
||||
|
||||
+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale
|
||||
{
|
||||
CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale);
|
||||
CGRect rect = CGRectMake(cropRect.origin.x * scale, cropRect.origin.y * scale, size.width, size.height);
|
||||
|
||||
return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
|
||||
}
|
||||
|
||||
- (void)resetScrollView
|
||||
{
|
||||
CGSize fittedContentSize = [self fittedContentSize];
|
||||
CGRect fittedCropRect = [self fittedCropRect:false];
|
||||
_entitiesWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height);
|
||||
|
||||
CGFloat scale = _entitiesOutsideContainerView.bounds.size.width / fittedCropRect.size.width;
|
||||
_entitiesWrapperView.transform = CGAffineTransformMakeScale(scale, scale);
|
||||
_entitiesWrapperView.frame = CGRectMake(0.0f, 0.0f, _entitiesOutsideContainerView.bounds.size.width, _entitiesOutsideContainerView.bounds.size.height);
|
||||
|
||||
CGSize contentSize = [self contentSize];
|
||||
_scrollView.minimumZoomScale = 1.0f;
|
||||
_scrollView.maximumZoomScale = 1.0f;
|
||||
_scrollView.normalZoomScale = 1.0f;
|
||||
_scrollView.zoomScale = 1.0f;
|
||||
_scrollView.contentSize = contentSize;
|
||||
[self contentView].frame = CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height);
|
||||
|
||||
[self adjustZoom];
|
||||
_scrollView.zoomScale = _scrollView.normalZoomScale;
|
||||
}
|
||||
|
||||
- (void)scrollViewWillBeginZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view
|
||||
{
|
||||
}
|
||||
|
||||
- (void)scrollViewDidZoom:(UIScrollView *)__unused scrollView
|
||||
{
|
||||
[self adjustZoom];
|
||||
[_entitiesView onZoom];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidEndZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view atScale:(CGFloat)__unused scale
|
||||
{
|
||||
[self adjustZoom];
|
||||
|
||||
if (_scrollView.zoomScale < _scrollView.normalZoomScale - FLT_EPSILON)
|
||||
{
|
||||
[TGHacks setAnimationDurationFactor:0.5f];
|
||||
[_scrollView setZoomScale:_scrollView.normalZoomScale animated:true];
|
||||
[TGHacks setAnimationDurationFactor:1.0f];
|
||||
}
|
||||
}
|
||||
|
||||
- (UIView *)contentView
|
||||
{
|
||||
return _scrollContentView;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize
|
||||
{
|
||||
return _scrollView.frame.size;
|
||||
}
|
||||
|
||||
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)__unused scrollView
|
||||
{
|
||||
return [self contentView];
|
||||
}
|
||||
|
||||
- (void)adjustZoom
|
||||
{
|
||||
CGSize contentSize = [self contentSize];
|
||||
CGSize boundsSize = _scrollView.frame.size;
|
||||
if (contentSize.width < FLT_EPSILON || contentSize.height < FLT_EPSILON || boundsSize.width < FLT_EPSILON || boundsSize.height < FLT_EPSILON)
|
||||
return;
|
||||
|
||||
CGFloat scaleWidth = boundsSize.width / contentSize.width;
|
||||
CGFloat scaleHeight = boundsSize.height / contentSize.height;
|
||||
CGFloat minScale = MIN(scaleWidth, scaleHeight);
|
||||
CGFloat maxScale = MAX(scaleWidth, scaleHeight);
|
||||
maxScale = MAX(maxScale, minScale * 3.0f);
|
||||
|
||||
if (ABS(maxScale - minScale) < 0.01f)
|
||||
maxScale = minScale;
|
||||
|
||||
_scrollView.contentInset = UIEdgeInsetsZero;
|
||||
|
||||
if (_scrollView.minimumZoomScale != 0.05f)
|
||||
_scrollView.minimumZoomScale = 0.05f;
|
||||
if (_scrollView.normalZoomScale != minScale)
|
||||
_scrollView.normalZoomScale = minScale;
|
||||
if (_scrollView.maximumZoomScale != maxScale)
|
||||
_scrollView.maximumZoomScale = maxScale;
|
||||
|
||||
CGRect contentFrame = [self contentView].frame;
|
||||
|
||||
if (boundsSize.width > contentFrame.size.width)
|
||||
contentFrame.origin.x = (boundsSize.width - contentFrame.size.width) / 2.0f;
|
||||
else
|
||||
contentFrame.origin.x = 0;
|
||||
|
||||
if (boundsSize.height > contentFrame.size.height)
|
||||
contentFrame.origin.y = (boundsSize.height - contentFrame.size.height) / 2.0f;
|
||||
else
|
||||
contentFrame.origin.y = 0;
|
||||
|
||||
[self contentView].frame = contentFrame;
|
||||
|
||||
_scrollView.scrollEnabled = ABS(_scrollView.zoomScale - _scrollView.normalZoomScale) > FLT_EPSILON;
|
||||
|
||||
[_drawingView updateZoomScale:_scrollView.zoomScale];
|
||||
}
|
||||
|
||||
#pragma mark - Transitions
|
||||
|
||||
- (void)transitionIn
|
||||
{
|
||||
// _portraitSettingsView.layer.shouldRasterize = true;
|
||||
// _landscapeSettingsView.layer.shouldRasterize = true;
|
||||
//
|
||||
// [UIView animateWithDuration:0.3f animations:^
|
||||
// {
|
||||
// _portraitToolsWrapperView.alpha = 1.0f;
|
||||
// _landscapeToolsWrapperView.alpha = 1.0f;
|
||||
//
|
||||
// _portraitActionsView.alpha = 1.0f;
|
||||
// _landscapeActionsView.alpha = 1.0f;
|
||||
// } completion:^(__unused BOOL finished)
|
||||
// {
|
||||
// _portraitSettingsView.layer.shouldRasterize = false;
|
||||
// _landscapeSettingsView.layer.shouldRasterize = false;
|
||||
// }];
|
||||
|
||||
if (self.presentedForAvatarCreation) {
|
||||
_drawingView.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
+ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation
|
||||
{
|
||||
CGRect frame = [TGPhotoEditorTabController photoContainerFrameForParentViewFrame:parentViewFrame toolbarLandscapeSize:toolbarLandscapeSize orientation:orientation panelSize:panelSize hasOnScreenNavigation:hasOnScreenNavigation];
|
||||
|
||||
switch (orientation)
|
||||
{
|
||||
case UIInterfaceOrientationLandscapeLeft:
|
||||
frame.origin.x -= TGPhotoPaintTopPanelSize;
|
||||
break;
|
||||
|
||||
case UIInterfaceOrientationLandscapeRight:
|
||||
frame.origin.x += TGPhotoPaintTopPanelSize;
|
||||
break;
|
||||
|
||||
default:
|
||||
frame.origin.y += TGPhotoPaintTopPanelSize;
|
||||
break;
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (CGRect)_targetFrameForTransitionInFromFrame:(CGRect)fromFrame
|
||||
{
|
||||
CGSize referenceSize = [self referenceViewSize];
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(fromFrame.size, containerFrame.size);
|
||||
CGRect toFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
|
||||
return toFrame;
|
||||
}
|
||||
|
||||
- (void)_finishedTransitionInWithView:(UIView *)transitionView
|
||||
{
|
||||
if ([transitionView isKindOfClass:[TGPhotoEditorPreviewView class]]) {
|
||||
|
||||
} else {
|
||||
[transitionView removeFromSuperview];
|
||||
}
|
||||
|
||||
[self setupCanvas];
|
||||
_entitiesView.hidden = false;
|
||||
|
||||
TGPhotoEditorPreviewView *previewView = _previewView;
|
||||
[previewView setPaintingHidden:true];
|
||||
previewView.hidden = false;
|
||||
[_scrollContainerView insertSubview:previewView belowSubview:_paintingWrapperView];
|
||||
[self updateContentViewLayout];
|
||||
[previewView performTransitionInIfNeeded];
|
||||
|
||||
CGRect rect = [self fittedCropRect:true];
|
||||
_entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
||||
_entitiesView.transform = CGAffineTransformMakeRotation(_photoEditor.cropRotation);
|
||||
|
||||
CGSize fittedOriginalSize = TGScaleToSize(_photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, _photoEditor.cropRotation);
|
||||
CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
|
||||
|
||||
CGFloat scale = fittedOriginalSize.width / _photoEditor.originalSize.width;
|
||||
CGPoint offset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterScale:scale]);
|
||||
|
||||
CGPoint boundsCenter = TGPaintCenterOfRect(_entitiesWrapperView.bounds);
|
||||
_entitiesView.center = TGPaintAddPoints(boundsCenter, offset);
|
||||
|
||||
if (!_skipEntitiesSetup || _entitiesReady) {
|
||||
[_entitiesWrapperView addSubview:_entitiesView];
|
||||
}
|
||||
_entitiesReady = true;
|
||||
[self resetScrollView];
|
||||
}
|
||||
|
||||
- (void)prepareForCustomTransitionOut
|
||||
{
|
||||
_previewView.hidden = true;
|
||||
_drawingView.hidden = true;
|
||||
_entitiesOutsideContainerView.hidden = true;
|
||||
}
|
||||
|
||||
- (void)transitionOutSwitching:(bool)__unused switching completion:(void (^)(void))completion
|
||||
{
|
||||
TGPhotoEditorPreviewView *previewView = self.previewView;
|
||||
previewView.interactionEnded = nil;
|
||||
|
||||
[_interfaceController animateOut:^{
|
||||
completion();
|
||||
}];
|
||||
}
|
||||
|
||||
- (CGRect)transitionOutSourceFrameForReferenceFrame:(CGRect)referenceFrame orientation:(UIInterfaceOrientation)orientation
|
||||
{
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(referenceFrame.size, containerFrame.size);
|
||||
return CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
}
|
||||
|
||||
- (void)_animatePreviewViewTransitionOutToFrame:(CGRect)targetFrame saving:(bool)saving parentView:(UIView *)parentView completion:(void (^)(void))completion
|
||||
{
|
||||
_dismissing = true;
|
||||
|
||||
// [_entitySelectionView removeFromSuperview];
|
||||
// _entitySelectionView = nil;
|
||||
|
||||
TGPhotoEditorPreviewView *previewView = self.previewView;
|
||||
[previewView prepareForTransitionOut];
|
||||
|
||||
UIInterfaceOrientation orientation = self.effectiveOrientation;
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
CGRect referenceFrame = CGRectMake(0, 0, self.photoEditor.rotatedCropSize.width, self.photoEditor.rotatedCropSize.height);
|
||||
CGRect rect = CGRectOffset([self transitionOutSourceFrameForReferenceFrame:referenceFrame orientation:orientation], -containerFrame.origin.x, -containerFrame.origin.y);
|
||||
previewView.frame = rect;
|
||||
|
||||
UIView *snapshotView = nil;
|
||||
POPSpringAnimation *snapshotAnimation = nil;
|
||||
NSMutableArray *animations = [[NSMutableArray alloc] init];
|
||||
|
||||
if (saving && CGRectIsNull(targetFrame) && parentView != nil)
|
||||
{
|
||||
snapshotView = [previewView snapshotViewAfterScreenUpdates:false];
|
||||
snapshotView.frame = [_scrollContainerView convertRect:previewView.frame toView:parentView];
|
||||
|
||||
UIView *canvasSnapshotView = [_paintingWrapperView resizableSnapshotViewFromRect:[_paintingWrapperView convertRect:previewView.bounds fromView:previewView] afterScreenUpdates:false withCapInsets:UIEdgeInsetsZero];
|
||||
canvasSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
canvasSnapshotView.transform = _entitiesOutsideContainerView.transform;
|
||||
canvasSnapshotView.frame = snapshotView.bounds;
|
||||
[snapshotView addSubview:canvasSnapshotView];
|
||||
|
||||
UIView *entitiesSnapshotView = [_entitiesWrapperView resizableSnapshotViewFromRect:[_entitiesWrapperView convertRect:previewView.bounds fromView:previewView] afterScreenUpdates:false withCapInsets:UIEdgeInsetsZero];
|
||||
entitiesSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
entitiesSnapshotView.transform = _entitiesOutsideContainerView.transform;
|
||||
entitiesSnapshotView.frame = snapshotView.bounds;
|
||||
[snapshotView addSubview:entitiesSnapshotView];
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(previewView.frame.size, self.view.frame.size);
|
||||
targetFrame = CGRectMake((self.view.frame.size.width - fittedSize.width) / 2, (self.view.frame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
|
||||
[parentView addSubview:snapshotView];
|
||||
|
||||
snapshotAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame];
|
||||
snapshotAnimation.fromValue = [NSValue valueWithCGRect:snapshotView.frame];
|
||||
snapshotAnimation.toValue = [NSValue valueWithCGRect:targetFrame];
|
||||
[animations addObject:snapshotAnimation];
|
||||
}
|
||||
|
||||
targetFrame = CGRectOffset(targetFrame, -containerFrame.origin.x, -containerFrame.origin.y);
|
||||
CGPoint targetCenter = TGPaintCenterOfRect(targetFrame);
|
||||
|
||||
POPSpringAnimation *previewAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame];
|
||||
previewAnimation.fromValue = [NSValue valueWithCGRect:previewView.frame];
|
||||
previewAnimation.toValue = [NSValue valueWithCGRect:targetFrame];
|
||||
[animations addObject:previewAnimation];
|
||||
|
||||
POPSpringAnimation *previewAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha];
|
||||
previewAlphaAnimation.fromValue = @(previewView.alpha);
|
||||
previewAlphaAnimation.toValue = @(0.0f);
|
||||
[animations addObject:previewAnimation];
|
||||
|
||||
POPSpringAnimation *entitiesAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewCenter];
|
||||
entitiesAnimation.fromValue = [NSValue valueWithCGPoint:_entitiesOutsideContainerView.center];
|
||||
entitiesAnimation.toValue = [NSValue valueWithCGPoint:targetCenter];
|
||||
[animations addObject:entitiesAnimation];
|
||||
|
||||
CGFloat targetEntitiesScale = targetFrame.size.width / _entitiesOutsideContainerView.frame.size.width;
|
||||
POPSpringAnimation *entitiesScaleAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewScaleXY];
|
||||
entitiesScaleAnimation.fromValue = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)];
|
||||
entitiesScaleAnimation.toValue = [NSValue valueWithCGSize:CGSizeMake(targetEntitiesScale, targetEntitiesScale)];
|
||||
[animations addObject:entitiesScaleAnimation];
|
||||
|
||||
POPSpringAnimation *entitiesAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha];
|
||||
entitiesAlphaAnimation.fromValue = @(_drawingView.alpha);
|
||||
entitiesAlphaAnimation.toValue = @(0.0f);
|
||||
[animations addObject:entitiesAlphaAnimation];
|
||||
|
||||
POPSpringAnimation *paintingAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewCenter];
|
||||
paintingAnimation.fromValue = [NSValue valueWithCGPoint:_paintingWrapperView.center];
|
||||
paintingAnimation.toValue = [NSValue valueWithCGPoint:targetCenter];
|
||||
[animations addObject:paintingAnimation];
|
||||
|
||||
CGFloat targetPaintingScale = targetFrame.size.width / _paintingWrapperView.frame.size.width;
|
||||
POPSpringAnimation *paintingScaleAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewScaleXY];
|
||||
paintingScaleAnimation.fromValue = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)];
|
||||
paintingScaleAnimation.toValue = [NSValue valueWithCGSize:CGSizeMake(targetPaintingScale, targetPaintingScale)];
|
||||
[animations addObject:paintingScaleAnimation];
|
||||
|
||||
POPSpringAnimation *paintingAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha];
|
||||
paintingAlphaAnimation.fromValue = @(_paintingWrapperView.alpha);
|
||||
paintingAlphaAnimation.toValue = @(0.0f);
|
||||
[animations addObject:paintingAlphaAnimation];
|
||||
|
||||
[TGPhotoEditorAnimation performBlock:^(__unused bool allFinished)
|
||||
{
|
||||
[snapshotView removeFromSuperview];
|
||||
|
||||
if (completion != nil)
|
||||
completion();
|
||||
} whenCompletedAllAnimations:animations];
|
||||
|
||||
if (snapshotAnimation != nil)
|
||||
[snapshotView pop_addAnimation:snapshotAnimation forKey:@"frame"];
|
||||
[previewView pop_addAnimation:previewAnimation forKey:@"frame"];
|
||||
[previewView pop_addAnimation:previewAlphaAnimation forKey:@"alpha"];
|
||||
|
||||
[_entitiesOutsideContainerView pop_addAnimation:entitiesAnimation forKey:@"frame"];
|
||||
[_entitiesOutsideContainerView pop_addAnimation:entitiesScaleAnimation forKey:@"scale"];
|
||||
[_entitiesOutsideContainerView pop_addAnimation:entitiesAlphaAnimation forKey:@"alpha"];
|
||||
|
||||
[_paintingWrapperView pop_addAnimation:paintingAnimation forKey:@"frame"];
|
||||
[_paintingWrapperView pop_addAnimation:paintingScaleAnimation forKey:@"scale"];
|
||||
[_paintingWrapperView pop_addAnimation:paintingAlphaAnimation forKey:@"alpha"];
|
||||
|
||||
if (saving)
|
||||
{
|
||||
_entitiesOutsideContainerView.hidden = true;
|
||||
_paintingWrapperView.hidden = true;
|
||||
previewView.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGRect)transitionOutReferenceFrame
|
||||
{
|
||||
TGPhotoEditorPreviewView *previewView = _previewView;
|
||||
return [previewView convertRect:previewView.bounds toView:self.view];
|
||||
}
|
||||
|
||||
- (UIView *)transitionOutReferenceView
|
||||
{
|
||||
return _previewView;
|
||||
}
|
||||
|
||||
- (UIView *)snapshotView
|
||||
{
|
||||
TGPhotoEditorPreviewView *previewView = self.previewView;
|
||||
return [previewView originalSnapshotView];
|
||||
}
|
||||
|
||||
- (id)currentResultRepresentation
|
||||
{
|
||||
return TGPaintCombineCroppedImages(self.photoEditor.currentResultImage, [self image], true, _photoEditor.originalSize, _photoEditor.cropRect, _photoEditor.cropOrientation, _photoEditor.cropRotation, false);
|
||||
}
|
||||
|
||||
#pragma mark - Layout
|
||||
|
||||
- (void)viewWillLayoutSubviews
|
||||
{
|
||||
[super viewWillLayoutSubviews];
|
||||
|
||||
[self updateLayout:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation]];
|
||||
}
|
||||
|
||||
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
|
||||
{
|
||||
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
|
||||
|
||||
[self updateLayout:toInterfaceOrientation];
|
||||
}
|
||||
|
||||
- (void)updateContentViewLayout
|
||||
{
|
||||
CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(_photoEditor.cropOrientation));
|
||||
_entitiesOutsideContainerView.transform = rotationTransform;
|
||||
_entitiesOutsideContainerView.frame = self.previewView.frame;
|
||||
[self resetScrollView];
|
||||
}
|
||||
|
||||
- (void)keyboardWillChangeFrame:(NSNotification *)notification
|
||||
{
|
||||
UIView *parentView = self.view;
|
||||
|
||||
NSTimeInterval duration = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] == nil ? 0.3 : [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||
int curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue];
|
||||
CGRect screenKeyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
CGRect keyboardFrame = [parentView convertRect:screenKeyboardFrame fromView:nil];
|
||||
|
||||
CGFloat keyboardHeight = (keyboardFrame.size.height <= FLT_EPSILON || keyboardFrame.size.width <= FLT_EPSILON) ? 0.0f : (parentView.frame.size.height - keyboardFrame.origin.y);
|
||||
keyboardHeight = MAX(keyboardHeight, 0.0f);
|
||||
|
||||
_keyboardHeight = keyboardHeight;
|
||||
|
||||
CGSize referenceSize = [self referenceViewSize];
|
||||
CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize;
|
||||
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
|
||||
CGFloat topInset = [self controllerStatusBarHeight] + 31.0;
|
||||
CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight - topInset;
|
||||
CGFloat yCenter = visibleArea / 2.0f;
|
||||
CGFloat offset = yCenter - _previewView.center.y - containerFrame.origin.y + topInset;
|
||||
CGFloat offsetHeight = _keyboardHeight > FLT_EPSILON ? offset : 0.0f;
|
||||
|
||||
[UIView animateWithDuration:duration delay:0.0 options:curve animations:^
|
||||
{
|
||||
_interfaceWrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2, _interfaceWrapperView.frame.size.width, _interfaceWrapperView.frame.size.height);
|
||||
_scrollContainerView.frame = CGRectMake(containerFrame.origin.x, containerFrame.origin.y + offsetHeight, containerFrame.size.width, containerFrame.size.height);
|
||||
} completion:nil];
|
||||
|
||||
[self updateInterfaceLayoutAnimated:true];
|
||||
}
|
||||
|
||||
- (void)updateInterfaceLayoutAnimated:(BOOL)animated {
|
||||
if (_interfaceController == nil)
|
||||
return;
|
||||
|
||||
CGSize size = [self referenceViewSize];
|
||||
_interfaceController.view.frame = CGRectMake((_interfaceWrapperView.frame.size.width - size.width) / 2.0, (_interfaceWrapperView.frame.size.height - size.height) / 2.0, size.width, size.height);
|
||||
[_interfaceController adapterContainerLayoutUpdatedSize:[self referenceViewSize]
|
||||
intrinsicInsets:_context.safeAreaInset
|
||||
safeInsets:UIEdgeInsetsMake(0.0, _context.safeAreaInset.left, 0.0, _context.safeAreaInset.right)
|
||||
statusBarHeight:[_context statusBarFrame].size.height
|
||||
inputHeight:_keyboardHeight
|
||||
animated:animated];
|
||||
|
||||
}
|
||||
|
||||
- (void)updateLayout:(UIInterfaceOrientation)orientation
|
||||
{
|
||||
if ([self inFormSheet] || [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
|
||||
{
|
||||
orientation = UIInterfaceOrientationPortrait;
|
||||
}
|
||||
|
||||
CGSize referenceSize = [self referenceViewSize];
|
||||
CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize;
|
||||
|
||||
bool sizeUpdated = false;
|
||||
if (!CGSizeEqualToSize(referenceSize, _previousSize)) {
|
||||
sizeUpdated = true;
|
||||
_previousSize = referenceSize;
|
||||
}
|
||||
|
||||
UIEdgeInsets safeAreaInset = [TGViewController safeAreaInsetForOrientation:orientation hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
UIEdgeInsets screenEdges = UIEdgeInsetsMake((screenSide - referenceSize.height) / 2, (screenSide - referenceSize.width) / 2, (screenSide + referenceSize.height) / 2, (screenSide + referenceSize.width) / 2);
|
||||
screenEdges.top += safeAreaInset.top;
|
||||
screenEdges.left += safeAreaInset.left;
|
||||
screenEdges.bottom -= safeAreaInset.bottom;
|
||||
screenEdges.right -= safeAreaInset.right;
|
||||
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
|
||||
PGPhotoEditor *photoEditor = self.photoEditor;
|
||||
TGPhotoEditorPreviewView *previewView = self.previewView;
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(photoEditor.rotatedCropSize, containerFrame.size);
|
||||
CGRect previewFrame = CGRectMake((containerFrame.size.width - fittedSize.width) / 2, (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
|
||||
CGFloat topInset = [self controllerStatusBarHeight] + 31.0;
|
||||
CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight - topInset;
|
||||
CGFloat yCenter = visibleArea / 2.0f;
|
||||
CGFloat offset = yCenter - _previewView.center.y - containerFrame.origin.y + topInset;
|
||||
CGFloat offsetHeight = _keyboardHeight > FLT_EPSILON ? offset : 0.0f;
|
||||
|
||||
_interfaceWrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2, screenSide, screenSide);
|
||||
[self updateInterfaceLayoutAnimated:false];
|
||||
|
||||
if (_dismissing || (previewView.superview != _scrollContainerView && previewView.superview != self.view))
|
||||
return;
|
||||
|
||||
if (previewView.superview == self.view)
|
||||
{
|
||||
previewFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
}
|
||||
|
||||
UIImageOrientation cropOrientation = _photoEditor.cropOrientation;
|
||||
CGRect cropRect = _photoEditor.cropRect;
|
||||
CGSize originalSize = _photoEditor.originalSize;
|
||||
CGFloat rotation = _photoEditor.cropRotation;
|
||||
|
||||
CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation));
|
||||
_entitiesOutsideContainerView.transform = rotationTransform;
|
||||
_entitiesOutsideContainerView.frame = previewFrame;
|
||||
|
||||
_scrollView.frame = self.view.bounds;
|
||||
|
||||
if (sizeUpdated) {
|
||||
[self resetScrollView];
|
||||
}
|
||||
[self adjustZoom];
|
||||
|
||||
_paintingWrapperView.transform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation));
|
||||
_paintingWrapperView.frame = previewFrame;
|
||||
|
||||
CGFloat originalWidth = TGOrientationIsSideward(cropOrientation, NULL) ? previewFrame.size.height : previewFrame.size.width;
|
||||
CGFloat ratio = originalWidth / cropRect.size.width;
|
||||
CGRect originalFrame = CGRectMake(-cropRect.origin.x * ratio, -cropRect.origin.y * ratio, originalSize.width * ratio, originalSize.height * ratio);
|
||||
|
||||
previewView.frame = previewFrame;
|
||||
|
||||
if ([self presentedForAvatarCreation]) {
|
||||
CGAffineTransform transform = CGAffineTransformMakeRotation(TGRotationForOrientation(photoEditor.cropOrientation));
|
||||
if (photoEditor.cropMirrored)
|
||||
transform = CGAffineTransformScale(transform, -1.0f, 1.0f);
|
||||
previewView.transform = transform;
|
||||
}
|
||||
|
||||
CGSize fittedOriginalSize = CGSizeMake(originalSize.width * ratio, originalSize.height * ratio);
|
||||
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, rotation);
|
||||
CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
|
||||
|
||||
CGFloat scale = fittedOriginalSize.width / _photoEditor.originalSize.width;
|
||||
CGPoint centerOffset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterScale:scale]);
|
||||
|
||||
_drawingView.transform = CGAffineTransformIdentity;
|
||||
_drawingView.frame = originalFrame;
|
||||
_drawingView.transform = CGAffineTransformMakeRotation(rotation);
|
||||
_drawingView.center = TGPaintAddPoints(TGPaintCenterOfRect(_paintingWrapperView.bounds), centerOffset);
|
||||
|
||||
_selectionContainerView.transform = CGAffineTransformRotate(rotationTransform, rotation);
|
||||
_selectionContainerView.frame = previewFrame;
|
||||
|
||||
_scrollContainerView.frame = CGRectMake(containerFrame.origin.x, containerFrame.origin.y + offsetHeight, containerFrame.size.width, containerFrame.size.height);
|
||||
}
|
||||
|
||||
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
|
||||
{
|
||||
return UIRectEdgeTop | UIRectEdgeBottom;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -39,6 +39,7 @@
|
||||
#import "TGPhotoCropController.h"
|
||||
#import "TGPhotoToolsController.h"
|
||||
#import "TGPhotoPaintController.h"
|
||||
#import "TGPhotoDrawingController.h"
|
||||
#import "TGPhotoQualityController.h"
|
||||
#import "TGPhotoAvatarPreviewController.h"
|
||||
|
||||
@@ -710,7 +711,7 @@
|
||||
startPosition = self.trimStartValue;
|
||||
|
||||
CMTime targetTime = CMTimeMakeWithSeconds(startPosition, NSEC_PER_SEC);
|
||||
[_player.currentItem seekToTime:targetTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
|
||||
[_player.currentItem seekToTime:targetTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:nil];
|
||||
|
||||
[self _setupPlaybackStartedObserver];
|
||||
|
||||
@@ -1232,7 +1233,7 @@
|
||||
[self savePaintingData];
|
||||
|
||||
bool resetTransform = false;
|
||||
if ([self presentedForAvatarCreation] && tab == TGPhotoEditorCropTab && [currentController isKindOfClass:[TGPhotoPaintController class]]) {
|
||||
if ([self presentedForAvatarCreation] && tab == TGPhotoEditorCropTab && [currentController isKindOfClass:[TGPhotoDrawingController class]]) {
|
||||
resetTransform = true;
|
||||
}
|
||||
|
||||
@@ -1560,10 +1561,26 @@
|
||||
|
||||
case TGPhotoEditorPaintTab:
|
||||
{
|
||||
TGPhotoPaintController *paintController = [[TGPhotoPaintController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView entitiesView:_fullEntitiesView];
|
||||
paintController.stickersContext = _stickersContext;
|
||||
paintController.toolbarLandscapeSize = TGPhotoEditorToolbarSize;
|
||||
paintController.controlVideoPlayback = ^(bool play) {
|
||||
[_portraitToolbarView setAllButtonsHidden:true animated:false];
|
||||
[_landscapeToolbarView setAllButtonsHidden:true animated:false];
|
||||
|
||||
[_containerView.superview bringSubviewToFront:_containerView];
|
||||
|
||||
TGPhotoDrawingController *drawingController = [[TGPhotoDrawingController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView entitiesView:_fullEntitiesView stickersContext:_stickersContext];
|
||||
drawingController.requestDismiss = ^{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
[strongSelf dismissEditor];
|
||||
};
|
||||
drawingController.requestApply = ^{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
[strongSelf applyEditor];
|
||||
};
|
||||
drawingController.toolbarLandscapeSize = TGPhotoEditorToolbarSize;
|
||||
drawingController.controlVideoPlayback = ^(bool play) {
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
@@ -1573,7 +1590,7 @@
|
||||
[strongSelf stopVideoPlayback:false];
|
||||
}
|
||||
};
|
||||
paintController.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView, bool *noTransitionView)
|
||||
drawingController.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView, bool *noTransitionView)
|
||||
{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
@@ -1585,7 +1602,7 @@
|
||||
|
||||
return transitionReferenceView;
|
||||
};
|
||||
paintController.finishedTransitionIn = ^
|
||||
drawingController.finishedTransitionIn = ^
|
||||
{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
@@ -1599,8 +1616,17 @@
|
||||
if (isInitialAppearance)
|
||||
[strongSelf startVideoPlayback:true];
|
||||
};
|
||||
drawingController.finishedTransitionOut = ^{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
controller = paintController;
|
||||
[strongSelf->_containerView.superview insertSubview:strongSelf->_containerView atIndex:2];
|
||||
[strongSelf->_portraitToolbarView setAllButtonsHidden:false animated:true];
|
||||
[strongSelf->_landscapeToolbarView setAllButtonsHidden:false animated:true];
|
||||
};
|
||||
|
||||
controller = drawingController;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1680,7 +1706,7 @@
|
||||
_currentTabController.switchingFromTab = switchingFromTab;
|
||||
_currentTabController.initialAppearance = isInitialAppearance;
|
||||
|
||||
if (![_currentTabController isKindOfClass:[TGPhotoPaintController class]])
|
||||
if (![_currentTabController isKindOfClass:[TGPhotoDrawingController class]])
|
||||
_currentTabController.availableTabs = _availableTabs;
|
||||
|
||||
if ([self presentedForAvatarCreation] && self.navigationController == nil)
|
||||
@@ -1877,57 +1903,57 @@
|
||||
};
|
||||
|
||||
TGPaintingData *paintingData = nil;
|
||||
if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]])
|
||||
paintingData = [(TGPhotoPaintController *)_currentTabController paintingData];
|
||||
if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]])
|
||||
paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData];
|
||||
|
||||
PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData];
|
||||
|
||||
if ((_initialAdjustments == nil && (![editorValues isDefaultValuesForAvatar:[self presentedForAvatarCreation]] || editorValues.cropOrientation != UIImageOrientationUp)) || (_initialAdjustments != nil && ![editorValues isEqual:_initialAdjustments]))
|
||||
{
|
||||
TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false];
|
||||
controller.dismissesByOutsideTap = true;
|
||||
controller.narrowInLandscape = true;
|
||||
__weak TGMenuSheetController *weakController = controller;
|
||||
|
||||
NSArray *items = @
|
||||
[
|
||||
[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
|
||||
{
|
||||
__strong TGMenuSheetController *strongController = weakController;
|
||||
if (strongController == nil)
|
||||
return;
|
||||
|
||||
[strongController dismissAnimated:true manual:false completion:^
|
||||
{
|
||||
// if ((_initialAdjustments == nil && (![editorValues isDefaultValuesForAvatar:[self presentedForAvatarCreation]] || editorValues.cropOrientation != UIImageOrientationUp)) || (_initialAdjustments != nil && ![editorValues isEqual:_initialAdjustments]))
|
||||
// {
|
||||
// TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false];
|
||||
// controller.dismissesByOutsideTap = true;
|
||||
// controller.narrowInLandscape = true;
|
||||
// __weak TGMenuSheetController *weakController = controller;
|
||||
//
|
||||
// NSArray *items = @
|
||||
// [
|
||||
// [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
|
||||
// {
|
||||
// __strong TGMenuSheetController *strongController = weakController;
|
||||
// if (strongController == nil)
|
||||
// return;
|
||||
//
|
||||
// [strongController dismissAnimated:true manual:false completion:^
|
||||
// {
|
||||
// dismiss();
|
||||
// }];
|
||||
// }],
|
||||
// [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^
|
||||
// {
|
||||
// __strong TGMenuSheetController *strongController = weakController;
|
||||
// if (strongController != nil)
|
||||
// [strongController dismissAnimated:true];
|
||||
// }]
|
||||
// ];
|
||||
//
|
||||
// [controller setItemViews:items];
|
||||
// controller.sourceRect = ^
|
||||
// {
|
||||
// __strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
// if (strongSelf == nil)
|
||||
// return CGRectZero;
|
||||
//
|
||||
// if (UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation))
|
||||
// return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView];
|
||||
// else
|
||||
// return [strongSelf.view convertRect:strongSelf->_landscapeToolbarView.cancelButtonFrame fromView:strongSelf->_landscapeToolbarView];
|
||||
// };
|
||||
// [controller presentInViewController:self sourceView:self.view animated:true];
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
dismiss();
|
||||
}];
|
||||
}],
|
||||
[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^
|
||||
{
|
||||
__strong TGMenuSheetController *strongController = weakController;
|
||||
if (strongController != nil)
|
||||
[strongController dismissAnimated:true];
|
||||
}]
|
||||
];
|
||||
|
||||
[controller setItemViews:items];
|
||||
controller.sourceRect = ^
|
||||
{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return CGRectZero;
|
||||
|
||||
if (UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation))
|
||||
return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView];
|
||||
else
|
||||
return [strongSelf.view convertRect:strongSelf->_landscapeToolbarView.cancelButtonFrame fromView:strongSelf->_landscapeToolbarView];
|
||||
};
|
||||
[controller presentInViewController:self sourceView:self.view animated:true];
|
||||
}
|
||||
else
|
||||
{
|
||||
dismiss();
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
- (void)doneButtonPressed
|
||||
@@ -1940,10 +1966,10 @@
|
||||
}
|
||||
|
||||
- (void)savePaintingData {
|
||||
if (![_currentTabController isKindOfClass:[TGPhotoPaintController class]])
|
||||
if (![_currentTabController isKindOfClass:[TGPhotoDrawingController class]])
|
||||
return;
|
||||
|
||||
TGPhotoPaintController *paintController = (TGPhotoPaintController *)_currentTabController;
|
||||
TGPhotoDrawingController *paintController = (TGPhotoDrawingController *)_currentTabController;
|
||||
TGPaintingData *paintingData = [paintController paintingData];
|
||||
_photoEditor.paintingData = paintingData;
|
||||
|
||||
@@ -1967,7 +1993,7 @@
|
||||
NSTimeInterval trimStartValue = 0.0;
|
||||
NSTimeInterval trimEndValue = 0.0;
|
||||
|
||||
if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]])
|
||||
if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]])
|
||||
{
|
||||
[self savePaintingData];
|
||||
}
|
||||
@@ -2244,8 +2270,8 @@
|
||||
[progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5];
|
||||
|
||||
TGPaintingData *paintingData = nil;
|
||||
if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]])
|
||||
paintingData = [(TGPhotoPaintController *)_currentTabController paintingData];
|
||||
if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]])
|
||||
paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData];
|
||||
|
||||
PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData];
|
||||
|
||||
@@ -2266,8 +2292,8 @@
|
||||
[progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5];
|
||||
|
||||
TGPaintingData *paintingData = nil;
|
||||
if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]])
|
||||
paintingData = [(TGPhotoPaintController *)_currentTabController paintingData];
|
||||
if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]])
|
||||
paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData];
|
||||
|
||||
PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData];
|
||||
|
||||
|
||||
@@ -50,3 +50,8 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation TGPhotoPaintStaticEntity
|
||||
|
||||
@end
|
||||
|
||||
@@ -28,6 +28,7 @@ swift_library(
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AttachmentUI:AttachmentUI",
|
||||
"//submodules/DrawingUI:DrawingUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -8,6 +8,7 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import YuvConversion
|
||||
import StickerResources
|
||||
import DrawingUI
|
||||
|
||||
protocol LegacyPaintEntity {
|
||||
var position: CGPoint { get }
|
||||
@@ -472,4 +473,27 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
class LegacyDrawingAdapter: NSObject, TGPhotoDrawingAdapter {
|
||||
let drawingView: TGPhotoDrawingView!
|
||||
let drawingEntitiesView: TGPhotoDrawingEntitiesView!
|
||||
let selectionContainerView: UIView
|
||||
let contentWrapperView: UIView!
|
||||
let interfaceController: TGPhotoDrawingInterfaceController!
|
||||
|
||||
init(context: AccountContext, size: CGSize) {
|
||||
let interfaceController = DrawingScreen(context: context, size: size)
|
||||
self.interfaceController = interfaceController
|
||||
self.drawingView = interfaceController.drawingView
|
||||
self.drawingEntitiesView = interfaceController.entitiesView
|
||||
self.selectionContainerView = interfaceController.selectionContainerView
|
||||
self.contentWrapperView = interfaceController.contentWrapperView
|
||||
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
public func drawingAdapter(_ size: CGSize) -> TGPhotoDrawingAdapter! {
|
||||
return LegacyDrawingAdapter(context: self.context, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ private final class AddPaymentMethodSheetComponent: CombinedComponent {
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: .white,
|
||||
backgroundColor: .color(.white),
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: {
|
||||
@@ -204,6 +204,7 @@ private final class AddPaymentMethodSheetComponent: CombinedComponent {
|
||||
SheetComponentEnvironment(
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: false,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
dismiss: { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
|
||||
@@ -1117,7 +1117,7 @@ private final class DemoSheetComponent: CombinedComponent {
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor,
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: {
|
||||
@@ -1125,6 +1125,7 @@ private final class DemoSheetComponent: CombinedComponent {
|
||||
SheetComponentEnvironment(
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
dismiss: { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
|
||||
@@ -1003,7 +1003,7 @@ private final class LimitSheetComponent: CombinedComponent {
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor,
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: {
|
||||
@@ -1011,6 +1011,7 @@ private final class LimitSheetComponent: CombinedComponent {
|
||||
SheetComponentEnvironment(
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
dismiss: { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
|
||||
@@ -53,8 +53,9 @@ public extension SegmentedControlTheme {
|
||||
}
|
||||
}
|
||||
|
||||
private func generateSelectionImage(theme: SegmentedControlTheme) -> UIImage? {
|
||||
return generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in
|
||||
private func generateSelectionImage(theme: SegmentedControlTheme, cornerRadius: CGFloat) -> UIImage? {
|
||||
let cornerRadius = cornerRadius - 1.0
|
||||
return generateImage(CGSize(width: 4.0 + cornerRadius * 2.0, height: 4.0 + cornerRadius * 2.0), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
@@ -62,8 +63,8 @@ private func generateSelectionImage(theme: SegmentedControlTheme) -> UIImage? {
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 6.0, color: theme.shadowColor.withAlphaComponent(0.12).cgColor)
|
||||
}
|
||||
context.setFillColor(theme.foregroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(x: 2.0, y: 2.0, width: 16.0, height: 16.0))
|
||||
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
|
||||
context.fillEllipse(in: CGRect(x: 2.0, y: 2.0, width: cornerRadius * 2.0, height: cornerRadius * 2.0))
|
||||
})?.stretchableImage(withLeftCapWidth: Int(2 + cornerRadius), topCapHeight: Int(2 + cornerRadius))
|
||||
}
|
||||
|
||||
public struct SegmentedControlItem: Equatable {
|
||||
@@ -119,7 +120,12 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
||||
let dividersCount = self._items.count > 2 ? self._items.count - 1 : 0
|
||||
if self.dividerNodes.count != dividersCount {
|
||||
self.dividerNodes.forEach { $0.removeFromSupernode() }
|
||||
self.dividerNodes = (0 ..< dividersCount).map { _ in ASDisplayNode() }
|
||||
self.dividerNodes = (0 ..< dividersCount).map { _ in
|
||||
let node = ASDisplayNode()
|
||||
node.backgroundColor = self.theme.dividerColor
|
||||
return node
|
||||
}
|
||||
self.dividerNodes.forEach(self.addSubnode(_:))
|
||||
}
|
||||
|
||||
if let layout = self.validLayout {
|
||||
@@ -164,7 +170,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
||||
f(true)
|
||||
}
|
||||
|
||||
public init(theme: SegmentedControlTheme, items: [SegmentedControlItem], selectedIndex: Int) {
|
||||
public init(theme: SegmentedControlTheme, items: [SegmentedControlItem], selectedIndex: Int, cornerRadius: CGFloat = 9.0) {
|
||||
self.theme = theme
|
||||
self._items = items
|
||||
self._selectedIndex = selectedIndex
|
||||
@@ -196,7 +202,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.cornerRadius = 9.0
|
||||
self.cornerRadius = cornerRadius
|
||||
|
||||
self.addSubnode(self.selectionNode)
|
||||
self.itemNodes.forEach(self.addSubnode(_:))
|
||||
@@ -204,7 +210,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
||||
self.dividerNodes.forEach(self.addSubnode(_:))
|
||||
|
||||
self.backgroundColor = self.theme.backgroundColor
|
||||
self.selectionNode.image = generateSelectionImage(theme: self.theme)
|
||||
self.selectionNode.image = generateSelectionImage(theme: self.theme, cornerRadius: cornerRadius)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
@@ -286,6 +292,26 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
||||
}
|
||||
}
|
||||
|
||||
public func animateSelection(to point: CGPoint, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||
self.isUserInteractionEnabled = false
|
||||
self.alpha = 0.0
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
||||
let selectionFrame = self.selectionNode.frame
|
||||
transition.animateFrame(node: self.selectionNode, from: self.selectionNode.frame, to: CGRect(origin: CGPoint(x: point.x - self.selectionNode.frame.height / 2.0, y: self.selectionNode.frame.minY), size: CGSize(width: self.selectionNode.frame.height, height: self.selectionNode.frame.height)))
|
||||
return selectionFrame
|
||||
}
|
||||
|
||||
public func animateSelection(from point: CGPoint, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||
self.isUserInteractionEnabled = true
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
let selectionFrame = self.selectionNode.frame
|
||||
transition.animateFrame(node: self.selectionNode, from: CGRect(origin: CGPoint(x: point.x - self.selectionNode.frame.height / 2.0, y: self.selectionNode.frame.minY), size: CGSize(width: self.selectionNode.frame.height, height: self.selectionNode.frame.height)), to: self.selectionNode.frame)
|
||||
return selectionFrame
|
||||
}
|
||||
|
||||
public func updateTheme(_ theme: SegmentedControlTheme) {
|
||||
guard theme != self.theme else {
|
||||
return
|
||||
@@ -293,7 +319,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundColor = self.theme.backgroundColor
|
||||
self.selectionNode.image = generateSelectionImage(theme: self.theme)
|
||||
self.selectionNode.image = generateSelectionImage(theme: self.theme, cornerRadius: self.cornerRadius)
|
||||
|
||||
for itemNode in self.itemNodes {
|
||||
if let title = itemNode.attributedTitle(for: .normal)?.string {
|
||||
|
||||
@@ -304,6 +304,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
|
||||
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
|
||||
"//submodules/DrawingUI:DrawingUI",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
||||
@@ -40,6 +40,7 @@ swift_library(
|
||||
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/LocalizedPeerData:LocalizedPeerData",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -23,6 +23,7 @@ import AudioToolbox
|
||||
import SolidRoundedButtonComponent
|
||||
import EmojiTextAttachmentView
|
||||
import EmojiStatusComponent
|
||||
import TelegramNotices
|
||||
|
||||
private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white)
|
||||
private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white)
|
||||
@@ -7059,6 +7060,469 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
return emojiItems
|
||||
}
|
||||
|
||||
public static func stickerInputData(
|
||||
context: AccountContext,
|
||||
animationCache: AnimationCache,
|
||||
animationRenderer: MultiAnimationRenderer,
|
||||
stickerNamespaces: [ItemCollectionId.Namespace],
|
||||
stickerOrderedItemListCollectionIds: [Int32],
|
||||
chatPeerId: EnginePeer.Id?
|
||||
) -> Signal<EmojiPagerContentComponent, NoError> {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
|
||||
struct PeerSpecificPackData: Equatable {
|
||||
var info: StickerPackCollectionInfo
|
||||
var items: [StickerPackItem]
|
||||
var peer: EnginePeer
|
||||
|
||||
static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool {
|
||||
if lhs.info.id != rhs.info.id {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
let peerSpecificPack: Signal<PeerSpecificPackData?, NoError>
|
||||
if let chatPeerId = chatPeerId {
|
||||
peerSpecificPack = combineLatest(
|
||||
context.engine.peers.peerSpecificStickerPack(peerId: chatPeerId),
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId))
|
||||
)
|
||||
|> map { packData, peer -> PeerSpecificPackData? in
|
||||
guard let peer = peer else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let (info, items) = packData.packInfo else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer)
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
} else {
|
||||
peerSpecificPack = .single(nil)
|
||||
}
|
||||
|
||||
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
|
||||
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000),
|
||||
hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false),
|
||||
context.account.viewTracker.featuredStickerPacks(),
|
||||
context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))),
|
||||
ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager),
|
||||
peerSpecificPack
|
||||
)
|
||||
|> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack -> EmojiPagerContentComponent in
|
||||
struct ItemGroup {
|
||||
var supergroupId: AnyHashable
|
||||
var id: AnyHashable
|
||||
var title: String
|
||||
var subtitle: String?
|
||||
var actionButtonTitle: String?
|
||||
var isPremiumLocked: Bool
|
||||
var isFeatured: Bool
|
||||
var displayPremiumBadges: Bool
|
||||
var headerItem: EntityKeyboardAnimationData?
|
||||
var items: [EmojiPagerContentComponent.Item]
|
||||
}
|
||||
var itemGroups: [ItemGroup] = []
|
||||
var itemGroupIndexById: [AnyHashable: Int] = [:]
|
||||
|
||||
var savedStickers: OrderedItemListView?
|
||||
var recentStickers: OrderedItemListView?
|
||||
var cloudPremiumStickers: OrderedItemListView?
|
||||
for orderedView in view.orderedItemListsViews {
|
||||
if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers {
|
||||
recentStickers = orderedView
|
||||
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers {
|
||||
savedStickers = orderedView
|
||||
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudAllPremiumStickers {
|
||||
cloudPremiumStickers = orderedView
|
||||
}
|
||||
}
|
||||
|
||||
var installedCollectionIds = Set<ItemCollectionId>()
|
||||
for (id, _, _) in view.collectionInfos {
|
||||
installedCollectionIds.insert(id)
|
||||
}
|
||||
|
||||
let dismissedTrendingStickerPacksSet = Set(dismissedTrendingStickerPacks ?? [])
|
||||
let featuredStickerPacksSet = Set(featuredStickerPacks.map(\.info.id.id))
|
||||
|
||||
if dismissedTrendingStickerPacksSet != featuredStickerPacksSet {
|
||||
let featuredStickersConfiguration = featuredStickersConfiguration?.get(FeaturedStickersConfiguration.self)
|
||||
for featuredStickerPack in featuredStickerPacks {
|
||||
if installedCollectionIds.contains(featuredStickerPack.info.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let item = featuredStickerPack.topItems.first else {
|
||||
continue
|
||||
}
|
||||
|
||||
let animationData: EntityKeyboardAnimationData
|
||||
|
||||
if let thumbnail = featuredStickerPack.info.thumbnail {
|
||||
let type: EntityKeyboardAnimationData.ItemType
|
||||
if item.file.isAnimatedSticker {
|
||||
type = .lottie
|
||||
} else if item.file.isVideoEmoji || item.file.isVideoSticker {
|
||||
type = .video
|
||||
} else {
|
||||
type = .still
|
||||
}
|
||||
|
||||
animationData = EntityKeyboardAnimationData(
|
||||
id: .stickerPackThumbnail(featuredStickerPack.info.id),
|
||||
type: type,
|
||||
resource: .stickerPackThumbnail(stickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), resource: thumbnail.resource),
|
||||
dimensions: thumbnail.dimensions.cgSize,
|
||||
immediateThumbnailData: featuredStickerPack.info.immediateThumbnailData,
|
||||
isReaction: false
|
||||
)
|
||||
} else {
|
||||
animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
}
|
||||
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let supergroupId = "featuredTop"
|
||||
let groupId: AnyHashable = supergroupId
|
||||
let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium
|
||||
if isPremiumLocked && isPremiumDisabled {
|
||||
continue
|
||||
}
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
|
||||
let trendingIsPremium = featuredStickersConfiguration?.isPremium ?? false
|
||||
let title = trendingIsPremium ? strings.Stickers_TrendingPremiumStickers : strings.StickerPacksSettings_FeaturedPacks
|
||||
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let savedStickers = savedStickers {
|
||||
for item in savedStickers.items {
|
||||
guard let item = item.contents.get(SavedStickerItem.self) else {
|
||||
continue
|
||||
}
|
||||
if isPremiumDisabled && item.file.isPremiumSticker {
|
||||
continue
|
||||
}
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "saved"
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleFavoriteStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let recentStickers = recentStickers {
|
||||
for item in recentStickers.items {
|
||||
guard let item = item.contents.get(RecentMediaItem.self) else {
|
||||
continue
|
||||
}
|
||||
if isPremiumDisabled && item.media.isPremiumSticker {
|
||||
continue
|
||||
}
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: item.media)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.media,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "recent"
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Stickers_FrequentlyUsed, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var premiumStickers: [StickerPackItem] = []
|
||||
if hasPremium {
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
|
||||
if item.file.isPremiumSticker {
|
||||
premiumStickers.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
if let cloudPremiumStickers = cloudPremiumStickers, !cloudPremiumStickers.items.isEmpty {
|
||||
premiumStickers.append(contentsOf: cloudPremiumStickers.items.compactMap { item -> StickerPackItem? in guard let item = item.contents.get(RecentMediaItem.self) else {
|
||||
return nil
|
||||
}
|
||||
return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.media, indexKeys: [])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !premiumStickers.isEmpty {
|
||||
var processedIds = Set<MediaId>()
|
||||
for item in premiumStickers {
|
||||
if isPremiumDisabled && item.file.isPremiumSticker {
|
||||
continue
|
||||
}
|
||||
if processedIds.contains(item.file.fileId) {
|
||||
continue
|
||||
}
|
||||
processedIds.insert(item.file.fileId)
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "premium"
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitlePremiumStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var avatarPeer: EnginePeer?
|
||||
if let peerSpecificPack = peerSpecificPack {
|
||||
avatarPeer = peerSpecificPack.peer
|
||||
|
||||
var processedIds = Set<MediaId>()
|
||||
for item in peerSpecificPack.items {
|
||||
if isPremiumDisabled && item.file.isPremiumSticker {
|
||||
continue
|
||||
}
|
||||
if processedIds.contains(item.file.fileId) {
|
||||
continue
|
||||
}
|
||||
processedIds.insert(item.file.fileId)
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let groupId = "peerSpecific"
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
let groupId = entry.index.collectionId
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
|
||||
var title = ""
|
||||
var headerItem: EntityKeyboardAnimationData?
|
||||
inner: for (id, info, _) in view.collectionInfos {
|
||||
if id == groupId, let info = info as? StickerPackCollectionInfo {
|
||||
title = info.title
|
||||
|
||||
if let thumbnail = info.thumbnail {
|
||||
let type: EntityKeyboardAnimationData.ItemType
|
||||
if item.file.isAnimatedSticker {
|
||||
type = .lottie
|
||||
} else if item.file.isVideoEmoji || item.file.isVideoSticker {
|
||||
type = .video
|
||||
} else {
|
||||
type = .still
|
||||
}
|
||||
|
||||
headerItem = EntityKeyboardAnimationData(
|
||||
id: .stickerPackThumbnail(info.id),
|
||||
type: type,
|
||||
resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource),
|
||||
dimensions: thumbnail.dimensions.cgSize,
|
||||
immediateThumbnailData: info.immediateThumbnailData,
|
||||
isReaction: false
|
||||
)
|
||||
}
|
||||
|
||||
break inner
|
||||
}
|
||||
}
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, headerItem: headerItem, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
|
||||
for featuredStickerPack in featuredStickerPacks {
|
||||
if installedCollectionIds.contains(featuredStickerPack.info.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
for item in featuredStickerPack.topItems {
|
||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: item.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
|
||||
let supergroupId = featuredStickerPack.info.id
|
||||
let groupId: AnyHashable = supergroupId
|
||||
let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium
|
||||
if isPremiumLocked && isPremiumDisabled {
|
||||
continue
|
||||
}
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
|
||||
let subtitle: String = strings.StickerPack_StickerCount(Int32(featuredStickerPack.info.count))
|
||||
var headerItem: EntityKeyboardAnimationData?
|
||||
|
||||
if let thumbnailFileId = featuredStickerPack.info.thumbnailFileId, let file = featuredStickerPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) {
|
||||
headerItem = EntityKeyboardAnimationData(file: file.file)
|
||||
} else if let thumbnail = featuredStickerPack.info.thumbnail {
|
||||
let info = featuredStickerPack.info
|
||||
let type: EntityKeyboardAnimationData.ItemType
|
||||
if item.file.isAnimatedSticker {
|
||||
type = .lottie
|
||||
} else if item.file.isVideoEmoji || item.file.isVideoSticker {
|
||||
type = .video
|
||||
} else {
|
||||
type = .still
|
||||
}
|
||||
|
||||
headerItem = EntityKeyboardAnimationData(
|
||||
id: .stickerPackThumbnail(info.id),
|
||||
type: type,
|
||||
resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource),
|
||||
dimensions: thumbnail.dimensions.cgSize,
|
||||
immediateThumbnailData: info.immediateThumbnailData,
|
||||
isReaction: false
|
||||
)
|
||||
}
|
||||
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, headerItem: headerItem, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent(
|
||||
id: "stickers",
|
||||
context: context,
|
||||
avatarPeer: avatarPeer,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
|
||||
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
|
||||
var hasClear = false
|
||||
var isEmbedded = false
|
||||
if group.id == AnyHashable("recent") {
|
||||
hasClear = true
|
||||
} else if group.id == AnyHashable("featuredTop") {
|
||||
hasClear = true
|
||||
isEmbedded = true
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: group.supergroupId,
|
||||
groupId: group.id,
|
||||
title: group.title,
|
||||
subtitle: group.subtitle,
|
||||
actionButtonTitle: group.actionButtonTitle,
|
||||
isFeatured: group.isFeatured,
|
||||
isPremiumLocked: group.isPremiumLocked,
|
||||
isEmbedded: isEmbedded,
|
||||
hasClear: hasClear,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: group.displayPremiumBadges,
|
||||
headerItem: group.headerItem,
|
||||
items: group.items
|
||||
)
|
||||
},
|
||||
itemLayoutType: .detailed,
|
||||
itemContentUniqueId: nil,
|
||||
warpContentsOnEdges: false,
|
||||
displaySearchWithPlaceholder: nil,
|
||||
searchInitiallyHidden: false,
|
||||
searchIsPlaceholderOnly: true,
|
||||
emptySearchResults: nil,
|
||||
enableLongPress: false,
|
||||
selectedItems: Set()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor], title: String) -> UIImage? {
|
||||
|
||||
@@ -90,7 +90,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
public let isContentInFocus: Bool
|
||||
public let containerInsets: UIEdgeInsets
|
||||
public let topPanelInsets: UIEdgeInsets
|
||||
public let emojiContent: EmojiPagerContentComponent
|
||||
public let emojiContent: EmojiPagerContentComponent?
|
||||
public let stickerContent: EmojiPagerContentComponent?
|
||||
public let gifContent: GifPagerContentComponent?
|
||||
public let hasRecentGifs: Bool
|
||||
@@ -116,7 +116,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
isContentInFocus: Bool,
|
||||
containerInsets: UIEdgeInsets,
|
||||
topPanelInsets: UIEdgeInsets,
|
||||
emojiContent: EmojiPagerContentComponent,
|
||||
emojiContent: EmojiPagerContentComponent?,
|
||||
stickerContent: EmojiPagerContentComponent?,
|
||||
gifContent: GifPagerContentComponent?,
|
||||
hasRecentGifs: Bool,
|
||||
@@ -304,17 +304,18 @@ public final class EntityKeyboardComponent: Component {
|
||||
}
|
||||
))
|
||||
))
|
||||
if let emojiContent = component.emojiContent {
|
||||
for emoji in component.availableGifSearchEmojies {
|
||||
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
id: emoji.emoji,
|
||||
isReorderable: false,
|
||||
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
|
||||
context: component.emojiContent.context,
|
||||
context: emojiContent.context,
|
||||
item: EntityKeyboardAnimationData(file: emoji.file),
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
animationCache: component.emojiContent.animationCache,
|
||||
animationRenderer: component.emojiContent.animationRenderer,
|
||||
animationCache: emojiContent.animationCache,
|
||||
animationRenderer: emojiContent.animationRenderer,
|
||||
theme: component.theme,
|
||||
title: emoji.title,
|
||||
pressed: { [weak self] in
|
||||
@@ -323,6 +324,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
))
|
||||
))
|
||||
}
|
||||
}
|
||||
let defaultActiveGifItemId: AnyHashable
|
||||
switch gifContent.subject {
|
||||
case .recent:
|
||||
@@ -480,9 +482,10 @@ public final class EntityKeyboardComponent: Component {
|
||||
}
|
||||
|
||||
let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
|
||||
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent)))
|
||||
if let emojiContent = component.emojiContent {
|
||||
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent)))
|
||||
var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = []
|
||||
for itemGroup in component.emojiContent.itemGroups {
|
||||
for itemGroup in emojiContent.itemGroups {
|
||||
if !itemGroup.items.isEmpty {
|
||||
if let id = itemGroup.groupId.base as? String {
|
||||
if id == "recent" {
|
||||
@@ -529,12 +532,12 @@ public final class EntityKeyboardComponent: Component {
|
||||
id: itemGroup.supergroupId,
|
||||
isReorderable: !itemGroup.isFeatured,
|
||||
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
|
||||
context: component.emojiContent.context,
|
||||
context: emojiContent.context,
|
||||
item: itemGroup.headerItem ?? animationData,
|
||||
isFeatured: itemGroup.isFeatured,
|
||||
isPremiumLocked: itemGroup.isPremiumLocked,
|
||||
animationCache: component.emojiContent.animationCache,
|
||||
animationRenderer: component.emojiContent.animationRenderer,
|
||||
animationCache: emojiContent.animationCache,
|
||||
animationRenderer: emojiContent.animationRenderer,
|
||||
theme: component.theme,
|
||||
title: itemGroup.title ?? "",
|
||||
pressed: { [weak self] in
|
||||
@@ -574,7 +577,9 @@ public final class EntityKeyboardComponent: Component {
|
||||
component.switchToTextInput()
|
||||
}
|
||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
let deleteBackwards = component.emojiContent.inputInteractionHolder.inputInteraction?.deleteBackwards
|
||||
}
|
||||
|
||||
let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards
|
||||
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputClearIcon",
|
||||
@@ -620,7 +625,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
theme: component.theme,
|
||||
containerInsets: component.containerInsets,
|
||||
deleteBackwards: { [weak self] in
|
||||
self?.component?.emojiContent.inputInteractionHolder.inputInteraction?.deleteBackwards()
|
||||
self?.component?.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards()
|
||||
AudioServicesPlaySystemSound(0x451)
|
||||
}
|
||||
)) : nil,
|
||||
@@ -665,7 +670,8 @@ public final class EntityKeyboardComponent: Component {
|
||||
)
|
||||
transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize))
|
||||
|
||||
if let searchComponent = self.searchComponent {
|
||||
let accountContext = component.emojiContent?.context ?? component.stickerContent?.context
|
||||
if let searchComponent = self.searchComponent, let accountContext = accountContext {
|
||||
var animateIn = false
|
||||
let searchView: ComponentHostView<EntitySearchContentEnvironment>
|
||||
var searchViewTransition = transition
|
||||
@@ -686,7 +692,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
component: AnyComponent(searchComponent),
|
||||
environment: {
|
||||
EntitySearchContentEnvironment(
|
||||
context: component.emojiContent.context,
|
||||
context: accountContext,
|
||||
theme: component.theme,
|
||||
deviceMetrics: component.deviceMetrics,
|
||||
inputHeight: component.inputHeight
|
||||
|
||||
21
submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "add.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/add.png
vendored
Normal file
|
After Width: | Height: | Size: 245 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/BrushArrow.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "arrowTip.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/BrushArrow.imageset/arrowTip.png
vendored
Normal file
|
After Width: | Height: | Size: 361 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/BrushBlur.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "blurTip.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/BrushBlur.imageset/blurTip.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
21
submodules/TelegramUI/Images.xcassets/Media Editor/BrushRemove.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "xmarkTip.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/BrushRemove.imageset/xmarkTip.png
vendored
Normal file
|
After Width: | Height: | Size: 400 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/BrushRound.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "roundTip.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/BrushRound.imageset/roundTip.png
vendored
Normal file
|
After Width: | Height: | Size: 428 B |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
12
submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_editor_check (2).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/ic_editor_check (2).pdf
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Media Editor/EraserRemove.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "EraserRemove.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/EraserRemove.imageset/EraserRemove.png
vendored
Normal file
|
After Width: | Height: | Size: 776 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "eyedropper.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/eyedropper.png
vendored
Normal file
|
After Width: | Height: | Size: 581 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shapeFill.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/shapeFill.png
vendored
Normal file
|
After Width: | Height: | Size: 504 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shapeFlip.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/shapeFlip.png
vendored
Normal file
|
After Width: | Height: | Size: 714 B |
12
submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Pencil.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Pencil.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
21
submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "undo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
21
submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Round.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Round.png
vendored
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shapeArrow.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/shapeArrow.png
vendored
Normal file
|
After Width: | Height: | Size: 293 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shapeBubble.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/shapeBubble.png
vendored
Normal file
|
After Width: | Height: | Size: 475 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shapeEllipse.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/shapeEllipse.png
vendored
Normal file
|
After Width: | Height: | Size: 556 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shapeRectangle.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/shapeRectangle.png
vendored
Normal file
|
After Width: | Height: | Size: 300 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shapeStar.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/shapeStar.png
vendored
Normal file
|
After Width: | Height: | Size: 626 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "spectrum.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/spectrum.png
vendored
Normal file
|
After Width: | Height: | Size: 336 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shapeStroke.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/shapeStroke.png
vendored
Normal file
|
After Width: | Height: | Size: 643 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "default.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/default.png
vendored
Normal file
|
After Width: | Height: | Size: 463 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "filled.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/filled.png
vendored
Normal file
|
After Width: | Height: | Size: 859 B |
21
submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "semi.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||