├── .gitignore ├── .npmignore ├── History.md ├── Makefile ├── Readme.md ├── examples ├── animated-chart.js ├── animation.js ├── chart.js ├── grid │ ├── grid.js │ ├── index.js │ └── package.js ├── lines.js ├── log.js ├── logs.js ├── nested-translations.js ├── platformer.js ├── scale.js ├── squares.js ├── state.js ├── tabs │ ├── index.js │ ├── tab.js │ └── tabs.js └── translate.js ├── index.js ├── lib ├── canvas.js ├── context2d.js ├── state.js └── term-canvas.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.sock 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.0.5 / 2012-07-21 3 | ================== 4 | 5 | * add tabs example 6 | * fix nested translations 7 | 8 | 0.0.4 / 2011-12-05 9 | ================== 10 | 11 | * Added animated chart example 12 | * Added grid example 13 | * Added `Context2d#translate(x,y)` 14 | * Added `Context2d#scale(x, y)` 15 | 16 | 0.0.3 / 2011-11-20 17 | ================== 18 | 19 | * Fixed clearRect() 20 | 21 | 0.0.2 / 2011-11-20 22 | ================== 23 | 24 | * Added `Context2d#save()` 25 | * Added `Context2d#restore()` 26 | * Added `Context2d#fillStyle=` 27 | * Added `Context2d#strokeStyle=` 28 | * Added `Context2d#resetState()` 29 | * Added `Context2d#lineTo()` 30 | * Added `Context2d#fillRect()` 31 | * Added __DEBUG__ support 32 | * Fixed platformer example for 0.6.x 33 | * Fixed rects exampl for 0.6.x 34 | 35 | 0.0.1 / 2010-01-03 36 | ================== 37 | 38 | * Initial release 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @echo "populate me" 4 | 5 | .PHONY: test -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # term-canvas 3 | 4 | experimental html canvas API for the terminal written with node.js. 5 | 6 | ![terminal canvas chart](http://f.cl.ly/items/0s112y02180S0l0N0u12/Grab.png) 7 | 8 | ![terminal canvas grid](http://f.cl.ly/items/3t3616131p0N032p1906/Screenshot.png) 9 | 10 | ## Installation 11 | 12 | ``` 13 | $ npm install term-canvas 14 | ``` 15 | 16 | ## Examples 17 | 18 | ### States 19 | 20 | ![state](http://f.cl.ly/items/0H1E3u371y1o3q2l2G2p/Grab.png) 21 | 22 | Source: 23 | 24 | ```js 25 | var Canvas = require('term-canvas'); 26 | 27 | var canvas = new Canvas(50, 100) 28 | , ctx = canvas.getContext('2d'); 29 | 30 | ctx.fillStyle = 'red'; 31 | ctx.fillRect(5, 5, 20, 10); 32 | ctx.save(); 33 | 34 | ctx.fillStyle = 'blue'; 35 | ctx.fillRect(6, 6, 18, 8); 36 | ctx.save(); 37 | 38 | ctx.fillStyle = 'yellow'; 39 | ctx.fillRect(7, 7, 16, 6); 40 | 41 | ctx.restore(); 42 | ctx.fillRect(8, 8, 14, 4); 43 | 44 | ctx.restore(); 45 | ctx.fillRect(9, 9, 12, 2); 46 | 47 | console.log('\n\n\n\n\n'); 48 | ctx.resetState(); 49 | ``` 50 | 51 | ### Fill styles 52 | 53 | Static colored rects with no draw loop: 54 | 55 | ![rects](http://f.cl.ly/items/3v3F3j2C0Q3H3t1C0r29/Grab.png) 56 | 57 | Source: 58 | 59 | ```js 60 | var Canvas = require('term-canvas'); 61 | 62 | var canvas = new Canvas(50, 100) 63 | , ctx = canvas.getContext('2d'); 64 | 65 | ctx.fillStyle = 'red'; 66 | ctx.fillRect(5, 5, 20, 10); 67 | 68 | ctx.fillStyle = 'blue'; 69 | ctx.fillRect(27, 5, 20, 10); 70 | 71 | ctx.fillStyle = 'yellow'; 72 | ctx.fillRect(49, 5, 20, 10); 73 | 74 | console.log('\n\n\n'); 75 | ctx.resetState(); 76 | 77 | ``` 78 | 79 | ### Animation 80 | 81 | ![animated rects](http://f.cl.ly/items/0s121k3C2R1R0q2w2I1y/Grab.png) 82 | 83 | Source: 84 | 85 | ```js 86 | var Canvas = require('term-canvas') 87 | , size = process.stdout.getWindowSize(); 88 | 89 | process.on('SIGINT', function(){ 90 | ctx.reset(); 91 | process.nextTick(function(){ 92 | process.exit(); 93 | }); 94 | }); 95 | 96 | process.on('SIGWINCH', function(){ 97 | size = process.stdout.getWindowSize(); 98 | canvas.width = size[0]; 99 | canvas.height = size[1]; 100 | x2 = x = 1; 101 | y2 = y = 1; 102 | }); 103 | 104 | var canvas = new Canvas(size[0], size[1]) 105 | , ctx = canvas.getContext('2d') 106 | , x = 1 107 | , y = 2 108 | , sx = 2 109 | , sy = 2 110 | , x2 = 1 111 | , y2 = 5 112 | , sx2 = 1 113 | , sy2 = 1; 114 | 115 | ctx.hideCursor(); 116 | setInterval(function(){ 117 | ctx.clearRect(0, 0, canvas.width, canvas.height); 118 | ctx.strokeStyle = 'blue'; 119 | ctx.strokeRect(1, 1, canvas.width - 1, canvas.height - 1); 120 | ctx.strokeStyle = 'green'; 121 | ctx.strokeRect(x += sx, y += sy, 30, 5); 122 | ctx.fillStyle = 'yellow'; 123 | ctx.fillRect(x2 += sx2, y2 += sy2, 12, 5); 124 | ctx.fillStyle = 'white'; 125 | ctx.fillText('Rectangle', x2 + 1, y2 + 2); 126 | ctx.moveTo(0, 10); 127 | if (x + 30 >= canvas.width || x <= 1) sx = -sx; 128 | if (x2 + 10 >= canvas.width || x2 <= 1) sx2 = -sx2; 129 | if (y + 5 >= canvas.height || y <= 1) sy = -sy; 130 | if (y2 + 5 >= canvas.height || y2 <= 1) sy2 = -sy2; 131 | }, 1000 / 20); 132 | ``` 133 | 134 | ## Tests 135 | 136 | There are none! currently testing with OSX "Terminal". 137 | 138 | ## License 139 | 140 | (The MIT License) 141 | 142 | Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca> 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining 145 | a copy of this software and associated documentation files (the 146 | 'Software'), to deal in the Software without restriction, including 147 | without limitation the rights to use, copy, modify, merge, publish, 148 | distribute, sublicense, and/or sell copies of the Software, and to 149 | permit persons to whom the Software is furnished to do so, subject to 150 | the following conditions: 151 | 152 | The above copyright notice and this permission notice shall be 153 | included in all copies or substantial portions of the Software. 154 | 155 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 156 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 157 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 158 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 159 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 160 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 161 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/animated-chart.js: -------------------------------------------------------------------------------- 1 | 2 | var Canvas = require('../') 3 | , canvas = new Canvas(50, 50) 4 | , ctx = canvas.getContext('2d'); 5 | 6 | process.on('SIGINT', function(){ 7 | ctx.reset(); 8 | process.nextTick(function(){ 9 | process.exit(); 10 | }); 11 | }); 12 | 13 | Array.prototype.max = function(){ 14 | return this.sort(function(a, b){ 15 | return a - b; 16 | }).pop(); 17 | }; 18 | 19 | function rand() { 20 | return Math.random() * 5 | 0; 21 | } 22 | 23 | ctx.hideCursor(); 24 | 25 | setInterval(function(){ 26 | var data = [rand(), rand(), rand(), rand(), rand(), rand()] 27 | , max = data.max() 28 | , x = 3; 29 | 30 | ctx.clearRect(0, 0, canvas.width, canvas.height); 31 | data.forEach(function(n){ 32 | var y = 20 33 | , w = 8 34 | , h = 20 * (n / max); 35 | 36 | ctx.fillStyle = 'blue'; 37 | ctx.fillRect(x, y - h + 3, w, h); 38 | ctx.fillStyle = 'white'; 39 | ctx.fillText(n.toString(), x + 3, y + 1); 40 | x += 9; 41 | }); 42 | }, 1000 / 6); -------------------------------------------------------------------------------- /examples/animation.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../') 7 | , size = process.stdout.getWindowSize(); 8 | 9 | process.on('SIGINT', function(){ 10 | ctx.reset(); 11 | process.nextTick(function(){ 12 | process.exit(); 13 | }); 14 | }); 15 | 16 | process.on('SIGWINCH', function(){ 17 | size = process.stdout.getWindowSize(); 18 | canvas.width = size[0]; 19 | canvas.height = size[1]; 20 | x2 = x = 1; 21 | y2 = y = 1; 22 | }); 23 | 24 | var canvas = new Canvas(size[0], size[1]) 25 | , ctx = canvas.getContext('2d') 26 | , x = 1 27 | , y = 2 28 | , sx = 2 29 | , sy = 2 30 | , x2 = 1 31 | , y2 = 5 32 | , sx2 = 1 33 | , sy2 = 1; 34 | 35 | ctx.hideCursor(); 36 | setInterval(function(){ 37 | ctx.clearRect(0, 0, canvas.width, canvas.height); 38 | ctx.strokeStyle = 'blue'; 39 | ctx.strokeRect(1, 1, canvas.width - 1, canvas.height - 1); 40 | ctx.strokeStyle = 'green'; 41 | ctx.strokeRect(x += sx, y += sy, 30, 5); 42 | ctx.fillStyle = 'yellow'; 43 | ctx.fillRect(x2 += sx2, y2 += sy2, 12, 5); 44 | ctx.fillStyle = 'white'; 45 | ctx.fillText('Rectangle', x2 + 1, y2 + 2); 46 | ctx.moveTo(0, 10); 47 | if (x + 30 >= canvas.width || x <= 1) sx = -sx; 48 | if (x2 + 10 >= canvas.width || x2 <= 1) sx2 = -sx2; 49 | if (y + 5 >= canvas.height || y <= 1) sy = -sy; 50 | if (y2 + 5 >= canvas.height || y2 <= 1) sy2 = -sy2; 51 | }, 1000 / 20); -------------------------------------------------------------------------------- /examples/chart.js: -------------------------------------------------------------------------------- 1 | 2 | var Canvas = require('../') 3 | , canvas = new Canvas(50, 50) 4 | , ctx = canvas.getContext('2d'); 5 | 6 | var data = [1, 2, 3, 2, 5, 1] 7 | , max = 5 8 | , x = 3; 9 | 10 | ctx.clear(); 11 | 12 | data.forEach(function(n){ 13 | var y = 20 14 | , w = 8 15 | , h = 20 * (n / max); 16 | 17 | ctx.fillStyle = 'blue'; 18 | ctx.fillRect(x, y - h + 3, w, h); 19 | ctx.fillStyle = 'white'; 20 | ctx.fillText(n.toString(), x + 3, y + 1); 21 | x += 9; 22 | }); 23 | 24 | ctx.resetState(); 25 | console.log('\n\n\n'); 26 | -------------------------------------------------------------------------------- /examples/grid/grid.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = Grid; 3 | 4 | function Grid(canvas) { 5 | this.canvas = canvas; 6 | this.width = canvas.width; 7 | this.height = canvas.height; 8 | this.x = 5; 9 | this.y = 2; 10 | this.xstep = 15; 11 | this.ystep = 4; 12 | this.objs = []; 13 | } 14 | 15 | Grid.prototype.clone = function(){ 16 | var grid = new Grid(this.canvas); 17 | this.objs.forEach(grid.add.bind(grid)); 18 | return grid; 19 | }; 20 | 21 | Grid.prototype.add = function(obj){ 22 | this.objs.push(obj); 23 | 24 | var w = this.width 25 | , h = this.height; 26 | 27 | if (this.x + this.xstep > w) { 28 | this.x = 5; 29 | this.y += this.ystep; 30 | } 31 | 32 | obj.moveTo(this.x, this.y); 33 | this.x += this.xstep; 34 | }; 35 | 36 | Grid.prototype.draw = function(ctx){ 37 | this.objs.forEach(function(obj){ 38 | obj.draw(ctx); 39 | }); 40 | }; -------------------------------------------------------------------------------- /examples/grid/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../../') 7 | , size = process.stdout.getWindowSize() 8 | , Package = require('./package') 9 | , Grid = require('./grid'); 10 | 11 | process.on('SIGINT', function(){ 12 | ctx.reset(); 13 | process.nextTick(function(){ 14 | process.exit(); 15 | }); 16 | }); 17 | 18 | process.on('SIGWINCH', function(){ 19 | size = process.stdout.getWindowSize(); 20 | canvas.width = size[0]; 21 | canvas.height = size[1]; 22 | grid = grid.clone(); 23 | }); 24 | 25 | var canvas = new Canvas(size[0], size[1]) 26 | , ctx = canvas.getContext('2d') 27 | , grid = new Grid(canvas); 28 | 29 | grid.add(new Package('express')); 30 | grid.add(new Package('tap')); 31 | grid.add(new Package('mocha')); 32 | grid.add(new Package('request')); 33 | grid.add(new Package('jade')); 34 | grid.add(new Package('swig')); 35 | grid.add(new Package('ejs')); 36 | grid.add(new Package('socket.io')); 37 | grid.add(new Package('log')); 38 | grid.add(new Package('flatiron')); 39 | grid.add(new Package('formidable')); 40 | grid.add(new Package('cluster')); 41 | grid.add(new Package('expresso')); 42 | grid.add(new Package('term-canvas')); 43 | grid.add(new Package('geddy')); 44 | grid.add(new Package('superagent')); 45 | 46 | ctx.hideCursor(); 47 | setInterval(function(){ 48 | ctx.clearRect(0, 0, canvas.width, canvas.height); 49 | grid.draw(ctx); 50 | }, 1000 / 20); 51 | 52 | // faux progress 53 | 54 | var states = ['downloading', 'unpacking', 'preinstall', 'postinstall', 'link', 'complete'] 55 | , total = states.length; 56 | 57 | grid.objs.forEach(function(obj){ 58 | obj.curr = 0; 59 | 60 | function update() { 61 | var state = states[obj.curr++]; 62 | if (!state) return; 63 | obj.text(state).complete(obj.curr / total); 64 | setTimeout(update, Math.random() * 800); 65 | } 66 | 67 | update(); 68 | }); -------------------------------------------------------------------------------- /examples/grid/package.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = Package; 3 | 4 | function Package(title) { 5 | this.complete(0); 6 | this.title(title); 7 | this.text(''); 8 | } 9 | 10 | Package.prototype.moveTo = function(x, y){ 11 | this.x = x; 12 | this.y = y; 13 | return this; 14 | }; 15 | 16 | Package.prototype.complete = function(n){ 17 | this._complete = n; 18 | return this; 19 | }; 20 | 21 | Package.prototype.title = function(str){ 22 | this._title = str; 23 | return this; 24 | }; 25 | 26 | Package.prototype.text = function(str){ 27 | this._text = str; 28 | return this; 29 | }; 30 | 31 | Package.prototype.draw = function(ctx){ 32 | var y = 0 33 | , text = this._text; 34 | 35 | ctx.save(); 36 | ctx.translate(this.x, this.y); 37 | 38 | // title 39 | if (this._title) { 40 | ctx.fillStyle = 'white'; 41 | ctx.fillText(this._title, 0, 0); 42 | } 43 | 44 | // text 45 | ctx.fillStyle = 'cyan'; 46 | ctx.fillText(text, 0, ++y); 47 | ctx.restore(); 48 | 49 | // progress 50 | var max = 10 51 | , width = (max * this._complete | 0) || 1 52 | , fill = Array(width).join('|') 53 | , left = Array(max - width).join(' ') 54 | , str = fill + left; 55 | 56 | // TODO: lame 57 | ctx.moveTo(this.x, this.y + ++y); 58 | ctx.write('\033[90m|\033[0m'); 59 | ctx.write(str); 60 | ctx.write('\033[90m|\033[0m'); 61 | }; -------------------------------------------------------------------------------- /examples/lines.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../') 7 | , tty = require('tty') 8 | , size = tty.getWindowSize(); 9 | 10 | process.on('SIGINT', function(){ 11 | ctx.reset(); 12 | process.nextTick(function(){ 13 | process.exit(); 14 | }); 15 | }); 16 | 17 | process.on('SIGWINCH', function(){ 18 | size = tty.getWindowSize(); 19 | canvas.width = size[1]; 20 | canvas.height = size[0]; 21 | }); 22 | 23 | var canvas = new Canvas(size[1], size[0]) 24 | , ctx = canvas.getContext('2d') 25 | , x = 15 26 | , y = 10; 27 | 28 | ctx.hideCursor(); 29 | setInterval(function(){ 30 | ctx.clearRect(0, 0, canvas.width, canvas.height); 31 | ctx.strokeStyle = 'yellow'; 32 | ctx.beginPath(); 33 | ctx.lineTo(5, 5); 34 | ctx.lineTo(x += .2, y += .1); 35 | ctx.lineTo(40, 5); 36 | ctx.lineTo(5, 5); 37 | ctx.stroke(); 38 | }, 1000 / 20); 39 | -------------------------------------------------------------------------------- /examples/log.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = Log; 3 | 4 | function Log(x, y) { 5 | this.x = x; 6 | this.y = y; 7 | this.lines = []; 8 | } 9 | 10 | Log.prototype.title = function(str){ 11 | this._title = str; 12 | return this; 13 | }; 14 | 15 | Log.prototype.write = function(line){ 16 | this.lines.push(line); 17 | return this; 18 | }; 19 | 20 | Log.prototype.draw = function(ctx){ 21 | var y = 0; 22 | ctx.save(); 23 | ctx.translate(this.x, this.y); 24 | 25 | // title 26 | if (this._title) { 27 | ctx.fillStyle = 'white'; 28 | ctx.fillText(this._title, 0, 0); 29 | } 30 | 31 | // lines 32 | ctx.fillStyle = 'cyan'; 33 | this.lines.forEach(function(line){ 34 | ctx.fillText(line, 0, y += 1); 35 | }); 36 | ctx.restore(); 37 | }; -------------------------------------------------------------------------------- /examples/logs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../') 7 | , size = process.stdout.getWindowSize() 8 | , Log = require('./log'); 9 | 10 | process.on('SIGINT', function(){ 11 | ctx.reset(); 12 | process.nextTick(function(){ 13 | process.exit(); 14 | }); 15 | }); 16 | 17 | process.on('SIGWINCH', function(){ 18 | size = process.stdout.getWindowSize(); 19 | canvas.width = size[0]; 20 | canvas.height = size[1]; 21 | }); 22 | 23 | var canvas = new Canvas(size[0], size[1]) 24 | , ctx = canvas.getContext('2d') 25 | , a = new Log(5, 2).title('express') 26 | , b = new Log(20, 2).title('tap') 27 | , c = new Log(35, 2).title('mocha') 28 | , d = new Log(50, 2).title('request') 29 | , objs = [a]; 30 | 31 | ctx.hideCursor(); 32 | setInterval(function(){ 33 | ctx.clearRect(0, 0, canvas.width, canvas.height); 34 | objs.forEach(function(obj){ 35 | obj.draw(ctx); 36 | }); 37 | }, 1000 / 20); 38 | 39 | setTimeout(function(){ 40 | objs.push(b); 41 | setTimeout(function(){ 42 | objs.push(c); 43 | setTimeout(function(){ 44 | objs.push(d); 45 | }, 300); 46 | }, 200); 47 | }, 100); 48 | 49 | setInterval(function(){ 50 | a.write(['something', 'else', 'happened'][Math.random() * 3 | 0]); 51 | d.write(['something', 'else', 'happened'][Math.random() * 3 | 0]); 52 | }, 200); 53 | 54 | setInterval(function(){ 55 | b.write(['something', 'else', 'happened'][Math.random() * 3 | 0]); 56 | }, 250); 57 | 58 | setInterval(function(){ 59 | c.write(['something', 'else', 'happened'][Math.random() * 3 | 0]); 60 | }, 150); 61 | -------------------------------------------------------------------------------- /examples/nested-translations.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../') 7 | , canvas = new Canvas(100, 100) 8 | , ctx = canvas.getContext('2d') 9 | , x = 0 10 | , y = 0; 11 | 12 | ctx.clearRect(0, 0, canvas.width, canvas.height); 13 | 14 | ctx.save(); 15 | ctx.translate(3, 3); 16 | ctx.fillStyle = 'yellow'; 17 | ctx.fillRect(0, 0, 10, 5); 18 | ctx.fillStyle = 'white'; 19 | ctx.fillText('Hey', 2, 1); 20 | 21 | ctx.save(); 22 | ctx.translate(3, 3); 23 | ctx.fillStyle = 'red'; 24 | ctx.fillRect(0, 0, 10, 5); 25 | ctx.fillStyle = 'white'; 26 | ctx.fillText('Hey', 2, 1); 27 | 28 | ctx.save(); 29 | ctx.translate(3, 3); 30 | ctx.fillStyle = 'blue'; 31 | ctx.fillRect(0, 0, 10, 5); 32 | ctx.fillStyle = 'white'; 33 | ctx.fillText('Hey', 2, 1); 34 | ctx.restore(); 35 | 36 | ctx.restore(); 37 | ctx.restore(); 38 | 39 | console.log('\n\n\n\n'); 40 | ctx.resetState(); 41 | -------------------------------------------------------------------------------- /examples/platformer.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../') 7 | , tty = require('tty') 8 | , size = process.stdout.getWindowSize(); 9 | 10 | process.on('SIGINT', function(){ 11 | ctx.reset(); 12 | process.nextTick(function(){ 13 | process.exit(); 14 | }); 15 | }); 16 | 17 | process.on('SIGWINCH', function(){ 18 | size = process.stdout.getWindowSize(); 19 | canvas.width = size[0]; 20 | canvas.height = size[1]; 21 | y = canvas.height - h; 22 | }); 23 | 24 | var canvas = new Canvas(size[0], size[1]) 25 | , ctx = canvas.getContext('2d') 26 | , w = 5 27 | , h = 3 28 | , sx = 0 29 | , sy = 0 30 | , x = 1 31 | , jumping = false 32 | , y = canvas.height - h; 33 | 34 | ctx.hideCursor(); 35 | setInterval(function(){ 36 | // move 37 | x += sx; 38 | sx *= .5; 39 | 40 | ctx.clearRect(0, 0, canvas.width, canvas.height); 41 | 42 | // player 43 | ctx.fillStyle = 'green'; 44 | ctx.fillRect(x, y, 5, 3); 45 | 46 | // ground 47 | ctx.fillStyle = 'white'; 48 | ctx.fillRect(0, canvas.height, canvas.width, 1); 49 | }, 1000 / 20); 50 | 51 | process.stdin.on('keypress', function(char, key){ 52 | switch (key.name) { 53 | case 'c': 54 | if (key.ctrl) process.kill(process.pid, 'SIGINT'); 55 | break; 56 | case 'up': 57 | break; 58 | case 'left': 59 | sx = 5; 60 | break; 61 | case 'right': 62 | sx = -5; 63 | break; 64 | } 65 | }).resume(); 66 | 67 | tty.setRawMode(true); -------------------------------------------------------------------------------- /examples/scale.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../') 7 | , size = process.stdout.getWindowSize(); 8 | 9 | process.on('SIGINT', function(){ 10 | ctx.reset(); 11 | process.nextTick(function(){ 12 | process.exit(); 13 | }); 14 | }); 15 | 16 | process.on('SIGWINCH', function(){ 17 | size = process.stdout.getWindowSize(); 18 | canvas.width = size[0]; 19 | canvas.height = size[1]; 20 | }); 21 | 22 | var canvas = new Canvas(size[0], size[1]) 23 | , ctx = canvas.getContext('2d') 24 | , sx = 1 25 | , sy = 1; 26 | 27 | ctx.hideCursor(); 28 | setInterval(function(){ 29 | ctx.clearRect(0, 0, canvas.width, canvas.height); 30 | ctx.save(); 31 | ctx.scale(sx -= .01, sy -= .01); 32 | ctx.fillStyle = 'red'; 33 | ctx.fillRect(10, 5, 60, 15); 34 | ctx.restore(); 35 | }, 1000 / 20); -------------------------------------------------------------------------------- /examples/squares.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../'); 7 | 8 | var canvas = new Canvas(50, 100) 9 | , ctx = canvas.getContext('2d'); 10 | 11 | ctx.clear(); 12 | 13 | ctx.fillStyle = 'red'; 14 | ctx.fillRect(5, 5, 20, 10); 15 | 16 | ctx.fillStyle = 'blue'; 17 | ctx.fillRect(27, 5, 20, 10); 18 | 19 | ctx.fillStyle = 'yellow'; 20 | ctx.fillRect(49, 5, 20, 10); 21 | 22 | console.log('\n\n\n'); 23 | ctx.resetState(); 24 | -------------------------------------------------------------------------------- /examples/state.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../'); 7 | 8 | var canvas = new Canvas(50, 100) 9 | , ctx = canvas.getContext('2d'); 10 | 11 | ctx.clear(); 12 | 13 | ctx.fillStyle = 'red'; 14 | ctx.fillRect(5, 5, 20, 10); 15 | ctx.save(); 16 | 17 | ctx.fillStyle = 'blue'; 18 | ctx.fillRect(6, 6, 18, 8); 19 | ctx.save(); 20 | 21 | ctx.fillStyle = 'yellow'; 22 | ctx.fillRect(7, 7, 16, 6); 23 | 24 | ctx.restore(); 25 | ctx.fillRect(8, 8, 14, 4); 26 | 27 | ctx.restore(); 28 | ctx.fillRect(9, 9, 12, 2); 29 | 30 | 31 | console.log('\n\n\n\n\n'); 32 | ctx.resetState(); -------------------------------------------------------------------------------- /examples/tabs/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../..') 7 | , canvas = new Canvas(100, 100) 8 | , ctx = canvas.getContext('2d') 9 | , Tabs = require('./tabs') 10 | , Tab = require('./tab'); 11 | 12 | ctx.clear(); 13 | 14 | var tabs = new Tabs; 15 | tabs.add(new Tab('Tab 1')) 16 | tabs.add(new Tab('Tab 2')) 17 | tabs.add(new Tab('Tab 3')) 18 | tabs.add(new Tab('Tab 4')) 19 | tabs.add(new Tab('Tab 5')) 20 | 21 | process.stdin.resume(); 22 | process.stdin.setRawMode(true); 23 | 24 | require('readline').emitKeypressEvents(process.stdin); 25 | 26 | process.stdin.on('keypress', function(s, key) { 27 | switch (key.name) { 28 | case 'c': 29 | if (key.ctrl) { 30 | ctx.reset(); 31 | process.exit(); 32 | } 33 | break; 34 | case 'up': 35 | tabs.prev(); 36 | tabs.render(ctx); 37 | break; 38 | case 'down': 39 | tabs.next(); 40 | tabs.render(ctx); 41 | break; 42 | } 43 | }); 44 | 45 | ctx.hideCursor(); 46 | tabs.render(ctx); 47 | -------------------------------------------------------------------------------- /examples/tabs/tab.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = Tab; 3 | 4 | function Tab(label) { 5 | this.label = label; 6 | this.active = false; 7 | } 8 | 9 | Tab.prototype.render = function(ctx){ 10 | ctx.save(); 11 | ctx.fillStyle = this.active ? 'red' : 'black'; 12 | ctx.fillRect(0, 0, 15, 3); 13 | ctx.fillStyle = 'white'; 14 | ctx.fillText(this.label, 3, 1); 15 | ctx.restore(); 16 | }; -------------------------------------------------------------------------------- /examples/tabs/tabs.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = Tabs; 3 | 4 | function Tabs() { 5 | this.tabs = []; 6 | this.active = 0; 7 | } 8 | 9 | Tabs.prototype.add = function(tab){ 10 | this.tabs.push(tab); 11 | }; 12 | 13 | Tabs.prototype.prev = function(){ 14 | this.active = Math.max(0, this.active - 1); 15 | }; 16 | 17 | Tabs.prototype.next = function(){ 18 | this.active = Math.min(this.tabs.length - 1, this.active + 1); 19 | }; 20 | 21 | Tabs.prototype.render = function(ctx){ 22 | var self = this; 23 | var y = 1; 24 | ctx.save(); 25 | ctx.translate(0, y); 26 | this.tabs.forEach(function(tab, i){ 27 | tab.active = i == self.active; 28 | tab.render(ctx); 29 | ctx.translate(0, y += 3); 30 | }); 31 | ctx.restore(); 32 | }; -------------------------------------------------------------------------------- /examples/translate.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Canvas = require('../') 7 | , size = process.stdout.getWindowSize(); 8 | 9 | process.on('SIGINT', function(){ 10 | ctx.reset(); 11 | process.nextTick(function(){ 12 | process.exit(); 13 | }); 14 | }); 15 | 16 | process.on('SIGWINCH', function(){ 17 | size = process.stdout.getWindowSize(); 18 | canvas.width = size[0]; 19 | canvas.height = size[1]; 20 | }); 21 | 22 | var canvas = new Canvas(size[0], size[1]) 23 | , ctx = canvas.getContext('2d') 24 | , x = 0 25 | , y = 0; 26 | 27 | ctx.hideCursor(); 28 | setInterval(function(){ 29 | ctx.clearRect(0, 0, canvas.width, canvas.height); 30 | ctx.save(); 31 | ctx.translate(x += .25, y += .1); 32 | ctx.fillStyle = 'yellow'; 33 | ctx.fillRect(0, 0, 10, 5); 34 | ctx.restore(); 35 | }, 1000 / 20); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/term-canvas'); -------------------------------------------------------------------------------- /lib/canvas.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var tty = require('tty') 7 | , Context2d = require('./context2d'); 8 | 9 | /** 10 | * Expose `Canvas`. 11 | */ 12 | 13 | module.exports = Canvas; 14 | 15 | /** 16 | * Initialize a new canvas with the given dimensions. 17 | * 18 | * @param {Number} width 19 | * @param {Number} height 20 | * @api public 21 | */ 22 | 23 | function Canvas(width, height) { 24 | this.width = width; 25 | this.height = height; 26 | this.stream = process.stdout; 27 | } 28 | 29 | /** 30 | * Return a rendering context of `type`. 31 | * 32 | * @param {String} type 33 | * @return {Context2d} 34 | * @api public 35 | */ 36 | 37 | Canvas.prototype.getContext = function(type){ 38 | switch (type) { 39 | case '2d': 40 | return new Context2d(this); 41 | default: 42 | throw new Error('invalid context type "' + type + '"'); 43 | } 44 | }; -------------------------------------------------------------------------------- /lib/context2d.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var State = require('./state'); 7 | 8 | /** 9 | * Expose `Context2d`. 10 | */ 11 | 12 | exports = module.exports = Context2d; 13 | 14 | /** 15 | * Math. 16 | */ 17 | 18 | var abs = Math.abs 19 | , round = Math.round; 20 | 21 | /** 22 | * Color map. 23 | */ 24 | 25 | exports.colors = { 26 | black: 0 27 | , red: 1 28 | , green: 2 29 | , yellow: 3 30 | , blue: 4 31 | , magenta: 5 32 | , cyan: 6 33 | , white: 7 34 | , normal: 9 35 | }; 36 | 37 | /** 38 | * Initialize a new 2d canvas rendering context. 39 | * 40 | * @param {Canvas} canvas 41 | * @api private 42 | */ 43 | 44 | function Context2d(canvas) { 45 | this.canvas = canvas; 46 | this.beginPath(); 47 | this.states = []; 48 | this.save(); 49 | } 50 | 51 | /** 52 | * Return the current `State` object. 53 | * 54 | * @return {State} 55 | * @api private 56 | */ 57 | 58 | Context2d.prototype.state = function(){ 59 | return this.states[this.states.length - 1]; 60 | }; 61 | 62 | /** 63 | * Get additive state `prop`. 64 | * 65 | * @param {String} prop 66 | * @return {Number} 67 | * @api private 68 | */ 69 | 70 | Context2d.prototype.getState = function(prop){ 71 | return this.states.reduce(function(sum, state){ 72 | return sum + state[prop]; 73 | }, 0); 74 | }; 75 | 76 | /** 77 | * Push a new `State` object to the stack. 78 | * 79 | * @api public 80 | */ 81 | 82 | Context2d.prototype.save = function(){ 83 | this.states.push(new State); 84 | }; 85 | 86 | /** 87 | * Pop the state stack. 88 | * 89 | * @api public 90 | */ 91 | 92 | Context2d.prototype.restore = function(){ 93 | this.states.pop(); 94 | }; 95 | 96 | /** 97 | * Begin a new path. 98 | * 99 | * @api public 100 | */ 101 | 102 | Context2d.prototype.beginPath = function(){ 103 | this.path = []; 104 | }; 105 | 106 | /** 107 | * Write the given `str`. 108 | * 109 | * @param {String} str 110 | * @api public 111 | */ 112 | 113 | Context2d.prototype.write = function(str){ 114 | if (process.env.DEBUG) { 115 | console.dir(str); 116 | } else { 117 | this.canvas.stream.write(str); 118 | } 119 | }; 120 | 121 | /** 122 | * Hide the cursor. 123 | * 124 | * @api public 125 | */ 126 | 127 | Context2d.prototype.hideCursor = function(){ 128 | this.write('\033[?25l'); 129 | }; 130 | 131 | /** 132 | * Show the cursor. 133 | * 134 | * @api public 135 | */ 136 | 137 | Context2d.prototype.showCursor = function(){ 138 | this.write('\033[?25h'); 139 | }; 140 | 141 | /** 142 | * Set fill style. 143 | * 144 | * @param {String} val 145 | * @api public 146 | */ 147 | 148 | Context2d.prototype.__defineSetter__('fillStyle', function(val){ 149 | this.state().fill = val; 150 | }); 151 | 152 | /** 153 | * Set stroke style. 154 | * 155 | * @param {String} val 156 | * @api public 157 | */ 158 | 159 | Context2d.prototype.__defineSetter__('strokeStyle', function(val){ 160 | this.state().stroke = val; 161 | }); 162 | 163 | /** 164 | * Move to the given point. 165 | * 166 | * @param {Number} x 167 | * @param {Number} y 168 | * @api public 169 | */ 170 | 171 | Context2d.prototype.moveTo = function(x, y){ 172 | x = round(x); 173 | y = round(y); 174 | this.write('\033[' + y + ';' + x + 'H'); 175 | }; 176 | 177 | /** 178 | * Scale (`x`, `y`) or (`x`, `x`). 179 | * 180 | * @param {Number} x 181 | * @param {Number} y 182 | * @return {Context2d} 183 | * @api public 184 | */ 185 | 186 | Context2d.prototype.scale = function(x, y){ 187 | if (2 == arguments.length) { 188 | this.state().scaleX = Math.max(x, 0); 189 | this.state().scaleY = Math.max(y, 0); 190 | return this; 191 | } else { 192 | return this.scale(x, x); 193 | } 194 | }; 195 | 196 | /** 197 | * Translate (`x`, `y`). 198 | * 199 | * @param {Number} x 200 | * @param {Number} y 201 | * @return {Context2d} 202 | * @api public 203 | */ 204 | 205 | Context2d.prototype.translate = function(x, y){ 206 | this.state().translateX = Math.max(x, 0); 207 | this.state().translateY = Math.max(y, 0); 208 | return this; 209 | }; 210 | 211 | /** 212 | * Plot line with the given points using 213 | * Bresenham's line algo. 214 | * 215 | * @param {Object} a 216 | * @param {Object} b 217 | * @api private 218 | */ 219 | 220 | Context2d.prototype.line = function(a, b){ 221 | var steep = abs(b.y - a.y) > abs(b.x - a.x) 222 | , tmp; 223 | 224 | if (steep) { 225 | tmp = a.y; 226 | a.y = a.x; 227 | a.x = tmp; 228 | 229 | tmp = b.y; 230 | b.y = b.x; 231 | b.x = tmp; 232 | } 233 | 234 | if (a.x > b.x) { 235 | tmp = b.x; 236 | b.x = a.x; 237 | a.x = tmp; 238 | 239 | tmp = b.y; 240 | b.y = a.y; 241 | a.y = tmp; 242 | } 243 | 244 | var dx = b.x - a.x 245 | , dy = abs(b.y - a.y) 246 | , err = 0 247 | , derr = dy / dx; 248 | 249 | var ystep 250 | , y = a.y; 251 | 252 | ystep = a.y < b.y 253 | ? 1 254 | : -1; 255 | 256 | for (var x = a.x; x < b.x; ++x) { 257 | if (steep) { 258 | this.moveTo(y, x); 259 | this.write(' '); 260 | } else { 261 | this.moveTo(x, y); 262 | this.write(' '); 263 | } 264 | 265 | err += derr; 266 | if (err > .5) { 267 | y += ystep; 268 | err -= 1; 269 | } 270 | } 271 | }; 272 | 273 | /** 274 | * Stroke the current path. 275 | * 276 | * @api public 277 | */ 278 | 279 | Context2d.prototype.stroke = function(){ 280 | var self = this 281 | , path = this.path 282 | , len = path.length 283 | , prev 284 | , curr; 285 | 286 | this.applyStrokeStyle(); 287 | for (var i = 1; i < len; ++i) { 288 | prev = path[i-1]; 289 | curr = path[i]; 290 | this.line(prev, curr); 291 | } 292 | }; 293 | 294 | /** 295 | * Add a line to the given point. 296 | * 297 | * @param {Number} x 298 | * @param {Number} y 299 | * @api public 300 | */ 301 | 302 | Context2d.prototype.lineTo = function(x, y){ 303 | this.path.push({ x: x, y: y }); 304 | }; 305 | 306 | /** 307 | * Clear rect. 308 | * 309 | * @param {Number} x 310 | * @param {Number} y 311 | * @param {Number} w 312 | * @param {Number} h 313 | * @api public 314 | */ 315 | 316 | Context2d.prototype.clearRect = function(ox, oy, w, h){ 317 | if ( 0 == ox 318 | && 0 == oy 319 | && w == this.canvas.width 320 | && h == this.canvas.height) { 321 | return this.clear(); 322 | } 323 | 324 | for (var y = 0; y < h; ++y) { 325 | for (var x = 0; x < w; ++x) { 326 | this.moveTo(ox + x, oy + y); 327 | this.write('\033[0m '); 328 | } 329 | } 330 | }; 331 | 332 | /** 333 | * Clear the line. 334 | * 335 | * @api private 336 | */ 337 | 338 | Context2d.prototype.clearLine = function(){ 339 | this.write('\033[2K'); 340 | }; 341 | 342 | /** 343 | * Clear down. 344 | * 345 | * @api private 346 | */ 347 | 348 | Context2d.prototype.clearDown = function(){ 349 | this.write('\033[J'); 350 | }; 351 | 352 | /** 353 | * Clear. 354 | * 355 | * @api public 356 | */ 357 | 358 | Context2d.prototype.clear = function(){ 359 | this.write('\033[0m\033[1J'); 360 | this.clearDown(); 361 | }; 362 | 363 | /** 364 | * Write text at the given point. 365 | * 366 | * @param {String} str 367 | * @param {Number} x 368 | * @param {Number} y 369 | * @api public 370 | */ 371 | 372 | Context2d.prototype.fillText = function(str, x, y){ 373 | x += this.getState('translateX'); 374 | y += this.getState('translateY'); 375 | this.applyForegroundFillStyle(); 376 | this.moveTo(x, y); 377 | this.write(str); 378 | }; 379 | 380 | /** 381 | * Reset the terminal state. 382 | * 383 | * @api public 384 | */ 385 | 386 | Context2d.prototype.reset = function(){ 387 | this.clear(); 388 | this.showCursor(); 389 | this.moveTo(0, 0); 390 | this.resetState(); 391 | }; 392 | 393 | /** 394 | * Reset state. 395 | * 396 | * @api public 397 | */ 398 | 399 | Context2d.prototype.resetState = function(){ 400 | this.state().fill = 'normal'; 401 | this.state().stroke = 'normal'; 402 | this.applyFillStyle(); 403 | this.applyStrokeStyle(); 404 | }; 405 | 406 | /** 407 | * Stroke a rect. 408 | * 409 | * TODO: use lineTo() 410 | * 411 | * @param {Number} x 412 | * @param {Number} y 413 | * @param {Number} w 414 | * @param {Number} h 415 | * @api public 416 | */ 417 | 418 | Context2d.prototype.strokeRect = function(x, y, w, h){ 419 | var sx = this.state().scaleX; 420 | var sy = this.state().scaleY; 421 | var tx = this.getState('translateX'); 422 | var ty = this.getState('translateY'); 423 | 424 | w *= sx; 425 | h *= sy; 426 | x += tx; 427 | y += ty; 428 | 429 | var hr = Array(Math.round(w + 1)).join(' '); 430 | this.applyStrokeStyle(); 431 | this.moveTo(x, y); 432 | this.write(hr); 433 | for (var i = 1; i < h; ++i) { 434 | this.moveTo(x, y + i); 435 | this.write(' '); 436 | this.moveTo(x + w - 1, y + i); 437 | this.write(' '); 438 | } 439 | this.moveTo(x, y + h); 440 | this.write(hr); 441 | }; 442 | 443 | /** 444 | * Fill a rect. 445 | * 446 | * @param {Number} x 447 | * @param {Number} y 448 | * @param {Number} w 449 | * @param {Number} h 450 | * @api public 451 | */ 452 | 453 | Context2d.prototype.fillRect = function(ox, oy, w, h){ 454 | var sx = this.state().scaleX; 455 | var sy = this.state().scaleY; 456 | var tx = this.getState('translateX'); 457 | var ty = this.getState('translateY'); 458 | 459 | w *= sx; 460 | h *= sy; 461 | ox += tx; 462 | oy += ty; 463 | 464 | this.applyFillStyle(); 465 | for (var y = 0; y < h; ++y) { 466 | for (var x = 0; x < w; ++x) { 467 | this.moveTo(ox + x, oy + y); 468 | this.write(' '); 469 | } 470 | } 471 | }; 472 | 473 | /** 474 | * Apply the current fg fill style. 475 | * 476 | * @api private 477 | */ 478 | 479 | Context2d.prototype.applyForegroundFillStyle = function(){ 480 | color = exports.colors[this.state().fill]; 481 | this.write('\033[3' + color + 'm'); 482 | }; 483 | 484 | /** 485 | * Apply the current fill style. 486 | * 487 | * @api private 488 | */ 489 | 490 | Context2d.prototype.applyFillStyle = function(){ 491 | color = exports.colors[this.state().fill]; 492 | this.write('\033[4' + color + 'm'); 493 | }; 494 | 495 | /** 496 | * Apply the current stroke style. 497 | * 498 | * @api private 499 | */ 500 | 501 | Context2d.prototype.applyStrokeStyle = function(){ 502 | color = exports.colors[this.state().stroke]; 503 | this.write('\033[4' + color + 'm'); 504 | }; 505 | -------------------------------------------------------------------------------- /lib/state.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Expose `State`. 4 | */ 5 | 6 | module.exports = State; 7 | 8 | /** 9 | * Initialize a new `State`. 10 | */ 11 | 12 | function State() { 13 | this.fill = 'normal'; 14 | this.stroke = 'normal'; 15 | this.scaleX = 1; 16 | this.scaleY = 1; 17 | this.translateX = 0; 18 | this.translateY = 0; 19 | } -------------------------------------------------------------------------------- /lib/term-canvas.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * term-canvas 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var Canvas = require('./canvas'); 13 | 14 | /** 15 | * Expose `Canvas`. 16 | */ 17 | 18 | module.exports = Canvas; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "term-canvas" 3 | , "version": "0.0.5" 4 | , "description": "Terminal canvas api written with node.js" 5 | , "keywords": ["canvas", "ascii", "ansi", "terminal", "shell"] 6 | , "author": "TJ Holowaychuk " 7 | , "dependencies": {} 8 | , "main": "index" 9 | } --------------------------------------------------------------------------------