diff --git a/README.md b/README.md index f270c2e..61d797b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ No web browser around? No worries - discover the planet in your console! ## Behind the scenes ### Libraries #### Mastering the console - * [`drawille-canvas-blessed-contrib`](https://github.com/yaronn/drawille-canvas-blessed-contrib/) for [braille](http://www.fileformat.info/info/unicode/block/braille_patterns/utf8test.htm) rendering * [`x256`](https://github.com/substack/node-x256) for finding nearest xterm-256 [color codes](https://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg) * [`term-mouse`](https://github.com/CoderPuppy/term-mouse) for mouse handling * [`keypress`](https://github.com/TooTallNate/keypress) for input handling @@ -38,9 +37,10 @@ No web browser around? No worries - discover the planet in your console! * [`vector-tile-js`](https://github.com/mapbox/vector-tile-js) for [VectorTile](https://github.com/mapbox/vector-tile-spec/tree/master/2.1) parsing #### Juggling the vectors and numbers -* [`pnltri`](https://github.com/jahting/pnltri.js) for polygon triangulation to draw 'em filled +* [`earcut`](https://github.com/mapbox/earcut) for polygon triangulation * [`rbush`](https://github.com/mourner/rbush) for 2D spatial indexing based label and mouse collision detection * [`gl-matrix`](https://github.com/toji/gl-matrix) for vector and matrix operations +* [`breseham`](https://github.com/madbence/node-bresenham) for line calculations * [`sphericalmercator`](https://github.com/mapbox/node-sphericalmercator) for [EPSG:3857](http://spatialreference.org/ref/sr-org/6864/) <> [WGS84](http://spatialreference.org/ref/epsg/wgs-84/) conversions ### TODOs diff --git a/package.json b/package.json index d554ee9..aff8bc8 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,12 @@ "author": "Michael Straßburger ", "license": "MIT", "dependencies": { + "bresenham": "0.0.4", "coffee-script": "^1.10.0", - "drawille-canvas-blessed-contrib": "git+https://github.com/rastapasta/drawille-canvas-blessed-contrib.git", + "earcut": "^2.1.1", "gl-matrix": "^2.3.2", "keypress": "^0.2.1", "pbf": "^3.0.0", - "pnltri": "^2.1.1", "rbush": "^2.0.1", "sphericalmercator": "^1.0.5", "term-mouse": "^0.1.1", diff --git a/src/BrailleBuffer.coffee b/src/BrailleBuffer.coffee index 15a7cd5..4b74166 100644 --- a/src/BrailleBuffer.coffee +++ b/src/BrailleBuffer.coffee @@ -5,8 +5,9 @@ Simple pixel to barille character mapper Implementation inspired by node-drawille (https://github.com/madbence/node-drawille) - * added written text support * added color support + * added support for filled polygons + * added text label support * general optimizations -> more bit shifting/operations, less Math.floors @@ -78,12 +79,5 @@ module.exports = class BrailleBuffer @colorBuffer[idx] = @termColor color writeText: (text, x, y, color, center = true) -> - x -= text.length/2 if center + x -= text.length/2+1 if center @setChar text.charAt(i), x+i*2, y, color for i in [0...text.length] - -# buffer = new BrailleBuffer 100, 16 -# for i in [0...255] -# buffer.setPixel i, 8+8*Math.cos(i/10*Math.PI/2), i -# -# buffer.writeText "ruth", 10, 0, 111 -# console.log buffer.frame() diff --git a/src/Canvas.coffee b/src/Canvas.coffee index 6caf625..ced7bb7 100644 --- a/src/Canvas.coffee +++ b/src/Canvas.coffee @@ -2,40 +2,107 @@ termap - Terminal Map Viewer by Michael Strassburger - Extends drawille-canvas to add additional drawing functions. - To be PRed up the tree at some point. + Simple pixel to barille character mapper + + Implementation inspired by node-drawille-canvas (https://github.com/madbence/node-drawille-canvas) + * added support for filled polygons + * improved text rendering + + Will most likely be turned into a stand alone module at some point ### -BlessedCanvas = require 'drawille-canvas-blessed-contrib' -vec2 = require('gl-matrix').vec2 +bresenham = require 'bresenham' +glMatrix = require 'gl-matrix' +earcut = require 'earcut' -module.exports = class Canvas extends BlessedCanvas +BrailleBuffer = require './BrailleBuffer' - # bresenham: (from, to) -> - # points = [] - # adx = Math.abs dx = to.x - from.x - # ady = Math.abs dy = to.y - from.y - # eps = 0 - # sx = if dx > 0 then 1 else -1 - # sy = if dy > 0 then 1 else -1 - # - # [x, y] = from - # if adx > ady - # while if sx < 0 then x >= x1 else x <= x1 - # points.add x:, y: y - # eps += ady - # if eps<<1 >= adx - # y += sy - # eps -= adx - # - # x += sx - # else - # while if sy < 0 then y >= y1 else y <= y1 - # fn(x, y); - # eps += adx; - # if eps<<1 >= ady - # x += sx; - # eps -= ady; - # - # y += sy - # arr +vec2 = glMatrix.vec2 +mat2d = glMatrix.mat2d + +module.exports = class Canvas + matrix: null + + constructor: (@width, @height) -> + @buffer = new BrailleBuffer @width, @height + @reset() + + reset: -> + @matrix = mat2d.create() + + print: -> + process.stdout.write @buffer.frame() + + translate: (x, y) -> + mat2d.translate @matrix, @matrix, vec2.fromValues(x, y) + + clear: -> + @buffer.clear() + + text: (text, x, y, color, center = true) -> + @buffer.writeText text, x, y, color, center + + polyline: (points, color) -> + projected = (@_project point[0], point[1] for point in points) + for i in [1...projected.length] + bresenham projected[i-1]..., projected[i]..., + (x, y) => @buffer.setPixel x, y, color + + # TODO: support for polygon holes + polygon: (points, color) -> + vertices = [] + for point in points + vertices = vertices.concat @_project point[0], point[1] + + triangles = earcut vertices + extract = (pointId) -> + [vertices[pointId*2], vertices[pointId*2+1]] + + for i in [0...triangles.length] by 3 + @_filledTriangle extract(triangles[i]), extract(triangles[i+1]), extract(triangles[i+2]), color + + _bresenham: (pointA, pointB) -> + bresenham pointA[0], pointA[1], + pointB[0], pointB[1] + + _project: (x, y) -> + point = vec2.transformMat2d vec2.create(), vec2.fromValues(x, y), @matrix + [Math.floor(point[0]), Math.floor(point[1])] + + _filledRectangle: (x, y, width, height, color) -> + pointA = @_project x, y + pointB = @_project x+width, y + pointC = @_project x, y+height + pointD = @_project x+width, y+height + + @_filledTriangle pointA, pointB, pointC, color + @_filledTriangle pointC, pointB, pointD, color + + # Draws a filled triangle + _filledTriangle: (pointA, pointB, pointC, color) -> + a = @_bresenham pointB, pointC + b = @_bresenham pointA, pointC + c = @_bresenham pointA, pointB + + # Filter out any points outside of the visible area + # TODO: benchmark - is it more effective to filter double points, or does + # it req more computing time than actually setting points multiple times? + + last = null + points = a.concat(b).concat(c) + .filter (point) => 0 <= point.y < @height + .sort (a, b) -> if a.y is b.y then a.x - b.x else a.y-b.y + .filter (point) -> + [lastPoint, last] = [last, point] + not lastPoint or lastPoint.x isnt point.x or lastPoint.y isnt point.y + + for i, point of points + next = points[i*1+1] + + if point.y is next?.y + left = Math.max 0, point.x + right = Math.min @width, next?.x + @buffer.setPixel x, point.y, color for x in [left..right] + + else + @buffer.setPixel point.x, point.y, color diff --git a/src/Renderer.coffee b/src/Renderer.coffee index 7768c6b..220da4a 100644 --- a/src/Renderer.coffee +++ b/src/Renderer.coffee @@ -5,7 +5,6 @@ The Console Vector Tile renderer - bäm! ### x256 = require 'x256' -triangulator = new (require('pnltri')).Triangulator() Canvas = require './Canvas' LabelBuffer = require './LabelBuffer' @@ -17,7 +16,8 @@ module.exports = class Renderer fillPolygons: true language: 'de' - drawOrder: ["admin", "building", "road", "water", "poi_label", "place_label", "housenum_label"] + #"poi_label", "housenum_label", "water", + drawOrder: ["admin", "building", "road", "place_label"] icons: car: "🚗" @@ -77,14 +77,13 @@ module.exports = class Renderer # @canvas.strokeStyle = x256 utils.hex2rgb(color)... # @canvas.fillRect 0, 0, @width, @height # else - @canvas.clearRect 0, 0, @width, @height + @canvas.clear() + @canvas.reset() - @canvas.save() @canvas.translate @view[0], @view[1] @_drawLayers() - @canvas.restore() - @_write @canvas._canvas.frame() + @canvas.print() @isDrawing = false @@ -114,9 +113,7 @@ module.exports = class Renderer return false toDraw = for points in feature.points - for point in points - x: point.x/scale - y: point.y/scale + [point.x/scale, point.y/scale] for point in points color = style.paint['line-color'] or style.paint['fill-color'] or style.paint['text-color'] @@ -124,16 +121,15 @@ module.exports = class Renderer if color instanceof Object color = color.stops[0][1] - @canvas.fillStyle = @canvas.strokeStyle = x256 utils.hex2rgb color + colorCode = x256 utils.hex2rgb color switch feature.type when "LineString" - @_drawWithLines points for points in toDraw + @canvas.polyline points, colorCode for points in toDraw true when "Polygon" - unless @config.fillPolygons and @_drawWithTriangles toDraw - @_drawWithLines points for points in toDraw + @canvas.polygon toDraw[0], colorCode true when "Point" @@ -150,37 +146,11 @@ module.exports = class Renderer x = point.x - text.length #continue if x-@view[0] < 0 if @labelBuffer.writeIfPossible text, x, point.y - @canvas.fillText text, x, point.y + @canvas.text text, x, point.y, colorCode wasDrawn = true wasDrawn - _drawWithTriangles: (points) -> - try - triangles = triangulator.triangulate_polygon points - catch - return false - - return false unless triangles.length - - # TODO: triangles are returned as vertex references to a flattened input. - # optimize it! - - arr = points.reduce (a, b) -> a.concat b - for triangle in triangles - try - @canvas.fillTriangle arr[triangle[0]], arr[triangle[1]], arr[triangle[2]] - catch - return false - true - - _drawWithLines: (points) -> - @canvas.beginPath() - first = points.shift() - @canvas.moveTo first.x, first.y - @canvas.lineTo point.x, point.y for point in points - @canvas.stroke() - _isOnScreen: (point) -> point.x+@view[0]>=4 and point.x+@view[0]<@width-4 and