Camera and editor improvements

This commit is contained in:
Ilya Laktyushin 2023-06-11 22:17:00 +04:00
parent 137e234310
commit ceaa808fad
5 changed files with 247 additions and 68 deletions

View File

@ -73,6 +73,7 @@ swift_library(
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/TooltipUI",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/Components/MetalImageView:MetalImageView",
],
visibility = [
"//visibility:public",

View File

@ -148,6 +148,7 @@ private final class CameraScreenComponent: CombinedComponent {
var cameraState = CameraState(mode: .photo, position: .unspecified, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0)
var swipeHint: CaptureControlsComponent.SwipeHint = .none
var isTransitioning = false
private let hapticFeedback = HapticFeedback()
@ -267,6 +268,12 @@ private final class CameraScreenComponent: CombinedComponent {
self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920))))
}
}))
self.isTransitioning = true
Queue.mainQueue().after(0.8, {
self.isTransitioning = false
self.updated(transition: .immediate)
})
self.updated(transition: .spring(duration: 0.4))
}
@ -290,7 +297,7 @@ private final class CameraScreenComponent: CombinedComponent {
let zoomControl = Child(ZoomComponent.self)
let flashButton = Child(CameraButton.self)
let modeControl = Child(ModeComponent.self)
let hintLabel = Child(MultilineTextComponent.self)
let hintLabel = Child(HintLabelComponent.self)
let timeBackground = Child(RoundedRectangle.self)
let timeLabel = Child(MultilineTextComponent.self)
@ -308,7 +315,7 @@ private final class CameraScreenComponent: CombinedComponent {
state?.updateCameraMode(mode)
})
if case .none = state.cameraState.recording {
if case .none = state.cameraState.recording, !state.isTransitioning {
let cancelButton = cancelButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
@ -420,6 +427,9 @@ private final class CameraScreenComponent: CombinedComponent {
}
let shutterState: ShutterButtonState
if state.isTransitioning {
shutterState = .transition
} else {
switch state.cameraState.recording {
case .handsFree:
shutterState = .stopRecording
@ -433,6 +443,7 @@ private final class CameraScreenComponent: CombinedComponent {
shutterState = .video
}
}
}
let captureControls = captureControls.update(
component: CaptureControlsComponent(
@ -505,7 +516,7 @@ private final class CameraScreenComponent: CombinedComponent {
isVideoRecording = true
}
if isVideoRecording {
if isVideoRecording && !state.isTransitioning {
let duration = Int(state.cameraState.duration)
let durationString = String(format: "%02d:%02d", (duration / 60) % 60, duration % 60)
let timeLabel = timeLabel.update(
@ -541,7 +552,7 @@ private final class CameraScreenComponent: CombinedComponent {
let hintText: String?
switch state.swipeHint {
case .none:
hintText = nil
hintText = " "
case .zoom:
hintText = "Swipe up to zoom"
case .lock:
@ -553,10 +564,7 @@ private final class CameraScreenComponent: CombinedComponent {
}
if let hintText {
let hintLabel = hintLabel.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: hintText.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: .white)),
horizontalAlignment: .center
),
component: HintLabelComponent(text: hintText),
availableSize: availableSize,
transition: .immediate
)
@ -569,7 +577,7 @@ private final class CameraScreenComponent: CombinedComponent {
}
}
if case .none = state.cameraState.recording {
if case .none = state.cameraState.recording, !state.isTransitioning {
let modeControl = modeControl.update(
component: ModeComponent(
availableModes: [.photo, .video],

View File

@ -11,6 +11,7 @@ enum ShutterButtonState: Equatable {
case video
case stopRecording
case holdRecording(progress: Float)
case transition
}
private let maximumShutterSize = CGSize(width: 96.0, height: 96.0)
@ -141,6 +142,12 @@ private final class ShutterButtonContentComponent: Component {
innerCornerRadius = innerSize.height / 2.0
ringSize = CGSize(width: 92.0, height: 92.0)
recordingProgress = progress
case .transition:
innerColor = videoRedColor
innerSize = CGSize(width: 60.0, height: 60.0)
innerCornerRadius = innerSize.height / 2.0
ringSize = CGSize(width: 68.0, height: 68.0)
recordingProgress = 0.0
}
self.ringLayer.fillColor = UIColor.clear.cgColor
@ -573,6 +580,7 @@ final class CaptureControlsComponent: Component {
let buttonSideInset: CGFloat = 28.0
//let buttonMaxOffset: CGFloat = 100.0
var isTransitioning = false
var isRecording = false
var isHolding = false
if case .stopRecording = component.shutterState {
@ -580,6 +588,8 @@ final class CaptureControlsComponent: Component {
} else if case .holdRecording = component.shutterState {
isRecording = true
isHolding = true
} else if case .transition = component.shutterState {
isTransitioning = true
}
let galleryButtonSize = self.galleryButtonView.update(
@ -615,8 +625,8 @@ final class CaptureControlsComponent: Component {
transition.setBounds(view: galleryButtonView, bounds: CGRect(origin: .zero, size: galleryButtonFrame.size))
transition.setPosition(view: galleryButtonView, position: galleryButtonFrame.center)
transition.setScale(view: galleryButtonView, scale: isRecording ? 0.1 : 1.0)
transition.setAlpha(view: galleryButtonView, alpha: isRecording ? 0.0 : 1.0)
transition.setScale(view: galleryButtonView, scale: isRecording || isTransitioning ? 0.1 : 1.0)
transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : 1.0)
}
let _ = self.lockView.update(
@ -678,13 +688,16 @@ final class CaptureControlsComponent: Component {
}
transition.setBounds(view: flipButtonView, bounds: CGRect(origin: .zero, size: flipButtonFrame.size))
transition.setPosition(view: flipButtonView, position: flipButtonFrame.center)
transition.setScale(view: flipButtonView, scale: isTransitioning ? 0.01 : 1.0)
transition.setAlpha(view: flipButtonView, alpha: isTransitioning ? 0.0 : 1.0)
}
var blobState: ShutterBlobView.BlobState
switch component.shutterState {
case .generic:
blobState = .generic
case .video:
case .video, .transition:
blobState = .video
case .stopRecording:
blobState = .stopVideo
@ -732,6 +745,8 @@ final class CaptureControlsComponent: Component {
}
transition.setBounds(view: shutterButtonView, bounds: CGRect(origin: .zero, size: shutterButtonFrame.size))
transition.setPosition(view: shutterButtonView, position: shutterButtonFrame.center)
transition.setScale(view: shutterButtonView, scale: isTransitioning ? 0.01 : 1.0)
transition.setAlpha(view: shutterButtonView, alpha: isTransitioning ? 0.0 : 1.0)
}
let guideSpacing: CGFloat = 9.0

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
extension CameraMode {
var title: String {
@ -161,3 +162,83 @@ final class ModeComponent: Component {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class HintLabelComponent: Component {
let text: String
init(
text: String
) {
self.text = text
}
static func ==(lhs: HintLabelComponent, rhs: HintLabelComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
return true
}
final class View: UIView {
private var component: HintLabelComponent?
private var componentView = ComponentView<Empty>()
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: HintLabelComponent, availableSize: CGSize, transition: Transition) -> CGSize {
let previousComponent = self.component
self.component = component
if let previousText = previousComponent?.text, !previousText.isEmpty && previousText != component.text {
if let componentView = self.componentView.view, let snapshotView = componentView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = componentView.frame
self.addSubview(snapshotView)
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.componentView.view?.removeFromSuperview()
self.componentView = ComponentView<Empty>()
}
let textSize = self.componentView.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.text.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: .white)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
if let view = self.componentView.view {
if view.superview == nil {
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0), y: 0.0), size: textSize)
}
return CGSize(width: availableSize.width, height: textSize.height)
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -3,6 +3,7 @@ import Metal
import MetalKit
import ComponentFlow
import Display
import MetalImageView
private final class PropertyAnimation<T: Interpolatable> {
let from: T
@ -63,6 +64,7 @@ private final class AnimatableProperty<T: Interpolatable> {
}
func tick(timestamp: Double) -> Bool {
guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else {
return false
}
@ -73,8 +75,7 @@ private final class AnimatableProperty<T: Interpolatable> {
case .easeInOut:
t = listViewAnimationCurveEaseInOut(t)
case .spring:
t = listViewAnimationCurveEaseInOut(t)
//t = listViewAnimationCurveSystem(t)
t = lookupSpringValue(t)
case let .custom(x1, y1, x2, y2):
t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t)
}
@ -88,7 +89,69 @@ private final class AnimatableProperty<T: Interpolatable> {
}
}
final class ShutterBlobView: MTKView, MTKViewDelegate {
private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
let table: [(CGFloat, CGFloat)] = [
(0.0, 0.0),
(0.0625, 0.1123005598783493),
(0.125, 0.31598418951034546),
(0.1875, 0.5103585720062256),
(0.25, 0.6650152802467346),
(0.3125, 0.777747631072998),
(0.375, 0.8557760119438171),
(0.4375, 0.9079672694206238),
(0.5, 0.942038357257843),
(0.5625, 0.9638798832893372),
(0.625, 0.9776856303215027),
(0.6875, 0.9863143563270569),
(0.75, 0.991658091545105),
(0.8125, 0.9949421286582947),
(0.875, 0.9969474077224731),
(0.9375, 0.9981651306152344),
(1.0, 1.0)
]
for i in 0 ..< table.count - 2 {
let lhs = table[i]
let rhs = table[i + 1]
if t >= lhs.0 && t <= rhs.0 {
let fraction = (t - lhs.0) / (rhs.0 - lhs.0)
let value = lhs.1 + fraction * (rhs.1 - lhs.1)
return value
}
}
return 1.0
// print("---start---")
// for i in 0 ..< 16 {
// let j = Double(i) * 1.0 / 16.0
// print("\(j) \(listViewAnimationCurveSystem(j))")
// }
// print("---end---")
}
private class ShutterBlobLayer: MetalImageLayer {
override public init() {
super.init()
self.renderer.imageUpdated = { [weak self] image in
self?.contents = image
}
}
override public init(layer: Any) {
super.init()
if let layer = layer as? ShutterBlobLayer {
self.contents = layer.contents
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class ShutterBlobView: UIView {
enum BlobState {
case generic
case video
@ -147,7 +210,6 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
private let commandQueue: MTLCommandQueue
private let drawPassthroughPipelineState: MTLRenderPipelineState
private var viewportDimensions = CGSize(width: 1, height: 1)
private var displayLink: SharedDisplayLinkDriver.Link?
@ -162,6 +224,10 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
private(set) var state: BlobState = .generic
static override var layerClass: AnyClass {
return ShutterBlobLayer.self
}
public init?(test: Bool) {
let mainBundle = Bundle(for: ShutterBlobView.self)
@ -207,17 +273,13 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
self.drawPassthroughPipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
super.init(frame: CGRect(), device: device)
super.init(frame: CGRect())
(self.layer as! ShutterBlobLayer).renderer.device = device
self.isOpaque = false
self.backgroundColor = .clear
self.colorPixelFormat = .bgra8Unorm
self.framebufferOnly = true
self.isPaused = true
self.delegate = self
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
self?.tick()
}
@ -232,10 +294,6 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
self.displayLink?.invalidate()
}
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
self.viewportDimensions = size
}
func updateState(_ state: BlobState, transition: Transition = .immediate) {
guard self.state != state else {
return
@ -297,40 +355,56 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
self.draw()
}
override public func draw(_ rect: CGRect) {
self.redraw(drawable: self.currentDrawable!)
override func layoutSubviews() {
super.layoutSubviews()
self.tick()
}
private func redraw(drawable: MTLDrawable) {
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
private func getNextDrawable(layer: MetalImageLayer, drawableSize: CGSize) -> MetalImageLayer.Drawable? {
layer.renderer.drawableSize = drawableSize
return layer.renderer.nextDrawable()
}
func draw() {
guard let layer = self.layer as? MetalImageLayer else {
return
}
self.updateAnimations()
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
return
}
let renderPassDescriptor = self.currentRenderPassDescriptor!
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0)
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
let viewportDimensions = self.viewportDimensions
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0))
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: drawableSize.width, height: drawableSize.height, znear: -1.0, zfar: 1.0))
renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState)
let w = Float(1)
let h = Float(1)
var vertices: [Float] = [
w, -h,
-w, -h,
-w, h,
w, -h,
-w, h,
w, h
1, -1,
-1, -1,
-1, 1,
1, -1,
-1, 1,
1, 1
]
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height))
var resolution = simd_uint2(UInt32(drawableSize.width), UInt32(drawableSize.height))
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
var primaryParameters = simd_float4(
@ -350,17 +424,17 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding()
commandBuffer.present(drawable)
var storedDrawable: MetalImageLayer.Drawable? = drawable
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {
autoreleasepool {
storedDrawable?.present(completion: {})
storedDrawable = nil
}
}
}
commandBuffer.commit()
}
override func layoutSubviews() {
super.layoutSubviews()
self.tick()
}
func draw(in view: MTKView) {
}
}