Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "DrawingUIMetalResources",
srcs = glob([
"MetalResources/**/*.*",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "DrawingUIBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.DrawingUI</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>PremiumUI</string>
"""
)
apple_resource_bundle(
name = "DrawingUIBundle",
infoplists = [
":DrawingUIBundleInfoPlist",
],
resources = [
":DrawingUIMetalResources",
],
)
filegroup(
name = "DrawingUIResources",
srcs = glob([
"Resources/**/*",
], exclude = ["Resources/**/.*"]),
visibility = ["//visibility:public"],
)
swift_library(
name = "DrawingUI",
module_name = "DrawingUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":DrawingUIBundle",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/AccountContext:AccountContext",
"//submodules/LegacyUI:LegacyUI",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/HexColor:HexColor",
"//submodules/ContextUI:ContextUI",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/SheetComponent:SheetComponent",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/StickerResources:StickerResources",
"//submodules/ImageBlur:ImageBlur",
"//submodules/TextFormat:TextFormat",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/FastBlur:FastBlur",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/LottieComponentResourceContent",
"//submodules/ImageTransparency",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramUniversalVideoContent",
"//submodules/TelegramUI/Components/CameraButtonComponent",
"//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/Camera",
"//submodules/TelegramUI/Components/DustEffect",
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
"//submodules/TelegramUI/Components/StickerPickerScreen",
"//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,63 @@
#include <metal_stdlib>
using namespace metal;
struct Vertex {
float4 position [[position]];
float2 tex_coord;
};
struct Uniforms {
float4x4 scaleMatrix;
};
struct Point {
float4 position [[position]];
float4 color;
float angle;
float size [[point_size]];
};
vertex Vertex vertex_render_target(constant Vertex *vertexes [[ buffer(0) ]],
constant Uniforms &uniforms [[ buffer(1) ]],
uint vid [[vertex_id]])
{
Vertex out = vertexes[vid];
out.position = uniforms.scaleMatrix * out.position;
return out;
};
fragment float4 fragment_render_target(Vertex vertex_data [[ stage_in ]],
texture2d<float> tex2d [[ texture(0) ]])
{
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
float4 color = float4(tex2d.sample(textureSampler, vertex_data.tex_coord));
return color;
};
float2 transformPointCoord(float2 pointCoord, float a, float2 anchor) {
float2 point20 = pointCoord - anchor;
float x = point20.x * cos(a) - point20.y * sin(a);
float y = point20.x * sin(a) + point20.y * cos(a);
return float2(x, y) + anchor;
}
vertex Point vertex_point_func(constant Point *points [[ buffer(0) ]],
constant Uniforms &uniforms [[ buffer(1) ]],
uint vid [[ vertex_id ]])
{
Point out = points[vid];
float2 pos = float2(out.position.x, out.position.y);
out.position = uniforms.scaleMatrix * float4(pos, 0, 1);
out.size = out.size;
return out;
};
fragment float4 fragment_point_func(Point point_data [[ stage_in ]],
texture2d<float> tex2d [[ texture(0) ]],
float2 pointCoord [[ point_coord ]])
{
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
float2 tex_coord = transformPointCoord(pointCoord, point_data.angle, float2(0.5));
float4 color = float4(tex2d.sample(textureSampler, tex_coord));
return float4(point_data.color.rgb, color.a * point_data.color.a);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1 @@
{"name" : "Arrow", "points" : [[68,222],[70,220],[73,218],[75,217],[77,215],[80,213],[82,212],[84,210],[87,209],[89,208],[92,206],[95,204],[101,201],[106,198],[112,194],[118,191],[124,187],[127,186],[132,183],[138,181],[141,180],[146,178],[154,173],[159,171],[161,170],[166,167],[168,167],[171,166],[174,164],[177,162],[180,160],[182,158],[183,156],[181,154],[178,153],[171,153],[164,153],[160,153],[150,154],[147,155],[141,157],[137,158],[135,158],[137,158],[140,157],[143,156],[151,154],[160,152],[170,149],[179,147],[185,145],[192,144],[196,144],[198,144],[200,144],[201,147],[199,149],[194,157],[191,160],[186,167],[180,176],[177,179],[171,187],[169,189],[165,194],[164,196]]}
@@ -0,0 +1 @@
{"name" : "Circle", "points" : [[127,141],[124,140],[120,139],[118,139],[116,139],[111,140],[109,141],[104,144],[100,147],[96,152],[93,157],[90,163],[87,169],[85,175],[83,181],[82,190],[82,195],[83,200],[84,205],[88,213],[91,216],[96,219],[103,222],[108,224],[111,224],[120,224],[133,223],[142,222],[152,218],[160,214],[167,210],[173,204],[178,198],[179,196],[182,188],[182,177],[178,167],[170,150],[163,138],[152,130],[143,129],[140,131],[129,136],[126,139]]}
@@ -0,0 +1 @@
{"name" : "Rectangle", "points" : [[78,149],[78,153],[78,157],[78,160],[79,162],[79,164],[79,167],[79,169],[79,173],[79,178],[79,183],[80,189],[80,193],[80,198],[80,202],[81,208],[81,210],[81,216],[82,222],[82,224],[82,227],[83,229],[83,231],[85,230],[88,232],[90,233],[92,232],[94,233],[99,232],[102,233],[106,233],[109,234],[117,235],[123,236],[126,236],[135,237],[142,238],[145,238],[152,238],[154,239],[165,238],[174,237],[179,236],[186,235],[191,235],[195,233],[197,233],[200,233],[201,235],[201,233],[199,231],[198,226],[198,220],[196,207],[195,195],[195,181],[195,173],[195,163],[194,155],[192,145],[192,143],[192,138],[191,135],[191,133],[191,130],[190,128],[188,129],[186,129],[181,132],[173,131],[162,131],[151,132],[149,132],[138,132],[136,132],[122,131],[120,131],[109,130],[107,130],[90,132],[81,133],[76,133]]}
@@ -0,0 +1 @@
{"name" : "Star", "points" : [[75,250],[75,247],[77,244],[78,242],[79,239],[80,237],[82,234],[82,232],[84,229],[85,225],[87,222],[88,219],[89,216],[91,212],[92,208],[94,204],[95,201],[96,196],[97,194],[98,191],[100,185],[102,178],[104,173],[104,171],[105,164],[106,158],[107,156],[107,152],[108,145],[109,141],[110,139],[112,133],[113,131],[116,127],[117,125],[119,122],[121,121],[123,120],[125,122],[125,125],[127,130],[128,133],[131,143],[136,153],[140,163],[144,172],[145,175],[151,189],[156,201],[161,213],[166,225],[169,233],[171,236],[174,243],[177,247],[178,249],[179,251],[180,253],[180,255],[179,257],[177,257],[174,255],[169,250],[164,247],[160,245],[149,238],[138,230],[127,221],[124,220],[112,212],[110,210],[96,201],[84,195],[74,190],[64,182],[55,175],[51,172],[49,170],[51,169],[56,169],[66,169],[78,168],[92,166],[107,164],[123,161],[140,162],[156,162],[171,160],[173,160],[186,160],[195,160],[198,161],[203,163],[208,163],[206,164],[200,167],[187,172],[174,179],[172,181],[153,192],[137,201],[123,211],[112,220],[99,229],[90,237],[80,244],[73,250],[69,254],[69,252]]}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,308 @@
import Foundation
private func intersect(seg1: [CGPoint], seg2: [CGPoint]) -> Bool {
func ccw(_ seg1: CGPoint, _ seg2: CGPoint, _ seg3: CGPoint) -> Bool {
let ccw = ((seg3.y - seg1.y) * (seg2.x - seg1.x)) - ((seg2.y - seg1.y) * (seg3.x - seg1.x))
return ccw > 0 ? true : ccw < 0 ? false : true
}
let segment1 = seg1[0]
let segment2 = seg1[1]
let segment3 = seg2[0]
let segment4 = seg2[1]
return ccw(segment1, segment3, segment4) != ccw(segment2, segment3, segment4)
&& ccw(segment1, segment2, segment3) != ccw(segment1, segment2, segment4)
}
private func convex(points: [CGPoint]) -> [CGPoint] {
func cross(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
}
func upperTangent(_ points: [CGPoint]) -> [CGPoint] {
var lower: [CGPoint] = []
for point in points {
while lower.count >= 2 && (cross(lower[lower.count - 2], lower[lower.count - 1], point) <= 0) {
_ = lower.popLast()
}
lower.append(point)
}
_ = lower.popLast()
return lower
}
func lowerTangent(_ points: [CGPoint]) -> [CGPoint] {
let reversed = points.reversed()
var upper: [CGPoint] = []
for point in reversed {
while upper.count >= 2 && (cross(upper[upper.count - 2], upper[upper.count - 1], point) <= 0) {
_ = upper.popLast()
}
upper.append(point)
}
_ = upper.popLast()
return upper
}
var convex: [CGPoint] = []
convex.append(contentsOf: upperTangent(points))
convex.append(contentsOf: lowerTangent(points))
return convex
}
private class Grid {
var cells = [Int: [Int: [CGPoint]]]()
var cellSize: Double = 0
init(_ points: [CGPoint], _ cellSize: Double) {
self.cellSize = cellSize
for point in points {
let cellXY = point2CellXY(point)
let x = cellXY[0]
let y = cellXY[1]
if self.cells[x] == nil {
self.cells[x] = [Int: [CGPoint]]()
}
if self.cells[x]![y] == nil {
self.cells[x]![y] = [CGPoint]()
}
self.cells[x]![y]!.append(point)
}
}
func point2CellXY(_ point: CGPoint) -> [Int] {
let x = Int(point.x / self.cellSize)
let y = Int(point.y / self.cellSize)
return [x, y]
}
func extendBbox(_ bbox: [Double], _ scaleFactor: Double) -> [Double] {
return [
bbox[0] - (scaleFactor * self.cellSize),
bbox[1] - (scaleFactor * self.cellSize),
bbox[2] + (scaleFactor * self.cellSize),
bbox[3] + (scaleFactor * self.cellSize)
]
}
func removePoint(_ point: CGPoint) {
let cellXY = point2CellXY(point)
let cell = self.cells[cellXY[0]]![cellXY[1]]!
var pointIdxInCell = 0
for idx in 0 ..< cell.count {
if cell[idx].x == point.x && cell[idx].y == point.y {
pointIdxInCell = idx
break
}
}
self.cells[cellXY[0]]![cellXY[1]]!.remove(at: pointIdxInCell)
}
func rangePoints(_ bbox: [Double]) -> [CGPoint] {
let tlCellXY = point2CellXY(CGPoint(x: bbox[0], y: bbox[1]))
let brCellXY = point2CellXY(CGPoint(x: bbox[2], y: bbox[3]))
var points: [CGPoint] = []
for x in tlCellXY[0]..<brCellXY[0]+1 {
for y in tlCellXY[1]..<brCellXY[1]+1 {
points += cellPoints(x, y)
}
}
return points
}
func cellPoints(_ xAbs: Int, _ yOrd: Int) -> [CGPoint] {
if let x = self.cells[xAbs] {
if let y = x[yOrd] {
return y
}
}
return []
}
}
private let maxConcaveAngleCos = cos(90.0 / (180.0 / Double.pi))
private func filterDuplicates(_ pointSet: [CGPoint]) -> [CGPoint] {
let sortedSet = sortByX(pointSet)
return sortedSet.filter { (point: CGPoint) -> Bool in
let index = pointSet.firstIndex(where: {(idx: CGPoint) -> Bool in
return idx.x == point.x && idx.y == point.y
})
if index == 0 {
return true
} else {
let prevEl = pointSet[index! - 1]
if prevEl.x != point.x || prevEl.y != point.y {
return true
}
return false
}
}
}
private func sortByX(_ pointSet: [CGPoint]) -> [CGPoint] {
return pointSet.sorted(by: { (lhs, rhs) -> Bool in
if lhs.x == rhs.x {
return lhs.y < rhs.y
} else {
return lhs.x < rhs.x
}
})
}
private func squaredLength(_ a: CGPoint, _ b: CGPoint) -> Double {
return pow(b.x - a.x, 2) + pow(b.y - a.y, 2)
}
private func cosFunc(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double {
let aShifted = [a.x - o.x, a.y - o.y]
let bShifted = [b.x - o.x, b.y - o.y]
let sqALen = squaredLength(o, a)
let sqBLen = squaredLength(o, b)
let dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1]
return dot / sqrt(sqALen * sqBLen)
}
private func intersectFunc(_ segment: [CGPoint], _ pointSet: [CGPoint]) -> Bool {
for idx in 0..<pointSet.count - 1 {
let seg = [pointSet[idx], pointSet[idx + 1]]
if segment[0].x == seg[0].x && segment[0].y == seg[0].y ||
segment[0].x == seg[1].x && segment[0].y == seg[1].y {
continue
}
if intersect(seg1: segment, seg2: seg) {
return true
}
}
return false
}
private func occupiedAreaFunc(_ points: [CGPoint]) -> CGPoint {
var minX = Double.infinity
var minY = Double.infinity
var maxX = -Double.infinity
var maxY = -Double.infinity
for idx in 0 ..< points.reversed().count {
if points[idx].x < minX {
minX = points[idx].x
}
if points[idx].y < minY {
minY = points[idx].y
}
if points[idx].x > maxX {
maxX = points[idx].x
}
if points[idx].y > maxY {
maxY = points[idx].y
}
}
return CGPoint(x: maxX - minX, y: maxY - minY)
}
private func bBoxAroundFunc(_ edge: [CGPoint]) -> [Double] {
return [min(edge[0].x, edge[1].x),
min(edge[0].y, edge[1].y),
max(edge[0].x, edge[1].x),
max(edge[0].y, edge[1].y)]
}
private func midPointFunc(_ edge: [CGPoint], _ innerPoints: [CGPoint], _ convex: [CGPoint]) -> CGPoint? {
var point: CGPoint?
var angle1Cos = maxConcaveAngleCos
var angle2Cos = maxConcaveAngleCos
var a1Cos: Double = 0
var a2Cos: Double = 0
for innerPoint in innerPoints {
a1Cos = cosFunc(edge[0], edge[1], innerPoint)
a2Cos = cosFunc(edge[1], edge[0], innerPoint)
if a1Cos > angle1Cos &&
a2Cos > angle2Cos &&
!intersectFunc([edge[0], innerPoint], convex) &&
!intersectFunc([edge[1], innerPoint], convex) {
angle1Cos = a1Cos
angle2Cos = a2Cos
point = innerPoint
}
}
return point
}
private func concaveFunc(_ convex: inout [CGPoint], _ maxSqEdgeLen: Double, _ maxSearchArea: [Double], _ grid: Grid, _ edgeSkipList: inout [String: Bool]) -> [CGPoint] {
var edge: [CGPoint]
var keyInSkipList: String = ""
var scaleFactor: Double
var midPoint: CGPoint?
var bBoxAround: [Double]
var bBoxWidth: Double = 0
var bBoxHeight: Double = 0
var midPointInserted: Bool = false
for idx in 0..<convex.count - 1 {
edge = [convex[idx], convex[idx+1]]
keyInSkipList = edge[0].key.appending(", ").appending(edge[1].key)
scaleFactor = 0
bBoxAround = bBoxAroundFunc(edge)
if squaredLength(edge[0], edge[1]) < maxSqEdgeLen || edgeSkipList[keyInSkipList] == true {
continue
}
repeat {
bBoxAround = grid.extendBbox(bBoxAround, scaleFactor)
bBoxWidth = bBoxAround[2] - bBoxAround[0]
bBoxHeight = bBoxAround[3] - bBoxAround[1]
midPoint = midPointFunc(edge, grid.rangePoints(bBoxAround), convex)
scaleFactor += 1
} while midPoint == nil && (maxSearchArea[0] > bBoxWidth || maxSearchArea[1] > bBoxHeight)
if bBoxWidth >= maxSearchArea[0] && bBoxHeight >= maxSearchArea[1] {
edgeSkipList[keyInSkipList] = true
}
if let midPoint = midPoint {
convex.insert(midPoint, at: idx + 1)
grid.removePoint(midPoint)
midPointInserted = true
}
}
if midPointInserted {
return concaveFunc(&convex, maxSqEdgeLen, maxSearchArea, grid, &edgeSkipList)
}
return convex
}
private extension CGPoint {
var key: String {
return "\(self.x),\(self.y)"
}
}
func getHull(_ points: [CGPoint], concavity: Double) -> [CGPoint] {
let points = filterDuplicates(points)
let occupiedArea = occupiedAreaFunc(points)
let maxSearchArea: [Double] = [
occupiedArea.x * 0.6,
occupiedArea.y * 0.6
]
var convex = convex(points: points)
var innerPoints = points.filter { (point: CGPoint) -> Bool in
let idx = convex.firstIndex(where: { (idx: CGPoint) -> Bool in
return idx.x == point.x && idx.y == point.y
})
return idx == nil
}
innerPoints.sort(by: { (lhs: CGPoint, rhs: CGPoint) -> Bool in
return lhs.x == rhs.x ? lhs.y > rhs.y : lhs.x > rhs.x
})
let cellSize = ceil(occupiedArea.x * occupiedArea.y / Double(points.count))
let grid = Grid(innerPoints, cellSize)
var skipList: [String: Bool] = [String: Bool]()
return concaveFunc(&convex, pow(concavity, 2), maxSearchArea, grid, &skipList)
}
@@ -0,0 +1,394 @@
import Foundation
import UIKit
import Display
import AccountContext
import MediaEditor
final class DrawingBubbleEntityView: DrawingEntityView {
private var bubbleEntity: DrawingBubbleEntity {
return self.entity as! DrawingBubbleEntity
}
private var currentSize: CGSize?
private var currentTailPosition: CGPoint?
private let shapeLayer = SimpleShapeLayer()
init(context: AccountContext, entity: DrawingBubbleEntity) {
super.init(context: context, entity: entity)
self.layer.addSublayer(self.shapeLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func update(animated: Bool) {
let size = self.bubbleEntity.size
self.center = self.bubbleEntity.position
self.bounds = CGRect(origin: .zero, size: size)
self.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation)
if size != self.currentSize || self.bubbleEntity.tailPosition != self.currentTailPosition {
self.currentSize = size
self.currentTailPosition = self.bubbleEntity.tailPosition
self.shapeLayer.frame = self.bounds
let cornerRadius = max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.045)
let smallCornerRadius = max(5.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.01)
let tailWidth = max(5.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1)
self.shapeLayer.path = CGPath.bubble(in: CGRect(origin: .zero, size: size), cornerRadius: cornerRadius, smallCornerRadius: smallCornerRadius, tailPosition: self.bubbleEntity.tailPosition, tailWidth: tailWidth)
}
switch self.bubbleEntity.drawType {
case .fill:
self.shapeLayer.fillColor = self.bubbleEntity.color.toCGColor()
self.shapeLayer.strokeColor = UIColor.clear.cgColor
case .stroke:
let minLineWidth = max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.01)
let maxLineWidth = max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.05)
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.bubbleEntity.lineWidth
self.shapeLayer.fillColor = UIColor.clear.cgColor
self.shapeLayer.strokeColor = self.bubbleEntity.color.toCGColor()
self.shapeLayer.lineWidth = lineWidth
}
super.update(animated: animated)
}
fileprivate var visualLineWidth: CGFloat {
return self.shapeLayer.lineWidth
}
private var maxLineWidth: CGFloat {
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.2, height: minSize * 0.2)
}
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)
if expandedBounds.contains(point) {
return true
}
return false
}
override func precisePoint(inside point: CGPoint) -> Bool {
if case .stroke = self.bubbleEntity.drawType, var path = self.shapeLayer.path {
path = path.copy(strokingWithWidth: maxLineWidth * 0.8, lineCap: .square, lineJoin: .bevel, miterLimit: 0.0)
if path.contains(point) {
return true
} else {
return false
}
} else {
return super.precisePoint(inside: point)
}
}
override func updateSelectionView() {
super.updateSelectionView()
guard let selectionView = self.selectionView as? DrawingBubbleEntitySelectionView else {
return
}
selectionView.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation)
selectionView.setNeedsLayout()
}
override func makeSelectionView() -> DrawingEntitySelectionView? {
if let selectionView = self.selectionView {
return selectionView
}
let selectionView = DrawingBubbleEntitySelectionView()
selectionView.entityView = self
return selectionView
}
func getRenderImage() -> UIImage? {
let rect = self.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0)
self.drawHierarchy(in: rect, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
final class DrawingBubbleEntitySelectionView: DrawingEntitySelectionView {
private let leftHandle = SimpleShapeLayer()
private let topLeftHandle = SimpleShapeLayer()
private let topHandle = SimpleShapeLayer()
private let topRightHandle = SimpleShapeLayer()
private let rightHandle = SimpleShapeLayer()
private let bottomLeftHandle = SimpleShapeLayer()
private let bottomHandle = SimpleShapeLayer()
private let bottomRightHandle = SimpleShapeLayer()
private let tailHandle = SimpleShapeLayer()
override init(frame: CGRect) {
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
let handles = [
self.leftHandle,
self.topLeftHandle,
self.topHandle,
self.topRightHandle,
self.rightHandle,
self.bottomLeftHandle,
self.bottomHandle,
self.bottomRightHandle,
self.tailHandle
]
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
for handle in handles {
handle.bounds = handleBounds
if handle === self.tailHandle {
handle.fillColor = UIColor(rgb: 0x00ff00).cgColor
} else {
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
}
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
handle.rasterizationScale = UIScreen.main.scale
handle.shouldRasterize = true
self.layer.addSublayer(handle)
}
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
if let self, let entityView = self.entityView {
entityView.onSnapUpdated(type, snapped)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scale: CGFloat = 1.0 {
didSet {
self.setNeedsLayout()
}
}
override var selectionInset: CGFloat {
return 5.5
}
private let snapTool = DrawingEntitySnapTool()
private var currentHandle: CALayer?
override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let entityView = self.entityView as? DrawingBubbleEntityView, let entity = entityView.entity as? DrawingBubbleEntity else {
return
}
let location = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
if let sublayers = self.layer.sublayers {
for layer in sublayers {
if layer.frame.contains(location) {
self.currentHandle = layer
entityView.onInteractionUpdated(true)
return
}
}
}
self.currentHandle = self.layer
entityView.onInteractionUpdated(true)
case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview)
var updatedSize = entity.size
var updatedPosition = entity.position
var updatedTailPosition = entity.tailPosition
let minimumSize = entityView.minimumSize
if self.currentHandle != nil && self.currentHandle !== self.layer {
if gestureRecognizer.numberOfTouches > 1 {
return
}
}
if self.currentHandle === self.leftHandle {
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
} else if self.currentHandle === self.rightHandle {
updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
} else if self.currentHandle === self.topHandle {
updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.bottomHandle {
updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.topLeftHandle {
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.topRightHandle {
updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.bottomLeftHandle {
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y)
updatedPosition.y += delta.y * 0.5
} else if self.currentHandle === self.bottomRightHandle {
updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
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)))
} else if self.currentHandle === self.layer {
updatedPosition.x += delta.x
updatedPosition.y += delta.y
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
}
entity.size = updatedSize
entity.position = updatedPosition
entity.tailPosition = updatedTailPosition
entityView.update(animated: false)
gestureRecognizer.setTranslation(.zero, in: entityView)
case .ended, .cancelled:
self.snapTool.reset()
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
return
}
switch gestureRecognizer.state {
case .began, .changed:
if case .began = gestureRecognizer.state {
entityView.onInteractionUpdated(true)
}
let scale = gestureRecognizer.scale
entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale)
entityView.update()
gestureRecognizer.scale = 1.0
case .ended, .cancelled:
entityView.onInteractionUpdated(false)
default:
break
}
}
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
return
}
let velocity = gestureRecognizer.velocity
var updatedRotation = entity.rotation
var rotation: CGFloat = 0.0
switch gestureRecognizer.state {
case .began:
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
case .changed:
rotation = gestureRecognizer.rotation
updatedRotation += rotation
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation)
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.rotation = 0.0
case .ended, .cancelled:
entityView.onInteractionUpdated(false)
self.snapTool.rotationReset()
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) || self.tailHandle.frame.contains(point)
}
override func layoutSubviews() {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
return
}
let inset = self.selectionInset
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
let lineWidth = (1.0 + UIScreenPixel) / self.scale
let handles = [
self.leftHandle,
self.topLeftHandle,
self.topHandle,
self.topRightHandle,
self.rightHandle,
self.bottomLeftHandle,
self.bottomHandle,
self.bottomRightHandle,
self.tailHandle
]
for handle in handles {
handle.path = handlePath
handle.bounds = bounds
handle.lineWidth = lineWidth
}
self.topLeftHandle.position = CGPoint(x: inset, y: inset)
self.topHandle.position = CGPoint(x: self.bounds.midX, y: inset)
self.topRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: inset)
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
self.bottomLeftHandle.position = CGPoint(x: inset, y: self.bounds.maxY - inset)
self.bottomHandle.position = CGPoint(x: self.bounds.midX, y: self.bounds.maxY - inset)
self.bottomRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.maxY - inset)
let selectionScale = (self.bounds.width - inset * 2.0) / (max(0.001, entity.size.width))
self.tailHandle.position = CGPoint(x: inset + (self.bounds.width - inset * 2.0) * entity.tailPosition.x, y: self.bounds.height - inset + entity.tailPosition.y * selectionScale)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,376 @@
import Foundation
import UIKit
private let snapTimeout = 1.0
class DrawingEntitySnapTool {
enum SnapType {
case centerX
case centerY
case top
case left
case right
case bottom
case rotation(CGFloat?)
static var allPositionTypes: [SnapType] {
return [
.centerX,
.centerY,
.top,
.left,
.right,
.bottom
]
}
}
struct SnapState {
let skipped: CGFloat
let waitForLeave: Bool
}
private var topEdgeState: SnapState?
private var leftEdgeState: SnapState?
private var rightEdgeState: SnapState?
private var bottomEdgeState: SnapState?
private var xState: SnapState?
private var yState: SnapState?
private var rotationState: (angle: CGFloat, skipped: CGFloat, waitForLeave: Bool)?
var onSnapUpdated: (SnapType, Bool) -> Void = { _, _ in }
var previousTopEdgeSnapTimestamp: Double?
var previousLeftEdgeSnapTimestamp: Double?
var previousRightEdgeSnapTimestamp: Double?
var previousBottomEdgeSnapTimestamp: Double?
var previousXSnapTimestamp: Double?
var previousYSnapTimestamp: Double?
var previousRotationSnapTimestamp: Double?
func reset() {
self.topEdgeState = nil
self.leftEdgeState = nil
self.rightEdgeState = nil
self.bottomEdgeState = nil
self.xState = nil
self.yState = nil
for type in SnapType.allPositionTypes {
self.onSnapUpdated(type, false)
}
}
func rotationReset() {
self.rotationState = nil
self.onSnapUpdated(.rotation(nil), false)
}
func maybeSkipFromStart(entityView: DrawingEntityView, position: CGPoint) {
self.topEdgeState = nil
self.leftEdgeState = nil
self.rightEdgeState = nil
self.bottomEdgeState = nil
self.xState = nil
self.yState = nil
let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() {
if position.x > snapLocation.x - snapXDelta && position.x < snapLocation.x + snapXDelta {
self.xState = SnapState(skipped: 0.0, waitForLeave: true)
}
if position.y > snapLocation.y - snapYDelta && position.y < snapLocation.y + snapYDelta {
self.yState = SnapState(skipped: 0.0, waitForLeave: true)
}
}
}
func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint, size: CGSize) -> CGPoint {
var updatedPosition = updatedPosition
guard let snapCenterLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() else {
return updatedPosition
}
let isStickerEditor = (entityView.superview as? DrawingEntitiesView)?.isStickerEditor ?? false
let snapEdgeLocations = (entityView.superview as? DrawingEntitiesView)?.getEntityEdgePositions()
let currentTimestamp = CACurrentMediaTime()
let snapDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
let snapVelocity: CGFloat = snapDelta * 12.0
let snapSkipTranslation: CGFloat = snapDelta * 2.0
let topPoint = updatedPosition.y - size.height / 2.0
let leftPoint = updatedPosition.x - size.width / 2.0
let rightPoint = updatedPosition.x + size.width / 2.0
let bottomPoint = updatedPosition.y + size.height / 2.0
func process(
state: SnapState?,
velocity: CGFloat,
delta: CGFloat,
value: CGFloat,
snapVelocity: CGFloat,
snapToValue: CGFloat?,
snapDelta: CGFloat,
snapSkipTranslation: CGFloat,
previousSnapTimestamp: Double?,
onSnapUpdated: (Bool) -> Void
) -> (
value: CGFloat,
state: SnapState?,
snapTimestamp: Double?
) {
var updatedValue = value
var updatedState = state
var updatedPreviousSnapTimestamp = previousSnapTimestamp
if abs(velocity) < snapVelocity || state?.waitForLeave == true {
if let snapToValue {
if let state {
let skipped = state.skipped
let waitForLeave = state.waitForLeave
if waitForLeave {
if value > snapToValue - snapDelta * 2.0 && value < snapToValue + snapDelta * 2.0 {
} else {
updatedState = nil
}
} else if abs(skipped) < snapSkipTranslation {
updatedState = SnapState(skipped: skipped + delta, waitForLeave: false)
updatedValue = snapToValue
} else {
updatedState = SnapState(skipped: snapSkipTranslation, waitForLeave: true)
onSnapUpdated(false)
}
} else {
if value > snapToValue - snapDelta && value < snapToValue + snapDelta {
if let previousSnapTimestamp, currentTimestamp - previousSnapTimestamp < snapTimeout {
} else {
updatedPreviousSnapTimestamp = currentTimestamp
updatedState = SnapState(skipped: 0.0, waitForLeave: false)
updatedValue = snapToValue
onSnapUpdated(true)
}
}
}
}
} else {
updatedState = nil
onSnapUpdated(false)
}
return (updatedValue, updatedState, updatedPreviousSnapTimestamp)
}
let (updatedXValue, updatedXState, updatedXPreviousTimestamp) = process(
state: self.xState,
velocity: velocity.x,
delta: delta.x,
value: updatedPosition.x,
snapVelocity: snapVelocity,
snapToValue: snapCenterLocation.x,
snapDelta: snapDelta,
snapSkipTranslation: snapSkipTranslation,
previousSnapTimestamp: self.previousXSnapTimestamp,
onSnapUpdated: { [weak self] snapped in
self?.onSnapUpdated(.centerX, snapped)
}
)
self.xState = updatedXState
self.previousXSnapTimestamp = updatedXPreviousTimestamp
let (updatedYValue, updatedYState, updatedYPreviousTimestamp) = process(
state: self.yState,
velocity: velocity.y,
delta: delta.y,
value: updatedPosition.y,
snapVelocity: snapVelocity,
snapToValue: snapCenterLocation.y,
snapDelta: snapDelta,
snapSkipTranslation: snapSkipTranslation,
previousSnapTimestamp: self.previousYSnapTimestamp,
onSnapUpdated: { [weak self] snapped in
self?.onSnapUpdated(.centerY, snapped)
}
)
self.yState = updatedYState
self.previousYSnapTimestamp = updatedYPreviousTimestamp
if let snapEdgeLocations, !isStickerEditor {
if updatedXState == nil {
let (updatedXLeftEdgeValue, updatedLeftEdgeState, updatedLeftEdgePreviousTimestamp) = process(
state: self.leftEdgeState,
velocity: velocity.x,
delta: delta.x,
value: leftPoint,
snapVelocity: snapVelocity,
snapToValue: snapEdgeLocations.left,
snapDelta: snapDelta,
snapSkipTranslation: snapSkipTranslation,
previousSnapTimestamp: self.previousLeftEdgeSnapTimestamp,
onSnapUpdated: { [weak self] snapped in
self?.onSnapUpdated(.left, snapped)
}
)
self.leftEdgeState = updatedLeftEdgeState
self.previousLeftEdgeSnapTimestamp = updatedLeftEdgePreviousTimestamp
if updatedLeftEdgeState != nil {
updatedPosition.x = updatedXLeftEdgeValue + size.width / 2.0
self.rightEdgeState = nil
self.previousRightEdgeSnapTimestamp = nil
} else {
let (updatedXRightEdgeValue, updatedRightEdgeState, updatedRightEdgePreviousTimestamp) = process(
state: self.rightEdgeState,
velocity: velocity.x,
delta: delta.x,
value: rightPoint,
snapVelocity: snapVelocity,
snapToValue: snapEdgeLocations.right,
snapDelta: snapDelta,
snapSkipTranslation: snapSkipTranslation,
previousSnapTimestamp: self.previousRightEdgeSnapTimestamp,
onSnapUpdated: { [weak self] snapped in
self?.onSnapUpdated(.right, snapped)
}
)
self.rightEdgeState = updatedRightEdgeState
self.previousRightEdgeSnapTimestamp = updatedRightEdgePreviousTimestamp
updatedPosition.x = updatedXRightEdgeValue - size.width / 2.0
}
} else {
updatedPosition.x = updatedXValue
}
if updatedYState == nil {
let (updatedYTopEdgeValue, updatedTopEdgeState, updatedTopEdgePreviousTimestamp) = process(
state: self.topEdgeState,
velocity: velocity.y,
delta: delta.y,
value: topPoint,
snapVelocity: snapVelocity,
snapToValue: snapEdgeLocations.top,
snapDelta: snapDelta,
snapSkipTranslation: snapSkipTranslation,
previousSnapTimestamp: self.previousTopEdgeSnapTimestamp,
onSnapUpdated: { [weak self] snapped in
self?.onSnapUpdated(.top, snapped)
}
)
self.topEdgeState = updatedTopEdgeState
self.previousTopEdgeSnapTimestamp = updatedTopEdgePreviousTimestamp
if updatedTopEdgeState != nil {
updatedPosition.y = updatedYTopEdgeValue + size.height / 2.0
self.bottomEdgeState = nil
self.previousBottomEdgeSnapTimestamp = nil
} else {
let (updatedYBottomEdgeValue, updatedBottomEdgeState, updatedBottomEdgePreviousTimestamp) = process(
state: self.bottomEdgeState,
velocity: velocity.y,
delta: delta.y,
value: bottomPoint,
snapVelocity: snapVelocity,
snapToValue: snapEdgeLocations.bottom,
snapDelta: snapDelta,
snapSkipTranslation: snapSkipTranslation,
previousSnapTimestamp: self.previousBottomEdgeSnapTimestamp,
onSnapUpdated: { [weak self] snapped in
self?.onSnapUpdated(.bottom, snapped)
}
)
self.bottomEdgeState = updatedBottomEdgeState
self.previousBottomEdgeSnapTimestamp = updatedBottomEdgePreviousTimestamp
updatedPosition.y = updatedYBottomEdgeValue - size.height / 2.0
}
} else {
updatedPosition.y = updatedYValue
}
} else {
updatedPosition.x = updatedXValue
updatedPosition.y = updatedYValue
}
return updatedPosition
}
private let snapRotations: [CGFloat] = [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
func maybeSkipFromStart(entityView: DrawingEntityView, rotation: CGFloat) {
self.rotationState = nil
let snapDelta: CGFloat = 0.01
for snapRotation in self.snapRotations {
let snapRotation = snapRotation * .pi
if rotation > snapRotation - snapDelta && rotation < snapRotation + snapDelta {
self.rotationState = (snapRotation, 0.0, true)
break
}
}
}
func update(entityView: DrawingEntityView, velocity: CGFloat, delta: CGFloat, updatedRotation: CGFloat, skipMultiplier: CGFloat = 1.0) -> CGFloat {
var updatedRotation = updatedRotation
if updatedRotation < 0.0 {
updatedRotation = 2.0 * .pi + updatedRotation
} else if updatedRotation > 2.0 * .pi {
while updatedRotation > 2.0 * .pi {
updatedRotation -= 2.0 * .pi
}
}
let currentTimestamp = CACurrentMediaTime()
let snapDelta: CGFloat = 0.01
let snapVelocity: CGFloat = snapDelta * 35.0
let snapSkipRotation: CGFloat = snapDelta * 45.0 * skipMultiplier
if abs(velocity) < snapVelocity || self.rotationState?.waitForLeave == true {
if let (snapRotation, skipped, waitForLeave) = self.rotationState {
if waitForLeave {
if updatedRotation > snapRotation - snapDelta * 2.0 && updatedRotation < snapRotation + snapDelta {
} else {
self.rotationState = nil
}
} else if abs(skipped) < snapSkipRotation {
self.rotationState = (snapRotation, skipped + delta, false)
updatedRotation = snapRotation
} else {
self.rotationState = (snapRotation, snapSkipRotation, true)
self.onSnapUpdated(.rotation(nil), false)
}
} else {
for snapRotation in self.snapRotations {
let snapRotation = snapRotation * .pi
if updatedRotation > snapRotation - snapDelta && updatedRotation < snapRotation + snapDelta {
if let previousRotationSnapTimestamp, currentTimestamp - previousRotationSnapTimestamp < snapTimeout {
} else {
self.previousRotationSnapTimestamp = currentTimestamp
self.rotationState = (snapRotation, 0.0, false)
updatedRotation = snapRotation
self.onSnapUpdated(.rotation(snapRotation), true)
}
break
}
}
}
} else {
self.rotationState = nil
self.onSnapUpdated(.rotation(nil), false)
}
return updatedRotation
}
}
@@ -0,0 +1,103 @@
import Foundation
import UIKit
class DrawingGestureRecognizer: UIPanGestureRecognizer {
var shouldBegin: (CGPoint) -> Bool = { _ in return true }
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if touches.count == 1, let touch = touches.first, self.shouldBegin(touch.location(in: self.view)) {
super.touchesBegan(touches, with: event)
self.state = .began
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
if touches.count > 1 {
self.state = .cancelled
} else {
super.touchesMoved(touches, with: event)
}
}
}
struct DrawingPoint {
let location: CGPoint
let velocity: CGFloat
var x: CGFloat {
return self.location.x
}
var y: CGFloat {
return self.location.y
}
}
final class DrawingGesturePipeline: NSObject, UIGestureRecognizerDelegate {
enum DrawingGestureState {
case began
case changed
case ended
case cancelled
}
var onDrawing: (DrawingGestureState, DrawingPoint) -> Void = { _, _ in }
var gestureRecognizer: DrawingGestureRecognizer?
var transform: CGAffineTransform = .identity
var enabled: Bool = true
weak var drawingView: DrawingView?
init(drawingView: DrawingView, gestureView: UIView) {
self.drawingView = drawingView
super.init()
let gestureRecognizer = DrawingGestureRecognizer(target: self, action: #selector(self.handleGesture(_:)))
gestureRecognizer.delegate = self
self.gestureRecognizer = gestureRecognizer
gestureView.addGestureRecognizer(gestureRecognizer)
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return self.enabled
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIPinchGestureRecognizer {
return true
}
return false
}
var previousPoint: DrawingPoint?
@objc private func handleGesture(_ gestureRecognizer: DrawingGestureRecognizer) {
let state: DrawingGestureState
switch gestureRecognizer.state {
case .began:
state = .began
case .changed:
state = .changed
case .ended:
state = .ended
case .cancelled:
state = .cancelled
case .failed:
state = .cancelled
case .possible:
state = .cancelled
@unknown default:
state = .cancelled
}
let originalLocation = gestureRecognizer.location(in: self.drawingView)
let location = originalLocation.applying(self.transform)
let velocity = gestureRecognizer.velocity(in: self.drawingView).applying(self.transform)
let velocityValue = velocity.length
let point = DrawingPoint(location: location, velocity: velocityValue)
self.onDrawing(state, point)
}
}
@@ -0,0 +1,624 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import AccountContext
import TelegramCore
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
import MediaEditor
private func generateIcon(style: DrawingLinkEntity.Style) -> UIImage? {
guard let image = UIImage(bundleImageName: "Premium/Link") else {
return nil
}
return generateImage(image.size, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let cgImage = image.cgImage {
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
}
if [.black, .white].contains(style) {
let green: UIColor
let blue: UIColor
if case .black = style {
green = UIColor(rgb: 0x64d2ff)
blue = UIColor(rgb: 0x64d2ff)
} else {
green = UIColor(rgb: 0x0a84ff)
blue = UIColor(rgb: 0x0a84ff)
}
var locations: [CGFloat] = [0.0, 1.0]
let colorsArray = [green.cgColor, blue.cgColor] as NSArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
} else {
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: size))
}
})
}
public final class DrawingLinkEntityView: DrawingEntityView, UITextViewDelegate {
private var linkEntity: DrawingLinkEntity {
return self.entity as! DrawingLinkEntity
}
let imageView: UIImageView
let backgroundView: UIView
let blurredBackgroundView: BlurredBackgroundView
let textView: DrawingTextView
let iconView: UIImageView
private let imageNode: TransformImageNode
private let cachedDisposable = MetaDisposable()
init(context: AccountContext, entity: DrawingLinkEntity) {
self.imageView = UIImageView()
self.backgroundView = UIView()
self.backgroundView.clipsToBounds = true
self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true)
self.blurredBackgroundView.clipsToBounds = true
self.textView = DrawingTextView(frame: .zero)
self.textView.clipsToBounds = false
self.textView.backgroundColor = .clear
self.textView.isEditable = false
self.textView.isSelectable = false
self.textView.contentInset = .zero
self.textView.showsHorizontalScrollIndicator = false
self.textView.showsVerticalScrollIndicator = false
self.textView.scrollsToTop = false
self.textView.isScrollEnabled = false
self.textView.textContainerInset = .zero
self.textView.minimumZoomScale = 1.0
self.textView.maximumZoomScale = 1.0
self.textView.keyboardAppearance = .dark
self.textView.autocorrectionType = .default
self.textView.spellCheckingType = .no
self.textView.textContainer.maximumNumberOfLines = 2
self.textView.textContainer.lineBreakMode = .byTruncatingTail
self.iconView = UIImageView()
self.imageNode = TransformImageNode()
super.init(context: context, entity: entity)
self.textView.delegate = self
self.addSubview(self.imageView)
self.addSubview(self.backgroundView)
self.addSubview(self.blurredBackgroundView)
self.addSubview(self.textView)
self.addSubview(self.iconView)
self.update(animated: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var textSize: CGSize = .zero
public override func sizeThatFits(_ size: CGSize) -> CGSize {
if self.linkEntity.webpage != nil, let image = self.linkEntity.whiteImage {
self.imageView.frame = CGRect(origin: .zero, size: image.size)
return image.size
} else {
var result = self.textView.sizeThatFits(CGSize(width: self.linkEntity.width, height: .greatestFiniteMagnitude))
self.textSize = result
let widthExtension = result.height * 0.65
result.width = floorToScreenPixels(max(104.0, ceil(result.width) + 20.0) + widthExtension)
result.height = ceil(result.height * 1.2);
return result;
}
}
public override func sizeToFit() {
let center = self.center
let transform = self.transform
self.transform = .identity
super.sizeToFit()
self.center = center
self.transform = transform
}
public override func layoutSubviews() {
super.layoutSubviews()
let iconSize: CGFloat
let iconOffset: CGFloat
iconSize = min(76.0, floor(self.bounds.height * 0.6))
iconOffset = 0.3
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0)
let imageSize = CGSize(width: iconSize, height: iconSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize)
self.backgroundView.frame = self.bounds
self.blurredBackgroundView.frame = self.bounds
self.blurredBackgroundView.update(size: self.bounds.size, transition: .immediate)
}
override func selectedTapAction() -> Bool {
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
let keyTimes = [0.0, 0.33, 1.0]
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
let updatedStyle: DrawingLinkEntity.Style
if self.linkEntity.webpage != nil {
switch self.linkEntity.style {
case .white:
updatedStyle = .black
default:
updatedStyle = .white
}
} else {
switch self.linkEntity.style {
case .white:
updatedStyle = .black
case .black:
updatedStyle = .transparent
case .transparent:
if self.linkEntity.hasCustomColor {
updatedStyle = .custom
} else {
updatedStyle = .white
}
case .custom:
updatedStyle = .white
case .blur:
updatedStyle = .white
}
}
self.linkEntity.style = updatedStyle
self.update()
return true
}
private var displayFontSize: CGFloat {
var textFontSize: CGFloat = 0.07
let textLength = self.linkEntity.url.count
if textLength > 10 {
textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0)
}
let minFontSize = max(10.0, max(self.linkEntity.referenceDrawingSize.width, self.linkEntity.referenceDrawingSize.height) * 0.025)
let maxFontSize = max(10.0, max(self.linkEntity.referenceDrawingSize.width, self.linkEntity.referenceDrawingSize.height) * 0.25)
let fontSize = minFontSize + (maxFontSize - minFontSize) * textFontSize
return fontSize
}
private func updateText() {
let string: String
if !self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
string = self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
} else {
string = self.linkEntity.url.uppercased().replacingOccurrences(of: "HTTP://", with: "").replacingOccurrences(of: "HTTPS://", with: "").replacingOccurrences(of: "TONSITE://", with: "")
}
let text = NSMutableAttributedString(string: string)
let range = NSMakeRange(0, text.length)
let fontSize = self.displayFontSize
self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24)
let font = Font.with(size: fontSize, design: .camera, weight: .semibold)
text.addAttribute(.font, value: font, range: range)
text.addAttribute(.kern, value: -3.5 as NSNumber, range: range)
self.textView.font = font
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
let textColor: UIColor
switch self.linkEntity.style {
case .white:
textColor = UIColor(rgb: 0x0a84ff)
case .black, .blur:
textColor = UIColor(rgb: 0x64d2ff)
case .transparent:
textColor = .white
case .custom:
let color = self.linkEntity.color.toUIColor()
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
}
text.addAttribute(.foregroundColor, value: textColor, range: range)
self.textView.attributedText = text
self.textView.visualText = text
}
private var currentStyle: DrawingLinkEntity.Style?
public override func update(animated: Bool = false) {
self.center = self.linkEntity.position
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.linkEntity.rotation), self.linkEntity.scale, self.linkEntity.scale)
if self.linkEntity.webpage != nil {
self.textView.isHidden = true
self.backgroundView.isHidden = true
self.blurredBackgroundView.isHidden = true
self.iconView.isHidden = true
if self.linkEntity.style == .white && self.imageView.image !== self.linkEntity.whiteImage {
self.imageView.image = self.linkEntity.whiteImage
} else if self.linkEntity.style == .black && self.imageView.image !== self.linkEntity.blackImage {
self.imageView.image = self.linkEntity.blackImage
}
} else {
self.textView.isHidden = false
self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0)
switch self.linkEntity.style {
case .white:
self.textView.textColor = UIColor(rgb: 0x0a84ff)
self.backgroundView.backgroundColor = .white
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .black:
self.textView.textColor = UIColor(rgb: 0x64d2ff)
self.backgroundView.backgroundColor = .black
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .transparent:
self.textView.textColor = .white
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .custom:
let color = self.linkEntity.color.toUIColor()
let textColor: UIColor
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
self.textView.textColor = textColor
self.backgroundView.backgroundColor = color
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .blur:
self.textView.textColor = .white
self.backgroundView.isHidden = true
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff)
self.blurredBackgroundView.isHidden = false
}
self.textView.textAlignment = .left
self.updateText()
self.iconView.isHidden = false
if self.currentStyle != self.linkEntity.style {
self.currentStyle = self.linkEntity.style
self.iconView.image = generateIcon(style: self.linkEntity.style)
}
self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2
self.blurredBackgroundView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
if #available(iOS 13.0, *) {
self.backgroundView.layer.cornerCurve = .continuous
self.blurredBackgroundView.layer.cornerCurve = .continuous
}
}
self.sizeToFit()
super.update(animated: animated)
}
override func updateSelectionView() {
guard let selectionView = self.selectionView as? DrawingLinkEntitySelectionView else {
return
}
self.pushIdentityTransformForMeasurement()
selectionView.transform = .identity
let bounds = self.selectionBounds
let center = bounds.center
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
selectionView.center = self.convert(center, to: selectionView.superview)
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.linkEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.linkEntity.scale) * scale + selectionView.selectionInset * 2.0))
selectionView.transform = CGAffineTransformMakeRotation(self.linkEntity.rotation)
self.popIdentityTransformForMeasurement()
}
override func makeSelectionView() -> DrawingEntitySelectionView? {
if let selectionView = self.selectionView {
return selectionView
}
let selectionView = DrawingLinkEntitySelectionView()
selectionView.entityView = self
return selectionView
}
func getRenderImage() -> UIImage? {
let rect = self.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0)
self.drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func getRenderSubEntities() -> [DrawingEntity] {
return []
}
}
final class DrawingLinkEntitySelectionView: DrawingEntitySelectionView {
private let border = SimpleShapeLayer()
private let leftHandle = SimpleShapeLayer()
private let rightHandle = SimpleShapeLayer()
private var longPressGestureRecognizer: UILongPressGestureRecognizer?
override init(frame: CGRect) {
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
let handles = [
self.leftHandle,
self.rightHandle
]
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
self.border.lineCap = .round
self.border.fillColor = UIColor.clear.cgColor
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.75).cgColor
self.layer.addSublayer(self.border)
for handle in handles {
handle.bounds = handleBounds
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
handle.rasterizationScale = UIScreen.main.scale
handle.shouldRasterize = true
self.layer.addSublayer(handle)
}
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
if let self, let entityView = self.entityView {
entityView.onSnapUpdated(type, snapped)
}
}
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
self.addGestureRecognizer(longPressGestureRecognizer)
self.longPressGestureRecognizer = longPressGestureRecognizer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scale: CGFloat = 1.0 {
didSet {
self.setNeedsLayout()
}
}
override var selectionInset: CGFloat {
return 15.0
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
private let snapTool = DrawingEntitySnapTool()
@objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
if case .began = gestureRecognizer.state {
self.longPressed()
}
}
private var currentHandle: CALayer?
override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingLinkEntity else {
return
}
let location = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
self.tapGestureRecognizer?.isEnabled = false
self.tapGestureRecognizer?.isEnabled = true
self.longPressGestureRecognizer?.isEnabled = false
self.longPressGestureRecognizer?.isEnabled = true
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
if let sublayers = self.layer.sublayers {
for layer in sublayers {
if layer.frame.contains(location) {
self.currentHandle = layer
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
return
}
}
}
self.currentHandle = self.layer
entityView.onInteractionUpdated(true)
case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview)
let parentLocation = gestureRecognizer.location(in: self.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview)
var updatedScale = entity.scale
var updatedPosition = entity.position
var updatedRotation = entity.rotation
if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle {
if gestureRecognizer.numberOfTouches > 1 {
return
}
var deltaX = gestureRecognizer.translation(in: self).x
if self.currentHandle === self.leftHandle {
deltaX *= -1.0
}
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
updatedScale = max(0.01, updatedScale * scaleDelta)
let newAngle: CGFloat
if self.currentHandle === self.leftHandle {
newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x)
} else {
newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x)
}
var delta = newAngle - updatedRotation
if delta < -.pi {
delta = 2.0 * .pi + delta
}
let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0)
} else if self.currentHandle === self.layer {
updatedPosition.x += delta.x
updatedPosition.y += delta.y
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
}
entity.scale = updatedScale
entity.position = updatedPosition
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.setTranslation(.zero, in: entityView)
case .ended, .cancelled:
self.snapTool.reset()
if self.currentHandle != nil {
self.snapTool.rotationReset()
}
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard let entityView = self.entityView as? DrawingLinkEntityView, let entity = entityView.entity as? DrawingLinkEntity else {
return
}
switch gestureRecognizer.state {
case .began, .changed:
if case .began = gestureRecognizer.state {
entityView.onInteractionUpdated(true)
}
let scale = gestureRecognizer.scale
entity.scale = max(0.1, entity.scale * scale)
entityView.update()
gestureRecognizer.scale = 1.0
case .ended, .cancelled:
entityView.onInteractionUpdated(false)
default:
break
}
}
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
guard let entityView = self.entityView as? DrawingLinkEntityView, let entity = entityView.entity as? DrawingLinkEntity else {
return
}
let velocity = gestureRecognizer.velocity
var updatedRotation = entity.rotation
var rotation: CGFloat = 0.0
switch gestureRecognizer.state {
case .began:
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
case .changed:
rotation = gestureRecognizer.rotation
updatedRotation += rotation
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation)
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.rotation = 0.0
case .ended, .cancelled:
self.snapTool.rotationReset()
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
}
override func layoutSubviews() {
let inset = self.selectionInset - 10.0
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
let lineWidth = (1.0 + UIScreenPixel) / self.scale
let handles = [
self.leftHandle,
self.rightHandle
]
for handle in handles {
handle.path = handlePath
handle.bounds = bounds
handle.lineWidth = lineWidth
}
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
let width: CGFloat = self.bounds.width - inset * 2.0
let height: CGFloat = self.bounds.height - inset * 2.0
let cornerRadius: CGFloat = 12.0 - self.scale
let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi))
let count = 12
let relativeDashLength: CGFloat = 0.25
let dashLength = perimeter / CGFloat(count)
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
self.border.lineWidth = 2.0 / self.scale
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath
}
}
@@ -0,0 +1,643 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import AccountContext
import TelegramCore
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
import MediaEditor
private func generateIcon(style: DrawingLocationEntity.Style) -> UIImage? {
guard let image = UIImage(bundleImageName: "Chat/Attach Menu/Location") else {
return nil
}
return generateImage(image.size, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let cgImage = image.cgImage {
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
}
if [.black, .white].contains(style) {
let green: UIColor
let blue: UIColor
if case .black = style {
green = UIColor(rgb: 0x3EF588)
blue = UIColor(rgb: 0x4FAAFF)
} else {
green = UIColor(rgb: 0x1EBD5E)
blue = UIColor(rgb: 0x1C92FF)
}
var locations: [CGFloat] = [0.0, 1.0]
let colorsArray = [green.cgColor, blue.cgColor] as NSArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
} else {
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: size))
}
})
}
public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDelegate {
private var locationEntity: DrawingLocationEntity {
return self.entity as! DrawingLocationEntity
}
let backgroundView: UIView
let blurredBackgroundView: BlurredBackgroundView
let textView: DrawingTextView
let iconView: UIImageView
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var didSetUpAnimationNode = false
private let stickerFetchedDisposable = MetaDisposable()
private let cachedDisposable = MetaDisposable()
init(context: AccountContext, entity: DrawingLocationEntity) {
self.backgroundView = UIView()
self.backgroundView.clipsToBounds = true
self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true)
self.blurredBackgroundView.clipsToBounds = true
self.textView = DrawingTextView(frame: .zero)
self.textView.clipsToBounds = false
self.textView.backgroundColor = .clear
self.textView.isEditable = false
self.textView.isSelectable = false
self.textView.contentInset = .zero
self.textView.showsHorizontalScrollIndicator = false
self.textView.showsVerticalScrollIndicator = false
self.textView.scrollsToTop = false
self.textView.isScrollEnabled = false
self.textView.textContainerInset = .zero
self.textView.minimumZoomScale = 1.0
self.textView.maximumZoomScale = 1.0
self.textView.keyboardAppearance = .dark
self.textView.autocorrectionType = .default
self.textView.spellCheckingType = .no
self.textView.textContainer.maximumNumberOfLines = 2
self.textView.textContainer.lineBreakMode = .byTruncatingTail
self.iconView = UIImageView()
self.imageNode = TransformImageNode()
super.init(context: context, entity: entity)
self.textView.delegate = self
self.addSubview(self.backgroundView)
self.addSubview(self.blurredBackgroundView)
self.addSubview(self.textView)
self.addSubview(self.iconView)
self.update(animated: false)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var textSize: CGSize = .zero
public override func sizeThatFits(_ size: CGSize) -> CGSize {
var result = self.textView.sizeThatFits(CGSize(width: self.locationEntity.width, height: .greatestFiniteMagnitude))
self.textSize = result
let widthExtension: CGFloat
if self.locationEntity.icon != nil {
widthExtension = result.height * 0.77
} else {
widthExtension = result.height * 0.65
}
result.width = floorToScreenPixels(max(224.0, ceil(result.width) + 20.0) + widthExtension)
result.height = ceil(result.height * 1.2);
return result;
}
public override func sizeToFit() {
let center = self.center
let transform = self.transform
self.transform = .identity
super.sizeToFit()
self.center = center
self.transform = transform
}
public override func layoutSubviews() {
super.layoutSubviews()
let iconSize: CGFloat
let iconOffset: CGFloat
if self.locationEntity.icon != nil {
iconSize = min(80.0, floor(self.bounds.height * 0.7))
iconOffset = 0.2
} else {
iconSize = min(76.0, floor(self.bounds.height * 0.6))
iconOffset = 0.3
}
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0)
let imageSize = CGSize(width: iconSize, height: iconSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize)
self.backgroundView.frame = self.bounds
self.blurredBackgroundView.frame = self.bounds
self.blurredBackgroundView.update(size: self.bounds.size, transition: .immediate)
}
override func selectedTapAction() -> Bool {
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
let keyTimes = [0.0, 0.33, 1.0]
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
let updatedStyle: DrawingLocationEntity.Style
switch self.locationEntity.style {
case .white:
updatedStyle = .black
case .black:
updatedStyle = .transparent
case .transparent:
if self.locationEntity.hasCustomColor {
updatedStyle = .custom
} else {
updatedStyle = .white
}
case .custom:
updatedStyle = .white
case .blur:
updatedStyle = .white
}
self.locationEntity.style = updatedStyle
self.update()
return true
}
private var displayFontSize: CGFloat {
var textFontSize: CGFloat = 0.07
let textLength = self.locationEntity.title.count
if textLength > 10 {
textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0)
}
let minFontSize = max(10.0, max(self.locationEntity.referenceDrawingSize.width, self.locationEntity.referenceDrawingSize.height) * 0.025)
let maxFontSize = max(10.0, max(self.locationEntity.referenceDrawingSize.width, self.locationEntity.referenceDrawingSize.height) * 0.25)
let fontSize = minFontSize + (maxFontSize - minFontSize) * textFontSize
return fontSize
}
private func updateText() {
let text = NSMutableAttributedString(string: self.locationEntity.title.uppercased())
let range = NSMakeRange(0, text.length)
let fontSize = self.displayFontSize
self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24)
let font = Font.with(size: fontSize, design: .camera, weight: .semibold)
text.addAttribute(.font, value: font, range: range)
text.addAttribute(.kern, value: -3.5 as NSNumber, range: range)
self.textView.font = font
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
let textColor: UIColor
switch self.locationEntity.style {
case .white:
textColor = .black
case .black, .transparent, .blur:
textColor = .white
case .custom:
let color = self.locationEntity.color.toUIColor()
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
}
text.addAttribute(.foregroundColor, value: textColor, range: range)
self.textView.attributedText = text
self.textView.visualText = text
}
private var currentStyle: DrawingLocationEntity.Style?
public override func update(animated: Bool = false) {
self.center = self.locationEntity.position
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.locationEntity.rotation), self.locationEntity.scale, self.locationEntity.scale)
self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0)
switch self.locationEntity.style {
case .white:
self.textView.textColor = .black
self.backgroundView.backgroundColor = .white
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .black:
self.textView.textColor = .white
self.backgroundView.backgroundColor = .black
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .transparent:
self.textView.textColor = .white
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .custom:
let color = self.locationEntity.color.toUIColor()
let textColor: UIColor
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
self.textView.textColor = textColor
self.backgroundView.backgroundColor = color
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .blur:
self.textView.textColor = .white
self.backgroundView.isHidden = true
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff)
self.blurredBackgroundView.isHidden = false
}
self.textView.textAlignment = .left
self.updateText()
self.sizeToFit()
if self.currentStyle != self.locationEntity.style {
self.currentStyle = self.locationEntity.style
self.iconView.image = generateIcon(style: self.locationEntity.style)
}
self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2
self.blurredBackgroundView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
if #available(iOS 13.0, *) {
self.backgroundView.layer.cornerCurve = .continuous
self.blurredBackgroundView.layer.cornerCurve = .continuous
}
super.update(animated: animated)
}
private func setup() {
if let file = self.locationEntity.icon {
self.iconView.isHidden = true
self.addSubnode(self.imageNode)
if let dimensions = file.dimensions {
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
if self.animationNode == nil {
let animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.autoplay = false
self.animationNode = animationNode
animationNode.started = { [weak self, weak animationNode] in
self?.imageNode.isHidden = true
let _ = animationNode
// if let animationNode = animationNode {
// let _ = (animationNode.status
// |> take(1)
// |> deliverOnMainQueue).start(next: { [weak self] status in
// self?.started?(status.duration)
// })
// }
}
self.addSubnode(animationNode)
if file.isCustomTemplateEmoji {
animationNode.dynamicColor = UIColor(rgb: 0xffffff)
}
}
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start())
} else {
if let animationNode = self.animationNode {
animationNode.visibility = false
self.animationNode = nil
animationNode.removeFromSupernode()
self.imageNode.isHidden = false
self.didSetUpAnimationNode = false
}
self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start())
}
self.setNeedsLayout()
}
}
}
override func updateSelectionView() {
guard let selectionView = self.selectionView as? DrawingLocationEntitySelectionView else {
return
}
self.pushIdentityTransformForMeasurement()
selectionView.transform = .identity
let bounds = self.selectionBounds
let center = bounds.center
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
selectionView.center = self.convert(center, to: selectionView.superview)
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.locationEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.locationEntity.scale) * scale + selectionView.selectionInset * 2.0))
selectionView.transform = CGAffineTransformMakeRotation(self.locationEntity.rotation)
self.popIdentityTransformForMeasurement()
}
override func makeSelectionView() -> DrawingEntitySelectionView? {
if let selectionView = self.selectionView {
return selectionView
}
let selectionView = DrawingLocationEntitySelectionView()
selectionView.entityView = self
return selectionView
}
func getRenderImage() -> UIImage? {
let rect = self.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0)
self.drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func getRenderSubEntities() -> [DrawingEntity] {
return []
}
}
final class DrawingLocationEntitySelectionView: DrawingEntitySelectionView {
private let border = SimpleShapeLayer()
private let leftHandle = SimpleShapeLayer()
private let rightHandle = SimpleShapeLayer()
private var longPressGestureRecognizer: UILongPressGestureRecognizer?
override init(frame: CGRect) {
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
let handles = [
self.leftHandle,
self.rightHandle
]
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
self.border.lineCap = .round
self.border.fillColor = UIColor.clear.cgColor
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.75).cgColor
self.layer.addSublayer(self.border)
for handle in handles {
handle.bounds = handleBounds
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
handle.rasterizationScale = UIScreen.main.scale
handle.shouldRasterize = true
self.layer.addSublayer(handle)
}
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
if let self, let entityView = self.entityView {
entityView.onSnapUpdated(type, snapped)
}
}
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
self.addGestureRecognizer(longPressGestureRecognizer)
self.longPressGestureRecognizer = longPressGestureRecognizer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scale: CGFloat = 1.0 {
didSet {
self.setNeedsLayout()
}
}
override var selectionInset: CGFloat {
return 15.0
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
private let snapTool = DrawingEntitySnapTool()
@objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
if case .began = gestureRecognizer.state {
self.longPressed()
}
}
private var currentHandle: CALayer?
override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingLocationEntity else {
return
}
let location = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
self.tapGestureRecognizer?.isEnabled = false
self.tapGestureRecognizer?.isEnabled = true
self.longPressGestureRecognizer?.isEnabled = false
self.longPressGestureRecognizer?.isEnabled = true
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
if let sublayers = self.layer.sublayers {
for layer in sublayers {
if layer.frame.contains(location) {
self.currentHandle = layer
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
return
}
}
}
self.currentHandle = self.layer
entityView.onInteractionUpdated(true)
case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview)
let parentLocation = gestureRecognizer.location(in: self.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview)
var updatedScale = entity.scale
var updatedPosition = entity.position
var updatedRotation = entity.rotation
if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle {
if gestureRecognizer.numberOfTouches > 1 {
return
}
var deltaX = gestureRecognizer.translation(in: self).x
if self.currentHandle === self.leftHandle {
deltaX *= -1.0
}
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
updatedScale = max(0.01, updatedScale * scaleDelta)
let newAngle: CGFloat
if self.currentHandle === self.leftHandle {
newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x)
} else {
newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x)
}
var delta = newAngle - updatedRotation
if delta < -.pi {
delta = 2.0 * .pi + delta
}
let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0)
} else if self.currentHandle === self.layer {
updatedPosition.x += delta.x
updatedPosition.y += delta.y
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
}
entity.scale = updatedScale
entity.position = updatedPosition
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.setTranslation(.zero, in: entityView)
case .ended, .cancelled:
self.snapTool.reset()
if self.currentHandle != nil {
self.snapTool.rotationReset()
}
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard let entityView = self.entityView as? DrawingLocationEntityView, let entity = entityView.entity as? DrawingLocationEntity else {
return
}
switch gestureRecognizer.state {
case .began, .changed:
if case .began = gestureRecognizer.state {
entityView.onInteractionUpdated(true)
}
let scale = gestureRecognizer.scale
entity.scale = max(0.1, entity.scale * scale)
entityView.update()
gestureRecognizer.scale = 1.0
case .ended, .cancelled:
entityView.onInteractionUpdated(false)
default:
break
}
}
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
guard let entityView = self.entityView as? DrawingLocationEntityView, let entity = entityView.entity as? DrawingLocationEntity else {
return
}
let velocity = gestureRecognizer.velocity
var updatedRotation = entity.rotation
var rotation: CGFloat = 0.0
switch gestureRecognizer.state {
case .began:
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
case .changed:
rotation = gestureRecognizer.rotation
updatedRotation += rotation
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation)
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.rotation = 0.0
case .ended, .cancelled:
self.snapTool.rotationReset()
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
}
override func layoutSubviews() {
let inset = self.selectionInset - 10.0
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
let lineWidth = (1.0 + UIScreenPixel) / self.scale
let handles = [
self.leftHandle,
self.rightHandle
]
for handle in handles {
handle.path = handlePath
handle.bounds = bounds
handle.lineWidth = lineWidth
}
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
let width: CGFloat = self.bounds.width - inset * 2.0
let height: CGFloat = self.bounds.height - inset * 2.0
let cornerRadius: CGFloat = 12.0 - self.scale
let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi))
let count = 12
let relativeDashLength: CGFloat = 0.25
let dashLength = perimeter / CGFloat(count)
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
self.border.lineWidth = 2.0 / self.scale
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath
}
}
@@ -0,0 +1,170 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import AccountContext
import MediaEditor
import Photos
public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMediaView {
private var mediaEntity: DrawingMediaEntity {
return self.entity as! DrawingMediaEntity
}
private var currentSize: CGSize?
private var isVisible = true
private var isPlaying = false
private let snapTool = DrawingEntitySnapTool()
init(context: AccountContext, entity: DrawingMediaEntity) {
super.init(context: context, entity: entity)
self.backgroundColor = UIColor.clear
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
if let self {
self.onSnapUpdated(type, snapped)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func play() {
self.isVisible = true
self.applyVisibility()
}
public override func pause() {
self.isVisible = false
self.applyVisibility()
}
public override func seek(to timestamp: Double) {
self.isVisible = false
self.isPlaying = false
}
override func resetToStart() {
self.isVisible = false
self.isPlaying = false
}
override func updateVisibility(_ visibility: Bool) {
self.isVisible = visibility
self.applyVisibility()
}
private func applyVisibility() {
let isPlaying = self.isVisible
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
}
}
private var didApplyVisibility = false
public override func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
if size.width > 0 && self.currentSize != size {
self.currentSize = size
self.update(animated: false)
}
}
public var updated: (() -> Void)?
public override func update(animated: Bool) {
self.center = self.mediaEntity.position
let size = self.mediaEntity.baseSize
let scale = self.mediaEntity.scale
self.bounds = CGRect(origin: .zero, size: size)
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.mediaEntity.rotation), scale, scale)
super.update(animated: animated)
self.updated?()
}
override func updateSelectionView() {
}
override func makeSelectionView() -> DrawingEntitySelectionView? {
return nil
}
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
let delta = gestureRecognizer.translation(in: self.superview)
var updatedPosition = self.mediaEntity.position
switch gestureRecognizer.state {
case .began, .changed:
updatedPosition.x += delta.x
updatedPosition.y += delta.y
gestureRecognizer.setTranslation(.zero, in: self.superview)
default:
break
}
self.mediaEntity.position = updatedPosition
self.update(animated: false)
}
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
switch gestureRecognizer.state {
case .began, .changed:
let scale = gestureRecognizer.scale
self.mediaEntity.scale = self.mediaEntity.scale * scale
self.update(animated: false)
gestureRecognizer.scale = 1.0
default:
break
}
}
private var beganRotating = false
@objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
var updatedRotation = self.mediaEntity.rotation
var rotation: CGFloat = 0.0
let velocity = gestureRecognizer.velocity
switch gestureRecognizer.state {
case .began:
self.beganRotating = false
self.snapTool.maybeSkipFromStart(entityView: self, rotation: self.mediaEntity.rotation)
case .changed:
rotation = gestureRecognizer.rotation
if self.beganRotating {
updatedRotation += rotation
gestureRecognizer.rotation = 0.0
} else if abs(rotation) >= 0.08 * .pi || abs(self.mediaEntity.rotation) >= 0.03 {
self.beganRotating = true
gestureRecognizer.rotation = 0.0
}
updatedRotation = self.snapTool.update(entityView: self, velocity: velocity, delta: rotation, updatedRotation: updatedRotation)
self.mediaEntity.rotation = updatedRotation
self.update(animated: false)
case .ended, .cancelled:
self.snapTool.rotationReset()
self.beganRotating = false
default:
break
}
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.bounds.contains(point)
}
}
@@ -0,0 +1,715 @@
import Foundation
import UIKit
import QuartzCore
import MetalKit
import Display
import SwiftSignalKit
import AppBundle
import MediaEditor
final class DrawingMetalView: MTKView {
let size: CGSize
private let commandQueue: MTLCommandQueue
fileprivate let library: MTLLibrary
private var pipelineState: MTLRenderPipelineState!
fileprivate var drawable: Drawable?
private var render_target_vertex: MTLBuffer!
private var render_target_uniform: MTLBuffer!
private var markerBrush: Brush?
init?(size: CGSize) {
let mainBundle = Bundle(for: DrawingView.self)
guard let path = mainBundle.path(forResource: "DrawingUIBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let device = MTLCreateSystemDefaultDevice() else {
return nil
}
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
self.library = defaultLibrary
guard let commandQueue = device.makeCommandQueue() else {
return nil
}
self.commandQueue = commandQueue
self.size = size
super.init(frame: CGRect(origin: .zero, size: size), device: device)
self.drawableSize = self.size
self.colorPixelFormat = .bgra8Unorm
self.autoResizeDrawable = false
self.isOpaque = false
self.contentScaleFactor = 1.0
self.isPaused = true
self.preferredFramesPerSecond = 60
self.presentsWithTransaction = true
self.clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
self.setup()
}
override var isHidden: Bool {
didSet {
if self.isHidden {
Queue.mainQueue().after(0.2) {
self.isPaused = true
}
} else {
self.isPaused = self.isHidden
}
}
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func makeTexture(with data: Data) -> MTLTexture? {
let textureLoader = MTKTextureLoader(device: device!)
return try? textureLoader.newTexture(data: data, options: [.SRGB : false])
}
func makeTexture(with image: UIImage) -> MTLTexture? {
if let data = image.pngData() {
return makeTexture(with: data)
} else {
return nil
}
}
func drawInContext(_ cgContext: CGContext) {
guard let texture = self.drawable?.texture, let image = texture.createCGImage() else {
return
}
let rect = CGRect(origin: .zero, size: CGSize(width: image.width, height: image.height))
cgContext.saveGState()
cgContext.translateBy(x: rect.midX, y: rect.midY)
cgContext.scaleBy(x: 1.0, y: -1.0)
cgContext.translateBy(x: -rect.midX, y: -rect.midY)
cgContext.draw(image, in: rect)
cgContext.restoreGState()
}
private func setup() {
self.drawable = Drawable(size: self.size, pixelFormat: self.colorPixelFormat, device: device)
let size = self.size
let w = size.width, h = size.height
let vertices = [
Vertex(position: CGPoint(x: 0 , y: 0), texCoord: CGPoint(x: 0, y: 0)),
Vertex(position: CGPoint(x: w , y: 0), texCoord: CGPoint(x: 1, y: 0)),
Vertex(position: CGPoint(x: 0 , y: h), texCoord: CGPoint(x: 0, y: 1)),
Vertex(position: CGPoint(x: w , y: h), texCoord: CGPoint(x: 1, y: 1)),
]
self.render_target_vertex = self.device?.makeBuffer(bytes: vertices, length: MemoryLayout<Vertex>.stride * vertices.count, options: .cpuCacheModeWriteCombined)
let matrix = Matrix.identity
matrix.scaling(x: 2.0 / Float(size.width), y: -2.0 / Float(size.height), z: 1)
matrix.translation(x: -1, y: 1, z: 0)
self.render_target_uniform = self.device?.makeBuffer(bytes: matrix.m, length: MemoryLayout<Float>.size * 16, options: [])
let vertexFunction = self.library.makeFunction(name: "vertex_render_target")
let fragmentFunction = self.library.makeFunction(name: "fragment_render_target")
let pipelineDescription = MTLRenderPipelineDescriptor()
pipelineDescription.vertexFunction = vertexFunction
pipelineDescription.fragmentFunction = fragmentFunction
pipelineDescription.colorAttachments[0].pixelFormat = self.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()
Queue.mainQueue().after(0.1) {
self.markerBrush?.pushPoint(CGPoint(x: 100.0, y: 100.0), color: DrawingColor.clear, size: 0.0, isEnd: true)
Queue.mainQueue().after(0.1) {
self.clear()
}
}
}
override var frame: CGRect {
get {
return super.frame
} set {
super.frame = newValue
self.drawableSize = self.size
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let drawable = self.drawable, let texture = drawable.texture?.texture else {
return
}
let renderPassDescriptor = MTLRenderPassDescriptor()
let attachment = renderPassDescriptor.colorAttachments[0]
attachment?.clearColor = self.clearColor
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()
commandBuffer?.commit()
commandBuffer?.waitUntilScheduled()
self.currentDrawable?.present()
}
func reset() {
let renderPassDescriptor = MTLRenderPassDescriptor()
let attachment = renderPassDescriptor.colorAttachments[0]
attachment?.clearColor = self.clearColor
attachment?.texture = self.currentDrawable?.texture
attachment?.loadAction = .clear
attachment?.storeAction = .store
let commandBuffer = self.commandQueue.makeCommandBuffer()
let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
commandEncoder?.endEncoding()
commandBuffer?.commit()
commandBuffer?.waitUntilScheduled()
self.currentDrawable?.present()
}
func clear() {
guard let drawable = self.drawable else {
return
}
drawable.updateBuffer(with: self.size)
drawable.clear()
self.reset()
}
enum BrushType {
case marker
}
func updated(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, brush: BrushType, color: DrawingColor, size: CGFloat) {
switch brush {
case .marker:
self.markerBrush?.updated(point, color: color, state: state, size: size)
}
}
}
private class Drawable {
public private(set) var texture: Texture?
internal var pixelFormat: MTLPixelFormat = .bgra8Unorm
internal var size: CGSize
internal var uniform_buffer: MTLBuffer!
internal var renderPassDescriptor: MTLRenderPassDescriptor?
internal var commandBuffer: MTLCommandBuffer?
internal var commandQueue: MTLCommandQueue?
internal var device: MTLDevice?
public init(size: CGSize, pixelFormat: MTLPixelFormat, device: MTLDevice?) {
self.size = size
self.pixelFormat = pixelFormat
self.device = device
self.texture = self.makeTexture()
self.commandQueue = device?.makeCommandQueue()
self.renderPassDescriptor = MTLRenderPassDescriptor()
let attachment = self.renderPassDescriptor?.colorAttachments[0]
attachment?.texture = self.texture?.texture
attachment?.loadAction = .load
attachment?.storeAction = .store
attachment?.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
self.updateBuffer(with: size)
}
func clear() {
self.texture?.clear()
}
func reset() {
self.prepareForDraw()
if let commandEncoder = self.makeCommandEncoder() {
commandEncoder.endEncoding()
}
self.commit(wait: true)
}
internal func updateBuffer(with size: CGSize) {
self.size = size
let matrix = Matrix.identity
self.uniform_buffer = device?.makeBuffer(bytes: matrix.m, length: MemoryLayout<Float>.size * 16, options: [])
}
internal func prepareForDraw() {
if self.commandBuffer == nil {
self.commandBuffer = self.commandQueue?.makeCommandBuffer()
}
}
internal func makeCommandEncoder() -> MTLRenderCommandEncoder? {
guard let commandBuffer = self.commandBuffer, let renderPassDescriptor = self.renderPassDescriptor else {
return nil
}
return commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
}
internal func commit(wait: Bool = false) {
self.commandBuffer?.commit()
if wait {
self.commandBuffer?.waitUntilCompleted()
}
self.commandBuffer = nil
}
internal func makeTexture() -> Texture? {
guard self.size.width * self.size.height > 0, let device = self.device else {
return nil
}
return Texture(device: device, width: Int(self.size.width), height: Int(self.size.height))
}
}
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
private class Brush {
private(set) var texture: MTLTexture?
private(set) var pipelineState: MTLRenderPipelineState!
weak var target: DrawingMetalView?
public enum Rotation {
case fixed(CGFloat)
case random
case ahead
}
var rotation: Rotation
required public init(texture: MTLTexture?, target: DrawingMetalView, rotation: Rotation) {
self.texture = texture
self.target = target
self.rotation = rotation
self.setupPipeline()
}
private func setupPipeline() {
guard let target = self.target, let device = target.device else {
return
}
let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
if let vertex_func = target.library.makeFunction(name: "vertex_point_func") {
renderPipelineDescriptor.vertexFunction = vertex_func
}
if let _ = self.texture {
if let fragment_func = target.library.makeFunction(name: "fragment_point_func") {
renderPipelineDescriptor.fragmentFunction = fragment_func
}
} else {
if let fragment_func = target.library.makeFunction(name: "fragment_point_func_without_texture") {
renderPipelineDescriptor.fragmentFunction = fragment_func
}
}
renderPipelineDescriptor.colorAttachments[0].pixelFormat = target.colorPixelFormat
let attachment = renderPipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.rgbBlendOperation = .add
attachment?.sourceRGBBlendFactor = .sourceAlpha
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
attachment?.alphaBlendOperation = .add
attachment?.sourceAlphaBlendFactor = .one
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
self.pipelineState = try! device.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
}
func render(stroke: Stroke, in drawable: Drawable? = nil) {
let drawable = drawable ?? target?.drawable
guard stroke.lines.count > 0, let target = drawable else {
return
}
target.prepareForDraw()
let commandEncoder = target.makeCommandEncoder()
commandEncoder?.setRenderPipelineState(self.pipelineState)
if let vertex_buffer = stroke.preparedBuffer(rotation: self.rotation) {
commandEncoder?.setVertexBuffer(vertex_buffer, offset: 0, index: 0)
commandEncoder?.setVertexBuffer(target.uniform_buffer, offset: 0, index: 1)
if let texture = texture {
commandEncoder?.setFragmentTexture(texture, index: 0)
}
commandEncoder?.drawPrimitives(type: .point, vertexStart: 0, vertexCount: stroke.vertexCount)
}
commandEncoder?.endEncoding()
}
private let bezier = BezierGenerator()
func updated(_ point: DrawingPoint, color: DrawingColor, state: DrawingGesturePipeline.DrawingGestureState, size: CGFloat) {
let point = point.location
switch state {
case .began:
self.bezier.begin(with: point)
let _ = self.pushPoint(point, color: color, size: size, isEnd: false)
case .changed:
if self.bezier.points.count > 0 && point != lastRenderedPoint {
self.pushPoint(point, color: color, size: size, isEnd: false)
}
case .ended, .cancelled:
if self.bezier.points.count >= 3 {
self.pushPoint(point, color: color, size: size, isEnd: true)
}
self.bezier.finish()
self.lastRenderedPoint = nil
}
}
func setup(_ inputPoints: [CGPoint], color: DrawingColor, size: CGFloat) {
guard inputPoints.count >= 2 else {
return
}
var pointStep: CGFloat
if case .random = self.rotation {
pointStep = size * 0.1
} else {
pointStep = 2.0
}
var lines: [Line] = []
var previousPoint = inputPoints[0]
var points: [CGPoint] = []
self.bezier.begin(with: inputPoints.first!)
for point in inputPoints {
let smoothPoints = self.bezier.pushPoint(point)
points.append(contentsOf: smoothPoints)
}
self.bezier.finish()
guard points.count >= 2 else {
return
}
for i in 1 ..< points.count {
let p = points[i]
if (i == points.count - 1) || pointStep <= 1 || (pointStep > 1 && previousPoint.distance(to: p) >= pointStep) {
let line = Line(start: previousPoint, end: p, pointSize: size, pointStep: pointStep)
lines.append(line)
previousPoint = p
}
}
if let drawable = self.target?.drawable {
let stroke = Stroke(color: color, lines: lines, target: drawable)
self.render(stroke: stroke, in: drawable)
drawable.commit(wait: true)
}
}
private var lastRenderedPoint: CGPoint?
func pushPoint(_ point: CGPoint, color: DrawingColor, size: CGFloat, isEnd: Bool) {
var pointStep: CGFloat
if case .random = self.rotation {
pointStep = size * 0.1
} else {
pointStep = 2.0
}
var lines: [Line] = []
let points = self.bezier.pushPoint(point)
guard points.count >= 2 else {
return
}
var previousPoint = self.lastRenderedPoint ?? points[0]
for i in 1 ..< points.count {
let p = points[i]
if (isEnd && i == points.count - 1) || pointStep <= 1 || (pointStep > 1 && previousPoint.distance(to: p) >= pointStep) {
let line = Line(start: previousPoint, end: p, pointSize: size, pointStep: pointStep)
lines.append(line)
previousPoint = p
}
}
if let drawable = self.target?.drawable {
let stroke = Stroke(color: color, lines: lines, target: drawable)
self.render(stroke: stroke, in: drawable)
drawable.commit()
}
}
}
private class Stroke {
private weak var target: Drawable?
let color: DrawingColor
var lines: [Line] = []
private(set) var vertexCount: Int = 0
private var vertex_buffer: MTLBuffer?
init(color: DrawingColor, lines: [Line] = [], target: Drawable) {
self.color = color
self.lines = lines
self.target = target
let _ = self.preparedBuffer(rotation: .fixed(0))
}
func append(_ lines: [Line]) {
self.lines.append(contentsOf: lines)
self.vertex_buffer = nil
}
func preparedBuffer(rotation: Brush.Rotation) -> MTLBuffer? {
guard !self.lines.isEmpty else {
return nil
}
var vertexes: [Point] = []
self.lines.forEach { (line) in
let count = max(line.length / line.pointStep, 1)
let overlapping = max(1, line.pointSize / line.pointStep)
var renderingColor = self.color
renderingColor.alpha = renderingColor.alpha / overlapping * 5.5
for i in 0 ..< Int(count) {
let index = CGFloat(i)
let x = line.start.x + (line.end.x - line.start.x) * (index / count)
let y = line.start.y + (line.end.y - line.start.y) * (index / count)
var angle: CGFloat = 0
switch rotation {
case let .fixed(a):
angle = a
case .random:
angle = CGFloat.random(in: -CGFloat.pi ... CGFloat.pi)
case .ahead:
angle = line.angle
}
vertexes.append(Point(x: x, y: y, color: renderingColor, size: line.pointSize, angle: angle))
}
}
self.vertexCount = vertexes.count
self.vertex_buffer = self.target?.device?.makeBuffer(bytes: vertexes, length: MemoryLayout<Point>.stride * vertexCount, options: .cpuCacheModeWriteCombined)
return self.vertex_buffer
}
}
class BezierGenerator {
init() {
}
init(beginPoint: CGPoint) {
self.begin(with: beginPoint)
}
func begin(with point: CGPoint) {
self.step = 0
self.points.removeAll()
self.points.append(point)
}
func pushPoint(_ point: CGPoint) -> [CGPoint] {
if point == self.points.last {
return []
}
self.points.append(point)
if self.points.count < 3 {
return []
}
self.step += 1
return self.generateSmoothPathPoints()
}
func finish() {
self.step = 0
self.points.removeAll()
}
var points: [CGPoint] = []
private var step = 0
private func generateSmoothPathPoints() -> [CGPoint] {
var begin: CGPoint
var control: CGPoint
let end = CGPoint.middle(p1: self.points[step], p2: self.points[self.step + 1])
var vertices: [CGPoint] = []
if self.step == 1 {
begin = self.points[0]
let middle1 = CGPoint.middle(p1: self.points[0], p2: self.points[1])
control = CGPoint.middle(p1: middle1, p2: self.points[1])
} else {
begin = CGPoint.middle(p1: self.points[self.step - 1], p2: self.points[self.step])
control = self.points[self.step]
}
let distance = begin.distance(to: end)
let segements = max(Int(distance / 5), 2)
for i in 0 ..< segements {
let t = CGFloat(i) / CGFloat(segements)
vertices.append(begin.quadBezierPoint(to: end, controlPoint: control, t: t))
}
vertices.append(end)
return vertices
}
}
private struct Line {
var start: CGPoint
var end: CGPoint
var pointSize: CGFloat
var pointStep: CGFloat
init(start: CGPoint, end: CGPoint, pointSize: CGFloat, pointStep: CGFloat) {
self.start = start
self.end = end
self.pointSize = pointSize
self.pointStep = pointStep
}
var length: CGFloat {
return self.start.distance(to: self.end)
}
var angle: CGFloat {
return self.end.angle(to: self.start)
}
}
final class Texture {
let buffer: MTLBuffer?
let width: Int
let height: Int
let bytesPerRow: Int
let texture: MTLTexture
init?(device: MTLDevice, width: Int, height: Int) {
let bytesPerPixel = 4
let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm)
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
self.buffer = nil
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget, .shaderRead]
textureDescriptor.storageMode = .shared
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
self.texture = texture
self.clear()
}
func clear() {
let region = MTLRegion(
origin: MTLOrigin(x: 0, y: 0, z: 0),
size: MTLSize(width: self.width, height: self.height, depth: 1)
)
let zeroData = [UInt8](repeating: 0, count: self.bytesPerRow * self.height)
zeroData.withUnsafeBytes { bytes in
self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes.baseAddress!, bytesPerRow: self.bytesPerRow)
}
}
func createCGImage() -> CGImage? {
let dataProvider: CGDataProvider
guard let data = NSMutableData(capacity: self.bytesPerRow * self.height) else {
return nil
}
data.length = self.bytesPerRow * self.height
self.texture.getBytes(data.mutableBytes, bytesPerRow: self.bytesPerRow, bytesPerImage: self.bytesPerRow * self.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.width, height: self.height, depth: 1)), mipmapLevel: 0, slice: 0)
guard let provider = CGDataProvider(data: data as CFData) else {
return nil
}
dataProvider = provider
guard let image = CGImage(
width: Int(self.width),
height: Int(self.height),
bitsPerComponent: 8,
bitsPerPixel: 8 * 4,
bytesPerRow: self.bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
provider: dataProvider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) else {
return nil
}
return image
}
}
@@ -0,0 +1,273 @@
import Foundation
import UIKit
import Display
import MediaEditor
final class NeonTool: DrawingElement {
class RenderView: UIView, DrawingRenderView {
private weak var element: NeonTool?
private var drawScale = CGSize(width: 1.0, height: 1.0)
let shadowLayer = SimpleShapeLayer()
let borderLayer = SimpleShapeLayer()
let fillLayer = SimpleShapeLayer()
func setup(element: NeonTool, size: CGSize, screenSize: CGSize) {
self.element = element
self.backgroundColor = .clear
self.isOpaque = false
self.contentScaleFactor = 1.0
let shadowRadius = element.renderShadowRadius
let strokeWidth = element.renderStrokeWidth
var shadowColor = element.color.toUIColor()
var fillColor: UIColor = .white
if shadowColor.lightness < 0.01 {
fillColor = shadowColor
shadowColor = UIColor(rgb: 0x440881)
}
let bounds = CGRect(origin: .zero, size: size)
self.frame = bounds
self.shadowLayer.frame = bounds
self.shadowLayer.contentsScale = 1.0
self.shadowLayer.backgroundColor = UIColor.clear.cgColor
self.shadowLayer.lineWidth = strokeWidth * 0.5
self.shadowLayer.lineCap = .round
self.shadowLayer.lineJoin = .round
self.shadowLayer.fillColor = fillColor.cgColor
self.shadowLayer.strokeColor = fillColor.cgColor
self.shadowLayer.shadowColor = shadowColor.cgColor
self.shadowLayer.shadowRadius = shadowRadius
self.shadowLayer.shadowOpacity = 1.0
self.shadowLayer.shadowOffset = .zero
self.borderLayer.frame = bounds
self.borderLayer.contentsScale = 1.0
self.borderLayer.lineWidth = strokeWidth
self.borderLayer.lineCap = .round
self.borderLayer.lineJoin = .round
self.borderLayer.fillColor = UIColor.clear.cgColor
self.borderLayer.strokeColor = fillColor.mixedWith(shadowColor, alpha: 0.25).cgColor
self.fillLayer.frame = bounds
self.fillLayer.contentsScale = 1.0
self.fillLayer.fillColor = fillColor.cgColor
self.layer.addSublayer(self.shadowLayer)
self.layer.addSublayer(self.borderLayer)
self.layer.addSublayer(self.fillLayer)
}
fileprivate func updatePath(_ path: CGPath, shadowPath: CGPath) {
self.shadowLayer.path = path
self.shadowLayer.shadowPath = shadowPath
self.borderLayer.path = path
self.fillLayer.path = path
}
}
let uuid: UUID
let drawingSize: CGSize
let color: DrawingColor
let renderStrokeWidth: CGFloat
let renderShadowRadius: CGFloat
let renderLineWidth: CGFloat
let renderColor: UIColor
private var pathStarted = false
private let path = UIBezierPath()
private var activePath: UIBezierPath?
private var addedPaths = 0
fileprivate var renderPath: CGPath?
fileprivate var shadowRenderPath: CGPath?
var translation: CGPoint = .zero
private weak var currentRenderView: DrawingRenderView?
var isValid: Bool {
return self.renderPath != nil
}
var bounds: CGRect {
if let renderPath = self.shadowRenderPath {
return normalizeDrawingRect(renderPath.boundingBoxOfPath.insetBy(dx: -self.renderShadowRadius - 30.0, dy: -self.renderShadowRadius - 30.0), drawingSize: self.drawingSize)
} else {
return .zero
}
}
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) {
self.uuid = UUID()
self.drawingSize = drawingSize
self.color = color
let strokeWidth = min(drawingSize.width, drawingSize.height) * 0.008
let shadowRadius = min(drawingSize.width, drawingSize.height) * 0.02
let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.002)
let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.07)
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
self.renderStrokeWidth = strokeWidth
self.renderShadowRadius = shadowRadius
self.renderLineWidth = lineWidth
self.renderColor = color.withUpdatedAlpha(1.0).toUIColor()
}
func setupRenderView(screenSize: CGSize) -> DrawingRenderView? {
let view = RenderView()
view.setup(element: self, size: self.drawingSize, screenSize: screenSize)
self.currentRenderView = view
return view
}
func setupRenderLayer() -> DrawingRenderLayer? {
return nil
}
func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) {
guard self.addPoint(point, state: state, zoomScale: zoomScale) || state == .ended else {
return
}
if let currentRenderView = self.currentRenderView as? RenderView {
let path = self.path.cgPath.mutableCopy()
if let activePath {
path?.addPath(activePath.cgPath)
}
if let renderPath = path?.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0),
let shadowRenderPath = path?.copy(strokingWithWidth: self.renderLineWidth * 2.0, lineCap: .round, lineJoin: .round, miterLimit: 0.0) {
self.renderPath = renderPath
self.shadowRenderPath = shadowRenderPath
currentRenderView.updatePath(renderPath, shadowPath: shadowRenderPath)
}
}
if state == .ended {
if let activePath = self.activePath {
self.path.append(activePath)
self.renderPath = self.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
} else if self.addedPaths == 0, let point = self.points.first {
self.renderPath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: point.x - self.renderLineWidth / 2.0, y: point.y - self.renderLineWidth / 2.0), size: CGSize(width: self.renderLineWidth, height: self.renderLineWidth)), transform: nil)
}
}
}
func draw(in context: CGContext, size: CGSize) {
guard let path = self.renderPath, let shadowPath = self.shadowRenderPath else {
return
}
context.saveGState()
context.translateBy(x: self.translation.x, y: self.translation.y)
context.setShouldAntialias(true)
context.setBlendMode(.normal)
var shadowColor = self.color.toUIColor()
var fillColor: UIColor = .white
if shadowColor.lightness < 0.01 {
fillColor = shadowColor
shadowColor = UIColor(rgb: 0x440881)
}
let shadowOffset = CGSize(width: 3000.0, height: 3000.0)
context.translateBy(x: -shadowOffset.width, y: -shadowOffset.height)
context.addPath(shadowPath)
context.setLineCap(.round)
context.setFillColor(fillColor.cgColor)
context.setStrokeColor(fillColor.cgColor)
context.setLineWidth(self.renderStrokeWidth * 0.5)
context.setShadow(offset: shadowOffset, blur: self.renderShadowRadius * 1.9, color: shadowColor.withAlphaComponent(0.87).cgColor)
context.drawPath(using: .fillStroke)
context.translateBy(x: shadowOffset.width, y: shadowOffset.height)
context.addPath(path)
context.setShadow(offset: .zero, blur: 0.0, color: UIColor.clear.cgColor)
context.setLineWidth(self.renderStrokeWidth)
context.setStrokeColor(fillColor.mixedWith(shadowColor, alpha: 0.25).cgColor)
context.strokePath()
context.addPath(path)
context.setFillColor(fillColor.cgColor)
context.fillPath()
context.restoreGState()
}
private var points: [CGPoint] = Array(repeating: .zero, count: 4)
private var pointPtr = 0
private func addPoint(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) -> Bool {
let filterDistance: CGFloat = 10.0 / zoomScale
if self.pointPtr == 0 {
self.points[0] = point.location
self.pointPtr += 1
} else {
let previousPoint = self.points[self.pointPtr - 1]
guard previousPoint.distance(to: point.location) > filterDistance else {
return false
}
if self.pointPtr >= 4 {
self.points[3] = self.points[2].point(to: point.location, t: 0.5)
if let bezierPath = self.currentBezierPath(3) {
self.path.append(bezierPath)
self.addedPaths += 1
self.activePath = nil
}
self.points[0] = self.points[3]
self.pointPtr = 1
}
self.points[self.pointPtr] = point.location
self.pointPtr += 1
}
guard let bezierPath = self.currentBezierPath(self.pointPtr - 1) else {
return false
}
self.activePath = bezierPath
return true
}
private func currentBezierPath(_ ctr: Int) -> UIBezierPath? {
switch ctr {
case 0:
return nil
case 1:
let path = UIBezierPath()
path.move(to: self.points[0])
path.addLine(to: self.points[1])
return path
case 2:
let path = UIBezierPath()
path.move(to: self.points[0])
path.addQuadCurve(to: self.points[2], controlPoint: self.points[1])
return path
case 3:
let path = UIBezierPath()
path.move(to: self.points[0])
path.addCurve(to: self.points[3], controlPoint1: self.points[1], controlPoint2: self.points[2])
return path
default:
return nil
}
}
}
@@ -0,0 +1,929 @@
import Foundation
import UIKit
import Display
import MediaEditor
private let activeWidthFactor: CGFloat = 0.7
final class PenTool: DrawingElement {
class RenderView: UIView, DrawingRenderView {
private weak var element: PenTool?
private var isEraser = false
private var accumulationImage: UIImage?
private var activeView: ActiveView?
private var start = 0
private var segmentsCount = 0
private var drawScale = CGSize(width: 1.0, height: 1.0)
func setup(size: CGSize, screenSize: CGSize, isEraser: Bool) {
self.isEraser = isEraser
self.backgroundColor = .clear
self.isOpaque = false
self.contentMode = .redraw
let scale = CGSize(width: 0.33, height: 0.33)
let viewSize = CGSize(width: size.width * scale.width, height: size.height * scale.height)
self.drawScale = CGSize(width: size.width / viewSize.width, height: size.height / viewSize.height)
self.bounds = CGRect(origin: .zero, size: viewSize)
self.transform = CGAffineTransform(scaleX: self.drawScale.width, y: self.drawScale.height)
self.frame = CGRect(origin: .zero, size: size)
self.drawScale.height = self.drawScale.width
let activeView = ActiveView(frame: CGRect(origin: .zero, size: self.bounds.size))
activeView.backgroundColor = .clear
activeView.contentMode = .redraw
activeView.isOpaque = false
activeView.parent = self
self.addSubview(activeView)
self.activeView = activeView
}
func animateArrowPaths(start: CGPoint, direction: CGFloat, length: CGFloat, lineWidth: CGFloat, completion: @escaping () -> Void) {
let scale = min(self.drawScale.width, self.drawScale.height)
let arrowStart = CGPoint(x: start.x / scale, y: start.y / scale)
let arrowLeftPath = UIBezierPath()
arrowLeftPath.move(to: arrowStart)
arrowLeftPath.addLine(to: arrowStart.pointAt(distance: length / scale, angle: direction - 0.45))
let arrowRightPath = UIBezierPath()
arrowRightPath.move(to: arrowStart)
arrowRightPath.addLine(to: arrowStart.pointAt(distance: length / scale, angle: direction + 0.45))
let leftArrowShape = CAShapeLayer()
leftArrowShape.path = arrowLeftPath.cgPath
leftArrowShape.lineWidth = lineWidth / scale
leftArrowShape.strokeColor = self.element?.color.toCGColor()
leftArrowShape.lineCap = .round
leftArrowShape.frame = self.bounds
self.layer.addSublayer(leftArrowShape)
let rightArrowShape = CAShapeLayer()
rightArrowShape.path = arrowRightPath.cgPath
rightArrowShape.lineWidth = lineWidth / scale
rightArrowShape.strokeColor = self.element?.color.toCGColor()
rightArrowShape.lineCap = .round
rightArrowShape.frame = self.bounds
self.layer.addSublayer(rightArrowShape)
leftArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
rightArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, completion: { [weak leftArrowShape, weak rightArrowShape] _ in
completion()
leftArrowShape?.removeFromSuperlayer()
rightArrowShape?.removeFromSuperlayer()
})
}
var onDryingUp: () -> Void = {}
var isDryingUp = false {
didSet {
if !self.isDryingUp {
self.onDryingUp()
}
}
}
var dryingLayersCount: Int = 0 {
didSet {
if self.dryingLayersCount > 0 {
self.isDryingUp = true
} else {
self.isDryingUp = false
}
}
}
fileprivate var displaySize: CGSize?
fileprivate func draw(element: PenTool, rect: CGRect) {
self.element = element
self.alpha = 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 displaySize = self.displaySize ?? CGSize(width: round(self.bounds.size.width), height: round(self.bounds.size.height))
let image = generateImage(displaySize, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
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)
context.scaleBy(x: 1.0 / self.drawScale.width, y: 1.0 / self.drawScale.height)
context.setBlendMode(.copy)
element.drawSegments(in: context, from: self.start, to: newStart)
}, opaque: false)
self.accumulationImage = image
self.layer.contents = image?.cgImage
self.start = newStart
}
if element.hasAnimations {
let count = CGFloat(element.segments.count - self.segmentsCount)
if count > 0 {
let dryingPath = CGMutablePath()
var abFactor: CGFloat = activeWidthFactor * 1.35
let delta: CGFloat = (1.0 - abFactor) / count
for i in self.segmentsCount ..< element.segments.count {
let segmentPath = element.pathForSegment(element.segments[i], abFactor: abFactor, cdFactor: abFactor + delta)
dryingPath.addPath(segmentPath)
abFactor += delta
}
self.setupDrying(path: dryingPath)
}
}
self.segmentsCount = element.segments.count
if let rect = rect {
self.activeView?.setNeedsDisplay(rect.insetBy(dx: -40.0, dy: -40.0).applying(CGAffineTransform(scaleX: 1.0 / self.drawScale.width, y: 1.0 / self.drawScale.height)))
} else {
self.activeView?.setNeedsDisplay()
}
}
private let dryingFactor: CGFloat = 0.4
func setupDrying(path: CGPath) {
guard let element = self.element else {
return
}
let dryingLayer = CAShapeLayer()
dryingLayer.contentsScale = 1.0
dryingLayer.fillColor = element.renderColor.cgColor
dryingLayer.strokeColor = element.renderColor.cgColor
dryingLayer.lineWidth = element.renderLineWidth * self.dryingFactor
dryingLayer.path = path
dryingLayer.animate(from: dryingLayer.lineWidth as NSNumber, to: 0.0 as NSNumber, keyPath: "lineWidth", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.4, removeOnCompletion: false, completion: { [weak dryingLayer] _ in
dryingLayer?.removeFromSuperlayer()
self.dryingLayersCount -= 1
})
dryingLayer.transform = CATransform3DMakeScale(1.0 / self.drawScale.width, 1.0 / self.drawScale.height, 1.0)
dryingLayer.frame = self.bounds
self.layer.addSublayer(dryingLayer)
self.dryingLayersCount += 1
}
private var isActiveDrying = false
func setupActiveSegmentsDrying() {
guard let element = self.element else {
return
}
if element.hasAnimations {
let dryingPath = CGMutablePath()
for segment in element.activeSegments {
let segmentPath = element.pathForSegment(segment)
dryingPath.addPath(segmentPath)
}
self.setupDrying(path: dryingPath)
self.isActiveDrying = true
self.setNeedsDisplay()
}
}
class ActiveView: UIView {
weak var parent: RenderView?
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext(), let parent = self.parent, let element = parent.element else {
return
}
parent.displaySize = rect.size
context.scaleBy(x: 1.0 / parent.drawScale.width, y: 1.0 / parent.drawScale.height)
element.drawSegments(in: context, from: parent.start, to: parent.segmentsCount)
if element.hasAnimations {
element.drawActiveSegments(in: context, strokeWidth: !parent.isActiveDrying ? element.renderLineWidth * parent.dryingFactor : nil)
} else {
element.drawActiveSegments(in: context, strokeWidth: nil)
}
}
}
}
let uuid: UUID
let drawingSize: CGSize
let color: DrawingColor
let renderLineWidth: CGFloat
let renderMinLineWidth: CGFloat
let renderColor: UIColor
let hasArrow: Bool
let renderArrowLength: CGFloat
var 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 currentRenderView: DrawingRenderView?
private var points: [Point] = Array(repeating: Point(location: .zero, width: 0.0), count: 4)
private var pointPtr = 0
private var smoothPoints: [Point] = []
private var activeSmoothPoints: [Point] = []
private var segments: [Segment] = []
private var activeSegments: [Segment] = []
private var previousActiveRect: CGRect?
private var previousRenderLineWidth: CGFloat?
private var segmentPaths: [Int: CGPath] = [:]
private var useCubicBezier = true
private let animationsEnabled: Bool
var hasAnimations: Bool {
return self.animationsEnabled && !self.isEraser && !self.isBlur
}
var isValid: Bool {
if self.hasArrow {
return self.arrowStart != nil && self.arrowDirection != nil
} else {
return self.segments.count > 0
}
}
var bounds: CGRect {
let segmentsBounds = boundingRect(from: 0, to: self.segments.count).insetBy(dx: -20.0, dy: -20.0)
var combinedBounds = segmentsBounds
if self.hasArrow, let arrowLeftPath, let arrowRightPath {
combinedBounds = combinedBounds.union(arrowLeftPath.bounds.insetBy(dx: -renderArrowLineWidth, dy: -renderArrowLineWidth)).union(arrowRightPath.bounds.insetBy(dx: -renderArrowLineWidth, dy: -renderArrowLineWidth)).insetBy(dx: -20.0, dy: -20.0)
}
return normalizeDrawingRect(combinedBounds, drawingSize: self.drawingSize)
}
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, hasArrow: Bool, isEraser: Bool, isBlur: Bool, blurredImage: UIImage?, animationsEnabled: Bool) {
self.uuid = UUID()
self.drawingSize = drawingSize
self.color = isEraser || isBlur ? DrawingColor(rgb: 0x000000) : color
self.hasArrow = hasArrow
self.isEraser = isEraser
self.isBlur = isBlur
self.blurredImage = blurredImage
self.animationsEnabled = animationsEnabled
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 = isEraser || isBlur ? lineWidth : minLineWidth + (lineWidth - minLineWidth) * 0.2
self.renderArrowLength = max(minRenderArrowLength, lineWidth * 3.0)
self.renderArrowLineWidth = max(minLineWidth * 1.8, lineWidth * 0.75)
self.renderColor = color.withUpdatedAlpha(1.0).toUIColor()
}
var isFinishingArrow = false
func finishArrow(_ completion: @escaping () -> Void) {
if let arrowStart, let arrowDirection {
self.isFinishingArrow = true
(self.currentRenderView as? RenderView)?.animateArrowPaths(start: arrowStart, direction: arrowDirection, length: self.renderArrowLength, lineWidth: self.renderArrowLineWidth, completion: { [weak self] in
self?.isFinishingArrow = false
completion()
})
} else {
completion()
}
}
func setupRenderView(screenSize: CGSize) -> DrawingRenderView? {
let view = RenderView()
view.setup(size: self.drawingSize, screenSize: screenSize, isEraser: self.isEraser)
self.currentRenderView = view
return view
}
func setupRenderLayer() -> DrawingRenderLayer? {
return nil
}
func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) {
let result = self.addPoint(point, state: state, zoomScale: zoomScale)
let resetActiveRect = result?.0 ?? false
let updatedRect = result?.1
var combinedRect = updatedRect
if let previousActiveRect = self.previousActiveRect {
combinedRect = updatedRect?.union(previousActiveRect) ?? previousActiveRect
}
if resetActiveRect {
self.previousActiveRect = updatedRect
} else {
self.previousActiveRect = combinedRect
}
if let currentRenderView = self.currentRenderView as? RenderView, let combinedRect {
currentRenderView.draw(element: self, rect: combinedRect)
}
if state == .ended {
if !self.activeSegments.isEmpty {
(self.currentRenderView as? RenderView)?.setupActiveSegmentsDrying()
self.segments.append(contentsOf: self.activeSegments)
self.smoothPoints.append(contentsOf: self.activeSmoothPoints)
}
if self.hasArrow {
var direction: CGFloat?
if self.smoothPoints.count > 4 {
let p2 = self.smoothPoints[self.smoothPoints.count - 1].location
for i in 1 ..< min(self.smoothPoints.count - 2, 200) {
let p1 = self.smoothPoints[self.smoothPoints.count - 1 - i].location
if p1.distance(to: p2) > self.renderArrowLength * 0.5 {
direction = p2.angle(to: p1)
break
}
}
}
self.arrowStart = self.smoothPoints.last?.location
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,
abCenter: CGPoint(x: point.x, y: point.y),
cdCenter: CGPoint(x: point.x, y: point.y + 0.1),
perpendicular: .zero,
rect: CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius * 2.0, height: radius * 2.0))
)
)
}
}
}
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
self.renderArrowLineWidth = self.smoothPoints.last?.width ?? self.renderArrowLineWidth
}
}
func draw(in context: CGContext, size: CGSize) {
guard !self.segments.isEmpty else {
return
}
context.saveGState()
if self.isEraser {
context.setBlendMode(.clear)
} else if self.isBlur {
context.setBlendMode(.normal)
} else {
context.setAlpha(self.color.alpha)
context.setBlendMode(.copy)
}
context.translateBy(x: self.translation.x, y: self.translation.y)
context.setShouldAntialias(true)
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, to: 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, to: self.segments.count)
}
if let arrowLeftPath, let arrowRightPath {
context.setStrokeColor(self.renderColor.cgColor)
context.setLineWidth(self.renderArrowLineWidth)
context.setLineCap(.round)
context.addPath(arrowLeftPath.cgPath)
context.strokePath()
context.addPath(arrowRightPath.cgPath)
context.strokePath()
}
context.restoreGState()
self.segmentPaths = [:]
}
private struct Segment: Codable {
let a: CGPoint
let b: CGPoint
let c: CGPoint
let d: CGPoint
let radius1: CGFloat
let radius2: CGFloat
let abCenter: CGPoint
let cdCenter: CGPoint
let perpendicular: CGPoint
let rect: CGRect
init(
a: CGPoint,
b: CGPoint,
c: CGPoint,
d: CGPoint,
radius1: CGFloat,
radius2: CGFloat,
abCenter: CGPoint,
cdCenter: CGPoint,
perpendicular: CGPoint,
rect: CGRect
) {
self.a = a
self.b = b
self.c = c
self.d = d
self.radius1 = radius1
self.radius2 = radius2
self.abCenter = abCenter
self.cdCenter = cdCenter
self.perpendicular = perpendicular
self.rect = rect
}
func withMultiplied(abFactor: CGFloat, cdFactor: CGFloat) -> Segment {
let a = CGPoint(
x: self.abCenter.x + self.perpendicular.x * self.radius1 * abFactor,
y: self.abCenter.y + self.perpendicular.y * self.radius1 * abFactor
)
let b = CGPoint(
x: self.abCenter.x - self.perpendicular.x * self.radius1 * abFactor,
y: self.abCenter.y - self.perpendicular.y * self.radius1 * abFactor
)
let c = CGPoint(
x: self.cdCenter.x + self.perpendicular.x * self.radius2 * cdFactor,
y: self.cdCenter.y + self.perpendicular.y * self.radius2 * cdFactor
)
let d = CGPoint(
x: self.cdCenter.x - self.perpendicular.x * self.radius2 * cdFactor,
y: self.cdCenter.y - self.perpendicular.y * self.radius2 * cdFactor
)
return Segment(
a: a,
b: b,
c: c,
d: d,
radius1: self.radius1 * abFactor,
radius2: self.radius2 * cdFactor,
abCenter: self.abCenter,
cdCenter: self.cdCenter,
perpendicular: self.perpendicular,
rect: self.rect
)
}
}
private struct Point {
let location: CGPoint
let width: CGFloat
init(
location: CGPoint,
width: CGFloat
) {
self.location = location
self.width = width
}
}
private var currentVelocity: CGFloat?
private func addPoint(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) -> (Bool, CGRect)? {
let filterDistance: CGFloat = 8.0 / zoomScale
var velocity = point.velocity
if velocity.isZero {
velocity = 1000.0
}
self.currentVelocity = velocity
var renderLineWidth = max(self.renderMinLineWidth, min(self.renderLineWidth - (velocity / 200.0), self.renderLineWidth))
if let previousRenderLineWidth = self.previousRenderLineWidth {
renderLineWidth = renderLineWidth * 0.3 + previousRenderLineWidth * 0.7
}
self.previousRenderLineWidth = renderLineWidth
var resetActiveRect = false
var finalizedRect: CGRect?
if self.pointPtr == 0 {
self.points[0] = Point(location: point.location, width: renderLineWidth)
self.pointPtr += 1
} else {
let previousPoint = self.points[self.pointPtr - 1].location
guard previousPoint.distance(to: point.location) > filterDistance else {
return nil
}
if self.pointPtr >= 4 {
self.points[3] = Point(
location: self.points[2].location.point(to: point.location, t: 0.5),
width: self.points[2].width
)
if var smoothPoints = self.currentSmoothPoints(3) {
if let previousSmoothPoint = self.smoothPoints.last {
smoothPoints.insert(previousSmoothPoint, at: 0)
}
let (segments, rect) = self.segments(fromSmoothPoints: smoothPoints)
self.smoothPoints.append(contentsOf: smoothPoints)
self.segments.append(contentsOf: segments)
finalizedRect = rect
self.activeSmoothPoints.removeAll()
self.activeSegments.removeAll()
resetActiveRect = true
}
self.points[0] = self.points[3]
self.pointPtr = 1
}
let point = Point(location: point.location, width: renderLineWidth)
self.points[self.pointPtr] = point
self.pointPtr += 1
}
guard let smoothPoints = self.currentSmoothPoints(self.pointPtr - 1) else {
if let finalizedRect {
return (resetActiveRect, finalizedRect)
} else {
return nil
}
}
let (segments, rect) = self.segments(fromSmoothPoints: smoothPoints)
self.activeSmoothPoints = smoothPoints
self.activeSegments = segments
var combinedRect: CGRect?
if let finalizedRect, let rect {
combinedRect = finalizedRect.union(rect)
} else {
combinedRect = rect ?? finalizedRect
}
if let combinedRect {
return (resetActiveRect, combinedRect)
} else {
return nil
}
}
private func currentSmoothPoints(_ ctr: Int) -> [Point]? {
switch ctr {
case 0:
return nil//return [self.points[0]]
case 1:
return nil//return self.smoothPoints(.line(self.points[0], self.points[1]))
case 2:
return self.smoothPoints(.quad(self.points[0], self.points[1], self.points[2]))
case 3:
return self.smoothPoints(.cubic(self.points[0], self.points[1], self.points[2], self.points[3]))
default:
return nil
}
}
private enum SmootherInput {
case line(Point, Point)
case quad(Point, Point, Point)
case cubic(Point, Point, Point, Point)
var start: Point {
switch self {
case let .line(start, _), let .quad(start, _, _), let .cubic(start, _, _, _):
return start
}
}
var end: Point {
switch self {
case let .line(_, end), let .quad(_, _, end), let .cubic(_, _, _, end):
return end
}
}
var distance: CGFloat {
return self.start.location.distance(to: self.end.location)
}
}
private func smoothPoints(_ input: SmootherInput) -> [Point] {
let segmentDistance: CGFloat = 6.0
let distance = input.distance
let numberOfSegments = min(48, max(floor(distance / segmentDistance), 24))
let step = 1.0 / numberOfSegments
var smoothPoints: [Point] = []
for t in stride(from: 0, to: 1, by: step) {
let point: Point
switch input {
case let .line(start, end):
point = Point(
location: start.location.linearBezierPoint(to: end.location, t: t),
width: CGPoint(x: start.width, y: 0.0).linearBezierPoint(to: CGPoint(x: end.width, y: 0.0), t: t).x
)
case let .quad(start, control, end):
let location = start.location.quadBezierPoint(to: end.location, controlPoint: control.location, t: t)
let width = CGPoint(x: start.width, y: 0.0).quadBezierPoint(to: CGPoint(x: end.width, y: 0.0), controlPoint: CGPoint(x: (start.width + end.width) / 2.0, y: 0.0), t: t).x
point = Point(
location: location,
width: width
)
case let .cubic(start, control1, control2, end):
let location = start.location.cubicBezierPoint(to: end.location, controlPoint1: control1.location, controlPoint2: control2.location, t: t)
let width = CGPoint(x: start.width, y: 0.0).cubicBezierPoint(to: CGPoint(x: end.width, y: 0.0), controlPoint1: CGPoint(x: (start.width + control1.width) / 2.0, y: 0.0), controlPoint2: CGPoint(x: (control2.width + end.width) / 2.0, y: 0.0), t: t).x
point = Point(
location: location,
width: width
)
}
smoothPoints.append(point)
}
smoothPoints.append(input.end)
return smoothPoints
}
fileprivate func boundingRect(from: Int, to: Int) -> CGRect {
var minX: CGFloat = .greatestFiniteMagnitude
var minY: CGFloat = .greatestFiniteMagnitude
var maxX: CGFloat = 0.0
var maxY: CGFloat = 0.0
for i in from ..< to {
let segment = self.segments[i]
if segment.rect.minX < minX {
minX = segment.rect.minX
}
if segment.rect.maxX > maxX {
maxX = segment.rect.maxX
}
if segment.rect.minY < minY {
minY = segment.rect.minY
}
if segment.rect.maxY > maxY {
maxY = segment.rect.maxY
}
}
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
}
private func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect?) {
var segments: [Segment] = []
var updateRect = CGRect.null
for i in 1 ..< smoothPoints.count {
let previousPoint = smoothPoints[i - 1].location
let previousWidth = smoothPoints[i - 1].width
let currentPoint = smoothPoints[i].location
let currentWidth = smoothPoints[i].width
let direction = CGPoint(
x: currentPoint.x - previousPoint.x,
y: currentPoint.y - previousPoint.y
)
guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else {
continue
}
var perpendicular = CGPoint(x: -direction.y, y: direction.x)
let length = perpendicular.length
if length > 0.0 {
perpendicular = CGPoint(
x: perpendicular.x / length,
y: perpendicular.y / length
)
}
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 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)
let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y)
let segmentRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
updateRect = updateRect.union(segmentRect)
let segment = Segment(
a: a,
b: b,
c: c,
d: d,
radius1: previousWidth / 2.0,
radius2: currentWidth / 2.0,
abCenter: abCenter,
cdCenter: cdCenter,
perpendicular: perpendicular,
rect: segmentRect
)
segments.append(segment)
}
return (segments, !updateRect.isNull ? updateRect : nil)
}
private func pathForSegment(_ segment: Segment, abFactor: CGFloat = 1.0, cdFactor: CGFloat = 1.0) -> CGPath {
var segment = segment
if abFactor != 1.0 || cdFactor != 1.0 {
segment = segment.withMultiplied(abFactor: abFactor, cdFactor: cdFactor)
}
let path = CGMutablePath()
path.move(to: segment.b)
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
}
func cachedPathForSegmentIndex(_ i: Int) -> CGPath {
var segmentPath: CGPath
if let current = self.segmentPaths[i] {
segmentPath = current
} else {
let segment = self.segments[i]
let path = self.pathForSegment(segment)
self.segmentPaths[i] = path
segmentPath = path
}
return segmentPath
}
private func drawSegments(in context: CGContext, from: Int, to: Int) {
context.setFillColor(self.renderColor.cgColor)
for i in from ..< to {
let segment = self.segments[i]
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()
}
}
private func drawActiveSegments(in context: CGContext, strokeWidth: CGFloat?) {
context.setFillColor(self.renderColor.cgColor)
if let strokeWidth {
context.setStrokeColor(self.renderColor.cgColor)
context.setLineWidth(strokeWidth)
}
var abFactor: CGFloat = activeWidthFactor
let delta: CGFloat = (1.0 - activeWidthFactor) / CGFloat(self.activeSegments.count + 1)
for segment in self.activeSegments {
let path = self.pathForSegment(segment)
context.addPath(path)
if let _ = strokeWidth {
context.drawPath(using: .fillStroke)
} else {
context.fillPath()
}
abFactor += delta
}
}
}
@@ -0,0 +1,368 @@
import Foundation
import UIKit
import AsyncDisplayKit
import AVFoundation
import Display
import SwiftSignalKit
import TelegramCore
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
import AccountContext
import MediaEditor
import TelegramPresentationData
import ReactionSelectionNode
import UndoUI
import EntityKeyboard
import ComponentFlow
public class DrawingReactionEntityView: DrawingStickerEntityView {
private var backgroundView: UIImageView
private var outlineView: UIImageView
override init(context: AccountContext, entity: DrawingStickerEntity) {
let backgroundView = UIImageView(image: UIImage(bundleImageName: "Stories/ReactionShadow"))
backgroundView.layer.zPosition = -1000.0
let outlineView = UIImageView(image: UIImage(bundleImageName: "Stories/ReactionOutline"))
outlineView.tintColor = .white
backgroundView.addSubview(outlineView)
self.backgroundView = backgroundView
self.outlineView = outlineView
super.init(context: context, entity: entity)
self.insertSubview(backgroundView, at: 0)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var isReaction: Bool {
return true
}
override func animateInsertion() {
super.animateInsertion()
Queue.mainQueue().after(0.2) {
let _ = self.selectedTapAction()
}
}
override func onSelection() {
self.presentReactionSelection()
}
override func onDeselection() {
let _ = self.dismissReactionSelection()
}
public override func update(animated: Bool) {
super.update(animated: animated)
if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type {
switch style {
case .white:
self.outlineView.tintColor = .white
case .black:
self.outlineView.tintColor = UIColor(rgb: 0x000000, alpha: 0.5)
}
}
}
override func updateMirroring(animated: Bool) {
let staticTransform = CATransform3DMakeScale(self.stickerEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0)
if animated {
let isCurrentlyMirrored = ((self.backgroundView.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
var animationSourceTransform = CATransform3DIdentity
var animationTargetTransform = CATransform3DIdentity
if isCurrentlyMirrored {
animationSourceTransform = CATransform3DRotate(animationSourceTransform, .pi, 0.0, 1.0, 0.0)
animationSourceTransform.m34 = -1.0 / self.imageNode.frame.width
}
if self.stickerEntity.mirrored {
animationTargetTransform = CATransform3DRotate(animationTargetTransform, .pi, 0.0, 1.0, 0.0)
animationTargetTransform.m34 = -1.0 / self.imageNode.frame.width
}
self.backgroundView.layer.transform = animationSourceTransform
let values = [1.0, 0.01, 1.0]
let keyTimes = [0.0, 0.5, 1.0]
self.animationNode?.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.25, keyPath: "transform.scale.x", timingFunction: CAMediaTimingFunctionName.linear.rawValue)
UIView.animate(withDuration: 0.25, animations: {
self.backgroundView.layer.transform = animationTargetTransform
}, completion: { finished in
self.backgroundView.layer.transform = staticTransform
})
} else {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.backgroundView.layer.transform = staticTransform
CATransaction.commit()
}
}
private weak var reactionContextNode: ReactionContextNode?
fileprivate func presentReactionSelection() {
guard let containerView = self.containerView, let superview = containerView.superview?.superview?.superview?.superview?.superview?.superview, self.reactionContextNode == nil else {
return
}
let availableSize = superview.frame.size
let reactionItems = containerView.getAvailableReactions()
let insets = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 64.0, right: 0.0)
let layout: (ContainedViewLayoutTransition) -> Void = { [weak self, weak superview] transition in
guard let self, let superview, let reactionContextNode = self.reactionContextNode else {
return
}
let anchorRect = self.convert(self.bounds, to: superview).offsetBy(dx: 0.0, dy: -20.0)
reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition)
}
let reactionContextNodeTransition: ComponentTransition = .immediate
let reactionContextNode: ReactionContextNode
reactionContextNode = ReactionContextNode(
context: self.context,
animationCache: self.context.animationCache,
presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme),
items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) },
selectedItems: Set(),
title: nil,
reactionsLocked: false,
alwaysAllowPremiumReactions: false,
allPresetReactionsAreAvailable: false,
getEmojiContent: { [weak self] animationCache, animationRenderer in
guard let self else {
preconditionFailure()
}
let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
}
return EmojiPagerContentComponent.emojiInputData(
context: self.context,
animationCache: animationCache,
animationRenderer: animationRenderer,
isStandalone: false,
subject: .reaction(onlyTop: false),
hasTrending: false,
topReactionItems: mappedReactionItems,
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId,
selectedItems: Set(),
premiumIfSavedMessages: false
)
},
isExpandedUpdated: { transition in
layout(transition)
},
requestLayout: { transition in
layout(transition)
},
requestUpdateOverlayWantsToBeBelowKeyboard: { transition in
layout(transition)
}
)
reactionContextNode.displayTail = true
reactionContextNode.forceTailToRight = true
reactionContextNode.forceDark = true
self.reactionContextNode = reactionContextNode
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
guard let self else {
return
}
let continueWithAnimationFile: (TelegramMediaFile) -> Void = { [weak self] animation in
guard let self else {
return
}
if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type {
self.stickerEntity.content = .file(.standalone(media: animation), .reaction(updateReaction.reaction, style))
}
var nodeToTransitionOut: ASDisplayNode?
if let animationNode = self.animationNode {
nodeToTransitionOut = animationNode
} else if !self.imageNode.isHidden {
nodeToTransitionOut = self.imageNode
}
if let nodeToTransitionOut, let snapshot = nodeToTransitionOut.view.snapshotView(afterScreenUpdates: false) {
snapshot.frame = nodeToTransitionOut.frame
snapshot.layer.transform = nodeToTransitionOut.transform
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshot.removeFromSuperview()
})
snapshot.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
self.addSubview(snapshot)
}
self.animationNode?.removeFromSupernode()
self.animationNode = nil
self.didSetUpAnimationNode = false
self.isPlaying = false
self.currentSize = nil
self.setup()
self.applyVisibility()
self.setNeedsLayout()
let nodeToTransitionIn: ASDisplayNode?
if let animationNode = self.animationNode {
nodeToTransitionIn = animationNode
} else {
nodeToTransitionIn = self.imageNode
}
if let nodeToTransitionIn {
nodeToTransitionIn.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
nodeToTransitionIn.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
let _ = self.dismissReactionSelection()
}
switch updateReaction {
case .builtin:
let _ = (self.context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { availableReactions in
guard let availableReactions else {
return
}
var animation: TelegramMediaFile?
for reaction in availableReactions.reactions {
if reaction.value == updateReaction.reaction {
animation = reaction.selectAnimation._parse()
break
}
}
if let animation {
continueWithAnimationFile(animation)
}
})
case let .custom(fileId, file):
if let file {
continueWithAnimationFile(file)
} else {
let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|> deliverOnMainQueue).start(next: { files in
if let itemFile = files[fileId] {
continueWithAnimationFile(itemFile)
}
})
}
case .stars:
let _ = (self.context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { availableReactions in
guard let availableReactions else {
return
}
var animation: TelegramMediaFile?
for reaction in availableReactions.reactions {
if reaction.value == updateReaction.reaction {
animation = reaction.selectAnimation._parse()
break
}
}
if let animation {
continueWithAnimationFile(animation)
}
})
}
}
reactionContextNode.premiumReactionsSelected = { [weak self] file in
guard let self else {
return
}
if let file {
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Story_Editor_TooltipPremiumReaction, undoText: nil, customAction: nil), elevatedLayout: true, animateInAsReplacement: false, appearance: UndoOverlayController.Appearance(isBlurred: true), action: { [weak self] action in
if case .info = action, let self {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
self.containerView?.push(controller)
}
return false
})
self.containerView?.present(controller)
} else {
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
self.containerView?.push(controller)
}
}
let anchorRect = self.convert(self.bounds, to: superview).offsetBy(dx: 0.0, dy: -20.0)
reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition)
superview.addSubnode(reactionContextNode)
reactionContextNode.animateIn(from: anchorRect)
}
fileprivate func dismissReactionSelection() -> Bool {
if let reactionContextNode = self.reactionContextNode {
reactionContextNode.animateOut(to: nil, animatingOutToReaction: false)
self.reactionContextNode = nil
Queue.mainQueue().after(0.35) {
reactionContextNode.view.removeFromSuperview()
}
return false
} else {
return true
}
}
override func selectedTapAction() -> Bool {
if case let .file(file, type) = self.stickerEntity.content, case let .reaction(reaction, style) = type {
guard self.reactionContextNode == nil else {
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
let keyTimes = [0.0, 0.33, 1.0]
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
let updatedStyle: DrawingStickerEntity.Content.FileType.ReactionStyle
switch style {
case .white:
updatedStyle = .black
case .black:
updatedStyle = .white
}
self.stickerEntity.content = .file(file, .reaction(reaction, updatedStyle))
self.update(animated: false)
return true
}
self.presentReactionSelection()
return true
} else {
return super.selectedTapAction()
}
}
override func innerLayoutSubview(boundingSize: CGSize) -> CGSize {
self.backgroundView.frame = CGRect(origin: .zero, size: boundingSize).insetBy(dx: -5.0, dy: -5.0)
self.outlineView.frame = backgroundView.bounds
return CGSize(width: floor(boundingSize.width * 0.63), height: floor(boundingSize.width * 0.63))
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,436 @@
import Foundation
import UIKit
import Display
import AccountContext
import MediaEditor
final class DrawingSimpleShapeEntityView: DrawingEntityView {
private var shapeEntity: DrawingSimpleShapeEntity {
return self.entity as! DrawingSimpleShapeEntity
}
private var currentShape: DrawingSimpleShapeEntity.ShapeType?
private var currentSize: CGSize?
private let shapeLayer = SimpleShapeLayer()
init(context: AccountContext, entity: DrawingSimpleShapeEntity) {
super.init(context: context, entity: entity)
self.layer.addSublayer(self.shapeLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func update(animated: Bool) {
let shapeType = self.shapeEntity.shapeType
let size = self.shapeEntity.size
self.center = self.shapeEntity.position
self.bounds = CGRect(origin: .zero, size: size)
self.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation)
if shapeType != self.currentShape || size != self.currentSize {
self.currentShape = shapeType
self.currentSize = size
self.shapeLayer.frame = self.bounds
let rect = CGRect(origin: .zero, size: size).insetBy(dx: maxLineWidth * 0.5, dy: maxLineWidth * 0.5)
switch shapeType {
case .rectangle:
self.shapeLayer.path = CGPath(rect: rect, transform: nil)
case .ellipse:
self.shapeLayer.path = CGPath(ellipseIn: rect, transform: nil)
case .star:
self.shapeLayer.path = CGPath.star(in: rect, extrusion: size.width * 0.2, points: 5)
}
}
switch self.shapeEntity.drawType {
case .fill:
self.shapeLayer.fillColor = self.shapeEntity.color.toCGColor()
self.shapeLayer.strokeColor = UIColor.clear.cgColor
case .stroke:
let minLineWidth = max(10.0, max(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.01)
let maxLineWidth = self.maxLineWidth
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.shapeEntity.lineWidth
self.shapeLayer.fillColor = UIColor.clear.cgColor
self.shapeLayer.strokeColor = self.shapeEntity.color.toCGColor()
self.shapeLayer.lineWidth = lineWidth
}
super.update(animated: animated)
}
fileprivate var visualLineWidth: CGFloat {
return self.shapeLayer.lineWidth
}
fileprivate var maxLineWidth: CGFloat {
return max(10.0, max(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.05)
}
fileprivate var minimumSize: CGSize {
let minSize = min(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height)
return CGSize(width: minSize * 0.2, height: minSize * 0.2)
}
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)
if expandedBounds.contains(point) {
return true
}
return false
}
override func precisePoint(inside point: CGPoint) -> Bool {
if case .stroke = self.shapeEntity.drawType, var path = self.shapeLayer.path {
path = path.copy(strokingWithWidth: self.maxLineWidth * 0.8, lineCap: .square, lineJoin: .bevel, miterLimit: 0.0)
if path.contains(point) {
return true
} else {
return false
}
} else {
return super.precisePoint(inside: point)
}
}
override func updateSelectionView() {
super.updateSelectionView()
guard let selectionView = self.selectionView as? DrawingSimpleShapeEntitySelectionView else {
return
}
// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
// selectionView.scale = scale
selectionView.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation)
}
override func makeSelectionView() -> DrawingEntitySelectionView? {
if let selectionView = self.selectionView {
return selectionView
}
let selectionView = DrawingSimpleShapeEntitySelectionView()
selectionView.entityView = self
return selectionView
}
func getRenderImage() -> UIImage? {
let rect = self.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0)
self.drawHierarchy(in: rect, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
override var selectionBounds: CGRect {
return self.bounds.insetBy(dx: self.maxLineWidth * 0.5, dy: self.maxLineWidth * 0.5)
}
}
final class DrawingSimpleShapeEntitySelectionView: DrawingEntitySelectionView {
private let leftHandle = SimpleShapeLayer()
private let topLeftHandle = SimpleShapeLayer()
private let topHandle = SimpleShapeLayer()
private let topRightHandle = SimpleShapeLayer()
private let rightHandle = SimpleShapeLayer()
private let bottomLeftHandle = SimpleShapeLayer()
private let bottomHandle = SimpleShapeLayer()
private let bottomRightHandle = SimpleShapeLayer()
override init(frame: CGRect) {
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
let handles = [
self.leftHandle,
self.topLeftHandle,
self.topHandle,
self.topRightHandle,
self.rightHandle,
self.bottomLeftHandle,
self.bottomHandle,
self.bottomRightHandle
]
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
for handle in handles {
handle.bounds = handleBounds
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
handle.rasterizationScale = UIScreen.main.scale
handle.shouldRasterize = true
self.layer.addSublayer(handle)
}
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
if let self, let entityView = self.entityView {
entityView.onSnapUpdated(type, snapped)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scale: CGFloat = 1.0 {
didSet {
self.setNeedsLayout()
}
}
override var selectionInset: CGFloat {
return 5.5
}
private let snapTool = DrawingEntitySnapTool()
private var currentHandle: CALayer?
override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let entityView = self.entityView as? DrawingSimpleShapeEntityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
return
}
let isAspectLocked = [.star].contains(entity.shapeType)
let location = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
if let sublayers = self.layer.sublayers {
for layer in sublayers {
if layer.frame.contains(location) {
self.currentHandle = layer
entityView.onInteractionUpdated(true)
return
}
}
}
self.currentHandle = self.layer
entityView.onInteractionUpdated(true)
case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview)
var updatedSize = entity.size
var updatedPosition = entity.position
let minimumSize = entityView.minimumSize
if self.currentHandle != nil && self.currentHandle !== self.layer {
if gestureRecognizer.numberOfTouches > 1 {
return
}
}
if self.currentHandle === self.leftHandle {
let deltaX = delta.x * cos(entity.rotation)
let deltaY = delta.x * sin(entity.rotation)
updatedSize.width = max(minimumSize.width, updatedSize.width - deltaX)
updatedPosition.x -= deltaX * -0.5
updatedPosition.y -= deltaY * -0.5
if isAspectLocked {
updatedSize.height = updatedSize.width
}
} else if self.currentHandle === self.rightHandle {
let deltaX = delta.x * cos(entity.rotation)
let deltaY = delta.x * sin(entity.rotation)
updatedSize.width = max(minimumSize.width, updatedSize.width + deltaX)
print(updatedSize.width)
updatedPosition.x += deltaX * 0.5
updatedPosition.y += deltaY * 0.5
if isAspectLocked {
updatedSize.height = updatedSize.width
}
} else if self.currentHandle === self.topHandle {
let deltaX = delta.y * sin(entity.rotation)
let deltaY = delta.y * cos(entity.rotation)
updatedSize.height = max(minimumSize.height, updatedSize.height - deltaY)
updatedPosition.x += deltaX * 0.5
updatedPosition.y += deltaY * 0.5
if isAspectLocked {
updatedSize.width = updatedSize.height
}
} else if self.currentHandle === self.bottomHandle {
let deltaX = delta.y * sin(entity.rotation)
let deltaY = delta.y * cos(entity.rotation)
updatedSize.height = max(minimumSize.height, updatedSize.height + deltaY)
updatedPosition.x += deltaX * 0.5
updatedPosition.y += deltaY * 0.5
if isAspectLocked {
updatedSize.width = updatedSize.height
}
} else if self.currentHandle === self.topLeftHandle {
var delta = delta
if isAspectLocked {
delta = CGPoint(x: delta.x, y: delta.x)
}
updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
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 = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
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 = max(minimumSize.width, updatedSize.width - delta.x)
updatedPosition.x -= delta.x * -0.5
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 = max(minimumSize.width, updatedSize.width + delta.x)
updatedPosition.x += delta.x * 0.5
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
updatedPosition.y += delta.y
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
}
entity.size = updatedSize
entity.position = updatedPosition
entityView.update(animated: false)
gestureRecognizer.setTranslation(.zero, in: entityView)
case .ended, .cancelled:
self.snapTool.reset()
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
return
}
switch gestureRecognizer.state {
case .began, .changed:
if case .began = gestureRecognizer.state {
entityView.onInteractionUpdated(true)
}
let scale = gestureRecognizer.scale
entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale)
entityView.update()
gestureRecognizer.scale = 1.0
case .ended, .cancelled:
entityView.onInteractionUpdated(false)
default:
break
}
}
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
return
}
let velocity = gestureRecognizer.velocity
var updatedRotation = entity.rotation
var rotation: CGFloat = 0.0
switch gestureRecognizer.state {
case .began:
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
case .changed:
rotation = gestureRecognizer.rotation
updatedRotation += rotation
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation)
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.rotation = 0.0
case .ended, .cancelled:
self.snapTool.rotationReset()
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
}
override func layoutSubviews() {
let inset = self.selectionInset
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
let lineWidth = (1.0 + UIScreenPixel) / self.scale
let handles = [
self.leftHandle,
self.topLeftHandle,
self.topHandle,
self.topRightHandle,
self.rightHandle,
self.bottomLeftHandle,
self.bottomHandle,
self.bottomRightHandle
]
for handle in handles {
handle.path = handlePath
handle.bounds = bounds
handle.lineWidth = lineWidth
}
self.topLeftHandle.position = CGPoint(x: inset, y: inset)
self.topHandle.position = CGPoint(x: self.bounds.midX, y: inset)
self.topRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: inset)
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
self.bottomLeftHandle.position = CGPoint(x: inset, y: self.bounds.maxY - inset)
self.bottomHandle.position = CGPoint(x: self.bounds.midX, y: self.bounds.maxY - inset)
self.bottomRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.maxY - inset)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,151 @@
import Foundation
import UIKit
import Display
import MediaEditor
final class MarkerTool: DrawingElement {
let uuid: UUID
let drawingSize: CGSize
let color: DrawingColor
let renderLineWidth: CGFloat
var translation = CGPoint()
var points: [CGPoint] = []
weak var metalView: DrawingMetalView?
var isValid: Bool {
return self.points.count > 6
}
var bounds: CGRect {
var minX: CGFloat = .greatestFiniteMagnitude
var minY: CGFloat = .greatestFiniteMagnitude
var maxX: CGFloat = 0.0
var maxY: CGFloat = 0.0
for point in self.points {
if point.x < minX {
minX = point.x
}
if point.x > maxX {
maxX = point.x
}
if point.y < minY {
minY = point.y
}
if point.y > maxY {
maxY = point.y
}
}
return normalizeDrawingRect(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY).insetBy(dx: -80.0, dy: -80.0), drawingSize: self.drawingSize)
}
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) {
self.uuid = UUID()
self.drawingSize = drawingSize
self.color = color
let minLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.01)
let maxLineWidth = max(20.0, max(drawingSize.width, drawingSize.height) * 0.09)
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
self.renderLineWidth = lineWidth
}
func setupRenderView(screenSize: CGSize) -> DrawingRenderView? {
return nil
}
func setupRenderLayer() -> DrawingRenderLayer? {
return nil
}
private var didSetup = false
func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) {
let filterDistance: CGFloat = 10.0 / zoomScale
if let lastPoint = self.points.last, lastPoint.distance(to: point.location) < filterDistance {
} else {
self.points.append(point.location)
}
self.didSetup = true
self.metalView?.updated(point, state: state, brush: .marker, color: self.color, size: self.renderLineWidth)
}
func draw(in context: CGContext, size: CGSize) {
guard !self.points.isEmpty else {
return
}
context.saveGState()
context.translateBy(x: self.translation.x, y: self.translation.y)
self.metalView?.drawInContext(context)
self.metalView?.clear()
context.restoreGState()
}
}
final class FillTool: DrawingElement {
let uuid: UUID
let drawingSize: CGSize
let color: DrawingColor
let isBlur: Bool
var blurredImage: UIImage?
var translation = CGPoint()
var isValid: Bool {
return true
}
var bounds: CGRect {
return CGRect(origin: .zero, size: self.drawingSize)
}
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
}
func setupRenderView(screenSize: CGSize) -> DrawingRenderView? {
return nil
}
func setupRenderLayer() -> DrawingRenderLayer? {
return nil
}
func updatePath(_ path: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) {
}
func draw(in context: CGContext, size: CGSize) {
context.setShouldAntialias(false)
context.setBlendMode(.copy)
if self.isBlur {
if let blurredImage = self.blurredImage?.cgImage {
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 {
context.setFillColor(self.color.toCGColor())
context.fill(CGRect(origin: .zero, size: size))
}
context.setBlendMode(.normal)
}
}
@@ -0,0 +1,546 @@
import Foundation
import UIKit
import QuartzCore
import simd
import MediaEditor
extension UIBezierPath {
convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) {
self.init()
let path = CGMutablePath()
let topLeft = rect.origin
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
if topLeftRadius != .zero {
path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
} else {
path.move(to: CGPoint(x: topLeft.x, y: topLeft.y))
}
if topRightRadius != .zero {
path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y))
path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius))
} else {
path.addLine(to: CGPoint(x: topRight.x, y: topRight.y))
}
if bottomRightRadius != .zero {
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius))
path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y))
} else {
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y))
}
if bottomLeftRadius != .zero {
path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y))
path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius))
} else {
path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y))
}
if topLeftRadius != .zero {
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius))
path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
} else {
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y))
}
path.closeSubpath()
self.cgPath = path
}
}
extension CGPoint {
func isEqual(to point: CGPoint, epsilon: CGFloat) -> Bool {
if x - epsilon <= point.x && point.x <= x + epsilon && y - epsilon <= point.y && point.y <= y + epsilon {
return true
}
return false
}
static public func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static public func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static public func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
}
static public func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x / rhs, y: lhs.y / rhs)
}
var length: CGFloat {
return sqrt(self.x * self.x + self.y * self.y)
}
static func middle(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5)
}
func distance(to point: CGPoint) -> CGFloat {
return sqrt(pow((point.x - self.x), 2) + pow((point.y - self.y), 2))
}
func distanceSquared(to point: CGPoint) -> CGFloat {
return pow((point.x - self.x), 2) + pow((point.y - self.y), 2)
}
func angle(to point: CGPoint) -> CGFloat {
return atan2((point.y - self.y), (point.x - self.x))
}
func pointAt(distance: CGFloat, angle: CGFloat) -> CGPoint {
return CGPoint(x: distance * cos(angle) + self.x, y: distance * sin(angle) + self.y)
}
func point(to point: CGPoint, t: CGFloat) -> CGPoint {
return CGPoint(x: self.x + t * (point.x - self.x), y: self.y + t * (point.y - self.y))
}
func perpendicularPointOnLine(start: CGPoint, end: CGPoint) -> CGPoint {
let l2 = start.distanceSquared(to: end)
if l2.isZero {
return start
}
let t = ((self.x - start.x) * (end.x - start.x) + (self.y - start.y) * (end.y - start.y)) / l2
return CGPoint(x: start.x + t * (end.x - start.x), y: start.y + t * (end.y - start.y))
}
func linearBezierPoint(to: CGPoint, t: CGFloat) -> CGPoint {
let dx = to.x - x;
let dy = to.y - y;
let px = x + (t * dx);
let py = y + (t * dy);
return CGPoint(x: px, y: py)
}
fileprivate func _cubicBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat {
let _t = 1 - t;
let _t2 = _t * _t;
let _t3 = _t * _t * _t ;
let t2 = t * t;
let t3 = t * t * t;
return _t3 * start +
3.0 * _t2 * t * c1 +
3.0 * _t * t2 * c2 +
t3 * end;
}
func cubicBezierPoint(to: CGPoint, controlPoint1 c1: CGPoint, controlPoint2 c2: CGPoint, t: CGFloat) -> CGPoint {
let x = _cubicBezier(t, self.x, c1.x, c2.x, to.x);
let y = _cubicBezier(t, self.y, c1.y, c2.y, to.y);
return CGPoint(x: x, y: y);
}
fileprivate func _quadBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ end: CGFloat) -> CGFloat {
let _t = 1 - t;
let _t2 = _t * _t;
let t2 = t * t;
return _t2 * start +
2 * _t * t * c1 +
t2 * end;
}
func quadBezierPoint(to: CGPoint, controlPoint: CGPoint, t: CGFloat) -> CGPoint {
let x = _quadBezier(t, self.x, controlPoint.x, to.x);
let y = _quadBezier(t, self.y, controlPoint.y, to.y);
return CGPoint(x: x, y: y);
}
}
extension CGPath {
static func star(in rect: CGRect, extrusion: CGFloat, points: Int = 5) -> CGPath {
func pointFrom(angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint {
return CGPoint(x: radius * cos(angle) + offset.x, y: radius * sin(angle) + offset.y)
}
let path = CGMutablePath()
let center = rect.center.offsetBy(dx: 0.0, dy: rect.height * 0.05)
var angle: CGFloat = -CGFloat(.pi / 2.0)
let angleIncrement = CGFloat(.pi * 2.0 / Double(points))
let radius = rect.width / 2.0
var firstPoint = true
for _ in 1 ... points {
let point = center.pointAt(distance: radius, angle: angle)
let nextPoint = center.pointAt(distance: radius, angle: angle + angleIncrement)
let midPoint = center.pointAt(distance: extrusion, angle: angle + angleIncrement * 0.5)
if firstPoint {
firstPoint = false
path.move(to: point)
}
path.addLine(to: midPoint)
path.addLine(to: nextPoint)
angle += angleIncrement
}
path.closeSubpath()
return path
}
static func arrow(from point: CGPoint, controlPoint: CGPoint, width: CGFloat, height: CGFloat, isOpen: Bool) -> CGPath {
let angle = atan2(point.y - controlPoint.y, point.x - controlPoint.x)
let angleAdjustment = atan2(width, -height)
let distance = hypot(width, height)
let path = CGMutablePath()
path.move(to: point)
path.addLine(to: point.pointAt(distance: distance, angle: angle - angleAdjustment))
if isOpen {
path.addLine(to: point)
}
path.addLine(to: point.pointAt(distance: distance, angle: angle + angleAdjustment))
if isOpen {
path.addLine(to: point)
} else {
path.closeSubpath()
}
return path
}
static func curve(start: CGPoint, end: CGPoint, mid: CGPoint, lineWidth: CGFloat?, arrowSize: CGSize?, twoSided: Bool = false) -> CGPath {
let linePath = CGMutablePath()
let controlPoints = configureControlPoints(data: [start, mid, end])
var lineStart = start
if let arrowSize = arrowSize, twoSided {
lineStart = start.pointAt(distance: -arrowSize.height * 0.5, angle: controlPoints[0].ctrl1.angle(to: start))
}
linePath.move(to: lineStart)
linePath.addCurve(to: mid, control1: controlPoints[0].ctrl1, control2: controlPoints[0].ctrl2)
var lineEnd = end
if let arrowSize = arrowSize {
lineEnd = end.pointAt(distance: -arrowSize.height * 0.5, angle: controlPoints[1].ctrl1.angle(to: end))
}
linePath.addCurve(to: lineEnd, control1: controlPoints[1].ctrl1, control2: controlPoints[1].ctrl2)
let path: CGMutablePath
if let lineWidth = lineWidth, let mutablePath = linePath.copy(strokingWithWidth: lineWidth, lineCap: .square, lineJoin: .round, miterLimit: 0.0).mutableCopy() {
path = mutablePath
} else {
path = linePath
}
if let arrowSize = arrowSize {
let arrowPath = arrow(from: end, controlPoint: controlPoints[1].ctrl1, width: arrowSize.width, height: arrowSize.height, isOpen: false)
path.addPath(arrowPath)
if twoSided {
let secondArrowPath = arrow(from: start, controlPoint: controlPoints[0].ctrl1, width: arrowSize.width, height: arrowSize.height, isOpen: false)
path.addPath(secondArrowPath)
}
}
return path
}
static func bubble(in rect: CGRect, cornerRadius: CGFloat, smallCornerRadius: CGFloat, tailPosition: CGPoint, tailWidth: CGFloat) -> CGPath {
let r1 = min(cornerRadius, min(rect.width, rect.height) / 3.0)
let r2 = min(smallCornerRadius, min(rect.width, rect.height) / 10.0)
let ax = tailPosition.x * rect.width
let ay = tailPosition.y
let width = min(max(tailWidth, ay / 2.0), rect.width / 4.0)
let angle = atan2(ay, width)
let h = r2 / tan(angle / 2.0)
let r1a = min(r1, min(rect.maxX - ax, ax - rect.minX) * 0.5)
let r2a = min(r2, min(rect.maxX - ax, ax - rect.minX) * 0.2)
let path = CGMutablePath()
path.addArc(center: CGPoint(x: rect.minX + r1, y: rect.minY + r1), radius: r1, startAngle: .pi, endAngle: .pi * 3.0 / 2.0, clockwise: false)
path.addArc(center: CGPoint(x: rect.maxX - r1, y: rect.minY + r1), radius: r1, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false)
if ax > rect.width / 2.0 {
if ax < rect.width - 1 {
path.addArc(center: CGPoint(x: rect.maxX - r1a, y: rect.maxY - r1a), radius: r1a, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false)
path.addArc(center: CGPoint(x: rect.minX + ax + r2a, y: rect.maxY + r2a), radius: r2a, startAngle: .pi * 3.0 / 2.0, endAngle: .pi, clockwise: true)
}
path.addLine(to: CGPoint(x: rect.minX + ax, y: rect.maxY + ay))
path.addArc(center: CGPoint(x: rect.minX + ax - width - r2, y: rect.maxY + h), radius: h, startAngle: -(CGFloat.pi / 2 - angle), endAngle: CGFloat.pi * 3 / 2, clockwise: true)
path.addArc(center: CGPoint(x: rect.minX + r1, y: rect.maxY - r1), radius: r1, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false)
} else {
path.addArc(center: CGPoint(x: rect.maxX - r1, y: rect.maxY - r1), radius: r1, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false)
path.addArc(center: CGPoint(x: rect.minX + ax + width + r2, y: rect.maxY + h), radius: h, startAngle: CGFloat.pi * 3 / 2, endAngle: CGFloat.pi * 3 / 2 - angle, clockwise: true)
path.addLine(to: CGPoint(x: rect.minX + ax, y: rect.maxY + ay))
if ax > 1 {
path.addArc(center: CGPoint(x: rect.minX + ax - r2a, y: rect.maxY + r2a), radius: r2a, startAngle: 0, endAngle: CGFloat.pi * 3 / 2, clockwise: true)
path.addArc(center: CGPoint(x: rect.minX + r1a, y: rect.maxY - r1a), radius: r1a, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false)
}
}
path.closeSubpath()
return path
}
}
private func configureControlPoints(data: [CGPoint]) -> [(ctrl1: CGPoint, ctrl2: CGPoint)] {
let segments = data.count - 1
if segments == 1 {
let p0 = data[0]
let p3 = data[1]
return [(p0, p3)]
} else if segments > 1 {
var ad: [CGFloat] = []
var d: [CGFloat] = []
var bd: [CGFloat] = []
var rhsArray: [CGPoint] = []
for i in 0 ..< segments {
var rhsXValue: CGFloat = 0.0
var rhsYValue: CGFloat = 0.0
let p0 = data[i]
let p3 = data[i + 1]
if i == 0 {
bd.append(0.0)
d.append(2.0)
ad.append(1.0)
rhsXValue = p0.x + 2.0 * p3.x
rhsYValue = p0.y + 2.0 * p3.y
} else if i == segments - 1 {
bd.append(2.0)
d.append(7.0)
ad.append(0.0)
rhsXValue = 8.0 * p0.x + p3.x
rhsYValue = 8.0 * p0.y + p3.y
} else {
bd.append(1.0)
d.append(4.0)
ad.append(1.0)
rhsXValue = 4.0 * p0.x + 2.0 * p3.x
rhsYValue = 4.0 * p0.y + 2.0 * p3.y
}
rhsArray.append(CGPoint(x: rhsXValue, y: rhsYValue))
}
var firstControlPoints: [CGPoint?] = []
var secondControlPoints: [CGPoint?] = []
var controlPoints : [(CGPoint, CGPoint)] = []
var solutionSet1 = [CGPoint?]()
solutionSet1 = Array(repeating: nil, count: segments)
ad[0] = ad[0] / d[0]
rhsArray[0].x = rhsArray[0].x / d[0]
rhsArray[0].y = rhsArray[0].y / d[0]
if segments > 2 {
for i in 1...segments - 2 {
let rhsValueX = rhsArray[i].x
let prevRhsValueX = rhsArray[i - 1].x
let rhsValueY = rhsArray[i].y
let prevRhsValueY = rhsArray[i - 1].y
ad[i] = ad[i] / (d[i] - bd[i] * ad[i - 1]);
let exp1x = (rhsValueX - (bd[i] * prevRhsValueX))
let exp1y = (rhsValueY - (bd[i] * prevRhsValueY))
let exp2 = (d[i] - bd[i] * ad[i - 1])
rhsArray[i].x = exp1x / exp2
rhsArray[i].y = exp1y / exp2
}
}
let lastElementIndex = segments - 1
let exp1 = (rhsArray[lastElementIndex].x - bd[lastElementIndex] * rhsArray[lastElementIndex - 1].x)
let exp1y = (rhsArray[lastElementIndex].y - bd[lastElementIndex] * rhsArray[lastElementIndex - 1].y)
let exp2 = (d[lastElementIndex] - bd[lastElementIndex] * ad[lastElementIndex - 1])
rhsArray[lastElementIndex].x = exp1 / exp2
rhsArray[lastElementIndex].y = exp1y / exp2
solutionSet1[lastElementIndex] = rhsArray[lastElementIndex]
for i in (0..<lastElementIndex).reversed() {
let controlPointX = rhsArray[i].x - (ad[i] * solutionSet1[i + 1]!.x)
let controlPointY = rhsArray[i].y - (ad[i] * solutionSet1[i + 1]!.y)
solutionSet1[i] = CGPoint(x: controlPointX, y: controlPointY)
}
firstControlPoints = solutionSet1
for i in (0..<segments) {
if i == (segments - 1) {
let lastDataPoint = data[i + 1]
let p1 = firstControlPoints[i]
guard let controlPoint1 = p1 else { continue }
let controlPoint2X = 0.5 * (lastDataPoint.x + controlPoint1.x)
let controlPoint2y = 0.5 * (lastDataPoint.y + controlPoint1.y)
let controlPoint2 = CGPoint(x: controlPoint2X, y: controlPoint2y)
secondControlPoints.append(controlPoint2)
}else {
let dataPoint = data[i+1]
let p1 = firstControlPoints[i+1]
guard let controlPoint1 = p1 else { continue }
let controlPoint2X = 2*dataPoint.x - controlPoint1.x
let controlPoint2Y = 2*dataPoint.y - controlPoint1.y
secondControlPoints.append(CGPoint(x: controlPoint2X, y: controlPoint2Y))
}
}
for i in (0..<segments) {
guard let firstControlPoint = firstControlPoints[i] else { continue }
guard let secondControlPoint = secondControlPoints[i] else { continue }
controlPoints.append((firstControlPoint, secondControlPoint))
}
return controlPoints
}
return []
}
class Matrix {
private(set) var m: [Float]
private init() {
m = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
}
@discardableResult
func translation(x: Float, y: Float, z: Float) -> Matrix {
m[12] = x
m[13] = y
m[14] = z
return self
}
@discardableResult
func scaling(x: Float, y: Float, z: Float) -> Matrix {
m[0] = x
m[5] = y
m[10] = z
return self
}
static var identity = Matrix()
}
struct Vertex {
var position: vector_float4
var texCoord: vector_float2
init(position: CGPoint, texCoord: CGPoint) {
self.position = position.toFloat4()
self.texCoord = texCoord.toFloat2()
}
}
struct Point {
var position: vector_float4
var color: vector_float4
var angle: Float
var size: Float
init(x: CGFloat, y: CGFloat, color: DrawingColor, size: CGFloat, angle: CGFloat = 0) {
self.position = vector_float4(Float(x), Float(y), 0, 1)
self.size = Float(size)
self.color = color.toFloat4()
self.angle = Float(angle)
}
}
extension CGPoint {
func toFloat4(z: CGFloat = 0, w: CGFloat = 1) -> vector_float4 {
return [Float(x), Float(y), Float(z) ,Float(w)]
}
func toFloat2() -> vector_float2 {
return [Float(x), Float(y)]
}
func offsetBy(_ offset: CGPoint) -> CGPoint {
return self.offsetBy(dx: offset.x, dy: offset.y)
}
}
func normalizeDrawingRect(_ rect: CGRect, drawingSize: CGSize) -> CGRect {
var rect = rect
if rect.origin.x < 0.0 {
rect.size.width += rect.origin.x
rect.origin.x = 0.0
}
if rect.origin.y < 0.0 {
rect.size.height += rect.origin.y
rect.origin.y = 0.0
}
if rect.maxX > drawingSize.width {
rect.size.width -= (rect.maxX - drawingSize.width)
}
if rect.maxY > drawingSize.height {
rect.size.height -= (rect.maxY - drawingSize.height)
}
return rect
}
extension CATransform3D {
func decompose() -> (translation: SIMD3<Float>, rotation: SIMD3<Float>, scale: SIMD3<Float>) {
let m0 = SIMD3<Float>(Float(self.m11), Float(self.m12), Float(self.m13))
let m1 = SIMD3<Float>(Float(self.m21), Float(self.m22), Float(self.m23))
let m2 = SIMD3<Float>(Float(self.m31), Float(self.m32), Float(self.m33))
let m3 = SIMD3<Float>(Float(self.m41), Float(self.m42), Float(self.m43))
let t = m3
let sx = length(m0)
let sy = length(m1)
let sz = length(m2)
let s = SIMD3<Float>(sx, sy, sz)
let rx = m0 / sx
let ry = m1 / sy
let rz = m2 / sz
let pitch = atan2(ry.z, rz.z)
let yaw = atan2(-rx.z, hypot(ry.z, rz.z))
let roll = atan2(rx.y, rx.x)
let r = SIMD3<Float>(pitch, yaw, roll)
return (t, r, s)
}
}
@@ -0,0 +1,295 @@
import Foundation
import UIKit
import Display
import AccountContext
import MediaEditor
final class DrawingVectorEntityView: DrawingEntityView {
private var vectorEntity: DrawingVectorEntity {
return self.entity as! DrawingVectorEntity
}
fileprivate let shapeLayer = SimpleShapeLayer()
init(context: AccountContext, entity: DrawingVectorEntity) {
super.init(context: context, entity: entity)
self.layer.addSublayer(self.shapeLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var selectionBounds: CGRect {
return self.shapeLayer.path?.boundingBox ?? self.bounds
}
private var maxLineWidth: CGFloat {
return max(10.0, max(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.1)
}
override func animateSelection() {
guard let selectionView = self.selectionView else {
return
}
selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
}
override func update(animated: Bool) {
self.center = CGPoint(x: self.vectorEntity.drawingSize.width * 0.5, y: self.vectorEntity.drawingSize.height * 0.5)
self.bounds = CGRect(origin: .zero, size: self.vectorEntity.drawingSize)
let minLineWidth = max(10.0, max(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.01)
let maxLineWidth = max(10.0, max(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.05)
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.vectorEntity.lineWidth
self.shapeLayer.path = CGPath.curve(
start: self.vectorEntity.start,
end: self.vectorEntity.end,
mid: self.midPoint,
lineWidth: lineWidth,
arrowSize: self.vectorEntity.type == .line ? nil : CGSize(width: lineWidth * 1.5, height: lineWidth * 3.0),
twoSided: self.vectorEntity.type == .twoSidedArrow
)
self.shapeLayer.fillColor = self.vectorEntity.color.toCGColor()
super.update(animated: animated)
}
override func updateSelectionView() {
guard let selectionView = self.selectionView as? DrawingVectorEntitySelectionView else {
return
}
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
let drawingSize = self.vectorEntity.drawingSize
selectionView.bounds = CGRect(origin: .zero, size: drawingSize)
selectionView.center = CGPoint(x: drawingSize.width * 0.5 * scale, y: drawingSize.height * 0.5 * scale)
selectionView.transform = CGAffineTransform(scaleX: scale, y: scale)
selectionView.scale = scale
}
override func precisePoint(inside point: CGPoint) -> Bool {
if let path = self.shapeLayer.path {
if path.contains(point) {
return true
} else {
let expandedPath = CGPath.curve(
start: self.vectorEntity.start,
end: self.vectorEntity.end,
mid: self.midPoint,
lineWidth: self.maxLineWidth * 0.8,
arrowSize: nil,
twoSided: false
)
return expandedPath.contains(point)
}
} else {
return super.precisePoint(inside: point)
}
}
override func makeSelectionView() -> DrawingEntitySelectionView? {
if let selectionView = self.selectionView {
return selectionView
}
let selectionView = DrawingVectorEntitySelectionView()
selectionView.entityView = self
return selectionView
}
func getRenderImage() -> UIImage? {
let rect = self.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0)
self.drawHierarchy(in: rect, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
var _cachedMidPoint: (start: CGPoint, end: CGPoint, midLength: CGFloat, midHeight: CGFloat, midPoint: CGPoint)?
var midPoint: CGPoint {
let entity = self.vectorEntity
if let (start, end, midLength, midHeight, midPoint) = self._cachedMidPoint, start == entity.start, end == entity.end, midLength == entity.mid.0, midHeight == entity.mid.1 {
return midPoint
} else {
let midPoint = midPointPositionFor(start: entity.start, end: entity.end, length: entity.mid.0, height: entity.mid.1)
self._cachedMidPoint = (entity.start, entity.end, entity.mid.0, entity.mid.1, midPoint)
return midPoint
}
}
}
private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, height: CGFloat) -> CGPoint {
let distance = end.distance(to: start)
let angle = start.angle(to: end)
let p1 = start.pointAt(distance: distance * length, angle: angle)
let p2 = p1.pointAt(distance: distance * height, angle: angle + .pi * 0.5)
return p2
}
final class DrawingVectorEntitySelectionView: DrawingEntitySelectionView {
private let startHandle = SimpleShapeLayer()
private let midHandle = SimpleShapeLayer()
private let endHandle = SimpleShapeLayer()
var scale: CGFloat = 1.0 {
didSet {
self.setNeedsLayout()
}
}
override init(frame: CGRect) {
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
self.startHandle.bounds = handleBounds
self.startHandle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
self.startHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor
self.startHandle.rasterizationScale = UIScreen.main.scale
self.startHandle.shouldRasterize = true
self.midHandle.bounds = handleBounds
self.midHandle.fillColor = UIColor(rgb: 0x00ff00).cgColor
self.midHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor
self.midHandle.rasterizationScale = UIScreen.main.scale
self.midHandle.shouldRasterize = true
self.endHandle.bounds = handleBounds
self.endHandle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
self.endHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor
self.endHandle.rasterizationScale = UIScreen.main.scale
self.endHandle.shouldRasterize = true
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
self.layer.addSublayer(self.startHandle)
self.layer.addSublayer(self.midHandle)
self.layer.addSublayer(self.endHandle)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var currentHandle: CALayer?
override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let entityView = self.entityView as? DrawingVectorEntityView, let entity = entityView.entity as? DrawingVectorEntity else {
return
}
let location = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
if let sublayers = self.layer.sublayers {
for layer in sublayers {
if layer.frame.contains(location) {
self.currentHandle = layer
entityView.onInteractionUpdated(true)
return
}
}
}
self.currentHandle = self.layer
case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
if gestureRecognizer.numberOfTouches > 1 {
return
}
let delta = gestureRecognizer.translation(in: entityView)
var updatedStart = entity.start
var updatedMid = entity.mid
var updatedEnd = entity.end
if self.currentHandle === self.startHandle {
updatedStart.x += delta.x
updatedStart.y += delta.y
} else if self.currentHandle === self.endHandle {
updatedEnd.x += delta.x
updatedEnd.y += delta.y
} else if self.currentHandle === self.midHandle {
var updatedMidPoint = entityView.midPoint
updatedMidPoint.x += delta.x
updatedMidPoint.y += delta.y
let distance = updatedStart.distance(to: updatedEnd)
let pointOnLine = updatedMidPoint.perpendicularPointOnLine(start: updatedStart, end: updatedEnd)
let angle = updatedStart.angle(to: updatedEnd)
let midAngle = updatedStart.angle(to: updatedMidPoint)
var height = updatedMidPoint.distance(to: pointOnLine) / distance
var deltaAngle = midAngle - angle
if deltaAngle > .pi {
deltaAngle = angle - 2 * .pi
} else if deltaAngle < -.pi {
deltaAngle = angle + 2 * .pi
}
if deltaAngle < 0.0 {
height *= -1.0
}
let length = updatedStart.distance(to: pointOnLine) / distance
updatedMid = (length, height)
} else if self.currentHandle === self.layer {
updatedStart.x += delta.x
updatedStart.y += delta.y
updatedEnd.x += delta.x
updatedEnd.y += delta.y
}
entity.start = updatedStart
entity.mid = updatedMid
entity.end = updatedEnd
entityView.update(animated: false)
gestureRecognizer.setTranslation(.zero, in: entityView)
case .ended, .cancelled:
entityView.onInteractionUpdated(false)
default:
break
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.startHandle.frame.contains(point) || self.midHandle.frame.contains(point) || self.endHandle.frame.contains(point) {
return true
} else if let entityView = self.entityView as? DrawingVectorEntityView, let path = entityView.shapeLayer.path {
return path.contains(self.convert(point, to: entityView))
}
return false
}
override func layoutSubviews() {
guard let entityView = self.entityView as? DrawingVectorEntityView, let entity = entityView.entity as? DrawingVectorEntity else {
return
}
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
let lineWidth = (1.0 + UIScreenPixel) / self.scale
self.startHandle.path = handlePath
self.startHandle.position = entity.start
self.startHandle.bounds = bounds
self.startHandle.lineWidth = lineWidth
self.midHandle.path = handlePath
self.midHandle.position = entityView.midPoint
self.midHandle.bounds = bounds
self.midHandle.lineWidth = lineWidth
self.endHandle.path = handlePath
self.endHandle.position = entity.end
self.endHandle.bounds = bounds
self.endHandle.lineWidth = lineWidth
}
}
@@ -0,0 +1,229 @@
import Foundation
import UIKit
import SwiftSignalKit
import Camera
import MediaEditor
import AVFoundation
public final class EntityVideoRecorder {
private weak var mediaEditor: MediaEditor?
private weak var entitiesView: DrawingEntitiesView?
private let maxDuration: Double
private let camera: Camera
private let previewView: CameraSimplePreviewView
private let entity: DrawingStickerEntity
private weak var entityView: DrawingStickerEntityView?
private var recordingDisposable = MetaDisposable()
private let durationPromise = ValuePromise<Double>()
private let micLevelPromise = Promise<Float>()
private var changingPositionDisposable: Disposable?
public var duration: Signal<Double, NoError> {
return self.durationPromise.get()
}
public var micLevel: Signal<Float, NoError> {
return self.micLevelPromise.get()
}
public var onAutomaticStop: () -> Void = {}
public init(mediaEditor: MediaEditor, entitiesView: DrawingEntitiesView) {
self.mediaEditor = mediaEditor
self.entitiesView = entitiesView
self.maxDuration = min(60.0, mediaEditor.duration ?? 60.0)
self.previewView = CameraSimplePreviewView(frame: .zero, main: true)
self.entity = DrawingStickerEntity(content: .dualVideoReference(true))
var preferLowerFramerate = false
if let mainFramerate = mediaEditor.mainFramerate {
let frameRate = Int(round(mainFramerate / 30.0) * 30.0)
if frameRate == 30 {
preferLowerFramerate = true
}
}
self.camera = Camera(
configuration: Camera.Configuration(
preset: .hd1920x1080,
position: .front,
isDualEnabled: false,
audio: true,
photo: false,
metadata: false,
preferWide: true,
preferLowerFramerate: preferLowerFramerate,
reportAudioLevel: true
),
previewView: self.previewView,
secondaryPreviewView: nil
)
self.camera.startCapture()
let action = { [weak self] in
self?.previewView.removePlaceholder(delay: 0.15)
Queue.mainQueue().after(0.1) {
self?.startRecording()
}
}
if #available(iOS 13.0, *) {
let _ = (self.previewView.isPreviewing
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { _ in
action()
})
} else {
Queue.mainQueue().after(0.35) {
action()
}
}
self.micLevelPromise.set(camera.audioLevel)
let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0
mediaEditor.stop()
mediaEditor.seek(start, andPlay: false)
self.changingPositionDisposable = (camera.modeChange
|> deliverOnMainQueue).start(next: { [weak self] modeChange in
guard let self else {
return
}
if case .position = modeChange {
self.entityView?.beginCameraSwitch()
} else {
self.entityView?.commitCameraSwitch()
}
})
}
deinit {
self.recordingDisposable.dispose()
self.changingPositionDisposable?.dispose()
}
public func setup(
referenceDrawingSize: CGSize,
scale: CGFloat,
position: CGPoint
) {
self.entity.referenceDrawingSize = referenceDrawingSize
self.entity.scale = scale
self.entity.position = position
self.entitiesView?.add(self.entity)
if let entityView = self.entitiesView?.getView(for: self.entity.uuid) as? DrawingStickerEntityView {
let maxDuration = self.maxDuration
entityView.setupCameraPreviewView(
self.previewView,
progress: self.durationPromise.get() |> map {
Float(max(0.0, min(1.0, $0 / maxDuration)))
}
)
self.previewView.resetPlaceholder(front: true)
entityView.animateInsertion()
self.entityView = entityView
}
self.entitiesView?.selectEntity(nil)
}
var start: Double = 0.0
private func startRecording() {
guard let mediaEditor = self.mediaEditor else {
self.onAutomaticStop()
return
}
mediaEditor.maybeMuteVideo()
mediaEditor.play()
self.start = CACurrentMediaTime()
self.recordingDisposable.set((self.camera.startRecording()
|> deliverOnMainQueue).startStrict(next: { [weak self] recordingData in
guard let self else {
return
}
self.durationPromise.set(recordingData.duration)
if recordingData.duration >= self.maxDuration {
let onAutomaticStop = self.onAutomaticStop
self.stopRecording(save: true, completion: {
onAutomaticStop()
})
}
}))
}
public func stopRecording(save: Bool, completion: @escaping () -> Void = {}) {
var save = save
let duration = CACurrentMediaTime() - self.start
if duration < 0.2 {
save = false
}
self.recordingDisposable.set((self.camera.stopRecording()
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self, let mediaEditor = self.mediaEditor, let entitiesView = self.entitiesView, case let .finished(mainResult, _, _, _, _) = result else {
return
}
if save {
let duration = AVURLAsset(url: URL(fileURLWithPath: mainResult.path)).duration
let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0
mediaEditor.setAdditionalVideoOffset(-start, apply: false)
mediaEditor.setAdditionalVideoTrimRange(0 ..< duration.seconds, apply: true)
mediaEditor.setAdditionalVideo(mainResult.path, positionChanges: [])
mediaEditor.stop()
Queue.mainQueue().justDispatch {
mediaEditor.seek(start, andPlay: true)
}
if let entityView = entitiesView.getView(for: self.entity.uuid) as? DrawingStickerEntityView {
entityView.snapshotCameraPreviewView()
mediaEditor.setOnNextAdditionalDisplay { [weak entityView] in
Queue.mainQueue().async {
entityView?.invalidateCameraPreviewView()
}
}
let entity = self.entity
let update = { [weak mediaEditor, weak entity] in
if let mediaEditor, let entity {
mediaEditor.setAdditionalVideoPosition(entity.position, scale: entity.scale, rotation: entity.rotation)
}
}
entityView.updated = {
update()
}
update()
}
self.camera.stopCapture(invalidate: true)
self.mediaEditor?.maybeUnmuteVideo()
completion()
}
}))
if !save {
self.camera.stopCapture(invalidate: true)
self.mediaEditor?.maybeUnmuteVideo()
self.entitiesView?.remove(uuid: self.entity.uuid, animated: true)
self.mediaEditor?.setAdditionalVideo(nil, positionChanges: [])
completion()
}
}
public func togglePosition() {
self.camera.togglePosition()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,587 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramCore
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
import MediaEditor
import TelegramStringFormatting
import LottieComponent
import LottieComponentResourceContent
private func generateIcon(style: DrawingWeatherEntity.Style) -> UIImage? {
guard let image = UIImage(bundleImageName: "Chat/Attach Menu/Location") else {
return nil
}
return generateImage(image.size, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let cgImage = image.cgImage {
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
}
if [.black, .white].contains(style) {
let green: UIColor
let blue: UIColor
if case .black = style {
green = UIColor(rgb: 0x3EF588)
blue = UIColor(rgb: 0x4FAAFF)
} else {
green = UIColor(rgb: 0x1EBD5E)
blue = UIColor(rgb: 0x1C92FF)
}
var locations: [CGFloat] = [0.0, 1.0]
let colorsArray = [green.cgColor, blue.cgColor] as NSArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
} else {
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: size))
}
})
}
public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelegate {
private var weatherEntity: DrawingWeatherEntity {
return self.entity as! DrawingWeatherEntity
}
let backgroundView: UIView
let textView: DrawingTextView
private var animation = ComponentView<Empty>()
private var didSetUpAnimationNode = false
private let stickerFetchedDisposable = MetaDisposable()
private let cachedDisposable = MetaDisposable()
let temperature: String
init(context: AccountContext, entity: DrawingWeatherEntity) {
self.temperature = stringForTemperature(entity.temperature)
self.backgroundView = UIView()
self.backgroundView.clipsToBounds = true
self.textView = DrawingTextView(frame: .zero)
self.textView.clipsToBounds = false
self.textView.backgroundColor = .clear
self.textView.isEditable = false
self.textView.isSelectable = false
self.textView.contentInset = .zero
self.textView.showsHorizontalScrollIndicator = false
self.textView.showsVerticalScrollIndicator = false
self.textView.scrollsToTop = false
self.textView.isScrollEnabled = false
self.textView.textContainerInset = .zero
self.textView.minimumZoomScale = 1.0
self.textView.maximumZoomScale = 1.0
self.textView.keyboardAppearance = .dark
self.textView.autocorrectionType = .default
self.textView.spellCheckingType = .no
self.textView.textContainer.maximumNumberOfLines = 2
self.textView.textContainer.lineBreakMode = .byTruncatingTail
super.init(context: context, entity: entity)
self.textView.delegate = self
self.addSubview(self.backgroundView)
self.addSubview(self.textView)
self.update(animated: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var textSize: CGSize = .zero
public override func sizeThatFits(_ size: CGSize) -> CGSize {
var result = self.textView.sizeThatFits(CGSize(width: self.weatherEntity.width, height: .greatestFiniteMagnitude))
self.textSize = result
let widthExtension: CGFloat = result.height * 0.7
result.width = floorToScreenPixels(max(224.0, ceil(result.width) + 20.0) + widthExtension)
result.height = ceil(result.height * 1.2);
return result;
}
public override func sizeToFit() {
let center = self.center
let transform = self.transform
self.transform = .identity
super.sizeToFit()
self.center = center
self.transform = transform
}
public override func layoutSubviews() {
super.layoutSubviews()
let iconSize = min(80.0, floor(self.bounds.height * 0.7))
let iconOffset: CGFloat = 0.3
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
if let icon = self.weatherEntity.icon {
let _ = self.animation.update(
transition: .immediate,
component: AnyComponent(
LottieComponent(
content: LottieComponent.ResourceContent(
context: self.context,
file: icon,
attemptSynchronously: true,
providesPlaceholder: true
),
color: nil,
placeholderColor: UIColor(rgb: 0x000000, alpha: 0.1),
loop: !["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"].contains(self.weatherEntity.emoji)
)
),
environment: {},
containerSize: iconFrame.size
)
if let animationView = self.animation.view {
if animationView.superview == nil {
self.addSubview(animationView)
}
animationView.frame = iconFrame
}
}
self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize)
self.backgroundView.frame = self.bounds
}
override func selectedTapAction() -> Bool {
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
let keyTimes = [0.0, 0.33, 1.0]
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
let updatedStyle: DrawingWeatherEntity.Style
switch self.weatherEntity.style {
case .white:
updatedStyle = .black
case .black:
updatedStyle = .transparent
case .transparent:
if self.weatherEntity.hasCustomColor {
updatedStyle = .custom
} else {
updatedStyle = .white
}
case .custom:
updatedStyle = .white
}
self.weatherEntity.style = updatedStyle
self.update()
return true
}
private var displayFontSize: CGFloat {
var textFontSize: CGFloat = 0.07
let textLength = self.temperature.count
if textLength > 10 {
textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0)
}
let minFontSize = max(10.0, max(self.weatherEntity.referenceDrawingSize.width, self.weatherEntity.referenceDrawingSize.height) * 0.025)
let maxFontSize = max(10.0, max(self.weatherEntity.referenceDrawingSize.width, self.weatherEntity.referenceDrawingSize.height) * 0.25)
let fontSize = minFontSize + (maxFontSize - minFontSize) * textFontSize
return fontSize
}
private func updateText() {
let text = NSMutableAttributedString(string: self.temperature.uppercased())
let range = NSMakeRange(0, text.length)
let fontSize = self.displayFontSize
self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24)
let font = Font.with(size: fontSize, design: .camera, weight: .semibold)
text.addAttribute(.font, value: font, range: range)
text.addAttribute(.kern, value: -3.5 as NSNumber, range: range)
self.textView.font = font
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
let textColor: UIColor
switch self.weatherEntity.style {
case .white:
textColor = .black
case .black, .transparent:
textColor = .white
case .custom:
let color = self.weatherEntity.color.toUIColor()
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
}
text.addAttribute(.foregroundColor, value: textColor, range: range)
self.textView.attributedText = text
self.textView.visualText = text
}
private var currentStyle: DrawingWeatherEntity.Style?
public override func update(animated: Bool = false) {
self.center = self.weatherEntity.position
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.weatherEntity.rotation), self.weatherEntity.scale, self.weatherEntity.scale)
self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0)
switch self.weatherEntity.style {
case .white:
self.textView.textColor = .black
self.backgroundView.backgroundColor = .white
self.backgroundView.isHidden = false
case .black:
self.textView.textColor = .white
self.backgroundView.backgroundColor = .black
self.backgroundView.isHidden = false
case .transparent:
self.textView.textColor = .white
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
self.backgroundView.isHidden = false
case .custom:
let color = self.weatherEntity.color.toUIColor()
let textColor: UIColor
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
self.textView.textColor = textColor
self.backgroundView.backgroundColor = color
self.backgroundView.isHidden = false
}
self.textView.textAlignment = .left
self.updateText()
self.sizeToFit()
self.currentStyle = self.weatherEntity.style
self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2
if #available(iOS 13.0, *) {
self.backgroundView.layer.cornerCurve = .continuous
}
super.update(animated: animated)
}
override func updateSelectionView() {
guard let selectionView = self.selectionView as? DrawingWeatherEntitySelectionView else {
return
}
self.pushIdentityTransformForMeasurement()
selectionView.transform = .identity
let bounds = self.selectionBounds
let center = bounds.center
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
selectionView.center = self.convert(center, to: selectionView.superview)
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.weatherEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.weatherEntity.scale) * scale + selectionView.selectionInset * 2.0))
selectionView.transform = CGAffineTransformMakeRotation(self.weatherEntity.rotation)
self.popIdentityTransformForMeasurement()
}
override func makeSelectionView() -> DrawingEntitySelectionView? {
if let selectionView = self.selectionView {
return selectionView
}
let selectionView = DrawingWeatherEntitySelectionView()
selectionView.entityView = self
return selectionView
}
func getRenderImage() -> UIImage? {
let rect = self.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0)
self.drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func getRenderSubEntities() -> [DrawingEntity] {
return []
}
}
final class DrawingWeatherEntitySelectionView: DrawingEntitySelectionView {
private let border = SimpleShapeLayer()
private let leftHandle = SimpleShapeLayer()
private let rightHandle = SimpleShapeLayer()
private var longPressGestureRecognizer: UILongPressGestureRecognizer?
override init(frame: CGRect) {
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
let handles = [
self.leftHandle,
self.rightHandle
]
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
self.border.lineCap = .round
self.border.fillColor = UIColor.clear.cgColor
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.75).cgColor
self.layer.addSublayer(self.border)
for handle in handles {
handle.bounds = handleBounds
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
handle.rasterizationScale = UIScreen.main.scale
handle.shouldRasterize = true
self.layer.addSublayer(handle)
}
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
if let self, let entityView = self.entityView {
entityView.onSnapUpdated(type, snapped)
}
}
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
self.addGestureRecognizer(longPressGestureRecognizer)
self.longPressGestureRecognizer = longPressGestureRecognizer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scale: CGFloat = 1.0 {
didSet {
self.setNeedsLayout()
}
}
override var selectionInset: CGFloat {
return 15.0
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
private let snapTool = DrawingEntitySnapTool()
@objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
if case .began = gestureRecognizer.state {
self.longPressed()
}
}
private var currentHandle: CALayer?
override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingWeatherEntity else {
return
}
let location = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
self.tapGestureRecognizer?.isEnabled = false
self.tapGestureRecognizer?.isEnabled = true
self.longPressGestureRecognizer?.isEnabled = false
self.longPressGestureRecognizer?.isEnabled = true
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
if let sublayers = self.layer.sublayers {
for layer in sublayers {
if layer.frame.contains(location) {
self.currentHandle = layer
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
return
}
}
}
self.currentHandle = self.layer
entityView.onInteractionUpdated(true)
case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview)
let parentLocation = gestureRecognizer.location(in: self.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview)
var updatedScale = entity.scale
var updatedPosition = entity.position
var updatedRotation = entity.rotation
if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle {
if gestureRecognizer.numberOfTouches > 1 {
return
}
var deltaX = gestureRecognizer.translation(in: self).x
if self.currentHandle === self.leftHandle {
deltaX *= -1.0
}
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
updatedScale = max(0.01, updatedScale * scaleDelta)
let newAngle: CGFloat
if self.currentHandle === self.leftHandle {
newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x)
} else {
newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x)
}
var delta = newAngle - updatedRotation
if delta < -.pi {
delta = 2.0 * .pi + delta
}
let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0)
} else if self.currentHandle === self.layer {
updatedPosition.x += delta.x
updatedPosition.y += delta.y
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
}
entity.scale = updatedScale
entity.position = updatedPosition
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.setTranslation(.zero, in: entityView)
case .ended, .cancelled:
self.snapTool.reset()
if self.currentHandle != nil {
self.snapTool.rotationReset()
}
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard let entityView = self.entityView as? DrawingWeatherEntityView, let entity = entityView.entity as? DrawingWeatherEntity else {
return
}
switch gestureRecognizer.state {
case .began, .changed:
if case .began = gestureRecognizer.state {
entityView.onInteractionUpdated(true)
}
let scale = gestureRecognizer.scale
entity.scale = max(0.1, entity.scale * scale)
entityView.update()
gestureRecognizer.scale = 1.0
case .ended, .cancelled:
entityView.onInteractionUpdated(false)
default:
break
}
}
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
guard let entityView = self.entityView as? DrawingWeatherEntityView, let entity = entityView.entity as? DrawingWeatherEntity else {
return
}
let velocity = gestureRecognizer.velocity
var updatedRotation = entity.rotation
var rotation: CGFloat = 0.0
switch gestureRecognizer.state {
case .began:
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
case .changed:
rotation = gestureRecognizer.rotation
updatedRotation += rotation
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation)
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.rotation = 0.0
case .ended, .cancelled:
self.snapTool.rotationReset()
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
}
override func layoutSubviews() {
let inset = self.selectionInset - 10.0
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
let lineWidth = (1.0 + UIScreenPixel) / self.scale
let handles = [
self.leftHandle,
self.rightHandle
]
for handle in handles {
handle.path = handlePath
handle.bounds = bounds
handle.lineWidth = lineWidth
}
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
let width: CGFloat = self.bounds.width - inset * 2.0
let height: CGFloat = self.bounds.height - inset * 2.0
let cornerRadius: CGFloat = 12.0 - self.scale
let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi))
let count = 12
let relativeDashLength: CGFloat = 0.25
let dashLength = perimeter / CGFloat(count)
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
self.border.lineWidth = 2.0 / self.scale
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath
}
}
@@ -0,0 +1,244 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import MediaEditor
private let size = CGSize(width: 148.0, height: 148.0)
private let outerWidth: CGFloat = 12.0
private let ringWidth: CGFloat = 5.0
private let selectionWidth: CGFloat = 4.0
private func generateShadowImage(size: CGSize) -> UIImage? {
let inset: CGFloat = 60.0
let imageSize = CGSize(width: size.width + inset * 2.0, height: size.height + inset * 2.0)
return generateImage(imageSize, rotatedContext: { imageSize, context in
context.clear(CGRect(origin: .zero, size: imageSize))
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 40.0, color: UIColor(rgb: 0x000000, alpha: 0.9).cgColor)
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.1).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: inset, y: inset), size: size))
})
}
private func generateGridImage(size: CGSize, light: Bool) -> UIImage? {
return generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(light ? UIColor.white.cgColor : UIColor(rgb: 0x505050).cgColor)
let lineWidth: CGFloat = 1.0
var offset: CGFloat = 7.0
for _ in 0 ..< 8 {
context.fill(CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: size.width, height: lineWidth)))
context.fill(CGRect(origin: CGPoint(x: offset, y: 0.0), size: CGSize(width: lineWidth, height: size.height)))
offset += 14.0
}
})
}
public final class EyedropperView: UIView {
private weak var drawingView: DrawingView?
private let containerView: UIView
private let shadowLayer: SimpleLayer
private let clipView: UIView
private let zoomedView: UIImageView
private let gridLayer: SimpleLayer
private let outerColorLayer: SimpleLayer
private let ringLayer: SimpleLayer
private let selectionLayer: SimpleLayer
private let sourceImage: (data: Data, size: CGSize, bytesPerRow: Int, info: CGBitmapInfo)?
var completed: (DrawingColor) -> Void = { _ in }
var dismissed: () -> Void = { }
init(containerSize: CGSize, drawingView: DrawingView, sourceImage: UIImage) {
self.drawingView = drawingView
self.zoomedView = UIImageView(image: sourceImage)
self.zoomedView.isOpaque = true
self.zoomedView.layer.magnificationFilter = .nearest
if let cgImage = sourceImage.cgImage, let pixelData = cgImage.dataProvider?.data as? Data {
self.sourceImage = (pixelData, sourceImage.size, cgImage.bytesPerRow, cgImage.bitmapInfo)
} else {
self.sourceImage = nil
}
let bounds = CGRect(origin: .zero, size: size)
self.containerView = UIView()
self.containerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - size.width) / 2.0), y: floorToScreenPixels((containerSize.height - size.height) / 2.0)), size: size)
self.shadowLayer = SimpleLayer()
self.shadowLayer.contents = generateShadowImage(size: size)?.cgImage
self.shadowLayer.frame = bounds.insetBy(dx: -60.0, dy: -60.0)
let clipFrame = bounds.insetBy(dx: outerWidth + ringWidth, dy: outerWidth + ringWidth)
self.clipView = UIView()
self.clipView.clipsToBounds = true
self.clipView.frame = bounds.insetBy(dx: outerWidth + ringWidth, dy: outerWidth + ringWidth)
self.clipView.layer.cornerRadius = size.width / 2.0 - outerWidth - ringWidth
if #available(iOS 13.0, *) {
self.clipView.layer.cornerCurve = .circular
}
self.clipView.addSubview(self.zoomedView)
self.gridLayer = SimpleLayer()
self.gridLayer.opacity = 0.6
self.gridLayer.frame = self.clipView.bounds
self.gridLayer.contents = generateGridImage(size: clipFrame.size, light: true)?.cgImage
self.outerColorLayer = SimpleLayer()
self.outerColorLayer.rasterizationScale = UIScreen.main.scale
self.outerColorLayer.shouldRasterize = true
self.outerColorLayer.frame = bounds
self.outerColorLayer.cornerRadius = self.outerColorLayer.frame.width / 2.0
self.outerColorLayer.borderWidth = outerWidth
self.ringLayer = SimpleLayer()
self.ringLayer.rasterizationScale = UIScreen.main.scale
self.ringLayer.shouldRasterize = true
self.ringLayer.borderColor = UIColor.white.cgColor
self.ringLayer.frame = bounds.insetBy(dx: outerWidth, dy: outerWidth)
self.ringLayer.cornerRadius = self.ringLayer.frame.width / 2.0
self.ringLayer.borderWidth = ringWidth
self.selectionLayer = SimpleLayer()
self.selectionLayer.borderColor = UIColor.white.cgColor
self.selectionLayer.borderWidth = selectionWidth
self.selectionLayer.cornerRadius = 2.0
self.selectionLayer.frame = CGRect(origin: CGPoint(x: clipFrame.minX + 48.0, y: clipFrame.minY + 48.0), size: CGSize(width: 17.0, height: 17.0)).insetBy(dx: -UIScreenPixel, dy: -UIScreenPixel)
super.init(frame: .zero)
self.addSubview(self.containerView)
self.clipView.layer.addSublayer(self.gridLayer)
self.containerView.layer.addSublayer(self.shadowLayer)
self.containerView.addSubview(self.clipView)
self.containerView.layer.addSublayer(self.ringLayer)
self.containerView.layer.addSublayer(self.outerColorLayer)
self.containerView.layer.addSublayer(self.selectionLayer)
self.containerView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
self.addGestureRecognizer(panGestureRecognizer)
Queue.mainQueue().justDispatch {
self.updateColor()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var gridIsLight = true
private var currentColor: DrawingColor?
func setColor(_ color: UIColor) {
self.currentColor = DrawingColor(color: color)
self.outerColorLayer.borderColor = color.cgColor
self.selectionLayer.backgroundColor = color.cgColor
if color.lightness > 0.9 {
self.ringLayer.borderColor = UIColor(rgb: 0x999999).cgColor
if self.gridIsLight {
self.gridIsLight = false
self.gridLayer.contents = generateGridImage(size: self.clipView.frame.size, light: false)?.cgImage
}
} else {
self.ringLayer.borderColor = UIColor.white.cgColor
if !self.gridIsLight {
self.gridIsLight = true
self.gridLayer.contents = generateGridImage(size: self.clipView.frame.size, light: true)?.cgImage
}
}
}
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
}
let x = Int(point.x)
let y = Int(point.y)
var color: UIColor?
sourceImage.data.withUnsafeMutableBytes { buffer in
guard let bytes = buffer.assumingMemoryBound(to: UInt8.self).baseAddress else {
return
}
let srcLine = bytes.advanced(by: y * sourceImage.bytesPerRow)
let srcPixel = srcLine + x * 4
let r = srcPixel.pointee
let g = srcPixel.advanced(by: 1).pointee
let b = srcPixel.advanced(by: 2).pointee
if sourceImage.info.contains(.byteOrder32Little) {
color = UIColor(red: CGFloat(b) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(r) / 255.0, alpha: 1.0)
} else {
color = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0)
}
}
return color
}
private func updateColor() {
guard let drawingView = self.drawingView else {
return
}
var point = self.convert(self.containerView.center, to: drawingView)
point.x /= drawingView.scale
point.y /= drawingView.scale
let scale: CGFloat = 15.0
self.zoomedView.transform = CGAffineTransformMakeScale(scale, scale)
self.zoomedView.center = CGPoint(x: self.clipView.frame.width / 2.0 + (self.zoomedView.bounds.width / 2.0 - point.x) * scale, y: self.clipView.frame.height / 2.0 + (self.zoomedView.bounds.height / 2.0 - point.y) * scale)
if let color = self.getColorAt(point) {
self.setColor(color)
}
}
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .changed:
let translation = gestureRecognizer.translation(in: self)
self.containerView.center = self.containerView.center.offsetBy(dx: translation.x, dy: translation.y)
gestureRecognizer.setTranslation(.zero, in: self)
self.updateColor()
case .ended, .cancelled:
if let color = currentColor {
self.containerView.alpha = 0.0
self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.removeFromSuperview()
})
self.containerView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
self.completed(color)
}
default:
break
}
}
}
@@ -0,0 +1,265 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import LegacyComponents
import TelegramCore
import SegmentedControlNode
private func generateMaskPath(size: CGSize, leftRadius: CGFloat, rightRadius: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: leftRadius, y: size.height / 2.0), radius: leftRadius, startAngle: .pi * 0.5, endAngle: -.pi * 0.5, clockwise: true)
path.addArc(withCenter: CGPoint(x: size.width - rightRadius, y: size.height / 2.0), radius: rightRadius, startAngle: -.pi * 0.5, endAngle: .pi * 0.5, clockwise: true)
path.close()
return path
}
private func generateKnobImage() -> UIImage? {
let side: CGFloat = 28.0
let margin: CGFloat = 10.0
let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side)))
})
return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5))
}
final class ModeAndSizeComponent: Component {
let values: [String]
let sizeValue: CGFloat
let isEditing: Bool
let isEnabled: Bool
let rightInset: CGFloat
let tag: AnyObject?
let selectedIndex: Int
let selectionChanged: (Int) -> Void
let sizeUpdated: (CGFloat) -> Void
let sizeReleased: () -> Void
init(values: [String], sizeValue: CGFloat, isEditing: Bool, isEnabled: Bool, rightInset: CGFloat, tag: AnyObject?, selectedIndex: Int, selectionChanged: @escaping (Int) -> Void, sizeUpdated: @escaping (CGFloat) -> Void, sizeReleased: @escaping () -> Void) {
self.values = values
self.sizeValue = sizeValue
self.isEditing = isEditing
self.isEnabled = isEnabled
self.rightInset = rightInset
self.tag = tag
self.selectedIndex = selectedIndex
self.selectionChanged = selectionChanged
self.sizeUpdated = sizeUpdated
self.sizeReleased = sizeReleased
}
static func ==(lhs: ModeAndSizeComponent, rhs: ModeAndSizeComponent) -> Bool {
if lhs.values != rhs.values {
return false
}
if lhs.sizeValue != rhs.sizeValue {
return false
}
if lhs.isEditing != rhs.isEditing {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.rightInset != rhs.rightInset {
return false
}
if lhs.selectedIndex != rhs.selectedIndex {
return false
}
return true
}
final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
private let backgroundNode: NavigationBackgroundNode
private let node: SegmentedControlNode
private var knob: UIImageView
private let maskLayer = SimpleShapeLayer()
private var isEditing: Bool?
private var isControlEnabled: Bool?
private var sliderWidth: CGFloat = 0.0
fileprivate var updated: (CGFloat) -> Void = { _ in }
fileprivate var released: () -> Void = { }
private var component: ModeAndSizeComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
init() {
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3))
self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0, cornerRadius: 16.0)
self.knob = UIImageView(image: generateKnobImage())
super.init(frame: CGRect())
self.layer.allowsGroupOpacity = true
self.addSubview(self.backgroundNode.view)
self.addSubview(self.node.view)
self.addSubview(self.knob)
self.backgroundNode.layer.mask = self.maskLayer
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
pressGestureRecognizer.minimumPressDuration = 0.01
pressGestureRecognizer.delegate = self
self.addGestureRecognizer(pressGestureRecognizer)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
self.addGestureRecognizer(panGestureRecognizer)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
guard self.frame.width > 0.0, case .began = gestureRecognizer.state else {
return
}
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
self.updated(value)
}
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .changed:
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
guard self.frame.width > 0.0 else {
return
}
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
self.updated(value)
case .ended, .cancelled:
self.released()
default:
break
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled {
return isEditing && isControlEnabled
} else {
return false
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func animateIn() {
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut() {
self.node.alpha = 0.0
self.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.backgroundNode.alpha = 0.0
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.updated = component.sizeUpdated
self.released = component.sizeReleased
let previousIsEditing = self.isEditing
self.isEditing = component.isEditing
self.isControlEnabled = component.isEnabled
if component.isEditing {
self.sliderWidth = availableSize.width
}
self.node.items = component.values.map { SegmentedControlItem(title: $0) }
self.node.setSelectedIndex(component.selectedIndex, animated: !transition.animation.isImmediate)
let selectionChanged = component.selectionChanged
self.node.selectedIndexChanged = { [weak self] index in
self?.window?.endEditing(true)
selectionChanged(index)
}
let nodeSize = self.node.updateLayout(.stretchToFill(width: availableSize.width + component.rightInset), transition: transition.containedViewLayoutTransition)
let size = CGSize(width: availableSize.width, height: nodeSize.height)
transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: nodeSize))
var isDismissingEditing = false
if component.isEditing != previousIsEditing && !component.isEditing {
isDismissingEditing = true
}
self.knob.alpha = component.isEditing ? 1.0 : 0.0
if !isDismissingEditing {
self.knob.frame = CGRect(origin: CGPoint(x: -12.0 + floorToScreenPixels((self.sliderWidth + 24.0 - self.knob.frame.size.width) * component.sizeValue), y: floorToScreenPixels((size.height - self.knob.frame.size.height) / 2.0)), size: self.knob.frame.size)
}
if component.isEditing != previousIsEditing {
let containedTransition = transition.containedViewLayoutTransition
let maskPath: UIBezierPath
if component.isEditing {
maskPath = generateMaskPath(size: size, leftRadius: 2.0, rightRadius: 11.5)
let selectionFrame = self.node.animateSelection(to: self.knob.center, transition: containedTransition)
containedTransition.animateFrame(layer: self.knob.layer, from: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
self.knob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else {
maskPath = generateMaskPath(size: size, leftRadius: 16.0, rightRadius: 16.0)
if previousIsEditing != nil {
let selectionFrame = self.node.animateSelection(from: self.knob.center, transition: containedTransition)
containedTransition.animateFrame(layer: self.knob.layer, from: self.knob.frame, to: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
self.knob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath)
}
transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize))
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition)
if let screenTransition = transition.userData(DrawingScreenTransition.self) {
switch screenTransition {
case .animateIn:
self.animateIn()
case .animateOut:
self.animateOut()
}
}
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,805 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import LegacyComponents
import TelegramCore
import LottieAnimationComponent
import MediaEditor
enum DrawingTextStyle: Equatable {
case regular
case filled
case semi
case stroke
case blur
init(style: DrawingTextEntity.Style) {
switch style {
case .regular:
self = .regular
case .filled:
self = .filled
case .semi:
self = .semi
case .stroke:
self = .stroke
case .blur:
self = .blur
}
}
}
enum DrawingTextAnimation: Equatable {
case none
case typing
case wiggle
case zoomIn
init(animation: DrawingTextEntity.Animation) {
switch animation {
case .none:
self = .none
case .typing:
self = .typing
case .wiggle:
self = .wiggle
case .zoomIn:
self = .zoomIn
}
}
}
enum DrawingTextAlignment: Equatable {
case left
case center
case right
init(alignment: DrawingTextEntity.Alignment) {
switch alignment {
case .left:
self = .left
case .center:
self = .center
case .right:
self = .right
}
}
}
enum DrawingTextFont: Equatable, Hashable {
case sanFrancisco
case other(String, String)
init(font: DrawingTextEntity.Font) {
switch font {
case .sanFrancisco:
self = .sanFrancisco
case let .other(font, name):
self = .other(font, name)
}
}
var font: DrawingTextEntity.Font {
switch self {
case .sanFrancisco:
return .sanFrancisco
case let .other(font, name):
return .other(font, name)
}
}
var title: String {
switch self {
case .sanFrancisco:
return "San Francisco"
case let .other(_, name):
return name
}
}
func uiFont(size: CGFloat) -> UIFont {
switch self {
case .sanFrancisco:
return Font.with(size: size, design: .regular, weight: .semibold)
case let .other(font, _):
return UIFont(name: font, size: size) ?? Font.semibold(size)
}
}
}
final class TextAlignmentComponent: Component {
let alignment: DrawingTextAlignment
init(alignment: DrawingTextAlignment) {
self.alignment = alignment
}
static func == (lhs: TextAlignmentComponent, rhs: TextAlignmentComponent) -> Bool {
return lhs.alignment == rhs.alignment
}
public final class View: UIView {
private let line1 = SimpleLayer()
private let line2 = SimpleLayer()
private let line3 = SimpleLayer()
private let line4 = SimpleLayer()
override init(frame: CGRect) {
super.init(frame: frame)
let lines = [self.line1, self.line2, self.line3, self.line4]
lines.forEach { line in
line.backgroundColor = UIColor.white.cgColor
line.cornerRadius = 1.0
line.masksToBounds = true
self.layer.addSublayer(line)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: TextAlignmentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let height = 2.0 - UIScreenPixel
let spacing: CGFloat = 3.0 + UIScreenPixel
let long = 21.0
let short = 13.0
let size = CGSize(width: long, height: 18.0)
switch component.alignment {
case .left:
transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: long, height: height)))
transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing), size: CGSize(width: short, height: height)))
transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing + height + spacing), size: CGSize(width: long, height: height)))
transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height)))
case .center:
transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - long) / 2.0), y: 0.0), size: CGSize(width: long, height: height)))
transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - short) / 2.0), y: height + spacing), size: CGSize(width: short, height: height)))
transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - long) / 2.0), y: height + spacing + height + spacing), size: CGSize(width: long, height: height)))
transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - short) / 2.0), y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height)))
case .right:
transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: size.width - long, y: 0.0), size: CGSize(width: long, height: height)))
transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: size.width - short, y: height + spacing), size: CGSize(width: short, height: height)))
transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: size.width - long, y: height + spacing + height + spacing), size: CGSize(width: long, height: height)))
transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: size.width - short, y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height)))
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class TextFontComponent: Component {
let selectedValue: DrawingTextFont
let tag: AnyObject?
let tapped: () -> Void
init(selectedValue: DrawingTextFont, tag: AnyObject?, tapped: @escaping () -> Void) {
self.selectedValue = selectedValue
self.tag = tag
self.tapped = tapped
}
static func == (lhs: TextFontComponent, rhs: TextFontComponent) -> Bool {
return lhs.selectedValue == rhs.selectedValue
}
final class View: UIView, ComponentTaggedView {
private var button = HighlightableButton()
private let icon = SimpleLayer()
private var component: TextFontComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.button)
self.button.layer.addSublayer(self.icon)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed(_ sender: HighlightableButton) {
if let component = self.component {
component.tapped()
}
}
func update(component: TextFontComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
if self.icon.contents == nil {
self.icon.contents = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/FontArrow"), color: UIColor(rgb: 0xffffff, alpha: 0.5))?.cgImage
}
let value = component.selectedValue
var disappearingSnapshotView: UIView?
let previousTitle = self.button.title(for: .normal)
if previousTitle != value.title {
if let snapshotView = self.button.titleLabel?.snapshotView(afterScreenUpdates: false) {
snapshotView.center = self.button.titleLabel?.center ?? snapshotView.center
self.button.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.button.titleLabel?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
disappearingSnapshotView = snapshotView
}
}
self.button.clipsToBounds = true
self.button.setTitle(value.title, for: .normal)
self.button.titleLabel?.font = value.uiFont(size: 13.0)
self.button.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 20.0)
var buttonSize = self.button.sizeThatFits(availableSize)
buttonSize.width += 20.0
buttonSize.height = 30.0
transition.setFrame(view: self.button, frame: CGRect(origin: .zero, size: buttonSize))
self.button.layer.cornerRadius = 11.0
self.button.layer.borderWidth = 1.0 - UIScreenPixel
self.button.layer.borderColor = UIColor.white.cgColor
self.button.addTarget(self, action: #selector(self.pressed(_:)), for: .touchUpInside)
let iconSize = CGSize(width: 16.0, height: 16.0)
let iconFrame = CGRect(origin: CGPoint(x: buttonSize.width - iconSize.width - 8.0, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize)
transition.setFrame(layer: self.icon, frame: iconFrame)
if let disappearingSnapshotView, let titleLabel = self.button.titleLabel {
disappearingSnapshotView.layer.animatePosition(from: disappearingSnapshotView.center, to: titleLabel.center, duration: 0.2, removeOnCompletion: false)
self.button.titleLabel?.layer.animatePosition(from: disappearingSnapshotView.center, to: titleLabel.center, duration: 0.2)
}
return CGSize(width: self.button.frame.width, height: availableSize.height)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class TextSettingsComponent: CombinedComponent {
let color: DrawingColor?
let style: DrawingTextStyle
let animation: DrawingTextAnimation
let alignment: DrawingTextAlignment
let font: DrawingTextFont
let isEmojiKeyboard: Bool
let tag: AnyObject?
let fontTag: AnyObject?
let presentColorPicker: () -> Void
let presentFastColorPicker: (GenericComponentViewTag) -> Void
let updateFastColorPickerPan: (CGPoint) -> Void
let dismissFastColorPicker: () -> Void
let toggleStyle: () -> Void
let toggleAnimation: () -> Void
let toggleAlignment: () -> Void
let presentFontPicker: () -> Void
let toggleKeyboard: (() -> Void)?
init(
color: DrawingColor?,
style: DrawingTextStyle,
animation: DrawingTextAnimation,
alignment: DrawingTextAlignment,
font: DrawingTextFont,
isEmojiKeyboard: Bool,
tag: AnyObject?,
fontTag: AnyObject?,
presentColorPicker: @escaping () -> Void = {},
presentFastColorPicker: @escaping (GenericComponentViewTag) -> Void = { _ in },
updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in },
dismissFastColorPicker: @escaping () -> Void = {},
toggleStyle: @escaping () -> Void,
toggleAnimation: @escaping () -> Void,
toggleAlignment: @escaping () -> Void,
presentFontPicker: @escaping () -> Void,
toggleKeyboard: (() -> Void)?
) {
self.color = color
self.style = style
self.animation = animation
self.alignment = alignment
self.font = font
self.isEmojiKeyboard = isEmojiKeyboard
self.tag = tag
self.fontTag = fontTag
self.presentColorPicker = presentColorPicker
self.presentFastColorPicker = presentFastColorPicker
self.updateFastColorPickerPan = updateFastColorPickerPan
self.dismissFastColorPicker = dismissFastColorPicker
self.toggleStyle = toggleStyle
self.toggleAnimation = toggleAnimation
self.toggleAlignment = toggleAlignment
self.presentFontPicker = presentFontPicker
self.toggleKeyboard = toggleKeyboard
}
static func ==(lhs: TextSettingsComponent, rhs: TextSettingsComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.style != rhs.style {
return false
}
if lhs.animation != rhs.animation {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.isEmojiKeyboard != rhs.isEmojiKeyboard {
return false
}
return true
}
final class State: ComponentState {
enum ImageKey: Hashable {
case regular
case filled
case semi
case stroke
case keyboard
case emoji
}
private var cachedImages: [ImageKey: UIImage] = [:]
func image(_ key: ImageKey) -> UIImage {
if let image = self.cachedImages[key] {
return image
} else {
var image: UIImage
switch key {
case .regular:
image = UIImage(bundleImageName: "Media Editor/TextDefault")!
case .filled:
image = UIImage(bundleImageName: "Media Editor/TextFilled")!
case .semi:
image = UIImage(bundleImageName: "Media Editor/TextSemi")!
case .stroke:
image = UIImage(bundleImageName: "Media Editor/TextStroke")!
case .keyboard:
image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard"), color: .white)!
case .emoji:
image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputEmojiIcon"), color: .white)!
}
cachedImages[key] = image
return image
}
}
}
class View: UIView, ComponentTaggedView {
var componentTag: AnyObject?
public func matches(tag: Any) -> Bool {
if let componentTag = self.componentTag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
func animateIn() {
var delay: Double = 0.0
for view in self.subviews {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay)
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2, delay: delay)
delay += 0.02
}
}
func animateOut(completion: @escaping () -> Void) {
var isFirst = true
for view in self.subviews {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: isFirst ? { _ in
completion()
} : nil)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
isFirst = false
}
}
}
func makeView() -> View {
let view = View()
view.componentTag = self.tag
return view
}
func makeState() -> State {
State()
}
static var body: Body {
let colorButton = Child(ColorSwatchComponent.self)
let colorButtonTag = GenericComponentViewTag()
let alignmentButton = Child(Button.self)
let styleButton = Child(Button.self)
let keyboardButton = Child(Button.self)
let font = Child(TextFontComponent.self)
return { context in
let component = context.component
let state = context.state
let toggleStyle = component.toggleStyle
let toggleAlignment = component.toggleAlignment
var offset: CGFloat = 6.0
if let color = component.color {
let presentColorPicker = component.presentColorPicker
let presentFastColorPicker = component.presentFastColorPicker
let updateFastColorPickerPan = component.updateFastColorPickerPan
let dismissFastColorPicker = component.dismissFastColorPicker
let colorButton = colorButton.update(
component: ColorSwatchComponent(
type: .main,
color: color,
tag: colorButtonTag,
action: {
presentColorPicker()
},
holdAction: {
presentFastColorPicker(colorButtonTag)
},
pan: { point in
updateFastColorPickerPan(point)
},
release: {
dismissFastColorPicker()
}
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: context.transition
)
context.add(colorButton
.position(CGPoint(x: colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height / 2.0))
)
offset += 42.0
}
let styleImage: UIImage
switch component.style {
case .regular:
styleImage = state.image(.regular)
case .filled:
styleImage = state.image(.filled)
case .semi:
styleImage = state.image(.semi)
case .stroke:
styleImage = state.image(.stroke)
case .blur:
styleImage = state.image(.stroke)
}
var fontAvailableWidth: CGFloat = context.availableSize.width
if component.color != nil {
fontAvailableWidth -= 72.0
}
let styleButton = styleButton.update(
component: Button(
content: AnyComponent(
Image(
image: styleImage
)
),
action: {
toggleStyle()
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .easeInOut(duration: 0.2)
)
context.add(styleButton
.position(CGPoint(x: offset + styleButton.size.width / 2.0, y: context.availableSize.height / 2.0))
.update(ComponentTransition.Update { _, view, transition in
if let snapshot = view.snapshotView(afterScreenUpdates: false) {
transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in
snapshot?.removeFromSuperview()
})
snapshot.frame = view.frame
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
view.superview?.addSubview(snapshot)
}
})
)
offset += 44.0
let alignmentButton = alignmentButton.update(
component: Button(
content: AnyComponent(
TextAlignmentComponent(
alignment: component.alignment
)
),
action: {
toggleAlignment()
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: context.availableSize,
transition: .easeInOut(duration: 0.2)
)
context.add(alignmentButton
.position(CGPoint(x: offset + alignmentButton.size.width / 2.0, y: context.availableSize.height / 2.0 + 1.0 - UIScreenPixel))
)
offset += 45.0
if let toggleKeyboard = component.toggleKeyboard {
let keyboardButton = keyboardButton.update(
component: Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(name: !component.isEmojiKeyboard ? "input_anim_smileToKey" : "input_anim_keyToSmile" , mode: .animateTransitionFromPrevious),
colors: ["__allcolors__": UIColor.white],
size: CGSize(width: 32.0, height: 32.0)
)
),
action: {
toggleKeyboard()
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 32.0, height: 32.0),
transition: .easeInOut(duration: 0.15)
)
context.add(keyboardButton
.position(CGPoint(x: offset + keyboardButton.size.width / 2.0 + (component.isEmojiKeyboard ? 3.0 : 0.0), y: context.availableSize.height / 2.0))
)
}
let presentFontPicker = component.presentFontPicker
let font = font.update(
component: TextFontComponent(
selectedValue: component.font,
tag: component.fontTag,
tapped: {
presentFontPicker()
}
),
availableSize: CGSize(width: fontAvailableWidth, height: 30.0),
transition: .easeInOut(duration: 0.2)
)
context.add(font
.position(CGPoint(x: context.availableSize.width - font.size.width / 2.0 - 16.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
private func generateMaskPath(size: CGSize, topRadius: CGFloat, bottomRadius: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: size.width / 2.0, y: topRadius), radius: topRadius, startAngle: .pi, endAngle: 0, clockwise: true)
path.addArc(withCenter: CGPoint(x: size.width / 2.0, y: size.height - bottomRadius), radius: bottomRadius, startAngle: 0, endAngle: .pi, clockwise: true)
path.close()
return path
}
private func generateKnobImage() -> UIImage? {
let side: CGFloat = 32.0
let margin: CGFloat = 10.0
let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side)))
})
return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5))
}
public final class TextSizeSliderComponent: Component {
let value: CGFloat
let tag: AnyObject?
let updated: (CGFloat) -> Void
let released: () -> Void
public init(
value: CGFloat,
tag: AnyObject?,
updated: @escaping (CGFloat) -> Void,
released: @escaping () -> Void
) {
self.value = value
self.tag = tag
self.updated = updated
self.released = released
}
public static func ==(lhs: TextSizeSliderComponent, rhs: TextSizeSliderComponent) -> Bool {
if lhs.value != rhs.value {
return false
}
return true
}
public final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
private var validSize: CGSize?
private let backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3))
private let maskLayer = SimpleShapeLayer()
private let knobContainer = SimpleLayer()
private let knob = SimpleLayer()
fileprivate var updated: (CGFloat) -> Void = { _ in }
fileprivate var released: () -> Void = { }
private var component: TextSizeSliderComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
init() {
super.init(frame: CGRect())
self.layer.allowsGroupOpacity = true
self.isExclusiveTouch = true
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
pressGestureRecognizer.minimumPressDuration = 0.01
pressGestureRecognizer.delegate = self
self.addGestureRecognizer(pressGestureRecognizer)
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))))
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
private var isTracking: Bool?
private var isPanning = false
private var isPressing = false
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
guard self.frame.height > 0.0 else {
return
}
switch gestureRecognizer.state {
case .began:
self.isPressing = true
if let size = self.validSize, let component = self.component {
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
}
let location = gestureRecognizer.location(in: self).offsetBy(dx: 0.0, dy: -12.0)
let value = 1.0 - max(0.0, min(1.0, location.y / (self.frame.height - 24.0)))
self.updated(value)
case .ended, .cancelled:
self.isPressing = false
if let size = self.validSize, let component = self.component {
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
}
self.released()
default:
break
}
}
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard self.frame.height > 0.0 else {
return
}
switch gestureRecognizer.state {
case .began, .changed:
self.isPanning = true
if let size = self.validSize, let component = self.component {
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
}
let location = gestureRecognizer.location(in: self).offsetBy(dx: 0.0, dy: -12.0)
let value = 1.0 - max(0.0, min(1.0, location.y / (self.frame.height - 24.0)))
self.updated(value)
case .ended, .cancelled:
self.isPanning = false
if let size = self.validSize, let component = self.component {
let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2))
}
self.released()
default:
break
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func updateLayout(size: CGSize, component: TextSizeSliderComponent, transition: ComponentTransition) -> CGSize {
self.component = component
let previousSize = self.validSize
self.validSize = size
if self.backgroundNode.view.superview == nil {
self.addSubview(self.backgroundNode.view)
}
if self.knobContainer.superlayer == nil {
self.layer.addSublayer(self.knobContainer)
}
if self.knob.superlayer == nil {
self.knob.contents = generateKnobImage()?.cgImage
self.knobContainer.addSublayer(self.knob)
}
let isTracking = self.isPanning || self.isPressing
if self.isTracking != isTracking {
self.isTracking = isTracking
transition.setSublayerTransform(view: self, transform: isTracking ? CATransform3DMakeTranslation(8.0, 0.0, 0.0) : CATransform3DMakeTranslation(-size.width / 2.0, 0.0, 0.0))
transition.setSublayerTransform(layer: self.knobContainer, transform: isTracking ? CATransform3DIdentity : CATransform3DMakeTranslation(4.0, 0.0, 0.0))
}
let knobTransition = self.isPanning ? transition.withAnimation(.none) : transition
let knobSize = CGSize(width: 52.0, height: 52.0)
let knobFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - knobSize.width) / 2.0), y: -12.0 + floorToScreenPixels((size.height + 24.0 - knobSize.height) * (1.0 - component.value))), size: knobSize)
knobTransition.setFrame(layer: self.knob, frame: knobFrame)
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.knobContainer, frame: CGRect(origin: CGPoint(), size: size))
if previousSize != size {
transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: size))
self.maskLayer.path = generateMaskPath(size: size, topRadius: 15.0, bottomRadius: 3.0).cgPath
self.backgroundNode.layer.mask = self.maskLayer
}
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
view.updated = self.updated
view.released = self.released
return view.updateLayout(size: availableSize, component: self, transition: transition)
}
}
@@ -0,0 +1,533 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import LegacyComponents
import TelegramCore
private let toolSize = CGSize(width: 40.0, height: 176.0)
private class ToolView: UIView, UIGestureRecognizerDelegate {
let type: DrawingToolState.Key
var isSelected = false
var isToolFocused = false
var isVisible = false
private var currentSize: CGFloat?
private let shadow: SimpleLayer
private let tip: UIImageView
private let background: SimpleLayer
private let band: SimpleGradientLayer
var pressed: (DrawingToolState.Key) -> Void = { _ in }
var swiped: (DrawingToolState.Key, CGFloat) -> Void = { _, _ in }
var released: () -> Void = { }
init(type: DrawingToolState.Key) {
self.type = type
self.shadow = SimpleLayer()
self.tip = UIImageView()
self.tip.isUserInteractionEnabled = false
self.background = SimpleLayer()
self.band = SimpleGradientLayer()
self.band.cornerRadius = 2.0
self.band.type = .axial
self.band.startPoint = CGPoint(x: 0.0, y: 0.5)
self.band.endPoint = CGPoint(x: 1.0, y: 0.5)
self.band.masksToBounds = true
let backgroundImage: UIImage?
let tipImage: UIImage?
let shadowImage: UIImage?
var tipAbove = true
var hasBand = true
switch type {
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)
} else {
self.addSubview(self.tip)
self.layer.addSublayer(self.background)
}
if hasBand {
self.layer.addSublayer(self.band)
}
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGestureRecognizer)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
self.addGestureRecognizer(panGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer {
if self.isSelected {
return true
} else {
return false
}
}
return self.isVisible
}
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
self.pressed(self.type)
}
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let size = self.currentSize else {
return
}
switch gestureRecognizer.state {
case .changed:
let translation = gestureRecognizer.translation(in: self)
gestureRecognizer.setTranslation(.zero, in: self)
let updatedSize = max(0.0, min(1.0, size - translation.y / 200.0))
self.swiped(self.type, updatedSize)
case .ended, .cancelled:
self.released()
default:
break
}
}
func animateIn(animated: Bool, delay: Double = 0.0) {
let layout = {
self.bounds = CGRect(origin: .zero, size: self.bounds.size)
}
if animated {
UIView.animate(withDuration: 0.5, delay: delay, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, animations: layout)
} else {
layout()
}
}
func animateOut(animated: Bool, delay: Double = 0.0, completion: @escaping () -> Void = {}) {
let layout = {
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: -140.0), size: self.bounds.size)
}
if animated {
UIView.animate(withDuration: 0.5, delay: delay, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, animations: layout, completion: { _ in
completion()
})
} else {
layout()
completion()
}
}
func update(state: DrawingToolState) {
self.currentSize = state.size
if let _ = self.tip.image {
let color = state.color?.toUIColor()
self.tip.tintColor = color
guard let color = color else {
return
}
var locations: [NSNumber] = [0.0, 1.0]
var colors: [CGColor] = []
switch self.type {
case .pen, .arrow:
locations = [0.0, 0.15, 0.85, 1.0]
colors = [
color.withMultipliedBrightnessBy(0.7).cgColor,
color.cgColor,
color.cgColor,
color.withMultipliedBrightnessBy(0.7).cgColor
]
case .marker:
locations = [0.0, 0.15, 0.85, 1.0]
colors = [
color.withMultipliedBrightnessBy(0.7).cgColor,
color.cgColor,
color.cgColor,
color.withMultipliedBrightnessBy(0.7).cgColor
]
case .neon:
locations = [0.0, 0.15, 0.85, 1.0]
colors = [
color.withMultipliedBrightnessBy(0.7).cgColor,
color.cgColor,
color.cgColor,
color.withMultipliedBrightnessBy(0.7).cgColor
]
default:
return
}
self.band.transform = CATransform3DMakeScale(1.0, 0.08 + 0.92 * (state.size ?? 1.0), 1.0)
self.band.locations = locations
self.band.colors = colors
}
}
}
final class ToolsComponent: Component {
let state: DrawingState
let isFocused: Bool
let tag: AnyObject?
let toolPressed: (DrawingToolState.Key) -> Void
let toolResized: (DrawingToolState.Key, CGFloat) -> Void
let sizeReleased: () -> Void
init(state: DrawingState, isFocused: Bool, tag: AnyObject?, toolPressed: @escaping (DrawingToolState.Key) -> Void, toolResized: @escaping (DrawingToolState.Key, CGFloat) -> Void, sizeReleased: @escaping () -> Void) {
self.state = state
self.isFocused = isFocused
self.tag = tag
self.toolPressed = toolPressed
self.toolResized = toolResized
self.sizeReleased = sizeReleased
}
static func == (lhs: ToolsComponent, rhs: ToolsComponent) -> Bool {
return lhs.state == rhs.state && lhs.isFocused == rhs.isFocused
}
public final class View: UIView, ComponentTaggedView {
private var toolViews: [ToolView] = []
private let maskImageView: UIImageView
private var isToolFocused: Bool?
private var component: ToolsComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
override init(frame: CGRect) {
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
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self {
return nil
}
return result
}
func animateIn(completion: @escaping () -> Void) {
var delay = 0.0
for i in 0 ..< self.toolViews.count {
let view = self.toolViews[i]
view.animateOut(animated: false)
view.animateIn(animated: true, delay: delay)
delay += 0.025
}
}
func animateOut(completion: @escaping () -> Void) {
let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))
var delay = 0.0
for i in 0 ..< self.toolViews.count {
let view = self.toolViews[i]
view.animateOut(animated: true, delay: delay, completion: i == self.toolViews.count - 1 ? completion : {})
delay += 0.025
transition.setPosition(view: view, position: CGPoint(x: view.center.x, y: toolSize.height / 2.0 - 30.0 + 34.0))
}
}
func update(component: ToolsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> 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
let toolPressed = component.toolPressed
let toolResized = component.toolResized
let toolSizeReleased = component.sizeReleased
let spacing: CGFloat = 44.0
let totalWidth = spacing * CGFloat(self.toolViews.count - 1)
let left = (availableSize.width - totalWidth) / 2.0
var xPositions: [CGFloat] = []
var selectedIndex = 0
let isFocused = component.isFocused
for i in 0 ..< self.toolViews.count {
xPositions.append(left + spacing * CGFloat(i))
if self.toolViews[i].type == component.state.selectedTool {
selectedIndex = i
}
}
if isFocused {
let originalFocusedToolPosition = xPositions[selectedIndex]
xPositions[selectedIndex] = availableSize.width / 2.0
let delta = availableSize.width / 2.0 - originalFocusedToolPosition
for i in 0 ..< xPositions.count {
if i != selectedIndex {
xPositions[i] += delta
}
}
}
var offset: CGFloat = 100.0
for i in 0 ..< self.toolViews.count {
let view = self.toolViews[i]
var scale = 0.5
var verticalOffset: CGFloat = 30.0
if i == selectedIndex {
if isFocused {
scale = 1.0
verticalOffset = 30.0
} else {
verticalOffset = 18.0
}
view.isSelected = true
view.isToolFocused = isFocused
view.isVisible = true
} else {
view.isSelected = false
view.isToolFocused = false
view.isVisible = !isFocused
}
view.isUserInteractionEnabled = view.isVisible
let layout = {
view.center = CGPoint(x: xPositions[i], y: toolSize.height / 2.0 - 30.0 + verticalOffset)
view.transform = CGAffineTransform(scaleX: scale, y: scale)
}
if case .curve = transition.animation {
UIView.animate(
withDuration: 0.7,
delay: 0.0,
usingSpringWithDamping: 0.6,
initialSpringVelocity: 0.0,
options: .allowUserInteraction,
animations: layout)
} else {
layout()
}
view.update(state: component.state.toolState(for: view.type))
view.pressed = { type in
toolPressed(type)
}
view.swiped = { type, size in
toolResized(type, size)
}
view.released = {
toolSizeReleased()
}
offset += 44.0
}
if wasFocused != nil && wasFocused != component.isFocused {
var animated = false
if case .curve = transition.animation {
animated = true
}
if isFocused {
var delay = 0.0
for i in (selectedIndex + 1 ..< self.toolViews.count).reversed() {
let view = self.toolViews[i]
view.animateOut(animated: animated, delay: delay)
delay += 0.025
}
delay = 0.0
for i in (0 ..< selectedIndex) {
let view = self.toolViews[i]
view.animateOut(animated: animated, delay: delay)
delay += 0.025
}
} else {
var delay = 0.0
for i in (selectedIndex + 1 ..< self.toolViews.count) {
let view = self.toolViews[i]
view.animateIn(animated: animated, delay: delay)
delay += 0.025
}
delay = 0.0
for i in (0 ..< selectedIndex).reversed() {
let view = self.toolViews[i]
view.animateIn(animated: animated, delay: delay)
delay += 0.025
}
}
}
self.maskImageView.frame = CGRect(origin: .zero, size: availableSize)
if let screenTransition = transition.userData(DrawingScreenTransition.self) {
switch screenTransition {
case .animateIn:
self.animateIn(completion: {})
case .animateOut:
self.animateOut(completion: {})
}
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class ZoomOutButtonContent: CombinedComponent {
let title: String
let image: UIImage
init(
title: String,
image: UIImage
) {
self.title = title
self.image = image
}
static func ==(lhs: ZoomOutButtonContent, rhs: ZoomOutButtonContent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.image !== rhs.image {
return false
}
return true
}
static var body: Body {
let title = Child(Text.self)
let image = Child(Image.self)
return { context in
let component = context.component
let title = title.update(
component: Text(
text: component.title,
font: Font.regular(17.0),
color: .white
),
availableSize: context.availableSize,
transition: .immediate
)
let image = image.update(
component: Image(image: component.image),
availableSize: CGSize(width: 24.0, height: 24.0),
transition: .immediate
)
let spacing: CGFloat = 2.0
let width = title.size.width + spacing + image.size.width
context.add(image
.position(CGPoint(x: image.size.width / 2.0, y: context.availableSize.height / 2.0))
)
context.add(title
.position(CGPoint(x: image.size.width + spacing + title.size.width / 2.0, y: context.availableSize.height / 2.0))
)
return CGSize(width: width, height: context.availableSize.height)
}
}
}
@@ -0,0 +1,241 @@
import Foundation
import UIKit
private let pointsCount: Int = 64
private let squareSize: Double = 250.0
private let diagonal = sqrt(squareSize * squareSize + squareSize * squareSize)
private let halfDiagonal = diagonal * 0.5
private let angleRange: Double = .pi / 4.0
private let anglePrecision: Double = .pi / 90.0
class Unistroke {
let points: [CGPoint]
init(points: [CGPoint]) {
var points = resample(points: points, totalPoints: pointsCount)
let radians = indicativeAngle(points: points)
points = rotate(points: points, byRadians: -radians)
points = scale(points: points, toSize: squareSize)
points = translate(points: points, to: .zero)
self.points = points
}
func match(templates: [UnistrokeTemplate], minThreshold: Double = 0.8) -> String? {
var bestDistance = Double.infinity
var bestTemplate: UnistrokeTemplate?
for template in templates {
let templateDistance = distanceAtBestAngle(points: self.points, strokeTemplate: template.points, fromAngle: -angleRange, toAngle: angleRange, threshold: anglePrecision)
if templateDistance < bestDistance {
bestDistance = templateDistance
bestTemplate = template
}
}
if let bestTemplate = bestTemplate {
bestDistance = 1.0 - bestDistance / halfDiagonal
if bestDistance < minThreshold {
return nil
}
return bestTemplate.name
} else {
return nil
}
}
}
class UnistrokeTemplate : Unistroke {
var name: String
init(name: String, points: [CGPoint]) {
self.name = name
super.init(points: points)
}
}
private struct Edge {
var minX: Double
var minY: Double
var maxX: Double
var maxY: Double
init(minX: Double, maxX: Double, minY: Double, maxY: Double) {
self.minX = minX
self.minY = minY
self.maxX = maxX
self.maxY = maxY
}
mutating func addPoint(value: CGPoint) {
self.minX = min(self.minX,value.x)
self.maxX = max(self.maxX,value.x)
self.minY = min(self.minY,value.y)
self.maxY = max(self.maxY,value.y)
}
}
private extension Double {
func toRadians() -> Double {
let res = self * .pi / 180.0
return res
}
}
private func resample(points: [CGPoint], totalPoints: Int) -> [CGPoint] {
var initialPoints = points
let interval = pathLength(points: initialPoints) / Double(totalPoints - 1)
var totalLength: Double = 0.0
var newPoints: [CGPoint] = [points[0]]
for i in 1 ..< initialPoints.count {
let currentLength = initialPoints[i - 1].distance(to: initialPoints[i])
if totalLength + currentLength >= interval {
let newPoint = CGPoint(
x: initialPoints[i - 1].x + ((interval - totalLength) / currentLength) * (initialPoints[i].x - initialPoints[i - 1].x),
y: initialPoints[i - 1].y + ((interval - totalLength) / currentLength) * (initialPoints[i].y - initialPoints[i - 1].y)
)
newPoints.append(newPoint)
initialPoints.insert(newPoint, at: i)
totalLength = 0.0
} else {
totalLength += currentLength
}
}
if newPoints.count == totalPoints - 1 {
newPoints.append(points.last!)
}
return newPoints
}
private func pathLength(points: [CGPoint]) -> Double {
var distance: Double = 0.0
for index in 1 ..< points.count {
distance += points[index - 1].distance(to: points[index])
}
return distance
}
private func pathDistance(path1: [CGPoint], path2: [CGPoint]) -> Double {
var d: Double = 0.0
for idx in 0 ..< min(path1.count, path2.count) {
d += path1[idx].distance(to: path2[idx])
}
return d / Double(path1.count)
}
private func centroid(points: [CGPoint]) -> CGPoint {
var centroidPoint: CGPoint = .zero
for point in points {
centroidPoint.x = centroidPoint.x + point.x
centroidPoint.y = centroidPoint.y + point.y
}
centroidPoint.x = (centroidPoint.x / Double(points.count))
centroidPoint.y = (centroidPoint.y / Double(points.count))
return centroidPoint
}
private func boundingBox(points: [CGPoint]) -> CGRect {
var edge = Edge(minX: +Double.infinity, maxX: -Double.infinity, minY: +Double.infinity, maxY: -Double.infinity)
for point in points {
edge.addPoint(value: point)
}
return CGRect(x: edge.minX, y: edge.minY, width: (edge.maxX - edge.minX), height: (edge.maxY - edge.minY))
}
private func rotate(points: [CGPoint], byRadians radians: Double) -> [CGPoint] {
let centroid = centroid(points: points)
let cosinus = cos(radians)
let sinus = sin(radians)
var result: [CGPoint] = []
for point in points {
result.append(
CGPoint(
x: (point.x - centroid.x) * cosinus - (point.y - centroid.y) * sinus + centroid.x,
y: (point.x - centroid.x) * sinus + (point.y - centroid.y) * cosinus + centroid.y
)
)
}
return result
}
private func scale(points: [CGPoint], toSize size: Double) -> [CGPoint] {
let boundingBox = boundingBox(points: points)
var result: [CGPoint] = []
for point in points {
result.append(
CGPoint(
x: point.x * (size / boundingBox.width),
y: point.y * (size / boundingBox.height)
)
)
}
return result
}
private func translate(points: [CGPoint], to pt: CGPoint) -> [CGPoint] {
let centroidPoint = centroid(points: points)
var newPoints: [CGPoint] = []
for point in points {
newPoints.append(
CGPoint(
x: point.x + pt.x - centroidPoint.x,
y: point.y + pt.y - centroidPoint.y
)
)
}
return newPoints
}
private func vectorize(points: [CGPoint]) -> [Double] {
var sum: Double = 0.0
var vector: [Double] = []
for point in points {
vector.append(point.x)
vector.append(point.y)
sum += (point.x * point.x) + (point.y * point.y)
}
let magnitude = sqrt(sum)
for i in 0 ..< vector.count {
vector[i] = vector[i] / magnitude
}
return vector
}
private func indicativeAngle(points: [CGPoint]) -> Double {
let centroid = centroid(points: points)
return atan2(centroid.y - points[0].y, centroid.x - points[0].x)
}
private func distanceAtBestAngle(points: [CGPoint], strokeTemplate: [CGPoint], fromAngle: Double, toAngle: Double, threshold: Double) -> Double {
func distanceAtAngle(points: [CGPoint], strokeTemplate: [CGPoint], radians: Double) -> Double {
let rotatedPoints = rotate(points: points, byRadians: radians)
return pathDistance(path1: rotatedPoints, path2: strokeTemplate)
}
let phi: Double = (0.5 * (-1.0 + sqrt(5.0)))
var fromAngle = fromAngle
var toAngle = toAngle
var x1 = phi * fromAngle + (1.0 - phi) * toAngle
var f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1)
var x2 = (1.0 - phi) * fromAngle + phi * toAngle
var f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2)
while abs(toAngle - fromAngle) > threshold {
if f1 < f2 {
toAngle = x2
x2 = x1
f2 = f1
x1 = phi * fromAngle + (1.0 - phi) * toAngle
f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1)
} else {
fromAngle = x1
x1 = x2
f1 = f2
x2 = (1.0 - phi) * fromAngle + phi * toAngle
f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2)
}
}
return min(f1, f2)
}