Ответ 1
Проблема заключается в том, что у вас есть несколько геометрий (системы координат), а также масштабные коэффициенты и контрольные точки в игре, и трудно держать их прямолинейными. У вас есть геометрия корневого представления, в которой определены кадры изображения и вид наклейки, а затем у вас есть геометрия графического контекста, и вы не делаете их совпадающими. Происхождение изображения не является источником геометрии супервизора, потому что вы ограничили его безопасными областями, и я не уверен, что вы должным образом компенсируете это смещение. Вы пытаетесь справиться с масштабированием изображения в представлении изображения, отрегулировав размер наклейки при рисовании наклейки. Вы не должным образом компенсируете тот факт, что у вас есть свойство стикера center
и его transform
, влияющие на его позу (местоположение/масштаб/вращение).
Пусть упрощается.
Во-первых, введем "представление холста" в качестве представления вида изображения. Вид холста может быть выложен, однако вы хотите, чтобы в отношении безопасных областей. Мы ограничим представление изображения, чтобы заполнить представление холста, поэтому начало изображения будет .zero
.
Затем мы установим представление наклейки layer.anchorPoint
на .zero
. Это приводит к тому, что вид transform
работает относительно верхнего левого угла наклейки, а не его центра. Он также делает view.layer.position
(что то же самое, что и view.center
) управляет положением верхнего левого угла вида вместо того, чтобы управлять положением центра представления. Нам нужны эти изменения, потому что они соответствуют тому, как Core Graphics рисует изображение наклейки в areaRect
, когда мы объединяем изображения.
Мы также установим view.layer.position
в .zero
. Это упрощает процесс вычисления изображения наклейки при объединении изображений.
private func makeStickerView(with image: UIImage, center: CGPoint) -> UIImageView {
let heightOnWidthRatio = image.size.height / image.size.width
let imageWidth: CGFloat = 150
// let newStickerImageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageWidth * heightOnWidthRatio)))
let view = UIImageView(frame: CGRect(x: 0, y: 0, width: imageWidth, height: imageWidth * heightOnWidthRatio))
view.image = image
view.clipsToBounds = true
view.contentMode = .scaleAspectFit
view.isUserInteractionEnabled = true
view.backgroundColor = UIColor.red.withAlphaComponent(0.7)
view.layer.anchorPoint = .zero
view.layer.position = .zero
return view
}
Это означает, что мы должны полностью разместить наклейку с помощью transform
, поэтому мы хотим инициализировать transform
, чтобы центрировать стикер:
@IBAction func resetPose(_ sender: Any) {
let center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
let size = stickerView.bounds.size
stickerView.transform = .init(translationX: center.x - size.width / 2, y: center.y - size.height / 2)
}
Из-за этих изменений нам приходится обращаться с пинчами и вращаться более сложным образом. Мы будем использовать вспомогательный метод для управления сложностью:
extension CGAffineTransform {
func around(_ locus: CGPoint, do body: (CGAffineTransform) -> (CGAffineTransform)) -> CGAffineTransform {
var transform = self.translatedBy(x: locus.x, y: locus.y)
transform = body(transform)
transform = transform.translatedBy(x: -locus.x, y: -locus.y)
return transform
}
}
Затем мы обрабатываем пинч и поворачиваем так:
@objc private func stickerDidPinch(pincher: UIPinchGestureRecognizer) {
guard let stickerView = pincher.view else { return }
stickerView.transform = stickerView.transform.around(pincher.location(in: stickerView), do: { $0.scaledBy(x: pincher.scale, y: pincher.scale) })
pincher.scale = 1
}
@objc private func stickerDidRotate(rotater: UIRotationGestureRecognizer) {
guard let stickerView = rotater.view else { return }
stickerView.transform = stickerView.transform.around(rotater.location(in: stickerView), do: { $0.rotated(by: rotater.rotation) })
rotater.rotation = 0
}
Это также делает работу по масштабированию и вращению лучше, чем раньше. В вашем коде масштабирование и поворот всегда происходят вокруг центра представления. С помощью этого кода они происходят вокруг центральной точки между пальцами пользователя, что кажется более естественным.
Наконец, чтобы объединить изображения, мы начнем с масштабирования геометрии контекста графики так же, как imageView
масштабирует свое изображение, потому что преобразование наклейки относится к размеру imageView
, а не размеру изображения. Поскольку мы теперь полностью позиционируем стикер, используя преобразование, и поскольку мы установили исходный вид изображения и изображения наклейки на .zero
, нам не нужно вносить какие-либо корректировки в странное происхождение.
extension UIImage {
func merge(in viewSize: CGSize, with imageTuples: [(image: UIImage, viewSize: CGSize, transform: CGAffineTransform)]) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
print("scale : \(UIScreen.main.scale)")
print("size : \(size)")
print("--------------------------------------")
guard let context = UIGraphicsGetCurrentContext() else { return nil }
// Scale the context geometry to match the size of the image view that displayed me, because that what all the transforms are relative to.
context.scaleBy(x: size.width / viewSize.width, y: size.height / viewSize.height)
draw(in: CGRect(origin: .zero, size: viewSize), blendMode: .normal, alpha: 1)
for imageTuple in imageTuples {
let areaRect = CGRect(origin: .zero, size: imageTuple.viewSize)
context.saveGState()
context.concatenate(imageTuple.transform)
context.setBlendMode(.color)
UIColor.purple.withAlphaComponent(0.5).setFill()
context.fill(areaRect)
imageTuple.image.draw(in: areaRect, blendMode: .normal, alpha: 1)
context.restoreGState()
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Вы можете найти мою фиксированную версию тестового проекта здесь.