Various fixes

This commit is contained in:
Ilya Laktyushin 2022-12-22 16:42:13 +04:00
parent 36918ecaf9
commit 1798554940
47 changed files with 1389 additions and 942 deletions

View File

@ -8518,6 +8518,7 @@ Sorry for the inconvenience.";
"Privacy.ProfilePhoto.CustomOverrideInfo" = "You can add users or entire groups which will not see your profile photo.";
"Privacy.ProfilePhoto.CustomOverrideAddInfo" = "Add users or entire groups which will still see your profile photo.";
"Privacy.ProfilePhoto.CustomOverrideBothInfo" = "You can add users or entire groups as exceptions that will override the settings above.";
"WebApp.AddToAttachmentAllowMessages" = "Allow **%@** to send me messages";

View File

@ -127,6 +127,12 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
self.iconDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.view.isExclusiveTouch = true
}
@objc private func pressed() {
guard let controller = self.getController() else {
return

View File

@ -91,6 +91,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/FastBlur:FastBlur",
],
visibility = [
"//visibility:public",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -277,7 +277,7 @@ private class ColorSliderComponent: Component {
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))))
if !self.transparencyLayer.isHidden {
self.transparencyLayer.contents = generateCheckeredImage(size: sliderSize, whiteColor: UIColor(rgb: 0xffffff, alpha: 0.1), blackColor: .clear, length: 12.0)?.cgImage
self.transparencyLayer.contents = generateCheckeredImage(size: sliderSize, whiteColor: UIColor(rgb: 0xffffff, alpha: 1.0), blackColor: .clear, length: 12.0)?.cgImage
}
self.knob.contents = generateKnobImage()?.cgImage
@ -437,6 +437,10 @@ private class ColorFieldComponent: Component {
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.selectAll(nil)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return false
}
@ -1247,13 +1251,14 @@ private final class ColorSlidersComponent: CombinedComponent {
type: .number,
value: "\(Int(component.color.red * 255.0))",
updated: { value in
if let intValue = Int(value) {
updateColor(currentColor.withUpdatedRed(CGFloat(intValue) / 255.0))
}
let intValue = Int(value) ?? 0
updateColor(currentColor.withUpdatedRed(CGFloat(intValue) / 255.0))
},
shouldUpdate: { value in
if let intValue = Int(value), intValue >= 0 && intValue <= 255 {
return true
} else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return true
} else {
return false
}
@ -1313,13 +1318,14 @@ private final class ColorSlidersComponent: CombinedComponent {
type: .number,
value: "\(Int(component.color.green * 255.0))",
updated: { value in
if let intValue = Int(value) {
updateColor(currentColor.withUpdatedGreen(CGFloat(intValue) / 255.0))
}
let intValue = Int(value) ?? 0
updateColor(currentColor.withUpdatedGreen(CGFloat(intValue) / 255.0))
},
shouldUpdate: { value in
if let intValue = Int(value), intValue >= 0 && intValue <= 255 {
return true
} else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return true
} else {
return false
}
@ -1379,13 +1385,14 @@ private final class ColorSlidersComponent: CombinedComponent {
type: .number,
value: "\(Int(component.color.blue * 255.0))",
updated: { value in
if let intValue = Int(value) {
updateColor(currentColor.withUpdatedBlue(CGFloat(intValue) / 255.0))
}
let intValue = Int(value) ?? 0
updateColor(currentColor.withUpdatedBlue(CGFloat(intValue) / 255.0))
},
shouldUpdate: { value in
if let intValue = Int(value), intValue >= 0 && intValue <= 255 {
return true
} else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return true
} else {
return false
}
@ -2198,8 +2205,19 @@ private final class ColorPickerContent: CombinedComponent {
type: .number,
value: "\(Int(state.selectedColor.alpha * 100.0))",
suffix: "%",
updated: { _ in },
shouldUpdate: { _ in return true }
updated: { value in
let intValue = Int(value) ?? 0
state.updateAlpha(CGFloat(intValue) / 100.0)
},
shouldUpdate: { value in
if let intValue = Int(value), intValue >= 0 && intValue <= 100 {
return true
} else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return true
} else {
return false
}
}
),
availableSize: CGSize(width: 77.0, height: 36.0),
transition: .immediate

View File

@ -39,6 +39,8 @@ public final class DrawingBubbleEntity: DrawingEntity, Codable {
return self.position
}
public var scale: CGFloat = 1.0
public var renderImage: UIImage?
init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
@ -174,6 +176,11 @@ final class DrawingBubbleEntityView: DrawingEntityView {
return max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1)
}
fileprivate var minimumSize: CGSize {
let minSize = min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height)
return CGSize(width: minSize * 0.1, height: minSize * 0.1)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let lineWidth = self.maxLineWidth * 0.5
let expandedBounds = self.bounds.insetBy(dx: -lineWidth, dy: -lineWidth)
@ -285,7 +292,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
self.snapTool.onSnapYUpdated = { [weak self] snapped in
if let strongSelf = self, let entityView = strongSelf.entityView {
entityView.onSnapToXAxis(snapped)
entityView.onSnapToYAxis(snapped)
}
}
}
@ -312,7 +319,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
private var currentHandle: CALayer?
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
guard let entityView = self.entityView as? DrawingBubbleEntityView, let entity = entityView.entity as? DrawingBubbleEntity else {
return
}
let location = gestureRecognizer.location(in: self)
@ -338,37 +345,39 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
var updatedPosition = entity.position
var updatedTailPosition = entity.tailPosition
let minimumSize = entityView.minimumSize
if self.currentHandle === self.leftHandle {
updatedSize.width -= delta.x
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
} else if self.currentHandle === self.rightHandle {
updatedSize.width += delta.x
updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
} else if self.currentHandle === self.topHandle {
updatedSize.height -= delta.y
updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.bottomHandle {
updatedSize.height += delta.y
updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.topLeftHandle {
updatedSize.width -= delta.x
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
updatedSize.height -= delta.y
updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.topRightHandle {
updatedSize.width += delta.x
updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
updatedSize.height -= delta.y
updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.bottomLeftHandle {
updatedSize.width -= delta.x
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
updatedSize.height += delta.y
updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.bottomRightHandle {
updatedSize.width += delta.x
updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
updatedSize.height += delta.y
updatedSize.height = max(minimumSize.height, 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)))
@ -382,7 +391,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
entity.size = updatedSize
entity.position = updatedPosition
entity.tailPosition = updatedTailPosition
entityView.update()
entityView.update(animated: false)
gestureRecognizer.setTranslation(.zero, in: entityView)
case .ended:

View File

@ -4,6 +4,7 @@ import Display
import LegacyComponents
import AccountContext
public protocol DrawingEntity: AnyObject {
var uuid: UUID { get }
var isAnimated: Bool { get }
@ -11,7 +12,9 @@ public protocol DrawingEntity: AnyObject {
var lineWidth: CGFloat { get set }
var color: DrawingColor { get set }
var scale: CGFloat { get set }
func duplicate() -> DrawingEntity
var currentEntityView: DrawingEntityView? { get }
@ -123,6 +126,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let context: AccountContext
private let size: CGSize
weak var drawingView: DrawingView?
weak var selectionContainerView: DrawingSelectionContainerView?
private var tapGestureRecognizer: UITapGestureRecognizer!
@ -167,6 +171,10 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
fatalError("init(coder:) has not been implemented")
}
deinit {
print()
}
public override func layoutSubviews() {
super.layoutSubviews()
@ -188,9 +196,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
return entities
}
private var initialEntitiesData: Data?
public func setup(withEntitiesData entitiesData: Data!) {
self.clear()
self.initialEntitiesData = entitiesData
if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) {
let entities = codableEntities.map { $0.entity }
for entity in entities {
@ -215,6 +226,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
}
}
var hasChanges: Bool {
if let initialEntitiesData = self.initialEntitiesData {
let entitiesData = self.entitiesData
return entitiesData != initialEntitiesData
} else {
return !self.entities.isEmpty
}
}
private func startPosition(relativeTo entity: DrawingEntity?) -> CGPoint {
let offsetLength = round(self.size.width * 0.1)
let offset = CGPoint(x: offsetLength, y: offsetLength)
@ -244,14 +264,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
}
private func newEntitySize() -> CGSize {
let width = round(self.size.width * 0.5)
let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0)
let width = round(self.size.width * 0.5) * zoomScale
return CGSize(width: width, height: width)
}
func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
let center = self.startPosition(relativeTo: relativeTo)
let rotation = self.getEntityInitialRotation()
let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0)
if let shape = entity as? DrawingSimpleShapeEntity {
shape.position = center
@ -280,7 +301,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
sticker.rotation = rotation
if setup {
sticker.referenceDrawingSize = self.size
sticker.scale = 1.0
sticker.scale = zoomScale
}
} else if let bubble = entity as? DrawingBubbleEntity {
bubble.position = center
@ -298,6 +319,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
text.referenceDrawingSize = self.size
text.width = floor(self.size.width * 0.9)
text.fontSize = 0.3
text.scale = zoomScale
}
}
}
@ -368,7 +390,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
view?.removeFromSuperview()
})
if !(view.entity is DrawingVectorEntity) {
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
view.layer.animateScale(from: view.entity.scale, to: 0.1, duration: 0.2, removeOnCompletion: false)
}
if let selectionView = view.selectionView {
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] _ in

View File

