Various fixes
@ -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";
|
||||
|
||||
|
@ -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
|
||||
|
@ -91,6 +91,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode",
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/FastBlur:FastBlur",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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] = []
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -56,6 +56,7 @@ typedef enum {
|
||||
|
||||
- (void)lockPortrait;
|
||||
- (void)unlockPortrait;
|
||||
- (void)disableInteractiveKeyboardGesture;
|
||||
|
||||
- (TGNavigationBarPallete *)navigationBarPallete;
|
||||
- (TGMenuSheetPallete *)menuSheetPallete;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -440,6 +440,8 @@
|
||||
[videoItemView setScrubbingPanelApperanceLocked:false];
|
||||
[videoItemView presentScrubbingPanelAfterReload:hasChanges];
|
||||
}
|
||||
|
||||
commit();
|
||||
};
|
||||
|
||||
controller.didFinishRenderingFullSizeImage = ^(UIImage *image)
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -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)! {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 }
|
||||
|
@ -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 {
|
||||
|
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Arrow.png
vendored
Normal file
After Width: | Height: | Size: 26 KiB |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Blur.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
21
submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Eraser.png
vendored
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Brush.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
21
submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Neon.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
21
submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Pen.png
vendored
Normal file
After Width: | Height: | Size: 23 KiB |
@ -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()
|
||||
|
@ -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
|
||||
|