pclkit
Version:
A PCL generation library for Node.js
469 lines (388 loc) • 11.8 kB
JavaScript
var SVGPath = require('../path');
// This constant is used to approximate a symmetrical arc using a cubic
// Bezier curve.
var KAPPA = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
module.exports = {
initVector: function() {
this._vectors = [];
this._lineWidth = 0.3;
this._ctm = [1, 0, 0, 1, 0, 0];
this._ctmStack = [];
this._lastMove = [0,0];
this._pos = [0, 0];
},
_normalizeCoord: function(x, y) {
x = x || 0;
y = y || 0;
// apply transformations
var m11 = this._ctm[0];
var m12 = this._ctm[1];
var m21 = this._ctm[2];
var m22 = this._ctm[3];
var m31 = this._ctm[4];
var m32 = this._ctm[5];
x = m11 * x + m21 * y + m31;
y = m12 * x + m22 * y + m32;
//y = this.Ymax - y;
return [x, y];
},
save: function() {
this._ctmStack.push(this._ctm.slice());
return this;
},
restore: function() {
this._ctm = this._ctmStack.pop() || [1, 0, 0, 1, 0, 0];
this.transform(1, 0, 0, 1, 0, 0);
return this;
},
closePath: function() {
return this.lineTo(this._lastMove[0], this._lastMove[1]);
},
lineWidth: function(w) {
// HP/GL line width is in millimeter
this._lineWidth = w * 0.3;
return this;
},
_CAP_STYLES: {
BUTT: 1,
SQUARE: 2,
TRIANGULAR: 3,
ROUND: 4
},
_lineCap: function(c) {
this._writeHPGL('LA1,' + c + ';');
},
lineCap: function(c) {
if (typeof c === 'string') {
c = this._CAP_STYLES[c.toUpperCase()];
}
this._vectors.push(['lineCap', c]);
return this;
},
_JOIN_STYLES: {
MITER: 1,
"MITER/BEVEL": 2,
TRIANGULAR: 3,
ROUND: 4,
BEVEL: 5,
NONE: 6
},
_lineJoin: function(j) {
this._writeHPGL('LA2,' + j + ';');
},
lineJoin: function(j) {
if (typeof j === 'string') {
j = this._JOIN_STYLES[j.toUpperCase()];
}
this._vectors.push(['lineJoin', j]);
return this;
},
_miterLimit: function(m) {
this._writeHPGL('LA3,' + m + ';');
},
miterLimit: function(m) {
this._vectors.push(['miterLimit', m]);
return this;
},
_dash: function(length, options) {
this._writeHPGL('LT2,' + length / 2 + ',1;');
},
dash: function(length, options) {
this._vectors.push(['dash', length, options]);
return this;
},
_undash: function() {
this._writeHPGL('LT;');
},
undash: function() {
this._vectors.push(['undash']);
return this;
},
moveTo: function(x, y) {
var coord = this._normalizeCoord(x, y);
this._vectors.push(['moveTo', coord]);
this._lastMove = this._pos = [x , y];
return this;
},
lineTo: function(x, y) {
var coord = this._normalizeCoord(x, y);
this._vectors.push(['lineTo', coord]);
this._pos = [x , y];
return this;
},
bezierCurveTo: function(cp1x, cp1y, cp2x, cp2y, x, y) {
var cp1 = this._normalizeCoord(cp1x, cp1y);
var cp2 = this._normalizeCoord(cp2x, cp2y);
var coord = this._normalizeCoord(x, y);
this._vectors.push(['bezierCurveTo', cp1, cp2, coord]);
this._pos = [x , y];
return this;
},
quadraticCurveTo: function(cpx, cpy, x, y) {
var cp1x = (this._pos[0] + 2 * cpx) / 3;
var cp1y = (this._pos[1] + 2 * cpy) / 3;
var cp2x = (x + 2 * cpx) / 3;
var cp2y = (y + 2 * cpy) / 3;
return this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
},
rect: function(x, y, w, h) {
this.moveTo(x, y);
this.lineTo(x + w, y);
this.lineTo(x + w, y + h);
this.lineTo(x, y + h);
this.lineTo(x, y);
return this;
},
roundedRect: function(x, y, w, h, r) {
if (r == null) {
r = 0;
}
r = Math.min(r, 0.5 * w, 0.5 * h);
// amount to inset control points from corners (see `ellipse`)
const c = r * (1.0 - KAPPA);
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.bezierCurveTo(x + w - c, y, x + w, y + c, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.bezierCurveTo(x + w, y + h - c, x + w - c, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.bezierCurveTo(x + c, y + h, x, y + h - c, x, y + h - r);
this.lineTo(x, y + r);
this.bezierCurveTo(x, y + c, x + c, y, x + r, y);
return this.closePath();
},
ellipse: function(x, y, r1, r2) {
// based on http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas/2173084#2173084
r2 = r2 || r1;
x -= r1;
y -= r2;
var ox = r1 * KAPPA;
var oy = r2 * KAPPA;
var xe = x + r1 * 2;
var ye = y + r2 * 2;
var xm = x + r1;
var ym = y + r2;
this.moveTo(x, ym);
this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
return this.closePath();
},
circle: function(x, y, radius) {
return this.ellipse(x, y, radius);
},
arc: function(x, y, radius, startAngle, endAngle, anticlockwise) {
anticlockwise = anticlockwise || false;
var TWO_PI = 2.0 * Math.PI;
var HALF_PI = 0.5 * Math.PI;
var deltaAng = endAngle - startAngle;
if (Math.abs(deltaAng) > TWO_PI) {
// draw only full circle if more than that is specified
deltaAng = TWO_PI;
} else if (deltaAng !== 0 && anticlockwise !== deltaAng < 0) {
// necessary to flip direction of rendering
var dir = anticlockwise ? -1 : 1;
deltaAng = dir * TWO_PI + deltaAng;
}
var numSegs = Math.ceil(Math.abs(deltaAng) / HALF_PI);
var segAng = deltaAng / numSegs;
var handleLen = (segAng / HALF_PI) * KAPPA * radius;
var curAng = startAngle;
// component distances between anchor point and control point
var deltaCx = -Math.sin(curAng) * handleLen;
var deltaCy = Math.cos(curAng) * handleLen;
// anchor point
var ax = x + Math.cos(curAng) * radius;
var ay = y + Math.sin(curAng) * radius;
// calculate and render segments
this.moveTo(ax, ay);
for (
var segIdx = 0, end = numSegs, asc = 0 <= end;
asc ? segIdx < end : segIdx > end;
asc ? segIdx++ : segIdx--
) {
// starting control point
var cp1x = ax + deltaCx;
var cp1y = ay + deltaCy;
// step angle
curAng += segAng;
// next anchor point
ax = x + Math.cos(curAng) * radius;
ay = y + Math.sin(curAng) * radius;
// next control point delta
deltaCx = -Math.sin(curAng) * handleLen;
deltaCy = Math.cos(curAng) * handleLen;
// ending control point
var cp2x = ax - deltaCx;
var cp2y = ay - deltaCy;
// render segment
this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, ax, ay);
}
return this;
},
polygon: function() {
var self = this;
var points = Array.prototype.slice.call(arguments);
var mv = points.shift();
this.moveTo(mv[0], mv[1]);
points.forEach(function(point) {
self.lineTo(point[0], point[1]);
});
return this.closePath();
},
path: function(path) {
SVGPath.apply(this, path);
return this;
},
_fill: function(vectors, options) {
var self = this;
self._writeHPGL('PC1,' + options.color[0] + ',' + options.color[1] + ',' + options.color[2] + ';SP1;');
self._writeHPGL('PW' + options.lineWidth + ';');
self._writeHPGL('TR0;');
// polygon mode status
self._writeHPGL('PM0;');
vectors.forEach(function(vector) {
// clone vector to avoid changes
vector = vector.slice();
var method = vector.shift();
switch(method) {
case 'moveTo':
self._writeHPGL('PM1;PU' + vector[0][0] + ',' + vector[0][1] + ';');
break;
case 'lineTo':
self._writeHPGL('PD' + vector[0][0] + ',' + vector[0][1] + ';');
break;
case 'bezierCurveTo':
self._writeHPGL('PD;BZ' + vector[0][0] + ',' + vector[0][1] + ',' + vector[1][0] + ',' + vector[1][1] + ',' + vector[2][0] + ',' + vector[2][1] + ';');
break;
}
});
this._writeHPGL('PM2;FP;SP1;');
return this;
},
fill: function(color, rule) {
if (/(even-?odd)|(non-?zero)/.test(color)) {
rule = color;
color = null;
}
if (color) {
color = this._normalizeColor(color);
this._fillColor[0] = color;
}
var vectors = this._vectors;
this._vectors = [];
var options = {
color: color || this._fillColor[0],
opacity: this._fillColor[1],
lineWidth: this._lineWidth
};
this.page.content.push(['fill', vectors, options]);
return this;
},
_stroke: function(vectors, options) {
var self = this;
self._writeHPGL('PC1,' + options.color[0] + ',' + options.color[1] + ',' + options.color[2] + ';SP1;');
self._writeHPGL('PW' + options.lineWidth + ';');
vectors.forEach(function(vector) {
// clone vector to avoid changes
vector = vector.slice();
var method = vector.shift();
switch(method) {
case 'moveTo':
self._writeHPGL('PU' + vector[0][0] + ',' + vector[0][1] + ';');
break;
case 'lineTo':
self._writeHPGL('PD' + vector[0][0] + ',' + vector[0][1] + ';');
break;
case 'bezierCurveTo':
self._writeHPGL('PD;BZ' + vector[0][0] + ',' + vector[0][1] + ',' + vector[1][0] + ',' + vector[1][1] + ',' + vector[2][0] + ',' + vector[2][1] + ';');
break;
}
});
},
stroke: function(color) {
if (color) {
color = this._normalizeColor(color);
}
var vectors = this._vectors;
var options = {
color: color || this._strokeColor[0],
opacity: this._strokeColor[1],
lineWidth: this._lineWidth
};
this._vectors = [];
this.page.content.push(['stroke', vectors, options]);
return this;
},
fillAndStroke: function(fillColor, strokeColor, rule) {
strokeColor = strokeColor || fillColor;
var isFillRule = /(even-?odd)|(non-?zero)/;
if (isFillRule.test(fillColor)) {
rule = fillColor;
fillColor = null;
}
if (isFillRule.test(strokeColor)) {
rule = strokeColor;
strokeColor = fillColor;
}
// save vectors
var vectors = this._vectors.slice();
this.fill(fillColor, rule);
// restore vectors
this._vectors = vectors;
this.stroke(strokeColor);
return this;
},
clip: function(rule) {
// pcl only support rectangular input window for soft clip
return this;
},
transform: function(m11, m12, m21, m22, dx, dy) {
// apply transformation to matrix
var [m0, m1, m2, m3, m4, m5] = this._ctm;
this._ctm[0] = m0 * m11 + m2 * m12;
this._ctm[1] = m1 * m11 + m3 * m12;
this._ctm[2] = m0 * m21 + m2 * m22;
this._ctm[3] = m1 * m21 + m3 * m22;
this._ctm[4] = m0 * dx + m2 * dy + m4;
this._ctm[5] = m1 * dx + m3 * dy + m5;
return this;
},
translate: function(x, y) {
return this.transform(1, 0, 0, 1, x, y);
},
rotate: function(angle, options) {
var y;
options = options || {};
var rad = (angle * Math.PI) / 180;
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var x = (y = 0);
if (options.origin != null) {
[x, y] = options.origin;
var x1 = x * cos - y * sin;
var y1 = x * sin + y * cos;
x -= x1;
y -= y1;
}
return this.transform(cos, sin, -sin, cos, x, y);
},
scale: function(xFactor, yFactor, options) {
yFactor = yFactor || xFactor;
options = options || {};
if (typeof yFactor === 'object') {
options = yFactor;
yFactor = xFactor;
}
var x = 0;
var y = 0;
if (options.origin != null) {
x = options.origin[0];
y = options.origin[1];
x -= xFactor * x;
y -= yFactor * y;
}
return this.transform(xFactor, 0, 0, yFactor, x, y);
}
};