diff --git a/src/Canvas.coffee b/src/Canvas.coffee deleted file mode 100644 index af12beb..0000000 --- a/src/Canvas.coffee +++ /dev/null @@ -1,161 +0,0 @@ -### - termap - Terminal Map Viewer - by Michael Strassburger - - Canvas-like painting abstraction for BrailleBuffer - - 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 -### - -bresenham = require 'bresenham' -simplify = require 'simplify-js' - -earcut = require 'earcut' -BrailleBuffer = require './BrailleBuffer' -utils = require './utils' - -module.exports = class Canvas - stack: [] - - constructor: (@width, @height) -> - @buffer = new BrailleBuffer @width, @height - - frame: -> - @buffer.frame() - - clear: -> - @buffer.clear() - - text: (text, x, y, color, center = false) -> - @buffer.writeText text, x, y, color, center - - line: (from, to, color, width = 1) -> - @_line from.x, from.y, to.x, to.y, color, width - - polyline: (points, color, width = 1) -> - for i in [1...points.length] - @_line points[i-1].x, points[i-1].y, points[i].x, points[i].y, width, color - - setBackground: (color) -> - @buffer.setGlobalBackground color - - background: (x, y, color) -> - @buffer.setBackground x, y, color - - polygon: (rings, color) -> - vertices = [] - holes = [] - - for ring in rings - if vertices.length - continue if ring.length < 3 - holes.push vertices.length/2 - else - return false if ring.length < 3 - - for point in ring - vertices.push point.x - vertices.push point.y - - try - triangles = earcut vertices, holes - catch e - return false - - for i in [0...triangles.length] by 3 - pa = @_polygonExtract vertices, triangles[i] - pb = @_polygonExtract vertices, triangles[i+1] - pc = @_polygonExtract vertices, triangles[i+2] - - @_filledTriangle pa, pb, pc, color - - true - - _polygonExtract: (vertices, pointId) -> - [vertices[pointId*2], vertices[pointId*2+1]] - - # Inspired by Alois Zingl's "The Beauty of Bresenham's Algorithm" - # -> http://members.chello.at/~easyfilter/bresenham.html - _line: (x0, y0, x1, y1, width, color) -> - - # Fall back to width-less bresenham algorithm if we dont have a width - unless width = Math.max 0, width-1 - return bresenham x0, y0, x1, y1, - (x, y) => @buffer.setPixel x, y, color - - dx = Math.abs x1-x0 - sx = if x0 < x1 then 1 else -1 - dy = Math.abs y1-y0 - sy = if y0 < y1 then 1 else -1 - - err = dx-dy - - ed = if dx+dy is 0 then 1 else Math.sqrt dx*dx+dy*dy - - width = (width+1)/2 - loop - @buffer.setPixel x0, y0, color - e2 = err - x2 = x0 - - if 2*e2 >= -dx - e2 += dy - y2 = y0 - while e2 < ed*width && (y1 != y2 || dx > dy) - @buffer.setPixel x0, y2 += sy, color - e2 += dx - break if x0 is x1 - e2 = err - err -= dy - x0 += sx - - if 2*e2 <= dy - e2 = dx-e2 - while e2 < ed*width && (x1 != x2 || dx < dy) - @buffer.setPixel x2 += sx, y0, color - e2 += dy - break if y0 is y1 - err += dx - y0 += sy - - _filledRectangle: (x, y, width, height, color) -> - pointA = [x, y] - pointB = [x+width, y] - pointC = [x, y+height] - pointD = [x+width, y+height] - - @_filledTriangle pointA, pointB, pointC, color - @_filledTriangle pointC, pointB, pointD, color - - _bresenham: (pointA, pointB) -> - bresenham pointA[0], pointA[1], - pointB[0], pointB[1] - - # Draws a filled triangle - _filledTriangle: (pointA, pointB, pointC, color) -> - a = @_bresenham pointB, pointC - b = @_bresenham pointA, pointC - c = @_bresenham pointA, pointB - - 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 - - for i in [0...points.length] - point = points[i] - next = points[i*1+1] - - if point.y is next?.y - left = Math.max 0, point.x - right = Math.min @width-1, next.x - if left >= 0 and right <= @width - @buffer.setPixel x, point.y, color for x in [left..right] - - else - @buffer.setPixel point.x, point.y, color - - break unless next diff --git a/src/Canvas.js b/src/Canvas.js new file mode 100644 index 0000000..50be2cd --- /dev/null +++ b/src/Canvas.js @@ -0,0 +1,200 @@ +/* + termap - Terminal Map Viewer + by Michael Strassburger + + Canvas-like painting abstraction for BrailleBuffer + + 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 + */ +'use strict'; +const bresenham = require('bresenham'); +const earcut = require('earcut'); +const BrailleBuffer = require('./BrailleBuffer'); + +class Canvas { + constructor(width, height) { + this.width = width; + this.height = height; + this.buffer = new BrailleBuffer(width, height); + } + + frame() { + return this.buffer.frame(); + } + + clear() { + this.buffer.clear(); + } + + text(text, x, y, color, center = false) { + this.buffer.writeText(text, x, y, color, center); + } + + line(from, to, color, width = 1) { + this._line(from.x, from.y, to.x, to.y, color, width); + } + + polyline(points, color, width = 1) { + for (let i = 1; i < points.length; i++) { + const x1 = points[i - 1].x; + const y1 = points[i - 1].y; + this._line(x1, y1, points[i].x, points[i].y, width, color); + } + } + + setBackground(color) { + this.buffer.setGlobalBackground(color); + } + + background(x, y, color) { + this.buffer.setBackground(x, y, color); + } + + polygon(rings, color) { + const vertices = []; + const holes = []; + for (const ring of rings) { + if (vertices.length) { + if (ring.length < 3) continue; + holes.push(vertices.length / 2); + } else { + if (ring.length < 3) return false; + } + for (const point of ring) { + vertices.push(point.x); + vertices.push(point.y); + } + } + + let triangles; + try { + triangles = earcut(vertices, holes); + } catch (error) { + return false; + } + for (let i = 0; i < triangles.length; i += 3) { + const pa = this._polygonExtract(vertices, triangles[i]); + const pb = this._polygonExtract(vertices, triangles[i + 1]); + const pc = this._polygonExtract(vertices, triangles[i + 2]); + this._filledTriangle(pa, pb, pc, color); + } + return true; + } + + _polygonExtract(vertices, pointId) { + return [vertices[pointId * 2], vertices[pointId * 2 + 1]]; + } + + // Inspired by Alois Zingl's "The Beauty of Bresenham's Algorithm" + // -> http://members.chello.at/~easyfilter/bresenham.html + _line(x0, y0, x1, y1, width, color) { + // Fall back to width-less bresenham algorithm if we dont have a width + if (!(width = Math.max(0, width - 1))) { + return bresenham(x0, y0, x1, y1, (x, y) => { + return this.buffer.setPixel(x, y, color); + }); + } + + const dx = Math.abs(x1 - x0); + const sx = x0 < x1 ? 1 : -1; + const dy = Math.abs(y1 - y0); + const sy = y0 < y1 ? 1 : -1; + + let err = dx - dy; + + const ed = dx + dy === 0 ? 1 : Math.sqrt(dx * dx + dy * dy); + + width = (width + 1) / 2; + + while (true) { + this.buffer.setPixel(x0, y0, color); + let e2 = err; + let x2 = x0; + if (2 * e2 >= -dx) { + e2 += dy; + let y2 = y0; + while (e2 < ed * width && (y1 !== y2 || dx > dy)) { + this.buffer.setPixel(x0, y2 += sy, color); + e2 += dx; + } + if (x0 === x1) { + break; + } + e2 = err; + err -= dy; + x0 += sx; + } + if (2 * e2 <= dy) { + e2 = dx - e2; + while (e2 < ed * width && (x1 !== x2 || dx < dy)) { + this.buffer.setPixel(x2 += sx, y0, color); + e2 += dy; + } + if (y0 === y1) { + break; + } + err += dx; + y0 += sy; + } + } + } + + _filledRectangle(x, y, width, height, color) { + const pointA = [x, y]; + const pointB = [x + width, y]; + const pointC = [x, y + height]; + const pointD = [x + width, y + height]; + this._filledTriangle(pointA, pointB, pointC, color); + this._filledTriangle(pointC, pointB, pointD, color); + } + + _bresenham(pointA, pointB) { + return bresenham(pointA[0], pointA[1], pointB[0], pointB[1]); + } + + // Draws a filled triangle + _filledTriangle(pointA, pointB, pointC, color) { + const a = this._bresenham(pointB, pointC); + const b = this._bresenham(pointA, pointC); + const c = this._bresenham(pointA, pointB); + + const points = a.concat(b).concat(c).filter((point) => { + var ref; + return (0 <= (ref = point.y) && ref < this.height); + }).sort(function(a, b) { + if (a.y === b.y) { + return a.x - b.x; + } else { + return a.y - b.y; + } + }); + + for (let i = 0; i < points.length; i++) { + const point = points[i]; + const next = points[i * 1 + 1]; + + if (point.y === (next || {}).y) { + const left = Math.max(0, point.x); + const right = Math.min(this.width - 1, next.x); + if (left >= 0 && right <= this.width) { + for (let x = left; x <= right; x++) { + this.buffer.setPixel(x, point.y, color); + } + } + } else { + this.buffer.setPixel(point.x, point.y, color); + } + if (!next) { + break; + } + } + } +} + +Canvas.prototype.stack = []; + +module.exports = Canvas;