TransWikia.com

Calculating points of a quadrilateral to emulate fluid in a container (in Swift)

Stack Overflow Asked on December 11, 2021

I am trying to create a FluidView shape in SwiftUI which acts like a fluid in a container, such that when the device is at a particular angle, so too is the shape / ‘fluid’. The shape also has a specific capacity, percentFilled, which indicates how much of the parent’s view should be filled.

Using these constraints, the invariant for the class is

lines.area == rect.area * percentFilled

where lines is the quadrilateral and rect is the bounding rectangle. This invariant implies that the ‘volume’ of the shape remains constant for each percentFilled irrespective of the tilt angle.

Here is what I have so far:

/// A View made using a specified angle and amount to fill
/// - Invariant: The area of the view is exactly equal to the area of the rectangle of the parent view times `percentFilled`
struct FluidView: Shape {
    var angle: CGFloat = 0.0
    var percentFilled: CGFloat = 0
    
    /// Creates a new FluidView
    /// - Parameters:
    ///   - angle: A value in the range `0...1`. A value of `0` indicates the view is horizontal, and an angle of `1` indicates the view is vertical (horizontal if viewed as landscape)
    ///   - percentFilled: the amount of the view bounds to fill represented as a value in the range `0...1`. A value of `x` indicates that `x * 100`% of the parent view is covered by this view
    init(angle: CGFloat = 0, percentFilled: CGFloat = 0) {
        precondition(0...1 ~= angle)
        precondition(0...1 ~= percentFilled)
        
        self.angle = angle
        self.percentFilled = percentFilled
    }
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: 0, y: rect.height * (1 - percentFilled))) // top left
        
        let lines = [
            (0,                                             rect.height                              ), // bottom left
            (rect.width * 1 / (1 + angle - percentFilled),  rect.height                              ), // bottom right
            (rect.width * 1 / (1 + angle - percentFilled),  rect.height * (1 + angle - percentFilled)), // top right
            (0,                                             rect.height * (1 - angle - percentFilled))  // top left
        ].map { x, y in
            // make sure no points exceed the bounds
            CGPoint(x: min(rect.width, x), y: min(rect.height, y))
        }
        
        // invariant
        assert(lines.area == rect.area * percentFilled)
        
        path.addLines(lines)
        return path
    }
}

I feel like what I have currently is somewhat close to the goal, however the invariant fails. I believe that my y-coordinates are correct, however I think my calculations for the x-coordinates have to change, but I’m not sure to what they should change.

Any help would be really appreciated, thanks!

One Answer

Try something like this,

struct FilledShape<S: Shape>: View {
  let shape: S
  @State var angle: Angle = .zero
  @State var percentFull: CGFloat
  
  var gradient: Gradient {
    Gradient(stops: [Gradient.Stop(color: .red, location: 0),
                     Gradient.Stop(color: .red, location: percentFull),
                     Gradient.Stop(color: .blue, location: percentFull),
                     Gradient.Stop(color: .blue, location: 1)])
  }
  
  var body: some View {
    shape.rotation(angle)
      .fill(LinearGradient(gradient: gradient, startPoint: .bottom, endPoint: .top))
  }
  
}
struct ContentView: View {
  @State var angle: Angle = .degrees(30)
  var body: some View {
    FilledShape(shape: Rectangle(), angle: angle, percentFull: 0.3).frame(width: 100, height: 100)
  }
}

Thing is, percent full is really the percent up along the y axis, not the percent of the area filled. You could use some kind of numeric method with GeometryReader to get the area and read the y-value at the appropriate filled area sum (or if you just use quadrilaterals it's easier). By brute force:

extension Shape {
  func area(in box: CGRect) -> Int {
    var area = 0
    for x in 0..<Int(box.width) {
      for y in 0..<Int(box.height) {
        let point = CGPoint(x: x, y: y)
        if self.contains(point) {
          area += 1
        }
      }
    }
    return area
  }
}

As a different approach, look into SpriteKit and SKPhysicsBody.

Answered by Cenk Bilgen on December 11, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP