diff --git a/Sources/SPQRCode/Interface/SPQRCameraController+AVCaptureMetadataOutputObjectsDelegate.swift b/Sources/SPQRCode/Interface/SPQRCameraController+AVCaptureMetadataOutputObjectsDelegate.swift index bc52635..f17a267 100644 --- a/Sources/SPQRCode/Interface/SPQRCameraController+AVCaptureMetadataOutputObjectsDelegate.swift +++ b/Sources/SPQRCode/Interface/SPQRCameraController+AVCaptureMetadataOutputObjectsDelegate.swift @@ -35,13 +35,6 @@ extension SPQRCameraController: AVCaptureMetadataOutputObjectsDelegate { guard let transformedObject = previewLayer.transformedMetadataObject(for: object) as? AVMetadataMachineReadableCodeObject else { return } let points = transformedObject.corners - guard let firstPoint = points.first else { return } - let path = UIBezierPath() - path.move(to: firstPoint) - var newPoints = points - newPoints.removeFirst() - newPoints.append(firstPoint) - newPoints.forEach { path.addLine(to: $0) } // Update Detail @@ -91,7 +84,7 @@ extension SPQRCameraController: AVCaptureMetadataOutputObjectsDelegate { // Update Frame - frameLayer.update(with: path) + frameLayer.update(using: points) // Timer diff --git a/Sources/SPQRCode/Interface/SPQRCorner.swift b/Sources/SPQRCode/Interface/SPQRCorner.swift new file mode 100644 index 0000000..25b7487 --- /dev/null +++ b/Sources/SPQRCode/Interface/SPQRCorner.swift @@ -0,0 +1,126 @@ +// The MIT License (MIT) +// Copyright © 2022 Sparrow Code (hello@sparrowcode.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + + +import Foundation +import CoreGraphics + +struct SPQRCorner: Equatable { + enum Kind: Int, CaseIterable { + case topLeft = 0 + case bottomLeft = 1 + case bottomRight = 2 + case topRight = 3 + + var verticalNeighbor: Kind { + switch self { + case .topLeft: + return .bottomLeft + case .bottomLeft: + return .topLeft + case .bottomRight: + return .topRight + case .topRight: + return .bottomRight + } + } + + var horizontalNeighbor: Kind { + switch self { + case .topLeft: + return .topRight + case .bottomLeft: + return .bottomRight + case .bottomRight: + return .bottomLeft + case .topRight: + return .topLeft + } + } + } + + let kind: Kind + let point: CGPoint + + let length: CGFloat + let radius: CGFloat + + func startPoint(using corners: [SPQRCorner]) -> CGPoint? { + guard let neighbor = corners.first(where: { self.kind.verticalNeighbor == $0.kind }) else { + return nil + } + + return pointOnLine( + startPoint: point, + endPoint: neighbor.point, + distance: (length + radius) + ) + } + + func preCurvePoint(using corners: [SPQRCorner]) -> CGPoint? { + guard let neighbor = corners.first(where: { self.kind.verticalNeighbor == $0.kind }) else { + return nil + } + + return pointOnLine( + startPoint: point, + endPoint: neighbor.point, + distance: radius + ) + } + + func postCurvePoint(using corners: [SPQRCorner]) -> CGPoint? { + guard let neighbor = corners.first(where: { self.kind.horizontalNeighbor == $0.kind }) else { + return nil + } + + return pointOnLine( + startPoint: point, + endPoint: neighbor.point, + distance: radius + ) + } + + func endPoint(using corners: [SPQRCorner]) -> CGPoint? { + guard let neighbor = corners.first(where: { self.kind.horizontalNeighbor == $0.kind }) else { + return nil + } + + return pointOnLine( + startPoint: point, + endPoint: neighbor.point, + distance: (length + radius) + ) + } + + private func pointOnLine(startPoint: CGPoint, endPoint: CGPoint, distance: CGFloat = 0.0) -> CGPoint { + let lDistance = endPoint.distance(from: startPoint) + let vector = CGPoint( + x: (endPoint.x - startPoint.x) / lDistance, + y: (endPoint.y - startPoint.y) / lDistance + ) + + return .init( + x: startPoint.x + distance * vector.x, + y: startPoint.y + distance * vector.y + ) + } +} diff --git a/Sources/SPQRCode/Interface/SPQRFrameLayer.swift b/Sources/SPQRCode/Interface/SPQRFrameLayer.swift index 3fa80b7..f11f7a4 100644 --- a/Sources/SPQRCode/Interface/SPQRFrameLayer.swift +++ b/Sources/SPQRCode/Interface/SPQRFrameLayer.swift @@ -23,31 +23,86 @@ import UIKit class SPQRFrameLayer: CAShapeLayer { + private let cLength: CGFloat + private let cRadius: CGFloat // MARK: - Init - override init() { + init( + length: CGFloat = 16.0, + radius: CGFloat = 16.0, + lineWidth: CGFloat = 5.0, + lineColor: UIColor = .systemYellow + ) { + self.cLength = length + self.cRadius = radius + super.init() - strokeColor = UIColor.systemYellow.cgColor - lineWidth = 5 - fillColor = UIColor.clear.cgColor + + self.strokeColor = lineColor.cgColor + self.fillColor = UIColor.clear.cgColor + + self.lineWidth = lineWidth } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - // MARK: - Actions - - func detected(with newBezierPath: UIBezierPath) { - path = newBezierPath.cgPath + override func action(forKey event: String) -> CAAction? { + if event == "path" { + let animation: CABasicAnimation = .init(keyPath: event) + + animation.duration = 0.3 + animation.timingFunction = CATransaction.animationTimingFunction() + + return animation + } + + return super.action(forKey: event) } - func update(with newBezierPath: UIBezierPath) { - path = newBezierPath.cgPath + // MARK: - Actions + + func update(using points: [CGPoint]) { + let corners = buildCorners(for: points) + + let framePath: UIBezierPath = .init() + + for corner in corners { + guard let cStartPoint = corner.startPoint(using: corners), + let cPreCurvePoint = corner.preCurvePoint(using: corners), + let cPostCurvePoint = corner.postCurvePoint(using: corners), + let cEndPoint = corner.endPoint(using: corners) + else { return } + + framePath.move(to: cStartPoint) + framePath.addLine(to: cPreCurvePoint) + framePath.addQuadCurve(to: cPostCurvePoint, controlPoint: corner.point) + framePath.addLine(to: cEndPoint) + } + + path = framePath.cgPath } func dissapear() { path = nil } + + private func buildCorners(for points: [CGPoint]) -> [SPQRCorner] { + var corners: [SPQRCorner] = .init() + + for corner in SPQRCorner.Kind.allCases { + corners.append( + .init( + kind: corner, + point: points[corner.rawValue], + length: cLength, + radius: cRadius + ) + ) + } + + return corners + } }