ふと、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()
paint.style=Paint.Style.FILL_AND_STROKE
val 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()
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){
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.drawColor(Color.WHITE)
canvas.drawBitmap(temp,0F,0F,null)
代わりのキャンバスを用意して、そこにブレンドモードで描画。その後に、本命のキャンバスに張り付けているだけ。
参考
- PorterDuffXfermode | Android Developers
- AndroidのCanvasを使いこなす! – PorterDuff – PSYENCE:MEDIA
クリエイティブコーディング
よさそうなサンプル探してきて改造してみる。
別の言語で書かれていても考え方は同じ。kotlinで書き直すだけなので、難しくない。
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()
}
}
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)
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いいよね。
クリエイティブコーディングは、コードさえ読めれば、どんな言語に対応できると思うので、気が向いたらこれで勉強します。