Edited at

100 行未満で爆発アニメーションを生成する

1.png

セル・オートマトンの要領で爆発アニメーションを自動生成するコードを紹介します。

実際に動作するデモは こちら です。



  • JavaScript + Canvas + requestAnimationFrame

    フレーム毎に爆発 CG を生成するため、解像度を上げると非常に重くなります。


  • HTML 含めて 99 行

    HTML ファイルを作って下記コードをコピペしてブラウザで開けば動作します。

<body></body>

<script>

let canvas = null
let context = null
let cells = null

const config = {
number: { x: 128, y: 128 },
scale: 4,
addingAmp: 0.1
}

const main = () => {
setupCanvas()
setupCells()
tick()
}

const setupCanvas = () => {
canvas = document.createElement('canvas')
canvas.setAttribute('width', config.number.x * config.scale)
canvas.setAttribute('height', config.number.y * config.scale)
context = canvas.getContext('2d')
context.mozImageSmoothingEnabled = context.msImageSmoothingEnabled = context.webkitImageSmoothingEnabled = context.imageSmoothingEnabled = false
document.body.appendChild(canvas)
}

const setupCells = () => {
cells = []
for (let y = 0, yy = config.number.y; y < yy; y ++) {
cells[y] = []
for (let x = 0, xx = config.number.x; x < xx; x ++) {
cells[y][x] = {
currValue: Math.random(),
nextValue: 0,
adding: Math.random() * config.addingAmp
}
}
}
}

const tick = () => {
renderBackground()
renderCells()
updateCells()
requestAnimationFrame(tick)
}

const renderBackground = () => {
context.fillStyle = '#000000'
context.fillRect(0, 0, canvas.width, canvas.height)
}

const renderCells = () => {
let imageData = context.getImageData(0, 0, canvas.width, canvas.height)
let data = imageData.data
for (let y = 0, yy = config.number.y; y < yy; y ++) {
for (let x = 0, xx = config.number.x; x < xx; x ++) {
let index = (canvas.width * y + x) * 4
let color = Math.round(Math.sin((cells[y][x].currValue % 1.0) / 1.0 * Math.PI) * 255)
data[index + 0] = data[index + 1] = data[index + 2] = color
}
}
context.putImageData(imageData, 0, 0, 0, 0, canvas.width, canvas.height)
context.drawImage(canvas, 0, 0, config.number.x, config.number.y, 0, 0, canvas.width, canvas.height)
}

const updateCells = () => {
for (let y = 0, yy = config.number.y; y < yy; y ++) {
for (let x = 0, xx = config.number.x; x < xx; x ++) {
let cell = cells[y][x]
if (x > 0) compareCell(cell, cells[y][x - 1], 1.0)
if (x + 1 < xx) compareCell(cell, cells[y][x + 1], 1.0)
if (y > 0) compareCell(cell, cells[y - 1][x], 1.0)
if (y + 1 < yy) compareCell(cell, cells[y + 1][x], 1.0)
if ((x > 0) && (y > 0)) compareCell(cell, cells[y - 1][x - 1], 0.5)
if ((x > 0) && (y + 1 < yy)) compareCell(cell, cells[y + 1][x - 1], 0.5)
if ((x + 1 < xx) && (y > 0)) compareCell(cell, cells[y - 1][x + 1], 0.5)
if ((x + 1 < xx) && (y + 1 < yy)) compareCell(cell, cells[y + 1][x + 1], 0.5)
}
}
for (let y = 0, yy = config.number.y; y < yy; y ++) {
for (let x = 0, xx = config.number.x; x < xx; x ++) {
cells[y][x].currValue = cells[y][x].nextValue
}
}
}

const compareCell = (src, dst, multiplier) => {
if ((dst.currValue - src.currValue) >= dst.adding) {
src.nextValue += dst.adding * multiplier
}
}

main()

</script>

完全オリジナル技なので、これがスタンダードなアプローチなのか、もっと上手い定石的なロジックがあるのかわかりません。

しかも、自分で書いたくせに「どうしてこうなるのか」説明できません。これがセル・オートマトンの恐ろしいところですね(言い逃れ)。感覚的には理解しているのですが、言語化するのが難しいのです。

逆にセル・オートマトンの良いところは、ちょっとした改変でその表情がコロコロ変わって楽しいところです。

例えば 75 行目を以下のように変えると、上に向かって爆発するようになります。

if (y > 0) compareCell(cell, cells[y - 1][x], Math.random())

2.png

37 行目を以下のように変えると、同心円状に爆発が広がるかのようになります。

adding: Math.random() * config.addingAmp * ((Math.sin(x / xx * Math.PI) * Math.sin(y / yy * Math.PI)) * 0.5 + 0.5)

3.png

あー、楽しい。もう夜中だわ。

以上、セル・オートマトンによる Generative Art の一例の紹介でした。


なお、掲載コードは最低限のコードなので、ツッコミどころはあります。


  • 77 行目の drawImage で拡大しているのですが、ちょっとお行儀が悪いですね…。ちゃんと拡大用の Canvas を用意した方が良いと思います。


  • imageSmoothingEnabled = false で drawImage による拡大時のアンチエイリアスを切っています。「爆発」なので切らない方が滑らかに補間されて美しく見えるのですが、切らないと Canvas の端に線が見えてしまうため、仕方なく切っています。なにこれ?