「プログラムでシダを描画する」を CoffeeScript (HTML5 Canvas) で描画する

「CoffeeScript でシダを描画する」をしました。。。これって、半年くらい前にブームだったんですね。
既にブームは去っているようですが、気にせず投稿。

![shida.png](https://qiita-image-store.s3.amazonaws.com/0/60624/342dfe09-50dd-89ac-a92c-19ede5171239.png)

※ブラウザ： Chromium バージョン 39.0.2171.65 Ubuntu 14.04 (64-bit)

---
JavaScript は CoffeeScript で記述。jQuery を使用。Canvas に描画。

```coffeescript:myscript.coffee
$ ->
  # 乱数
  rand = (max = 0xff, min = 0) -> min + Math.random() * (max - min + 1) | 0  

  #
  # ---- Canvas ---------------------------------------------
  #
  plotter = (color = (() -> [0, 0xff, 0, 0xff]), canvas = $('canvas')[0]) ->
    ctx = canvas.getContext '2d'
    img = ctx.createImageData canvas.width, canvas.height

    put_pixel = (x, y, rgba) ->
      offset = (x * 4) + (y * canvas.width * 4)
      rgba.forEach (c, i) -> img.data[offset + i] = c

    put: (xy, rgba = color()) ->
      x = xy[0] * (+1) * (canvas.width  - 10) + (canvas.width / 2) |0
      y = xy[1] * (-1) * (canvas.height - 10) + (canvas.height   ) |0
      put_pixel x, y, rgba
      this

    plot: (g) ->
      g.apply null, [this.put].concat([].slice.call arguments, 1)
      this

    clear: (rgba = [0,0,0,0]) ->
      [0...canvas.width].forEach (x) ->
        [0...canvas.height].forEach (y) -> put_pixel x, y, rgba
      this

    show: () ->
      ctx.putImageData img, 0, 0
      this

  #
  # ---- 「シダ」のアルゴリズム ----------------------------
  #
  fern = (callback = (() ->), k = 20) ->
    w1x = (x, y) ->  0.836 * x + 0.044 * y
    w1y = (x, y) -> -0.044 * x + 0.836 * y + 0.169
    w2x = (x, y) -> -0.141 * x + 0.302 * y
    w2y = (x, y) ->  0.302 * x + 0.141 * y + 0.127
    w3x = (x, y) ->  0.141 * x - 0.302 * y
    w3y = (x, y) ->  0.302 * x + 0.141 * y + 0.169
    w4x = (x, y) ->  0.0
    w4y = (x, y) ->  0.175337 * y

    f = (k, x, y) ->
      if 0 < k
        f(k - 1, w1x(x, y), w1y(x, y))
        f(k - 1, w2x(x, y), w2y(x, y)) if Math.random() < 0.3
        f(k - 1, w3x(x, y), w3y(x, y)) if Math.random() < 0.3
        f(k - 1, w4x(x, y), w4y(x, y)) if Math.random() < 0.3
      else 
        callback [x, y]

    f(k, 0, 0)

  #
  # ---- 描画の実行 -----------------------------------------
  #
  console.log '描画開始'

  color = () -> [rand()/4, rand(), rand()/2, 0xff - rand()/8]

  plotter color     # ここを plotter() にすると緑単色(デフォルト)になる
    .clear()
    .plot fern
    .show()

  console.log '描画終了'
```

---

HTML は Haml で記述。

```haml:sample.haml
!!!
%meta(charset="UTF-8")
%title #{@title="CoffeeScript でシダを描画する"}

%h1 #{@title}
%canvas(width="500" height="500" style="background-color: #000000;")
  このブラウザは HTML5 Canvas に対応していません。

%script(src="//code.jquery.com/jquery-1.11.1.min.js")
%script(src="myscript.js")
```

---

Haml -> HTML、CoffeeScript -> JavaScript への変換。

```shell-session
$ haml -q sample.haml sample.html   # 'sample.html' が生成される
$ coffee -c myscript.coffee         # 'myscript.js' が生成される
```

変換後の HTML と JavaScript

```html:sample.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>CoffeeScript でシダを描画する</title>
<h1>CoffeeScript でシダを描画する</h1>
<canvas height="500" style="background-color: #000000;" width="500">
  このブラウザは HTML5 Canvas に対応していません。
</canvas>
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="myscript.js"></script>
```
```js:myscript.js
// Generated by CoffeeScript 1.8.0
$(function() {
  var color, fern, plotter, rand;
  rand = function(max, min) {
    if (max == null) {
      max = 0xff;
    }
    if (min == null) {
      min = 0;
    }
    return min + Math.random() * (max - min + 1) | 0;
  };
  plotter = function(color, canvas) {
    var ctx, img, put_pixel;
    if (color == null) {
      color = (function() {
        return [0, 0xff, 0, 0xff];
      });
    }
    if (canvas == null) {
      canvas = $('canvas')[0];
    }
    ctx = canvas.getContext('2d');
    img = ctx.createImageData(canvas.width, canvas.height);
    put_pixel = function(x, y, rgba) {
      var offset;
      offset = (x * 4) + (y * canvas.width * 4);
      return rgba.forEach(function(c, i) {
        return img.data[offset + i] = c;
      });
    };
    return {
      put: function(xy, rgba) {
        var x, y;
        if (rgba == null) {
          rgba = color();
        }
        x = xy[0] * (+1) * (canvas.width - 10) + (canvas.width / 2) | 0;
        y = xy[1] * (-1) * (canvas.height - 10) + canvas.height | 0;
        put_pixel(x, y, rgba);
        return this;
      },
      plot: function(g) {
        g.apply(null, [this.put].concat([].slice.call(arguments, 1)));
        return this;
      },
      clear: function(rgba) {
        var _i, _ref, _results;
        if (rgba == null) {
          rgba = [0, 0, 0, 0];
        }
        (function() {
          _results = [];
          for (var _i = 0, _ref = canvas.width; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
          return _results;
        }).apply(this).forEach(function(x) {
          var _i, _ref, _results;
          return (function() {
            _results = [];
            for (var _i = 0, _ref = canvas.height; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
            return _results;
          }).apply(this).forEach(function(y) {
            return put_pixel(x, y, rgba);
          });
        });
        return this;
      },
      show: function() {
        ctx.putImageData(img, 0, 0);
        return this;
      }
    };
  };
  fern = function(callback, k) {
    var f, w1x, w1y, w2x, w2y, w3x, w3y, w4x, w4y;
    if (callback == null) {
      callback = (function() {});
    }
    if (k == null) {
      k = 20;
    }
    w1x = function(x, y) {
      return 0.836 * x + 0.044 * y;
    };
    w1y = function(x, y) {
      return -0.044 * x + 0.836 * y + 0.169;
    };
    w2x = function(x, y) {
      return -0.141 * x + 0.302 * y;
    };
    w2y = function(x, y) {
      return 0.302 * x + 0.141 * y + 0.127;
    };
    w3x = function(x, y) {
      return 0.141 * x - 0.302 * y;
    };
    w3y = function(x, y) {
      return 0.302 * x + 0.141 * y + 0.169;
    };
    w4x = function(x, y) {
      return 0.0;
    };
    w4y = function(x, y) {
      return 0.175337 * y;
    };
    f = function(k, x, y) {
      if (0 < k) {
        f(k - 1, w1x(x, y), w1y(x, y));
        if (Math.random() < 0.3) {
          f(k - 1, w2x(x, y), w2y(x, y));
        }
        if (Math.random() < 0.3) {
          f(k - 1, w3x(x, y), w3y(x, y));
        }
        if (Math.random() < 0.3) {
          return f(k - 1, w4x(x, y), w4y(x, y));
        }
      } else {
        return callback([x, y]);
      }
    };
    return f(k, 0, 0);
  };
  console.log('描画開始');
  color = function() {
    return [rand() / 4, rand(), rand() / 2, 0xff - rand() / 8];
  };
  plotter(color).clear().plot(fern).show();
  return console.log('描画終了');
});
```

---
確認したブラウザ

- Chromium バージョン 39.0.2171.65 Ubuntu 14.04 (64-bit) [Ubuntu Linux 14.04]
- Firefox 34.0 Mozilla Firefox for Ubuntu canonical - 1.0 [Ubuntu Linux 14.04]
- Google Chrome バージョン 39.0.2171.95 (64-bit) [Mac OS X 10.10.1 Yosemite]
- Safari バージョン 8.0.2 (10600.2.5) [Mac OS X 10.10.1 Yosemite]
- Safari [iOS 8.1.2]