Skip to content

Commit 83b2881

Browse files
authored
Merge pull request #3 from onl1ner/main
Added animation on path update and corner only path for QR-code frame
2 parents 69c3d65 + eb4d180 commit 83b2881

3 files changed

Lines changed: 192 additions & 18 deletions

File tree

Sources/SPQRCode/Interface/SPQRCameraController+AVCaptureMetadataOutputObjectsDelegate.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ extension SPQRCameraController: AVCaptureMetadataOutputObjectsDelegate {
3535
guard let transformedObject = previewLayer.transformedMetadataObject(for: object) as? AVMetadataMachineReadableCodeObject else { return }
3636

3737
let points = transformedObject.corners
38-
guard let firstPoint = points.first else { return }
39-
let path = UIBezierPath()
40-
path.move(to: firstPoint)
41-
var newPoints = points
42-
newPoints.removeFirst()
43-
newPoints.append(firstPoint)
44-
newPoints.forEach { path.addLine(to: $0) }
4538

4639
// Update Detail
4740

@@ -91,7 +84,7 @@ extension SPQRCameraController: AVCaptureMetadataOutputObjectsDelegate {
9184

9285
// Update Frame
9386

94-
frameLayer.update(with: path)
87+
frameLayer.update(using: points)
9588

9689
// Timer
9790

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// The MIT License (MIT)
2+
// Copyright © 2022 Sparrow Code (hello@sparrowcode.io)
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy
5+
// of this software and associated documentation files (the "Software"), to deal
6+
// in the Software without restriction, including without limitation the rights
7+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the Software is
9+
// furnished to do so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in all
12+
// copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
// SOFTWARE.
21+
22+
23+
import Foundation
24+
import CoreGraphics
25+
26+
struct SPQRCorner: Equatable {
27+
enum Kind: Int, CaseIterable {
28+
case topLeft = 0
29+
case bottomLeft = 1
30+
case bottomRight = 2
31+
case topRight = 3
32+
33+
var verticalNeighbor: Kind {
34+
switch self {
35+
case .topLeft:
36+
return .bottomLeft
37+
case .bottomLeft:
38+
return .topLeft
39+
case .bottomRight:
40+
return .topRight
41+
case .topRight:
42+
return .bottomRight
43+
}
44+
}
45+
46+
var horizontalNeighbor: Kind {
47+
switch self {
48+
case .topLeft:
49+
return .topRight
50+
case .bottomLeft:
51+
return .bottomRight
52+
case .bottomRight:
53+
return .bottomLeft
54+
case .topRight:
55+
return .topLeft
56+
}
57+
}
58+
}
59+
60+
let kind: Kind
61+
let point: CGPoint
62+
63+
let length: CGFloat
64+
let radius: CGFloat
65+
66+
func startPoint(using corners: [SPQRCorner]) -> CGPoint? {
67+
guard let neighbor = corners.first(where: { self.kind.verticalNeighbor == $0.kind }) else {
68+
return nil
69+
}
70+
71+
return pointOnLine(
72+
startPoint: point,
73+
endPoint: neighbor.point,
74+
distance: (length + radius)
75+
)
76+
}
77+
78+
func preCurvePoint(using corners: [SPQRCorner]) -> CGPoint? {
79+
guard let neighbor = corners.first(where: { self.kind.verticalNeighbor == $0.kind }) else {
80+
return nil
81+
}
82+
83+
return pointOnLine(
84+
startPoint: point,
85+
endPoint: neighbor.point,
86+
distance: radius
87+
)
88+
}
89+
90+
func postCurvePoint(using corners: [SPQRCorner]) -> CGPoint? {
91+
guard let neighbor = corners.first(where: { self.kind.horizontalNeighbor == $0.kind }) else {
92+
return nil
93+
}
94+
95+
return pointOnLine(
96+
startPoint: point,
97+
endPoint: neighbor.point,
98+
distance: radius
99+
)
100+
}
101+
102+
func endPoint(using corners: [SPQRCorner]) -> CGPoint? {
103+
guard let neighbor = corners.first(where: { self.kind.horizontalNeighbor == $0.kind }) else {
104+
return nil
105+
}
106+
107+
return pointOnLine(
108+
startPoint: point,
109+
endPoint: neighbor.point,
110+
distance: (length + radius)
111+
)
112+
}
113+
114+
private func pointOnLine(startPoint: CGPoint, endPoint: CGPoint, distance: CGFloat = 0.0) -> CGPoint {
115+
let lDistance = endPoint.distance(from: startPoint)
116+
let vector = CGPoint(
117+
x: (endPoint.x - startPoint.x) / lDistance,
118+
y: (endPoint.y - startPoint.y) / lDistance
119+
)
120+
121+
return .init(
122+
x: startPoint.x + distance * vector.x,
123+
y: startPoint.y + distance * vector.y
124+
)
125+
}
126+
}

Sources/SPQRCode/Interface/SPQRFrameLayer.swift

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,86 @@
2323
import UIKit
2424

2525
class SPQRFrameLayer: CAShapeLayer {
26+
private let cLength: CGFloat
27+
private let cRadius: CGFloat
2628

2729
// MARK: - Init
2830

29-
override init() {
31+
init(
32+
length: CGFloat = 16.0,
33+
radius: CGFloat = 16.0,
34+
lineWidth: CGFloat = 5.0,
35+
lineColor: UIColor = .systemYellow
36+
) {
37+
self.cLength = length
38+
self.cRadius = radius
39+
3040
super.init()
31-
strokeColor = UIColor.systemYellow.cgColor
32-
lineWidth = 5
33-
fillColor = UIColor.clear.cgColor
41+
42+
self.strokeColor = lineColor.cgColor
43+
self.fillColor = UIColor.clear.cgColor
44+
45+
self.lineWidth = lineWidth
3446
}
3547

3648
required init?(coder: NSCoder) {
3749
fatalError("init(coder:) has not been implemented")
3850
}
3951

40-
// MARK: - Actions
41-
42-
func detected(with newBezierPath: UIBezierPath) {
43-
path = newBezierPath.cgPath
52+
override func action(forKey event: String) -> CAAction? {
53+
if event == "path" {
54+
let animation: CABasicAnimation = .init(keyPath: event)
55+
56+
animation.duration = 0.3
57+
animation.timingFunction = CATransaction.animationTimingFunction()
58+
59+
return animation
60+
}
61+
62+
return super.action(forKey: event)
4463
}
4564

46-
func update(with newBezierPath: UIBezierPath) {
47-
path = newBezierPath.cgPath
65+
// MARK: - Actions
66+
67+
func update(using points: [CGPoint]) {
68+
let corners = buildCorners(for: points)
69+
70+
let framePath: UIBezierPath = .init()
71+
72+
for corner in corners {
73+
guard let cStartPoint = corner.startPoint(using: corners),
74+
let cPreCurvePoint = corner.preCurvePoint(using: corners),
75+
let cPostCurvePoint = corner.postCurvePoint(using: corners),
76+
let cEndPoint = corner.endPoint(using: corners)
77+
else { return }
78+
79+
framePath.move(to: cStartPoint)
80+
framePath.addLine(to: cPreCurvePoint)
81+
framePath.addQuadCurve(to: cPostCurvePoint, controlPoint: corner.point)
82+
framePath.addLine(to: cEndPoint)
83+
}
84+
85+
path = framePath.cgPath
4886
}
4987

5088
func dissapear() {
5189
path = nil
5290
}
91+
92+
private func buildCorners(for points: [CGPoint]) -> [SPQRCorner] {
93+
var corners: [SPQRCorner] = .init()
94+
95+
for corner in SPQRCorner.Kind.allCases {
96+
corners.append(
97+
.init(
98+
kind: corner,
99+
point: points[corner.rawValue],
100+
length: cLength,
101+
radius: cRadius
102+
)
103+
)
104+
}
105+
106+
return corners
107+
}
53108
}

0 commit comments

Comments
 (0)