📐 using earcut for triangulation, implementing own Canvas

This commit is contained in:
Michael Straßburger 2016-09-27 13:57:59 +02:00
parent a3fb7149f2
commit 06528f488d
5 changed files with 117 additions and 86 deletions

View File

@ -27,7 +27,6 @@ No web browser around? No worries - discover the planet in your console!
## Behind the scenes ## Behind the scenes
### Libraries ### Libraries
#### Mastering the console #### 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) * [`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 * [`term-mouse`](https://github.com/CoderPuppy/term-mouse) for mouse handling
* [`keypress`](https://github.com/TooTallNate/keypress) for input 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 * [`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 #### 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 * [`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 * [`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 * [`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 ### TODOs

View File

@ -20,12 +20,12 @@
"author": "Michael Straßburger <codepoet@cpan.org>", "author": "Michael Straßburger <codepoet@cpan.org>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bresenham": "0.0.4",
"coffee-script": "^1.10.0", "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", "gl-matrix": "^2.3.2",
"keypress": "^0.2.1", "keypress": "^0.2.1",
"pbf": "^3.0.0", "pbf": "^3.0.0",
"pnltri": "^2.1.1",
"rbush": "^2.0.1", "rbush": "^2.0.1",
"sphericalmercator": "^1.0.5", "sphericalmercator": "^1.0.5",
"term-mouse": "^0.1.1", "term-mouse": "^0.1.1",

View File

@ -5,8 +5,9 @@
Simple pixel to barille character mapper Simple pixel to barille character mapper
Implementation inspired by node-drawille (https://github.com/madbence/node-drawille) Implementation inspired by node-drawille (https://github.com/madbence/node-drawille)
* added written text support
* added color support * added color support
* added support for filled polygons
* added text label support
* general optimizations * general optimizations
-> more bit shifting/operations, less Math.floors -> more bit shifting/operations, less Math.floors
@ -78,12 +79,5 @@ module.exports = class BrailleBuffer
@colorBuffer[idx] = @termColor color @colorBuffer[idx] = @termColor color
writeText: (text, x, y, color, center = true) -> 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] @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()

View File

@ -2,40 +2,107 @@
termap - Terminal Map Viewer termap - Terminal Map Viewer
by Michael Strassburger <codepoet@cpan.org> by Michael Strassburger <codepoet@cpan.org>
Extends drawille-canvas to add additional drawing functions. Simple pixel to barille character mapper
To be PRed up the tree at some point.
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' bresenham = require 'bresenham'
vec2 = require('gl-matrix').vec2 glMatrix = require 'gl-matrix'
earcut = require 'earcut'
module.exports = class Canvas extends BlessedCanvas BrailleBuffer = require './BrailleBuffer'
# bresenham: (from, to) -> vec2 = glMatrix.vec2
# points = [] mat2d = glMatrix.mat2d
# adx = Math.abs dx = to.x - from.x
# ady = Math.abs dy = to.y - from.y module.exports = class Canvas
# eps = 0 matrix: null
# sx = if dx > 0 then 1 else -1
# sy = if dy > 0 then 1 else -1 constructor: (@width, @height) ->
# @buffer = new BrailleBuffer @width, @height
# [x, y] = from @reset()
# if adx > ady
# while if sx < 0 then x >= x1 else x <= x1 reset: ->
# points.add x:, y: y @matrix = mat2d.create()
# eps += ady
# if eps<<1 >= adx print: ->
# y += sy process.stdout.write @buffer.frame()
# eps -= adx
# translate: (x, y) ->
# x += sx mat2d.translate @matrix, @matrix, vec2.fromValues(x, y)
# else
# while if sy < 0 then y >= y1 else y <= y1 clear: ->
# fn(x, y); @buffer.clear()
# eps += adx;
# if eps<<1 >= ady text: (text, x, y, color, center = true) ->
# x += sx; @buffer.writeText text, x, y, color, center
# eps -= ady;
# polyline: (points, color) ->
# y += sy projected = (@_project point[0], point[1] for point in points)
# arr 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

View File

@ -5,7 +5,6 @@
The Console Vector Tile renderer - bäm! The Console Vector Tile renderer - bäm!
### ###
x256 = require 'x256' x256 = require 'x256'
triangulator = new (require('pnltri')).Triangulator()
Canvas = require './Canvas' Canvas = require './Canvas'
LabelBuffer = require './LabelBuffer' LabelBuffer = require './LabelBuffer'
@ -17,7 +16,8 @@ module.exports = class Renderer
fillPolygons: true fillPolygons: true
language: 'de' 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: icons:
car: "🚗" car: "🚗"
@ -77,14 +77,13 @@ module.exports = class Renderer
# @canvas.strokeStyle = x256 utils.hex2rgb(color)... # @canvas.strokeStyle = x256 utils.hex2rgb(color)...
# @canvas.fillRect 0, 0, @width, @height # @canvas.fillRect 0, 0, @width, @height
# else # else
@canvas.clearRect 0, 0, @width, @height @canvas.clear()
@canvas.reset()
@canvas.save()
@canvas.translate @view[0], @view[1] @canvas.translate @view[0], @view[1]
@_drawLayers() @_drawLayers()
@canvas.restore()
@_write @canvas._canvas.frame() @canvas.print()
@isDrawing = false @isDrawing = false
@ -114,9 +113,7 @@ module.exports = class Renderer
return false return false
toDraw = for points in feature.points toDraw = for points in feature.points
for point in points [point.x/scale, point.y/scale] for point in points
x: point.x/scale
y: point.y/scale
color = style.paint['line-color'] or style.paint['fill-color'] or style.paint['text-color'] 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 if color instanceof Object
color = color.stops[0][1] color = color.stops[0][1]
@canvas.fillStyle = @canvas.strokeStyle = x256 utils.hex2rgb color colorCode = x256 utils.hex2rgb color
switch feature.type switch feature.type
when "LineString" when "LineString"
@_drawWithLines points for points in toDraw @canvas.polyline points, colorCode for points in toDraw
true true
when "Polygon" when "Polygon"
unless @config.fillPolygons and @_drawWithTriangles toDraw @canvas.polygon toDraw[0], colorCode
@_drawWithLines points for points in toDraw
true true
when "Point" when "Point"
@ -150,37 +146,11 @@ module.exports = class Renderer
x = point.x - text.length x = point.x - text.length
#continue if x-@view[0] < 0 #continue if x-@view[0] < 0
if @labelBuffer.writeIfPossible text, x, point.y if @labelBuffer.writeIfPossible text, x, point.y
@canvas.fillText text, x, point.y @canvas.text text, x, point.y, colorCode
wasDrawn = true wasDrawn = true
wasDrawn 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) -> _isOnScreen: (point) ->
point.x+@view[0]>=4 and point.x+@view[0]>=4 and
point.x+@view[0]<@width-4 and point.x+@view[0]<@width-4 and