I have a Rect view which comes from a shape. For the final touch, I need to use CGAffineTransform when adding my custom path. When I use CGAffineTransform, the parameters a and tx need to be calculated and updated inside the shape to keep the entire final path visible. Currently, I don't know the exact method to have a and tx update themselves while I am changing the view size. I have to adjust them manually using a slider, which is not part of the app or program—it's just a debugging tool. As you can see in the video, by changing the values of a and tx, I can keep the entire path within the frame, but the user needs this to happen automatically. So, how can I make a and tx update automatically while keeping the entire path in the frame?
Part 1:
Part 2:
code:
import SwiftUI
struct ContentView: View {
@State private var a: Double = 0.8
@State private var b: Double = 0
@State private var c: Double = -0.17
@State private var d: Double = 1
@State private var tx: Double = 100
@State private var ty: Double = 0
var body: some View {
FinalShape(a: a, b: b, c: c, d: d, tx: tx, ty: ty)
.strokeBorder(style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round))
.background(Color.purple)
.cornerRadius(5.0)
.padding()
VStack {
HStack {
Text("a:")
Slider(value: $a, in: -1...1)
}
HStack {
Text("b:")
Slider(value: $b, in: -1...1)
}
HStack {
Text("c:")
Slider(value: $c, in: -1...1)
}
HStack {
Text("d:")
Slider(value: $d, in: -1...1)
}
HStack {
Text("tx:")
Slider(value: $tx, in: -200...200)
}
HStack {
Text("ty:")
Slider(value: $ty, in: -200...200)
}
}
.padding()
HStack {
VStack(alignment: .leading) {
Text("a:" + String(describing: a))
Text("b:" + String(describing: b))
Text("c:" + String(describing: c))
Text("d:" + String(describing: d))
Text("tx:" + String(describing: tx))
Text("ty:" + String(describing: ty))
}
Spacer()
}
.padding()
}
}
struct FinalShape: InsettableShape {
var a: Double
var b: Double
var c: Double
var d: Double
var tx: Double
var ty: Double
var insetAmount: CGFloat = CGFloat.zero
func path(in rect: CGRect) -> Path {
return Path { path in
let transform = CGAffineTransform(a: a, b: b, c: c, d: d, tx: tx, ty: ty)
path.addPath(RecShape(insetAmount: insetAmount).path(in: CGRect(origin: CGPoint(x: rect.minX, y: rect.minY), size: rect.size)), transform: transform)
}
}
func inset(by amount: CGFloat) -> some InsettableShape {
var myShape: Self = self
myShape.insetAmount += amount
return myShape
}
}
struct RecShape: InsettableShape {
var insetAmount: CGFloat = CGFloat.zero
func path(in rect: CGRect) -> Path {
return Path { path in
path.addRect(CGRect(origin: CGPoint(x: rect.minX + insetAmount, y: rect.minY + insetAmount), size: CGSize(width: rect.width - 2*insetAmount, height: rect.height - 2*insetAmount)))
}
}
func inset(by amount: CGFloat) -> some InsettableShape {
var myShape: Self = self
myShape.insetAmount += amount
return myShape
}
}
I assume that b
, c
, d
, ty
are all fixed.
Recall the equation that describes how a point will be transformed by an affine transform.
x' = ax + cy + tx
y' = bx + dy + ty
Based on your gifs, you seem to want the rectangle's minX
and maxX
to not change after applying the transformation. We can set up two equations and find the two unknowns a
and tx
. Here I have considered the two points (minX, maxY) and (maxX, minY).
minX = a * minX + c * maxY + tx
maxX = a * maxX + c * minY + tx
After solving these, you can write:
func path(in rect: CGRect) -> Path {
let insettedRect = rect.insetBy(dx: insetAmount, dy: insetAmount)
if insettedRect.minX - insettedRect.maxX == 0 {
// in this case, the rect has no width, and the equation has no solution
// - you should decide what you want to return
return Path()
}
// I just used the path from a Rectangle() here, but you can use any path you like
let path = Rectangle().path(in: insettedRect)
let numerator = (insettedRect.minX - c * insettedRect.maxY - insettedRect.maxX - c * insettedRect.minY)
let a = numerator / (insettedRect.minX - insettedRect.maxX)
let tx = insettedRect.maxX - a * insettedRect.maxX + c * insettedRect.minY
return path.applying(CGAffineTransform(a, b, c, d, tx, ty))
}
What "inset" means for this shape is questionable. Here I just insetted the given rect
, but I suggest not making this insettable at all.
Example usage:
FinalShape(b: 0, c: -0.17, d: 1, ty: 0)
.strokeBorder(style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round))
.background(Color.purple)
.padding()
Note that it is still possible to pass in values of d
and ty
such that the shape exceeds the rectangle vertically. No values of a
and tx
can help you with that, because a
and tx
controls the transformation in the horizontal axis.