ふと、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
の使い方は、見ての通り。duration
とTimeListener
を設定して、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の透過を行ったものがこちら。
ぼっち・ざ・ろっくでクリエイティブコーディングした。
— Aliake (@aliake9t9) 2023年1月22日
初めてにしては、奇麗にできたと思うので、満足。 pic.twitter.com/wBB4mpUEb9
この備忘録もそのうち書くが、gistのコードを貼っておく。
BtrArt1 · GitHub
感想
どちらかというと最近は、コーディングより、芸術的センスのほうが欲しいかも。
というわけで、これを買いました。
この記事には出てこないけどopenFrameworksいいよね。
クリエイティブコーディングは、コードさえ読めれば、どんな言語に対応できると思うので、気が向いたらこれで勉強します。