├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── docs └── images │ ├── conway.gif │ ├── mn-gon.gif │ ├── sinus.gif │ └── stencil.gif ├── drawille.gemspec ├── examples ├── conway.rb ├── mn-gon.rb ├── stencil-1.png ├── stencil-2.jpg └── stencil.rb ├── lib ├── drawille.rb └── drawille │ ├── brush.rb │ ├── canvas.rb │ ├── flipbook.rb │ ├── frameable.rb │ └── version.rb └── spec ├── drawille_spec.rb ├── sinus.dat └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in drawille.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Marcin Skirzynski 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Drawille for Ruby 2 | ======== 3 | [![Gem Version](https://badge.fury.io/rb/drawille.svg)](http://badge.fury.io/rb/drawille) 4 | [![Build Status](https://codeship.com/projects/f07b1f00-3a92-0133-f398-52eae9218b1b/status?branch=master)](https://codeship.com/projects/101918) 5 | 6 | Draw in your terminal with Unicode [Braille][] characters. Implementation based on [drawille][] by @asciimoo. 7 | 8 | [Braille]: http://en.wikipedia.org/wiki/Braille 9 | [Drawille]: https://github.com/asciimoo/drawille 10 | 11 | 12 | ## Installation 13 | 14 | Add this line to your application's Gemfile: 15 | 16 | gem 'drawille' 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install drawille 25 | 26 | ## Usage 27 | 28 | Drawille can be used like the Python implementation due to the similar API. Here is one of its examples: 29 | 30 | ```ruby 31 | require 'drawille' 32 | 33 | canvas = Drawille::Canvas.new 34 | 35 | (0..1800).step(10).each do |x| 36 | canvas.set(x/10, 10 + Math.sin(x * Math::PI / 180) * 10) 37 | end 38 | 39 | puts canvas.frame 40 | ``` 41 | 42 | ![Sinus](docs/images/sinus.gif) 43 | 44 | But in the end you can use it in every possible situation where you have only two colors. This means it is perfect for some stencil graffitis. 45 | 46 | ```ruby 47 | require 'drawille' 48 | require 'chunky_png' 49 | 50 | include ChunkyPNG 51 | 52 | canvas = Drawille::Canvas.new 53 | 54 | def draw canvas, img, xoffset=0 55 | (0..img.dimension.width-1).each do |x| 56 | (0..img.dimension.height-1).each do |y| 57 | r = Color.r(img[x,y]) 58 | g = Color.g(img[x,y]) 59 | b = Color.b(img[x,y]) 60 | canvas.set(x+xoffset, y) if (r + b + g) > 100 61 | end 62 | end 63 | end 64 | 65 | draw canvas, Image.from_file('examples/stencil-1.png') 66 | draw canvas, Image.from_file('examples/stencil-2.jpg'), 141 67 | 68 | puts canvas.frame 69 | ``` 70 | 71 | ![Stencil](docs/images/stencil.gif) 72 | 73 | With a "flipbook" you can also create animations on your terminal. 74 | 75 | ![Conway](docs/images/conway.gif) 76 | 77 | This implementation also includes a [Turtle graphics](http://en.wikipedia.org/wiki/Turtle_graphics) API for all your beloved fractals: 78 | 79 | ```ruby 80 | require 'drawille' 81 | 82 | canvas = Drawille::Canvas.new 83 | 84 | frame = canvas.paint do 85 | move 300, 300 86 | down 87 | 88 | 36.times do 89 | right 10 90 | 36.times do 91 | right 10 92 | forward 8 93 | end 94 | end 95 | 96 | end.frame 97 | 98 | puts frame 99 | ``` 100 | 101 | ![MN-gon](docs/images/mn-gon.gif) 102 | 103 | ### Turtle-API 104 | 105 | ```ruby 106 | canvas = Drawille::Canvas.new 107 | canvas.paint do 108 | # Move your brush with the following commands 109 | end 110 | ``` 111 | 112 | ``forward 10`` or ``fw 10`` 113 | 114 | Moves your brush in the current direction (default is 0 which points to the right). Please note that your brush has to be set down to actually draw. 115 | 116 | ``back 10`` or ``bk 10`` 117 | 118 | Moves your brush in the opposite direction. Please note that your brush has to be set down to actually draw. 119 | 120 | ``right 90`` or ``rt 90`` 121 | 122 | Turn the direction of the brush by 90 degrees to the right. 123 | 124 | ``left 90`` or ``lt 90`` 125 | 126 | Turn the direction of the brush by 90 degrees to the left. 127 | 128 | ``up`` or ``pu`` 129 | 130 | Sets the brush up which means all following operations will have no effect on the canvas itself. 131 | 132 | ``down`` or ``pd`` 133 | 134 | Sets the drush down which means that moving the brush will draw a stroke. 135 | 136 | ``move 100, 100`` or ``mv 100, 100`` 137 | 138 | Moves the brush to the position ``x=100`` and ``y=100``. 139 | 140 | ``line from: [30, 20], to: [100, 100]`` 141 | 142 | A line between the two given points will be drawn. The movement to the starting point will be with an ``up`` brush, but the former state of the brush will be restored after this line. 143 | 144 | ### ``Drawille::Canvas`` API 145 | 146 | ``#set(x, y)`` / ``#[x, y] = true`` 147 | 148 | Sets the state of the given position to ``true``, i.e. the ``#frame`` method will render a point at ``[x,y]``. 149 | 150 | ``#unset(x, y)`` / ``#[x, y] = false`` 151 | 152 | Sets the state of the given position to ``false``, i.e. the ``#frame`` method will _not_ render a point at ``[x,y]``. 153 | 154 | ``#toggle(x, y)`` 155 | 156 | Toggles the state of the given position. 157 | 158 | ``#clear`` 159 | 160 | No point will be rendered by the ``#frame`` method. 161 | 162 | ``#frame`` 163 | 164 | Returns newline-delimited string of the given canvas state. Braille characters are used to represent points. Please note that a single character contains 2 x 4 pixels. 165 | 166 | ### Flip-book 167 | 168 | ```ruby 169 | c = Drawille::Canvas.new 170 | f = Drawille::FlipBook.new 171 | 172 | c.paint do 173 | move 200, 100 174 | down 175 | 176 | 36.times do 177 | right 10 178 | 36.times do 179 | right 10 180 | forward 8 181 | end 182 | f.snapshot canvas 183 | end 184 | 185 | end 186 | 187 | f.play repeat: true, fps: 6 188 | ``` 189 | 190 | With the flip-book it is possible to create and play animations. Just draw on the canvas as usual and create a snapshot for every frame you want to be included in the animation. 191 | 192 | ``FlipBook#snapshot canvas`` 193 | 194 | Saves a snapshot of the current state of the canvas. 195 | 196 | ``FlipBook#snapshot#play`` 197 | 198 | Will render the animation on the terminal. The method also takes an option hash with the options ``:repeat`` ``:fps``. 199 | 200 | As an alternative to snapshots it is possible to pass a block which will be called consecutively and should return a canvas which will be rendered as a frame in the animation or ``nil`` to stop the animation. 201 | 202 | ## License 203 | 204 | MIT License 205 | 206 | Permission is hereby granted, free of charge, to any person obtaining 207 | a copy of this software and associated documentation files (the 208 | "Software"), to deal in the Software without restriction, including 209 | without limitation the rights to use, copy, modify, merge, publish, 210 | distribute, sublicense, and/or sell copies of the Software, and to 211 | permit persons to whom the Software is furnished to do so, subject to 212 | the following conditions: 213 | 214 | The above copyright notice and this permission notice shall be 215 | included in all copies or substantial portions of the Software. 216 | 217 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 218 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 219 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 220 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 221 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 222 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 223 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 224 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec 8 | -------------------------------------------------------------------------------- /docs/images/conway.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maerch/ruby-drawille/73f5845e3d5c4805427d4ccbd9e27a0cde6f84a6/docs/images/conway.gif -------------------------------------------------------------------------------- /docs/images/mn-gon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maerch/ruby-drawille/73f5845e3d5c4805427d4ccbd9e27a0cde6f84a6/docs/images/mn-gon.gif -------------------------------------------------------------------------------- /docs/images/sinus.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maerch/ruby-drawille/73f5845e3d5c4805427d4ccbd9e27a0cde6f84a6/docs/images/sinus.gif -------------------------------------------------------------------------------- /docs/images/stencil.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maerch/ruby-drawille/73f5845e3d5c4805427d4ccbd9e27a0cde6f84a6/docs/images/stencil.gif -------------------------------------------------------------------------------- /drawille.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'drawille/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "drawille" 8 | gem.version = Drawille::VERSION 9 | gem.authors = ["Marcin Skirzynski"] 10 | gem.description = %q{Drawing in terminal.} 11 | gem.summary = %q{Drawing in terminal with Unicode Braille characters.} 12 | gem.license = "MIT" 13 | gem.homepage = "https://github.com/maerch/ruby-drawille" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.add_dependency 'curses' 21 | gem.add_development_dependency 'rspec' 22 | end 23 | -------------------------------------------------------------------------------- /examples/conway.rb: -------------------------------------------------------------------------------- 1 | require 'drawille' 2 | require 'curses' 3 | 4 | class Conway 5 | 6 | OFFSET_MATRIX = [[-1,-1], [0, -1], [1, -1], 7 | [-1, 0], [1, 0], 8 | [-1, 1], [0, 1], [1, 1]] 9 | 10 | def initialize height, width 11 | @height = height 12 | @width = width 13 | 14 | @world = initialize_world { |x, y| rand(0..1) } 15 | end 16 | 17 | def initialize_world 18 | Array.new(@width) { |x| Array.new(@height) { |y| yield x, y }} 19 | end 20 | 21 | def alive_neighbours x, y 22 | OFFSET_MATRIX.reduce(0) do |memo, offset| 23 | x_offset = x + offset[0] 24 | y_offset = y + offset[1] 25 | 26 | memo += @world[x_offset][y_offset] if inside_range? x_offset, y_offset 27 | memo 28 | end 29 | end 30 | 31 | def evolve 32 | next_world = initialize_world { |x, y| 0 } 33 | each do |x, y| 34 | alive_neighbours = alive_neighbours x, y 35 | next_world[x][y] = 36 | if cell_alive? x, y 37 | (2..3).include?(alive_neighbours) ? 1 : 0 38 | else 39 | alive_neighbours == 3 ? 1 : 0 40 | end 41 | end 42 | @world = next_world 43 | end 44 | 45 | def cell_alive? x, y 46 | @world[x][y] == 1 47 | end 48 | 49 | def inside_range? x, y 50 | (0...@height).include?(y) && (0...@width).include?(x) 51 | end 52 | 53 | def to_canvas 54 | canvas = Drawille::Canvas.new 55 | each do |x, y| 56 | canvas.set(x, y) if @world[x][y] > 0 57 | end 58 | canvas 59 | end 60 | 61 | private 62 | 63 | def each 64 | (0...@height).each do |y| 65 | (0...@width).each do |x| 66 | yield x, y 67 | end 68 | end 69 | end 70 | 71 | end 72 | 73 | Curses::init_screen() 74 | 75 | conway = Conway.new Curses.lines * 4, Curses.cols * 2 - 2 76 | flipbook = Drawille::FlipBook.new 77 | 78 | flipbook.play do 79 | canvas = conway.to_canvas 80 | conway.evolve 81 | canvas 82 | end 83 | -------------------------------------------------------------------------------- /examples/mn-gon.rb: -------------------------------------------------------------------------------- 1 | require 'drawille' 2 | 3 | c = Drawille::Canvas.new 4 | 5 | frame = c.paint do 6 | move 0, 0 7 | down 8 | 9 | 36.times do 10 | right 10 11 | 36.times do 12 | right 10 13 | forward 8 14 | end 15 | end 16 | 17 | end.frame 18 | 19 | puts frame 20 | -------------------------------------------------------------------------------- /examples/stencil-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maerch/ruby-drawille/73f5845e3d5c4805427d4ccbd9e27a0cde6f84a6/examples/stencil-1.png -------------------------------------------------------------------------------- /examples/stencil-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maerch/ruby-drawille/73f5845e3d5c4805427d4ccbd9e27a0cde6f84a6/examples/stencil-2.jpg -------------------------------------------------------------------------------- /examples/stencil.rb: -------------------------------------------------------------------------------- 1 | require 'drawille' 2 | require 'chunky_png' 3 | 4 | include ChunkyPNG 5 | 6 | canvas = Drawille::Canvas.new 7 | 8 | def draw canvas, img, xoffset=0 9 | (0..img.dimension.width-1).each do |x| 10 | (0..img.dimension.height-1).each do |y| 11 | r = Color.r(img[x,y]) 12 | g = Color.g(img[x,y]) 13 | b = Color.b(img[x,y]) 14 | canvas.set(x+xoffset, y) if (r + b + g) > 100 15 | end 16 | end 17 | end 18 | 19 | draw canvas, Image.from_file('examples/stencil-1.png') 20 | draw canvas, Image.from_file('examples/stencil-2.jpg'), 141 21 | 22 | puts canvas.frame 23 | -------------------------------------------------------------------------------- /lib/drawille.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require "drawille/version" 3 | 4 | require 'drawille/frameable' 5 | require "drawille/canvas" 6 | require "drawille/brush" 7 | require "drawille/flipbook" 8 | -------------------------------------------------------------------------------- /lib/drawille/brush.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | module Drawille 4 | 5 | class Brush 6 | 7 | attr_accessor :canvas 8 | 9 | def initialize canvas 10 | @canvas = canvas 11 | @state = { 12 | x: 0, 13 | y: 0, 14 | up: true, 15 | rotation: 0 16 | } 17 | end 18 | 19 | def down 20 | @state[:up] = false 21 | end 22 | 23 | def up 24 | @state[:up] = true 25 | end 26 | 27 | def forward length 28 | theta = ((@state[:rotation]) / 180.0 * Math::PI) 29 | x = (@state[:x] + length * Math::cos(theta)).round 30 | y = (@state[:y] + length * Math::sin(theta)).round 31 | 32 | move x, y 33 | end 34 | 35 | def back length 36 | forward -length 37 | end 38 | 39 | def right angle 40 | @state[:rotation] += angle 41 | end 42 | 43 | def left angle 44 | @state[:rotation] -= angle 45 | end 46 | 47 | def move x, y 48 | unless @state[:up] 49 | x1 = @state[:x].round 50 | y1 = @state[:y].round 51 | x2 = x 52 | y2 = y 53 | 54 | xdiff = [x1, x2].max - [x1, x2].min 55 | ydiff = [y1, y2].max - [y1, y2].min 56 | 57 | xdir = x1 <= x2 ? 1 : -1 58 | ydir = y1 <= y2 ? 1 : -1 59 | 60 | r = [xdiff, ydiff].max 61 | 62 | (0..r).each do |i| 63 | x, y = x1, y1 64 | y += (i.to_f*ydiff)/r*ydir if ydiff > 0 65 | x += (i.to_f*xdiff)/r*xdir if xdiff > 0 66 | @canvas.set(x, y) 67 | end 68 | end 69 | @state[:x], @state[:y] = x, y 70 | end 71 | 72 | def line coordinates={} 73 | last_state = @state[:up] 74 | 75 | up 76 | move *coordinates[:from] 77 | down 78 | move *coordinates[:to] 79 | 80 | @state[:up] = last_state 81 | end 82 | 83 | alias_method :fd, :forward 84 | alias_method :bk, :back 85 | alias_method :rt, :right 86 | alias_method :lt, :left 87 | alias_method :pu, :up 88 | alias_method :pd, :down 89 | alias_method :mv, :move 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/drawille/canvas.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | module Drawille 3 | 4 | class Canvas 5 | include Frameable 6 | 7 | attr_reader :chars 8 | 9 | def initialize 10 | clear 11 | @snapshots = [{}] 12 | end 13 | 14 | def paint &block 15 | Brush.new(self).instance_eval(&block) 16 | self 17 | end 18 | 19 | def clear 20 | @chars = Hash.new { |h,v| h[v] = Hash.new(0) } 21 | end 22 | 23 | def set x, y 24 | x, y, px, py = convert x, y 25 | @chars[py][px] |= PIXEL_MAP[y % 4][x % 2] 26 | end 27 | 28 | def unset x, y 29 | x, y, px, py = convert x, y 30 | @chars[py][px] &= ~PIXEL_MAP[y % 4][x % 2] 31 | end 32 | 33 | def get x, y 34 | x, y, px, py = convert x, y 35 | @chars[py][px] & PIXEL_MAP[y % 4][x % 2] != 0 36 | end 37 | 38 | def []= x, y, bool 39 | bool ? set(x, y) : unset(x, y) 40 | end 41 | 42 | alias_method :[], :get 43 | 44 | def toggle x, y 45 | x, y, px, py = convert x, y 46 | @chars[py][px] & PIXEL_MAP[y % 4][x % 2] == 0 ? set(x, y) : unset(x, y) 47 | end 48 | 49 | def convert x, y 50 | x = x.round 51 | y = y.round 52 | [x, y, (x / 2).floor, (y / 4).floor] 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/drawille/flipbook.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'curses' 3 | 4 | module Drawille 5 | 6 | class FlipBook 7 | include Frameable 8 | 9 | def initialize 10 | clear 11 | end 12 | 13 | def clear 14 | @snapshots = [] 15 | @chars = {} 16 | end 17 | 18 | def snapshot canvas 19 | @snapshots << canvas.frame 20 | @chars = canvas.chars 21 | end 22 | 23 | def each_frame options={} 24 | return enum_for(__callee__) unless block_given? 25 | @snapshots.each do |frame| 26 | yield frame 27 | end 28 | end 29 | 30 | def play options={} 31 | options = { 32 | repeat: false, fps: 6, 33 | min_x: 0, min_y: 0 34 | }.merge(options) 35 | 36 | Curses::init_screen 37 | begin 38 | Curses::crmode 39 | Curses::curs_set 0 40 | repeat options do 41 | if block_given? 42 | loop { 43 | canvas = yield 44 | raise StopIteration if canvas == nil 45 | draw canvas.frame 46 | } 47 | else 48 | each_frame options do |frame| 49 | draw frame 50 | sleep(1.0/options[:fps]) 51 | end 52 | end 53 | end 54 | ensure 55 | Curses::close_screen 56 | end 57 | end 58 | 59 | def repeat options 60 | options[:repeat] ? loop { yield; clear_screen options } : yield 61 | end 62 | 63 | private 64 | 65 | def draw frame 66 | Curses::setpos(0, 0) 67 | Curses::addstr(frame) 68 | Curses::refresh 69 | end 70 | 71 | def clear_screen options 72 | Curses::clear 73 | Curses::refresh 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/drawille/frameable.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | module Drawille 4 | 5 | module Frameable 6 | 7 | PIXEL_MAP = [[0x01, 0x08], 8 | [0x02, 0x10], 9 | [0x04, 0x20], 10 | [0x40, 0x80]] 11 | 12 | BRAILLE_CHAR_OFFSET = 0x2800 13 | 14 | def row y, options={} 15 | chars = options[:chars] || @chars 16 | row = chars[y] 17 | min = options[:min_x] 18 | max = options[:max_x] 19 | return "" if min.nil? || max.nil? 20 | (min..max).reduce("") { |memo, i| memo << to_braille(row.nil? ? 0 : row[i] || 0) } 21 | end 22 | 23 | def rows options={} 24 | chars = options[:chars] || @chars 25 | min = options[:min_y] || [(chars.keys.min || 0), 0].min 26 | max = options[:max_y] || chars.keys.max 27 | return [] if min.nil? || max.nil? 28 | options[:min_x] ||= [chars.reduce([]) { |m,x| m << x.last.keys }.flatten.min, 0].min 29 | options[:max_x] ||= chars.reduce([]) { |m,x| m << x.last.keys }.flatten.max 30 | (min..max).map { |i| row i, options } 31 | end 32 | 33 | def frame options={} 34 | rows(options).join("\n") 35 | end 36 | 37 | def char x, y 38 | to_braille @chars[y][x] 39 | end 40 | 41 | def to_braille x 42 | [BRAILLE_CHAR_OFFSET + x].pack("U*") 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/drawille/version.rb: -------------------------------------------------------------------------------- 1 | module Drawille 2 | VERSION = "0.3.3" 3 | end 4 | -------------------------------------------------------------------------------- /spec/drawille_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | include Drawille 5 | 6 | describe Canvas do 7 | subject(:canvas) { Canvas.new } 8 | 9 | describe '#set' do 10 | it 'sets pixel in the first char in the first row' do 11 | set_and_check_char (0..1), (0..3), 0, 0 12 | end 13 | 14 | it 'sets pixel in the third char in the first row' do 15 | set_and_check_char (4..5), (0..3), 2, 0 16 | end 17 | 18 | it 'sets pixel in the second char in the second row' do 19 | set_and_check_char (2..3), (4..7), 1, 1 20 | end 21 | end 22 | 23 | describe "#unset" do 24 | it 'sets and unsets a pixel' do 25 | canvas.set(0, 0) 26 | expect(canvas.char(0, 0)).to eq BRAILLE[0] 27 | canvas.unset(0, 0) 28 | expect(canvas.char(0, 0)).to eq BRAILLE.last 29 | end 30 | 31 | it 'sets four pixel but only unsets one' do 32 | canvas.set(0, 0) 33 | canvas.set(0, 1) 34 | canvas.set(1, 0) 35 | canvas.set(1, 1) 36 | expect(canvas.char(0, 0)).to eq BRAILLE[3] 37 | canvas.unset(1, 1) 38 | expect(canvas.char(0, 0)).to eq BRAILLE[2] 39 | end 40 | end 41 | 42 | describe "#get" do 43 | it 'returns the state of a pixel' do 44 | canvas.set(1, 1) 45 | expect(canvas.get(0, 0)).to eq false 46 | expect(canvas.get(1, 1)).to eq true 47 | end 48 | end 49 | 50 | describe "#toggle" do 51 | it 'toggles a pixel' do 52 | canvas.toggle(0, 0) 53 | canvas.toggle(1, 0) 54 | expect(canvas.char(0, 0)).to eq BRAILLE[1] 55 | canvas.toggle(1, 0) 56 | expect(canvas.char(0, 0)).to eq BRAILLE[0] 57 | canvas.toggle(0, 0) 58 | expect(canvas.char(0, 0)).to eq BRAILLE.last 59 | end 60 | end 61 | 62 | describe "#clear" do 63 | it 'clears the canvas' do 64 | canvas.set(0, 0) 65 | canvas.set(2, 0) 66 | expect(canvas.char(0, 0)).to eq BRAILLE[0] 67 | expect(canvas.char(1, 0)).to eq BRAILLE[0] 68 | canvas.clear 69 | expect(canvas.char(0, 0)).to eq BRAILLE.last 70 | expect(canvas.char(1, 0)).to eq BRAILLE.last 71 | end 72 | end 73 | 74 | describe '#[]' do 75 | it 'works with alternate syntax' do 76 | canvas[0, 0] = true 77 | expect(canvas.char(0, 0)).to eq BRAILLE[0] 78 | expect(canvas[0, 0]).to eq true 79 | expect(canvas[1, 0]).to eq false 80 | canvas[0, 0] = false 81 | expect(canvas.char(0, 0)).to eq BRAILLE.last 82 | expect(canvas[0, 0]).to eq false 83 | end 84 | end 85 | 86 | describe "#rows" do 87 | it 'has over 9 columns and 3 rows' do 88 | canvas.set(0, 1) 89 | canvas.set(4, 4) 90 | canvas.set(8, 5) 91 | canvas.set(16, 8) 92 | 93 | expect(canvas.rows.size).to eq 3 94 | canvas.rows.each do |row| 95 | expect(row.length).to eq 9 96 | end 97 | end 98 | end 99 | 100 | describe "#frame" do 101 | it 'prints a happy sinus' do 102 | (0..1800).step(10).each do |x| 103 | canvas.set(x/10, 10 + Math.sin(x * Math::PI / 180) * 10) 104 | end 105 | expect(canvas.frame).to eq IO.read("spec/sinus.dat") 106 | end 107 | 108 | it 'does not throw an exception on an empty canvas' do 109 | canvas.frame 110 | end 111 | 112 | it 'is an empty frame for an empty canvas' do 113 | expect(canvas.rows.size).to eq 0 114 | expect(canvas.frame).to eq "" 115 | end 116 | end 117 | end 118 | 119 | describe Brush do 120 | subject(:canvas) { Canvas.new } 121 | subject(:brush) { Brush.new(canvas) } 122 | 123 | describe "#up" do 124 | it 'is up by default' do 125 | brush.forward 40 126 | expect(canvas.rows.size).to eq 0 127 | end 128 | 129 | it 'does not draw anymore after putting brush up' do 130 | brush.down 131 | brush.forward 1 132 | brush.up 133 | brush.forward 2 134 | expect(canvas.rows.size).to eq 1 135 | expect(canvas.rows[0].size).to eq 1 136 | expect(canvas.char(0, 0)).to eq BRAILLE[1] 137 | end 138 | end 139 | 140 | describe "#down" do 141 | it "draws if brush is put down" do 142 | brush.down 143 | brush.forward 1 144 | expect(canvas.rows.size).to eq 1 145 | expect(canvas.rows[0].size).to eq 1 146 | expect(canvas.char(0, 0)).to eq BRAILLE[1] 147 | end 148 | end 149 | 150 | describe "#forward" do 151 | it 'moves to the right without changing the direction' do 152 | brush.down 153 | brush.forward 3 154 | expect(canvas.rows.size).to eq 1 155 | expect(canvas.rows[0].size).to eq 2 156 | expect(canvas.char(0, 0)).to eq BRAILLE[1] 157 | expect(canvas.char(1, 0)).to eq BRAILLE[1] 158 | end 159 | end 160 | 161 | describe "#back" do 162 | it 'moves backward without drawing' do 163 | brush.forward 10 164 | brush.back 10 165 | expect(canvas.rows.size).to eq 0 166 | end 167 | 168 | it 'moves backward with drawing' do 169 | brush.forward 9 170 | brush.down 171 | brush.back 9 172 | expect(canvas.rows.size).to eq 1 173 | expect(canvas.rows[0].size). to eq 5 174 | 5.times do |i| 175 | expect(canvas.char(i, 0)).to eq BRAILLE[1] 176 | end 177 | end 178 | end 179 | 180 | describe "#right" do 181 | it 'changes the direction' do 182 | brush.down 183 | brush.forward 1 184 | brush.right 90 185 | brush.forward 3 186 | brush.right 90 187 | brush.forward 1 188 | brush.right 90 189 | brush.forward 3 190 | expect(canvas.rows.size).to eq 1 191 | expect(canvas.rows[0].size).to eq 1 192 | expect(canvas.char(0, 0)).to eq BRAILLE[-2] 193 | end 194 | end 195 | 196 | describe "#left" do 197 | it 'changes the direction' do 198 | brush.move 0, 3 199 | brush.down 200 | brush.down 201 | brush.forward 1 202 | brush.left 90 203 | brush.forward 3 204 | brush.left 90 205 | brush.forward 1 206 | brush.left 90 207 | brush.forward 3 208 | expect(canvas.rows.size).to eq 1 209 | expect(canvas.rows[0].size).to eq 1 210 | expect(canvas.char(0, 0)).to eq BRAILLE[-2] 211 | end 212 | end 213 | 214 | describe "#move" do 215 | it 'moves and does not draw a line' do 216 | brush.move 200, 200 217 | expect(canvas.rows.size).to eq 0 218 | end 219 | 220 | it 'moves and does draw a line' do 221 | brush.down 222 | brush.move 99, 0 223 | brush.up 224 | brush.move 99, 4 225 | brush.down 226 | brush.move 0, 4 227 | expect(canvas.rows.size).to eq 2 228 | expect(canvas.rows[0].size).to eq 50 229 | 50.times do |i| 230 | expect(canvas.char(i, 0)).to eq BRAILLE[1] 231 | expect(canvas.char(i, 1)).to eq BRAILLE[1] 232 | end 233 | end 234 | end 235 | 236 | describe "#line" do 237 | it 'should draw a line regardless of the brush state' do 238 | brush.line from: [2, 0], to: [5, 0] 239 | expect(canvas.rows.size).to eq 1 240 | expect(canvas.char(0, 0)).to eq BRAILLE.last 241 | expect(canvas.char(1, 0)).to eq BRAILLE[1] 242 | expect(canvas.char(2, 0)).to eq BRAILLE[1] 243 | end 244 | 245 | it 'should not draw with the move to the beginning of the line' do 246 | brush.down 247 | brush.line from: [2, 0], to: [5, 0] 248 | expect(canvas.rows.size).to eq 1 249 | expect(canvas.char(0, 0)).to eq BRAILLE.last 250 | expect(canvas.char(1, 0)).to eq BRAILLE[1] 251 | expect(canvas.char(2, 0)).to eq BRAILLE[1] 252 | end 253 | end 254 | end 255 | 256 | describe FlipBook do 257 | subject(:canvas) { Canvas.new } 258 | subject(:flipbook) { FlipBook.new } 259 | 260 | describe "#each_frame" do 261 | it 'yields four frames' do 262 | canvas.set(0, 0) 263 | flipbook.snapshot canvas 264 | canvas.set(1, 0) 265 | flipbook.snapshot canvas 266 | canvas.set(0, 1) 267 | flipbook.snapshot canvas 268 | canvas.set(1, 1) 269 | flipbook.snapshot canvas 270 | 271 | i = 0 272 | flipbook.each_frame do |frame| 273 | expect(frame).to eq BRAILLE[i] 274 | i += 1 275 | end 276 | end 277 | 278 | it 'yields multiline frames' do 279 | canvas.set(0, 0) 280 | flipbook.snapshot canvas 281 | canvas.set(0, 4) 282 | flipbook.snapshot canvas 283 | 284 | expect(flipbook.rows.size).to eq 2 285 | expect(flipbook.char(0, 0)).to eq BRAILLE[0] 286 | expect(flipbook.char(0, 1)).to eq BRAILLE[0] 287 | end 288 | 289 | it 'yields the same frame twice' do 290 | canvas.set(0, 0) 291 | flipbook.snapshot canvas 292 | canvas.set(1, 0) 293 | flipbook.snapshot canvas 294 | 2.times do 295 | i = 0 296 | flipbook.each_frame do |frame| 297 | expect(frame).to eq BRAILLE[i] 298 | i += 1 299 | end 300 | end 301 | end 302 | 303 | it 'returns an enumerator without a block given' do 304 | canvas.set(0, 0) 305 | flipbook.snapshot canvas 306 | canvas.set(1, 0) 307 | flipbook.snapshot canvas 308 | flipbook.each_frame.with_index do |frame, i| 309 | expect(frame).to eq BRAILLE[i] 310 | end 311 | end 312 | end 313 | end 314 | -------------------------------------------------------------------------------- /spec/sinus.dat: -------------------------------------------------------------------------------- 1 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠒⠉⠑⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠒⠉⠑⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠒⠉⠑⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠒⠉⠑⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠒⠉⠑⠢⠀⠀⠀ 2 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡐⠁⠀⠀⠀⠀⠑⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡐⠁⠀⠀⠀⠀⠑⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡐⠁⠀⠀⠀⠀⠑⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡐⠁⠀⠀⠀⠀⠑⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡐⠁⠀⠀⠀⠀⠑⡀⠀ 3 | ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠌⠀⠀⠀⠀⠀⠀⠀⠈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠌⠀⠀⠀⠀⠀⠀⠀⠈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠌⠀⠀⠀⠀⠀⠀⠀⠈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠌⠀⠀⠀⠀⠀⠀⠀⠈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠌⠀⠀⠀⠀⠀⠀⠀⠈⠄ 4 | ⠈⢂⠀⠀⠀⠀⠀⢀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢂⠀⠀⠀⠀⠀⢀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢂⠀⠀⠀⠀⠀⢀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢂⠀⠀⠀⠀⠀⢀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢂⠀⠀⠀⠀⠀⢀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 5 | ⠀⠀⠡⣀⠀⢀⡠⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠡⣀⠀⢀⡠⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠡⣀⠀⢀⡠⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠡⣀⠀⢀⡠⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠡⣀⠀⢀⡠⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 6 | ⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'drawille' 3 | 4 | BRAILLE = %w[⠁ ⠉ ⠋ ⠛ ⠟ ⠿ ⡿ ⣿ ⠀] 5 | 6 | def set_and_check_char px_range, py_range, cx, cy 7 | i = 0 8 | py_range.each do |y| 9 | px_range.each do |x| 10 | canvas.set(x, y) 11 | expect(canvas.char(cx, cy)).to eq BRAILLE[i] 12 | i += 1 13 | end 14 | end 15 | end 16 | --------------------------------------------------------------------------------