Related
I have what I thought was a simple task, but it appears otherwise. As part of my code, I am simply displaying some circles and when the user clicks on one, it will navigate to a new view depending on which circle was pressed.
If I hard code the views in the NavigationLink, it works fine, but I want to use a dynamic array. In the following code, it doesn't matter which circle is pressed, it will always display the id of the last item in the array; ie it passes the last defined dot.destinationId to the Game view
struct Dot: Identifiable {
let id = UUID()
let x: CGFloat
let y: CGFloat
let color: Color
let radius: CGFloat
let destinationId: Int
}
struct ContentView: View {
var dots = [
Dot(x:10.0,y:10.0, color: Color.green, radius: 12, destinationId: 1),
Dot(x:110.0,y:10.0, color: Color.green, radius: 12, destinationId: 2),
Dot(x:110.0,y:110.0, color: Color.blue, radius: 12, destinationId: 3),
Dot(x:210.0,y:110.0, color: Color.blue, radius: 12, destinationId: 4),
Dot(x:310.0,y:110.0, color: Color.red, radius: 14, destinationId: 5),
Dot(x:210.0,y:210.0, color: Color.blue, radius: 12, destinationId: 6)]
var lineWidth: CGFloat = 1
var body: some View {
NavigationView {
ZStack
Group {
ForEach(dots){ dot in
NavigationLink(destination: Game(id: dot.destinationId))
{
Circle()
.fill(dot.color)
.frame(width: dot.radius, height: dot.radius)
.position(x:dot.x, y: dot.y )
}
}
}
.frame(width: .infinity, height: .infinity, alignment: .center )
}
.navigationBarTitle("")
.navigationBarTitleDisplayMode(.inline)
}
}
struct Game: View {
var id: Int
var body: some View {
Text("\(id)")
}
}
I tried to send the actual view as i want to show different views and used AnyView, but the same occurred It was always causing the last defined view to be navigated to.
I'm really not sure what I a doing wrong, any pointers would be greatly appreciated
While it might seem that you're clicking on different dots, the reality is that you have a ZStack with overlapping views and you're always clicking on the top-most view. You can see when you tap that the dot with ID 6 is always the one actually getting pressed (notice its opacity changes when you click).
To fix this, move your frame and position modifiers outside of the NavigationLink so that they affect the link and the circle contained in it, instead of just the inner view.
Also, I modified your last frame since there were warnings about an invalid frame size (note the different between width/maxWidth and height/maxHeight when specifying .infinite).
struct ContentView: View {
var dots = [
Dot(x:10.0,y:10.0, color: Color.green, radius: 12, destinationId: 1),
Dot(x:110.0,y:10.0, color: Color.green, radius: 12, destinationId: 2),
Dot(x:110.0,y:110.0, color: Color.blue, radius: 12, destinationId: 3),
Dot(x:210.0,y:110.0, color: Color.blue, radius: 12, destinationId: 4),
Dot(x:310.0,y:110.0, color: Color.red, radius: 14, destinationId: 5),
Dot(x:210.0,y:210.0, color: Color.blue, radius: 12, destinationId: 6)]
var lineWidth: CGFloat = 1
var body: some View {
NavigationView {
ZStack {
Group {
ForEach(dots){ dot in
NavigationLink(destination: Game(id: dot.destinationId))
{
Circle()
.fill(dot.color)
}
.frame(width: dot.radius, height: dot.radius) //<-- Here
.position(x:dot.x, y: dot.y ) //<-- Here
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center )
}
.navigationBarTitle("")
.navigationBarTitleDisplayMode(.inline)
}
}
}
I would like to show a bunch of vertical bars, and on the click of a button have the first one swap places with a randomly selected one. I want this to be animated and the gif below shows the closest I've come, but as you can see, the entire set of vertical bars flashes briefly on each button click, which I do not want.
I've tried a few different approaches. The most elegant one I think would be to have the array of values which I iterate through to show the bars be a state variable, and when the array is sorted, the list just reloads and the swapping is animated. However, this didn't result in a swapping effect. It would just shrink and grow the respective bars to match the sizes of the array elements that were swapped :( I still feel like this might be the best approach, so I'm open to completely changing the implementation.
So the closest I've come is what you see in the gif, but it's hacky and I don't like the flashing of the entire view when it reloads. I'm using matchedGeometryEffect (hero animation) and even to get that working, I had to have a state variable that would reload the view. As you will see in the code below, this forces me to have an if/else even if I am just reloading the view under either condition. So I don't truly need the if(animate){} else {}, but using a hero animation forces me to.
Here's my code:
import SwiftUI
class SortElement: Identifiable{
var id: Int
var name: String
var color: Color
var height: CGFloat
init(id: Int, name: String, color: Color, height: CGFloat){
self.id = id
self.name = name
self.color = color
self.height = height
}
}
struct SortArrayAnimationDemo: View {
#Namespace private var animationSwap
#State private var animate: Bool = false
private static var sortableElements: [SortElement] = [
SortElement(id: 6, name:"6", color: Color(UIColor.lightGray), height: 60),
SortElement(id: 2, name:"2", color: Color(UIColor.lightGray), height: 20),
SortElement(id: 5, name:"5", color: Color(UIColor.lightGray), height: 50),
SortElement(id: 1, name:"1", color: Color(UIColor.lightGray), height: 10),
SortElement(id: 3, name:"3", color: Color(UIColor.lightGray), height: 30),
SortElement(id: 9, name:"9", color: Color(UIColor.lightGray), height: 90),
SortElement(id: 4, name:"4", color: Color(UIColor.lightGray), height: 40),
SortElement(id: 8, name:"8", color: Color(UIColor.lightGray), height: 80)
]
var body: some View {
VStack{
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.blue)
.frame(width: 200.0, height: 20.0)
if(animate){
fetchSwappingView()
}else {
fetchSwappingView()
}
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.blue)
.frame(width: 200.0, height: 20.0)
Button(action: { doAnimate() }) { Text("Swap!") }.padding(.top, 30)
}//vstack
}
#ViewBuilder
private func fetchSwappingView() -> some View {
HStack(alignment: .bottom){
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.red)
.frame(width: 30.0, height: 100.0)
ForEach(0..<SortArrayAnimationDemo.sortableElements.count) { i in
let sortableElement = SortArrayAnimationDemo.sortableElements[i]
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.gray)
.frame(width: 20.0, height: sortableElement.height)
.matchedGeometryEffect(id: sortableElement.id,
in: animationSwap,
properties: .position)
}
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.red)
.frame(width: 30.0, height: 100.0)
}//hstack
}
private func doAnimate() -> Void {
let randomIndex = Int.random(in: 1..<8)
let tmpLeftSortableElement: SortElement = SortArrayAnimationDemo.sortableElements[0]
SortArrayAnimationDemo.sortableElements[0] = SortArrayAnimationDemo.sortableElements[randomIndex]
SortArrayAnimationDemo.sortableElements[randomIndex] = tmpLeftSortableElement
withAnimation(.spring(dampingFraction: 0.7).speed(1.5).delay(0.05)){
animate.toggle()
}
}
}
Your code has a couple problems. First, you're working around the immutability of structs with this:
private static var sortableElements: [SortElement] = [
static isn't going to make SwiftUI refresh the view. I think you found this out too, so added another #State:
#State private var animate: Bool = false
...
if (animate) {
fetchSwappingView()
} else {
fetchSwappingView()
}
Well... this is where your flashing is coming from. The default transition applied to an if-else is a simple fade, which is what you're seeing.
But, if you think about it, this if-else doesn't make any sense. There's no need for an intermediary animate state — just let SwiftUI do the work!
You were initially on the right track.
The most elegant one I think would be to have the array of values which I iterate through to show the bars be a state variable, and when the array is sorted, the list just reloads and the swapping is animated. However, this didn't result in a swapping effect. It would just shrink and grow the respective bars to match the sizes of the array elements that were swapped :( I still feel like this might be the best approach, so I'm open to completely changing the implementation.
As you thought, the better way would be to just have a single #State array of SortElements. I'll get to the swapping effect problem in a bit — first, replace private static var with #State private var:
#State private var sortableElements: [SortElement] = [
This makes it possible to directly modify sortableElements. Now, in your doAnimate function, just set sortableElements instead of doing all sorts of weird stuff.
private func doAnimate() -> Void {
let randomIndex = Int.random(in: 1..<8)
let tmpLeftSortableElement: SortElement = sortableElements[0]
withAnimation(.spring(dampingFraction: 0.7).speed(1.5).delay(0.05)){
sortableElements[0] = sortableElements[randomIndex]
sortableElements[randomIndex] = tmpLeftSortableElement
}
}
Don't forget to also replace
if (animate) {
fetchSwappingView()
} else {
fetchSwappingView()
}
with:
fetchSwappingView()
At this point, your result should look like this:
This matches your problem description: the heights change fine, but there's no animation!
this didn't result in a swapping effect. It would just shrink and grow the respective bars to match the sizes of the array elements that were swapped :(
The problem is here:
ForEach(0..<sortableElements.count) { i in
Since you're simply looping over sortableElements.count, SwiftUI has no idea how the order changed. Instead, you should directly loop over sortableElements. The array's elements, SortElement, all conform to Identifiable — so SwiftUI can now determine how the order changed.
ForEach(sortableElements) { element in
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.gray)
.frame(width: 20.0, height: element.height)
.matchedGeometryEffect(
id: element.id,
in: animationSwap,
properties: .position
)
}
Here's the final code:
struct SortArrayAnimationDemo: View {
#Namespace private var animationSwap
#State private var sortableElements: [SortElement] = [
SortElement(id: 6, name:"6", color: Color(UIColor.lightGray), height: 60),
SortElement(id: 2, name:"2", color: Color(UIColor.lightGray), height: 20),
SortElement(id: 5, name:"5", color: Color(UIColor.lightGray), height: 50),
SortElement(id: 1, name:"1", color: Color(UIColor.lightGray), height: 10),
SortElement(id: 3, name:"3", color: Color(UIColor.lightGray), height: 30),
SortElement(id: 9, name:"9", color: Color(UIColor.lightGray), height: 90),
SortElement(id: 4, name:"4", color: Color(UIColor.lightGray), height: 40),
SortElement(id: 8, name:"8", color: Color(UIColor.lightGray), height: 80)
]
var body: some View {
VStack{
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.blue)
.frame(width: 200.0, height: 20.0)
fetchSwappingView() /// NO if else needed!
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.blue)
.frame(width: 200.0, height: 20.0)
Button(action: { doAnimate() }) { Text("Swap!") }.padding(.top, 30)
}//vstack
}
#ViewBuilder
private func fetchSwappingView() -> some View {
HStack(alignment: .bottom){
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.red)
.frame(width: 30.0, height: 100.0)
/// directly loop over `sortableElements` instead of its count
ForEach(sortableElements) { element in
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.gray)
.frame(width: 20.0, height: element.height)
.matchedGeometryEffect(
id: element.id,
in: animationSwap,
properties: .position
)
}
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.red)
.frame(width: 30.0, height: 100.0)
}//hstack
}
private func doAnimate() -> Void {
let randomIndex = Int.random(in: 1..<8)
let tmpLeftSortableElement: SortElement = sortableElements[0]
/// directly set `sortableElements`
withAnimation(.spring(dampingFraction: 0.7).speed(1.5).delay(0.05)){
sortableElements[0] = sortableElements[randomIndex]
sortableElements[randomIndex] = tmpLeftSortableElement
}
}
}
Result:
I wanted to build a Playground which stores my notes for my next examination using either Xcode playgrounds or just Swift Playgrounds. However, I attempted to build a list with an arrow at the side for navigation in Playgrounds, but with the lack of information after searching lots of tutorials, there's no way I could create a navigation list on
Swift Playgrounds. So I switched to Xcode playgrounds. But I could not seem to load the LiveView.
Note: no errors shown. Xcode playgrounds displays "Successful" but nothing is shown.
import SwiftUI
import PlaygroundSupport
//--------------------------------------------------------------//
// Weighted Asesstment III Notes
// Subject: Science
// Written in: English & SwiftUI
struct IntroView: View {
// arrays cus I wanna put this to a list
let expTech = ["Chromatography","Evaporation", "Distillation", "Dissolving","Magnetic Attraction", "Filtration"]
let expUse = ["Chromatography",
"Method: Paper",
"Used for: ",
"Evaporation",
"Method: Heating",
"Used for: Separating salt & water",
"Distillation",
"Method: Heating of liquid mixture with different boiling points",
"eg. Used for: Obtaining pure water from alcohol",
"Dissolving",
"Method",
"Used for",
"Magnetic Attraction",
//I need complete silence lol
//Iron nickel cobalt steel
"Method: Magnets (I,N,C,S)",
"Used for: Separating magnetic objects from non magnets",
"Filtration",
"Method: Filtering/Use of filter paper",
"Used for: Separating mixture of insoluble solid from liquid"]
var body: some View {
// ZStack
ZStack {
// background color
Color.white
// VStack
VStack {
LinearGradient(gradient: Gradient(colors: [.white, .gray, .black]), startPoint: .leading, endPoint: .trailing)
.mask(Text("WA 3 Sci Notes: M5C7")
.font(.system(size: 30, weight: .bold, design: .monospaced)))
.foregroundColor(Color.black)
.offset(x: 10, y: -325)
.padding()
Text("Types of experimental techniques")
.foregroundColor(Color.black)
.offset(x: -100, y: -710)
.font(.system(size: 25, weight: .semibold))
}
ZStack {
Rectangle()
.frame(width: 340, height: 240)
.cornerRadius(20)
.scaledToFill()
.shadow(color: Color.black, radius: 10)
.offset(x: -100, y: -120)
// list
List(expTech, id: \.self) { expTech in Text(expTech) }
.frame(width: 350, height: 250, alignment: .center)
.cornerRadius(25)
.offset(x: -100, y: -120)
}
Text("Uses of Experimental Techniques")
.foregroundColor(Color.blue)
.offset(x: 80, y: 40)
.font(.system(size: 25, weight: .semibold))
ZStack {
VStack {
Rectangle()
.frame(width: 600, height: 250)
.cornerRadius(20)
.scaledToFill()
.shadow(color: Color.black, radius: 5)
.offset(x: 5, y: 530)
}
}
}
}
}
PlaygroundPage.current.setLiveView(IntroView())
The issue could be that your View doesn't render properly. I put your code into an Xcode project (not a storyboard) and it looks like this:
You can even see it rendering weirdly in the playground:
Circle()
.trim(from: viewModel.lenghts[index].from, to: viewModel.lenghts[index].to)
.stroke(someGradient, style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270.0))
Tried set a gradient in .stroke initializer, tried to use .fill(Gradient..)
And my circle stroke looks pixelated. When use just a color (foregraundColor(Color..) all is ok. My gradient: AngularGradient(gradient: Gradient(colors: [CustomColors.red, CustomColors.pink]), center: .center, angle: .degrees(270))
What am I doing wrong? Tried linear, angular gradients
It seems like you've zoomed in. Try a bigger shape for better quality.
Compare these two visually identical rings and see the difference of zooming in and out:
High Quality
This one zoomed out to match the size that you are seeing.
Low Quality
This one zoomed in to match the size that you are seeing.
You can see the difference even in the text.
Full Demo code:
var body: some View {
ZStack {
Text("Low quality")
.font(.system(size: 8))
Circle()
.trim(from: 0.1, to: 0.9)
.stroke(g, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 60, height: 60)
.padding(8)
}
ZStack {
Text("Hight quality")
.font(.system(size: 50))
Circle()
.trim(from: 0.1, to: 0.9)
.stroke(g, style: StrokeStyle(lineWidth: 60, lineCap: .round, lineJoin: .round))
.frame(width: 400, height: 400)
.padding(50)
}
}
Note that you can use .scaleEffect modifier to scale it with code (if you really need to)
I am trying to create a shaded cross-hatched. But so far I can do it by adding a Image.
How can I create a custom view in which lines will be drawn and not filled with a Image?
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Image("lineFilledBG").resizable().clipShape(Circle())
Circle().stroke()
Circle().foregroundColor(.yellow).opacity(0.3)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is how it looks now. Want lines to draw on top of another view or shape, without adding opacity and image pattern filling.
Thank to Cenk Bilgen for stripe pattern. Tweaked a bit so that you can rotate the hatch for any shape.
import SwiftUI
import CoreImage.CIFilterBuiltins
extension CGImage {
static func generateStripePattern(
colors: (UIColor, UIColor) = (.clear, .black),
width: CGFloat = 6,
ratio: CGFloat = 1) -> CGImage? {
let context = CIContext()
let stripes = CIFilter.stripesGenerator()
stripes.color0 = CIColor(color: colors.0)
stripes.color1 = CIColor(color: colors.1)
stripes.width = Float(width)
stripes.center = CGPoint(x: 1-width*ratio, y: 0)
let size = CGSize(width: width, height: 1)
guard
let stripesImage = stripes.outputImage,
let image = context.createCGImage(stripesImage, from: CGRect(origin: .zero, size: size))
else { return nil }
return image
}
}
extension Shape {
func stripes(angle: Double = 45) -> AnyView {
guard
let stripePattern = CGImage.generateStripePattern()
else { return AnyView(self)}
return AnyView(Rectangle().fill(ImagePaint(
image: Image(decorative: stripePattern, scale: 1.0)))
.scaleEffect(2)
.rotationEffect(.degrees(angle))
.clipShape(self))
}
}
And usage
struct ContentView: View {
var body: some View {
VStack {
Rectangle()
.stripes(angle: 30)
Circle().stripes()
Capsule().stripes(angle: 90)
}
}
}
Output image link
I'm not sure this is what you want, but CoreImage can generate a striped pattern.
import CoreImage.CIFilterBuiltins
import SwiftUI
extension CGImage {
// width is for total, ratio is second stripe relative to full width
static func stripes(colors: (UIColor, UIColor), width: CGFloat, ratio: CGFloat) -> CGImage {
let filter = CIFilter.stripesGenerator()
filter.color0 = CIColor(color: colors.0)
filter.color1 = CIColor(color: colors.1)
filter.width = Float(width-width*ratio)
filter.center = CGPoint(x: width, y: 0)
let size = CGSize(width: width+width*ratio, height: 1)
let bounds = CGRect(origin: .zero, size: size)
// keep a reference to a CIContext if calling this often
return CIContext().createCGImage(filter.outputImage!.clamped(to: bounds), from: bounds)!
}
}
then use ImagePaint
Circle().fill(
ImagePaint(image: Image(decorative: CGImage.stripes(colors: (.blue, .yellow), width: 80, ratio: 0.25), scale: 1))
)
.rotationEffect(.degrees(-30))
Or CILineScreen filter may be more what you want.
I`m not sure what do you want but another possible solution is using different backgrounds in a ZStack linear gradient and shadows to make convex or concave effect., for this it helps that your background with a little color, not white white. some samples:
The code:
struct ContentView: View {
var body: some View {
ZStack {
Color.yellow.opacity(0.1)
VStack(spacing: 50) {
myCircle()
myCircle1()
}
}
}
}
struct myCircle: View {
var body: some View {
Circle().foregroundColor(Color.clear)
.frame(width: 100, height: 100)
.background(
ZStack {
LinearGradient(gradient: Gradient(colors: [Color( #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) ), Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1))]), startPoint: .topLeading, endPoint: .bottomTrailing)
Circle()
.stroke(Color.clear, lineWidth: 10)
.shadow(color: Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)), radius: 3, x: -5, y: -5)
Circle()
.stroke(Color.clear, lineWidth: 10)
.shadow(color: Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1)), radius: 3, x: 3, y: 3)
}
)
.clipShape(Circle())
.shadow(color: Color( #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) ), radius: 20, x: -20, y: -20)
.shadow(color: Color( #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) ), radius: 20, x: 20, y: 20)
}
}
struct myCircle1: View {
var body: some View {
Circle().foregroundColor(Color.clear)
.frame(width: 100, height: 100)
.background(
ZStack {
LinearGradient(gradient: Gradient(colors: [Color( #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) ), Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1))]), startPoint: .topLeading, endPoint: .bottomTrailing)
Circle()
.stroke(Color.clear, lineWidth: 10)
.shadow(color: Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)), radius: 3, x: -5, y: -5)
Circle()
.stroke(Color.clear, lineWidth: 10)
.shadow(color: Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1)), radius: 3, x: 3, y: 3)
}
)
.clipShape(Circle())
.shadow(color: Color( #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) ), radius: 20, x: -20, y: -20)
.shadow(color: Color( #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) ), radius: 20, x: 20, y: 20)
}
}
Thank you to Cenk Bilgen and flyer2001, I edited the code and come up with this code so it can be used with a view.
import SwiftUI
import CoreImage.CIFilterBuiltins
extension CGImage {
static func generateStripePattern(
colors: (UIColor, UIColor) = (.clear, .black),
width: CGFloat = 6,
ratio: CGFloat = 1) -> CGImage? {
let context = CIContext()
let stripes = CIFilter.stripesGenerator()
stripes.color0 = CIColor(color: colors.0)
stripes.color1 = CIColor(color: colors.1)
stripes.width = Float(width)
stripes.center = CGPoint(x: 1 - (width * ratio), y: 0)
let size = CGSize(width: width, height: 1)
guard
let stripesImage = stripes.outputImage,
let image = context.createCGImage(stripesImage, from: CGRect(origin: .zero, size: size))
else { return nil }
return image
}
}
struct ViewStripes : ViewModifier {
var stripeColor : Color
var width : CGFloat
var ratio : CGFloat
var angle : Double
var frameW : CGFloat
var frameH : CGFloat
func body(content: Content) -> some View {
if let stripePattern = CGImage.generateStripePattern(colors: (.clear,UIColor(stripeColor)), width: width, ratio: ratio) {
content
.overlay(Rectangle().fill(ImagePaint(image: Image(decorative: stripePattern, scale: 1.0)))
.frame(width: hypotenuse((frameW / 2), frameH / 2) * 2, height: hypotenuse(frameW / 2, frameH / 2) * 2)
//.scaleEffect(1)
.rotationEffect(.degrees(angle))
.mask(content))
}
}
}
extension View {
func drawStripes(stripeColor: Color, width: CGFloat, ratio: CGFloat, angle : Double, frameW : CGFloat, frameH : CGFloat) -> some View {
modifier(ViewStripes(stripeColor: stripeColor, width: width, ratio: ratio, angle: angle, frameW: frameW, frameH: frameH))
}
}
func hypotenuse(_ a: Double, _ b: Double) -> Double {
return (a * a + b * b).squareRoot()
}
And for usage:
Rectangle()
.foregroundColor(Color.Red)
.frame(width:20, height: 20)
//This is the code itself
.drawStripes(stripeColor: Color.white, width: 3, ratio: 0.9, angle: 45, frameW: 20, frameH: 20)
Hope it may be helpful for someone.