This commit is contained in:
Ilya Laktyushin
2022-12-03 21:57:32 +04:00
parent 9c1c901a97
commit 02e06779ef
135 changed files with 19521 additions and 711 deletions

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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

View File

@@ -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 {

View File

@@ -13,6 +13,7 @@ swift_library(
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
],
visibility = [
"//visibility:public",

View File

@@ -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) {

View File

@@ -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()),

View 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",
],
)

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View 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]]}

View 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]]}

View 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]]}

View 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]]}

File diff suppressed because it is too large Load Diff

View 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)
}

View 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)
}
}

View 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
}
}

File diff suppressed because it is too large Load Diff

View 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)
}
}

File diff suppressed because it is too large Load Diff

View 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)
}
}

View 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
}
}

View 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
}
}

File diff suppressed because it is too large Load Diff

View 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)
}
}

View 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)
}
}

View 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()
}
}

View 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
}
}
}

View 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)
}
}

View 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()
}
}
}

View 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))
}
}

View 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)
}
}

View 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)
}
}
}

View 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)
}

View File

@@ -4,6 +4,12 @@
@protocol TGMediaEditAdjustments;
@protocol TGPhotoEditorTabProtocol
@end
@interface TGPhotoEditorTabController : TGViewController
{
bool _dismissing;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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];

View File

@@ -50,3 +50,8 @@
}
@end
@implementation TGPhotoPaintStaticEntity
@end

View File

@@ -28,6 +28,7 @@ swift_library(
"//submodules/StickerResources:StickerResources",
"//submodules/TextFormat:TextFormat",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/DrawingUI:DrawingUI",
],
visibility = [
"//visibility:public",

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -40,6 +40,7 @@ swift_library(
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/TelegramNotices:TelegramNotices",
],
visibility = [
"//visibility:public",

View File

@@ -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? {

View File

@@ -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

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_editor_check (2).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Pencil.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

View 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
}
}

Some files were not shown because too many files have changed in this diff Show More