思考メモ

ときたま、考えたことを出力する。

Kotlinでクリエイティブコーディングする【備忘録】

ふと、Android で動作するデジタルアートを作りたいと思い立ち、衝動に身を任せて簡単なものを作った。そのとき学んだことを、忘れっぽい自分のためにメモしておく。

開発環境はAndroid Studio Electric Eel 2022.1.1

目次

準備
- アニメーション
- ブレンドモード
本編
- クリエイティブコーディング

アニメーション

一定時間ごとに、onDraw()を呼び出せば良い。invalidate()を使えば、onDraw()を強制的に呼び出すことができる。
Handlerを使う方法もあるが、今回はTimerAnimatorで実装してみる。

import ... //省略

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //タイトルバーを消す
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
        //レイアウトのセット
        val myView=MyView(this)
        setContentView(myView)
        //アニメーションの設定
        val anim = TimeAnimator()
        val fpm=(1000/60).toLong()
        anim.duration=fpm//単位はミリ秒
        anim.setTimeListener { _, _, _ ->
            myView.invalidate()//ここに一定時間ごとの処理を書く
        }
        anim.start()
    }
}

class MyView(context: Context): View(context){
    val paint: Paint = Paint()
    var radian=0.0

    override fun onDraw(canvas: Canvas){
        //値の更新
        radian+=PI/180

        val cx=(canvas.width/2).toFloat()
        val cy=(canvas.height/2).toFloat()
        val r=500F
        //キャンバスを塗りつぶす
        canvas.drawColor(Color.WHITE)
        //三角形
        paint.color = 0xFFFFC0CB.toInt()  //AARRGGBBで色を指定
        paint.style=Paint.Style.FILL_AND_STROKE
        val path = Path()  //多角形を描くにはPathを使う
        path.moveTo((r*cos(radian)+cx).toFloat(),
            (r*sin(radian)+cy).toFloat())
        path.lineTo((r*cos(radian+2*PI/3)+cx).toFloat(),
            (r*sin(radian+2*PI/3)+cy).toFloat())
        path.lineTo((r*cos(radian+4*PI/3)+cx).toFloat(),
            (r*sin(radian+4*PI/3)+cy).toFloat())
        path.close()  //FILLのときは省略可
        canvas.drawPath(path, paint)
        //矩形
        paint.color=Color.CYAN
        canvas.drawRect(cx-150F,cy-300F,cx,cy-150F,paint)
        paint.color=Color.YELLOW
        canvas.drawRect(cx-300F,cy-150F,cx-150F,cy,paint)
    }
}

実際はピンクの三角形が回転している。

見ていたアニメが分かる配色。これぞ、抽象芸術。多分、きっと、おそらく、ピカソも絶賛したに違いない。

TimerAnimatorの使い方は、見ての通り。durationTimeListenerを設定して、start()するだけ。
なお、上のsetTimeListenerの書き方は、

anim.setTimeListener(object:TimeAnimator.TimeListener{
    override fun onTimeUpdate(animation: TimeAnimator?, 
                              totalTime: Long, 
                              deltaTime: Long) {
         //なにか処理
     }
})

これをラムダ式で短くしたもの。

参考
- TimeAnimator  |  Android Developers

ブレンドモード

要するに、色を重ね合わせたときの効果。PorterDuffとも言う。
どのような種類の効果があるのかは、参考サイトに任せる。

override fun onDraw(canvas: Canvas){
        //省略
        //キャンバスのPorterDuffモードをクリアにする
        canvas.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR)
        //三角形
        //省略

        //ブレンドモードを設定
        paint.xfermode= PorterDuffXfermode(PorterDuff.Mode.DARKEN)
        paint.color=Color.CYAN
        canvas.drawRect(cx-150F,cy-300F,cx,cy-150F,paint)
        paint.color=Color.YELLOW
        canvas.drawRect(cx-300F,cy-150F,cx-150F,cy,paint)
        //設定を解除
        paint.xfermode=null
    }

これで、三角形と四角形の重なった部分にDARKENが適応される。

代わりに背景色が消えてしまったので、以下のように対処する。

//代わりのキャンバスを用意
val temp= createBitmap(width,height,Bitmap.Config.ARGB_8888)
val canvas2=Canvas()
canvas2.setBitmap(bmp)
canvas2.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR)

//canvasの代わりにcanvas2に描画
//省略

//canvas2をcanvasに貼る
canvas.drawColor(Color.WHITE)
canvas.drawBitmap(temp,0F,0F,null)

代わりのキャンバスを用意して、そこにブレンドモードで描画。その後に、本命のキャンバスに張り付けているだけ。

