🦋

SwiftUI: CIFilterで絵文字をスタンプ風に表現してみた

2023/09/24に公開


サンプル

用意するもの

  1. スタンプのベースになる画像

    stamp.frame
  2. ラベル部分をくり抜くための画像

    stamp.text.frame

コード

CIFilterをたくさん使って欲しい画像を組み立てます。

import SwiftUI
import CoreImage.CIFilterBuiltins

struct PlaygroundView: View {
    var body: some View {
        convertToStamp(emoji: "🛠️", label: "開発")
            .renderingMode(.template) // 好きな色で塗りつぶすのに必要
            .resizable()
            .frame(width: 160, height: 160)
            .foregroundColor(Color.red)
    }

    func convertToStamp(emoji: String, label: String) -> Image {
        let dummy = Image(systemName: "questionmark.circle.fill")
        let stampSize = CGSize(width: 320, height: 320)

        // 絵文字を画像として出力(勝手に白黒になった)
        let textFilter = CIFilter.textImageGenerator()
        textFilter.text = emoji
        textFilter.fontSize = 150
        textFilter.scaleFactor = 1.0
        guard let output = textFilter.outputImage else { return dummy }

        // 絵文字をスタンプの中心になるようにずらず
        var textSize = output.extent.size
        var textAt = CGPoint(x: 0.5 * (stampSize.width - textSize.width),
                             y: 0.5 * (stampSize.height - textSize.height))
        let output2 = output.transformed(by: CGAffineTransform(translationX: textAt.x, y: textAt.y))

        // モノクロ絵文字の濃淡が淡いので濃くする
        let exposureFilter = CIFilter.exposureAdjust()
        exposureFilter.inputImage = output2
        exposureFilter.ev = -0.5
        guard let output3 = exposureFilter.outputImage else { return dummy }

        // スタンプのベースになる画像と組み合わせる
        let sourceOverFilter = CIFilter.sourceOverCompositing()
        sourceOverFilter.inputImage = output3
        sourceOverFilter.backgroundImage = CIImage(image: UIImage(named: "stamp.frame")!)
        guard let output4 = sourceOverFilter.outputImage else { return dummy }

        // ラベル部分をくり抜く
        let sourceOutFilter = CIFilter.sourceOutCompositing()
        sourceOutFilter.inputImage = output4
        sourceOutFilter.backgroundImage = CIImage(image: UIImage(named: "stamp.text.frame")!)
        guard let output5 = sourceOutFilter.outputImage else { return dummy }

        // ラベルの文字列を画像として出力
        textFilter.text = label
        textFilter.fontSize = 30
        textFilter.scaleFactor = 1.0
        guard let output6 = textFilter.outputImage else { return dummy }

        // ラベルの位置を調整
        textSize = output6.extent.size
        let ratio: CGFloat = min(1.0, 200 / textSize.width)
        textAt = CGPoint(x: 0.5 * (stampSize.width - ratio * textSize.width), y: 52)
        let output7 = output6
            .transformed(by: CGAffineTransform(scaleX: ratio, y: 1))
            .transformed(by: CGAffineTransform(translationX: textAt.x, y: textAt.y))

        // スタンプとラベルを組み合わせる
        sourceOverFilter.inputImage = output7
        sourceOverFilter.backgroundImage = output5
        guard let output8 = sourceOverFilter.outputImage else { return dummy }

        // 白黒反転する
        let invertFilter = CIFilter.colorInvert()
        invertFilter.inputImage = output8
        guard let output9 = invertFilter.outputImage else { return dummy }

        // 黒抜きをする(白抜きだったら白黒反転しなくてもいいのに)
        let alphaFilter = CIFilter.maskToAlpha()
        alphaFilter.inputImage = output9
        guard let output10 = alphaFilter.outputImage else { return dummy }

        // CIImageをImageに変換する
        guard let cgImage = CIContext().createCGImage(output10, from: output10.extent) else { return dummy }
        return Image(cgImage, scale: 1.0, label: Text(emoji))
    }
}


スタンプ風絵文字の出来上がり!

Discussion