読者です 読者をやめる 読者になる 読者になる

ux00ff

ビールとプログラミングと

文字を画像化してピクセルデータを利用する

もともと文字の形状を扱いたかったのですが、まずはピクセルベースでの処理を試そうと思い、 OpenProcessing に公開されている「create ‘webby’ numbers and letters」を参考に Scala に手元で書き直してみました。

www.openprocessing.org

こういうやつです。

f:id:ux00ff:20170211114014p:plain

PFont

createFont メソッドを用いて PFont を作成するのが手っ取り早いのですが、このやり方だとシステムにあるフォントしか利用できません。

ポータブルなアプリケーションを目指す場合、.vlw ファイルを作成し、アプリケーションと一緒に配布して loadFontメソッドを用いて読み込むようにします。

Loads a .vlw formatted font into a PFont object. Create a .vlw font by selecting “Create Font…” from the Tools menu. This tool creates a texture for each alphanumeric character and then adds them as a .vlw file to the current sketch’s data folder. Because the letters are defined as textures (and not vector data) the size at which the fonts are created must be considered in relation to the size at which they are drawn.

と、あるようにvlwファイルはベクターデータだけでなく指定サイズまでのテクスチャも同時に生成してくれています。含める文字を「一般的な文字」にするか「全てのUnicode文字」にするかでだいぶサイズが違います。

f:id:ux00ff:20170211112808p:plain

手元で動かすぶんには createFont が手軽なのでこっちを使っています。

移植してみた

結果はこちら。少し長くなってしまった。コードはGistにも置いてあります。

コードがそんなに長くないので、Ball を内部クラスにすることで PApplet の描画メソッドにそのままアクセスさせています。

import processing.core.PConstants._
import processing.core.{PApplet, PFont, PGraphics, PVector}

import scala.collection.mutable.ArrayBuffer

// Original is : https://www.openprocessing.org/sketch/149337 by Jerome Herr
//
class App extends PApplet {
  lazy val letterGraphic: PGraphics = {
    createGraphics(width, height)
  }
  lazy val font: PFont = {
    createFont("ArialHebrew", FONT_SIZE)
  }
  val NUM = 2000
  val FONT_SIZE = 400
  var letter = "A"
  var theta: Float = _
  val ballCollection = new ArrayBuffer[Ball](NUM)

  override def settings(): Unit = {
    size(400, 400, JAVA2D)
  }

  override def setup(): Unit = {
    updateLetter()
    createBallAndLines()
  }

  private def updateLetter(): Unit ={
    ballCollection.clear()
    letterGraphic.beginDraw()
    letterGraphic.noStroke()
    letterGraphic.background(255)
    letterGraphic.fill(0)
    letterGraphic.textFont(font, FONT_SIZE)
    letterGraphic.textAlign(CENTER)
    letterGraphic.text(letter, 200f, 350f)
    letterGraphic.endDraw()
    letterGraphic.loadPixels()
  }

  private def createBallAndLines(): Unit = {
    for (i <- 0 to NUM) {
      val x = random(width).toInt
      val y = random(height).toInt
      val c = letterGraphic.pixels(x + y * width)
      if (brightness(c) < 255) {
        val org = new PVector(x, y)
        val radius = random(5, 10)
        val loc = new PVector(org.x + radius, org.y)
        val offSet = random(TWO_PI)
        val dir = if (random(1) > .5) {
          -1
        } else {
          1
        }
        val myBall = new Ball(org, loc, radius, dir, offSet)
        ballCollection.append(myBall)
      }
    }
  }

  override def keyPressed(): Unit = {
    if (key != CODED) {
      letter = PApplet.str(key)
      updateLetter()
      createBallAndLines()
    }
  }

  override def draw(): Unit = {
    background(20)
    textAlign(LEFT, TOP)
    text("%2.3f fps".format(frameRate), 0, 0)
    text("current : %s".format(letter), 0 , 15)

    ballCollection.foreach({ ball =>
      ball.draw()
    })

    theta += .0523f
  }

  class Ball(val org: PVector, val loc: PVector, val radius: Float, val dir: Int, val offset: Float) {
    val SIZE = 2
    val MAX_DIST = 20
    val OPACITY = 35
    val connection = new Array[Boolean](NUM)
    var countC = 1

    def draw(): Unit = {
      display()
      move()
      lineBetween()
    }

    private def move(): Unit = {
      loc.x = org.x + PApplet.sin(theta * dir + offset) * radius
      loc.y = org.y + PApplet.cos(theta * dir + offset) * radius
    }

    private def display(): Unit = {
      noStroke()
      fill(255, 100)
      ellipse(loc.x, loc.y, SIZE, SIZE)
    }

    private def lineBetween(): Unit = {
      var other: Ball = null
      for (i <- 0 to ballCollection.size - 1) {
        other = ballCollection(i)
        val distance = loc.dist(other.loc)
        if (distance > 0 && distance < MAX_DIST) {
          stroke(255, OPACITY)
          line(loc.x, loc.y, other.loc.x, other.loc.y)
        }
      }
    }
  }
}

いくらか使われていなかった変数とか処理とかがあったりして気になったので、少しロジックは改変しています。

キー入力された文字をPGraphicに書き出し、ランダムにとったピクセル位置の色を調べて明るさが255より小さければ(つまり少しでも色がついていれば)そのピクセルの位置を中心に Ball を出現させます。あとは Ball が自分と近い距離の別の Ball と線をつなぎ、自分自身で動き回る。画像ではわかりませんが、これもウネウネ動いています。

なお、

f:id:ux00ff:20170211114337p:plain

ひらがなでもイケます。