You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI粒子吸入效果问题:需将四角吸入改为全向吸入

问题描述

我实现了两个按钮,一个用于向外发射粒子,一个用于向内吸入粒子。向外发射的按钮功能正常,但向内吸入的按钮仅能从-45°、45°、-135°、135°这四个角落吸入粒子,希望将其修改为从四周全方向吸入粒子。

原代码

struct AllVideoView: View {
    @State private var isLiked: [Bool] = [false, false]
    var body: some View {
            
            VStack(spacing: 40){
                CustomButton(systemImage: "hand.thumbsup.fill", status: isLiked[0], activeTint: .green, inActiveTint: .gray) {
                    isLiked[0].toggle()
                    if isLiked[1] {
                        isLiked[1] = false
                    }
                }
                
                CustomButton(systemImage: "hand.thumbsdown.fill", status: isLiked[1], activeTint: .pink, inActiveTint: .gray) {
                    isLiked[1].toggle()
                    if isLiked[0] {
                        isLiked[0] = false
                    }
                }
            
        }
    }
    @ViewBuilder
    func CustomButton(systemImage: String, status: Bool, activeTint: Color, inActiveTint: Color, onTap: @escaping () -> ()) -> some View {
        Button(action: onTap) {
            Image(systemName: systemImage)
                .font(.title2)
                .particleEffect360(
                    systemImage: systemImage,
                    font: .body,
                    status: status,
                    activeTint: activeTint,
                    inActiveTint: inActiveTint,
                    direction: systemImage == "hand.thumbsup.fill" ? true : false
                )
                .foregroundColor(status ? activeTint : inActiveTint)
                .padding(18)
                .background {
                    Circle()
                        .fill(status ? activeTint.opacity(0.6) : Color.blue.opacity(0.6))
                }
            
        }
    }
}

struct Particle: Identifiable {//these particles are pushed from origin so start at 0,0
    var id: UUID = .init()
    var randomX: CGFloat = 0
    var randomY: CGFloat = 0
    var scale: CGFloat = 1
    var opacity: CGFloat = 1
    mutating func reset() {
        randomX = 0
        randomY = 0
        scale = 1
        opacity = 1
    }
}
struct Negative_Particle360: Identifiable {//these particles are pulled in so start them far from origin
    var id: UUID = .init()
    var randomX: CGFloat = Bool.random() ? CGFloat.random(in: -100...(-80)) : CGFloat.random(in: 80...100)
    var randomY: CGFloat = Bool.random() ? CGFloat.random(in: -100...(-80)) : CGFloat.random(in: 80...100)
    var scale: CGFloat = 1
    var opacity: CGFloat = 1

    mutating func reset() {
        if Bool.random() {
            randomX = CGFloat.random(in: -100...(-80))
        } else {
            randomX = CGFloat.random(in: 80...100)
        }
        if Bool.random() {
            randomY = CGFloat.random(in: -100...(-80))
        } else {
            randomY = CGFloat.random(in: 80...100)
        }
        scale = 1
        opacity = 1
    }
}

extension View {
    @ViewBuilder
    func particleEffect360(systemImage: String, font: Font, status: Bool, activeTint: Color, inActiveTint: Color, direction: Bool) -> some View {
        self
            .modifier(
                ParticleModifier360(systemImage: systemImage, font: font, status: status, activeTint: activeTint, inActiveTint: inActiveTint, direction: direction)
            )
        
    }
}

