Skip to content

Commit 89efdd7

Browse files
committed
Merge branch 'ANSI'
2 parents b6e24bd + f854222 commit 89efdd7

File tree

6 files changed

+263
-28
lines changed

6 files changed

+263
-28
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
# Three.js in the Terminal aka Three.js Terminal Renderer
1+
# Terminal Renderer for Three.js
22

33
This is an example of how you can run three.js in your terminal.
44
Sysadmins can now run three.js in remote ssh sessions!
55

66
![threejs-term](https://cloud.githubusercontent.com/assets/314997/19834595/b96b3e84-9ea0-11e6-950b-5b3103969a9b.gif)
77

88
### Install and Running
9-
Without Git Clonning. Otherwise look at Development section.
9+
10+
If you prefer to run it by git cloning enviornment, look at the development section.
1011

1112
```
1213
$ npm install -g threejs-term # install and link binary
1314
$ threejs-term # runs the demo
1415
```
1516

17+
### Using demo
18+
19+
Keys
20+
`m` - Toggle Ascii Mode
21+
`q`, `Ctrl-C`, `Esc` - Quits app.
22+
`Ctrl-F12` - Developer's console
23+
`a` - Camera Rotate Mode
24+
`s` - Camera Scale Mode
25+
`d` - Camera Position Mode
26+
1627
### Troubleshooting
1728
If npm install fails to compile Canvas bindings to Cario, make sure your system have the [necessary libraries](https://github.com/Automattic/node-canvas).
1829
For mac homebrew users, you can simply run `brew install cario`.
@@ -23,6 +34,7 @@ For mac homebrew users, you can simply run `brew install cario`.
2334
- Screen resize detection aka "Responsive Design"!
2435
- Support mouse events
2536
- Emulate keypress
37+
- Toggle different ASCII rendering modes
2638

2739
### Internals
2840
This is built with some awesome libraries.

TerminalRenderer.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ require('three/examples/js/renderers/Projector');
55
// require('three/examples/js/renderers/SoftwareRenderer');
66
require('three/examples/js/renderers/CanvasRenderer');
77

8-
98
// TODO use a proxy class?
109
// TODO support WebGLRenderer via headless-gl or node-webgl?
1110

12-
class TerminalRenderer {
11+
const ansi = require('./ansi');
1312

13+
class TerminalRenderer {
1414
constructor(screen) {
1515
this.screen = screen;
1616
// Set up fake canvas
@@ -21,13 +21,17 @@ class TerminalRenderer {
2121
canvas: canvas, // pass in fake canvas
2222
};
2323

24+
this.ctx = canvas.getContext('2d');
25+
2426
// renderer = new THREE.SoftwareRenderer(params); // TODO pass in raw arrays and render that instead
2527
const renderer = new THREE.CanvasRenderer(params);
2628
this.canvas = canvas;
2729
this.renderer = renderer;
2830
}
2931

3032
setSize(w, h) {
33+
this.width = w;
34+
this.height = h;
3135
this.renderer.setSize(w, h);
3236
}
3337

@@ -37,17 +41,22 @@ class TerminalRenderer {
3741

3842
render(scene, camera) {
3943
this.renderer.render(scene, camera);
40-
this.screen.setImage(this.canvas.toBuffer());
44+
// this.screen.setImage(this.canvas.toBuffer()); // only for AnsiImage widget // seems to be faster but less configurable
45+
// console.error(`Size ${this.width},${this.height} ${this.screen.width},${this.screen.height} `);
46+
this.image = this.ctx.getImageData(0, 0, this.width, this.height);
47+
const c = ansi.convert(this.image, this.screen.width, this.screen.height)
48+
this.screen.setContent(c);
49+
}
50+
51+
setAnsiOptions(o) {
52+
ansi.setOptions(o);
4153
}
4254

4355
saveRenderToFile(file) {
4456
// Write canvas to file
4557
const out = fs.createWriteStream(file);
4658
return canvas.pngStream().pipe(out);
4759
}
48-
49-
50-
5160
}
5261

5362
THREE.TerminalRenderer = TerminalRenderer;

ansi.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Adopted from blessed/vendor/tng.js
2+
3+
const colors = require('blessed/lib/colors');
4+
5+
let options = {
6+
plain_formatting: true, // overrides bg and ascii_formatting
7+
bg_formatting: true, // colors background
8+
ascii_formatting: true, // use ascii for forground
9+
10+
bga: 1, // background alpha blending
11+
fga: 0.5, // forground alpha blending,
12+
dchars: ''
13+
};
14+
15+
// 3 Modules
16+
// ANSI only - No colors
17+
// BG COLORS only - No ascii
18+
// ASCII + BG COLORS + FG Colors
19+
// ASCII + FG Colors
20+
21+
22+
// Taken from libcaca:
23+
options.dchars = '????8@8@#8@8##8#MKXWwz$&%x><\\/xo;+=|^-:i\'.`, `. '.split('').reverse().join('')
24+
25+
// options.dchars = ' .,:;=|iI+hHOE#`$'
26+
27+
// darker bolder character set from https://github.com/saw/Canvas-ASCII-Art/
28+
// options.dchars = ' .\'`^",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$'
29+
// options.dchars = ' .:-=+*#%@';
30+
// options.dchars = '●☭☮☯♔♛♙♞';
31+
// options.dchars = '█▓▒░ '
32+
33+
options.dchars = options.dchars.split('').reverse().join('');
34+
35+
function pixelToSGR(pixel, ch) {
36+
var a = pixel.a / 255
37+
, bga = options.bga
38+
, fga = options.fga
39+
, bg
40+
, fg;
41+
42+
bg = colors.match(
43+
pixel.r * a * bga | 0,
44+
pixel.g * a * bga | 0,
45+
pixel.b * a * bga | 0);
46+
47+
if (options.ascii_formatting && ch) {
48+
fg = colors.match(
49+
pixel.r * a * fga | 0,
50+
pixel.g * a * fga | 0,
51+
pixel.b * a * fga | 0);
52+
if (a === 0 || !options.bg_formatting) {
53+
return '\x1b[38;5;' + fg + 'm' + ch + '\x1b[m';
54+
}
55+
return '\x1b[38;5;' + fg + 'm\x1b[48;5;' + bg + 'm' + ch + '\x1b[m';
56+
}
57+
58+
if (a === 0) return ' ';
59+
60+
return '\x1b[48;5;' + bg + 'm \x1b[m';
61+
}
62+
63+
function luminance(pixel) {
64+
var a = pixel.a / 255
65+
, r = pixel.r * a
66+
, g = pixel.g * a
67+
, b = pixel.b * a
68+
, l = 0.2126 * r + 0.7152 * g + 0.0722 * b;
69+
70+
return l / 255;
71+
};
72+
73+
function getOutch(pixel) {
74+
var lumi = luminance(pixel)
75+
, dchars = options.dchars
76+
, outch = dchars[lumi * (dchars.length - 1) | 0];
77+
78+
return outch;
79+
}
80+
81+
function convert(image, targetWidth, targetHeight) {
82+
const { width, height, data } = image;
83+
let out = [];
84+
let ty, tx, i, pixel, outch;
85+
for (let y = 0; y < targetHeight; y++) {
86+
ty = y / targetHeight * height | 0;
87+
for (let x = 0; x < targetWidth; x++) {
88+
tx = x / targetWidth * width | 0;
89+
i = ty * width + tx;
90+
91+
pixel = {
92+
r: data[i * 4 + 0],
93+
g: data[i * 4 + 1],
94+
b: data[i * 4 + 2],
95+
a: data[i * 4 + 0]
96+
};
97+
98+
outch = getOutch(pixel);
99+
if (options.plain_formatting) {
100+
out.push(outch);
101+
}
102+
else {
103+
out.push(pixelToSGR(pixel, outch));
104+
}
105+
}
106+
out.push('\n');
107+
}
108+
return out.join('');
109+
}
110+
111+
module.exports = {
112+
convert: convert,
113+
setOptions(o) {
114+
Object.assign(options, o);
115+
},
116+
};

play.js

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ const { FPSCounter, MemCounter } = require('./counters');
1717
* 29 Oct 2016
1818
*
1919
* TODOs
20-
* - optimize ascii conversion by pulling from canvas data (without png conversion)
2120
* - backbuffer screen updates based on network latency?
2221
* - try rendering to terminal using drawille / braille characters
2322
* - make this runs with more examples! (preferably by automating most stuff)
2423
* - add docopts to configure parameters (scale, renderers)
2524
* - support webgl and software renderers
26-
* - add key controls to adjust parameters inside the terminal
25+
* - add key controls to adjust parameters inside the terminal (scaling, screenshot, stats, ascii characters)
2726
* - try ttystudio
2827
* - profit? :D
2928
*
@@ -41,17 +40,18 @@ const { FPSCounter, MemCounter } = require('./counters');
4140
* - Dom Polyfilling
4241
* - TerminalRenderer
4342
* - add nice fps graphs
43+
* - optimize ascii conversion by pulling from canvas data (without png conversion)
4444
*
4545
* Also see,
4646
* https://threejs.org/examples/canvas_ascii_effect.html
4747
* https://github.com/mrdoob/three.js/issues/7085
4848
* https://github.com/mrdoob/three.js/issues/2182
4949
*/
5050

51-
let y_scale = 2;
52-
let rendering_scale = 0.15;
53-
width = 640 * rendering_scale;
54-
height = 480 * rendering_scale;
51+
let y_scale = 1.23; // pixel ratio of a single terminal character height / width
52+
let pixel_sampling = 1; // mulitplier of target pixels to actual canvas render size
53+
width = 100;
54+
height = y_scale * 50;
5555

5656
// Create a screen object.
5757
const screen = blessed.screen({
@@ -73,7 +73,7 @@ const screen = blessed.screen({
7373
screen.title = 'Three.js Terminal';
7474

7575
// placeholder for renderering
76-
const canvas = blessed.image({
76+
const canvas = blessed.box({ // box image
7777
parent: screen,
7878
top: 0,
7979
left: 0,
@@ -133,6 +133,32 @@ screen.key(['escape', 'q', 'C-c'], function(ch, key) {
133133
return process.exit(0);
134134
});
135135

136+
mode = 0;
137+
138+
screen.key(['m'], function(ch, key) {
139+
mode = ++mode % 4;
140+
let options = {};
141+
switch (mode) {
142+
case 0:
143+
options.plain_formatting = true;
144+
break;
145+
case 1:
146+
options.plain_formatting = false;
147+
options.bg_formatting = true;
148+
options.ascii_formatting = false;
149+
break;
150+
case 2:
151+
options.bg_formatting = true;
152+
options.ascii_formatting = true;
153+
break;
154+
case 3:
155+
options.bg_formatting = false;
156+
options.ascii_formatting = true;
157+
break;
158+
}
159+
renderer.setAnsiOptions(options);
160+
});
161+
136162
// Focus our element.
137163
canvas.focus();
138164
box.on('click', clearlog);
@@ -150,24 +176,28 @@ function init() {
150176
controls.panSpeed *= 4;
151177

152178
renderer = new THREE.TerminalRenderer(canvas);
153-
renderer.setClearColor( 0xf0f0f0 );
179+
// renderer.setClearColor( 0xf0f0f0 );
180+
renderer.setClearColor( 0xffffff );
154181

155182
function onResize(res) {
156183
if (!res) {
157-
resize(screen.width, screen.height * y_scale);
184+
setSize(screen.width, screen.height * y_scale);
158185
return;
159186
}
160-
log(`Resized ${screen.program.columns}, ${screen.program.rows}`);
187+
screen.debug(`Resized ${screen.program.columns}, ${screen.program.rows}`);
161188
const fontWidth = res.width / screen.width;
162189
const fontHeight = res.height / screen.height;
163190
y_scale = fontHeight / fontWidth;
164-
log(`Estimated font size ${fontWidth.toFixed(3)}x${fontHeight.toFixed(3)}, ratio ${y_scale.toFixed(3)}`);
191+
screen.debug(`Estimated font size ${fontWidth.toFixed(3)}x${fontHeight.toFixed(3)}, ratio ${y_scale.toFixed(3)}`);
192+
193+
const actual_screen_ratio = res.height / res.width;
165194

166-
width = res.width * rendering_scale | 0;
167-
height = res.height * rendering_scale | 0;
168-
log(`Rendering using ${width}x${height}px`);
195+
// target pixels to render
196+
width = screen.width;
197+
height = screen.width * actual_screen_ratio;
198+
screen.debug(`Rendering using ${width}x${height}px`);
169199

170-
resize(width, height);
200+
setSize(width, height);
171201
}
172202

173203
window.addEventListener('resize', onResize);
@@ -182,6 +212,8 @@ function render() {
182212
sphere.rotation.x = start * 0.0003;
183213
sphere.rotation.z = start * 0.0002;
184214

215+
scene.rotation.y += 0.005;
216+
185217
controls.update();
186218

187219
// Render
@@ -234,14 +266,14 @@ setInterval( () => {
234266
);
235267
}, 1000);
236268

237-
function resize(w, h) {
269+
function setSize(w, h) {
270+
width = w * pixel_sampling | 0;
271+
height = h * pixel_sampling | 0;
238272
// screen.debug('resizing', w, h, screen.width, screen.height);
239273
controls.handleResize();
240-
width = w;
241-
height = h;
242-
camera.aspect = w / h;
274+
camera.aspect = width / height;
243275
camera.updateProjectionMatrix();
244-
renderer.setSize(w, h);
276+
renderer.setSize(width, height);
245277
}
246278

247279
function saveCanvas() {

scene.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require('three/examples/js/geometries/TeapotBufferGeometry')
2+
13
const scene = new THREE.Scene();
24
const light1 = new THREE.PointLight( 0xffffff );
35
light1.position.set( 500, 500, 500 );
@@ -15,4 +17,7 @@ plane.position.y = - 200;
1517
plane.rotation.x = - Math.PI / 2;
1618
scene.add( plane );
1719

20+
teapot = require('./teapot');
21+
// scene.add( teapot );
22+
1823
module.exports = { scene };

0 commit comments

Comments
 (0)