2023-06-10 00:50:25 +04:00

729 lines
29 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import LegacyComponents
import MediaEditor
import MultilineTextComponent
private class HistogramView: UIView {
private var size: CGSize?
private var histogramBins: MediaEditorHistogram.HistogramBins?
private var color: UIColor?
private let shapeLayer = SimpleShapeLayer()
var dataPointsUpdated: (([Float]) -> Void)?
init() {
super.init(frame: .zero)
self.layer.addSublayer(self.shapeLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateSize(size: CGSize, histogramBins: MediaEditorHistogram.HistogramBins?, color: UIColor, transition: Transition) {
guard self.size != size || self.color != color || self.histogramBins != histogramBins else {
return
}
self.size = size
self.histogramBins = histogramBins
self.color = color
self.update(transition: transition)
}
func update(transition: Transition) {
guard let size = self.size, let histogramBins = self.histogramBins, histogramBins.count > 0, let color = self.color else {
self.shapeLayer.path = nil
return
}
transition.setShapeLayerFillColor(layer: self.shapeLayer, color: color)
let (path, _) = curveThroughPoints(
count: histogramBins.count,
valueAtIndex: { index in
return histogramBins.valueAtIndex(index, mirrored: true)
},
positionAtIndex: { index, step in
return CGFloat(index) * step
},
size: size,
type: .filled,
granularity: 200
)
transition.setShapeLayerPath(layer: self.shapeLayer, path: path.cgPath)
}
}
enum CurvesSection {
case all
case red
case green
case blue
}
class CurvesInternalState {
var section: CurvesSection = .all
}
final class CurvesComponent: Component {
typealias EnvironmentType = Empty
let histogram: MediaEditorHistogram?
let internalState: CurvesInternalState
init(
histogram: MediaEditorHistogram?,
internalState: CurvesInternalState
) {
self.histogram = histogram
self.internalState = internalState
}
static func ==(lhs: CurvesComponent, rhs: CurvesComponent) -> Bool {
if lhs.histogram != rhs.histogram {
return false
}
return true
}
final class State: ComponentState {
let internalState: CurvesInternalState
init(internalState: CurvesInternalState) {
self.internalState = internalState
}
var section: CurvesSection {
get {
return self.internalState.section
}
set {
self.internalState.section = newValue
}
}
}
func makeState() -> State {
return State(internalState: self.internalState)
}
final class View: UIView {
private var allButton = ComponentView<Empty>()
private var redButton = ComponentView<Empty>()
private var greenButton = ComponentView<Empty>()
private var blueButton = ComponentView<Empty>()
private let histogramView = HistogramView()
private var component: CurvesComponent?
private weak var state: State?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.histogramView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: CurvesComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let topInset: CGFloat = 11.0
let allButtonSize = self.allButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: "All",
font: Font.regular(14.0),
color: state.section == .all ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .all
state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
)
),
environment: {},
containerSize: availableSize
)
let allButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 - allButtonSize.width / 2.0), y: topInset), size: allButtonSize)
if let view = self.allButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: allButtonFrame)
}
let redButtonSize = self.redButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: "Red",
font: Font.regular(14.0),
color: state.section == .red ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .red
state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
)
),
environment: {},
containerSize: availableSize
)
let redButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 2.0 - redButtonSize.width / 2.0), y: topInset), size: redButtonSize)
if let view = self.redButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: redButtonFrame)
}
let greenButtonSize = self.greenButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: "Green",
font: Font.regular(14.0),
color: state.section == .green ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .green
state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
)
),
environment: {},
containerSize: availableSize
)
let greenButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 3.0 - greenButtonSize.width / 2.0), y: topInset), size: greenButtonSize)
if let view = self.greenButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: greenButtonFrame)
}
let blueButtonSize = self.blueButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: "Blue",
font: Font.regular(14.0),
color: state.section == .blue ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .blue
state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
)
),
environment: {},
containerSize: availableSize
)
let blueButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 4.0 - blueButtonSize.width / 2.0), y: topInset), size: blueButtonSize)
if let view = self.blueButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: blueButtonFrame)
}
let histogramHeight: CGFloat = 85.0
let histogramColor: UIColor
let histogramBins: MediaEditorHistogram.HistogramBins?
switch state.section {
case .all:
histogramColor = .white
histogramBins = component.histogram?.luminance
case .red:
histogramColor = UIColor(rgb: 0xed3d4c)
histogramBins = component.histogram?.red
case .green:
histogramColor = UIColor(rgb: 0x10ee9d)
histogramBins = component.histogram?.green
case .blue:
histogramColor = UIColor(rgb: 0x3377fb)
histogramBins = component.histogram?.blue
}
let histogramSize = CGSize(width: availableSize.width, height: histogramHeight)
let verticalSpacing: CGFloat = 3.0
self.histogramView.updateSize(size: histogramSize, histogramBins: histogramBins, color: histogramColor, transition: transition)
transition.setFrame(view: self.histogramView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + allButtonSize.height + verticalSpacing), size: histogramSize))
return CGSize(width: availableSize.width, height: topInset + allButtonSize.height + verticalSpacing + histogramHeight)
}
}
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 CurvesScreenComponent: Component {
typealias EnvironmentType = Empty
let value: CurvesValue
let section: CurvesSection
let valueUpdated: (CurvesValue) -> Void
init(
value: CurvesValue,
section: CurvesSection,
valueUpdated: @escaping (CurvesValue) -> Void
) {
self.value = value
self.section = section
self.valueUpdated = valueUpdated
}
static func ==(lhs: CurvesScreenComponent, rhs: CurvesScreenComponent) -> Bool {
if lhs.value != rhs.value {
return false
}
if lhs.section != rhs.section {
return false
}
return true
}
final class View: UIView {
enum Field {
case blacks
case shadows
case midtones
case highlights
case whites
}
private var blacks = ComponentView<Empty>()
private var shadows = ComponentView<Empty>()
private var midtones = ComponentView<Empty>()
private var highlights = ComponentView<Empty>()
private let whites = ComponentView<Empty>()
private let line1 = SimpleLayer()
private let line2 = SimpleLayer()
private let line3 = SimpleLayer()
private let line4 = SimpleLayer()
private let curveContainer = SimpleLayer()
private let guideLayer = SimpleShapeLayer()
private let curveLayer = SimpleShapeLayer()
private var component: CurvesScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.line1)
self.layer.addSublayer(self.line2)
self.layer.addSublayer(self.line3)
self.layer.addSublayer(self.line4)
self.layer.addSublayer(self.curveContainer)
self.curveContainer.addSublayer(self.guideLayer)
self.curveContainer.addSublayer(self.curveLayer)
self.curveContainer.masksToBounds = true
self.curveContainer.cornerRadius = 12.0
if #available(iOS 13.0, *) {
self.curveContainer.cornerCurve = .continuous
}
self.line1.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.line2.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.line3.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.line4.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.guideLayer.lineWidth = 1.5
self.guideLayer.lineDashPattern = [7, 4]
self.guideLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.curveLayer.lineWidth = 2.0
self.curveLayer.fillColor = UIColor.clear.cgColor
let allLayers = [
self.line1,
self.line2,
self.line3,
self.line4,
self.guideLayer,
]
for layer in allLayers {
layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
layer.shadowRadius = 1.5
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.3
}
self.curveLayer.shadowOffset = CGSize(width: 0.0, height: 0.0)
self.curveLayer.shadowRadius = 2.0
self.curveLayer.shadowColor = UIColor.black.cgColor
self.curveLayer.shadowOpacity = 0.16
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
self.addGestureRecognizer(panGestureRecognizer)
let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:)))
doubleTapGestureRecognizer.numberOfTapsRequired = 2
self.addGestureRecognizer(doubleTapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var selectedField: Field?
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
let fieldWidth = self.frame.width / 5.0
switch gestureRecognizer.state {
case .began:
let location = gestureRecognizer.location(in: gestureRecognizer.view).x
let index = floor(location / fieldWidth)
switch index {
case 0:
self.selectedField = .blacks
case 1:
self.selectedField = .shadows
case 2:
self.selectedField = .midtones
case 3:
self.selectedField = .highlights
case 4:
self.selectedField = .whites
default:
break
}
case .changed:
guard let selectedField = self.selectedField else {
return
}
let translation = gestureRecognizer.translation(in: gestureRecognizer.view).y
let delta = Float(min(2.0, -1.0 * translation / 8.0) / 100.0)
var updatedValue = component.value
var curve: CurvesValue.CurveValue
switch component.section {
case .all:
curve = updatedValue.all
case .red:
curve = updatedValue.red
case .green:
curve = updatedValue.green
case .blue:
curve = updatedValue.blue
}
switch selectedField {
case .blacks:
curve = curve.withUpdatedBlacks(max(0.0, min(1.0, curve.blacks + delta)))
case .shadows:
curve = curve.withUpdatedShadows(max(0.0, min(1.0, curve.shadows + delta)))
case .midtones:
curve = curve.withUpdatedMidtones(max(0.0, min(1.0, curve.midtones + delta)))
case .highlights:
curve = curve.withUpdatedHighlights(max(0.0, min(1.0, curve.highlights + delta)))
case .whites:
curve = curve.withUpdatedWhites(max(0.0, min(1.0, curve.whites + delta)))
}
switch component.section {
case .all:
updatedValue = updatedValue.withUpdatedAll(curve)
case .red:
updatedValue = updatedValue.withUpdatedRed(curve)
case .green:
updatedValue = updatedValue.withUpdatedGreen(curve)
case .blue:
updatedValue = updatedValue.withUpdatedBlue(curve)
}
component.valueUpdated(updatedValue)
gestureRecognizer.setTranslation(.zero, in: gestureRecognizer.view)
default:
self.selectedField = nil
}
}
@objc func handleDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
switch component.section {
case .all:
component.valueUpdated(component.value.withUpdatedAll(.initial))
case .red:
component.valueUpdated(component.value.withUpdatedRed(.initial))
case .green:
component.valueUpdated(component.value.withUpdatedGreen(.initial))
case .blue:
component.valueUpdated(component.value.withUpdatedBlue(.initial))
}
}
func update(component: CurvesScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let value: CurvesValue.CurveValue
let lineColor: UIColor
switch component.section {
case .all:
lineColor = UIColor.white
value = component.value.all
case .red:
lineColor = UIColor(rgb: 0xed3d4c)
value = component.value.red
case .green:
lineColor = UIColor(rgb: 0x10ee9d)
value = component.value.green
case .blue:
lineColor = UIColor(rgb: 0x3377fb)
value = component.value.blue
}
let fieldWidth = availableSize.width / 5.0
let bottomInset: CGFloat = 5.0
let blacksSize = self.blacks.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.blacks),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let blacksFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - blacksSize.height - bottomInset), size: blacksSize)
if let view = self.blacks.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: blacksFrame)
}
let shadowsSize = self.shadows.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.shadows),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let shadowsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - shadowsSize.height - bottomInset), size: shadowsSize)
if let view = self.shadows.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: shadowsFrame)
}
let midtonesSize = self.midtones.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.midtones),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let midtonesFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 2.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - midtonesSize.height - bottomInset), size: midtonesSize)
if let view = self.midtones.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: midtonesFrame)
}
let highlightsSize = self.highlights.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.highlights),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let highlightsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 3.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - highlightsSize.height - bottomInset), size: highlightsSize)
if let view = self.highlights.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: highlightsFrame)
}
let whitesSize = self.whites.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.whites),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let whitesFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 4.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - whitesSize.height - bottomInset), size: whitesSize)
if let view = self.whites.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: whitesFrame)
}
self.curveContainer.frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: availableSize.height + 12.0))
let lineWidth: CGFloat = 1.0 - UIScreenPixel
self.line1.frame = CGRect(x: fieldWidth, y: 0.0, width: lineWidth, height: availableSize.height)
self.line2.frame = CGRect(x: fieldWidth * 2.0, y: 0.0, width: lineWidth, height: availableSize.height)
self.line3.frame = CGRect(x: fieldWidth * 3.0, y: 0.0, width: lineWidth, height: availableSize.height)
self.line4.frame = CGRect(x: fieldWidth * 4.0, y: 0.0, width: lineWidth, height: availableSize.height)
let guidePath = UIBezierPath()
guidePath.move(to: CGPoint(x: 0.0, y: availableSize.height))
guidePath.addLine(to: CGPoint(x: availableSize.width, y: 0.0))
self.guideLayer.frame = CGRect(origin: .zero, size: availableSize)
self.guideLayer.path = guidePath.cgPath
self.curveLayer.strokeColor = lineColor.cgColor
self.curveLayer.frame = CGRect(origin: .zero, size: availableSize)
let points: [Float] = [
value.blacks,
value.blacks,
value.shadows,
value.midtones,
value.highlights,
value.whites,
value.whites
]
let (curvePath, _) = curveThroughPoints(
count: points.count,
valueAtIndex: { index in
return 1.0 - points[index]
},
positionAtIndex: { index, _ in
switch index {
case 0:
return -1.0
case 1:
return 0.0
case 2:
return 0.25 * availableSize.width
case 3:
return 0.5 * availableSize.width
case 4:
return 0.75 * availableSize.width
case 5:
return availableSize.width
default:
return availableSize.width + 1.0
}
},
size: availableSize,
type: .line,
granularity: 100
)
self.curveLayer.path = curvePath.cgPath
return availableSize
}
}
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)
}
}