├── .gitignore ├── .travis.yml ├── ChangeLog.md ├── LICENSE ├── README.md ├── composer.json ├── docs └── images │ ├── octocat.png │ ├── sb.png │ ├── sin.png │ └── turtle.png ├── examples ├── ImagePrinter.php ├── basic.php ├── composer.json ├── img2term.php ├── octocat2term.php ├── turtle.php └── xkcd2term.php ├── phpunit.xml.dist ├── src └── Drawille │ ├── Canvas.php │ └── Turtle.php └── tests └── Drawille ├── CanvasTest.php └── TurtleTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | phpunit.xml 4 | vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | 9 | before_script: 10 | - composer install --prefer-source 11 | 12 | script: vendor/bin/phpunit -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | ========= 3 | 4 | 1.0.1 5 | ----- 6 | 7 | * Fixed notice. 8 | * Added more examples. 9 | 10 | 1.0.0 11 | ----- 12 | 13 | * It begins! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jeff Welch 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-drawille 2 | ============ 3 | 4 | Terminal drawing with [braille](http://en.wikipedia.org/wiki/Braille). 5 | 6 | ![Octocat example](docs/images/octocat.png) 7 | 8 | ## Requirements 9 | 10 | php-drawille requires PHP 5.4.0 or later. 11 | 12 | ## Installation 13 | 14 | The recommended way to install php-drawille is [through 15 | composer](http://getcomposer.org). Just create a `composer.json` file and 16 | run the `php composer.phar install` command to install it: 17 | 18 | ~~~json 19 | { 20 | "require": { 21 | "whatthejeff/drawille": "~1.0" 22 | } 23 | } 24 | ~~~ 25 | 26 | ## Usage 27 | 28 | ~~~php 29 | use Drawille\Canvas; 30 | 31 | $canvas = new Canvas(); 32 | 33 | for($x = 0; $x <= 1800; $x += 10) { 34 | $canvas->set($x / 10, 10 + sin($x * M_PI / 180) * 10); 35 | } 36 | 37 | echo $canvas->frame(), "\n"; 38 | ~~~ 39 | 40 | ![Usage example](docs/images/sin.png) 41 | 42 | ~~~php 43 | use Drawille\Turtle; 44 | 45 | $turtle = new Turtle(); 46 | 47 | for($x = 0; $x < 36; $x++) { 48 | $turtle->right(10); 49 | 50 | for($y = 0; $y < 36; $y++) { 51 | $turtle->right(10); 52 | $turtle->forward(8); 53 | } 54 | } 55 | 56 | echo $turtle->frame(), "\n"; 57 | ~~~ 58 | 59 | ![Turtle example](docs/images/turtle.png) 60 | 61 | ## Examples 62 | 63 | To use the scripts in the [examples](examples) directory, you need to install the 64 | dependencies with composer. 65 | 66 | $ cd examples 67 | $ php composer.phar install 68 | $ ./img2term.php --fab --threshold 600 ~/Pictures/sb.png 69 | 70 | ![img2term example](docs/images/sb.png) 71 | 72 | ## Tests 73 | 74 | [![Build Status](https://travis-ci.org/whatthejeff/php-drawille.png?branch=master)](https://travis-ci.org/whatthejeff/php-drawille) 75 | 76 | To run the test suite, you need [composer](http://getcomposer.org). 77 | 78 | $ php composer.phar install 79 | $ vendor/bin/phpunit 80 | 81 | ## Acknowledgements 82 | 83 | php-drawille is a port of [drawille](https://github.com/asciimoo/drawille). 84 | 85 | ## License 86 | 87 | php-drawille is licensed under the [MIT license](LICENSE). -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatthejeff/drawille", 3 | "description": "Terminal drawing with braille", 4 | "keywords": ["terminal","console","braille","drawing","turtle"], 5 | "homepage": "http://github.com/whatthejeff/php-drawille", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Jeff Welch", 10 | "email": "whatthejeff@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.4.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "4.1.*" 18 | }, 19 | "autoload": { 20 | "psr-0": { 21 | "Drawille": "src/" 22 | } 23 | }, 24 | "extra": { 25 | "branch-alias": { 26 | "dev-master": "1.0.x-dev" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /docs/images/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatthejeff/php-drawille/541b2517a70b988d9f2a6249603f7331c1814957/docs/images/octocat.png -------------------------------------------------------------------------------- /docs/images/sb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatthejeff/php-drawille/541b2517a70b988d9f2a6249603f7331c1814957/docs/images/sb.png -------------------------------------------------------------------------------- /docs/images/sin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatthejeff/php-drawille/541b2517a70b988d9f2a6249603f7331c1814957/docs/images/sin.png -------------------------------------------------------------------------------- /docs/images/turtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatthejeff/php-drawille/541b2517a70b988d9f2a6249603f7331c1814957/docs/images/turtle.png -------------------------------------------------------------------------------- /examples/ImagePrinter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | use Imagine\Gd\Imagine; 13 | use Imagine\Image\Box; 14 | use Imagine\Image\Point; 15 | 16 | use Drawille\Canvas; 17 | 18 | use Fab\SuperFab; 19 | 20 | class ImagePrinter 21 | { 22 | private $image; 23 | private $threshold; 24 | private $ratio; 25 | private $invert; 26 | private $fab; 27 | 28 | public function __construct($image, $threshold = 385.2, $ratio = null, $invert = false, $fab = false) { 29 | $this->image = $image; 30 | $this->threshold = (float) $threshold; 31 | $this->ratio = $ratio; 32 | $this->invert = $invert; 33 | $this->fab = $fab; 34 | } 35 | 36 | public function run($terminalWidth, $terminalHeight) { 37 | $imagine = new Imagine(); 38 | $image = $imagine->open($this->image); 39 | 40 | $size = $image->getSize(); 41 | $width = $size->getWidth(); 42 | $height = $size->getHeight(); 43 | 44 | if ($this->ratio) { 45 | $ratio = (float) $this->ratio; 46 | $width = floor($width * $ratio); 47 | $height = floor($height * $ratio); 48 | $image->resize(new Box($width, $height)); 49 | } 50 | 51 | else { 52 | $height_ratio = $terminalHeight * 4 / $height; 53 | $width_ratio = $terminalWidth * 2 / $width; 54 | $ratio = min($height_ratio, $width_ratio); 55 | 56 | if ($ratio < 1.0) { 57 | $width = floor($width * $ratio); 58 | $height = floor($height * $ratio); 59 | $image->resize(new Box($width, $height)); 60 | } 61 | } 62 | 63 | $canvas = new Canvas(); 64 | 65 | for ($y = 0; $y < $height; $y++) { 66 | for ($x = 0; $x < $width; $x++) { 67 | $color = $image->getColorAt(new Point($x, $y)); 68 | $total = $color->getRed() + $color->getGreen() + $color->getBlue(); 69 | 70 | if (!$this->invert ^ $total > $this->threshold) { 71 | $canvas->set($x, $y); 72 | } 73 | } 74 | } 75 | 76 | if ($this->fab) { 77 | $fab = new SuperFab(); 78 | echo $fab->paint($canvas->frame()), "\n"; 79 | } 80 | 81 | else { 82 | echo $canvas->frame(), "\n"; 83 | } 84 | } 85 | } 86 | 87 | ?> -------------------------------------------------------------------------------- /examples/basic.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require_once __DIR__ . '/vendor/autoload.php'; 13 | 14 | use Drawille\Canvas; 15 | 16 | $canvas = new Canvas(); 17 | 18 | for ($x = 0; $x <= 1800; $x++) { 19 | $canvas->set($x / 10, sin($x * M_PI / 180) * 10); 20 | } 21 | 22 | echo $canvas->frame(), "\n"; 23 | $canvas->clear(); 24 | 25 | for ($x = 0; $x <= 1800; $x += 10) { 26 | $canvas->set($x / 10, 10 + sin($x * M_PI / 180) * 10); 27 | $canvas->set($x / 10, 10 + cos($x * M_PI / 180) * 10); 28 | } 29 | 30 | echo $canvas->frame(), "\n"; 31 | $canvas->clear(); 32 | 33 | for ($x = 0; $x <= 3600; $x += 20) { 34 | $canvas->set($x / 20, 4 + sin($x * M_PI / 180) * 4); 35 | } 36 | 37 | echo $canvas->frame(), "\n"; 38 | $canvas->clear(); 39 | 40 | for ($x = 0; $x <= 360; $x += 4) { 41 | $canvas->set($x / 4, 30 + sin($x * M_PI / 180) * 30); 42 | } 43 | 44 | for ($x = 0; $x <= 30; $x++) { 45 | for ($y = 0; $y <= 30; $y++) { 46 | $canvas->set($x, $y); 47 | $canvas->toggle($x+30, $y+30); 48 | $canvas->toggle($x+60, $y); 49 | } 50 | } 51 | 52 | echo $canvas->frame(), "\n"; -------------------------------------------------------------------------------- /examples/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatthejeff/drawille-examples", 3 | "description": "drawille examples", 4 | "keywords": ["terminal","console","braille","drawing","xkcd","octocat","image"], 5 | "homepage": "http://github.com/whatthejeff/drawille", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Jeff Welch", 10 | "email": "whatthejeff@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.4.0", 15 | "imagine/imagine": "~0.5.0", 16 | "symfony/console": "~2.4", 17 | "fabpot/Goutte": "~2.0", 18 | "whatthejeff/drawille": "~1.0", 19 | "whatthejeff/fab": "~1.0", 20 | "ext-gd": "*" 21 | }, 22 | "autoload": { 23 | "psr-0": { 24 | "ImagePrinter": "" 25 | } 26 | }, 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0.x-dev" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/img2term.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | require_once __DIR__ . '/vendor/autoload.php'; 14 | 15 | use Symfony\Component\Console\Application as ConsoleApplication; 16 | use Symfony\Component\Console\Command\Command as ConsoleCommand; 17 | 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Input\InputDefinition; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | 24 | class Application extends ConsoleApplication 25 | { 26 | public function __construct() { 27 | parent::__construct(basename(__FILE__), '1.0'); 28 | $this->add(new Command); 29 | } 30 | 31 | protected function getCommandName(InputInterface $input) { 32 | return basename(__FILE__); 33 | } 34 | 35 | protected function getDefaultInputDefinition() 36 | { 37 | return new InputDefinition(array( 38 | new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), 39 | new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version.') 40 | )); 41 | } 42 | } 43 | 44 | class Command extends ConsoleCommand 45 | { 46 | protected function configure() 47 | { 48 | $this->setName(basename(__FILE__)) 49 | ->setDescription('convert an image to terminal') 50 | ->addArgument( 51 | 'image', 52 | InputArgument::REQUIRED, 53 | 'Image file path/url' 54 | ) 55 | ->addOption( 56 | 'threshold', 57 | 't', 58 | InputOption::VALUE_REQUIRED, 59 | 'Color threshold', 60 | 382.5 61 | ) 62 | ->addOption( 63 | 'ratio', 64 | 'r', 65 | InputOption::VALUE_REQUIRED, 66 | 'Image resize ratio' 67 | ) 68 | ->addOption( 69 | 'fab', 70 | 'f', 71 | InputOption::VALUE_NONE, 72 | 'Make the output fabulous' 73 | ) 74 | ->addOption( 75 | 'invert', 76 | 'i', 77 | InputOption::VALUE_NONE, 78 | 'Invert colors' 79 | ); 80 | } 81 | 82 | protected function execute(InputInterface $input, OutputInterface $output) 83 | { 84 | list($terminalWidth, $terminalHeight) = $this->getApplication()->getTerminalDimensions(); 85 | 86 | $printer = new ImagePrinter( 87 | $input->getArgument('image'), 88 | $input->getOption('threshold'), 89 | $input->getOption('ratio'), 90 | $input->getOption('invert'), 91 | $input->getOption('fab') 92 | ); 93 | 94 | $printer->run($terminalWidth, $terminalHeight); 95 | } 96 | } 97 | 98 | $console = new Application(); 99 | $console->run(); -------------------------------------------------------------------------------- /examples/octocat2term.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | require_once __DIR__ . '/vendor/autoload.php'; 14 | 15 | use Symfony\Component\Console\Application as ConsoleApplication; 16 | use Symfony\Component\Console\Command\Command as ConsoleCommand; 17 | 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Input\InputDefinition; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | 24 | use Goutte\Client; 25 | 26 | class Application extends ConsoleApplication 27 | { 28 | public function __construct() { 29 | parent::__construct(basename(__FILE__), '1.0'); 30 | $this->add(new Command); 31 | } 32 | 33 | protected function getCommandName(InputInterface $input) { 34 | return basename(__FILE__); 35 | } 36 | 37 | protected function getDefaultInputDefinition() 38 | { 39 | return new InputDefinition(array( 40 | new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), 41 | new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version.') 42 | )); 43 | } 44 | } 45 | 46 | class Command extends ConsoleCommand 47 | { 48 | protected function configure() 49 | { 50 | $this->setName(basename(__FILE__)) 51 | ->setDescription('convert an octocat to terminal') 52 | ->addArgument( 53 | 'cat', 54 | InputArgument::REQUIRED, 55 | 'Cat number, name, title, or "random"' 56 | ) 57 | ->addOption( 58 | 'threshold', 59 | 't', 60 | InputOption::VALUE_REQUIRED, 61 | 'Color threshold', 62 | 382.5 63 | ) 64 | ->addOption( 65 | 'ratio', 66 | 'r', 67 | InputOption::VALUE_REQUIRED, 68 | 'Image resize ratio' 69 | ) 70 | ->addOption( 71 | 'fab', 72 | 'f', 73 | InputOption::VALUE_NONE, 74 | 'Make the output fabulous' 75 | ) 76 | ->addOption( 77 | 'invert', 78 | 'i', 79 | InputOption::VALUE_NONE, 80 | 'Invert colors' 81 | ); 82 | } 83 | 84 | protected function execute(InputInterface $input, OutputInterface $output) 85 | { 86 | $url = 'https://octodex.github.com'; 87 | $cat = $input->getArgument('cat'); 88 | 89 | $client = new Client(); 90 | $crawler = $client->request('GET', $url); 91 | 92 | try { 93 | if (is_numeric($cat) || $cat == 'random') { 94 | $filter = $crawler->filter('.preview-image > img'); 95 | $total = iterator_count($filter); 96 | 97 | if ($cat == 'random') { 98 | $cat = mt_rand(1, $total); 99 | } 100 | 101 | $image = $filter->eq($total - $cat)->attr('data-src'); 102 | } 103 | 104 | else if (substr($cat, 0, 4) == 'the ') { 105 | $image = $crawler->filter("img[alt=\"$cat\"]")->attr('data-src'); 106 | } 107 | 108 | else { 109 | $image = $crawler->filter("a[href=\"/$cat\"] > img")->attr('data-src'); 110 | } 111 | } 112 | 113 | catch (InvalidArgumentException $exception) { 114 | throw new RuntimeException('Octocat not found at: ' . $url); 115 | } 116 | 117 | list($terminalWidth, $terminalHeight) = $this->getApplication()->getTerminalDimensions(); 118 | 119 | $printer = new ImagePrinter( 120 | 'https://octodex.github.com' . $image, 121 | $input->getOption('threshold'), 122 | $input->getOption('ratio'), 123 | $input->getOption('invert'), 124 | $input->getOption('fab') 125 | ); 126 | 127 | $printer->run($terminalWidth, $terminalHeight); 128 | } 129 | } 130 | 131 | $console = new Application(); 132 | $console->run(); 133 | -------------------------------------------------------------------------------- /examples/turtle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require_once __DIR__ . '/vendor/autoload.php'; 13 | 14 | use Drawille\Turtle; 15 | 16 | $turtle = new Turtle(); 17 | 18 | for ($x = 0; $x < 36; $x++) { 19 | $turtle->right(10); 20 | 21 | for ($y = 0; $y < 36; $y++) { 22 | $turtle->right(10); 23 | $turtle->forward(8); 24 | } 25 | } 26 | 27 | echo $turtle->frame(), "\n"; -------------------------------------------------------------------------------- /examples/xkcd2term.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | require_once __DIR__ . '/vendor/autoload.php'; 14 | 15 | use Symfony\Component\Console\Application as ConsoleApplication; 16 | use Symfony\Component\Console\Command\Command as ConsoleCommand; 17 | 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Input\InputDefinition; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | 24 | use Goutte\Client; 25 | 26 | class Application extends ConsoleApplication 27 | { 28 | public function __construct() { 29 | parent::__construct(basename(__FILE__), '1.0'); 30 | $this->add(new Command); 31 | } 32 | 33 | protected function getCommandName(InputInterface $input) { 34 | return basename(__FILE__); 35 | } 36 | 37 | protected function getDefaultInputDefinition() 38 | { 39 | return new InputDefinition(array( 40 | new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), 41 | new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version.') 42 | )); 43 | } 44 | } 45 | 46 | class Command extends ConsoleCommand 47 | { 48 | protected function configure() 49 | { 50 | $this->setName(basename(__FILE__)) 51 | ->setDescription('convert an xkcd comic to terminal') 52 | ->addArgument( 53 | 'comic', 54 | InputArgument::REQUIRED, 55 | 'Comic ID or "random"' 56 | ) 57 | ->addOption( 58 | 'threshold', 59 | 't', 60 | InputOption::VALUE_REQUIRED, 61 | 'Color threshold', 62 | 382.5 63 | ) 64 | ->addOption( 65 | 'ratio', 66 | 'r', 67 | InputOption::VALUE_REQUIRED, 68 | 'Image resize ratio' 69 | ) 70 | ->addOption( 71 | 'fab', 72 | 'f', 73 | InputOption::VALUE_NONE, 74 | 'Make the output fabulous' 75 | ) 76 | ->addOption( 77 | 'invert', 78 | 'i', 79 | InputOption::VALUE_NONE, 80 | 'Invert colors' 81 | ); 82 | } 83 | 84 | protected function execute(InputInterface $input, OutputInterface $output) 85 | { 86 | $comic = $input->getArgument('comic'); 87 | $url = $comic == 'random' ? 'http://c.xkcd.com/random/comic/' : "http://xkcd.com/$comic/"; 88 | 89 | $client = new Client(); 90 | $crawler = $client->request('GET', $url); 91 | 92 | try { 93 | $image = $crawler->filter('#comic > img')->attr('src'); 94 | } 95 | 96 | catch(InvalidArgumentException $exception) { 97 | throw new RuntimeException('No comic found on: ' . $url); 98 | } 99 | 100 | list($terminalWidth, $terminalHeight) = $this->getApplication()->getTerminalDimensions(); 101 | 102 | $printer = new ImagePrinter( 103 | $image, 104 | $input->getOption('threshold'), 105 | $input->getOption('ratio'), 106 | $input->getOption('invert'), 107 | $input->getOption('fab') 108 | ); 109 | 110 | $printer->run($terminalWidth, $terminalHeight); 111 | } 112 | } 113 | 114 | $console = new Application(); 115 | $console->run(); -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | tests 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Drawille/Canvas.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Drawille; 13 | 14 | /** 15 | * Pixel surface 16 | * 17 | * @author Jeff Welch 18 | */ 19 | class Canvas 20 | { 21 | /** 22 | * Dots: 23 | * 24 | * ,___, 25 | * |1 4| 26 | * |2 5| 27 | * |3 6| 28 | * |7 8| 29 | * ````` 30 | * 31 | * @var array 32 | * @see http://www.alanwood.net/unicode/braille_patterns.html 33 | */ 34 | private static $pixel_map = [ 35 | [0x01, 0x08], 36 | [0x02, 0x10], 37 | [0x04, 0x20], 38 | [0x40, 0x80] 39 | ]; 40 | 41 | /** 42 | * Braille characters starts at 0x2800 43 | * 44 | * @var integer 45 | */ 46 | private static $braille_char_offset = 0x2800; 47 | 48 | /** 49 | * Canvas representation 50 | * 51 | * @var array 52 | */ 53 | private $chars = []; 54 | 55 | /** 56 | * Clears the canvas 57 | */ 58 | public function clear() { 59 | $this->chars = []; 60 | } 61 | 62 | /** 63 | * Sets a pixel at the given position 64 | * 65 | * @param integer $x x position 66 | * @param integer $y y position 67 | */ 68 | public function set($x, $y) { 69 | list($x, $y, $px, $py) = $this->prime($x, $y); 70 | $this->chars[$py][$px] |= $this->getDotFromMap($x, $y); 71 | } 72 | 73 | /** 74 | * Unsets a pixel at the given position 75 | * 76 | * @param integer $x x position 77 | * @param integer $y y position 78 | */ 79 | public function reset($x, $y) { 80 | list($x, $y, $px, $py) = $this->prime($x, $y); 81 | $this->chars[$py][$px] &= ~$this->getDotFromMap($x, $y); 82 | } 83 | 84 | /** 85 | * Gets the pixel state at a given position 86 | * 87 | * @param integer $x x position 88 | * @param integer $y y position 89 | * 90 | * @return bool the pixel state 91 | */ 92 | public function get($x, $y) { 93 | list($x, $y, , , $char) = $this->prime($x, $y); 94 | return (bool) ($char & $this->getDotFromMap($x, $y)); 95 | } 96 | 97 | /** 98 | * Toggles the pixel state on/off at a given position 99 | * 100 | * @param integer $x x position 101 | * @param integer $y y position 102 | */ 103 | public function toggle($x, $y) { 104 | $this->get($x, $y) ? $this->reset($x, $y) : $this->set($x, $y); 105 | } 106 | 107 | /** 108 | * Gets a line 109 | * 110 | * @param integer $y y position 111 | * @param array $options options 112 | * 113 | * @return string line 114 | */ 115 | public function row($y, array $options = []) { 116 | $row = isset($this->chars[$y]) ? $this->chars[$y] : []; 117 | 118 | if (!isset($options['min_x']) || !isset($options['max_x'])) { 119 | if (!($keys = array_keys($row))) { 120 | return ''; 121 | } 122 | } 123 | 124 | $min = isset($options['min_x']) ? $options['min_x'] : min($keys); 125 | $max = isset($options['max_x']) ? $options['max_x'] : max($keys); 126 | 127 | return array_reduce(range($min, $max), function ($carry, $item) use ($row) { 128 | return $carry .= $this->toBraille(isset($row[$item]) ? $row[$item] : 0); 129 | }, ''); 130 | } 131 | 132 | /** 133 | * Gets all lines 134 | * 135 | * @param array $options options 136 | * 137 | * @return array line 138 | */ 139 | public function rows(array $options = []) { 140 | if (!isset($options['min_y']) || !isset($options['max_y'])) { 141 | if (!($keys = array_keys($this->chars))) { 142 | return []; 143 | } 144 | } 145 | 146 | $min = isset($options['min_y']) ? $options['min_y'] : min($keys); 147 | $max = isset($options['max_y']) ? $options['max_y'] : max($keys); 148 | 149 | if (!isset($options['min_x']) || !isset($options['max_x'])) { 150 | $flattened = array(); 151 | foreach ($this->chars as $key => $char) { 152 | $flattened = array_merge($flattened, array_keys($char)); 153 | } 154 | } 155 | 156 | $options['min_x'] = isset($options['min_x']) ? $options['min_x'] : min($flattened); 157 | $options['max_x'] = isset($options['max_x']) ? $options['max_x'] : max($flattened); 158 | 159 | return array_map(function ($i) use ($options) { 160 | return $this->row($i, $options); 161 | }, range($min, $max)); 162 | } 163 | 164 | /** 165 | * Gets a string representation of the canvas 166 | * 167 | * @param array $options options 168 | * 169 | * @return string representation 170 | */ 171 | public function frame(array $options = []) { 172 | return join("\n", $this->rows($options)); 173 | } 174 | 175 | /** 176 | * Gets the canvas representation. 177 | * 178 | * @return array characters 179 | */ 180 | public function getChars() { 181 | return $this->chars; 182 | } 183 | 184 | /** 185 | * Gets a braille unicode character 186 | * 187 | * @param integer $code character code 188 | * 189 | * @return string braille 190 | */ 191 | private function toBraille($code) { 192 | return html_entity_decode('&#' . (self::$braille_char_offset + $code) . ';', ENT_NOQUOTES, 'UTF-8'); 193 | } 194 | 195 | /** 196 | * Gets a dot from the pixel map. 197 | * 198 | * @param integer $x x position 199 | * @param integer $y y position 200 | * 201 | * @return integer dot 202 | */ 203 | private function getDotFromMap($x, $y) { 204 | $y = $y % 4; 205 | $x = $x % 2; 206 | 207 | return self::$pixel_map[$y < 0 ? 4 + $y : $y][$x < 0 ? 2 + $x : $x]; 208 | } 209 | 210 | /** 211 | * Autovivification for a canvas position. 212 | * 213 | * @param integer $x x position 214 | * @param integer $y y position 215 | * 216 | * @return array 217 | */ 218 | private function prime($x, $y) { 219 | $x = round($x); 220 | $y = round($y); 221 | $px = floor($x / 2); 222 | $py = floor($y / 4); 223 | 224 | if (!isset($this->chars[$py][$px])) { 225 | $this->chars[$py][$px] = 0; 226 | } 227 | 228 | return [$x, $y, $px, $py, $this->chars[$py][$px]]; 229 | } 230 | } -------------------------------------------------------------------------------- /src/Drawille/Turtle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Drawille; 13 | 14 | /** 15 | * Basic turtle graphics interface 16 | * 17 | * @author Jeff Welch 18 | * @see http://en.wikipedia.org/wiki/Turtle_graphics 19 | */ 20 | class Turtle extends Canvas 21 | { 22 | /** 23 | * Current x position 24 | * 25 | * @var integer 26 | */ 27 | private $x = 0; 28 | /** 29 | * Current y position 30 | * 31 | * @var integer 32 | */ 33 | private $y = 0; 34 | /** 35 | * Current canvas rotation 36 | * 37 | * @var integer 38 | */ 39 | private $rotation = 0; 40 | 41 | /** 42 | * If the pen is up 43 | * 44 | * @var boolean 45 | */ 46 | private $up = false; 47 | 48 | /** 49 | * Constructor 50 | * 51 | * @param int $y starting x position 52 | * @param int $y starting y position 53 | */ 54 | public function __construct($x = 0, $y = 0) { 55 | $this->x = $x; 56 | $this->y = $y; 57 | } 58 | 59 | /** 60 | * Gets the current x position. 61 | * 62 | * @return integer x position 63 | */ 64 | public function getX() { 65 | return $this->x; 66 | } 67 | 68 | /** 69 | * Gets the current y position. 70 | * 71 | * @return integer y position 72 | */ 73 | public function getY() { 74 | return $this->y; 75 | } 76 | 77 | /** 78 | * Gets the current canvas rotation 79 | * 80 | * @return integer current canvas rotation 81 | */ 82 | public function getRotation() { 83 | return $this->rotation; 84 | } 85 | 86 | /** 87 | * Push the pen down 88 | */ 89 | public function down() { 90 | $this->up = false; 91 | } 92 | 93 | /** 94 | * Pull the pen up 95 | */ 96 | public function up() { 97 | $this->up = true; 98 | } 99 | 100 | /** 101 | * Move the pen forward 102 | * 103 | * @param integer $length distance to move forward 104 | */ 105 | public function forward($length) { 106 | $theta = $this->rotation / 180.0 * M_PI; 107 | $x = $this->x + $length * cos($theta); 108 | $y = $this->y + $length * sin($theta); 109 | 110 | $this->move($x, $y); 111 | } 112 | 113 | /** 114 | * Move the pen backwards 115 | * 116 | * @param integer $length distance to move backwards 117 | */ 118 | public function back($length) { 119 | $this->forward(-$length); 120 | } 121 | 122 | /** 123 | * Angle the canvas to the right. 124 | * 125 | * @param integer $angle degree to angle 126 | */ 127 | public function right($angle) { 128 | $this->rotation += $angle; 129 | } 130 | 131 | /** 132 | * Angle the canvas to the left. 133 | * 134 | * @param integer $angle degree to angle 135 | */ 136 | public function left($angle) { 137 | $this->rotation -= $angle; 138 | } 139 | 140 | /** 141 | * Move the pen, drawing if the pen is down. 142 | * 143 | * @param int $y new x position 144 | * @param int $y new y position 145 | */ 146 | public function move($x, $y) { 147 | if (!$this->up) { 148 | $x1 = round($this->x); 149 | $y1 = round($this->y); 150 | $x2 = $x; 151 | $y2 = $y; 152 | 153 | $xdiff = max($x1, $x2) - min($x1, $x2); 154 | $ydiff = max($y1, $y2) - min($y1, $y2); 155 | 156 | $xdir = $x1 <= $x2 ? 1 : -1; 157 | $ydir = $y1 <= $y2 ? 1 : -1; 158 | 159 | $r = max($xdiff, $ydiff); 160 | 161 | for ($i = 0; $i <= $r; $i++) { 162 | $x = $x1; 163 | $y = $y1; 164 | 165 | if ($ydiff > 0) { 166 | $y += ((float)$i * $ydiff) / $r * $ydir; 167 | } 168 | 169 | if ($xdiff > 0) { 170 | $x += ((float)$i * $xdiff) / $r * $xdir; 171 | } 172 | 173 | $this->set($x, $y); 174 | } 175 | } 176 | 177 | $this->x = $x; 178 | $this->y = $y; 179 | } 180 | 181 | /** 182 | * Pull the pen up 183 | */ 184 | public function pu() { 185 | $this->up(); 186 | } 187 | 188 | /** 189 | * Push the pen up 190 | */ 191 | public function pd() { 192 | $this->down(); 193 | } 194 | 195 | /** 196 | * Move the pen forward 197 | * 198 | * @param integer $length distance to move forward 199 | */ 200 | public function fd($length) { 201 | $this->forward($length); 202 | } 203 | 204 | /** 205 | * Move the pen, drawing if the pen is down. 206 | * 207 | * @param int $y new x position 208 | * @param int $y new y position 209 | */ 210 | public function mv($x, $y) { 211 | $this->move($x, $y); 212 | } 213 | 214 | /** 215 | * Angle the canvas to the right. 216 | * 217 | * @param integer $angle degree to angle 218 | */ 219 | public function rt($angle) { 220 | $this->right($angle); 221 | } 222 | 223 | /** 224 | * Angle the canvas to the left. 225 | * 226 | * @param integer $angle degree to angle 227 | */ 228 | public function lt($angle) { 229 | $this->left($angle); 230 | } 231 | 232 | /** 233 | * Move the pen backwards 234 | * 235 | * @param integer $length distance to move backwards 236 | */ 237 | public function bk($length) { 238 | $this->back($length); 239 | } 240 | } -------------------------------------------------------------------------------- /tests/Drawille/CanvasTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Drawille; 13 | 14 | class CanvasTest extends \PHPUnit_Framework_TestCase 15 | { 16 | private $canvas; 17 | 18 | protected function setUp() { 19 | $this->canvas = new Canvas(); 20 | } 21 | 22 | public function testSet() { 23 | $this->canvas->set(0, 0); 24 | $this->assertEquals([[1]], $this->canvas->getChars()); 25 | } 26 | 27 | /** 28 | * @depends testSet 29 | */ 30 | public function testReset() { 31 | $this->canvas->set(0, 0); 32 | $this->canvas->reset(0, 0); 33 | $this->assertEquals([[0]], $this->canvas->getChars()); 34 | } 35 | 36 | /** 37 | * @depends testSet 38 | */ 39 | public function testClear() { 40 | $this->canvas->set(0, 0); 41 | $this->canvas->clear(); 42 | $this->assertEmpty($this->canvas->getChars()); 43 | } 44 | 45 | public function testToggle() { 46 | $this->canvas->toggle(0, 0); 47 | $this->assertEquals([[1]], $this->canvas->getChars()); 48 | 49 | $this->canvas->toggle(0, 0); 50 | $this->assertEquals([[0]], $this->canvas->getChars()); 51 | } 52 | 53 | /** 54 | * @depends testSet 55 | */ 56 | public function testFrame() { 57 | $this->assertEquals($this->canvas->frame(), ''); 58 | 59 | $this->canvas->set(0, 0); 60 | $this->assertEquals($this->canvas->frame(), '⠁'); 61 | } 62 | 63 | /** 64 | * @depends testSet 65 | */ 66 | public function testGet() { 67 | $this->assertFalse($this->canvas->get(0, 0)); 68 | $this->canvas->set(0, 0); 69 | 70 | $this->assertTrue($this->canvas->get(0, 0)); 71 | $this->assertFalse($this->canvas->get(1, 0)); 72 | $this->assertFalse($this->canvas->get(0, 1)); 73 | $this->assertFalse($this->canvas->get(1, 1)); 74 | } 75 | } -------------------------------------------------------------------------------- /tests/Drawille/TurtleTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Drawille; 13 | 14 | class TurtleTest extends \PHPUnit_Framework_TestCase 15 | { 16 | private $turtle; 17 | 18 | protected function setUp() { 19 | $this->turtle = new Turtle(); 20 | } 21 | 22 | public function testPosition() { 23 | $this->assertEquals(0, $this->turtle->getX()); 24 | $this->assertEquals(0, $this->turtle->getY()); 25 | 26 | $this->turtle->move(1, 2); 27 | $this->assertEquals(1, $this->turtle->getX()); 28 | $this->assertEquals(2, $this->turtle->getY()); 29 | } 30 | 31 | public function testRotation() { 32 | $this->assertEquals(0, $this->turtle->getRotation()); 33 | 34 | $this->turtle->right(30); 35 | $this->assertEquals(30, $this->turtle->getRotation()); 36 | 37 | $this->turtle->left(30); 38 | $this->assertEquals(0, $this->turtle->getRotation()); 39 | } 40 | 41 | public function testBrush() { 42 | $this->assertFalse($this->turtle->get($this->turtle->getX(), $this->turtle->getY())); 43 | 44 | $this->turtle->forward(1); 45 | $this->assertTrue($this->turtle->get(0, 0)); 46 | $this->assertTrue($this->turtle->get($this->turtle->getX(), $this->turtle->getY())); 47 | 48 | $this->turtle->up(); 49 | $this->turtle->move(2, 0); 50 | $this->assertFalse($this->turtle->get($this->turtle->getX(), $this->turtle->getY())); 51 | 52 | $this->turtle->down(); 53 | $this->turtle->move(3, 0); 54 | $this->assertTrue($this->turtle->get($this->turtle->getX(), $this->turtle->getY())); 55 | } 56 | } --------------------------------------------------------------------------------