如何用图像剪辑/填充形状而不固定它

问题描述 投票:0回答:1

下面是我的代码,我试图解决的问题是圆形形状中的图像看起来像平铺背景,当形状围绕其移动时固定在位置上,而我想要的结果是形状本身实际上是个人资料图片并且图像随着形状的移动而移动,与固定位置的背景不同。

如果我尝试使用clipShape(CustomCircleShape...

,也会发生同样的问题

我很困惑如何将圆圈及其运动转化为图像。

struct WaveAnimationView: View {
    
    @State private var screenCoveragePercent = 45.0
    @State private var waveOffset = Angle(degrees: 0)
    
    var body: some View {
        ZStack {
            
            let shape = CustomCircleShape(offSet: Angle(degrees: waveOffset.degrees), percent: screenCoveragePercent)
            
            shape.fill(ImagePaint(image: Image("ryan"), sourceRect: CGRect(x: 0, y: 0, width: 1, height: 1), scale: 0.1)) .ignoresSafeArea(.all)
                        
        }
        .onAppear {
            withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
                self.waveOffset = Angle(degrees: 360)
            }
        }
    }
    
}

struct CustomCircleShape: Shape {
    
    var offSet: Angle
    var percent: Double
    
    var animatableData: Double {
        get { offSet.degrees }
        set { offSet = Angle(degrees: newValue) }
    }
    
    func path(in rect: CGRect) -> Path {
        var p = Path()
        
        let lowestWave = 0.02
        let highestWave = 1.00
        
        let newPercent = lowestWave + (highestWave - lowestWave) * (percent / 100)
        let waveHeight = 0.0020 * rect.height
        let yOffSet = CGFloat(1 - newPercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight
        let startAngle = offSet
        let endAngle = offSet + Angle(degrees: 360 + 10)
            
        var circleCenter = CGPoint.zero
        
        for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) {
            let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width
            let y = yOffSet + waveHeight * CGFloat(sin(Angle(degrees: angle).radians))
            
            if x == rect.midX {
                circleCenter = CGPoint(x: x, y: y)
            }
        }
        if circleCenter == .zero {
            circleCenter = CGPoint(x: rect.midX, y: yOffSet + waveHeight * CGFloat(sin(Angle(degrees: offSet.degrees + 180).radians)))
        }
        
        let circleRadius: CGFloat = 50
        p.addEllipse(in: CGRect(x: circleCenter.x - circleRadius, y: circleCenter.y - circleRadius, width: 2 * circleRadius, height: 2 * circleRadius))
        
        return p
    }
}

编辑:下面是显示波浪动画的代码,其中圆圈(我希望是一个图像)正在模仿波浪的运动。

struct WaveAnimation: View {
    
    @State private var screenCoveragePercent = 25.0
    @State private var waveOffset = Angle(degrees: 0)
    
    var body: some View {
        ZStack {
            
            let opacityOfWater = 0.25
            
            Wave(offSet: Angle(degrees: waveOffset.degrees), percent: screenCoveragePercent)
                .fill(Color.blue).opacity(opacityOfWater)
                .ignoresSafeArea(.all)
          
            CustomCircle(offSet: Angle(degrees: waveOffset.degrees), percent: screenCoveragePercent)
                .fill(Color.blue).opacity(1.0)
                .ignoresSafeArea(.all)
            
            InvisibleSlider(percent: $screenCoveragePercent)
        }
        .onAppear {
            withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
                self.waveOffset = Angle(degrees: 360)
            }
        }
    }
    
}

struct CustomCircle: Shape {
    
    var offSet: Angle
    var percent: Double
    
    var animatableData: Double {
        get { offSet.degrees }
        set { offSet = Angle(degrees: newValue) }
    }
    
    func path(in rect: CGRect) -> Path {
        var p = Path()
        
        let lowestWave = 0.02
        let highestWave = 1.00
        
        let newPercent = lowestWave + (highestWave - lowestWave) * (percent / 100)
        let waveHeight = 0.0020 * rect.height
        let yOffSet = CGFloat(1 - newPercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight
        let startAngle = offSet
        let endAngle = offSet + Angle(degrees: 360 + 10)
            
        var circleCenter = CGPoint.zero
        
        for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) {
            let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width
            let y = yOffSet + waveHeight * CGFloat(sin(Angle(degrees: angle).radians))
            
            if x == rect.midX {
                circleCenter = CGPoint(x: x, y: y)
            }
        }
        if circleCenter == .zero {
            circleCenter = CGPoint(x: rect.midX, y: yOffSet + waveHeight * CGFloat(sin(Angle(degrees: offSet.degrees + 180).radians)))
        }
        
        let circleRadius: CGFloat = 50
        p.addEllipse(in: CGRect(x: circleCenter.x - circleRadius, y: circleCenter.y - circleRadius, width: 2 * circleRadius, height: 2 * circleRadius))
        
        return p
    }
}

