tiles-3
2012. 06. 18.

"Follow the white rabbit."

Click on the screen to randomize the pattern. Press enter key to change the message.

» launch tiles-3

rint =    (a, b) -> parseInt random(a, b)
Array::sample  = -> this[rint(0, this.length)]
String::sample = -> this[rint(0, this.length)]
Object::keys   = -> _(this).keys()
Array::each    = (f) -> _(this).each(f)

msg = 'follow the white rabbit'
message = ->
  (msg = window.prompt("message:", msg) or msg).split(/\s+/)

p5       = processing
nsizes   = 3
sr       = 1
srange   = [64, 224]
sparsity = 5
trials   = 5
randness = 0.0
fillers  = "~!@#$%^&*()-_+=?:{}[]<>,.|\\\""
reserved = message()
themes   = [
  {bg: 255, fg: 0 }
  {bg: 0,   fg: 255 }
  {bg: 0,   fg: color(0, 255, 0) }
]

font     = null
fonts    = null
words    = null
sizes    = null
grids    = null
theme    = null

pick_slots = (sz, len, r, c) ->
  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 = len or max(_(word_lens).min(), 5)
  max_word_len = _(word_lens).max()

  selected = { slots: [], l2r: null }
  size_keys = grids[sz].keys().map((k) -> parseInt k)
  for t in [0...trials]
    if c?
      x = c * parseInt(sz * sr)
      continue unless _(size_keys).include(x)
    else
      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

    if r?
      y = r * parseInt(sz * sr)
      continue unless _(size_keys).include(y)
    else
      y = grids[sz][x].keys().sample()

    slots = []
    while x? and y? and slots.length < (len or 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 > selected.slots.length
      selected.slots = slots
      selected.l2r   = l2r

    # Do not incentivise long words
    break

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

  [slots, l2r] = [selected.slots, selected.l2r]
  len = len or  _(words.keys().map((l) -> parseInt l)).
            filter((l) -> l <= slots.length and
                          l >= min_word_len).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

preload =
  fonts: [
    'WHITRABT.TTF'
    'rough_typewriter.otf'
    'daisywhl.otf'
    'type-ra.ttf'
  ].map((f) -> "/fonts/#{f}")

setup = ->
  size window.p5w or $(window).width(), window.p5h or $(window).height()

  theme = themes.sample()
  fonts = _(preload.fonts).reduce(
    ((o, e) -> o[e] = createFont e, 0; o),
    {})
  rectMode CENTER
  ellipseMode CENTER
  noStroke()
  noLoop()

  background 255
  fill 0
  textFont fonts[preload.fonts.sample()], 20
  text 'please wait ...', 100, 100

  $.ajax
    url: '/p5/data/words.json'
    dataType: 'json'
    success: (data, textStatus, jqXHR) ->
      words = data
      words[1] = ['.']
      mousePressed()

mousePressed = ->
  clear()
  randomize()
  paint()

keyPressed = ->
  if keyCode() == ENTER || keyCode() == RETURN
    reserved = message()
    clear()
    paint()

clear = ->
  size window.p5w or $(window).width(), window.p5h or $(window).height()
  background theme.bg

randomize = ->
  theme = themes.sample()
  background theme.bg
  fill theme.fg
  font = fonts[preload.fonts.sample()]

  nsizes = rint 1, 6
  sizes = {}
  max_sz = parseInt random(srange[0], srange[1])
  for i in [0...nsizes]
    sz = parseInt max_sz /= pow(8, 1 / nsizes)

    assigned = p5.width * p5.height / nsizes
    sizes[sz] = parseInt(assigned / pow(sz, 2) / sparsity)

paint_word = (sz, slots, word, hl) ->
  # Highlight (underline, sketchy circles)
  if hl
    f = slots[0]
    l = _(slots).last()
    w = l.x - f.x + sz * 0.8
    h = l.y - f.y + sz * 0.8
    cx = (slots[0].x + _(slots).last().x + sz) / 2
    cy = (slots[0].y + _(slots).last().y + sz) / 2 - sz * 0.1
    pushMatrix()
    translate cx, cy
    fill theme.fg
    rect 0, 0, w, h
    fill theme.bg
    popMatrix()
  else
    fill theme.fg

  _(slots).each (p, idx) ->
    c = word.substr(idx, 1)
    x = parseInt p.x + rint(-sz, sz) * randness
    y = parseInt p.y + rint(-sz, sz) * randness

    csz = parseInt(sz * random(1 - randness, 1 + randness))
    pushMatrix()
    translate x + csz * 0.5, y + csz * 0.5
    #rotate 0.015 * random(-PI, PI)
    textFont font, csz
    text c, -csz * 0.3, -csz * 0.4, csz, csz
    popMatrix()

paint = ->
  return unless words?
  rwords = _(reserved).clone()

  # initialize grid
  grids = {}
  _(sizes).each (_, sz) ->
    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
  
  max_ww = _(rwords.map((w) -> w.length)).max()# + rwords.length
  max_wh = max_ww #rwords.length + max_ww - 1
  xpos = undefined
  ypos = undefined
  rsizes = _(sizes.keys()).sortBy((e) -> -parseInt(e))
  _(rsizes).each (sz, idx) ->
    sz    = parseInt sz
    ypos  ?= max((p5.height - sz * max_wh) * random(), 0)
    count = sizes[sz]
    for i in [0...parseInt(count * (idx * 0.2 + 1))]
      if rwords.length > 0
        word = rwords.shift()
        xpos ?= max((p5.width - sz * max_ww) * random(), 0)
        row  = parseInt(ypos / sz)
        col  = parseInt(xpos / sz)
        ret  = pick_slots(sz, word.length, row, col)
        xpos += sz * parseInt(if ret.l2r then random(3) else 1 + random(2))
        ypos += sz * parseInt(if ret.l2r then 1 else random(2))
        hl  = true
      else
        word = undefined
        ret = pick_slots(sz)
        hl  = false

      break unless ret.ok

      slots = ret.slots
      if slots.length > 0
        word ?= words[slots.length].sample()
        continue unless word?
        
        while word.indexOf('.') != -1
          word = word.replace(/\./, fillers.sample())
        if random() < 0.5
          word = word.toUpperCase()
        else
          word = word.toLowerCase()

        paint_word sz, slots, word, hl

  sz = parseInt _(sizes.keys().map((s) -> parseInt s)).min()
  while (ret = pick_slots(sz, 1)).ok
    paint_word sz, ret.slots, fillers.sample(), false
» capture | close