参考
- PorterDuffXfermode  |  Android Developers
- AndroidのCanvasを使いこなす! – PorterDuff – PSYENCE:MEDIA

クリエイティブコーディング

よさそうなサンプル探してきて改造してみる。
別の言語で書かれていても考え方は同じ。kotlinで書き直すだけなので、難しくない。

//MainActivity.kt
import ...

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //タイトルバーを消す
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE)

        val myView=MyView(this)
        setContentView(myView)
        //アニメーションの設定
        val anim =TimeAnimator()
        val fpm=(1000/60).toLong()
        anim.duration=fpm
        anim.setTimeListener { _, _, _ ->
            myView.invalidate()
            myView.update()
        }
        anim.start()
    }

}
//MyView.kt
import ...

//描画本体
class MyView(context: Context): View(context){
    
    private val paint: Paint = Paint()
    
    private val starsNum=40
    private var stars= arrayListOf<Star>()
    //初期化
    init {
        for(i in 0 until starsNum){
            stars.add(Star(paint))
        }
        //
    }

    //描画関数
    @RequiresApi(Build.VERSION_CODES.Q)
    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas){
        val bmp= createBitmap(width,height,Bitmap.Config.ARGB_8888)
        val cbmp=Canvas()

        cbmp.setBitmap(bmp)
        cbmp.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR)

        draw2(cbmp)
        //背景塗りつぶし
        canvas.drawColor(Color.WHITE)
        canvas.drawBitmap(bmp,0F,0F,null)
    }

    //描画する
    @RequiresApi(Build.VERSION_CODES.Q)
    private fun draw2(canvas: Canvas){
        paint.style=Paint.Style.FILL_AND_STROKE
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.LIGHTEN)

        for (i in 0 until starsNum)
            for (j in i + 1 until starsNum)
                for (k in j + 1 until starsNum) {
                    val s = stars[i]
                    val t = stars[j]
                    val u = stars[k]
                    if (s.kind == t.kind && t.kind == u.kind) {
                        if (s.distance(t) < 1000F && s.distance(u) < 1000F && t.distance(u) < 1000F) {
                            paint.color = s.col and 0x0FFFFFFF
                            val path = Path()
                            path.moveTo(s.x, s.y)
                            path.lineTo(t.x, t.y)
                            path.lineTo(u.x, u.y)
                            //path.close()//FILLのときは省略可
                            canvas.drawPath(path, paint)
                        }
                    }
                }

        paint.xfermode=null

        for(s in stars){
            s.drawCircle(canvas)
        }
    }

    //値更新
    fun update(){
        for (s in stars){
            s.update(this)
        }
    }

    class Star(val paint:Paint){

        var radius=Random.nextDouble(150.0,500.0)
        var cx=Random.nextDouble(-900.0,900.0)
        var cy=Random.nextDouble(-900.0,900.0)
        var direction= if(Random.nextBoolean())1.0 else -1.0
        var radian=Random.nextDouble(2*PI)

        var cr=150F
        var deg=Random.nextInt(360)
        var degd=if(Random.nextBoolean())1 else -1

        var x=0F
        var y=0F

        val kind= Random.nextInt(4)
        val cols=arrayOf(Color.MAGENTA,Color.YELLOW,Color.CYAN,Color.RED)
        var col=cols[kind]

        @RequiresApi(Build.VERSION_CODES.Q)
        fun drawCircle(canvas:Canvas){
            paint.color=col and 0x4FFFFFFF
            paint.style= Paint.Style.FILL
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.LIGHTEN)
            
            canvas.drawCircle(x,y,cr,paint)
        }

        fun update(layout:View){
            radian+=direction*2*PI*(1/radius)
            deg=if(deg+degd>0)(deg+degd)%360 else 360+degd
            x=(cos(radian)*radius+cx).toFloat()+layout.width/2
            y= (sin(radian)*radius+cy).toFloat()+layout.height/2
        }

        fun distance(s:Star): Float {
            val vx=x-s.x
            val vy=y-s.y
            return sqrt(vx*vx+vy*vy)
        }
    }

}

こんな感じの画面が作れた。

さらに、動画のフレーム切り出しと、GBの透過を行ったものがこちら。

この備忘録もそのうち書くが、gistのコードを貼っておく。
BtrArt1 · GitHub

感想

どちらかというと最近は、コーディングより、芸術的センスのほうが欲しいかも。

というわけで、これを買いました。
この記事には出てこないけどopenFrameworksいいよね。


クリエイティブコーディングは、コードさえ読めれば、どんな言語に対応できると思うので、気が向いたらこれで勉強します。