tiles-2
2012. 06. 03.

Randomized display of the lines from the poem 별 헤는 밤 (A night of counting stars) by 윤동주 (Yun, Dong-ju).

Works on Firefox. (Chrome may render texts incorrectly.)

» launch tiles-2

rint =   (a, b) -> parseInt random(a, b)
Array::sample = -> this[rint(0, this.length)]
Array::pick   = -> this.splice(rint(0, this.length), 1)[0]
Object::keys  = -> _(this).keys()
Object::each  = (f) ->
  for k, v of this
    f(k, v) if this.hasOwnProperty(k)
  null
Array::each   = (f) -> _(this).each(f)

p5       = processing
shapes   = 5
sr       = 1
srange   = [15, 35]
sparsity = 20
font     = null
words    = null
sizes    = null
grids    = null
data     = """
  계절이 지나가는 하늘에는 가을로 가득 차 있습니다.
  나는 아무 걱정도 없이 가을 속의 별들을 다 헤일 듯합니다.
  가슴속에 하나 둘 새겨지는 별을 이제 다 못 헤는 것은
  쉬이 아침이 오는 까닭이요,
  내일 밤이 남은 까닭이요,
  아직 나의 청춘이 다하지 않은 까닭입니다.
  별 하나에 추억과
  별 하나의 사랑과
  별 하나에 쓸쓸함과
  별 하나에 동경과
  별 하나에 시와
  별 하나에 어머니, 어머니,
  어머님, 나는 별 하나에 
  아름다운 말 한 마디씩 불러 봅니다.
  소학교 때 책상을 같이 했던 아이들의 이름과, 
  패, 경, 옥 이런 이국 소녀들의 이름과
  벌써 애기 어머니 된 계집애들의 이름과,
  가난한 이웃 사람들의 이름과, 
  비둘기, 강아지, 토끼, 노새, 노루,
  프랑시스 잠, 라이너 마리아 릴케
  이런 시인의 이름을 불러 봅니다.
  이네들은 너무나 멀리 있습니다.
  별이 아스라히 멀듯이,
  어머님,
  그리고 당신은 멀리 북간도에 계십니다.
  나는 무엇인지 그리워
  이 많은 별빛이 내린 언덕 위에
  내 이름자를 써 보고,
  흙으로 덮어 버리었습니다.
  딴은 밤을 새워 우는 벌레는
  부끄러운 이름을 슬퍼하는 까닭입니다.
  그러나 겨울이 지나고 나의 별에도 봄이 오면
  무덤 위에 파란 잔디가 피어나듯이
  내 이름자 묻힌 언덕 위에도 자랑처럼 풀이 무성할 거외다.
""".replace(/\n+/, '\n').replace(/^ +| +$|[\.',]/g, '')

pick_slots = (sz) ->
  sz = parseInt sz

  # Clean up words object
  _(words.keys()).filter((k) -> words[k].length == 0).each (k) ->
    delete words[k]
  word_lens = words.keys().map (k) -> parseInt(k)
  min_word_len = _(word_lens).min()
  max_word_len = _(word_lens).max()

  # Try to choose the longest line
  longest = { slots: [], l2r: null }
  for t in [0...5]
    x = grids[sz].keys().sample()

    # Whoa. No space at all! Stop searching.
    return {ok: false, slots: []} unless x?

    # Traverse rightward or downward
    l2r   = random(1.0) < 0.5
    y     = grids[sz][x].keys().sample()
    slots = []
    while x? and y? and slots.length < max_word_len
      x  = parseInt x
      y  = parseInt y
      slots.push { x: x, y: y }

      # Left-to-right
      if l2r
        x = if grids[sz][x + sz]? then x + sz else null
        y = if x? and grids[sz][x][y]? then y else null
      # Top-to-bottom
      else
        y = if grids[sz][x]? and grids[sz][x][y + sz]?
              y + sz
            else
              null

    if slots.length > longest.slots.length
      longest.slots = slots
      longest.l2r   = l2r

  # Tried, but to no avail. Maybe, bad luck.
  return {ok: true, slots: []} if longest.slots.length < min_word_len

  [slots, l2r] = [longest.slots, longest.l2r]
  len   = _(words.keys().map((l) -> parseInt l)).
            filter((l) -> l <= slots.length).sample()
  slots = slots[0...len]
  slots.each (p) -> occupy sz, p.x, p.y
  {ok: true, l2r: l2r, slots: slots}

occupy = (sz, x, y) ->
  sizes.each (s, _) ->
    s    = parseInt s
    step = parseInt s * sr

    sx = floor((x - s) / step) * step
    sy = floor((y - s) / step) * step

    for xx in [sx...(x + sz)] by step
      for yy in [sy...(y + sz)] by step
        if grids[s][xx]? and xx > sx and yy > sy
          delete grids[s][xx][yy]
          if grids[s][xx].keys().length == 0
            delete grids[s][xx]
  null

# Preloading support in coffee-processing 0.0.6
preload = fonts: ["/fonts/UnPen.woff"]

setup = ->
  font = createFont("/fonts/UnPen.woff", 32)
  colorMode HSB, 1.0
  rectMode  CENTER
  noFill()
  noStroke()
  noLoop()
  mousePressed()

mousePressed = ->
  size       $(window).width(), $(window).height()
  background 1

  colors = for i in [0...5]
      color(0, 0, 0.6 + 0.35 * i / 5, 0.8)

  words = data.split('\n').map((l) -> l.replace(/^ +| +$/g, '')).
          reduce(((h, e) ->
                   a = h[e.length] ?= []
                   a.push e
                   h), {})

  grids = {}
  sizes = {}
  for i in [0...shapes]
    ratio = max(1, p5.width * p5.height / pow(10, 6))
    sz = rint(srange[0] * ratio, srange[1] * ratio)
    sizes[sz] = parseInt(p5.width * p5.height /
                         (shapes + sparsity) / pow(sz, 2))

  sizes.each (sz, count) ->
    sz = parseInt sz
    step = parseInt(sz * sr)
    grids[sz] = {}
    for x in [0..p5.width - sz] by step
      grids[sz][x] = {}
      for y in [0..p5.height - sz] by step
        grids[sz][x][y] = 1

  rsizes = _(sizes.keys()).sortBy((e) -> -parseInt(e))
  _(rsizes).each (sz, idx) ->
    noStroke()

    sz    = parseInt sz
    count = sizes[sz]
    for i in [0...parseInt(count * (idx * 0.2 + 1))]
      ret = pick_slots(sz)
      break unless ret.ok

      slots = ret.slots
      l2r   = ret.l2r
      if slots.length > 0
        word = words[slots.length].pick()
        continue unless word?

        # Box
        f = slots[0]
        l = _(slots).last()
        w = l.x - f.x + sz
        h = l.y - f.y + sz
        cx = (slots[0].x + _(slots).last().x + sz) / 2
        cy = (slots[0].y + _(slots).last().y + sz) / 2
        pushMatrix()
        translate cx, cy
        rotate(0.015 * random(-PI, PI))
        fill colors.sample()
        rect 0, 0, w, h
        popMatrix()

        # Letters
        fill 0
        num_spaces = word.split(' ').length - 1
        offset = { x: 0, y: 0 }
        if slots.length > 1 and word.indexOf(' ') != -1
          if l2r
            offset.x += sz * 0.2 * num_spaces
          else
            offset.y += sz * 0.2 * num_spaces

        _(slots).each (p, idx) ->
          c = word.substr(idx, 1)
          x = parseInt p.x + rint(-sz, sz) * 0.1
          y = parseInt p.y + rint(-sz, sz) * 0.1
          if c == ' '
            if l2r
              offset.x -= sz * 0.4
            else
              offset.y -= sz * 0.4

          pushMatrix()
          hsz = parseInt(sz * 0.5)
          translate x + hsz + offset.x, y + hsz + offset.y
          rotate 0.015 * random(-PI, PI)
          csz = parseInt(sz * random(0.8, 1.2))
          textFont font, csz
          text c, -csz * 0.5, -csz * 0.5, csz, csz
          popMatrix()

  # Grains
  for i in [0...parseInt(p5.width * p5.height / 500)]
    stroke random(1)
    strokeWeight random(1, 3)
    point random(p5.width), random(p5.height)
» capture | close