mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
867 lines
38 KiB
Swift
867 lines
38 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import LegacyComponents
|
|
import MediaEditor
|
|
import TelegramPresentationData
|
|
|
|
private final class BlurModeComponent: Component {
|
|
typealias EnvironmentType = Empty
|
|
|
|
let title: String
|
|
let icon: UIImage?
|
|
let isSelected: Bool
|
|
|
|
init(
|
|
title: String,
|
|
icon: UIImage?,
|
|
isSelected: Bool
|
|
) {
|
|
self.title = title
|
|
self.icon = icon
|
|
self.isSelected = isSelected
|
|
}
|
|
|
|
static func ==(lhs: BlurModeComponent, rhs: BlurModeComponent) -> Bool {
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if lhs.icon !== rhs.icon {
|
|
return false
|
|
}
|
|
if lhs.isSelected != rhs.isSelected {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let icon = ComponentView<Empty>()
|
|
private let title = ComponentView<Empty>()
|
|
|
|
private var component: BlurModeComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: BlurModeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let iconSize = self.icon.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
Image(
|
|
image: component.icon,
|
|
tintColor: component.isSelected ? UIColor(rgb: 0xf8d74a) : .white,
|
|
size: CGSize(width: 30.0, height: 30.0)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let titleSize = self.title.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
Text(
|
|
text: component.title,
|
|
font: Font.regular(14.0),
|
|
color: component.isSelected ? UIColor(rgb: 0xf8d74a) : UIColor(rgb: 0x808080)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
let spacing: CGFloat = 3.0
|
|
let size = CGSize(width: 66.0, height: iconSize.height + spacing + titleSize.height)
|
|
|
|
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 0.0), size: iconSize)
|
|
if let view = self.icon.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: iconFrame)
|
|
}
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: iconSize.height + spacing), size: titleSize)
|
|
if let view = self.title.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: titleFrame)
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class BlurComponent: Component {
|
|
typealias EnvironmentType = Empty
|
|
|
|
let strings: PresentationStrings
|
|
let value: BlurValue
|
|
let hasPortrait: Bool
|
|
let valueUpdated: (BlurValue) -> Void
|
|
let isTrackingUpdated: (Bool) -> Void
|
|
|
|
init(
|
|
strings: PresentationStrings,
|
|
value: BlurValue,
|
|
hasPortrait: Bool,
|
|
valueUpdated: @escaping (BlurValue) -> Void,
|
|
isTrackingUpdated: @escaping (Bool) -> Void
|
|
) {
|
|
self.strings = strings
|
|
self.value = value
|
|
self.hasPortrait = hasPortrait
|
|
self.valueUpdated = valueUpdated
|
|
self.isTrackingUpdated = isTrackingUpdated
|
|
}
|
|
|
|
static func ==(lhs: BlurComponent, rhs: BlurComponent) -> Bool {
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.value != rhs.value {
|
|
return false
|
|
}
|
|
if lhs.hasPortrait != rhs.hasPortrait {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func makeState() -> State {
|
|
return State(value: self.value)
|
|
}
|
|
|
|
final class State: ComponentState {
|
|
var value: BlurValue
|
|
|
|
init(value: BlurValue) {
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let title = ComponentView<Empty>()
|
|
private let offButton = ComponentView<Empty>()
|
|
private let radialButton = ComponentView<Empty>()
|
|
private let linearButton = ComponentView<Empty>()
|
|
private let portraitButton = ComponentView<Empty>()
|
|
|
|
private let slider = ComponentView<Empty>()
|
|
|
|
private var component: BlurComponent?
|
|
private weak var state: State?
|
|
|
|
private let offImage = UIImage(bundleImageName: "Media Editor/BlurOff")
|
|
private let radialImage = UIImage(bundleImageName: "Media Editor/BlurRadial")
|
|
private let linearImage = UIImage(bundleImageName: "Media Editor/BlurLinear")
|
|
private let portraitImage = UIImage(bundleImageName: "Media Editor/BlurPortrait")
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: BlurComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
state.value = component.value
|
|
|
|
let valueUpdated = component.valueUpdated
|
|
|
|
let titleSize = self.title.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
Text(
|
|
text: component.strings.Story_Editor_Blur_Title,
|
|
font: Font.regular(14.0),
|
|
color: UIColor(rgb: 0x808080)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 11.0), size: titleSize)
|
|
if let view = self.title.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: titleFrame)
|
|
}
|
|
|
|
let offButtonSize = self.offButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
Button(
|
|
content: AnyComponent(
|
|
BlurModeComponent(
|
|
title: component.strings.Story_Editor_Blur_Off,
|
|
icon: self.offImage,
|
|
isSelected: state.value.mode == .off
|
|
)
|
|
),
|
|
action: { [weak state] in
|
|
if let state {
|
|
valueUpdated(state.value.withUpdatedMode(.off))
|
|
}
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let _ = self.radialButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
Button(
|
|
content: AnyComponent(
|
|
BlurModeComponent(
|
|
title: component.strings.Story_Editor_Blur_Radial,
|
|
icon: self.radialImage,
|
|
isSelected: state.value.mode == .radial
|
|
)
|
|
),
|
|
action: { [weak state] in
|
|
if let state {
|
|
valueUpdated(state.value.withUpdatedMode(.radial))
|
|
}
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let _ = self.linearButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
Button(
|
|
content: AnyComponent(
|
|
BlurModeComponent(
|
|
title: component.strings.Story_Editor_Blur_Linear,
|
|
icon: self.linearImage,
|
|
isSelected: state.value.mode == .linear
|
|
)
|
|
),
|
|
action: { [weak state] in
|
|
if let state {
|
|
valueUpdated(state.value.withUpdatedMode(.linear))
|
|
}
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let _ = self.portraitButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
Button(
|
|
content: AnyComponent(
|
|
BlurModeComponent(
|
|
title: component.strings.Story_Editor_Blur_Portrait,
|
|
icon: self.portraitImage,
|
|
isSelected: state.value.mode == .portrait
|
|
)
|
|
),
|
|
action: { [weak state] in
|
|
if let state {
|
|
valueUpdated(state.value.withUpdatedMode(.portrait))
|
|
}
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
let isTrackingUpdated: (Bool) -> Void = { [weak self] isTracking in
|
|
component.isTrackingUpdated(isTracking)
|
|
|
|
if let self {
|
|
let transition: Transition
|
|
if isTracking {
|
|
transition = .immediate
|
|
} else {
|
|
transition = .easeInOut(duration: 0.25)
|
|
}
|
|
|
|
let alpha: CGFloat = isTracking ? 0.0 : 1.0
|
|
if let view = self.title.view {
|
|
transition.setAlpha(view: view, alpha: alpha)
|
|
}
|
|
if let view = self.offButton.view {
|
|
transition.setAlpha(view: view, alpha: alpha)
|
|
}
|
|
if let view = self.radialButton.view {
|
|
transition.setAlpha(view: view, alpha: alpha)
|
|
}
|
|
if let view = self.linearButton.view {
|
|
transition.setAlpha(view: view, alpha: alpha)
|
|
}
|
|
if let view = self.portraitButton.view {
|
|
transition.setAlpha(view: view, alpha: alpha)
|
|
}
|
|
}
|
|
}
|
|
|
|
let sliderSize = self.slider.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
AdjustmentSliderComponent(
|
|
title: "",
|
|
value: state.value.intensity,
|
|
minValue: 0.0,
|
|
maxValue: 1.0,
|
|
startValue: 0.0,
|
|
isEnabled: state.value.mode != .off,
|
|
trackColor: nil,
|
|
displayValue: false,
|
|
valueUpdated: { [weak state] value in
|
|
if let state {
|
|
valueUpdated(state.value.withUpdatedIntensity(value))
|
|
}
|
|
},
|
|
isTrackingUpdated: { isTracking in
|
|
isTrackingUpdated(isTracking)
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
var buttons = [self.offButton, self.radialButton, self.linearButton]
|
|
if component.hasPortrait {
|
|
buttons.append(self.portraitButton)
|
|
}
|
|
|
|
let topInset: CGFloat = 34.0
|
|
let horizontalSpacing: CGFloat = 24.0
|
|
let width: CGFloat = CGFloat(buttons.count) * offButtonSize.width + (CGFloat(buttons.count - 1) * horizontalSpacing)
|
|
let commonX = floorToScreenPixels((availableSize.width - width) / 2.0)
|
|
var offsetX: CGFloat = commonX
|
|
for button in buttons {
|
|
if let view = button.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: offsetX, y: topInset), size: offButtonSize))
|
|
}
|
|
offsetX += offButtonSize.width + horizontalSpacing
|
|
}
|
|
|
|
let verticalSpacing: CGFloat = -5.0
|
|
let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + offButtonSize.height + verticalSpacing), size: sliderSize)
|
|
if let view = self.slider.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: sliderFrame)
|
|
}
|
|
|
|
return CGSize(width: availableSize.width, height: topInset + offButtonSize.height + verticalSpacing + sliderSize.height + 6.0)
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private let blurInsetProximity: CGFloat = 20.0
|
|
private let blurMinimumFalloff: Float = 0.1
|
|
private let blurMinimumDifference: Float = 0.02
|
|
private let blurViewCenterInset: CGFloat = 30.0
|
|
private let blurViewRadiusInset: CGFloat = 30.0
|
|
|
|
final class BlurScreenComponent: Component {
|
|
typealias EnvironmentType = Empty
|
|
|
|
let value: BlurValue
|
|
let valueUpdated: (BlurValue) -> Void
|
|
let isTrackingUpdated: (Bool) -> Void
|
|
|
|
init(
|
|
value: BlurValue,
|
|
valueUpdated: @escaping (BlurValue) -> Void,
|
|
isTrackingUpdated: @escaping (Bool) -> Void
|
|
|
|
) {
|
|
self.value = value
|
|
self.valueUpdated = valueUpdated
|
|
self.isTrackingUpdated = isTrackingUpdated
|
|
}
|
|
|
|
static func ==(lhs: BlurScreenComponent, rhs: BlurScreenComponent) -> Bool {
|
|
if lhs.value != rhs.value {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView, UIGestureRecognizerDelegate {
|
|
enum Control {
|
|
case center
|
|
case innerRadius
|
|
case outerRadius
|
|
case rotation
|
|
case wholeArea
|
|
}
|
|
private var component: BlurScreenComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.backgroundColor = .clear
|
|
self.contentMode = .redraw
|
|
self.isOpaque = false
|
|
|
|
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
|
panGestureRecognizer.delegate = self
|
|
self.addGestureRecognizer(panGestureRecognizer)
|
|
|
|
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:)))
|
|
pinchGestureRecognizer.delegate = self
|
|
self.addGestureRecognizer(pinchGestureRecognizer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private var activeControl: Control?
|
|
private var startCenterPoint: CGPoint?
|
|
private var startDistance: CGFloat?
|
|
private var startRadius: CGFloat?
|
|
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
|
|
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
|
let centerPoint = CGPoint(
|
|
x: component.value.position.x * self.frame.width,
|
|
y: component.value.position.y * self.frame.height
|
|
)
|
|
let delta = CGPoint(x: location.x - centerPoint.x, y: location.y - centerPoint.y)
|
|
let shorterSide = min(self.frame.width, self.frame.height)
|
|
let innerRadius = shorterSide * CGFloat(component.value.falloff)
|
|
let outerRadius = shorterSide * CGFloat(component.value.size)
|
|
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
switch component.value.mode {
|
|
case .radial:
|
|
component.isTrackingUpdated(true)
|
|
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
|
|
|
|
let close = abs(outerRadius - innerRadius) < blurInsetProximity
|
|
let innerRadiusOuterInset = close ? 0 : blurViewRadiusInset
|
|
let outerRadiusInnerInset = close ? 0 : blurViewRadiusInset
|
|
|
|
if distance < blurViewCenterInset {
|
|
self.activeControl = .center
|
|
self.startCenterPoint = centerPoint
|
|
}
|
|
else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset {
|
|
self.activeControl = .innerRadius
|
|
self.startDistance = distance
|
|
self.startRadius = innerRadius
|
|
} else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset {
|
|
self.activeControl = .outerRadius
|
|
self.startDistance = distance
|
|
self.startRadius = outerRadius
|
|
}
|
|
case .linear:
|
|
component.isTrackingUpdated(true)
|
|
let radialDistance = sqrt(delta.x * delta.x + delta.y * delta.y)
|
|
let distance = abs(delta.x * cos(CGFloat(component.value.rotation) + .pi / 2.0) + delta.y * sin(CGFloat(component.value.rotation) + .pi / 2.0))
|
|
|
|
let close = abs(outerRadius - innerRadius) < blurInsetProximity
|
|
let innerRadiusOuterInset = close ? 0 : blurViewRadiusInset
|
|
let outerRadiusInnerInset = close ? 0 : blurViewRadiusInset
|
|
|
|
if radialDistance < blurViewCenterInset {
|
|
self.activeControl = .center
|
|
self.startCenterPoint = centerPoint
|
|
} else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset {
|
|
self.activeControl = .innerRadius
|
|
self.startDistance = distance
|
|
self.startRadius = innerRadius
|
|
} else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset {
|
|
self.activeControl = .outerRadius;
|
|
self.startDistance = distance
|
|
self.startRadius = outerRadius
|
|
} else if distance <= innerRadius - blurViewRadiusInset || distance >= outerRadius + blurViewRadiusInset {
|
|
self.activeControl = .rotation
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
case .changed:
|
|
switch component.value.mode {
|
|
case .radial:
|
|
guard let activeControl = self.activeControl else {
|
|
return
|
|
}
|
|
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
|
|
|
|
switch activeControl {
|
|
case .center:
|
|
guard let startCenterPoint = self.startCenterPoint else {
|
|
return
|
|
}
|
|
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
|
let centerPoint = CGPoint(
|
|
x: max(0.0, min(self.frame.width, startCenterPoint.x + translation.x)),
|
|
y: max(0.0, min(self.frame.height, startCenterPoint.y + translation.y))
|
|
)
|
|
let position = CGPoint(
|
|
x: centerPoint.x / self.frame.width,
|
|
y: centerPoint.y / self.frame.height
|
|
)
|
|
component.valueUpdated(component.value.withUpdatedPosition(position))
|
|
case .innerRadius:
|
|
guard let startDistance = self.startDistance, let startRadius = self.startRadius else {
|
|
return
|
|
}
|
|
let delta = distance - startDistance
|
|
let falloff = min(max(blurMinimumFalloff, Float((startRadius + delta) / shorterSide)), component.value.size - blurMinimumDifference)
|
|
component.valueUpdated(component.value.withUpdatedFalloff(falloff))
|
|
case .outerRadius:
|
|
guard let startDistance = self.startDistance, let startRadius = self.startRadius else {
|
|
return
|
|
}
|
|
let delta = distance - startDistance
|
|
let size = max(component.value.falloff + blurMinimumDifference, Float((startRadius + delta) / shorterSide))
|
|
component.valueUpdated(component.value.withUpdatedSize(size))
|
|
default:
|
|
break
|
|
}
|
|
case .linear:
|
|
guard let activeControl = self.activeControl else {
|
|
return
|
|
}
|
|
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
|
|
|
|
switch activeControl {
|
|
case .center:
|
|
guard let startCenterPoint = self.startCenterPoint else {
|
|
return
|
|
}
|
|
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
|
let centerPoint = CGPoint(
|
|
x: max(0.0, min(self.frame.width, startCenterPoint.x + translation.x)),
|
|
y: max(0.0, min(self.frame.height, startCenterPoint.y + translation.y))
|
|
)
|
|
let position = CGPoint(
|
|
x: centerPoint.x / self.frame.width,
|
|
y: centerPoint.y / self.frame.height
|
|
)
|
|
component.valueUpdated(component.value.withUpdatedPosition(position))
|
|
case .innerRadius:
|
|
guard let startDistance = self.startDistance, let startRadius = self.startRadius else {
|
|
return
|
|
}
|
|
let delta = distance - startDistance
|
|
let falloff = min(max(blurMinimumFalloff, Float((startRadius + delta) / shorterSide)), component.value.size - blurMinimumDifference)
|
|
component.valueUpdated(component.value.withUpdatedFalloff(falloff))
|
|
case .outerRadius:
|
|
guard let startDistance = self.startDistance, let startRadius = self.startRadius else {
|
|
return
|
|
}
|
|
let delta = distance - startDistance
|
|
let size = max(component.value.falloff + blurMinimumDifference, Float((startRadius + delta) / shorterSide))
|
|
component.valueUpdated(component.value.withUpdatedSize(size))
|
|
case .rotation:
|
|
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
|
var clockwise = false
|
|
let right = location.x > centerPoint.x
|
|
let bottom = location.y > centerPoint.y
|
|
|
|
if !right && !bottom {
|
|
if abs(translation.y) > abs(translation.x) {
|
|
if translation.y < 0.0 {
|
|
clockwise = true
|
|
}
|
|
} else {
|
|
if translation.x > 0.0 {
|
|
clockwise = true
|
|
}
|
|
}
|
|
} else if right && !bottom {
|
|
if abs(translation.y) > abs(translation.x) {
|
|
if translation.y > 0.0 {
|
|
clockwise = true
|
|
}
|
|
} else
|
|
{
|
|
if translation.x > 0.0 {
|
|
clockwise = true
|
|
}
|
|
}
|
|
} else if right && bottom {
|
|
if abs(translation.y) > abs(translation.x) {
|
|
if translation.y > 0 {
|
|
clockwise = true
|
|
}
|
|
} else {
|
|
if translation.x < 0 {
|
|
clockwise = true
|
|
}
|
|
}
|
|
} else {
|
|
if abs(translation.y) > abs(translation.x) {
|
|
if translation.y < 0 {
|
|
clockwise = true
|
|
}
|
|
} else {
|
|
if translation.x < 0 {
|
|
clockwise = true
|
|
}
|
|
}
|
|
}
|
|
|
|
let delta = sqrt(translation.x * translation.x + translation.y * translation.y)
|
|
|
|
let angleInDegress = radiansToDegrees(radians: CGFloat(component.value.rotation))
|
|
let updatedAngle = angleInDegress + delta * (clockwise ? 1.0 : -1.0) / .pi / 1.15
|
|
component.valueUpdated(component.value.withUpdatedRotation(Float(degreesToRadians(degrees: updatedAngle))))
|
|
|
|
gestureRecognizer.setTranslation(.zero, in: gestureRecognizer.view)
|
|
default:
|
|
break
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
default:
|
|
component.isTrackingUpdated(false)
|
|
self.activeControl = nil
|
|
self.startCenterPoint = nil
|
|
self.startDistance = nil
|
|
self.startRadius = nil
|
|
}
|
|
}
|
|
|
|
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
component.isTrackingUpdated(true)
|
|
self.activeControl = .wholeArea
|
|
case .changed:
|
|
let scale = Float(gestureRecognizer.scale)
|
|
|
|
let size = max(component.value.falloff + blurMinimumDifference, component.value.size * scale)
|
|
let falloff = max(blurMinimumFalloff, component.value.falloff * scale)
|
|
component.valueUpdated(component.value.withUpdatedSize(size).withUpdatedFalloff(falloff))
|
|
|
|
gestureRecognizer.scale = 1.0
|
|
default:
|
|
component.isTrackingUpdated(false)
|
|
self.activeControl = nil
|
|
}
|
|
}
|
|
|
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
guard let component = self.component else {
|
|
return false
|
|
}
|
|
|
|
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
|
let centerPoint = CGPoint(
|
|
x: component.value.position.x * self.frame.width,
|
|
y: component.value.position.y * self.frame.height
|
|
)
|
|
let delta = CGPoint(x: location.x - centerPoint.x, y: location.y - centerPoint.y)
|
|
let innerRadius = min(self.frame.width, self.frame.height) * CGFloat(component.value.falloff)
|
|
let outerRadius = min(self.frame.width, self.frame.height) * CGFloat(component.value.size)
|
|
|
|
switch component.value.mode {
|
|
case .radial:
|
|
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
|
|
|
|
let close = abs(outerRadius - innerRadius) < blurInsetProximity
|
|
let innerRadiusOuterInset = close ? 0.0 : blurViewRadiusInset
|
|
let outerRadiusInnerInset = close ? 0.0 : blurViewRadiusInset
|
|
|
|
if distance < blurViewCenterInset && gestureRecognizer is UIPanGestureRecognizer {
|
|
return true
|
|
} else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset {
|
|
return true
|
|
} else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset {
|
|
return true
|
|
}
|
|
case .linear:
|
|
let radialDistance = sqrt(delta.x * delta.x + delta.y * delta.y)
|
|
let distance = abs(delta.x * cos(CGFloat(component.value.rotation) + .pi / 2.0) + delta.y * sin(CGFloat(component.value.rotation) + .pi / 2.0))
|
|
|
|
let close = abs(outerRadius - innerRadius) < blurInsetProximity
|
|
let innerRadiusOuterInset = close ? 0.0 : blurViewRadiusInset
|
|
let outerRadiusInnerInset = close ? 0.0 : blurViewRadiusInset
|
|
|
|
if radialDistance < blurViewCenterInset && gestureRecognizer is UIPanGestureRecognizer {
|
|
return true
|
|
} else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset {
|
|
return true
|
|
} else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset {
|
|
return true
|
|
} else if distance <= innerRadius - blurViewRadiusInset || distance >= outerRadius + blurViewRadiusInset {
|
|
return true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
return false
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return true
|
|
}
|
|
|
|
func update(component: BlurScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
self.setNeedsDisplay()
|
|
|
|
return availableSize
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
guard let context = UIGraphicsGetCurrentContext(), let component = self.component else {
|
|
return
|
|
}
|
|
guard ![.off, .portrait].contains(component.value.mode) else {
|
|
return
|
|
}
|
|
|
|
let centerPoint = CGPoint(
|
|
x: component.value.position.x * rect.size.width,
|
|
y: component.value.position.y * rect.size.height
|
|
)
|
|
let innerRadius = min(rect.size.width, rect.size.height) * CGFloat(component.value.falloff)
|
|
let outerRadius = min(rect.size.width, rect.size.height) * CGFloat(component.value.size)
|
|
|
|
context.setFillColor(UIColor.white.cgColor)
|
|
context.setShadow(offset: .zero, blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
|
|
|
|
let knobSize = CGSize(width: 16.0, height: 16.0)
|
|
switch component.value.mode {
|
|
case .radial:
|
|
var radSpace = degreesToRadians(degrees: 6.15)
|
|
var radLen = degreesToRadians(degrees: 10.2)
|
|
for i in 0 ..< 22 {
|
|
let cgPath = CGMutablePath()
|
|
cgPath.addArc(
|
|
center: centerPoint,
|
|
radius: innerRadius,
|
|
startAngle: CGFloat(i) * (radSpace + radLen),
|
|
endAngle: CGFloat(i) * (radSpace + radLen) + radLen,
|
|
clockwise: false
|
|
)
|
|
let strokedArc = cgPath.copy(strokingWithWidth: 1.5, lineCap: .butt, lineJoin: .miter, miterLimit: 10.0)
|
|
context.addPath(strokedArc)
|
|
}
|
|
|
|
radSpace = degreesToRadians(degrees: 2.02)
|
|
radLen = degreesToRadians(degrees: 3.6)
|
|
for i in 0 ..< 64 {
|
|
let cgPath = CGMutablePath()
|
|
cgPath.addArc(
|
|
center: centerPoint,
|
|
radius: outerRadius,
|
|
startAngle: CGFloat(i) * (radSpace + radLen),
|
|
endAngle: CGFloat(i) * (radSpace + radLen) + radLen,
|
|
clockwise: false
|
|
)
|
|
let strokedArc = cgPath.copy(strokingWithWidth: 1.5, lineCap: .butt, lineJoin: .miter, miterLimit: 10.0)
|
|
context.addPath(strokedArc)
|
|
}
|
|
context.fillPath()
|
|
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: centerPoint.x - knobSize.width / 2.0, y: centerPoint.y - knobSize.height / 2.0), size: knobSize))
|
|
case .linear:
|
|
context.translateBy(x: centerPoint.x, y: centerPoint.y)
|
|
context.rotate(by: CGFloat(component.value.rotation))
|
|
|
|
let space: CGFloat = 6.0
|
|
var length: CGFloat = 12.0
|
|
let thickness: CGFloat = 1.5
|
|
|
|
for i in 0 ..< 30 {
|
|
context.addRect(CGRect(x: CGFloat(i) * (length + space), y: -innerRadius, width: length, height: thickness))
|
|
context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: -innerRadius, width: length, height: thickness))
|
|
|
|
context.addRect(CGRect(x: CGFloat(i) * (length + space), y: innerRadius, width: length, height: thickness))
|
|
context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: innerRadius, width: length, height: thickness))
|
|
}
|
|
|
|
length = 6.0
|
|
|
|
for i in 0 ..< 64 {
|
|
context.addRect(CGRect(x: CGFloat(i) * (length + space), y: -outerRadius, width: length, height: thickness))
|
|
context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: -outerRadius, width: length, height: thickness))
|
|
|
|
context.addRect(CGRect(x: CGFloat(i) * (length + space), y: outerRadius, width: length, height: thickness))
|
|
context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: outerRadius, width: length, height: thickness))
|
|
}
|
|
|
|
context.fillPath()
|
|
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: -knobSize.width / 2.0, y: -knobSize.height / 2.0), size: knobSize))
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private func degreesToRadians(degrees: CGFloat) -> CGFloat {
|
|
return degrees * .pi / 180.0
|
|
}
|
|
|
|
private func radiansToDegrees(radians: CGFloat) -> CGFloat {
|
|
return radians * 180.0 / .pi
|
|
}
|