SwiftUIでビリビリ電気エフェクトを表現する

## はじめに

今年、はじめて **SwiftUI** を実務で採用する機会があったのですが、**UIKit** では面倒だったPhotoshop的な加工処理がシンプルに実装できて（※本来は非常に不向きな事だと思います）驚いたので、[PY - PARTY TECH STUDIO Advent Calendar 2024](https://qiita.com/advent-calendar/2024/p-y) への参加も兼ねてシェアします。

Pathで描画した **『ただの線』** を下記の参考画像のルックに近づけていきます。

<img src="https://www.shutterstock.com/image-vector/abstract-background-form-lightning-powerful-260nw-1212238315.jpg" width=50%>

参考：[shutterstock.com 1212238315](https://www.shutterstock.com/ja/image-vector/abstract-background-form-lightning-powerful-charge-1212238315)

## ベースとなるPathを作成

![Shutterstock_1212238315.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174081/dc821342-d6c6-8105-b0d1-e133a7e0e077.png)

```swift:ElectricView.swift
// 図形のパターン。インタラクティブに値を変更することもできる。
var amplitudePattern: [Float] = [0.53, 0.49, 0.51, 0.55, 0.69, 0.67, 0.69, 0.69, 0.67, 0.65, 0.63, 0.59, 0.58, 1.25, 0.53, 0.49, 0.61, 0.60, 0.55, 0.52, 0.49, 0.44, 0.41, 0.39]
let darkblueColor = Color(red: 0.082, green: 0.219, blue: 0.615)

ElectricShape(power: 5.0, time: time)
                .stroke(darkblueColor, lineWidth: 5)
                .frame(width: 300, height: 300)
```

```swift:ElectricShape.swift
import SwiftUI

struct ElectricShape: Shape {
    var power: Double = 1.0
    var time: CGFloat = 0.0
    var amplitudePattern: [Float] = []
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let size = CGSize(width: rect.width, height: rect.height)
        let staticRadius = sqrt(size.width * size.height / .pi)
        let segmentCount = 200
        let center = CGPoint(x: size.width / 2, y: size.height / 2)

        // 円形の点を計算するヘルパー関数
        func calculateCircularPoint(index: Int) -> CGPoint {
            let target = index % amplitudePattern.count
            let baseCircleRadius = staticRadius + CGFloat(amplitudePattern[target] * 30.0)
            let waveMotion = CGFloat(sin(Double(time) * 10.0 + Double(index)) +
                                     sin(Double(time) * 5.0 * Double(amplitudePattern[target]) * 5.0))
            let modulatedRadius = CGFloat(amplitudePattern[target] * 5.0) * (abs(sin(Double(time) * 5.0)) * Double(amplitudePattern[target]))
            let dynamicRadius = baseCircleRadius + modulatedRadius + waveMotion * (power * abs(sin(Double(time) * 5.0 + Double(index))))
            let radian = 2 * .pi / CGFloat(segmentCount) * CGFloat(index)
            return CGPoint(
                x: dynamicRadius * CGFloat(cos(Double(radian))) + center.x,
                y: dynamicRadius * CGFloat(sin(Double(radian))) + center.y
            )
        }

        // 線の描画を開始する
        let firstPoint = calculateCircularPoint(index: 0)
        path.move(to: firstPoint)

        // 各点への線の追加
        for i in 1..<segmentCount {
            path.addLine(to: calculateCircularPoint(index: i))
        }

        // 最初の点に戻る
        path.addLine(to: firstPoint)

        return path
    }
}

```

## Pathをぼかす
UIKitでは少し面倒（＋UIBlurEffectでは痒いところに手も届かない）な **ぼかし効果** ですが、SwiftUIではたったひとつのmodifierで実装できます。 

![ScreenShot 2024-12-09 4.26.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174081/a22a9319-7fd7-1807-fb93-ca814b3e6b29.png)
```swift
Text("This is some blurry text.").blur(radius: 4.0) 
```


![ScreenShot 2024-12-09 4.30.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174081/9407a873-7f80-a69a-53da-0100547d0eec.png)

```swift:ElectricView.swift
 ElectricShape(power: 2.0,  time: time, amplitudePattern: amplitudePattern)
    .stroke(darkblueColor, lineWidth: 10)
    .frame(width: 300, height: 300)
    .blur(radius: 4)
```

## ハイライトを追加
Viewのブレンド合成も同様に簡単です。blendMode(.screen)を使用して、ハイライト部分の線を追加します。

![ScreenShot 2024-12-09 4.35.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174081/375dc1fd-b6f6-5398-8b33-b3de1c3cf0aa.png)

```swift:ElectricView.swift
ZStack{
    ElectricShape(power: 5.0,  time: time, amplitudePattern: amplitudePattern)
        .stroke(darkblueColor, lineWidth: 10)
        .frame(width: 300, height: 300)
        .blur(radius: 4)
                
    ElectricShape(power: 5.0, time: time, amplitudePattern: amplitudePattern)
        .stroke(Color.white, lineWidth: 1)
        .frame(width: 300, height: 300)
        .shadow(color: Color.white, radius: 1, x: 0, y: 0) //わずかな光彩効果
        .blendMode(.screen)
}
```

もう一本重ねてビリビリしている感じに

![ScreenShot 2024-12-09 4.50.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174081/90bcc8a0-e89c-1774-3e3c-2afdeb56b62e.png)
```swift:ElectricView.swift
ElectricShape(power: 2.0, time: time, amplitudePattern: amplitudePattern)
    .stroke(Color.white, lineWidth: 1)
    .frame(width: 300, height: 300)
    .blendMode(.screen)
```


## 歪みを加えてもう少し有機的な図形に
iOS 17.0+で使用可能になった VisualEffect を使用することで、あらゆるUI要素に直接シェーダー効果を適用できます！（これ最高！）

今回は [distortionEffect](https://developer.apple.com/documentation/swiftui/visualeffect/distortioneffect(_:maxsampleoffset:isenabled:)) を使用しますが、ピクセルカラーを操作したい場合は、[colorEffect](https://developer.apple.com/documentation/swiftui/visualeffect/coloreffect(_:isenabled:))を使用する必要があります。

![ScreenShot 2024-12-09 5.03.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174081/877bcbff-c07e-6c44-bff7-df731a92ff72.png)

```swift:ElectricView.swift
.visualEffect { content, proxy in
    content.distortionEffect(ShaderLibrary.distortLightning(
         .float(1), .float2(proxy.size), .float(1), .float(2), .float(10)
    ), maxSampleOffset: CGSize(width: 10.0, height: 10.0))
}
```

```metal:DistortLightningShader.metal
#include <metal_stdlib>
using namespace metal;

[[ stitchable ]] float2 distortLightning(float2 position, float time, float2 size, float speed, float strength, float frequency) {
    float2 normalizedPosition = position / size / 0.1;
    float moveAmount = time * speed;

    position.x += sin((normalizedPosition.x + moveAmount) * frequency) * strength;
    position.y += cos((normalizedPosition.y + moveAmount) * frequency) * strength;

    return position;
}
```

昔はUIImageにMetalシェーダーを適用するのも一苦労だったのでこれだけのコードで実現できるなんて.. 

↓ 5年前に書いた記事

https://qiita.com/knou/items/332afc3ed2241002a0a4

## 回転モーションを加えて完成

![Dec-09-2024 05-12-54.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174081/b79d4086-d382-1703-bcf9-c7087cfd6342.gif)

```swift:ElectricView.swift
.rotationEffect(.degrees(isAnimating ? 360 : 0))
.animation( .linear(duration: 40.0).repeatForever(autoreverses: false),
    value: isAnimating
)
```

## さいごに

読んでいただき、ありがとうございます。

https://qiita.com/advent-calendar/2024/p-y

他のメンバーの記事もぜひご覧ください！