fileprivate struct ParticleModifier360: ViewModifier {
    var systemImage: String
    var font: Font
    var status: Bool
    var activeTint: Color
    var inActiveTint: Color
    var direction: Bool
    @State private var particles: [Particle] = []
    @State private var negative_particles: [Negative_Particle360] = []
    
    func body(content: Content) -> some View {
        content
            .overlay(alignment: .top) {
                ZStack {
                    if direction {
                        ForEach(particles) { particle in
                            Image(systemName: systemImage)
                                .font(font)
                                .foregroundColor(status ? activeTint : inActiveTint)
                                .scaleEffect(particle.scale)
                                .offset(x: particle.randomX, y: particle.randomY)
                                .opacity(particle.opacity)
                                .opacity(status ? 1 : 0)
                                .animation(.none, value: status)
                        }
                    } else {
                        ForEach(negative_particles) { particle in
                            Image(systemName: systemImage)
                                .font(font)
                                .foregroundColor(status ? activeTint : inActiveTint)
                                .scaleEffect(particle.scale)
                                .offset(x: particle.randomX, y: particle.randomY)
                                .opacity(particle.opacity)
                                .opacity(status ? 1 : 0)
                                .animation(.none, value: status)
                        }
                    }
                }
                .onAppear {
                    if direction {
                        if particles.isEmpty {
                            for _ in 1...15 {
                                let particle = Particle()
                                particles.append(particle)
                            }
                        }
                    } else {
                        if negative_particles.isEmpty {
                            for _ in 1...15 {
                                let particle = Negative_Particle360()
                                negative_particles.append(particle)
                            }
                        }
                    }
                }
                .onChange(of: status) { newValue in
                    if !newValue {
                        if direction {
                            for index in particles.indices {
                                particles[index].reset()
                            }
                        } else {
                            for index in negative_particles.indices {
                                negative_particles[index].reset()
                            }
                        }
                    } else {

                        if direction { //pushing particles out
                            for index in particles.indices {
                                let total: CGFloat = CGFloat(particles.count)
                                let progress: CGFloat = CGFloat(index) / total
                                
                                let angle: CGFloat = progress * 2 * .pi
                                
                                let radius: CGFloat = 75
                                let centerX: CGFloat = 0
                                let centerY: CGFloat = 0
                                
                                let randomX: CGFloat = radius * cos(angle) + centerX
                                let randomY: CGFloat = radius * sin(angle) + centerY
                                let randomScale: CGFloat = .random(in: 0.35...1)
                                
                                withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                                    let extraRandomX: CGFloat = .random(in: -10...10)
                                    let extraRandomY: CGFloat = .random(in: 0...40)
                                    
                                    particles[index].randomX = randomX + extraRandomX
                                    particles[index].randomY = randomY + extraRandomY
                                }
                                withAnimation(.easeInOut(duration: 0.3)) {
                                    particles[index].scale = randomScale
                                }
                                withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)
                                    .delay(0.25 + (Double(index) * 0.005))) {
                                        particles[index].scale = 0.001
                                }
                            }
                        } else {//pulling particles in logic
                            for index in negative_particles.indices {
                                let randomScale: CGFloat = .random(in: 0.8...1)
                                
                                withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
                                    let extraRandomX: CGFloat = .random(in: -5...5)
                                    let extraRandomY: CGFloat = .random(in: -5...5)
                                    
                                    negative_particles[index].randomX = extraRandomX
                                    negative_particles[index].randomY = extraRandomY
                                }
                                withAnimation(.easeInOut(duration: 0.3)) {
                                    negative_particles[index].scale = randomScale
                                }
                                withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)
                                    .delay(0.25 + (Double(index) * 0.005))) {
                                        negative_particles[index].scale = 0.001
                                }
                            }
                        }
                    }
                }
            }
        
    }
}
解决方案

问题出在Negative_Particle360的初始化和reset方法中,当前逻辑仅让粒子从四个角落区域生成。我们可以通过极坐标随机角度的方式,让粒子从四周任意位置生成,实现全方向吸入效果。

修改后的Negative_Particle360结构体

struct Negative_Particle360: Identifiable {
    var id: UUID = .init()
    var randomX: CGFloat
    var randomY: CGFloat
    var scale: CGFloat = 1
    var opacity: CGFloat = 1

    // 初始化时生成随机圆周位置
    init() {
        let radius: CGFloat = CGFloat.random(in: 80...100)
        let angle: CGFloat = .random(in: 0...2 * .pi)
        randomX = radius * cos(angle)
        randomY = radius * sin(angle)
    }

    mutating func reset() {
        let radius: CGFloat = CGFloat.random(in: 80...100)
        let angle: CGFloat = .random(in: 0...2 * .pi)
        randomX = radius * cos(angle)
        randomY = radius * sin(angle)
        scale = 1
        opacity = 1
    }
}

逻辑说明

  1. 用0...2 * .pi的随机角度覆盖360°全范围,确保粒子来自四周任意方向
  2. 通过三角函数cos(angle)和sin(angle)计算圆周上的X、Y坐标,半径保持80-100的范围,和原逻辑的粒子初始距离一致
  3. 吸入动画的逻辑无需修改,粒子会从随机圆周位置向中心移动,实现全方向吸入的效果

内容的提问来源于stack exchange,提问作者Ahmed Zaidan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 02:44:54