@ -255,7 +255,22 @@ class DrawingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate
}
class DrawingGesturePipeline {
struct Point {
let location: CGPoint
let velocity: CGFloat
let timestamp: Double
var x: CGFloat {
return self.location.x
}
var y: CGFloat {
return self.location.y
}
}
enum Mode {
case direct
case location
case smoothCurve
case polyline
@ -269,6 +284,7 @@ class DrawingGesturePipeline {
}
enum DrawingResult {
case point(DrawingGesturePipeline.Point)
case location(Polyline.Point)
case smoothCurve(BezierPath)
case polyline(Polyline)
@ -303,6 +319,7 @@ class DrawingGesturePipeline {
view.addGestureRecognizer(gestureRecognizer)
}
var previousPoint: Point?
@objc private func handleGesture(_ gestureRecognizer: DrawingGestureRecognizer) {
let state: DrawingGestureState
switch gestureRecognizer.state {
@ -322,6 +339,29 @@ class DrawingGesturePipeline {
state = .cancelled
}
if case .direct = self.mode, let touch = self.pendingTouches.first {
if state == .began {
self.previousPoint = nil
}
var velocity: Double = 0.0
if let previousPoint = self.previousPoint {
let distance = touch.location.distance(to: previousPoint.location)
let elapsed = max(0.0, touch.timestamp - previousPoint.timestamp)
velocity = elapsed > 0.0 ? distance / elapsed : 0.0
} else {
velocity = 0.0
}
let point = Point(location: touch.location, velocity: velocity, timestamp: touch.timestamp)
self.previousPoint = point
self.onDrawing(state, .point(point))
self.pendingTouches.removeAll()
return
}
let touchDeltas = self.processTouchEvents(self.pendingTouches)
let polylineDeltas = self.processTouchPaths(inputDeltas: touchDeltas)
let simplifiedPolylineDeltas = self.simplifyPolylines(inputDeltas: polylineDeltas)
@ -339,6 +379,8 @@ class DrawingGesturePipeline {
if let polyline = self.simplifiedPolylines.last {
self.onDrawing(state, .polyline(polyline))
}
case .direct:
break
}
self.pendingTouches.removeAll()

View File

@ -22,7 +22,7 @@ final class DrawingMetalView: MTKView {
init?(size: CGSize) {
var size = size
if Int(size.width) % 16 != 0 {
size.width = round(size.width / 16.0) * 16.0
size.width = ceil(size.width / 16.0) * 16.0
}
let mainBundle = Bundle(for: DrawingView.self)
@ -46,11 +46,14 @@ final class DrawingMetalView: MTKView {
self.commandQueue = commandQueue
self.size = size
super.init(frame: CGRect(origin: .zero, size: size), device: device)
self.autoResizeDrawable = false
self.isOpaque = false
self.contentScaleFactor = 1.0
self.drawableSize = self.size
//self.presentsWithTransaction = true
self.setup()
}
@ -87,7 +90,7 @@ final class DrawingMetalView: MTKView {
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 = [
@ -97,32 +100,46 @@ final class DrawingMetalView: MTKView {
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)
}
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.55))
}
self.drawable?.clear()
}
var clearOnce = false
override var frame: CGRect {
get {
return super.frame
} set {
super.frame = newValue
self.drawableSize = self.size
}
}
override func draw(_ rect: CGRect) {
guard !self.isHidden || (self.drawable?.isClearing ?? false) else {
return
}
super.draw(rect)
guard let drawable = self.drawable, let texture = drawable.texture?.texture else {
@ -135,38 +152,35 @@ final class DrawingMetalView: MTKView {
attachment?.texture = self.currentDrawable?.texture
attachment?.loadAction = .clear
attachment?.storeAction = .store
guard let _ = attachment?.texture else {
return
}
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
}
self.clearOnce = true
drawable.updateBuffer(with: self.size)
drawable.clear()
drawable.commit(wait: true)
}
enum BrushType {
@ -215,7 +229,9 @@ private class Drawable {
self.updateBuffer(with: size)
}
var isClearing = false
func clear() {
self.isClearing = true
self.texture?.clear()
self.commit(wait: true)
}
@ -246,6 +262,10 @@ private class Drawable {
self.commandBuffer?.waitUntilCompleted()
}
self.commandBuffer = nil
if self.isClearing && wait {
self.isClearing = false
}
}
internal func makeTexture() -> Texture? {
@ -645,7 +665,7 @@ final class Texture {
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget, .shaderRead]
textureDescriptor.storageMode = .shared
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
@ -706,7 +726,7 @@ final class Texture {
bitsPerComponent: 8,
bitsPerPixel: 8 * 4,
bytesPerRow: self.bytesPerRow,
space: DeviceGraphicsContextSettings.shared.colorSpace,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
provider: dataProvider,
decode: nil,

View File

@ -18,6 +18,7 @@ import ContextUI
import ChatEntityKeyboardInputNode
import EntityKeyboard
import TelegramUIPreferences
import FastBlur
enum DrawingToolState: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
@ -31,8 +32,8 @@ enum DrawingToolState: Equatable, Codable {
case arrow = 1
case marker = 2
case neon = 3
case eraser = 4
case blur = 5
case blur = 4
case eraser = 5
}
struct BrushState: Equatable, Codable {
@ -100,8 +101,8 @@ enum DrawingToolState: Equatable, Codable {
case arrow(BrushState)
case marker(BrushState)
case neon(BrushState)
case eraser(EraserState)
case blur(EraserState)
case eraser(EraserState)
func withUpdatedColor(_ color: DrawingColor) -> DrawingToolState {
switch self {
@ -113,7 +114,7 @@ enum DrawingToolState: Equatable, Codable {
return .marker(state.withUpdatedColor(color))
case let .neon(state):
return .neon(state.withUpdatedColor(color))
case .eraser, .blur:
case .blur, .eraser:
return self
}
}
@ -128,10 +129,10 @@ enum DrawingToolState: Equatable, Codable {
return .marker(state.withUpdatedSize(size))
case let .neon(state):
return .neon(state.withUpdatedSize(size))
case let .eraser(state):
return .eraser(state.withUpdatedSize(size))
case let .blur(state):
return .blur(state.withUpdatedSize(size))
case let .eraser(state):
return .eraser(state.withUpdatedSize(size))
}
}
@ -148,7 +149,7 @@ enum DrawingToolState: Equatable, Codable {
switch self {
case let .pen(state), let .arrow(state), let .marker(state), let .neon(state):
return state.size
case let .eraser(state), let .blur(state):
case let .blur(state), let .eraser(state):
return state.size
}
}
@ -163,10 +164,10 @@ enum DrawingToolState: Equatable, Codable {
return .marker
case .neon:
return .neon
case .eraser:
return .eraser
case .blur:
return .blur
case .eraser:
return .eraser
}
}
@ -183,10 +184,10 @@ enum DrawingToolState: Equatable, Codable {
self = .marker(try container.decode(BrushState.self, forKey: .brushState))
case .neon:
self = .neon(try container.decode(BrushState.self, forKey: .brushState))
case .eraser:
self = .eraser(try container.decode(EraserState.self, forKey: .eraserState))
case .blur:
self = .blur(try container.decode(EraserState.self, forKey: .eraserState))
case .eraser:
self = .eraser(try container.decode(EraserState.self, forKey: .eraserState))
}
} else {
self = .pen(BrushState(color: DrawingColor(rgb: 0x000000), size: 0.5))
@ -208,12 +209,12 @@ enum DrawingToolState: Equatable, Codable {
case let .neon(state):
try container.encode(DrawingToolState.Key.neon.rawValue, forKey: .type)
try container.encode(state, forKey: .brushState)
case let .eraser(state):
try container.encode(DrawingToolState.Key.eraser.rawValue, forKey: .type)
try container.encode(state, forKey: .eraserState)
case let .blur(state):
try container.encode(DrawingToolState.Key.blur.rawValue, forKey: .type)
try container.encode(state, forKey: .eraserState)
case let .eraser(state):
try container.encode(DrawingToolState.Key.eraser.rawValue, forKey: .type)
try container.encode(state, forKey: .eraserState)
}
}
}
@ -285,11 +286,24 @@ struct DrawingState: Equatable {
.arrow(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff8a00), size: 0.23)),
.marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xffd60a), size: 0.75)),
.neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34c759), size: 0.4)),
.eraser(DrawingToolState.EraserState(size: 0.5)),
.blur(DrawingToolState.EraserState(size: 0.5))
.blur(DrawingToolState.EraserState(size: 0.5)),
.eraser(DrawingToolState.EraserState(size: 0.5))
]
)
}
func forVideo() -> DrawingState {
return DrawingState(
selectedTool: self.selectedTool,
tools: self.tools.filter { tool in
if case .blur = tool {
return false
} else {
return true
}
}
)
}
}
final class DrawingSettings: Codable, Equatable {
@ -365,6 +379,7 @@ private final class DrawingScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let isVideo: Bool
let isAvatar: Bool
let present: (ViewController) -> Void
let updateState: ActionSlot<DrawingView.NavigationState>
@ -374,8 +389,9 @@ private final class DrawingScreenComponent: CombinedComponent {
let updateSelectedEntity: ActionSlot<DrawingEntity?>
let insertEntity: ActionSlot<DrawingEntity>
let deselectEntity: ActionSlot<Void>
let updatePlayback: ActionSlot<Bool>
let updateEntitiesPlayback: ActionSlot<Bool>
let previewBrushSize: ActionSlot<CGFloat?>
let dismissEyedropper: ActionSlot<Void>
let apply: ActionSlot<Void>
let dismiss: ActionSlot<Void>
@ -386,6 +402,7 @@ private final class DrawingScreenComponent: CombinedComponent {
init(
context: AccountContext,
isVideo: Bool,
isAvatar: Bool,
present: @escaping (ViewController) -> Void,
updateState: ActionSlot<DrawingView.NavigationState>,
@ -395,8 +412,9 @@ private final class DrawingScreenComponent: CombinedComponent {
updateSelectedEntity: ActionSlot<DrawingEntity?>,
insertEntity: ActionSlot<DrawingEntity>,
deselectEntity: ActionSlot<Void>,
updatePlayback: ActionSlot<Bool>,
updateEntitiesPlayback: ActionSlot<Bool>,
previewBrushSize: ActionSlot<CGFloat?>,
dismissEyedropper: ActionSlot<Void>,
apply: ActionSlot<Void>,
dismiss: ActionSlot<Void>,
presentColorPicker: @escaping (DrawingColor) -> Void,
@ -405,6 +423,7 @@ private final class DrawingScreenComponent: CombinedComponent {
dismissFastColorPicker: @escaping () -> Void
) {
self.context = context
self.isVideo = isVideo
self.isAvatar = isAvatar
self.present = present
self.updateState = updateState
@ -414,8 +433,9 @@ private final class DrawingScreenComponent: CombinedComponent {
self.updateSelectedEntity = updateSelectedEntity
self.insertEntity = insertEntity
self.deselectEntity = deselectEntity
self.updatePlayback = updatePlayback
self.updateEntitiesPlayback = updateEntitiesPlayback
self.previewBrushSize = previewBrushSize
self.dismissEyedropper = dismissEyedropper
self.apply = apply
self.dismiss = dismiss
self.presentColorPicker = presentColorPicker
@ -484,7 +504,8 @@ private final class DrawingScreenComponent: CombinedComponent {
private let updateToolState: ActionSlot<DrawingToolState>
private let insertEntity: ActionSlot<DrawingEntity>
private let deselectEntity: ActionSlot<Void>
private let updatePlayback: ActionSlot<Bool>
private let updateEntitiesPlayback: ActionSlot<Bool>
private let dismissEyedropper: ActionSlot<Void>
private let present: (ViewController) -> Void
var currentMode: Mode
@ -497,12 +518,13 @@ private final class DrawingScreenComponent: CombinedComponent {
private let stickerPickerInputData = Promise<StickerPickerInputData>()
init(context: AccountContext, updateToolState: ActionSlot<DrawingToolState>, insertEntity: ActionSlot<DrawingEntity>, deselectEntity: ActionSlot<Void>, updatePlayback: ActionSlot<Bool>, present: @escaping (ViewController) -> Void) {
init(context: AccountContext, updateToolState: ActionSlot<DrawingToolState>, insertEntity: ActionSlot<DrawingEntity>, deselectEntity: ActionSlot<Void>, updateEntitiesPlayback: ActionSlot<Bool>, dismissEyedropper: ActionSlot<Void>, present: @escaping (ViewController) -> Void) {
self.context = context
self.updateToolState = updateToolState
self.insertEntity = insertEntity
self.deselectEntity = deselectEntity
self.updatePlayback = updatePlayback
self.updateEntitiesPlayback = updateEntitiesPlayback
self.dismissEyedropper = dismissEyedropper
self.present = present
self.currentMode = .drawing
@ -647,6 +669,8 @@ private final class DrawingScreenComponent: CombinedComponent {
var skipSelectedEntityUpdate = false
func updateSelectedEntity(_ entity: DrawingEntity?) {
self.dismissEyedropper.invoke(Void())
self.selectedEntity = entity
if let entity = entity {
if !entity.color.isClear {
@ -758,10 +782,10 @@ private final class DrawingScreenComponent: CombinedComponent {
func presentStickerPicker() {
self.currentMode = .sticker
self.updatePlayback.invoke(false)
self.updateEntitiesPlayback.invoke(false)
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get())
controller.completion = { [weak self] file in
self?.updatePlayback.invoke(true)
self?.updateEntitiesPlayback.invoke(true)
if let file = file {
let stickerEntity = DrawingStickerEntity(file: file)
@ -776,7 +800,7 @@ private final class DrawingScreenComponent: CombinedComponent {
}
func makeState() -> State {
return State(context: self.context, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updatePlayback: self.updatePlayback, present: self.present)
return State(context: self.context, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, present: self.present)
}
static var body: Body {
@ -836,6 +860,8 @@ private final class DrawingScreenComponent: CombinedComponent {
let previewBrushSize = component.previewBrushSize
let performAction = component.performAction
let dismissEyedropper = component.dismissEyedropper
component.updateState.connect { [weak state] updatedState in
state?.updateDrawingState(updatedState)
}
@ -952,6 +978,7 @@ private final class DrawingScreenComponent: CombinedComponent {
let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0
let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in
dismissEyedropper.invoke(Void())
if let state = state {
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
state.updateSelectedTool(.pen, update: false)
@ -1186,15 +1213,17 @@ private final class DrawingScreenComponent: CombinedComponent {
} else {
let tools = tools.update(
component: ToolsComponent(
state: state.drawingState,
state: component.isVideo ? state.drawingState.forVideo() : state.drawingState,
isFocused: false,
tag: toolsTag,
toolPressed: { [weak state] tool in
dismissEyedropper.invoke(Void())
if let state = state {
state.updateSelectedTool(tool)
}
},
toolResized: { [weak state] _, size in
dismissEyedropper.invoke(Void())
state?.updateBrushSize(size)
if state?.selectedEntity == nil {
previewBrushSize.invoke(size)
@ -1353,6 +1382,7 @@ private final class DrawingScreenComponent: CombinedComponent {
)
),
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.zoomOut)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag),
@ -1379,6 +1409,7 @@ private final class DrawingScreenComponent: CombinedComponent {
tag: sizeSliderTag,
updated: { [weak state] size in
if let state = state {
dismissEyedropper.invoke(Void())
state.updateBrushSize(size)
if state.selectedEntity == nil {
previewBrushSize.invoke(size)
@ -1402,6 +1433,7 @@ private final class DrawingScreenComponent: CombinedComponent {
),
isEnabled: state.drawingViewState.canUndo,
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.undo)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag),
@ -1421,6 +1453,7 @@ private final class DrawingScreenComponent: CombinedComponent {
Image(image: state.image(.redo))
),
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.redo)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
@ -1440,6 +1473,7 @@ private final class DrawingScreenComponent: CombinedComponent {
),
isEnabled: state.drawingViewState.canClear,
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.clear)
}
).tagged(clearAllButtonTag),
@ -1536,7 +1570,6 @@ private final class DrawingScreenComponent: CombinedComponent {
.disappear(.default(scale: true))
)
let modeRightInset: CGFloat = 57.0
let addButton = addButton.update(
component: Button(
@ -1563,12 +1596,15 @@ private final class DrawingScreenComponent: CombinedComponent {
}
switch state.currentMode {
case .drawing:
dismissEyedropper.invoke(Void())
if let buttonView = controller.node.componentHost.findTaggedView(tag: addButtonTag) as? Button.View {
state.presentShapePicker(buttonView)
}
case .sticker:
dismissEyedropper.invoke(Void())
state.presentStickerPicker()
case .text:
dismissEyedropper.invoke(Void())
state.addTextEntity()
}
}
@ -1588,6 +1624,7 @@ private final class DrawingScreenComponent: CombinedComponent {
Image(image: state.image(.done))
),
action: { [weak state] in
dismissEyedropper.invoke(Void())
state?.saveToolState()
apply.invoke(Void())
}
@ -1639,6 +1676,7 @@ private final class DrawingScreenComponent: CombinedComponent {
tag: modeTag,
selectedIndex: selectedIndex,
selectionChanged: { [weak state] index in
dismissEyedropper.invoke(Void())
guard let state = state else {
return
}
@ -1653,6 +1691,7 @@ private final class DrawingScreenComponent: CombinedComponent {
},
sizeUpdated: { [weak state] size in
if let state = state {
dismissEyedropper.invoke(Void())
state.updateBrushSize(size)
if state.selectedEntity == nil {
previewBrushSize.invoke(size)
@ -1690,6 +1729,7 @@ private final class DrawingScreenComponent: CombinedComponent {
),
action: { [weak state] in
if let state = state {
dismissEyedropper.invoke(Void())
state.saveToolState()
dismiss.invoke(Void())
}
@ -1718,8 +1758,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
private let updateSelectedEntity: ActionSlot<DrawingEntity?>
private let insertEntity: ActionSlot<DrawingEntity>
private let deselectEntity: ActionSlot<Void>
private let updatePlayback: ActionSlot<Bool>
private let updateEntitiesPlayback: ActionSlot<Bool>
private let previewBrushSize: ActionSlot<CGFloat?>
private let dismissEyedropper: ActionSlot<Void>
private let apply: ActionSlot<Void>
private let dismiss: ActionSlot<Void>
@ -1752,36 +1793,6 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
strongSelf.updateState.invoke(state)
}
}
self._drawingView?.requestMenu = { [weak self] elements, rect in
if let strongSelf = self, let drawingView = strongSelf._drawingView {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
var actions: [ContextMenuAction] = []
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self] in
if let strongSelf = self {
strongSelf._drawingView?.removeElements(elements)
}
}))
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Duplicate, accessibilityLabel: presentationData.strings.Paint_Duplicate), action: { [weak self] in
if let strongSelf = self {
strongSelf._drawingView?.removeElements(elements)
}
}))
let strokeFrame = drawingView.lassoView.convert(rect, to: strongSelf.view).offsetBy(dx: 0.0, dy: -6.0)
let controller = ContextMenuController(actions: actions)
strongSelf.currentMenuController = controller
strongSelf.controller?.present(
controller,
in: .window(.root),
with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (strongSelf, strokeFrame, strongSelf, strongSelf.bounds)
} else {
return nil
}
})
)
}
}
self.performAction.connect { [weak self] action in
if let strongSelf = self {
if action == .clear {
@ -1816,6 +1827,11 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
strongSelf._drawingView?.setBrushSizePreview(size)
}
}
self.dismissEyedropper.connect { [weak self] in
if let strongSelf = self {
strongSelf.dismissCurrentEyedropper()
}
}
}
return self._drawingView!
}
@ -1826,33 +1842,48 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
if self._entitiesView == nil, let controller = self.controller {
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size)
self._drawingView?.entitiesView = self._entitiesView
self._entitiesView?.drawingView = self._drawingView
self._entitiesView?.entityAdded = { [weak self] entity in
self?._drawingView?.onEntityAdded(entity)
}
self._entitiesView?.entityRemoved = { [weak self] entity in
self?._drawingView?.onEntityRemoved(entity)
}
let entitiesLayer = self.entitiesView.layer
self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in
self._drawingView?.getFullImage = { [weak self] in
if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() {
if withDrawing {
let image = generateImage(controller.size, contextGenerator: { size, context in
let size = controller.size.fitted(CGSize(width: 256.0, height: 256.0))
if let imageContext = DrawingContext(size: size, scale: 1.0, opaque: true, clear: false) {
imageContext.withFlippedContext { c in
let bounds = CGRect(origin: .zero, size: size)
if let cgImage = currentImage.cgImage {
context.draw(cgImage, in: bounds)
c.draw(cgImage, in: bounds)
}
if let cgImage = strongSelf.drawingView.drawingImage?.cgImage {
context.draw(cgImage, in: bounds)
c.draw(cgImage, in: bounds)
}
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
entitiesLayer?.render(in: context)
}, opaque: true, scale: 1.0)
return image
telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes)
telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes)
}
return imageContext.generateImage()
} else {
return currentImage
return nil
}
// let image = generateImage(controller.size, contextGenerator: { size, context in
// let bounds = CGRect(origin: .zero, size: size)
// if let cgImage = currentImage.cgImage {
// context.draw(cgImage, in: bounds)
// }
// if let cgImage = strongSelf.drawingView.drawingImage?.cgImage {
// context.draw(cgImage, in: bounds)
// }
// telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes)
//
//
//
// }, opaque: true, scale: 1.0)
// return image
} else {
return nil
}
@ -1908,7 +1939,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
in: .window(.root),
with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (strongSelf, entityFrame, strongSelf, strongSelf.bounds)
return (strongSelf, entityFrame, strongSelf, strongSelf.bounds.insetBy(dx: 0.0, dy: 160.0))
} else {
return nil
}
@ -1926,7 +1957,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
textEntityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView)
} else {
entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
entityView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2)
if let selectionView = entityView.selectionView {
selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2)
@ -1940,7 +1971,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
entitiesView.selectEntity(nil)
}
}
self.updatePlayback.connect { [weak self] play in
self.updateEntitiesPlayback.connect { [weak self] play in
if let strongSelf = self, let entitiesView = strongSelf._entitiesView {
if play {
entitiesView.play()
@ -1979,8 +2010,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
self.updateSelectedEntity = ActionSlot<DrawingEntity?>()
self.insertEntity = ActionSlot<DrawingEntity>()
self.deselectEntity = ActionSlot<Void>()
self.updatePlayback = ActionSlot<Bool>()
self.updateEntitiesPlayback = ActionSlot<Bool>()
self.previewBrushSize = ActionSlot<CGFloat?>()
self.dismissEyedropper = ActionSlot<Void>()
self.apply = ActionSlot<Void>()
self.dismiss = ActionSlot<Void>()
@ -2000,7 +2032,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
}
self.dismiss.connect { [weak self] _ in
if let strongSelf = self {
if !strongSelf.drawingView.isEmpty {
if strongSelf.drawingView.canUndo || strongSelf.entitiesView.hasChanges {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
@ -2031,13 +2063,30 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
func presentEyedropper(dismissed: @escaping () -> Void) {
private var currentEyedropperView: EyedropperView?
func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) {
guard let controller = self.controller else {
return
}
self.entitiesView.pause()
if controller.isVideo && retryLaterForVideo {
controller.updateVideoPlayback(false)
Queue.mainQueue().after(0.1) {
self.presentEyedropper(retryLaterForVideo: false, dismissed: dismissed)
}
return
}
guard let currentImage = controller.getCurrentImage() else {
// if controller.isVideo && retryIfFailed {
// Queue.mainQueue().after(0.01) {
// self.presentEyedropper(retryIfFailed: false, dismissed: dismissed)
// }
// } else {
self.entitiesView.play()
controller.updateVideoPlayback(true)
// }
return
}
@ -2063,14 +2112,29 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
if let strongSelf = self, let controller = controller {
strongSelf.updateColor.invoke(color)
controller.entitiesView.play()
controller.updateVideoPlayback(true)
dismissed()
}
}
eyedropperView.dismissed = {
controller.entitiesView.play()
controller.updateVideoPlayback(true)
}
eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view)
controller.view.addSubview(eyedropperView)
self.currentEyedropperView = eyedropperView
}
func dismissCurrentEyedropper() {
if let currentEyedropperView = self.currentEyedropperView {
self.currentEyedropperView = nil
currentEyedropperView.dismiss()
}
}
func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) {
self.dismissCurrentEyedropper()
guard let controller = self.controller else {
return
}
@ -2087,6 +2151,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
private var fastColorPickerView: ColorSpectrumPickerView?
func presentFastColorPicker(sourceView: UIView) {
self.dismissCurrentEyedropper()
guard self.fastColorPickerView == nil, let superview = sourceView.superview else {
return
}
@ -2275,6 +2341,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
component: AnyComponent(
DrawingScreenComponent(
context: self.context,
isVideo: controller.isVideo,
isAvatar: controller.isAvatar,
present: { [weak self] c in
self?.controller?.present(c, in: .window(.root))
@ -2286,8 +2353,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
updateSelectedEntity: self.updateSelectedEntity,
insertEntity: self.insertEntity,
deselectEntity: self.deselectEntity,
updatePlayback: self.updatePlayback,
updateEntitiesPlayback: self.updateEntitiesPlayback,
previewBrushSize: self.previewBrushSize,
dismissEyedropper: self.dismissEyedropper,
apply: self.apply,
dismiss: self.dismiss,
presentColorPicker: { [weak self] initialColor in
@ -2496,16 +2564,19 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
private let context: AccountContext
private let size: CGSize
private let originalSize: CGSize
private let isVideo: Bool
private let isAvatar: Bool
public var requestDismiss: (() -> Void)!
public var requestApply: (() -> Void)!
public var getCurrentImage: (() -> UIImage?)!
public var updateVideoPlayback: ((Bool) -> Void)!
public init(context: AccountContext, size: CGSize, originalSize: CGSize, isAvatar: Bool) {
public init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool ) {
self.context = context
self.size = size
self.originalSize = originalSize
self.isVideo = isVideo
self.isAvatar = isAvatar
super.init(navigationBarPresentationData: nil)
@ -2585,7 +2656,6 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
}
let drawingData = self.drawingView.drawingData
let entitiesData = self.entitiesView.entitiesData
var stickers: [Any] = []

View File

@ -45,6 +45,8 @@ public final class DrawingSimpleShapeEntity: DrawingEntity, Codable {
return self.position
}
public var scale: CGFloat = 1.0
public var renderImage: UIImage?
init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
@ -305,7 +307,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
self.snapTool.onSnapYUpdated = { [weak self] snapped in
if let strongSelf = self, let entityView = strongSelf.entityView {
entityView.onSnapToXAxis(snapped)
entityView.onSnapToYAxis(snapped)
}
}
}
@ -407,36 +409,37 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
if isAspectLocked {
delta = CGPoint(x: delta.x, y: delta.x)
}
updatedSize.width -= delta.x
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
updatedSize.height -= delta.y
updatedSize.height = max(minimumSize.height, 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
updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
updatedSize.height -= delta.y
updatedSize.height = max(minimumSize.height, 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
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
updatedSize.height += delta.y
updatedSize.height = max(minimumSize.height, 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
updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
updatedSize.height += delta.y
updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.layer {
updatedPosition.x += delta.x

View File

@ -254,6 +254,7 @@ final class DrawingStickerEntityView: DrawingEntityView {
self.applyVisibility()
}
}
self.update(animated: false)
}
}
}
@ -537,15 +538,20 @@ class DrawingEntitySnapTool {
private var xState: (skipped: CGFloat, waitForLeave: Bool)?
private var yState: (skipped: CGFloat, waitForLeave: Bool)?
private var rotationState: (skipped: CGFloat, waitForLeave: Bool)?
var onSnapXUpdated: (Bool) -> Void = { _ in }
var onSnapYUpdated: (Bool) -> Void = { _ in }
var onSnapRotationUpdated: (CGFloat?) -> Void = { _ in }
func reset() {
self.xState = nil
self.yState = nil
self.rotationState = nil
self.onSnapXUpdated(false)
self.onSnapYUpdated(false)
self.onSnapRotationUpdated(nil)
}
func maybeSkipFromStart(entityView: DrawingEntityView, position: CGPoint) {
@ -565,7 +571,7 @@ class DrawingEntitySnapTool {
}
}
}
func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint) -> CGPoint {
var updatedPosition = updatedPosition
@ -640,4 +646,17 @@ class DrawingEntitySnapTool {
return updatedPosition
}
private let snapRotations: [CGFloat] = [0.0, .pi / 4.0, .pi / 2.0, .pi * 1.5,]
func maybeSkipFromStart(entityView: DrawingEntityView, rotation: CGFloat) {
self.rotationState = nil
let snapDelta: CGFloat = 0.087
for snapRotation in self.snapRotations {
if rotation > snapRotation - snapDelta && rotation < snapRotation + snapDelta {
self.rotationState = (0.0, true)
break
}
}
}
}

View File

@ -350,7 +350,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
self.customEmojiContainerView = customEmojiContainerView
}
customEmojiContainerView.update(fontSize: self.displayFontSize * 0.8, textColor: textColor, emojiRects: customEmojiRects)
customEmojiContainerView.update(fontSize: self.displayFontSize * 0.78, textColor: textColor, emojiRects: customEmojiRects)
} else if let customEmojiContainerView = self.customEmojiContainerView {
customEmojiContainerView.removeFromSuperview()
self.customEmojiContainerView = nil
@ -684,7 +684,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
let scale = self.textEntity.scale
let rotation = self.textEntity.rotation
let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.8 / 17.0)
let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0)
var entities: [DrawingStickerEntity] = []
for (emojiRect, emojiAttribute) in self.emojiRects {
@ -754,7 +754,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
self.snapTool.onSnapYUpdated = { [weak self] snapped in
if let strongSelf = self, let entityView = strongSelf.entityView {
entityView.onSnapToXAxis(snapped)
entityView.onSnapToYAxis(snapped)
}
}
}
@ -1033,8 +1033,8 @@ private class DrawingTextLayoutManager: NSLayoutManager {
context.saveGState()
context.translateBy(x: origin.x, y: origin.y)
context.setBlendMode(.normal)
context.setBlendMode(.copy)
context.setFillColor(frameColor.cgColor)
context.setStrokeColor(frameColor.cgColor)

View File

@ -19,6 +19,10 @@ final class MarkerTool: DrawingElement, Codable {
var points: [CGPoint] = []
weak var metalView: DrawingMetalView?
var isValid: Bool {
return !self.points.isEmpty
}
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) {
self.uuid = UUID()
@ -162,6 +166,10 @@ final class NeonTool: DrawingElement, Codable {
var translation = CGPoint()
private var currentRenderLayer: DrawingRenderLayer?
var isValid: Bool {
return self.renderPath != nil
}
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) {
self.uuid = UUID()
@ -267,17 +275,26 @@ final class NeonTool: DrawingElement, Codable {
}
final class FillTool: DrawingElement, Codable {
let uuid: UUID
let drawingSize: CGSize
let color: DrawingColor
let isBlur: Bool
var blurredImage: UIImage?
var translation = CGPoint()
required init(drawingSize: CGSize, color: DrawingColor) {
var isValid: Bool {
return true
}
required init(drawingSize: CGSize, color: DrawingColor, blur: Bool, blurredImage: UIImage?) {
self.uuid = UUID()
self.drawingSize = drawingSize
self.color = color
self.isBlur = blur
self.blurredImage = blurredImage
}
private enum CodingKeys: String, CodingKey {
@ -291,6 +308,7 @@ final class FillTool: DrawingElement, Codable {
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize)
self.color = try container.decode(DrawingColor.self, forKey: .color)
self.isBlur = false
// self.points = try container.decode([CGPoint].self, forKey: .points)
}
@ -313,10 +331,16 @@ final class FillTool: DrawingElement, Codable {
context.setShouldAntialias(false)
context.setBlendMode(.copy)
context.setFillColor(self.color.toCGColor())
context.fill(CGRect(origin: .zero, size: size))
if self.isBlur {
if let blurredImage = self.blurredImage?.cgImage {
context.draw(blurredImage, in: CGRect(origin: .zero, size: size))
}
} else {
context.setFillColor(self.color.toCGColor())
context.fill(CGRect(origin: .zero, size: size))
}
context.setBlendMode(.normal)
}
@ -382,127 +406,10 @@ final class BlurTool: DrawingElement, Codable {
private var currentRenderLayer: DrawingRenderLayer?
required init(drawingSize: CGSize, lineWidth: CGFloat) {
self.uuid = UUID()
self.drawingSize = drawingSize
let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.003)
let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.09)
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
self.renderLineWidth = lineWidth
var isValid: Bool {
return self.renderPath != nil
}
private enum CodingKeys: String, CodingKey {
case uuid
case drawingSize
case renderLineWidth
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize)
self.renderLineWidth = try container.decode(CGFloat.self, forKey: .renderLineWidth)
// self.points = try container.decode([CGPoint].self, forKey: .points)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.drawingSize, forKey: .drawingSize)
try container.encode(self.renderLineWidth, forKey: .renderLineWidth)
// try container.encode(self.points, forKey: .points)
}
func setupRenderLayer() -> DrawingRenderLayer? {
let layer = RenderLayer()
layer.setup(size: self.drawingSize, lineWidth: self.renderLineWidth, image: self.getFullImage())
self.currentRenderLayer = layer
return layer
}
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
guard case let .smoothCurve(bezierPath) = path else {
return
}
self.path = bezierPath
let renderPath = bezierPath.path.cgPath
self.renderPath = renderPath
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
currentRenderLayer.updatePath(renderPath)
}
}
func draw(in context: CGContext, size: CGSize) {
context.translateBy(x: self.translation.x, y: self.translation.y)
let renderLayer: DrawingRenderLayer?
if let currentRenderLayer = self.currentRenderLayer {
renderLayer = currentRenderLayer
} else {
renderLayer = self.setupRenderLayer()
}
renderLayer?.render(in: context)
}
}
final class EraserTool: DrawingElement, Codable {
class RenderLayer: SimpleLayer, DrawingRenderLayer {
var lineWidth: CGFloat = 0.0
let blurLayer = SimpleLayer()
let fillLayer = SimpleShapeLayer()
func setup(size: CGSize, lineWidth: CGFloat, image: UIImage?) {
self.contentsScale = 1.0
self.lineWidth = lineWidth
let bounds = CGRect(origin: .zero, size: size)
self.frame = bounds
self.blurLayer.frame = bounds
self.fillLayer.frame = bounds
if self.blurLayer.contents == nil, let image = image {
self.blurLayer.contents = image.cgImage
self.blurLayer.contentsGravity = .resize
}
self.blurLayer.mask = self.fillLayer
self.fillLayer.frame = bounds
self.fillLayer.contentsScale = 1.0
self.fillLayer.strokeColor = UIColor.white.cgColor
self.fillLayer.fillColor = UIColor.clear.cgColor
self.fillLayer.lineCap = .round
self.fillLayer.lineWidth = lineWidth
self.addSublayer(self.blurLayer)
}
func updatePath(_ path: CGPath) {
self.fillLayer.path = path
}
}
var getFullImage: () -> UIImage? = { return nil }
let uuid: UUID
let drawingSize: CGSize
var path: BezierPath?
var renderPath: CGPath?
let renderLineWidth: CGFloat
var translation = CGPoint()
private var currentRenderLayer: DrawingRenderLayer?
required init(drawingSize: CGSize, lineWidth: CGFloat) {
self.uuid = UUID()
self.drawingSize = drawingSize
@ -575,7 +482,6 @@ enum CodableDrawingElement {
case pen(PenTool)
case marker(MarkerTool)
case neon(NeonTool)
case eraser(EraserTool)
case blur(BlurTool)
case fill(FillTool)
@ -586,8 +492,6 @@ enum CodableDrawingElement {
self = .marker(element)
} else if let element = element as? NeonTool {
self = .neon(element)
} else if let element = element as? EraserTool {
self = .eraser(element)
} else if let element = element as? BlurTool {
self = .blur(element)
} else if let element = element as? FillTool {
@ -597,7 +501,7 @@ enum CodableDrawingElement {
}
}
var entity: DrawingElement {
var element: DrawingElement {
switch self {
case let .pen(element):
return element
@ -605,8 +509,6 @@ enum CodableDrawingElement {
return element
case let .neon(element):
return element
case let .eraser(element):
return element
case let .blur(element):
return element
case let .fill(element):
@ -625,7 +527,6 @@ extension CodableDrawingElement: Codable {
case pen
case marker
case neon
case eraser
case blur
case fill
}
@ -640,8 +541,6 @@ extension CodableDrawingElement: Codable {
self = .marker(try container.decode(MarkerTool.self, forKey: .element))
case .neon:
self = .neon(try container.decode(NeonTool.self, forKey: .element))
case .eraser:
self = .eraser(try container.decode(EraserTool.self, forKey: .element))
case .blur:
self = .blur(try container.decode(BlurTool.self, forKey: .element))
case .fill:
@ -661,9 +560,6 @@ extension CodableDrawingElement: Codable {
case let .neon(payload):
try container.encode(ElementType.neon, forKey: .type)
try container.encode(payload, forKey: .element)
case let .eraser(payload):
try container.encode(ElementType.eraser, forKey: .type)
try container.encode(payload, forKey: .element)
case let .blur(payload):
try container.encode(ElementType.blur, forKey: .type)
try container.encode(payload, forKey: .element)

View File

@ -51,6 +51,8 @@ public final class DrawingVectorEntity: DrawingEntity, Codable {
return self.start
}
public var scale: CGFloat = 1.0
public var renderImage: UIImage?
init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) {

View File

@ -10,7 +10,8 @@ import ImageBlur
protocol DrawingElement: AnyObject {
var uuid: UUID { get }
var translation: CGPoint { get set }
var isValid: Bool { get }
func setupRenderLayer() -> DrawingRenderLayer?
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState)
@ -43,23 +44,21 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
enum Tool {
case pen
case arrow
case marker
case neon
case eraser
case lasso
case blur
}
var tool: Tool = .pen
var toolColor: DrawingColor = DrawingColor(color: .white)
var toolBrushSize: CGFloat = 0.25
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 }
var getFullImage: () -> UIImage? = { return nil }
private var elements: [DrawingElement] = []
private var undoStack: [DrawingOperation] = []
@ -69,18 +68,17 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
private(set) var drawingImage: UIImage?
private let renderer: UIGraphicsImageRenderer
private var currentDrawingView: UIView
private var currentDrawingView: UIImageView
private var currentDrawingLayer: DrawingRenderLayer?
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(set) var zoomScale: CGFloat = 1.0
private var drawingGesturePipeline: DrawingGesturePipeline?
private var longPressGestureRecognizer: UILongPressGestureRecognizer?
@ -109,13 +107,15 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.loadedTemplates.append(template)
}
}
load("shape_rectangle")
load("shape_circle")
load("shape_star")
load("shape_arrow")
}
private let hapticFeedback = HapticFeedback()
init(size: CGSize) {
self.imageSize = size
@ -123,7 +123,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
format.scale = 1.0
self.renderer = UIGraphicsImageRenderer(size: size, format: format)
self.currentDrawingView = UIView()
self.currentDrawingView = UIImageView()
self.currentDrawingView.frame = CGRect(origin: .zero, size: size)
self.currentDrawingView.contentScaleFactor = 1.0
self.currentDrawingView.backgroundColor = .clear
@ -135,9 +135,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.pannedSelectionView.backgroundColor = .clear
self.pannedSelectionView.isUserInteractionEnabled = false
self.lassoView = DrawingLassoView(size: size)
self.lassoView.isHidden = true
self.metalView = DrawingMetalView(size: size)!
self.metalView.isHidden = true
@ -160,11 +157,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.backgroundColor = .clear
self.contentScaleFactor = 1.0
self.isExclusiveTouch = true
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)
@ -173,7 +170,10 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
if !strongSelf.shouldBegin(point) {
return false
}
if !strongSelf.lassoView.isHidden && strongSelf.lassoView.point(inside: strongSelf.convert(point, to: strongSelf.lassoView), with: nil) {
if strongSelf.elements.isEmpty && !strongSelf.hasOpaqueData && strongSelf.tool == .eraser {
return false
}
if let uncommitedElement = strongSelf.uncommitedElement as? PenTool, uncommitedElement.isFinishingArrow {
return false
}
return true
@ -200,7 +200,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
}
if newElement is MarkerTool {
self?.metalView.isHidden = false
strongSelf.metalView.isHidden = false
}
if let renderLayer = newElement.setupRenderLayer() {
@ -208,7 +208,20 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
strongSelf.currentDrawingLayer = nil
currentDrawingLayer.removeFromSuperlayer()
}
strongSelf.currentDrawingView.layer.addSublayer(renderLayer)
if strongSelf.tool == .eraser {
strongSelf.currentDrawingView.removeFromSuperview()
strongSelf.currentDrawingView.backgroundColor = .white
renderLayer.compositingFilter = "xor"
strongSelf.currentDrawingView.layer.addSublayer(renderLayer)
strongSelf.mask = strongSelf.currentDrawingView
} else if strongSelf.tool == .blur {
strongSelf.currentDrawingView.layer.mask = renderLayer
strongSelf.currentDrawingView.image = strongSelf.preparedBlurredImage
} else {
strongSelf.currentDrawingView.layer.addSublayer(renderLayer)
}
strongSelf.currentDrawingLayer = renderLayer
}
newElement.updatePath(path, state: state)
@ -309,45 +322,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
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.updateSelectionContent()
}
}
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, completion: {
strongSelf.pannedSelectionView.layer.contents = nil
strongSelf.lassoView.bounds = CGRect(origin: .zero, size: strongSelf.lassoView.bounds.size)
strongSelf.lassoView.translate(offset)
})
}
}
}
required init?(coder: NSCoder) {
@ -360,26 +334,33 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
}
public func setup(withDrawing drawingData: Data!) {
if let drawingData = drawingData, let image = UIImage(data: drawingData) {
self.hasOpaqueData = true
self.drawingImage = image
self.layer.contents = image.cgImage
//let codableElements = try? JSONDecoder().decode([CodableDrawingElement].self, from: drawingData) {
//self.elements = codableElements.map { $0.element }
//self.commit(reset: true)
self.updateInternalState()
}
}
var hasOpaqueData = false
var drawingData: Data? {
guard !self.elements.isEmpty else {
guard !self.elements.isEmpty || self.hasOpaqueData else {
return nil
}
return self.drawingImage?.pngData()
let codableElements = self.elements.compactMap({ CodableDrawingElement(element: $0) })
if let data = try? JSONEncoder().encode(codableElements) {
return data
} else {
return nil
}
// let codableElements = self.elements.compactMap({ CodableDrawingElement(element: $0) })
// if let data = try? JSONEncoder().encode(codableElements) {
// return data
// } else {
// return nil
// }
}
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === self.longPressGestureRecognizer, !self.lassoView.isHidden {
return false
}
return true
}
@ -390,10 +371,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
private var longPressTimer: SwiftSignalKit.Timer?
private var fillCircleLayer: CALayer?
@objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
guard ![.eraser, .blur].contains(self.tool) else {
return
}
let location = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
@ -401,33 +378,64 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.longPressTimer = nil
if self.longPressTimer == nil {
self.longPressTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
var toolColor = self.toolColor
var blurredImage: UIImage?
if self.tool == .marker {
toolColor = toolColor.withUpdatedAlpha(toolColor.alpha * 0.7)
} else if self.tool == .eraser {
toolColor = DrawingColor.clear
} else if self.tool == .blur {
blurredImage = self.preparedBlurredImage
}
self.hapticFeedback.tap()
let fillCircleLayer = SimpleShapeLayer()
self.longPressTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self, weak fillCircleLayer] in
if let strongSelf = self {
strongSelf.cancelDrawing()
let newElement = FillTool(drawingSize: strongSelf.imageSize, color: strongSelf.toolColor)
strongSelf.uncommitedElement = newElement
strongSelf.finishDrawing()
let action = {
let newElement = FillTool(drawingSize: strongSelf.imageSize, color: toolColor, blur: blurredImage != nil, blurredImage: blurredImage)
strongSelf.uncommitedElement = newElement
strongSelf.finishDrawing(synchronous: true)
}
if [.eraser, .blur].contains(strongSelf.tool) {
UIView.transition(with: strongSelf, duration: 0.2, options: .transitionCrossDissolve) {
action()
}
} else {
action()
}
strongSelf.fillCircleLayer = nil
fillCircleLayer?.removeFromSuperlayer()
strongSelf.hapticFeedback.impact(.medium)
}
}, 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()
if [.eraser, .blur].contains(self.tool) {
} else {
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 = 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()
@ -475,13 +483,24 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.layer.contents = updatedImage.cgImage
if let currentDrawingLayer = self.currentDrawingLayer {
if case .eraser = self.tool {
currentDrawingLayer.removeFromSuperlayer()
self.mask = nil
self.insertSubview(self.currentDrawingView, at: 0)
self.currentDrawingView.backgroundColor = .clear
} else if case .blur = self.tool {
self.currentDrawingView.layer.mask = nil
self.currentDrawingView.image = nil
} else {
currentDrawingLayer.removeFromSuperlayer()
}
self.currentDrawingLayer = nil
currentDrawingLayer.removeFromSuperlayer()
}
self.metalView.clear()
self.metalView.isHidden = true
if self.tool == .marker {
self.metalView.clear()
self.metalView.isHidden = true
}
completion()
}
}
@ -509,13 +528,25 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.uncommitedElement = nil
if let currentDrawingLayer = self.currentDrawingLayer {
if self.tool == .eraser {
currentDrawingLayer.removeFromSuperlayer()
self.mask = nil
self.insertSubview(self.currentDrawingView, at: 0)
self.currentDrawingView.backgroundColor = .clear
} else if self.tool == .blur {
self.currentDrawingView.mask = nil
} else {
currentDrawingLayer.removeFromSuperlayer()
}
self.currentDrawingLayer = nil
currentDrawingLayer.removeFromSuperlayer()
}
}
fileprivate func finishDrawing() {
fileprivate func finishDrawing(synchronous: Bool = false) {
let complete: (Bool) -> Void = { synchronous in
if let uncommitedElement = self.uncommitedElement, !uncommitedElement.isValid {
self.uncommitedElement = nil
}
self.commit(interactive: true, synchronous: synchronous)
self.redoStack.removeAll()
@ -532,7 +563,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
complete(true)
})
} else {
complete(false)
complete(synchronous)
}
}
@ -544,6 +575,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.elements.removeAll()
self.undoStack.removeAll()
self.redoStack.removeAll()
self.hasOpaqueData = false
let snapshotView = UIImageView(image: self.drawingImage)
snapshotView.frame = self.bounds
@ -560,7 +592,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.updateInternalState()
self.lassoView.reset()
self.updateBlurredImage()
}
var canUndo: Bool {
return !self.undoStack.isEmpty
}
private func undo() {
@ -573,16 +609,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.redoStack.append(.element(element))
self.elements.removeAll(where: { $0.uuid == element.uuid })
let snapshotView = UIImageView(image: self.drawingImage)
snapshotView.frame = self.bounds
self.addSubview(snapshotView)
self.commit(reset: true)
Queue.mainQueue().justDispatch {
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) {
self.commit(reset: true)
}
self.updateBlurredImage()
case let .addEntity(uuid):
if let entityView = self.entitiesView?.getView(for: uuid) {
self.entitiesView?.remove(uuid: uuid, animated: true, announce: false)
@ -592,7 +623,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
if let view = self.entitiesView?.add(entity, announce: false) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if !(entity is DrawingVectorEntity) {
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2)
}
}
self.redoStack.append(.addEntity(entity.uuid))
@ -615,8 +646,12 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.undoStack.append(.element(element))
self.uncommitedElement = element
self.commit(reset: false)
UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) {
self.commit(reset: false)
}
self.uncommitedElement = nil
self.updateBlurredImage()
case let .addEntity(uuid):
if let entityView = self.entitiesView?.getView(for: uuid) {
self.entitiesView?.remove(uuid: uuid, animated: true, announce: false)
@ -626,7 +661,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
if let view = self.entitiesView?.add(entity, announce: false) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if !(entity is DrawingVectorEntity) {
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2)
}
}
self.undoStack.append(.addEntity(entity.uuid))
@ -651,71 +686,61 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.updateInternalState()
}
private var preparredEraserImage: UIImage?
private var preparedBlurredImage: UIImage?
func updateToolState(_ state: DrawingToolState) {
let previousTool = self.tool
switch state {
case let .pen(brushState):
self.drawingGesturePipeline?.mode = .polyline
self.drawingGesturePipeline?.mode = .direct
self.tool = .pen
self.toolColor = brushState.color
self.toolBrushSize = brushState.size
self.toolHasArrow = false
case let .arrow(brushState):
self.drawingGesturePipeline?.mode = .polyline
self.tool = .pen
self.drawingGesturePipeline?.mode = .direct
self.tool = .arrow
self.toolColor = brushState.color
self.toolBrushSize = brushState.size
self.toolHasArrow = true
case let .marker(brushState):
self.drawingGesturePipeline?.mode = .location
self.tool = .marker
self.toolColor = brushState.color
self.toolBrushSize = brushState.size
self.toolHasArrow = false
case let .neon(brushState):
self.drawingGesturePipeline?.mode = .smoothCurve
self.tool = .neon
self.toolColor = brushState.color
self.toolBrushSize = brushState.size
self.toolHasArrow = false
case let .eraser(eraserState):
self.tool = .eraser
self.drawingGesturePipeline?.mode = .smoothCurve
self.toolBrushSize = eraserState.size
case let .blur(blurState):
self.tool = .blur
self.drawingGesturePipeline?.mode = .smoothCurve
self.drawingGesturePipeline?.mode = .direct
self.toolBrushSize = blurState.size
case let .eraser(eraserState):
self.tool = .eraser
self.drawingGesturePipeline?.mode = .direct
self.toolBrushSize = eraserState.size
}
if [.eraser, .blur].contains(self.tool) {
if self.tool != previousTool {
self.updateBlurredImage()
} else {
self.preparedBlurredImage = nil
}
}
func updateBlurredImage() {
if case .blur = 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
}
if let image = self.getFullImage() {
Queue.mainQueue().async {
self.preparedBlurredImage = image
}
}
}
} else {
self.preparredEraserImage = nil
self.preparedBlurredImage = nil
}
}
func performAction(_ action: Action) {
@ -733,15 +758,16 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
private func updateInternalState() {
self.stateUpdated(NavigationState(
canUndo: !self.elements.isEmpty || !self.undoStack.isEmpty,
canUndo: !self.undoStack.isEmpty,
canRedo: !self.redoStack.isEmpty,
canClear: !self.elements.isEmpty || !(self.entitiesView?.entities.isEmpty ?? true),
canClear: !self.elements.isEmpty || self.hasOpaqueData || !(self.entitiesView?.entities.isEmpty ?? true),
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
isDrawing: self.isDrawing
))
}
public func updateZoomScale(_ scale: CGFloat) {
self.cancelDrawing()
self.zoomScale = scale
self.updateInternalState()
}
@ -755,7 +781,21 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
drawingSize: self.imageSize,
color: self.toolColor,
lineWidth: self.toolBrushSize * scale,
hasArrow: self.toolHasArrow
hasArrow: false,
isEraser: false,
isBlur: false,
blurredImage: nil
)
element = penTool
case .arrow:
let penTool = PenTool(
drawingSize: self.imageSize,
color: self.toolColor,
lineWidth: self.toolBrushSize * scale,
hasArrow: true,
isEraser: false,
isBlur: false,
blurredImage: nil
)
element = penTool
case .marker:
@ -773,25 +813,27 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
lineWidth: self.toolBrushSize * scale
)
case .blur:
let blurTool = BlurTool(
let penTool = PenTool(
drawingSize: self.imageSize,
lineWidth: self.toolBrushSize * scale
color: self.toolColor,
lineWidth: self.toolBrushSize * scale,
hasArrow: false,
isEraser: false,
isBlur: true,
blurredImage: self.preparedBlurredImage
)
blurTool.getFullImage = { [weak self] in
return self?.preparredEraserImage
}
element = blurTool
element = penTool
case .eraser:
let eraserTool = EraserTool(
let penTool = PenTool(
drawingSize: self.imageSize,
lineWidth: self.toolBrushSize * scale
color: self.toolColor,
lineWidth: self.toolBrushSize * scale,
hasArrow: false,
isEraser: true,
isBlur: false,
blurredImage: nil
)
eraserTool.getFullImage = { [weak self] in
return self?.preparredEraserImage
}
element = eraserTool
default:
element = nil
element = penTool
}
return element
}
@ -804,15 +846,16 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
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 minLineWidth = max(1.0, max(self.frame.width, self.frame.height) * 0.002)
let maxLineWidth = max(10.0, max(self.frame.width, self.frame.height) * 0.07)
let minBrushSize = minLineWidth
let maxBrushSize = maxLineWidth
let brushSize = minBrushSize + (maxBrushSize - minBrushSize) * size
self.brushSizePreviewLayer.transform = CATransform3DMakeScale(brushSize / 100.0, brushSize / 100.0, 1.0)
@ -831,10 +874,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
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
@ -853,171 +893,3 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
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

@ -54,6 +54,7 @@ final class EyedropperView: UIView {
private let sourceImage: (data: Data, size: CGSize, bytesPerRow: Int, info: CGBitmapInfo)?
var completed: (DrawingColor) -> Void = { _ in }
var dismissed: () -> Void = { }
init(containerSize: CGSize, drawingView: DrawingView, sourceImage: UIImage) {
self.drawingView = drawingView
@ -162,6 +163,15 @@ final class EyedropperView: UIView {
}
}
func dismiss() {
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.dismissed()
}
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

View File

@ -2,49 +2,53 @@ import Foundation
import UIKit
import Display
final class PenTool: DrawingElement, Codable {
final class PenTool: DrawingElement, Codable {
class RenderLayer: SimpleLayer, DrawingRenderLayer {
private weak var element: PenTool?
private var isEraser = false
private var accumulationImage: UIImage?
private var activeLayer: ActiveLayer?
private var start = 0
private var segmentsCount = 0
private var velocity: CGFloat?
private var previousRect: CGRect?
var displayLink: ConstantDisplayLinkAnimator?
func setup(size: CGSize) {
func setup(size: CGSize, isEraser: Bool, useDisplayLink: Bool) {
self.isEraser = isEraser
self.shouldRasterize = true
self.contentsScale = 1.0
self.allowsGroupOpacity = true
let bounds = CGRect(origin: .zero, size: size)
self.frame = bounds
self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
if let strongSelf = self {
if let element = strongSelf.element, strongSelf.segmentsCount < element.segments.count, let velocity = strongSelf.velocity {
let delta = max(9, Int(velocity / 100.0))
let start = strongSelf.segmentsCount
strongSelf.segmentsCount = min(strongSelf.segmentsCount + delta, element.segments.count)
let rect = element.boundingRect(from: start, to: strongSelf.segmentsCount)
strongSelf.setNeedsDisplay(rect.insetBy(dx: -80.0, dy: -80.0))
}
}
})
self.displayLink?.frameInterval = 1
self.displayLink?.isPaused = false
}
fileprivate func draw(element: PenTool, velocity: CGFloat, rect: CGRect) {
self.element = element
let activeLayer = ActiveLayer()
activeLayer.shouldRasterize = true
activeLayer.contentsScale = 1.0
activeLayer.frame = bounds
activeLayer.parent = self
self.addSublayer(activeLayer)
self.activeLayer = activeLayer
self.previousRect = rect
if let previous = self.velocity {
self.velocity = velocity * 0.4 + previous * 0.6
} else {
self.velocity = velocity
if useDisplayLink {
self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
if let strongSelf = self {
if let element = strongSelf.element, strongSelf.segmentsCount < element.segments.count, let velocity = strongSelf.velocity {
let delta = max(12, Int(velocity / 100.0))
let start = strongSelf.segmentsCount
strongSelf.segmentsCount = min(strongSelf.segmentsCount + delta, element.segments.count)
let rect = element.boundingRect(from: start, to: strongSelf.segmentsCount)
strongSelf.activeLayer?.setNeedsDisplay(rect.insetBy(dx: -10.0, dy: -10.0))
}
}
})
self.displayLink?.frameInterval = 1
self.displayLink?.isPaused = false
}
self.setNeedsDisplay(rect.insetBy(dx: -80.0, dy: -80.0))
}
func animateArrowPaths(leftArrowPath: UIBezierPath, rightArrowPath: UIBezierPath, lineWidth: CGFloat, completion: @escaping () -> Void) {
@ -73,8 +77,63 @@ final class PenTool: DrawingElement, Codable {
})
}
override func draw(in ctx: CGContext) {
self.element?.drawSegments(in: ctx, upTo: self.segmentsCount)
var i = 0
fileprivate func draw(element: PenTool, velocity: CGFloat, rect: CGRect) {
self.element = element
self.opacity = Float(element.color.alpha)
guard !rect.isInfinite && !rect.isEmpty && !rect.isNull else {
return
}
var rect: CGRect? = rect
let limit = 512
let activeCount = self.segmentsCount - self.start
if activeCount > limit {
rect = nil
let newStart = self.start + limit
let image = generateImage(self.bounds.size, contextGenerator: { size, context in
if let accumulationImage = self.accumulationImage, let cgImage = accumulationImage.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
}
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
element.drawSegments(in: context, from: self.start, upTo: newStart)
}, opaque: false, scale: 1.0)
self.accumulationImage = image
self.contents = image?.cgImage
i += 1
print("accumulated \(i)")
self.start = newStart
}
self.segmentsCount = element.segments.count
if let previous = self.velocity {
self.velocity = velocity * 0.4 + previous * 0.6
} else {
self.velocity = velocity
}
if let rect = rect {
self.activeLayer?.setNeedsDisplay(rect.insetBy(dx: -10.0, dy: -10.0))
} else {
self.activeLayer?.setNeedsDisplay()
}
}
class ActiveLayer: SimpleLayer {
weak var parent: RenderLayer?
override func draw(in ctx: CGContext) {
guard let parent = self.parent, let element = parent.element else {
return
}
element.drawSegments(in: ctx, from: parent.start, upTo: parent.segmentsCount)
}
}
}
@ -83,44 +142,73 @@ final class PenTool: DrawingElement, Codable {
let color: DrawingColor
let renderLineWidth: CGFloat
let renderMinLineWidth: CGFloat
let renderColor: UIColor
let hasArrow: Bool
let renderArrowLength: CGFloat
let renderArrowLineWidth: CGFloat
let isEraser: Bool
let isBlur: Bool
var arrowStart: CGPoint?
var arrowDirection: CGFloat?
var arrowLeftPath: UIBezierPath?
var arrowRightPath: UIBezierPath?
var translation: CGPoint = .zero
var blurredImage: UIImage?
private weak var currentRenderLayer: DrawingRenderLayer?
var isValid: Bool {
if self.hasArrow {
return self.arrowStart != nil && self.arrowDirection != nil
} else {
return self.segments.count > 0
}
}
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, hasArrow: Bool) {
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, hasArrow: Bool, isEraser: Bool, isBlur: Bool, blurredImage: UIImage?) {
self.uuid = UUID()
self.drawingSize = drawingSize
self.color = color
self.color = isEraser || isBlur ? DrawingColor(rgb: 0x000000) : color
self.hasArrow = hasArrow
self.isEraser = isEraser
self.isBlur = isBlur
self.blurredImage = blurredImage
let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.002)
let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.07)
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
let minRenderArrowLength = max(10.0, max(drawingSize.width, drawingSize.height) * 0.02)
self.renderLineWidth = lineWidth
self.renderMinLineWidth = minLineWidth + (lineWidth - minLineWidth) * 0.3
self.renderArrowLength = lineWidth * 3.0
self.renderArrowLineWidth = lineWidth * 0.8
self.renderMinLineWidth = isEraser || isBlur ? lineWidth : minLineWidth + (lineWidth - minLineWidth) * 0.3
self.renderArrowLength = max(minRenderArrowLength, lineWidth * 3.0)
self.renderArrowLineWidth = max(minLineWidth * 1.8, lineWidth * 0.75)
self.renderColor = color.withUpdatedAlpha(1.0).toUIColor()
}
private enum CodingKeys: String, CodingKey {
case uuid
case drawingSize
case color
case isEraser
case hasArrow
case isBlur
case renderLineWidth
case renderMinLineWidth
case renderArrowLength
case renderArrowLineWidth
case arrowStart
case arrowDirection
case renderSegments
}
@ -130,11 +218,21 @@ final class PenTool: DrawingElement, Codable {
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize)
self.color = try container.decode(DrawingColor.self, forKey: .color)
self.isEraser = try container.decode(Bool.self, forKey: .isEraser)
self.hasArrow = try container.decode(Bool.self, forKey: .hasArrow)
self.isBlur = try container.decode(Bool.self, forKey: .isBlur)
self.renderLineWidth = try container.decode(CGFloat.self, forKey: .renderLineWidth)
self.renderMinLineWidth = try container.decode(CGFloat.self, forKey: .renderMinLineWidth)
self.renderArrowLength = try container.decode(CGFloat.self, forKey: .renderArrowLength)
self.renderArrowLineWidth = try container.decode(CGFloat.self, forKey: .renderArrowLineWidth)
self.arrowStart = try container.decodeIfPresent(CGPoint.self, forKey: .arrowStart)
self.arrowDirection = try container.decodeIfPresent(CGFloat.self, forKey: .arrowDirection)
self.segments = try container.decode([Segment].self, forKey: .renderSegments)
self.renderColor = self.color.withUpdatedAlpha(1.0).toUIColor()
self.maybeSetupArrow()
}
public func encode(to encoder: Encoder) throws {
@ -142,16 +240,26 @@ final class PenTool: DrawingElement, Codable {
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.drawingSize, forKey: .drawingSize)
try container.encode(self.color, forKey: .color)
try container.encode(self.isEraser, forKey: .isEraser)
try container.encode(self.hasArrow, forKey: .hasArrow)
try container.encode(self.isBlur, forKey: .isBlur)
try container.encode(self.renderLineWidth, forKey: .renderLineWidth)
try container.encode(self.renderMinLineWidth, forKey: .renderMinLineWidth)
try container.encode(self.renderArrowLength, forKey: .renderArrowLength)
try container.encode(self.renderArrowLineWidth, forKey: .renderArrowLineWidth)
try container.encodeIfPresent(self.arrowStart, forKey: .arrowStart)
try container.encodeIfPresent(self.arrowDirection, forKey: .arrowDirection)
try container.encode(self.segments, forKey: .renderSegments)
}
var isFinishingArrow = false
func finishArrow(_ completion: @escaping () -> Void) {
if let arrowLeftPath, let arrowRightPath {
(self.currentRenderLayer as? RenderLayer)?.animateArrowPaths(leftArrowPath: arrowLeftPath, rightArrowPath: arrowRightPath, lineWidth: self.renderArrowLineWidth, completion: {
self.isFinishingArrow = true
(self.currentRenderLayer as? RenderLayer)?.animateArrowPaths(leftArrowPath: arrowLeftPath, rightArrowPath: arrowRightPath, lineWidth: self.renderArrowLineWidth, completion: { [weak self] in
self?.isFinishingArrow = false
completion()
})
} else {
@ -161,35 +269,36 @@ final class PenTool: DrawingElement, Codable {
func setupRenderLayer() -> DrawingRenderLayer? {
let layer = RenderLayer()
layer.setup(size: self.drawingSize)
layer.setup(size: self.drawingSize, isEraser: self.isEraser, useDisplayLink: self.color.toUIColor().rgb == 0xbf5af2)
self.currentRenderLayer = layer
return layer
}
var previousPoint: CGPoint?
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
guard case let .polyline(line) = path, let point = line.points.last else {
guard case let .point(point) = path else {
return
}
let filterDistance: CGFloat
var filterDistance: CGFloat
if point.velocity > 1200.0 {
filterDistance = 75.0
filterDistance = 90.0
} else {
filterDistance = 35.0
filterDistance = 20.0
}
// filterDistance = 0.0
if let previousPoint, point.location.distance(to: previousPoint) < filterDistance, state == .changed, self.segments.count > 0 {
if let previousPoint, point.location.distance(to: previousPoint) < filterDistance, state == .changed, self.segments.count > 1 {
return
}
self.previousPoint = point.location
var velocity = point.velocity
if velocity.isZero {
velocity = 600.0
velocity = 1000.0
}
var effectiveRenderLineWidth = max(self.renderMinLineWidth, min(self.renderLineWidth + 1.0 - (velocity / 180.0), self.renderLineWidth))
var effectiveRenderLineWidth = max(self.renderMinLineWidth, min(self.renderLineWidth + 1.0 - (velocity / 220.0), self.renderLineWidth))
if let previousRenderLineWidth = self.previousRenderLineWidth {
effectiveRenderLineWidth = effectiveRenderLineWidth * 0.2 + previousRenderLineWidth * 0.8
}
@ -214,23 +323,42 @@ final class PenTool: DrawingElement, Codable {
}
}
}
if let point = self.smoothPoints.last?.position, let direction {
let arrowLeftPath = UIBezierPath()
arrowLeftPath.move(to: point)
arrowLeftPath.addLine(to: point.pointAt(distance: self.renderArrowLength, angle: direction - 0.45))
let arrowRightPath = UIBezierPath()
arrowRightPath.move(to: point)
arrowRightPath.addLine(to: point.pointAt(distance: self.renderArrowLength, angle: direction + 0.45))
self.arrowLeftPath = arrowLeftPath
self.arrowRightPath = arrowRightPath
}
self.arrowStart = self.smoothPoints.last?.position
self.arrowDirection = direction
self.maybeSetupArrow()
} else if self.segments.isEmpty {
let radius = self.renderLineWidth / 2.0
self.segments.append(
Segment(
a: CGPoint(x: point.x - radius, y: point.y),
b: CGPoint(x: point.x + radius, y: point.y),
c: CGPoint(x: point.x - radius, y: point.y + 0.1),
d: CGPoint(x: point.x + radius, y: point.y + 0.1),
radius1: radius,
radius2: radius,
rect: .zero
)
)
}
}
}
func maybeSetupArrow() {
if let start = self.arrowStart, let direction = self.arrowDirection {
let arrowLeftPath = UIBezierPath()
arrowLeftPath.move(to: start)
arrowLeftPath.addLine(to: start.pointAt(distance: self.renderArrowLength, angle: direction - 0.45))
let arrowRightPath = UIBezierPath()
arrowRightPath.move(to: start)
arrowRightPath.addLine(to: start.pointAt(distance: self.renderArrowLength, angle: direction + 0.45))
self.arrowLeftPath = arrowLeftPath
self.arrowRightPath = arrowRightPath
}
}
func draw(in context: CGContext, size: CGSize) {
guard !self.segments.isEmpty else {
return
@ -238,14 +366,36 @@ final class PenTool: DrawingElement, Codable {
context.saveGState()
if self.isEraser {
context.setBlendMode(.clear)
} else {
context.setAlpha(self.color.alpha)
context.setBlendMode(.copy)
}
context.translateBy(x: self.translation.x, y: self.translation.y)
context.setShouldAntialias(true)
self.drawSegments(in: context, upTo: self.segments.count)
if self.isBlur, let blurredImage = self.blurredImage {
let maskContext = DrawingContext(size: size, scale: 0.5, clear: true)
maskContext?.withFlippedContext { maskContext in
self.drawSegments(in: maskContext, from: 0, upTo: self.segments.count)
}
if let maskImage = maskContext?.generateImage()?.cgImage, let blurredImage = blurredImage.cgImage {
context.clip(to: CGRect(origin: .zero, size: size), mask: maskImage)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.draw(blurredImage, in: CGRect(origin: .zero, size: size))
}
} else {
self.drawSegments(in: context, from: 0, upTo: self.segments.count)
}
if let arrowLeftPath, let arrowRightPath {
context.setStrokeColor(self.color.toCGColor())
context.setStrokeColor(self.renderColor.cgColor)
context.setLineWidth(self.renderArrowLineWidth)
context.setLineCap(.round)
@ -257,9 +407,11 @@ final class PenTool: DrawingElement, Codable {
}
context.restoreGState()
self.segmentPaths = [:]
}
private struct Segment {
private struct Segment: Codable {
let a: CGPoint
let b: CGPoint
let c: CGPoint
@ -267,6 +419,48 @@ final class PenTool: DrawingElement, Codable {
let radius1: CGFloat
let radius2: CGFloat
let rect: CGRect
init(a: CGPoint, b: CGPoint, c: CGPoint, d: CGPoint, radius1: CGFloat, radius2: CGFloat, rect: CGRect) {
self.a = a
self.b = b
self.c = c
self.d = d
self.radius1 = radius1
self.radius2 = radius2
self.rect = rect
}
private enum CodingKeys: String, CodingKey {
case a
case b
case c
case d
case radius1
case radius2
case rect
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.a = try container.decode(CGPoint.self, forKey: .a)
self.b = try container.decode(CGPoint.self, forKey: .b)
self.c = try container.decode(CGPoint.self, forKey: .c)
self.d = try container.decode(CGPoint.self, forKey: .d)
self.radius1 = try container.decode(CGFloat.self, forKey: .radius1)
self.radius2 = try container.decode(CGFloat.self, forKey: .radius2)
self.rect = try container.decode(CGRect.self, forKey: .rect)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.a, forKey: .a)
try container.encode(self.b, forKey: .b)
try container.encode(self.c, forKey: .c)
try container.encode(self.d, forKey: .d)
try container.encode(self.radius1, forKey: .radius1)
try container.encode(self.radius2, forKey: .radius2)
try container.encode(self.rect, forKey: .rect)
}
}
private struct Point {
@ -291,57 +485,71 @@ final class PenTool: DrawingElement, Codable {
private func append(point: Point) -> CGRect? {
self.points.append(point)
guard self.points.count > 2 else { return .null }
guard self.points.count > 2 else { return nil }
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(
var newSmoothPoints = self.smoothPoints(
fromPoint0: point0,
point1: point1,
point2: point2
)
let lastOldSmoothPoint = smoothPoints.last
let previousSmoothPoint = self.smoothPoints.last
self.smoothPoints.append(contentsOf: newSmoothPoints)
guard self.smoothPoints.count > 1 else {
return nil
}
let newSegments: ([Segment], CGRect) = {
guard let lastOldSmoothPoint = lastOldSmoothPoint else {
return segments(fromSmoothPoints: newSmoothPoints)
}
return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints)
}()
self.segments.append(contentsOf: newSegments.0)
if let previousSmoothPoint = previousSmoothPoint {
newSmoothPoints.insert(previousSmoothPoint, at: 0)
}
let (nextSegments, rect) = self.segments(fromSmoothPoints: newSmoothPoints)
self.segments.append(contentsOf: nextSegments)
for i in self.segments.count - nextSegments.count ..< self.segments.count {
let segment = self.segments[i]
let path = self.pathForSegment(segment)
self.segmentPaths[i] = path
}
return newSegments.1
return rect
}
private 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 midPoint1 = CGPoint(
x: (point0.position.x + point1.position.x) * 0.5,
y: (point0.position.y + point1.position.y) * 0.5
)
let midPoint2 = CGPoint(
x: (point1.position.x + point2.position.x) * 0.5,
y: (point1.position.y + point2.position.y) * 0.5
)
let segmentDistance: CGFloat = 3.0
let midWidth1 = (point0.width + point1.width) * 0.5
let midWidth2 = (point1.width + point2.width) * 0.5
let segmentDistance: CGFloat = 6.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 pX = midPoint1.x * pow(1 - t, 2) + point1.position.x * 2.0 * (1 - t) * t + midPoint2.x * t * t
let pY = midPoint1.y * pow(1 - t, 2) + point1.position.y * 2.0 * (1 - t) * t + midPoint2.y * t * t
let w = midWidth1 * pow(1 - t, 2) + point1.width * 2.0 * (1 - t) * t + midWidth2 * t * t
smoothPoints.append(Point(position: CGPoint(x: pX, y: pY), width: w))
}
let finalPoint = Point(position: midPoint2, width: (point1.width + point2.width) * 0.5)
smoothPoints.append(finalPoint)
smoothPoints.append(Point(position: midPoint2, width: midWidth2))
return smoothPoints
}
@ -372,7 +580,7 @@ final class PenTool: DrawingElement, Codable {
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
}
private func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) {
private func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect?) {
var segments: [Segment] = []
var updateRect = CGRect.null
for i in 1 ..< smoothPoints.count {
@ -380,7 +588,10 @@ final class PenTool: DrawingElement, Codable {
let previousWidth = smoothPoints[i - 1].width
let currentPoint = smoothPoints[i].position
let currentWidth = smoothPoints[i].width
let direction = currentPoint - previousPoint
let direction = CGPoint(
x: currentPoint.x - previousPoint.x,
y: currentPoint.y - previousPoint.y
)
guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else {
continue
@ -389,25 +600,55 @@ final class PenTool: DrawingElement, Codable {
var perpendicular = CGPoint(x: -direction.y, y: direction.x)
let length = perpendicular.length
if length > 0.0 {
perpendicular = perpendicular / length
perpendicular = CGPoint(
x: perpendicular.x / length,
y: perpendicular.y / 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 a = CGPoint(
x: previousPoint.x + perpendicular.x * previousWidth / 2.0,
y: previousPoint.y + perpendicular.y * previousWidth / 2.0
)
let b = CGPoint(
x: previousPoint.x - perpendicular.x * previousWidth / 2.0,
y: previousPoint.y - perpendicular.y * previousWidth / 2.0
)
let c = CGPoint(
x: currentPoint.x + perpendicular.x * currentWidth / 2.0,
y: currentPoint.y + perpendicular.y * currentWidth / 2.0
)
let d = CGPoint(
x: currentPoint.x - perpendicular.x * currentWidth / 2.0,
y: currentPoint.y - perpendicular.y * currentWidth / 2.0
)
let 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 abCenter = CGPoint(
x: (a.x + b.x) / 2.0,
y: (a.y + b.y) / 2.0
)
let abRadius = CGPoint(
x: abCenter.x - b.x,
y: abCenter.y - b.y
)
let ab = CGPoint(
x: abCenter.x - abRadius.y,
y: abCenter.y + abRadius.x
)
let cdCenter = CGPoint(
x: (c.x + d.x) / 2.0,
y: (c.y + d.y) / 2.0
)
let cdRadius = CGPoint(
x: cdCenter.x - c.x,
y: cdCenter.y - c.y
)
let cd = CGPoint(
x: cdCenter.x - cdRadius.y,
y: cdCenter.y + cdRadius.x
)
let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x)
let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y)
let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x)
@ -415,50 +656,72 @@ final class PenTool: DrawingElement, Codable {
let segmentRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
updateRect = updateRect.union(segmentRect)
segments.append(Segment(a: a, b: b, c: c, d: d, radius1: previousWidth / 2.0, radius2: currentWidth / 2.0, rect: segmentRect))
let segment = Segment(a: a, b: b, c: c, d: d, radius1: previousWidth / 2.0, radius2: currentWidth / 2.0, rect: segmentRect)
segments.append(segment)
}
return (segments, updateRect)
return (segments, !updateRect.isNull ? updateRect : nil)
}
private func drawSegments(in context: CGContext, upTo: Int) {
context.setStrokeColor(self.color.toCGColor())
context.setFillColor(self.color.toCGColor())
private var segmentPaths: [Int: CGPath] = [:]
private func pathForSegment(_ segment: Segment) -> CGPath {
let path = CGMutablePath()
path.move(to: segment.b)
for i in 0 ..< upTo {
let abStartAngle = atan2(
segment.b.y - segment.a.y,
segment.b.x - segment.a.x
)
path.addArc(
center: CGPoint(
x: (segment.a.x + segment.b.x) / 2,
y: (segment.a.y + segment.b.y) / 2
),
radius: segment.radius1,
startAngle: abStartAngle,
endAngle: abStartAngle + .pi,
clockwise: true
)
path.addLine(to: segment.c)
let cdStartAngle = atan2(
segment.c.y - segment.d.y,
segment.c.x - segment.d.x
)
path.addArc(
center: CGPoint(
x: (segment.c.x + segment.d.x) / 2,
y: (segment.c.y + segment.d.y) / 2
),
radius: segment.radius2,
startAngle: cdStartAngle,
endAngle: cdStartAngle + .pi,
clockwise: true
)
path.closeSubpath()
return path
}
private func drawSegments(in context: CGContext, from: Int, upTo: Int) {
// context.setStrokeColor(self.renderColor.cgColor)
context.setFillColor(self.renderColor.cgColor)
for i in from ..< upTo {
let segment = self.segments[i]
context.beginPath()
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.radius1,
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.radius2,
startAngle: cdStartAngle,
endAngle: cdStartAngle + .pi,
clockwise: true
)
context.closePath()
var segmentPath: CGPath
if let current = self.segmentPaths[i] {
segmentPath = current
} else {
let path = self.pathForSegment(segment)
self.segmentPaths[i] = path
segmentPath = path
}
context.addPath(segmentPath)
context.fillPath()
context.strokePath()
// context.strokePath()
}
}
}

View File

@ -13,6 +13,8 @@ import EntityKeyboard
import PagerComponent
import FeaturedStickersScreen
import TelegramNotices
import ChatEntityKeyboardInputNode
import ContextUI
struct StickerPickerInputData: Equatable {
var emoji: EmojiPagerContentComponent
@ -440,6 +442,18 @@ class StickerPickerScreen: ViewController {
hideBackground: true
)
var stickerPeekBehavior: EmojiContentPeekBehaviorImpl?
if let controller = self.controller {
stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
context: controller.context,
interaction: nil,
chatPeerId: nil,
present: { [weak controller] c, a in
controller?.presentInGlobalOverlay(c, with: a)
}
)
}
content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] _, item, _, _, _, _ in
guard let strongSelf = self, let file = item.itemFile else {
@ -541,7 +555,7 @@ class StickerPickerScreen: ViewController {
updateSearchQuery: { _, _ in
},
chatPeerId: nil,
peekBehavior: nil,
peekBehavior: stickerPeekBehavior,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
@ -594,6 +608,10 @@ class StickerPickerScreen: ViewController {
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
if otherGestureRecognizer is PagerPanGestureRecognizer {
return false
} else if otherGestureRecognizer is UIPanGestureRecognizer, let scrollView = otherGestureRecognizer.view, scrollView.frame.width > scrollView.frame.height {
return false
} else if otherGestureRecognizer is PeekControllerGestureRecognizer {
return false
}
return true
}

View File

@ -760,6 +760,8 @@ final class TextSizeSliderComponent: Component {
init() {
super.init(frame: CGRect())
self.isExclusiveTouch = true
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
pressGestureRecognizer.minimumPressDuration = 0.01
pressGestureRecognizer.delegate = self

View File

@ -17,6 +17,7 @@ private class ToolView: UIView, UIGestureRecognizerDelegate {
var isVisible = false
private var currentSize: CGFloat?
private let shadow: SimpleLayer
private let tip: UIImageView
private let background: SimpleLayer
private let band: SimpleGradientLayer
@ -27,6 +28,8 @@ private class ToolView: UIView, UIGestureRecognizerDelegate {
init(type: DrawingToolState.Key) {
self.type = type
self.shadow = SimpleLayer()
self.tip = UIImageView()
self.tip.isUserInteractionEnabled = false
@ -41,6 +44,7 @@ private class ToolView: UIView, UIGestureRecognizerDelegate {
let backgroundImage: UIImage?
let tipImage: UIImage?
let shadowImage: UIImage?
var tipAbove = true
var hasBand = true
@ -49,39 +53,49 @@ private class ToolView: UIView, UIGestureRecognizerDelegate {
case .pen:
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolPen")
tipImage = UIImage(bundleImageName: "Media Editor/ToolPenTip")?.withRenderingMode(.alwaysTemplate)
shadowImage = UIImage(bundleImageName: "Media Editor/ToolPenShadow")
case .arrow:
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolArrow")
tipImage = UIImage(bundleImageName: "Media Editor/ToolArrowTip")?.withRenderingMode(.alwaysTemplate)
shadowImage = UIImage(bundleImageName: "Media Editor/ToolArrowShadow")
case .marker:
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolMarker")
tipImage = UIImage(bundleImageName: "Media Editor/ToolMarkerTip")?.withRenderingMode(.alwaysTemplate)
tipAbove = false
shadowImage = UIImage(bundleImageName: "Media Editor/ToolMarkerShadow")
case .neon:
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolNeon")
tipImage = UIImage(bundleImageName: "Media Editor/ToolNeonTip")?.withRenderingMode(.alwaysTemplate)
tipAbove = false
shadowImage = UIImage(bundleImageName: "Media Editor/ToolNeonShadow")
case .eraser:
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolEraser")
tipImage = nil
hasBand = false
shadowImage = UIImage(bundleImageName: "Media Editor/ToolEraserShadow")
case .blur:
backgroundImage = UIImage(bundleImageName: "Media Editor/ToolBlur")
tipImage = UIImage(bundleImageName: "Media Editor/ToolBlurTip")
tipAbove = false
hasBand = false
shadowImage = UIImage(bundleImageName: "Media Editor/ToolBlurShadow")
}
self.tip.image = tipImage
self.background.contents = backgroundImage?.cgImage
self.shadow.contents = shadowImage?.cgImage
super.init(frame: CGRect(origin: .zero, size: toolSize))
self.tip.frame = CGRect(origin: .zero, size: toolSize)
self.shadow.frame = CGRect(origin: .zero, size: toolSize).insetBy(dx: -4.0, dy: 0.0)
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.layer.addSublayer(self.shadow)
if tipAbove {
self.layer.addSublayer(self.background)
self.addSubview(self.tip)
@ -107,7 +121,7 @@ private class ToolView: UIView, UIGestureRecognizerDelegate {
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer {
if self.isSelected && !self.isToolFocused {
if self.isSelected {
return true
} else {
return false
@ -164,12 +178,12 @@ private class ToolView: UIView, UIGestureRecognizerDelegate {
}
func update(state: DrawingToolState) {
self.currentSize = state.size
if let _ = self.tip.image {
let color = state.color?.toUIColor()
self.tip.tintColor = color
self.currentSize = state.size
guard let color = color else {
return
}
@ -234,7 +248,7 @@ final class ToolsComponent: Component {
}
public final class View: UIView, ComponentTaggedView {
private let toolViews: [ToolView]
private var toolViews: [ToolView] = []
private let maskImageView: UIImageView
private var isToolFocused: Bool?
@ -251,20 +265,12 @@ final class ToolsComponent: Component {
}
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) {
@ -304,6 +310,18 @@ final class ToolsComponent: Component {
func update(component: ToolsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
if self.toolViews.isEmpty {
var toolViews: [ToolView] = []
for type in DrawingToolState.Key.allCases {
if component.state.tools.contains(where: { $0.key == type }) {
let toolView = ToolView(type: type)
toolViews.append(toolView)
self.addSubview(toolView)
}
}
self.toolViews = toolViews
}
let wasFocused = self.isToolFocused
self.isToolFocused = component.isFocused
@ -455,64 +473,6 @@ final class ToolsComponent: Component {
}
}
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

View File

@ -56,6 +56,7 @@ typedef enum {
- (void)lockPortrait;
- (void)unlockPortrait;
- (void)disableInteractiveKeyboardGesture;
- (TGNavigationBarPallete *)navigationBarPallete;
- (TGMenuSheetPallete *)menuSheetPallete;

View File

@ -103,6 +103,7 @@
@property (nonatomic, copy) void(^requestDismiss)(void);
@property (nonatomic, copy) void(^requestApply)(void);
@property (nonatomic, copy) UIImage *(^getCurrentImage)(void);
@property (nonatomic, copy) void(^updateVideoPlayback)(bool);
- (TGPaintingData *)generateResultData;
- (void)animateOut:(void(^)(void))completion;
@ -142,7 +143,7 @@
- (UIView<TGPhotoSolidRoundedButtonView> *)solidRoundedButton:(NSString *)title action:(void(^)(void))action;
- (id<TGPhotoDrawingAdapter>)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize isAvatar:(bool)isAvatar;
- (id<TGPhotoDrawingAdapter>)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize isVideo:(bool)isVideo isAvatar:(bool)isAvatar;
- (UIView<TGPhotoDrawingEntitiesView> *)drawingEntitiesViewWithSize:(CGSize)size;

View File

@ -495,6 +495,8 @@
} else {
[(TGMediaAssetsController *)strongSelf.navigationController completeWithAvatarImage:resultImage];
}
commit();
};
controller.didFinishEditingVideo = ^(AVAsset *asset, id<TGMediaEditAdjustments> adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) {
if (!hasChanges)

View File

@ -440,6 +440,8 @@
[videoItemView setScrubbingPanelApperanceLocked:false];
[videoItemView presentScrubbingPanelAfterReload:hasChanges];
}
commit();
};
controller.didFinishRenderingFullSizeImage = ^(UIImage *image)

View File

@ -30,7 +30,7 @@
const CGFloat TGPhotoPaintTopPanelSize = 44.0f;
const CGFloat TGPhotoPaintBottomPanelSize = 79.0f;
const CGSize TGPhotoPaintingLightMaxSize = { 1280.0f, 1280.0f };
const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
const CGSize TGPhotoPaintingMaxSize = { 1920.0f, 1920.0f };
@interface TGPhotoDrawingController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
{
@ -85,7 +85,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
_stickersContext = stickersContext;
CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]);
_drawingAdapter = [_stickersContext drawingAdapter:size originalSize:photoEditor.originalSize isAvatar:isAvatar];
_drawingAdapter = [_stickersContext drawingAdapter:size originalSize:photoEditor.originalSize isVideo:photoEditor.forVideo isAvatar:isAvatar];
_interfaceController = (UIViewController<TGPhotoDrawingInterfaceController> *)_drawingAdapter.interfaceController;
__weak TGPhotoDrawingController *weakSelf = self;
@ -108,6 +108,13 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
return [strongSelf.photoEditor currentResultImage];
};
_interfaceController.updateVideoPlayback = ^(bool play) {
__strong TGPhotoDrawingController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf.controlVideoPlayback(play);
};
self.photoEditor = photoEditor;
self.previewView = previewView;
@ -474,6 +481,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
- (void)transitionIn {
[_context lockPortrait];
[_context disableInteractiveKeyboardGesture];
// if (self.presentedForAvatarCreation) {
// _drawingView.hidden = true;
// }
@ -900,6 +908,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
- (CGPoint)entityCenterPoint
{
//return [_scrollView convertPoint:TGPaintCenterOfRect(_scrollView.bounds) toView:_entitiesView];
return [_previewView convertPoint:TGPaintCenterOfRect(_previewView.bounds) toView:_entitiesView];
}

View File

@ -485,7 +485,7 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender
public func entities(for time: CMTime, fps: Int, size: CGSize, completion: (([CIImage]?) -> Void)!) {
let entities = self.entities
let maxSide = max(size.width, size.height)
let paintingScale = maxSide / 2560.0
let paintingScale = maxSide / 1920.0
self.queue.async {
if entities.isEmpty {
@ -607,8 +607,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
let contentWrapperView: UIView!
let interfaceController: TGPhotoDrawingInterfaceController!
init(context: AccountContext, size: CGSize, originalSize: CGSize, isAvatar: Bool) {
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isAvatar: isAvatar)
init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool) {
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar)
self.interfaceController = interfaceController
self.drawingView = interfaceController.drawingView
self.drawingEntitiesView = interfaceController.entitiesView
@ -619,8 +619,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
}
}
public func drawingAdapter(_ size: CGSize, originalSize: CGSize, isAvatar: Bool) -> TGPhotoDrawingAdapter! {
return LegacyDrawingAdapter(context: self.context, size: size, originalSize: originalSize, isAvatar: isAvatar)
public func drawingAdapter(_ size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool) -> TGPhotoDrawingAdapter! {
return LegacyDrawingAdapter(context: self.context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar)
}
public func solidRoundedButton(_ title: String!, action: (() -> Void)!) -> (UIView & TGPhotoSolidRoundedButtonView)! {

View File

@ -121,6 +121,13 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
}
}
public func disableInteractiveKeyboardGesture() {
if let controller = self.controller as? LegacyController {
controller.view.disablesInteractiveModalDismiss = true
controller.view.disablesInteractiveKeyboardGestureRecognizer = true
}
}
public func keyCommandController() -> TGKeyCommandController! {
return nil

View File

@ -342,9 +342,10 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account
for photo in photos {
let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count))
if result.isEmpty, let first = initialEntries.first {
var videoRepresentations: [VideoRepresentationWithReference] = photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) })
if videoRepresentations.isEmpty {
videoRepresentations = first.videoRepresentations
var videoRepresentations: [VideoRepresentationWithReference] = first.videoRepresentations
let isPersonal = first.representations.first?.representation.isPersonal == true
if videoRepresentations.isEmpty, !isPersonal {
videoRepresentations = photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) })
}
result.append(.image(photo.image.imageId, photo.image.reference, first.representations, videoRepresentations, peer, secondEntry != nil ? 0 : photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false))
} else {

View File

@ -76,6 +76,11 @@ public struct SegmentedControlItem: Equatable {
}
private class SegmentedControlItemNode: HighlightTrackingButtonNode {
override func didLoad() {
super.didLoad()
self.view.isExclusiveTouch = true
}
}
public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDelegate {

View File

@ -700,9 +700,12 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
}
let exceptionsInfo: String
if case .profilePhoto = kind {
if case .nobody = state.setting {
switch state.setting {
case .nobody:
exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideAddInfo
} else {
case .contacts:
exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideBothInfo
case .everybody:
exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideInfo
}
} else {

View File

@ -247,6 +247,14 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
return nil
}
}
} else if let customPeerPhotoMode, case .custom = customPeerPhotoMode {
transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in
if let cachedPeerData = cachedPeerData as? CachedUserData {
return cachedPeerData.withUpdatedPersonalPhoto(.known(image))
} else {
return nil
}
}
}
}
return (.complete(representations), photoResult.resource, videoResult?.resource)
@ -376,6 +384,14 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
return nil
}
}
} else if let customPeerPhotoMode, case .custom = customPeerPhotoMode {
transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in
if let cachedPeerData = cachedPeerData as? CachedUserData {
return cachedPeerData.withUpdatedPersonalPhoto(.known(nil))
} else {
return nil
}
}
}
return .complete([])
} |> mapError { _ -> UploadPeerPhotoError in }

View File

@ -889,8 +889,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
if let controllerInteraction = controllerInteraction {
stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
context: self.context,
controllerInteraction: controllerInteraction,
chatPeerId: chatPeerId
interaction: EmojiContentPeekBehaviorImpl.Interaction(sendSticker: controllerInteraction.sendSticker, presentController: controllerInteraction.presentController, presentGlobalOverlayController: controllerInteraction.presentGlobalOverlayController, navigationController: controllerInteraction.navigationController),
chatPeerId: chatPeerId,
present: { c, a in
controllerInteraction.presentGlobalOverlayController(c, a)
}
)
}
self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
@ -1974,21 +1977,37 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
}
}
private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
public class Interaction {
public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
public let presentController: (ViewController, Any?) -> Void
public let presentGlobalOverlayController: (ViewController, Any?) -> Void
public let navigationController: () -> NavigationController?
public init(sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?) {
self.sendSticker = sendSticker
self.presentController = presentController
self.presentGlobalOverlayController = presentGlobalOverlayController
self.navigationController = navigationController
}
}
private let context: AccountContext
private let controllerInteraction: ChatControllerInteraction
private let interaction: Interaction?
private let chatPeerId: EnginePeer.Id?
private let present: (ViewController, Any?) -> Void
private var peekRecognizer: PeekControllerGestureRecognizer?
private weak var peekController: PeekController?
init(context: AccountContext, controllerInteraction: ChatControllerInteraction, chatPeerId: EnginePeer.Id?) {
public init(context: AccountContext, interaction: Interaction?, chatPeerId: EnginePeer.Id?, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.controllerInteraction = controllerInteraction
self.interaction = interaction
self.chatPeerId = chatPeerId
self.present = present
}
func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?) {
public func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?) {
if self.peekRecognizer == nil {
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self, weak view] point in
guard let strongSelf = self else {
@ -2021,125 +2040,116 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
return nil
}
var menuItems: [ContextMenuItem] = []
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in
guard let strongSelf = self else {
return
}
let _ = strongSelf.controllerInteraction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets)
}
let isLocked = file.isPremiumSticker && !hasPremium
if let chatPeerId = strongSelf.chatPeerId, !isLocked {
if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat {
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor)
if let interaction = strongSelf.interaction {
let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in
let _ = interaction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets)
}
if let chatPeerId = strongSelf.chatPeerId, !isLocked {
if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat {
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
if let strongSelf = self, let peekController = strongSelf.peekController {
if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode {
sendSticker(.standalone(media: file), true, false, nil, false, animationNode.view, animationNode.bounds, nil)
} else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode {
sendSticker(.standalone(media: file), true, false, nil, false, imageNode.view, imageNode.bounds, nil)
}
}
f(.default)
})))
}
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
if let strongSelf = self, let peekController = strongSelf.peekController {
if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode {
sendSticker(.standalone(media: file), true, false, nil, false, animationNode.view, animationNode.bounds, nil)
let _ = sendSticker(.standalone(media: file), false, true, nil, false, animationNode.view, animationNode.bounds, nil)
} else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode {
sendSticker(.standalone(media: file), true, false, nil, false, imageNode.view, imageNode.bounds, nil)
let _ = sendSticker(.standalone(media: file), false, true, nil, false, imageNode.view, imageNode.bounds, nil)
}
}
f(.default)
})))
}
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
if let strongSelf = self, let peekController = strongSelf.peekController {
if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode {
let _ = sendSticker(.standalone(media: file), false, true, nil, false, animationNode.view, animationNode.bounds, nil)
} else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode {
let _ = sendSticker(.standalone(media: file), false, true, nil, false, imageNode.view, imageNode.bounds, nil)
}
}
f(.default)
})))
}
menuItems.append(
.action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred)
|> deliverOnMainQueue).start(next: { result in
menuItems.append(
.action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred)
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
text = presentationData.strings.Premium_MaxFavedStickersFinalText
} else {
text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in
if case .info = action {
let controller = PremiumIntroScreen(context: context, source: .savedStickers)
interaction.navigationController()?.pushViewController(controller)
return true
}
return false
}), nil)
}
})
}))
)
menuItems.append(
.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
guard let strongSelf = self else {
return
}
switch result {
case .generic:
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
text = presentationData.strings.Premium_MaxFavedStickersFinalText
} else {
text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in
guard let strongSelf = self else {
return false
}
if case .info = action {
let controller = PremiumIntroScreen(context: context, source: .savedStickers)
strongSelf.controllerInteraction.navigationController()?.pushViewController(controller)
return true
}
return false
}), nil)
}
})
}))
)
menuItems.append(
.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
guard let strongSelf = self else {
return
}
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _):
if let packReference = packReference {
let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceView, sourceRect in
sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil)
return true
})
strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true)
strongSelf.controllerInteraction.presentController(controller, nil)
switch attribute {
case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _):
if let packReference = packReference {
let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in
sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil)
return true
})
interaction.navigationController()?.view.window?.endEditing(true)
interaction.presentController(controller, nil)
}
break loop
default:
break
}
break loop
default:
break
}
}
}))
)
}))
)
}
guard let view = view else {
return nil
}
return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(account: context.account, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked && !isStarred, menu: menuItems, openPremiumIntro: {
guard let strongSelf = self else {
guard let strongSelf = self, let interaction = strongSelf.interaction else {
return
}
let controller = PremiumIntroScreen(context: context, source: .stickers)
strongSelf.controllerInteraction.navigationController()?.pushViewController(controller)
interaction.navigationController()?.pushViewController(controller)
}))
}
}, present: { [weak self] content, sourceView, sourceRect in
@ -2157,7 +2167,7 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
self?.simulateUpdateLayout(isVisible: !visible)
}*/
strongSelf.peekController = controller
strongSelf.controllerInteraction.presentGlobalOverlayController(controller, nil)
strongSelf.present(controller, nil)
return controller
}, updateContent: { [weak self] content in
guard let strongSelf = self else {

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Arrow.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Blur.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Eraser.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Brush.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Neon.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Pen.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -122,9 +122,25 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
guard let strongSelf = self else {
return
}
let cachedPeerData = peerView.cachedData
if let cachedPeerData = cachedPeerData as? CachedUserData, case let .known(maybePhoto) = cachedPeerData.photo {
if let photo = maybePhoto, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
let cachedPeerData = peerView.cachedData as? CachedUserData
var personalPhoto: TelegramMediaImage?
var profilePhoto: TelegramMediaImage?
var isKnown = false
if let cachedPeerData = cachedPeerData {
if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto {
personalPhoto = maybePersonalPhoto
isKnown = true
}
if case let .known(maybePhoto) = cachedPeerData.photo {
profilePhoto = maybePhoto
isKnown = true
}
}
if isKnown {
let photo = personalPhoto ?? profilePhoto
if let photo = photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, "header"), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false)
@ -157,7 +173,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer()
strongSelf.hierarchyTrackingLayer = nil
}
strongSelf.updateVideoVisibility()
} else {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start()

View File

@ -1404,7 +1404,17 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
removeText = user.photo.first?.hasVideo == true ? presentationData.strings.UserInfo_RemoveCustomVideo : presentationData.strings.UserInfo_RemoveCustomPhoto
}
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemReset, text: removeText, color: .accent, icon: nil, iconSignal: peerAvatarCompleteImage(account: context.account, peer: EnginePeer(user), forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0)), action: {
let imageSignal: Signal<UIImage?, NoError>
if let representation, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(user), authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: 28.0, height: 28.0)) {
imageSignal = signal
|> map { data -> UIImage? in
return data?.0
}
} else {
imageSignal = peerAvatarCompleteImage(account: context.account, peer: EnginePeer(user), forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0))
}
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemReset, text: removeText, color: .accent, icon: nil, iconSignal: imageSignal, action: {
interaction.resetCustomPhoto()
}))
}
@ -6872,6 +6882,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
(strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
case .custom:
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).start()
case .suggest:
if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in
@ -7076,6 +7088,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
(strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
case .custom:
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).start()
case .suggest:
if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in