struct Wave: Shape {
    
    var offSet: Angle
    var percent: Double
    
    var animatableData: Double {
        get { offSet.degrees }
        set { offSet = Angle(degrees: newValue) }
    }
    
    func path(in rect: CGRect) -> Path {
        var p = Path()
        
        let lowestWave = 0.02
        let highestWave = 1.00
        
        let newPercent = lowestWave + (highestWave - lowestWave) * (percent / 100)
        let waveHeight = 0.0020 * rect.height
        let yOffSet = CGFloat(1 - newPercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight
        let startAngle = offSet
        let endAngle = offSet + Angle(degrees: 360 + 10)
        
        // Draw wave
        p.move(to: CGPoint(x: 0, y: yOffSet + waveHeight * CGFloat(sin(offSet.radians))))
        for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) {
            let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width
            let y = yOffSet + waveHeight * CGFloat(sin(Angle(degrees: angle).radians))
            p.addLine(to: CGPoint(x: x, y: y))
        }
        
        p.addLine(to: CGPoint(x: rect.width, y: rect.height))
        p.addLine(to: CGPoint(x: 0, y: rect.height))
        p.closeSubpath()
   
        return p
    }
}
swiftui
1个回答
0
投票

您的代码正在对形状的 path 进行动画处理,相对于其边界(

rect
参数)。

您应该创建一个

Image(...).clipShape(Circle())
,并向其添加
.offset(y: ...)
。然后您可以为整个图像的 y 偏移设置动画。

我会设计

Wave
,使其填充整个给定的
rect
。即删除
percent
。可以使用
Spacer
中的
VStack
调整波浪覆盖的空间大小。

struct Wave: Shape {
    var offset: Angle
    let maxHeight: CGFloat
    
    var animatableData: Double {
        get { offset.degrees }
        set { offset = Angle(degrees: newValue) }
    }
    
    func path(in rect: CGRect) -> Path {
        Path { p in
            p.move(to: rect.origin)
            for deg in stride(from: 0, through: 360, by: 5) {
                let angle = Angle.degrees(Double(deg)) + offset
                let height = sin(angle.radians) * maxHeight
                p.addLine(to: .init(x: Double(deg) / 360 * rect.size.width + rect.minX, y: height + rect.minY))
            }
            p.addLine(to: .init(x: rect.maxX, y: rect.maxY))
            p.addLine(to: .init(x: rect.minX, y: rect.maxY))
            p.closeSubpath()
        }
    }
}

现在我们将图像放在它上面。图像的 y 偏移量是通过数学计算得出的。查看

yOffset
的实现如何对应于
Wave.path(in:)
的实现。

struct WaveAndCircle: View, Animatable {
    var waveOffset: Angle
    let maxWaveHeight: CGFloat = 20

    var animatableData: Double {
        get { waveOffset.degrees }
        set { waveOffset = Angle(degrees: newValue) }
    }
    
    var body: some View {
        ZStack(alignment: .top) {
            Wave(offset: waveOffset, maxHeight: maxWaveHeight)
                .fill(.blue.opacity(0.25))
            Image("my_image")
                .resizable()
                .frame(width: 50, height: 50)
                .clipShape(Circle())
                // change the alignment guide of the image, so that its center
                // is aligned to the top of the wave
                .alignmentGuide(.top, computeValue: { dimension in
                    dimension[VerticalAlignment.center]
                })
                .offset(y: yOffset)
        }
    }
    
    var yOffset: CGFloat {
        let offsetAngle = waveOffset + .degrees(180)
        let sinResult = sin(offsetAngle.radians)
        return sinResult * maxWaveHeight
    }   
}

最后,将所有内容放在一起。

struct WaveAnimation: View {
    
    @State private var screenCoveragePercent = 25.0
    @State private var waveOffset = Angle(degrees: 0)
    
    var body: some View {
        GeometryReader { geo in
            VStack {
                Spacer().frame(height: geo.size.height * (1 - screenCoveragePercent))
                WaveAndCircle(waveOffset: waveOffset)
            }
        }
        .onAppear {
            withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
                self.waveOffset = Angle(degrees: 360)
            }
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.