みなさーーーーん!
進捗、どうですかーーーー!?
(´・ω・`)
Contributionを自動でツイートしてくれるサービス
GitHubのcontributionを自動でツイートしてくれるサービスとして、GitHubitterやContributterなどがあります。
私も以前少しの間だけ使ってみたことがあるのですが、**「contribution数: 0」**が連続でツイートされまくるのに心が疲れてしまい(自業自得)数ヶ月で連携を解除してしまいました。
毎日精力的にcontributionし続ける方にはよいツールなのかもしれませんが、ちょっとサボったことまでTLに流れてしまうのはちょっとうーんというところがあります。
でもどうにかして進捗アピール()したい!できればTLを汚さずに!
…ならアイコンをいじるだけなら問題ないんじゃないか?
ということで(?)作ってみました。
Fleetっぽい見た目にする
今回はTwitterの新機能「Fleet」を参考に、その日生えた草の色のリングをアイコンに重ねるデザインとしました。
(イメージ画像)
描画処理
リングの描画処理はSwiftで実装しました。(CoreGraphics
の勉強も兼ねて…)
※やたら長くなってしまうのを防ぐため、ソースコードは(基本的に)一部を抜粋して掲載しています(コピペでの動作を考慮していません)。実装の詳細はページ最下部にあるGitHubへのリンクを参照してください。
CGContext生成
まずはTwitterAPIを使用してユーザアイコンの画像URLを取得し、CIImage(url:)
-> CIContext.createCGImage(ciImage:, from:)
という流れでCGImage
を生成します。
(TwitterAPIの使用方法およびCIImageの生成については割愛します)
次に、以下のコードによりCGContext
を生成します。
図形の描画やリサイズなど、ピクセル単位の画像処理はこの子がやってくれるようです。
/// CGContext生成
/// - Parameters:
/// - cgImage: CGContextの元となる画像
/// - Return: 生成したCGContext
func generateCGContext(cgImage: CGImage) -> CGContext?{
guard let cgContext = CGContext(
data: nil,
width: cgImage.width,
height: cgImage.height,
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: cgImage.bytesPerRow,
space: cgImage.colorSpace!,
bitmapInfo: cgImage.bitmapInfo.rawValue
)else{return nil}
return cgContext
}
図形描画
次に、生成したCGContext
を使用して図形を描画していきます。
グラデーションのついた円の描画についてはこちらを参考にしました。
/// contributionリングの描画
/// - Parameters:
/// - colors: グラデーションに使用する色
/// - radius: 円の半径
/// - thickness: 線の太さ
/// - completion: 処理完了時に呼ばれる関数
func generateCenterCircleFilteredImage(colors: [CGColor], radius: CGFloat, thickness: CGFloat, completion: (_ image: CGImage?) -> Void){
self.apply { (context) in
// CGImageを描いて
let imageRect = CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)
context.draw(cgImage, in: imageRect)
// 円を描く
let origin = CGPoint(x: cgImage.width / 2, y: cgImage.height / 2)
let ellipseRect = CGRect(x: origin.x - radius, y: origin.y - radius, width: radius * 2, height: radius * 2)
let clipPath = CGPath(ellipseIn: ellipseRect, transform: nil)
context.saveGState()
context.setLineWidth(thickness)
context.addPath(clipPath)
context.replacePathWithStrokedPath()
context.clip()
// グラデ
let offsets = [ CGFloat(0.0), CGFloat(1.0) ]
let grad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: offsets)
let start = CGPoint(x: imageRect.minX, y: imageRect.minY)
let end = CGPoint(x: imageRect.maxX, y: imageRect.maxY)
context.drawLinearGradient(grad!, start: start, end: end, options: [])
context.restoreGState()
completion(self.generateImageFromContext())
}
}
// contextに任意の処理を加える
func apply(process: (_ context:CGContext) -> Void){
if let cgContext = self.cgContext{
process(cgContext)
}
}
// 現在のContextからCGImageを生成
func generateImageFromContext() -> CGImage?{
return self.cgContext?.makeImage()
}
グラデーションの配色は、
- 左下から右上にかけて
- ライトモードの時の草の色からダークモードの時の草の色に変化する
ようにしました。(正直初期状態だと若干くすんだ色になりますが、commitするほど綺麗な緑の円になるのでご愛嬌ということで…)
CGContextから画像を生成
処理が終了したら、CGContext
から編集後の画像を生成して…
import Foundation
import CoreImage
class ImageFormatter {
private let ciContext = CIContext()
@available(OSX 10.13, *)
func generatePNGImageData(image: CIImage, format:CIFormat = .RGBA8, colorSpace: CGColorSpace? = nil, options: [CIImageRepresentationOption: Any]? = nil) -> Data? {
let ciContext = CIContext()
return ciContext.pngRepresentation(of: image, format: format, colorSpace: image.colorSpace!, options: options ?? [:])
}
@available(OSX 10.12, *)
func generateJPEGImageData(image: CIImage, format:CIFormat = .RGBA8, colorSpace: CGColorSpace? = nil, options: [CIImageRepresentationOption: Any]? = nil) -> Data? {
let ciContext = CIContext()
return ciContext.jpegRepresentation(of: image, colorSpace: image.colorSpace!, options: options ?? [:])
}
}
アイコンに割り当てれば、ひとまず処理部分は完成です。(OSX10.12以前ってどうやってCGContextから画像生成してたんでしょう?)
GitHub Actionsで自動更新
最後に、アイコンの定期更新処理をGitHub Actionsに設定します。
ワークフローを開始するイベントの一つにschedule
があり、これを設定することで一定時間おきに自動でジョブを実行してくれます。
ワークフローをトリガーするイベント - GitHub Docs
スケジュールしたイベント
schedule イベントを使用すると、スケジュールされた時間にワークフローをトリガーできます。
POSIX クーロン構文を使用して、特定の UTC 時間にワークフローを実行できるようスケジュール設定できます。 スケジュールしたワークフローは、デフォルトまたはベースブランチの直近のコミットで実行されます。
# 定期更新ワークフロー
name: Periodic update
# 5時間ごと、またはmainブランチにpushしたときに起動
on:
schedule:
- cron: '0 */5 * * *'
push:
branches: [ main ]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
# ビルド
- name: Build
run: swift build -c release
# 実行
- name: Run
env:
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
TWITTER_OAUTH_TOKEN: ${{ secrets.TWITTER_OAUTH_TOKEN }}
TWITTER_OAUTH_SECRET: ${{ secrets.TWITTER_OAUTH_SECRET }}
run: swift run GitGrassIndicator ${TWITTER_CONSUMER_KEY} ${TWITTER_CONSUMER_SECRET} ${TWITTER_OAUTH_TOKEN} ${TWITTER_OAUTH_SECRET}
README.md
にバッジをつけておくと、Actionsを調べに行かなくても状態が分かるので便利かもしれません。
![badge](![Periodic update](https://github.com/Enchan1207/GitGrassIndicator/workflows/Periodic%20update/badge.svg))
実際のアイコン
実際に反映したものがこちらになります(料理番組のようなノリ)。
当初目指したFleetっぽさが…出て…いないこともないですね!()
最後に
ここまで読んでいただきありがとうございました。
Swift × CLIツールというのはなかなか意外な組み合わせだなーと思っていましたが、
正直もう全部Swiftでいいんじゃないかとすら思えるほど快適でした。Swiftは いいぞ。
XCodeの強力なコード補完を利用でき、かつ普段iOSアプリ開発で使っているライブラリがそのまま使えるというのは大きなメリットなのではないでしょうか。(当然ながらライブラリがUIKit
をimportしているなどの場合は詰んでしまうのですが…)
ソースコードはGitHubにて公開しておりますので、
「ソースの一部じゃなくて全体が見たい!」とか「Sources/main.swift
コールバック地獄のクソコードやんけ!」等ございましたらコメントまたはPRいただければ幸いです。
2021/04/09追記: コールバック地獄を改善しました。よろしければこちらの記事もご覧ください。
ではまた!